From d6a01403023af3f5229ea1aaf6cd91e80522dc48 Mon Sep 17 00:00:00 2001 From: Chris McCord Date: Thu, 31 Oct 2024 12:59:26 -0400 Subject: [PATCH] Apply allow_upload's chunk_timeout to push timeouts. Closes #3348 This is useful when you want to upload very large chunk sizes that risk timing out slower connections. --- assets/js/phoenix_live_view/entry_uploader.js | 9 ++++++--- assets/js/phoenix_live_view/utils.js | 2 +- lib/phoenix_live_view/upload.ex | 3 ++- priv/static/phoenix_live_view.cjs.js | 11 +++++++---- priv/static/phoenix_live_view.cjs.js.map | 4 ++-- priv/static/phoenix_live_view.esm.js | 11 +++++++---- priv/static/phoenix_live_view.esm.js.map | 4 ++-- priv/static/phoenix_live_view.js | 11 +++++++---- priv/static/phoenix_live_view.min.js | 8 ++++---- 9 files changed, 38 insertions(+), 25 deletions(-) diff --git a/assets/js/phoenix_live_view/entry_uploader.js b/assets/js/phoenix_live_view/entry_uploader.js index 24308e445e..ccabd24fb1 100644 --- a/assets/js/phoenix_live_view/entry_uploader.js +++ b/assets/js/phoenix_live_view/entry_uploader.js @@ -3,11 +3,14 @@ import { } from "./utils" export default class EntryUploader { - constructor(entry, chunkSize, liveSocket){ + constructor(entry, config, liveSocket){ + console.log(config) + let {chunk_size, chunk_timeout} = config this.liveSocket = liveSocket this.entry = entry this.offset = 0 - this.chunkSize = chunkSize + this.chunkSize = chunk_size + this.chunkTimeout = chunk_timeout this.chunkTimer = null this.errored = false this.uploadChannel = liveSocket.channel(`lvu:${entry.ref}`, {token: entry.metadata()}) @@ -46,7 +49,7 @@ export default class EntryUploader { pushChunk(chunk){ if(!this.uploadChannel.isJoined()){ return } - this.uploadChannel.push("chunk", chunk) + this.uploadChannel.push("chunk", chunk, this.chunkTimeout) .receive("ok", () => { this.entry.progress((this.offset / this.entry.file.size) * 100) if(!this.isDone()){ diff --git a/assets/js/phoenix_live_view/utils.js b/assets/js/phoenix_live_view/utils.js index 97e5faec14..1eda3960f9 100644 --- a/assets/js/phoenix_live_view/utils.js +++ b/assets/js/phoenix_live_view/utils.js @@ -57,7 +57,7 @@ export let maybe = (el, callback) => el && callback(el) export let channelUploader = function (entries, onError, resp, liveSocket){ entries.forEach(entry => { - let entryUploader = new EntryUploader(entry, resp.config.chunk_size, liveSocket) + let entryUploader = new EntryUploader(entry, resp.config, liveSocket) entryUploader.upload() }) } diff --git a/lib/phoenix_live_view/upload.ex b/lib/phoenix_live_view/upload.ex index 91e9bf5537..3fda5fea7c 100644 --- a/lib/phoenix_live_view/upload.ex +++ b/lib/phoenix_live_view/upload.ex @@ -350,7 +350,8 @@ defmodule Phoenix.LiveView.Upload do client_meta = %{ max_file_size: conf.max_file_size, max_entries: conf.max_entries, - chunk_size: conf.chunk_size + chunk_size: conf.chunk_size, + chunk_timeout: conf.chunk_timeout } {new_socket, new_conf, new_entries} = mark_preflighted(socket, conf, refs) diff --git a/priv/static/phoenix_live_view.cjs.js b/priv/static/phoenix_live_view.cjs.js index 490f86b91b..977f6b7107 100644 --- a/priv/static/phoenix_live_view.cjs.js +++ b/priv/static/phoenix_live_view.cjs.js @@ -121,11 +121,14 @@ var STREAM = "stream"; // js/phoenix_live_view/entry_uploader.js var EntryUploader = class { - constructor(entry, chunkSize, liveSocket) { + constructor(entry, config, liveSocket) { + console.log(config); + let { chunk_size, chunk_timeout } = config; this.liveSocket = liveSocket; this.entry = entry; this.offset = 0; - this.chunkSize = chunkSize; + this.chunkSize = chunk_size; + this.chunkTimeout = chunk_timeout; this.chunkTimer = null; this.errored = false; this.uploadChannel = liveSocket.channel(`lvu:${entry.ref}`, { token: entry.metadata() }); @@ -163,7 +166,7 @@ var EntryUploader = class { if (!this.uploadChannel.isJoined()) { return; } - this.uploadChannel.push("chunk", chunk).receive("ok", () => { + this.uploadChannel.push("chunk", chunk, this.chunkTimeout).receive("ok", () => { this.entry.progress(this.offset / this.entry.file.size * 100); if (!this.isDone()) { this.chunkTimer = setTimeout(() => this.readNextChunk(), this.liveSocket.getLatencySim() || 0); @@ -222,7 +225,7 @@ var isEmpty = (obj) => { var maybe = (el, callback) => el && callback(el); var channelUploader = function(entries, onError, resp, liveSocket) { entries.forEach((entry) => { - let entryUploader = new EntryUploader(entry, resp.config.chunk_size, liveSocket); + let entryUploader = new EntryUploader(entry, resp.config, liveSocket); entryUploader.upload(); }); }; diff --git a/priv/static/phoenix_live_view.cjs.js.map b/priv/static/phoenix_live_view.cjs.js.map index c9d6b662f6..4d3d81dfc6 100644 --- a/priv/static/phoenix_live_view.cjs.js.map +++ b/priv/static/phoenix_live_view.cjs.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../assets/js/phoenix_live_view/index.js", "../../assets/js/phoenix_live_view/constants.js", "../../assets/js/phoenix_live_view/entry_uploader.js", "../../assets/js/phoenix_live_view/utils.js", "../../assets/js/phoenix_live_view/browser.js", "../../assets/js/phoenix_live_view/aria.js", "../../assets/js/phoenix_live_view/js.js", "../../assets/js/phoenix_live_view/dom.js", "../../assets/js/phoenix_live_view/upload_entry.js", "../../assets/js/phoenix_live_view/live_uploader.js", "../../assets/js/phoenix_live_view/hooks.js", "../../assets/js/phoenix_live_view/element_ref.js", "../../assets/js/phoenix_live_view/dom_post_morph_restorer.js", "../../assets/node_modules/morphdom/dist/morphdom-esm.js", "../../assets/js/phoenix_live_view/dom_patch.js", "../../assets/js/phoenix_live_view/rendered.js", "../../assets/js/phoenix_live_view/view_hook.js", "../../assets/js/phoenix_live_view/view.js", "../../assets/js/phoenix_live_view/live_socket.js"], - "sourcesContent": ["/*\n================================================================================\nPhoenix LiveView JavaScript Client\n================================================================================\n\nSee the hexdocs at `https://hexdocs.pm/phoenix_live_view` for documentation.\n\n*/\n\nimport LiveSocket, {isUsedInput} from \"./live_socket\"\nimport DOM from \"./dom\"\nimport ViewHook from \"./view_hook\"\nimport View from \"./view\"\n\n/** Creates a ViewHook instance for the given element and callbacks.\n *\n * @param {HTMLElement} el - The element to associate with the hook.\n * @param {Object} [callbacks] - The list of hook callbacks, such as mounted,\n * updated, destroyed, etc.\n *\n * @example\n *\n * class MyComponent extends HTMLElement {\n * connectedCallback(){\n * let onLiveViewMounted = () => this.hook.pushEvent(...))\n * this.hook = createHook(this, {mounted: onLiveViewMounted})\n * }\n * }\n *\n * *Note*: `createHook` must be called from the `connectedCallback` lifecycle\n * which is triggered after the element has been added to the DOM. If you try\n * to call `createHook` from the constructor, an error will be logged.\n *\n * @returns {ViewHook} Returns the ViewHook instance for the custom element.\n */\nlet createHook = (el, callbacks = {}) => {\n let existingHook = DOM.getCustomElHook(el)\n if(existingHook){ return existingHook }\n\n let hook = new ViewHook(View.closestView(el), el, callbacks)\n DOM.putCustomElHook(el, hook)\n return hook\n}\n\nexport {\n LiveSocket,\n isUsedInput,\n createHook\n}", "export const CONSECUTIVE_RELOADS = \"consecutive-reloads\"\nexport const MAX_RELOADS = 10\nexport const RELOAD_JITTER_MIN = 5000\nexport const RELOAD_JITTER_MAX = 10000\nexport const FAILSAFE_JITTER = 30000\nexport const PHX_EVENT_CLASSES = [\n \"phx-click-loading\", \"phx-change-loading\", \"phx-submit-loading\",\n \"phx-keydown-loading\", \"phx-keyup-loading\", \"phx-blur-loading\", \"phx-focus-loading\",\n \"phx-hook-loading\"\n]\nexport const PHX_COMPONENT = \"data-phx-component\"\nexport const PHX_LIVE_LINK = \"data-phx-link\"\nexport const PHX_TRACK_STATIC = \"track-static\"\nexport const PHX_LINK_STATE = \"data-phx-link-state\"\nexport const PHX_REF_LOADING = \"data-phx-ref-loading\"\nexport const PHX_REF_SRC = \"data-phx-ref-src\"\nexport const PHX_REF_LOCK = \"data-phx-ref-lock\"\nexport const PHX_TRACK_UPLOADS = \"track-uploads\"\nexport const PHX_UPLOAD_REF = \"data-phx-upload-ref\"\nexport const PHX_PREFLIGHTED_REFS = \"data-phx-preflighted-refs\"\nexport const PHX_DONE_REFS = \"data-phx-done-refs\"\nexport const PHX_DROP_TARGET = \"drop-target\"\nexport const PHX_ACTIVE_ENTRY_REFS = \"data-phx-active-refs\"\nexport const PHX_LIVE_FILE_UPDATED = \"phx:live-file:updated\"\nexport const PHX_SKIP = \"data-phx-skip\"\nexport const PHX_MAGIC_ID = \"data-phx-id\"\nexport const PHX_PRUNE = \"data-phx-prune\"\nexport const PHX_CONNECTED_CLASS = \"phx-connected\"\nexport const PHX_LOADING_CLASS = \"phx-loading\"\nexport const PHX_ERROR_CLASS = \"phx-error\"\nexport const PHX_CLIENT_ERROR_CLASS = \"phx-client-error\"\nexport const PHX_SERVER_ERROR_CLASS = \"phx-server-error\"\nexport const PHX_PARENT_ID = \"data-phx-parent-id\"\nexport const PHX_MAIN = \"data-phx-main\"\nexport const PHX_ROOT_ID = \"data-phx-root-id\"\nexport const PHX_VIEWPORT_TOP = \"viewport-top\"\nexport const PHX_VIEWPORT_BOTTOM = \"viewport-bottom\"\nexport const PHX_TRIGGER_ACTION = \"trigger-action\"\nexport const PHX_HAS_FOCUSED = \"phx-has-focused\"\nexport const FOCUSABLE_INPUTS = [\"text\", \"textarea\", \"number\", \"email\", \"password\", \"search\", \"tel\", \"url\", \"date\", \"time\", \"datetime-local\", \"color\", \"range\"]\nexport const CHECKABLE_INPUTS = [\"checkbox\", \"radio\"]\nexport const PHX_HAS_SUBMITTED = \"phx-has-submitted\"\nexport const PHX_SESSION = \"data-phx-session\"\nexport const PHX_VIEW_SELECTOR = `[${PHX_SESSION}]`\nexport const PHX_STICKY = \"data-phx-sticky\"\nexport const PHX_STATIC = \"data-phx-static\"\nexport const PHX_READONLY = \"data-phx-readonly\"\nexport const PHX_DISABLED = \"data-phx-disabled\"\nexport const PHX_DISABLE_WITH = \"disable-with\"\nexport const PHX_DISABLE_WITH_RESTORE = \"data-phx-disable-with-restore\"\nexport const PHX_HOOK = \"hook\"\nexport const PHX_DEBOUNCE = \"debounce\"\nexport const PHX_THROTTLE = \"throttle\"\nexport const PHX_UPDATE = \"update\"\nexport const PHX_STREAM = \"stream\"\nexport const PHX_STREAM_REF = \"data-phx-stream\"\nexport const PHX_KEY = \"key\"\nexport const PHX_PRIVATE = \"phxPrivate\"\nexport const PHX_AUTO_RECOVER = \"auto-recover\"\nexport const PHX_LV_DEBUG = \"phx:live-socket:debug\"\nexport const PHX_LV_PROFILE = \"phx:live-socket:profiling\"\nexport const PHX_LV_LATENCY_SIM = \"phx:live-socket:latency-sim\"\nexport const PHX_PROGRESS = \"progress\"\nexport const PHX_MOUNTED = \"mounted\"\nexport const PHX_RELOAD_STATUS = \"__phoenix_reload_status__\"\nexport const LOADER_TIMEOUT = 1\nexport const MAX_CHILD_JOIN_ATTEMPTS = 3\nexport const BEFORE_UNLOAD_LOADER_TIMEOUT = 200\nexport const BINDING_PREFIX = \"phx-\"\nexport const PUSH_TIMEOUT = 30000\nexport const LINK_HEADER = \"x-requested-with\"\nexport const RESPONSE_URL_HEADER = \"x-response-url\"\nexport const DEBOUNCE_TRIGGER = \"debounce-trigger\"\nexport const THROTTLED = \"throttled\"\nexport const DEBOUNCE_PREV_KEY = \"debounce-prev-key\"\nexport const DEFAULTS = {\n debounce: 300,\n throttle: 300\n}\nexport const PHX_PENDING_ATTRS = [PHX_REF_LOADING, PHX_REF_SRC, PHX_REF_LOCK]\n// Rendered\nexport const DYNAMICS = \"d\"\nexport const STATIC = \"s\"\nexport const ROOT = \"r\"\nexport const COMPONENTS = \"c\"\nexport const EVENTS = \"e\"\nexport const REPLY = \"r\"\nexport const TITLE = \"t\"\nexport const TEMPLATES = \"p\"\nexport const STREAM = \"stream\"\n", "import {\n logError\n} from \"./utils\"\n\nexport default class EntryUploader {\n constructor(entry, chunkSize, liveSocket){\n this.liveSocket = liveSocket\n this.entry = entry\n this.offset = 0\n this.chunkSize = chunkSize\n this.chunkTimer = null\n this.errored = false\n this.uploadChannel = liveSocket.channel(`lvu:${entry.ref}`, {token: entry.metadata()})\n }\n\n error(reason){\n if(this.errored){ return }\n this.uploadChannel.leave()\n this.errored = true\n clearTimeout(this.chunkTimer)\n this.entry.error(reason)\n }\n\n upload(){\n this.uploadChannel.onError(reason => this.error(reason))\n this.uploadChannel.join()\n .receive(\"ok\", _data => this.readNextChunk())\n .receive(\"error\", reason => this.error(reason))\n }\n\n isDone(){ return this.offset >= this.entry.file.size }\n\n readNextChunk(){\n let reader = new window.FileReader()\n let blob = this.entry.file.slice(this.offset, this.chunkSize + this.offset)\n reader.onload = (e) => {\n if(e.target.error === null){\n this.offset += e.target.result.byteLength\n this.pushChunk(e.target.result)\n } else {\n return logError(\"Read error: \" + e.target.error)\n }\n }\n reader.readAsArrayBuffer(blob)\n }\n\n pushChunk(chunk){\n if(!this.uploadChannel.isJoined()){ return }\n this.uploadChannel.push(\"chunk\", chunk)\n .receive(\"ok\", () => {\n this.entry.progress((this.offset / this.entry.file.size) * 100)\n if(!this.isDone()){\n this.chunkTimer = setTimeout(() => this.readNextChunk(), this.liveSocket.getLatencySim() || 0)\n }\n })\n .receive(\"error\", ({reason}) => this.error(reason))\n }\n}\n", "import {\n PHX_VIEW_SELECTOR\n} from \"./constants\"\n\nimport EntryUploader from \"./entry_uploader\"\n\nexport let logError = (msg, obj) => console.error && console.error(msg, obj)\n\nexport let isCid = (cid) => {\n let type = typeof(cid)\n return type === \"number\" || (type === \"string\" && /^(0|[1-9]\\d*)$/.test(cid))\n}\n\nexport function detectDuplicateIds(){\n let ids = new Set()\n let elems = document.querySelectorAll(\"*[id]\")\n for(let i = 0, len = elems.length; i < len; i++){\n if(ids.has(elems[i].id)){\n console.error(`Multiple IDs detected: ${elems[i].id}. Ensure unique element ids.`)\n } else {\n ids.add(elems[i].id)\n }\n }\n}\n\nexport let debug = (view, kind, msg, obj) => {\n if(view.liveSocket.isDebugEnabled()){\n console.log(`${view.id} ${kind}: ${msg} - `, obj)\n }\n}\n\n// wraps value in closure or returns closure\nexport let closure = (val) => typeof val === \"function\" ? val : function (){ return val }\n\nexport let clone = (obj) => { return JSON.parse(JSON.stringify(obj)) }\n\nexport let closestPhxBinding = (el, binding, borderEl) => {\n do {\n if(el.matches(`[${binding}]`) && !el.disabled){ return el }\n el = el.parentElement || el.parentNode\n } while(el !== null && el.nodeType === 1 && !((borderEl && borderEl.isSameNode(el)) || el.matches(PHX_VIEW_SELECTOR)))\n return null\n}\n\nexport let isObject = (obj) => {\n return obj !== null && typeof obj === \"object\" && !(obj instanceof Array)\n}\n\nexport let isEqualObj = (obj1, obj2) => JSON.stringify(obj1) === JSON.stringify(obj2)\n\nexport let isEmpty = (obj) => {\n for(let x in obj){ return false }\n return true\n}\n\nexport let maybe = (el, callback) => el && callback(el)\n\nexport let channelUploader = function (entries, onError, resp, liveSocket){\n entries.forEach(entry => {\n let entryUploader = new EntryUploader(entry, resp.config.chunk_size, liveSocket)\n entryUploader.upload()\n })\n}\n", "let Browser = {\n canPushState(){ return (typeof (history.pushState) !== \"undefined\") },\n\n dropLocal(localStorage, namespace, subkey){\n return localStorage.removeItem(this.localKey(namespace, subkey))\n },\n\n updateLocal(localStorage, namespace, subkey, initial, func){\n let current = this.getLocal(localStorage, namespace, subkey)\n let key = this.localKey(namespace, subkey)\n let newVal = current === null ? initial : func(current)\n localStorage.setItem(key, JSON.stringify(newVal))\n return newVal\n },\n\n getLocal(localStorage, namespace, subkey){\n return JSON.parse(localStorage.getItem(this.localKey(namespace, subkey)))\n },\n\n updateCurrentState(callback){\n if(!this.canPushState()){ return }\n history.replaceState(callback(history.state || {}), \"\", window.location.href)\n },\n\n pushState(kind, meta, to){\n if(this.canPushState()){\n if(to !== window.location.href){\n if(meta.type == \"redirect\" && meta.scroll){\n // If we're redirecting store the current scrollY for the current history state.\n let currentState = history.state || {}\n currentState.scroll = meta.scroll\n history.replaceState(currentState, \"\", window.location.href)\n }\n\n delete meta.scroll // Only store the scroll in the redirect case.\n history[kind + \"State\"](meta, \"\", to || null) // IE will coerce undefined to string\n\n // when using navigate, we'd call pushState immediately before patching the DOM,\n // jumping back to the top of the page, effectively ignoring the scrollIntoView;\n // therefore we wait for the next frame (after the DOM patch) and only then try\n // to scroll to the hashEl\n window.requestAnimationFrame(() => {\n let hashEl = this.getHashTargetEl(window.location.hash)\n \n if(hashEl){\n hashEl.scrollIntoView()\n } else if(meta.type === \"redirect\"){\n window.scroll(0, 0)\n }\n })\n }\n } else {\n this.redirect(to)\n }\n },\n\n setCookie(name, value, maxAgeSeconds){\n let expires = typeof(maxAgeSeconds) === \"number\" ? ` max-age=${maxAgeSeconds};` : \"\"\n document.cookie = `${name}=${value};${expires} path=/`\n },\n\n getCookie(name){\n return document.cookie.replace(new RegExp(`(?:(?:^|.*;\\s*)${name}\\s*\\=\\s*([^;]*).*$)|^.*$`), \"$1\")\n },\n\n deleteCookie(name){\n document.cookie = `${name}=; max-age=-1; path=/`\n },\n\n redirect(toURL, flash){\n if(flash){ this.setCookie(\"__phoenix_flash__\", flash, 60) }\n window.location = toURL\n },\n\n localKey(namespace, subkey){ return `${namespace}-${subkey}` },\n\n getHashTargetEl(maybeHash){\n let hash = maybeHash.toString().substring(1)\n if(hash === \"\"){ return }\n return document.getElementById(hash) || document.querySelector(`a[name=\"${hash}\"]`)\n }\n}\n\nexport default Browser\n", "let ARIA = {\n anyOf(instance, classes){ return classes.find(name => instance instanceof name) },\n\n isFocusable(el, interactiveOnly){\n return(\n (el instanceof HTMLAnchorElement && el.rel !== \"ignore\") ||\n (el instanceof HTMLAreaElement && el.href !== undefined) ||\n (!el.disabled && (this.anyOf(el, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement, HTMLButtonElement]))) ||\n (el instanceof HTMLIFrameElement) ||\n (el.tabIndex > 0 || (!interactiveOnly && el.getAttribute(\"tabindex\") !== null && el.getAttribute(\"aria-hidden\") !== \"true\"))\n )\n },\n\n attemptFocus(el, interactiveOnly){\n if(this.isFocusable(el, interactiveOnly)){ try{ el.focus() } catch(e){} }\n return !!document.activeElement && document.activeElement.isSameNode(el)\n },\n\n focusFirstInteractive(el){\n let child = el.firstElementChild\n while(child){\n if(this.attemptFocus(child, true) || this.focusFirstInteractive(child, true)){\n return true\n }\n child = child.nextElementSibling\n }\n },\n\n focusFirst(el){\n let child = el.firstElementChild\n while(child){\n if(this.attemptFocus(child) || this.focusFirst(child)){\n return true\n }\n child = child.nextElementSibling\n }\n },\n\n focusLast(el){\n let child = el.lastElementChild\n while(child){\n if(this.attemptFocus(child) || this.focusLast(child)){\n return true\n }\n child = child.previousElementSibling\n }\n }\n}\nexport default ARIA\n", "import DOM from \"./dom\"\nimport ARIA from \"./aria\"\n\nlet focusStack = []\nlet default_transition_time = 200\n\nlet JS = {\n // private\n exec(e, eventType, phxEvent, view, sourceEl, defaults){\n let [defaultKind, defaultArgs] = defaults || [null, {callback: defaults && defaults.callback}]\n let commands = phxEvent.charAt(0) === \"[\" ?\n JSON.parse(phxEvent) : [[defaultKind, defaultArgs]]\n\n commands.forEach(([kind, args]) => {\n if(kind === defaultKind && defaultArgs.data){\n args.data = Object.assign(args.data || {}, defaultArgs.data)\n args.callback = args.callback || defaultArgs.callback\n }\n this.filterToEls(view.liveSocket, sourceEl, args).forEach(el => {\n this[`exec_${kind}`](e, eventType, phxEvent, view, sourceEl, el, args)\n })\n })\n },\n\n isVisible(el){\n return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length > 0)\n },\n\n // returns true if any part of the element is inside the viewport\n isInViewport(el){\n const rect = el.getBoundingClientRect()\n const windowHeight = window.innerHeight || document.documentElement.clientHeight\n const windowWidth = window.innerWidth || document.documentElement.clientWidth\n\n return (\n rect.right > 0 &&\n rect.bottom > 0 &&\n rect.left < windowWidth &&\n rect.top < windowHeight\n )\n },\n\n // private\n\n // commands\n\n exec_exec(e, eventType, phxEvent, view, sourceEl, el, {attr, to}){\n let nodes = to ? DOM.all(document, to) : [sourceEl]\n nodes.forEach(node => {\n let encodedJS = node.getAttribute(attr)\n if(!encodedJS){ throw new Error(`expected ${attr} to contain JS command on \"${to}\"`) }\n view.liveSocket.execJS(node, encodedJS, eventType)\n })\n },\n\n exec_dispatch(e, eventType, phxEvent, view, sourceEl, el, {to, event, detail, bubbles}){\n detail = detail || {}\n detail.dispatcher = sourceEl\n DOM.dispatchEvent(el, event, {detail, bubbles})\n },\n\n exec_push(e, eventType, phxEvent, view, sourceEl, el, args){\n let {event, data, target, page_loading, loading, value, dispatcher, callback} = args\n let pushOpts = {loading, value, target, page_loading: !!page_loading}\n let targetSrc = eventType === \"change\" && dispatcher ? dispatcher : sourceEl\n let phxTarget = target || targetSrc.getAttribute(view.binding(\"target\")) || targetSrc\n view.withinTargets(phxTarget, (targetView, targetCtx) => {\n if(!targetView.isConnected()){ return }\n if(eventType === \"change\"){\n let {newCid, _target} = args\n _target = _target || (DOM.isFormInput(sourceEl) ? sourceEl.name : undefined)\n if(_target){ pushOpts._target = _target }\n targetView.pushInput(sourceEl, targetCtx, newCid, event || phxEvent, pushOpts, callback)\n } else if(eventType === \"submit\"){\n let {submitter} = args\n targetView.submitForm(sourceEl, targetCtx, event || phxEvent, submitter, pushOpts, callback)\n } else {\n targetView.pushEvent(eventType, sourceEl, targetCtx, event || phxEvent, data, pushOpts, callback)\n }\n })\n },\n\n exec_navigate(e, eventType, phxEvent, view, sourceEl, el, {href, replace}){\n view.liveSocket.historyRedirect(e, href, replace ? \"replace\" : \"push\", null, sourceEl)\n },\n\n exec_patch(e, eventType, phxEvent, view, sourceEl, el, {href, replace}){\n view.liveSocket.pushHistoryPatch(e, href, replace ? \"replace\" : \"push\", sourceEl)\n },\n\n exec_focus(e, eventType, phxEvent, view, sourceEl, el){\n window.requestAnimationFrame(() => ARIA.attemptFocus(el))\n },\n\n exec_focus_first(e, eventType, phxEvent, view, sourceEl, el){\n window.requestAnimationFrame(() => ARIA.focusFirstInteractive(el) || ARIA.focusFirst(el))\n },\n\n exec_push_focus(e, eventType, phxEvent, view, sourceEl, el){\n window.requestAnimationFrame(() => focusStack.push(el || sourceEl))\n },\n\n exec_pop_focus(e, eventType, phxEvent, view, sourceEl, el){\n window.requestAnimationFrame(() => {\n const el = focusStack.pop()\n if(el){ el.focus() }\n })\n },\n\n exec_add_class(e, eventType, phxEvent, view, sourceEl, el, {names, transition, time, blocking}){\n this.addOrRemoveClasses(el, names, [], transition, time, view, blocking)\n },\n\n exec_remove_class(e, eventType, phxEvent, view, sourceEl, el, {names, transition, time, blocking}){\n this.addOrRemoveClasses(el, [], names, transition, time, view, blocking)\n },\n\n exec_toggle_class(e, eventType, phxEvent, view, sourceEl, el, {to, names, transition, time, blocking}){\n this.toggleClasses(el, names, transition, time, view, blocking)\n },\n\n exec_toggle_attr(e, eventType, phxEvent, view, sourceEl, el, {attr: [attr, val1, val2]}){\n this.toggleAttr(el, attr, val1, val2)\n },\n\n exec_transition(e, eventType, phxEvent, view, sourceEl, el, {time, transition, blocking}){\n this.addOrRemoveClasses(el, [], [], transition, time, view, blocking)\n },\n\n exec_toggle(e, eventType, phxEvent, view, sourceEl, el, {display, ins, outs, time, blocking}){\n this.toggle(eventType, view, el, display, ins, outs, time, blocking)\n },\n\n exec_show(e, eventType, phxEvent, view, sourceEl, el, {display, transition, time, blocking}){\n this.show(eventType, view, el, display, transition, time, blocking)\n },\n\n exec_hide(e, eventType, phxEvent, view, sourceEl, el, {display, transition, time, blocking}){\n this.hide(eventType, view, el, display, transition, time, blocking)\n },\n\n exec_set_attr(e, eventType, phxEvent, view, sourceEl, el, {attr: [attr, val]}){\n this.setOrRemoveAttrs(el, [[attr, val]], [])\n },\n\n exec_remove_attr(e, eventType, phxEvent, view, sourceEl, el, {attr}){\n this.setOrRemoveAttrs(el, [], [attr])\n },\n\n // utils for commands\n\n show(eventType, view, el, display, transition, time, blocking){\n if(!this.isVisible(el)){\n this.toggle(eventType, view, el, display, transition, null, time, blocking)\n }\n },\n\n hide(eventType, view, el, display, transition, time, blocking){\n if(this.isVisible(el)){\n this.toggle(eventType, view, el, display, null, transition, time, blocking)\n }\n },\n\n toggle(eventType, view, el, display, ins, outs, time, blocking){\n time = time || default_transition_time\n let [inClasses, inStartClasses, inEndClasses] = ins || [[], [], []]\n let [outClasses, outStartClasses, outEndClasses] = outs || [[], [], []]\n if(inClasses.length > 0 || outClasses.length > 0){\n if(this.isVisible(el)){\n let onStart = () => {\n this.addOrRemoveClasses(el, outStartClasses, inClasses.concat(inStartClasses).concat(inEndClasses))\n window.requestAnimationFrame(() => {\n this.addOrRemoveClasses(el, outClasses, [])\n window.requestAnimationFrame(() => this.addOrRemoveClasses(el, outEndClasses, outStartClasses))\n })\n }\n let onEnd = () => {\n this.addOrRemoveClasses(el, [], outClasses.concat(outEndClasses))\n DOM.putSticky(el, \"toggle\", currentEl => currentEl.style.display = \"none\")\n el.dispatchEvent(new Event(\"phx:hide-end\"))\n }\n el.dispatchEvent(new Event(\"phx:hide-start\"))\n if(blocking === false){\n onStart()\n setTimeout(onEnd, time)\n } else {\n view.transition(time, onStart, onEnd)\n }\n } else {\n if(eventType === \"remove\"){ return }\n let onStart = () => {\n this.addOrRemoveClasses(el, inStartClasses, outClasses.concat(outStartClasses).concat(outEndClasses))\n let stickyDisplay = display || this.defaultDisplay(el)\n DOM.putSticky(el, \"toggle\", currentEl => currentEl.style.display = stickyDisplay)\n window.requestAnimationFrame(() => {\n this.addOrRemoveClasses(el, inClasses, [])\n window.requestAnimationFrame(() => this.addOrRemoveClasses(el, inEndClasses, inStartClasses))\n })\n }\n let onEnd = () => {\n this.addOrRemoveClasses(el, [], inClasses.concat(inEndClasses))\n el.dispatchEvent(new Event(\"phx:show-end\"))\n }\n el.dispatchEvent(new Event(\"phx:show-start\"))\n if(blocking === false){\n onStart()\n setTimeout(onEnd, time)\n } else {\n view.transition(time, onStart, onEnd)\n }\n }\n } else {\n if(this.isVisible(el)){\n window.requestAnimationFrame(() => {\n el.dispatchEvent(new Event(\"phx:hide-start\"))\n DOM.putSticky(el, \"toggle\", currentEl => currentEl.style.display = \"none\")\n el.dispatchEvent(new Event(\"phx:hide-end\"))\n })\n } else {\n window.requestAnimationFrame(() => {\n el.dispatchEvent(new Event(\"phx:show-start\"))\n let stickyDisplay = display || this.defaultDisplay(el)\n DOM.putSticky(el, \"toggle\", currentEl => currentEl.style.display = stickyDisplay)\n el.dispatchEvent(new Event(\"phx:show-end\"))\n })\n }\n }\n },\n\n toggleClasses(el, classes, transition, time, view, blocking){\n window.requestAnimationFrame(() => {\n let [prevAdds, prevRemoves] = DOM.getSticky(el, \"classes\", [[], []])\n let newAdds = classes.filter(name => prevAdds.indexOf(name) < 0 && !el.classList.contains(name))\n let newRemoves = classes.filter(name => prevRemoves.indexOf(name) < 0 && el.classList.contains(name))\n this.addOrRemoveClasses(el, newAdds, newRemoves, transition, time, view, blocking)\n })\n },\n\n toggleAttr(el, attr, val1, val2){\n if(el.hasAttribute(attr)){\n if(val2 !== undefined){\n // toggle between val1 and val2\n if(el.getAttribute(attr) === val1){\n this.setOrRemoveAttrs(el, [[attr, val2]], [])\n } else {\n this.setOrRemoveAttrs(el, [[attr, val1]], [])\n }\n } else {\n // remove attr\n this.setOrRemoveAttrs(el, [], [attr])\n }\n } else {\n this.setOrRemoveAttrs(el, [[attr, val1]], [])\n }\n },\n\n addOrRemoveClasses(el, adds, removes, transition, time, view, blocking){\n time = time || default_transition_time\n let [transitionRun, transitionStart, transitionEnd] = transition || [[], [], []]\n if(transitionRun.length > 0){\n let onStart = () => {\n this.addOrRemoveClasses(el, transitionStart, [].concat(transitionRun).concat(transitionEnd))\n window.requestAnimationFrame(() => {\n this.addOrRemoveClasses(el, transitionRun, [])\n window.requestAnimationFrame(() => this.addOrRemoveClasses(el, transitionEnd, transitionStart))\n })\n }\n let onDone = () => this.addOrRemoveClasses(el, adds.concat(transitionEnd), removes.concat(transitionRun).concat(transitionStart))\n if(blocking === false){\n onStart()\n setTimeout(onDone, time)\n } else {\n view.transition(time, onStart, onDone)\n }\n return\n }\n\n window.requestAnimationFrame(() => {\n let [prevAdds, prevRemoves] = DOM.getSticky(el, \"classes\", [[], []])\n let keepAdds = adds.filter(name => prevAdds.indexOf(name) < 0 && !el.classList.contains(name))\n let keepRemoves = removes.filter(name => prevRemoves.indexOf(name) < 0 && el.classList.contains(name))\n let newAdds = prevAdds.filter(name => removes.indexOf(name) < 0).concat(keepAdds)\n let newRemoves = prevRemoves.filter(name => adds.indexOf(name) < 0).concat(keepRemoves)\n\n DOM.putSticky(el, \"classes\", currentEl => {\n currentEl.classList.remove(...newRemoves)\n currentEl.classList.add(...newAdds)\n return [newAdds, newRemoves]\n })\n })\n },\n\n setOrRemoveAttrs(el, sets, removes){\n let [prevSets, prevRemoves] = DOM.getSticky(el, \"attrs\", [[], []])\n\n let alteredAttrs = sets.map(([attr, _val]) => attr).concat(removes)\n let newSets = prevSets.filter(([attr, _val]) => !alteredAttrs.includes(attr)).concat(sets)\n let newRemoves = prevRemoves.filter((attr) => !alteredAttrs.includes(attr)).concat(removes)\n\n DOM.putSticky(el, \"attrs\", currentEl => {\n newRemoves.forEach(attr => currentEl.removeAttribute(attr))\n newSets.forEach(([attr, val]) => currentEl.setAttribute(attr, val))\n return [newSets, newRemoves]\n })\n },\n\n hasAllClasses(el, classes){ return classes.every(name => el.classList.contains(name)) },\n\n isToggledOut(el, outClasses){\n return !this.isVisible(el) || this.hasAllClasses(el, outClasses)\n },\n\n filterToEls(liveSocket, sourceEl, {to}){\n let defaultQuery = () => {\n if(typeof(to) === \"string\"){\n return document.querySelectorAll(to)\n } else if(to.closest){\n let toEl = sourceEl.closest(to.closest)\n return toEl ? [toEl] : []\n } else if(to.inner){\n return sourceEl.querySelectorAll(to.inner)\n }\n }\n return to ? liveSocket.jsQuerySelectorAll(sourceEl, to, defaultQuery) : [sourceEl]\n },\n\n defaultDisplay(el){\n return {tr: \"table-row\", td: \"table-cell\"}[el.tagName.toLowerCase()] || \"block\"\n },\n\n transitionClasses(val){\n if(!val){ return null }\n\n let [trans, tStart, tEnd] = Array.isArray(val) ? val : [val.split(\" \"), [], []]\n trans = Array.isArray(trans) ? trans : trans.split(\" \")\n tStart = Array.isArray(tStart) ? tStart : tStart.split(\" \")\n tEnd = Array.isArray(tEnd) ? tEnd : tEnd.split(\" \")\n return [trans, tStart, tEnd]\n }\n}\n\nexport default JS\n", "import {\n CHECKABLE_INPUTS,\n DEBOUNCE_PREV_KEY,\n DEBOUNCE_TRIGGER,\n FOCUSABLE_INPUTS,\n PHX_COMPONENT,\n PHX_HAS_FOCUSED,\n PHX_HAS_SUBMITTED,\n PHX_MAIN,\n PHX_PARENT_ID,\n PHX_PRIVATE,\n PHX_REF_SRC,\n PHX_PENDING_ATTRS,\n PHX_ROOT_ID,\n PHX_SESSION,\n PHX_STATIC,\n PHX_UPLOAD_REF,\n PHX_VIEW_SELECTOR,\n PHX_STICKY,\n PHX_EVENT_CLASSES,\n THROTTLED,\n} from \"./constants\"\n\nimport JS from \"./js\"\n\nimport {\n logError\n} from \"./utils\"\n\nlet DOM = {\n byId(id){ return document.getElementById(id) || logError(`no id found for ${id}`) },\n\n removeClass(el, className){\n el.classList.remove(className)\n if(el.classList.length === 0){ el.removeAttribute(\"class\") }\n },\n\n all(node, query, callback){\n if(!node){ return [] }\n let array = Array.from(node.querySelectorAll(query))\n return callback ? array.forEach(callback) : array\n },\n\n childNodeLength(html){\n let template = document.createElement(\"template\")\n template.innerHTML = html\n return template.content.childElementCount\n },\n\n isUploadInput(el){ return el.type === \"file\" && el.getAttribute(PHX_UPLOAD_REF) !== null },\n\n isAutoUpload(inputEl){ return inputEl.hasAttribute(\"data-phx-auto-upload\") },\n\n findUploadInputs(node){\n const formId = node.id\n const inputsOutsideForm = this.all(document, `input[type=\"file\"][${PHX_UPLOAD_REF}][form=\"${formId}\"]`)\n return this.all(node, `input[type=\"file\"][${PHX_UPLOAD_REF}]`).concat(inputsOutsideForm)\n },\n\n findComponentNodeList(node, cid){\n return this.filterWithinSameLiveView(this.all(node, `[${PHX_COMPONENT}=\"${cid}\"]`), node)\n },\n\n isPhxDestroyed(node){\n return node.id && DOM.private(node, \"destroyed\") ? true : false\n },\n\n wantsNewTab(e){\n let wantsNewTab = e.ctrlKey || e.shiftKey || e.metaKey || (e.button && e.button === 1)\n let isDownload = (e.target instanceof HTMLAnchorElement && e.target.hasAttribute(\"download\"))\n let isTargetBlank = e.target.hasAttribute(\"target\") && e.target.getAttribute(\"target\").toLowerCase() === \"_blank\"\n let isTargetNamedTab = e.target.hasAttribute(\"target\") && !e.target.getAttribute(\"target\").startsWith(\"_\")\n return wantsNewTab || isTargetBlank || isDownload || isTargetNamedTab\n },\n\n isUnloadableFormSubmit(e){\n // Ignore form submissions intended to close a native element\n // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog#usage_notes\n let isDialogSubmit = (e.target && e.target.getAttribute(\"method\") === \"dialog\") ||\n (e.submitter && e.submitter.getAttribute(\"formmethod\") === \"dialog\")\n\n if(isDialogSubmit){\n return false\n } else {\n return !e.defaultPrevented && !this.wantsNewTab(e)\n }\n },\n\n isNewPageClick(e, currentLocation){\n let href = e.target instanceof HTMLAnchorElement ? e.target.getAttribute(\"href\") : null\n let url\n\n if(e.defaultPrevented || href === null || this.wantsNewTab(e)){ return false }\n if(href.startsWith(\"mailto:\") || href.startsWith(\"tel:\")){ return false }\n if(e.target.isContentEditable){ return false }\n\n try {\n url = new URL(href)\n } catch(e) {\n try {\n url = new URL(href, currentLocation)\n } catch(e) {\n // bad URL, fallback to let browser try it as external\n return true\n }\n }\n\n if(url.host === currentLocation.host && url.protocol === currentLocation.protocol){\n if(url.pathname === currentLocation.pathname && url.search === currentLocation.search){\n return url.hash === \"\" && !url.href.endsWith(\"#\")\n }\n }\n return url.protocol.startsWith(\"http\")\n },\n\n markPhxChildDestroyed(el){\n if(this.isPhxChild(el)){ el.setAttribute(PHX_SESSION, \"\") }\n this.putPrivate(el, \"destroyed\", true)\n },\n\n findPhxChildrenInFragment(html, parentId){\n let template = document.createElement(\"template\")\n template.innerHTML = html\n return this.findPhxChildren(template.content, parentId)\n },\n\n isIgnored(el, phxUpdate){\n return (el.getAttribute(phxUpdate) || el.getAttribute(\"data-phx-update\")) === \"ignore\"\n },\n\n isPhxUpdate(el, phxUpdate, updateTypes){\n return el.getAttribute && updateTypes.indexOf(el.getAttribute(phxUpdate)) >= 0\n },\n\n findPhxSticky(el){ return this.all(el, `[${PHX_STICKY}]`) },\n\n findPhxChildren(el, parentId){\n return this.all(el, `${PHX_VIEW_SELECTOR}[${PHX_PARENT_ID}=\"${parentId}\"]`)\n },\n\n findExistingParentCIDs(node, cids){\n // we only want to find parents that exist on the page\n // if a cid is not on the page, the only way it can be added back to the page\n // is if a parent adds it back, therefore if a cid does not exist on the page,\n // we should not try to render it by itself (because it would be rendered twice,\n // one by the parent, and a second time by itself)\n let parentCids = new Set()\n let childrenCids = new Set()\n\n cids.forEach(cid => {\n this.filterWithinSameLiveView(this.all(node, `[${PHX_COMPONENT}=\"${cid}\"]`), node).forEach(parent => {\n parentCids.add(cid)\n this.all(parent, `[${PHX_COMPONENT}]`)\n .map(el => parseInt(el.getAttribute(PHX_COMPONENT)))\n .forEach(childCID => childrenCids.add(childCID))\n })\n })\n\n childrenCids.forEach(childCid => parentCids.delete(childCid))\n\n return parentCids\n },\n\n filterWithinSameLiveView(nodes, parent){\n if(parent.querySelector(PHX_VIEW_SELECTOR)){\n return nodes.filter(el => this.withinSameLiveView(el, parent))\n } else {\n return nodes\n }\n },\n\n withinSameLiveView(node, parent){\n while(node = node.parentNode){\n if(node.isSameNode(parent)){ return true }\n if(node.getAttribute(PHX_SESSION) !== null){ return false }\n }\n },\n\n private(el, key){ return el[PHX_PRIVATE] && el[PHX_PRIVATE][key] },\n\n deletePrivate(el, key){ el[PHX_PRIVATE] && delete (el[PHX_PRIVATE][key]) },\n\n putPrivate(el, key, value){\n if(!el[PHX_PRIVATE]){ el[PHX_PRIVATE] = {} }\n el[PHX_PRIVATE][key] = value\n },\n\n updatePrivate(el, key, defaultVal, updateFunc){\n let existing = this.private(el, key)\n if(existing === undefined){\n this.putPrivate(el, key, updateFunc(defaultVal))\n } else {\n this.putPrivate(el, key, updateFunc(existing))\n }\n },\n\n syncPendingAttrs(fromEl, toEl){\n if(!fromEl.hasAttribute(PHX_REF_SRC)){ return }\n PHX_EVENT_CLASSES.forEach(className => {\n fromEl.classList.contains(className) && toEl.classList.add(className)\n })\n PHX_PENDING_ATTRS.filter(attr => fromEl.hasAttribute(attr)).forEach(attr => {\n toEl.setAttribute(attr, fromEl.getAttribute(attr))\n })\n },\n\n copyPrivates(target, source){\n if(source[PHX_PRIVATE]){\n target[PHX_PRIVATE] = source[PHX_PRIVATE]\n }\n },\n\n putTitle(str){\n let titleEl = document.querySelector(\"title\")\n if(titleEl){\n let {prefix, suffix} = titleEl.dataset\n document.title = `${prefix || \"\"}${str}${suffix || \"\"}`\n } else {\n document.title = str\n }\n },\n\n debounce(el, event, phxDebounce, defaultDebounce, phxThrottle, defaultThrottle, asyncFilter, callback){\n let debounce = el.getAttribute(phxDebounce)\n let throttle = el.getAttribute(phxThrottle)\n\n if(debounce === \"\"){ debounce = defaultDebounce }\n if(throttle === \"\"){ throttle = defaultThrottle }\n let value = debounce || throttle\n switch(value){\n case null: return callback()\n\n case \"blur\":\n if(this.once(el, \"debounce-blur\")){\n el.addEventListener(\"blur\", () => {\n if(asyncFilter()){ callback() }\n })\n }\n return\n\n default:\n let timeout = parseInt(value)\n let trigger = () => throttle ? this.deletePrivate(el, THROTTLED) : callback()\n let currentCycle = this.incCycle(el, DEBOUNCE_TRIGGER, trigger)\n if(isNaN(timeout)){ return logError(`invalid throttle/debounce value: ${value}`) }\n if(throttle){\n let newKeyDown = false\n if(event.type === \"keydown\"){\n let prevKey = this.private(el, DEBOUNCE_PREV_KEY)\n this.putPrivate(el, DEBOUNCE_PREV_KEY, event.key)\n newKeyDown = prevKey !== event.key\n }\n\n if(!newKeyDown && this.private(el, THROTTLED)){\n return false\n } else {\n callback()\n const t = setTimeout(() => {\n if(asyncFilter()){ this.triggerCycle(el, DEBOUNCE_TRIGGER) }\n }, timeout)\n this.putPrivate(el, THROTTLED, t)\n }\n } else {\n setTimeout(() => {\n if(asyncFilter()){ this.triggerCycle(el, DEBOUNCE_TRIGGER, currentCycle) }\n }, timeout)\n }\n\n let form = el.form\n if(form && this.once(form, \"bind-debounce\")){\n form.addEventListener(\"submit\", () => {\n Array.from((new FormData(form)).entries(), ([name]) => {\n let input = form.querySelector(`[name=\"${name}\"]`)\n this.incCycle(input, DEBOUNCE_TRIGGER)\n this.deletePrivate(input, THROTTLED)\n })\n })\n }\n if(this.once(el, \"bind-debounce\")){\n el.addEventListener(\"blur\", () => {\n // because we trigger the callback here,\n // we also clear the throttle timeout to prevent the callback\n // from being called again after the timeout fires\n clearTimeout(this.private(el, THROTTLED))\n this.triggerCycle(el, DEBOUNCE_TRIGGER)\n })\n }\n }\n },\n\n triggerCycle(el, key, currentCycle){\n let [cycle, trigger] = this.private(el, key)\n if(!currentCycle){ currentCycle = cycle }\n if(currentCycle === cycle){\n this.incCycle(el, key)\n trigger()\n }\n },\n\n once(el, key){\n if(this.private(el, key) === true){ return false }\n this.putPrivate(el, key, true)\n return true\n },\n\n incCycle(el, key, trigger = function (){ }){\n let [currentCycle] = this.private(el, key) || [0, trigger]\n currentCycle++\n this.putPrivate(el, key, [currentCycle, trigger])\n return currentCycle\n },\n\n // maintains or adds privately used hook information\n // fromEl and toEl can be the same element in the case of a newly added node\n // fromEl and toEl can be any HTML node type, so we need to check if it's an element node\n maintainPrivateHooks(fromEl, toEl, phxViewportTop, phxViewportBottom){\n // maintain the hooks created with createHook\n if(fromEl.hasAttribute && fromEl.hasAttribute(\"data-phx-hook\") && !toEl.hasAttribute(\"data-phx-hook\")){\n toEl.setAttribute(\"data-phx-hook\", fromEl.getAttribute(\"data-phx-hook\"))\n }\n // add hooks to elements with viewport attributes\n if(toEl.hasAttribute && (toEl.hasAttribute(phxViewportTop) || toEl.hasAttribute(phxViewportBottom))){\n toEl.setAttribute(\"data-phx-hook\", \"Phoenix.InfiniteScroll\")\n }\n },\n\n putCustomElHook(el, hook){\n if(el.isConnected){\n el.setAttribute(\"data-phx-hook\", \"\")\n } else {\n console.error(`\n hook attached to non-connected DOM element\n ensure you are calling createHook within your connectedCallback. ${el.outerHTML}\n `)\n }\n this.putPrivate(el, \"custom-el-hook\", hook)\n },\n\n getCustomElHook(el){ return this.private(el, \"custom-el-hook\") },\n\n isUsedInput(el){\n return (el.nodeType === Node.ELEMENT_NODE &&\n (this.private(el, PHX_HAS_FOCUSED) || this.private(el, PHX_HAS_SUBMITTED)))\n },\n\n resetForm(form){\n Array.from(form.elements).forEach(input => {\n this.deletePrivate(input, PHX_HAS_FOCUSED)\n this.deletePrivate(input, PHX_HAS_SUBMITTED)\n })\n },\n\n isPhxChild(node){\n return node.getAttribute && node.getAttribute(PHX_PARENT_ID)\n },\n\n isPhxSticky(node){\n return node.getAttribute && node.getAttribute(PHX_STICKY) !== null\n },\n\n isChildOfAny(el, parents){\n return !!parents.find(parent => parent.contains(el))\n },\n\n firstPhxChild(el){\n return this.isPhxChild(el) ? el : this.all(el, `[${PHX_PARENT_ID}]`)[0]\n },\n\n dispatchEvent(target, name, opts = {}){\n let defaultBubble = true\n let isUploadTarget = target.nodeName === \"INPUT\" && target.type === \"file\"\n if(isUploadTarget && name === \"click\"){\n defaultBubble = false\n }\n let bubbles = opts.bubbles === undefined ? defaultBubble : !!opts.bubbles\n let eventOpts = {bubbles: bubbles, cancelable: true, detail: opts.detail || {}}\n let event = name === \"click\" ? new MouseEvent(\"click\", eventOpts) : new CustomEvent(name, eventOpts)\n target.dispatchEvent(event)\n },\n\n cloneNode(node, html){\n if(typeof (html) === \"undefined\"){\n return node.cloneNode(true)\n } else {\n let cloned = node.cloneNode(false)\n cloned.innerHTML = html\n return cloned\n }\n },\n\n // merge attributes from source to target\n // if an element is ignored, we only merge data attributes\n // including removing data attributes that are no longer in the source\n mergeAttrs(target, source, opts = {}){\n let exclude = new Set(opts.exclude || [])\n let isIgnored = opts.isIgnored\n let sourceAttrs = source.attributes\n for(let i = sourceAttrs.length - 1; i >= 0; i--){\n let name = sourceAttrs[i].name\n if(!exclude.has(name)){\n const sourceValue = source.getAttribute(name)\n if(target.getAttribute(name) !== sourceValue && (!isIgnored || (isIgnored && name.startsWith(\"data-\")))){\n target.setAttribute(name, sourceValue)\n }\n } else {\n // We exclude the value from being merged on focused inputs, because the\n // user's input should always win.\n // We can still assign it as long as the value property is the same, though.\n // This prevents a situation where the updated hook is not being triggered\n // when an input is back in its \"original state\", because the attribute\n // was never changed, see:\n // https://github.com/phoenixframework/phoenix_live_view/issues/2163\n if(name === \"value\" && target.value === source.value){\n // actually set the value attribute to sync it with the value property\n target.setAttribute(\"value\", source.getAttribute(name))\n }\n }\n }\n\n let targetAttrs = target.attributes\n for(let i = targetAttrs.length - 1; i >= 0; i--){\n let name = targetAttrs[i].name\n if(isIgnored){\n if(name.startsWith(\"data-\") && !source.hasAttribute(name) && !PHX_PENDING_ATTRS.includes(name)){ target.removeAttribute(name) }\n } else {\n if(!source.hasAttribute(name)){ target.removeAttribute(name) }\n }\n }\n },\n\n mergeFocusedInput(target, source){\n // skip selects because FF will reset highlighted index for any setAttribute\n if(!(target instanceof HTMLSelectElement)){ DOM.mergeAttrs(target, source, {exclude: [\"value\"]}) }\n\n if(source.readOnly){\n target.setAttribute(\"readonly\", true)\n } else {\n target.removeAttribute(\"readonly\")\n }\n },\n\n hasSelectionRange(el){\n return el.setSelectionRange && (el.type === \"text\" || el.type === \"textarea\")\n },\n\n restoreFocus(focused, selectionStart, selectionEnd){\n if(focused instanceof HTMLSelectElement){ focused.focus() }\n if(!DOM.isTextualInput(focused)){ return }\n\n let wasFocused = focused.matches(\":focus\")\n if(!wasFocused){ focused.focus() }\n if(this.hasSelectionRange(focused)){\n focused.setSelectionRange(selectionStart, selectionEnd)\n }\n },\n\n isFormInput(el){ return /^(?:input|select|textarea)$/i.test(el.tagName) && el.type !== \"button\" },\n\n syncAttrsToProps(el){\n if(el instanceof HTMLInputElement && CHECKABLE_INPUTS.indexOf(el.type.toLocaleLowerCase()) >= 0){\n el.checked = el.getAttribute(\"checked\") !== null\n }\n },\n\n isTextualInput(el){ return FOCUSABLE_INPUTS.indexOf(el.type) >= 0 },\n\n isNowTriggerFormExternal(el, phxTriggerExternal){\n return el.getAttribute && el.getAttribute(phxTriggerExternal) !== null\n },\n\n cleanChildNodes(container, phxUpdate){\n if(DOM.isPhxUpdate(container, phxUpdate, [\"append\", \"prepend\"])){\n let toRemove = []\n container.childNodes.forEach(childNode => {\n if(!childNode.id){\n // Skip warning if it's an empty text node (e.g. a new-line)\n let isEmptyTextNode = childNode.nodeType === Node.TEXT_NODE && childNode.nodeValue.trim() === \"\"\n if(!isEmptyTextNode && childNode.nodeType !== Node.COMMENT_NODE){\n logError(\"only HTML element tags with an id are allowed inside containers with phx-update.\\n\\n\" +\n `removing illegal node: \"${(childNode.outerHTML || childNode.nodeValue).trim()}\"\\n\\n`)\n }\n toRemove.push(childNode)\n }\n })\n toRemove.forEach(childNode => childNode.remove())\n }\n },\n\n replaceRootContainer(container, tagName, attrs){\n let retainedAttrs = new Set([\"id\", PHX_SESSION, PHX_STATIC, PHX_MAIN, PHX_ROOT_ID])\n if(container.tagName.toLowerCase() === tagName.toLowerCase()){\n Array.from(container.attributes)\n .filter(attr => !retainedAttrs.has(attr.name.toLowerCase()))\n .forEach(attr => container.removeAttribute(attr.name))\n\n Object.keys(attrs)\n .filter(name => !retainedAttrs.has(name.toLowerCase()))\n .forEach(attr => container.setAttribute(attr, attrs[attr]))\n\n return container\n\n } else {\n let newContainer = document.createElement(tagName)\n Object.keys(attrs).forEach(attr => newContainer.setAttribute(attr, attrs[attr]))\n retainedAttrs.forEach(attr => newContainer.setAttribute(attr, container.getAttribute(attr)))\n newContainer.innerHTML = container.innerHTML\n container.replaceWith(newContainer)\n return newContainer\n }\n },\n\n getSticky(el, name, defaultVal){\n let op = (DOM.private(el, \"sticky\") || []).find(([existingName, ]) => name === existingName)\n if(op){\n let [_name, _op, stashedResult] = op\n return stashedResult\n } else {\n return typeof(defaultVal) === \"function\" ? defaultVal() : defaultVal\n }\n },\n\n deleteSticky(el, name){\n this.updatePrivate(el, \"sticky\", [], ops => {\n return ops.filter(([existingName, _]) => existingName !== name)\n })\n },\n\n putSticky(el, name, op){\n let stashedResult = op(el)\n this.updatePrivate(el, \"sticky\", [], ops => {\n let existingIndex = ops.findIndex(([existingName, ]) => name === existingName)\n if(existingIndex >= 0){\n ops[existingIndex] = [name, op, stashedResult]\n } else {\n ops.push([name, op, stashedResult])\n }\n return ops\n })\n },\n\n applyStickyOperations(el){\n let ops = DOM.private(el, \"sticky\")\n if(!ops){ return }\n\n ops.forEach(([name, op, _stashed]) => this.putSticky(el, name, op))\n }\n}\n\nexport default DOM\n", "import {\n PHX_ACTIVE_ENTRY_REFS,\n PHX_LIVE_FILE_UPDATED,\n PHX_PREFLIGHTED_REFS\n} from \"./constants\"\n\nimport {\n channelUploader,\n logError\n} from \"./utils\"\n\nimport LiveUploader from \"./live_uploader\"\n\nexport default class UploadEntry {\n static isActive(fileEl, file){\n let isNew = file._phxRef === undefined\n let activeRefs = fileEl.getAttribute(PHX_ACTIVE_ENTRY_REFS).split(\",\")\n let isActive = activeRefs.indexOf(LiveUploader.genFileRef(file)) >= 0\n return file.size > 0 && (isNew || isActive)\n }\n\n static isPreflighted(fileEl, file){\n let preflightedRefs = fileEl.getAttribute(PHX_PREFLIGHTED_REFS).split(\",\")\n let isPreflighted = preflightedRefs.indexOf(LiveUploader.genFileRef(file)) >= 0\n return isPreflighted && this.isActive(fileEl, file)\n }\n\n static isPreflightInProgress(file){\n return file._preflightInProgress === true\n }\n\n static markPreflightInProgress(file){\n file._preflightInProgress = true\n }\n\n constructor(fileEl, file, view, autoUpload){\n this.ref = LiveUploader.genFileRef(file)\n this.fileEl = fileEl\n this.file = file\n this.view = view\n this.meta = null\n this._isCancelled = false\n this._isDone = false\n this._progress = 0\n this._lastProgressSent = -1\n this._onDone = function(){ }\n this._onElUpdated = this.onElUpdated.bind(this)\n this.fileEl.addEventListener(PHX_LIVE_FILE_UPDATED, this._onElUpdated)\n this.autoUpload = autoUpload\n }\n\n metadata(){ return this.meta }\n\n progress(progress){\n this._progress = Math.floor(progress)\n if(this._progress > this._lastProgressSent){\n if(this._progress >= 100){\n this._progress = 100\n this._lastProgressSent = 100\n this._isDone = true\n this.view.pushFileProgress(this.fileEl, this.ref, 100, () => {\n LiveUploader.untrackFile(this.fileEl, this.file)\n this._onDone()\n })\n } else {\n this._lastProgressSent = this._progress\n this.view.pushFileProgress(this.fileEl, this.ref, this._progress)\n }\n }\n }\n\n isCancelled(){ return this._isCancelled }\n\n cancel(){\n this.file._preflightInProgress = false\n this._isCancelled = true\n this._isDone = true\n this._onDone()\n }\n\n isDone(){ return this._isDone }\n\n error(reason = \"failed\"){\n this.fileEl.removeEventListener(PHX_LIVE_FILE_UPDATED, this._onElUpdated)\n this.view.pushFileProgress(this.fileEl, this.ref, {error: reason})\n if(!this.isAutoUpload()){ LiveUploader.clearFiles(this.fileEl) }\n }\n\n isAutoUpload(){ return this.autoUpload }\n\n //private\n\n onDone(callback){\n this._onDone = () => {\n this.fileEl.removeEventListener(PHX_LIVE_FILE_UPDATED, this._onElUpdated)\n callback()\n }\n }\n\n onElUpdated(){\n let activeRefs = this.fileEl.getAttribute(PHX_ACTIVE_ENTRY_REFS).split(\",\")\n if(activeRefs.indexOf(this.ref) === -1){\n LiveUploader.untrackFile(this.fileEl, this.file)\n this.cancel()\n }\n }\n\n toPreflightPayload(){\n return {\n last_modified: this.file.lastModified,\n name: this.file.name,\n relative_path: this.file.webkitRelativePath,\n size: this.file.size,\n type: this.file.type,\n ref: this.ref,\n meta: typeof(this.file.meta) === \"function\" ? this.file.meta() : undefined\n }\n }\n\n uploader(uploaders){\n if(this.meta.uploader){\n let callback = uploaders[this.meta.uploader] || logError(`no uploader configured for ${this.meta.uploader}`)\n return {name: this.meta.uploader, callback: callback}\n } else {\n return {name: \"channel\", callback: channelUploader}\n }\n }\n\n zipPostFlight(resp){\n this.meta = resp.entries[this.ref]\n if(!this.meta){ logError(`no preflight upload response returned with ref ${this.ref}`, {input: this.fileEl, response: resp}) }\n }\n}\n", "import {\n PHX_DONE_REFS,\n PHX_PREFLIGHTED_REFS,\n PHX_UPLOAD_REF\n} from \"./constants\"\n\nimport {\n} from \"./utils\"\n\nimport DOM from \"./dom\"\nimport UploadEntry from \"./upload_entry\"\n\nlet liveUploaderFileRef = 0\n\nexport default class LiveUploader {\n static genFileRef(file){\n let ref = file._phxRef\n if(ref !== undefined){\n return ref\n } else {\n file._phxRef = (liveUploaderFileRef++).toString()\n return file._phxRef\n }\n }\n\n static getEntryDataURL(inputEl, ref, callback){\n let file = this.activeFiles(inputEl).find(file => this.genFileRef(file) === ref)\n callback(URL.createObjectURL(file))\n }\n\n static hasUploadsInProgress(formEl){\n let active = 0\n DOM.findUploadInputs(formEl).forEach(input => {\n if(input.getAttribute(PHX_PREFLIGHTED_REFS) !== input.getAttribute(PHX_DONE_REFS)){\n active++\n }\n })\n return active > 0\n }\n\n static serializeUploads(inputEl){\n let files = this.activeFiles(inputEl)\n let fileData = {}\n files.forEach(file => {\n let entry = {path: inputEl.name}\n let uploadRef = inputEl.getAttribute(PHX_UPLOAD_REF)\n fileData[uploadRef] = fileData[uploadRef] || []\n entry.ref = this.genFileRef(file)\n entry.last_modified = file.lastModified\n entry.name = file.name || entry.ref\n entry.relative_path = file.webkitRelativePath\n entry.type = file.type\n entry.size = file.size\n if(typeof(file.meta) === \"function\"){ entry.meta = file.meta() }\n fileData[uploadRef].push(entry)\n })\n return fileData\n }\n\n static clearFiles(inputEl){\n inputEl.value = null\n inputEl.removeAttribute(PHX_UPLOAD_REF)\n DOM.putPrivate(inputEl, \"files\", [])\n }\n\n static untrackFile(inputEl, file){\n DOM.putPrivate(inputEl, \"files\", DOM.private(inputEl, \"files\").filter(f => !Object.is(f, file)))\n }\n\n static trackFiles(inputEl, files, dataTransfer){\n if(inputEl.getAttribute(\"multiple\") !== null){\n let newFiles = files.filter(file => !this.activeFiles(inputEl).find(f => Object.is(f, file)))\n DOM.updatePrivate(inputEl, \"files\", [], (existing) => existing.concat(newFiles))\n inputEl.value = null\n } else {\n // Reset inputEl files to align output with programmatic changes (i.e. drag and drop)\n if(dataTransfer && dataTransfer.files.length > 0){ inputEl.files = dataTransfer.files }\n DOM.putPrivate(inputEl, \"files\", files)\n }\n }\n\n static activeFileInputs(formEl){\n let fileInputs = DOM.findUploadInputs(formEl)\n return Array.from(fileInputs).filter(el => el.files && this.activeFiles(el).length > 0)\n }\n\n static activeFiles(input){\n return (DOM.private(input, \"files\") || []).filter(f => UploadEntry.isActive(input, f))\n }\n\n static inputsAwaitingPreflight(formEl){\n let fileInputs = DOM.findUploadInputs(formEl)\n return Array.from(fileInputs).filter(input => this.filesAwaitingPreflight(input).length > 0)\n }\n\n static filesAwaitingPreflight(input){\n return this.activeFiles(input).filter(f => !UploadEntry.isPreflighted(input, f) && !UploadEntry.isPreflightInProgress(f))\n }\n\n static markPreflightInProgress(entries){\n entries.forEach(entry => UploadEntry.markPreflightInProgress(entry.file))\n }\n\n constructor(inputEl, view, onComplete){\n this.autoUpload = DOM.isAutoUpload(inputEl)\n this.view = view\n this.onComplete = onComplete\n this._entries =\n Array.from(LiveUploader.filesAwaitingPreflight(inputEl) || [])\n .map(file => new UploadEntry(inputEl, file, view, this.autoUpload))\n\n // prevent sending duplicate preflight requests\n LiveUploader.markPreflightInProgress(this._entries)\n\n this.numEntriesInProgress = this._entries.length\n }\n\n isAutoUpload(){ return this.autoUpload }\n\n entries(){ return this._entries }\n\n initAdapterUpload(resp, onError, liveSocket){\n this._entries =\n this._entries.map(entry => {\n if(entry.isCancelled()){\n this.numEntriesInProgress--\n if(this.numEntriesInProgress === 0){ this.onComplete() }\n } else {\n entry.zipPostFlight(resp)\n entry.onDone(() => {\n this.numEntriesInProgress--\n if(this.numEntriesInProgress === 0){ this.onComplete() }\n })\n }\n return entry\n })\n\n let groupedEntries = this._entries.reduce((acc, entry) => {\n if(!entry.meta){ return acc }\n let {name, callback} = entry.uploader(liveSocket.uploaders)\n acc[name] = acc[name] || {callback: callback, entries: []}\n acc[name].entries.push(entry)\n return acc\n }, {})\n\n for(let name in groupedEntries){\n let {callback, entries} = groupedEntries[name]\n callback(entries, onError, resp, liveSocket)\n }\n }\n}\n", "import {\n PHX_ACTIVE_ENTRY_REFS,\n PHX_LIVE_FILE_UPDATED,\n PHX_PREFLIGHTED_REFS,\n PHX_UPLOAD_REF\n} from \"./constants\"\n\nimport LiveUploader from \"./live_uploader\"\nimport ARIA from \"./aria\"\n\nlet Hooks = {\n LiveFileUpload: {\n activeRefs(){ return this.el.getAttribute(PHX_ACTIVE_ENTRY_REFS) },\n\n preflightedRefs(){ return this.el.getAttribute(PHX_PREFLIGHTED_REFS) },\n\n mounted(){ this.preflightedWas = this.preflightedRefs() },\n\n updated(){\n let newPreflights = this.preflightedRefs()\n if(this.preflightedWas !== newPreflights){\n this.preflightedWas = newPreflights\n if(newPreflights === \"\"){\n this.__view().cancelSubmit(this.el.form)\n }\n }\n\n if(this.activeRefs() === \"\"){ this.el.value = null }\n this.el.dispatchEvent(new CustomEvent(PHX_LIVE_FILE_UPDATED))\n }\n },\n\n LiveImgPreview: {\n mounted(){\n this.ref = this.el.getAttribute(\"data-phx-entry-ref\")\n this.inputEl = document.getElementById(this.el.getAttribute(PHX_UPLOAD_REF))\n LiveUploader.getEntryDataURL(this.inputEl, this.ref, url => {\n this.url = url\n this.el.src = url\n })\n },\n destroyed(){\n URL.revokeObjectURL(this.url)\n }\n },\n FocusWrap: {\n mounted(){\n this.focusStart = this.el.firstElementChild\n this.focusEnd = this.el.lastElementChild\n this.focusStart.addEventListener(\"focus\", () => ARIA.focusLast(this.el))\n this.focusEnd.addEventListener(\"focus\", () => ARIA.focusFirst(this.el))\n this.el.addEventListener(\"phx:show-end\", () => this.el.focus())\n if(window.getComputedStyle(this.el).display !== \"none\"){\n ARIA.focusFirst(this.el)\n }\n }\n }\n}\n\nlet findScrollContainer = (el) => {\n // the scroll event won't be fired on the html/body element even if overflow is set\n // therefore we return null to instead listen for scroll events on document\n if ([\"HTML\", \"BODY\"].indexOf(el.nodeName.toUpperCase()) >= 0) return null\n if([\"scroll\", \"auto\"].indexOf(getComputedStyle(el).overflowY) >= 0) return el\n return findScrollContainer(el.parentElement)\n}\n\nlet scrollTop = (scrollContainer) => {\n if(scrollContainer){\n return scrollContainer.scrollTop\n } else {\n return document.documentElement.scrollTop || document.body.scrollTop\n }\n}\n\nlet bottom = (scrollContainer) => {\n if(scrollContainer){\n return scrollContainer.getBoundingClientRect().bottom\n } else {\n // when we have no container, the whole page scrolls,\n // therefore the bottom coordinate is the viewport height\n return window.innerHeight || document.documentElement.clientHeight\n }\n}\n\nlet top = (scrollContainer) => {\n if(scrollContainer){\n return scrollContainer.getBoundingClientRect().top\n } else {\n // when we have no container the whole page scrolls,\n // therefore the top coordinate is 0\n return 0\n }\n}\n\nlet isAtViewportTop = (el, scrollContainer) => {\n let rect = el.getBoundingClientRect()\n return Math.ceil(rect.top) >= top(scrollContainer) && Math.ceil(rect.left) >= 0 && Math.floor(rect.top) <= bottom(scrollContainer)\n}\n\nlet isAtViewportBottom = (el, scrollContainer) => {\n let rect = el.getBoundingClientRect()\n return Math.ceil(rect.bottom) >= top(scrollContainer) && Math.ceil(rect.left) >= 0 && Math.floor(rect.bottom) <= bottom(scrollContainer)\n}\n\nlet isWithinViewport = (el, scrollContainer) => {\n let rect = el.getBoundingClientRect()\n return Math.ceil(rect.top) >= top(scrollContainer) && Math.ceil(rect.left) >= 0 && Math.floor(rect.top) <= bottom(scrollContainer)\n}\n\nHooks.InfiniteScroll = {\n mounted(){\n this.scrollContainer = findScrollContainer(this.el)\n let scrollBefore = scrollTop(this.scrollContainer)\n let topOverran = false\n let throttleInterval = 500\n let pendingOp = null\n\n let onTopOverrun = this.throttle(throttleInterval, (topEvent, firstChild) => {\n pendingOp = () => true\n this.liveSocket.execJSHookPush(this.el, topEvent, {id: firstChild.id, _overran: true}, () => {\n pendingOp = null\n })\n })\n\n let onFirstChildAtTop = this.throttle(throttleInterval, (topEvent, firstChild) => {\n pendingOp = () => firstChild.scrollIntoView({block: \"start\"})\n this.liveSocket.execJSHookPush(this.el, topEvent, {id: firstChild.id}, () => {\n pendingOp = null\n // make sure that the DOM is patched by waiting for the next tick\n window.requestAnimationFrame(() => {\n if(!isWithinViewport(firstChild, this.scrollContainer)){\n firstChild.scrollIntoView({block: \"start\"})\n }\n })\n })\n })\n\n let onLastChildAtBottom = this.throttle(throttleInterval, (bottomEvent, lastChild) => {\n pendingOp = () => lastChild.scrollIntoView({block: \"end\"})\n this.liveSocket.execJSHookPush(this.el, bottomEvent, {id: lastChild.id}, () => {\n pendingOp = null\n // make sure that the DOM is patched by waiting for the next tick\n window.requestAnimationFrame(() => {\n if(!isWithinViewport(lastChild, this.scrollContainer)){\n lastChild.scrollIntoView({block: \"end\"})\n }\n })\n })\n })\n\n this.onScroll = (_e) => {\n let scrollNow = scrollTop(this.scrollContainer)\n\n if(pendingOp){\n scrollBefore = scrollNow\n return pendingOp()\n }\n let rect = this.el.getBoundingClientRect()\n let topEvent = this.el.getAttribute(this.liveSocket.binding(\"viewport-top\"))\n let bottomEvent = this.el.getAttribute(this.liveSocket.binding(\"viewport-bottom\"))\n let lastChild = this.el.lastElementChild\n let firstChild = this.el.firstElementChild\n let isScrollingUp = scrollNow < scrollBefore\n let isScrollingDown = scrollNow > scrollBefore\n\n // el overran while scrolling up\n if(isScrollingUp && topEvent && !topOverran && rect.top >= 0){\n topOverran = true\n onTopOverrun(topEvent, firstChild)\n } else if(isScrollingDown && topOverran && rect.top <= 0){\n topOverran = false\n }\n\n if(topEvent && isScrollingUp && isAtViewportTop(firstChild, this.scrollContainer)){\n onFirstChildAtTop(topEvent, firstChild)\n } else if(bottomEvent && isScrollingDown && isAtViewportBottom(lastChild, this.scrollContainer)){\n onLastChildAtBottom(bottomEvent, lastChild)\n }\n scrollBefore = scrollNow\n }\n\n if(this.scrollContainer){\n this.scrollContainer.addEventListener(\"scroll\", this.onScroll)\n } else {\n window.addEventListener(\"scroll\", this.onScroll)\n }\n },\n \n destroyed(){\n if(this.scrollContainer){\n this.scrollContainer.removeEventListener(\"scroll\", this.onScroll)\n } else {\n window.removeEventListener(\"scroll\", this.onScroll)\n }\n },\n\n throttle(interval, callback){\n let lastCallAt = 0\n let timer\n\n return (...args) => {\n let now = Date.now()\n let remainingTime = interval - (now - lastCallAt)\n\n if(remainingTime <= 0 || remainingTime > interval){\n if(timer) {\n clearTimeout(timer)\n timer = null\n }\n lastCallAt = now\n callback(...args)\n } else if(!timer){\n timer = setTimeout(() => {\n lastCallAt = Date.now()\n timer = null\n callback(...args)\n }, remainingTime)\n }\n }\n }\n}\nexport default Hooks\n", "import {\n PHX_REF_LOADING,\n PHX_REF_LOCK,\n PHX_REF_SRC,\n PHX_EVENT_CLASSES,\n PHX_DISABLED,\n PHX_READONLY,\n PHX_DISABLE_WITH_RESTORE\n} from \"./constants\"\n\nimport DOM from \"./dom\"\n\nexport default class ElementRef {\n constructor(el){\n this.el = el\n this.loadingRef = el.hasAttribute(PHX_REF_LOADING) ? parseInt(el.getAttribute(PHX_REF_LOADING), 10) : null\n this.lockRef = el.hasAttribute(PHX_REF_LOCK) ? parseInt(el.getAttribute(PHX_REF_LOCK), 10) : null\n }\n\n // public\n\n maybeUndo(ref, phxEvent, eachCloneCallback){\n if(!this.isWithin(ref)){ return }\n\n // undo locks and apply clones\n this.undoLocks(ref, phxEvent, eachCloneCallback)\n\n // undo loading states\n this.undoLoading(ref, phxEvent)\n\n // clean up if fully resolved\n if(this.isFullyResolvedBy(ref)){ this.el.removeAttribute(PHX_REF_SRC) }\n }\n\n // private\n\n isWithin(ref){\n return !((this.loadingRef !== null && this.loadingRef > ref) && (this.lockRef !== null && this.lockRef > ref))\n }\n\n // Check for cloned PHX_REF_LOCK element that has been morphed behind\n // the scenes while this element was locked in the DOM.\n // When we apply the cloned tree to the active DOM element, we must\n //\n // 1. execute pending mounted hooks for nodes now in the DOM\n // 2. undo any ref inside the cloned tree that has since been ack'd\n undoLocks(ref, phxEvent, eachCloneCallback){\n if(!this.isLockUndoneBy(ref)){ return }\n\n let clonedTree = DOM.private(this.el, PHX_REF_LOCK)\n if(clonedTree){\n eachCloneCallback(clonedTree)\n DOM.deletePrivate(this.el, PHX_REF_LOCK)\n }\n this.el.removeAttribute(PHX_REF_LOCK)\n\n let opts = {detail: {ref: ref, event: phxEvent}, bubbles: true, cancelable: false}\n this.el.dispatchEvent(new CustomEvent(`phx:undo-lock:${this.lockRef}`, opts))\n }\n\n undoLoading(ref, phxEvent){\n if(!this.isLoadingUndoneBy(ref)){\n if(this.canUndoLoading(ref) && this.el.classList.contains(\"phx-submit-loading\")){\n this.el.classList.remove(\"phx-change-loading\")\n }\n return\n }\n\n if(this.canUndoLoading(ref)){\n this.el.removeAttribute(PHX_REF_LOADING)\n let disabledVal = this.el.getAttribute(PHX_DISABLED)\n let readOnlyVal = this.el.getAttribute(PHX_READONLY)\n // restore inputs\n if(readOnlyVal !== null){\n this.el.readOnly = readOnlyVal === \"true\" ? true : false\n this.el.removeAttribute(PHX_READONLY)\n }\n if(disabledVal !== null){\n this.el.disabled = disabledVal === \"true\" ? true : false\n this.el.removeAttribute(PHX_DISABLED)\n }\n // restore disables\n let disableRestore = this.el.getAttribute(PHX_DISABLE_WITH_RESTORE)\n if(disableRestore !== null){\n this.el.innerText = disableRestore\n this.el.removeAttribute(PHX_DISABLE_WITH_RESTORE)\n }\n\n let opts = {detail: {ref: ref, event: phxEvent}, bubbles: true, cancelable: false}\n this.el.dispatchEvent(new CustomEvent(`phx:undo-loading:${this.loadingRef}`, opts))\n }\n\n // remove classes\n PHX_EVENT_CLASSES.forEach(name => {\n if(name !== \"phx-submit-loading\" || this.canUndoLoading(ref)){\n DOM.removeClass(this.el, name)\n }\n })\n }\n\n isLoadingUndoneBy(ref){ return this.loadingRef === null ? false : this.loadingRef <= ref }\n isLockUndoneBy(ref){ return this.lockRef === null ? false : this.lockRef <= ref }\n\n isFullyResolvedBy(ref){\n return (this.loadingRef === null || this.loadingRef <= ref) && (this.lockRef === null || this.lockRef <= ref)\n }\n\n // only remove the phx-submit-loading class if we are not locked\n canUndoLoading(ref){ return this.lockRef === null || this.lockRef <= ref }\n}\n", "import {\n maybe\n} from \"./utils\"\n\nimport DOM from \"./dom\"\n\nexport default class DOMPostMorphRestorer {\n constructor(containerBefore, containerAfter, updateType){\n let idsBefore = new Set()\n let idsAfter = new Set([...containerAfter.children].map(child => child.id))\n\n let elementsToModify = []\n\n Array.from(containerBefore.children).forEach(child => {\n if(child.id){ // all of our children should be elements with ids\n idsBefore.add(child.id)\n if(idsAfter.has(child.id)){\n let previousElementId = child.previousElementSibling && child.previousElementSibling.id\n elementsToModify.push({elementId: child.id, previousElementId: previousElementId})\n }\n }\n })\n\n this.containerId = containerAfter.id\n this.updateType = updateType\n this.elementsToModify = elementsToModify\n this.elementIdsToAdd = [...idsAfter].filter(id => !idsBefore.has(id))\n }\n\n // We do the following to optimize append/prepend operations:\n // 1) Track ids of modified elements & of new elements\n // 2) All the modified elements are put back in the correct position in the DOM tree\n // by storing the id of their previous sibling\n // 3) New elements are going to be put in the right place by morphdom during append.\n // For prepend, we move them to the first position in the container\n perform(){\n let container = DOM.byId(this.containerId)\n this.elementsToModify.forEach(elementToModify => {\n if(elementToModify.previousElementId){\n maybe(document.getElementById(elementToModify.previousElementId), previousElem => {\n maybe(document.getElementById(elementToModify.elementId), elem => {\n let isInRightPlace = elem.previousElementSibling && elem.previousElementSibling.id == previousElem.id\n if(!isInRightPlace){\n previousElem.insertAdjacentElement(\"afterend\", elem)\n }\n })\n })\n } else {\n // This is the first element in the container\n maybe(document.getElementById(elementToModify.elementId), elem => {\n let isInRightPlace = elem.previousElementSibling == null\n if(!isInRightPlace){\n container.insertAdjacentElement(\"afterbegin\", elem)\n }\n })\n }\n })\n\n if(this.updateType == \"prepend\"){\n this.elementIdsToAdd.reverse().forEach(elemId => {\n maybe(document.getElementById(elemId), elem => container.insertAdjacentElement(\"afterbegin\", elem))\n })\n }\n }\n}\n", "var DOCUMENT_FRAGMENT_NODE = 11;\n\nfunction morphAttrs(fromNode, toNode) {\n var toNodeAttrs = toNode.attributes;\n var attr;\n var attrName;\n var attrNamespaceURI;\n var attrValue;\n var fromValue;\n\n // document-fragments dont have attributes so lets not do anything\n if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE || fromNode.nodeType === DOCUMENT_FRAGMENT_NODE) {\n return;\n }\n\n // update attributes on original DOM element\n for (var i = toNodeAttrs.length - 1; i >= 0; i--) {\n attr = toNodeAttrs[i];\n attrName = attr.name;\n attrNamespaceURI = attr.namespaceURI;\n attrValue = attr.value;\n\n if (attrNamespaceURI) {\n attrName = attr.localName || attrName;\n fromValue = fromNode.getAttributeNS(attrNamespaceURI, attrName);\n\n if (fromValue !== attrValue) {\n if (attr.prefix === 'xmlns'){\n attrName = attr.name; // It's not allowed to set an attribute with the XMLNS namespace without specifying the `xmlns` prefix\n }\n fromNode.setAttributeNS(attrNamespaceURI, attrName, attrValue);\n }\n } else {\n fromValue = fromNode.getAttribute(attrName);\n\n if (fromValue !== attrValue) {\n fromNode.setAttribute(attrName, attrValue);\n }\n }\n }\n\n // Remove any extra attributes found on the original DOM element that\n // weren't found on the target element.\n var fromNodeAttrs = fromNode.attributes;\n\n for (var d = fromNodeAttrs.length - 1; d >= 0; d--) {\n attr = fromNodeAttrs[d];\n attrName = attr.name;\n attrNamespaceURI = attr.namespaceURI;\n\n if (attrNamespaceURI) {\n attrName = attr.localName || attrName;\n\n if (!toNode.hasAttributeNS(attrNamespaceURI, attrName)) {\n fromNode.removeAttributeNS(attrNamespaceURI, attrName);\n }\n } else {\n if (!toNode.hasAttribute(attrName)) {\n fromNode.removeAttribute(attrName);\n }\n }\n }\n}\n\nvar range; // Create a range object for efficently rendering strings to elements.\nvar NS_XHTML = 'http://www.w3.org/1999/xhtml';\n\nvar doc = typeof document === 'undefined' ? undefined : document;\nvar HAS_TEMPLATE_SUPPORT = !!doc && 'content' in doc.createElement('template');\nvar HAS_RANGE_SUPPORT = !!doc && doc.createRange && 'createContextualFragment' in doc.createRange();\n\nfunction createFragmentFromTemplate(str) {\n var template = doc.createElement('template');\n template.innerHTML = str;\n return template.content.childNodes[0];\n}\n\nfunction createFragmentFromRange(str) {\n if (!range) {\n range = doc.createRange();\n range.selectNode(doc.body);\n }\n\n var fragment = range.createContextualFragment(str);\n return fragment.childNodes[0];\n}\n\nfunction createFragmentFromWrap(str) {\n var fragment = doc.createElement('body');\n fragment.innerHTML = str;\n return fragment.childNodes[0];\n}\n\n/**\n * This is about the same\n * var html = new DOMParser().parseFromString(str, 'text/html');\n * return html.body.firstChild;\n *\n * @method toElement\n * @param {String} str\n */\nfunction toElement(str) {\n str = str.trim();\n if (HAS_TEMPLATE_SUPPORT) {\n // avoid restrictions on content for things like `Hi` which\n // createContextualFragment doesn't support\n //