diff --git a/CHANGELOG.md b/CHANGELOG.md index 44c2334e3..eb22115d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.20.16 (2024-06-20) + +### Bug fixes +* Fix bug introduced in 0.20.15 causing incorrect patching on form elements when awaiting acknowledgements + ## 0.20.15 (2024-06-18) ### Enhancements diff --git a/assets/js/phoenix_live_view/dom.js b/assets/js/phoenix_live_view/dom.js index 953606b45..e060a0ba4 100644 --- a/assets/js/phoenix_live_view/dom.js +++ b/assets/js/phoenix_live_view/dom.js @@ -507,9 +507,18 @@ let DOM = { if(ref === null){ return true } let refSrc = fromEl.getAttribute(PHX_REF_SRC) - if(DOM.isUploadInput(fromEl)){ DOM.mergeAttrs(fromEl, toEl, {isIgnored: true}) } - DOM.putPrivate(fromEl, PHX_REF, toEl) - return false + if(DOM.isFormInput(fromEl) || fromEl.getAttribute(disableWith) !== null){ + if(DOM.isUploadInput(fromEl)){ DOM.mergeAttrs(fromEl, toEl, {isIgnored: true}) } + DOM.putPrivate(fromEl, PHX_REF, toEl) + return false + } else { + PHX_EVENT_CLASSES.forEach(className => { + fromEl.classList.contains(className) && toEl.classList.add(className) + }) + toEl.setAttribute(PHX_REF, ref) + toEl.setAttribute(PHX_REF_SRC, refSrc) + return true + } }, cleanChildNodes(container, phxUpdate){ diff --git a/priv/static/phoenix_live_view.cjs.js b/priv/static/phoenix_live_view.cjs.js index 4e514c6b6..944f443c9 100644 --- a/priv/static/phoenix_live_view.cjs.js +++ b/priv/static/phoenix_live_view.cjs.js @@ -1,15 +1,27 @@ var __defProp = Object.defineProperty; -var __markAsModule = (target) => __defProp(target, "__esModule", { value: true }); +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { - __markAsModule(target); for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // js/phoenix_live_view/index.js -__export(exports, { +var phoenix_live_view_exports = {}; +__export(phoenix_live_view_exports, { LiveSocket: () => LiveSocket }); +module.exports = __toCommonJS(phoenix_live_view_exports); // js/phoenix_live_view/constants.js var CONSECUTIVE_RELOADS = "consecutive-reloads"; @@ -165,7 +177,7 @@ var isCid = (cid) => { return type === "number" || type === "string" && /^(0|[1-9]\d*)$/.test(cid); }; function detectDuplicateIds() { - let ids = new Set(); + let ids = /* @__PURE__ */ new Set(); let elems = document.querySelectorAll("*[id]"); for (let i = 0, len = elems.length; i < len; i++) { if (ids.has(elems[i].id)) { @@ -359,12 +371,15 @@ var JS = { isVisible(el) { return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length > 0); }, + // returns true if any part of the element is inside the viewport isInViewport(el) { const rect = el.getBoundingClientRect(); const windowHeight = window.innerHeight || document.documentElement.clientHeight; const windowWidth = window.innerWidth || document.documentElement.clientWidth; return rect.right > 0 && rect.bottom > 0 && rect.left < windowWidth && rect.top < windowHeight; }, + // private + // commands exec_exec(eventType, phxEvent, view, sourceEl, el, { attr, to }) { let nodes = to ? dom_default.all(document, to) : [sourceEl]; nodes.forEach((node) => { @@ -469,6 +484,7 @@ var JS = { exec_remove_attr(eventType, phxEvent, view, sourceEl, el, { attr }) { this.setOrRemoveAttrs(el, [], [attr]); }, + // utils for commands show(eventType, view, el, display, transition, time) { if (!this.isVisible(el)) { this.toggle(eventType, view, el, display, transition, null, time); @@ -702,8 +718,8 @@ var DOM = { return this.all(el, `${PHX_VIEW_SELECTOR}[${PHX_PARENT_ID}="${parentId}"]`); }, findExistingParentCIDs(node, cids) { - let parentCids = new Set(); - let childrenCids = new Set(); + let parentCids = /* @__PURE__ */ new Set(); + let childrenCids = /* @__PURE__ */ new Set(); cids.forEach((cid) => { this.filterWithinSameLiveView(this.all(node, `[${PHX_COMPONENT}="${cid}"]`), node).forEach((parent) => { parentCids.add(cid); @@ -961,6 +977,9 @@ var DOM = { return cloned; } }, + // merge attributes from source to target + // if an element is ignored, we only merge data attributes + // including removing data attributes that are no longer in the source mergeAttrs(target, source, opts = {}) { let exclude = new Set(opts.exclude || []); let isIgnored = opts.isIgnored; @@ -1040,11 +1059,20 @@ var DOM = { return true; } let refSrc = fromEl.getAttribute(PHX_REF_SRC); - if (DOM.isUploadInput(fromEl)) { - DOM.mergeAttrs(fromEl, toEl, { isIgnored: true }); + if (DOM.isFormInput(fromEl) || fromEl.getAttribute(disableWith) !== null) { + if (DOM.isUploadInput(fromEl)) { + DOM.mergeAttrs(fromEl, toEl, { isIgnored: true }); + } + DOM.putPrivate(fromEl, PHX_REF, toEl); + return false; + } else { + PHX_EVENT_CLASSES.forEach((className) => { + fromEl.classList.contains(className) && toEl.classList.add(className); + }); + toEl.setAttribute(PHX_REF, ref); + toEl.setAttribute(PHX_REF_SRC, refSrc); + return true; } - DOM.putPrivate(fromEl, PHX_REF, toEl); - return false; }, cleanChildNodes(container, phxUpdate) { if (DOM.isPhxUpdate(container, phxUpdate, ["append", "prepend"])) { @@ -1066,7 +1094,7 @@ removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}" } }, replaceRootContainer(container, tagName, attrs) { - let retainedAttrs = new Set(["id", PHX_SESSION, PHX_STATIC, PHX_MAIN, PHX_ROOT_ID]); + let retainedAttrs = /* @__PURE__ */ new Set(["id", PHX_SESSION, PHX_STATIC, PHX_MAIN, PHX_ROOT_ID]); if (container.tagName.toLowerCase() === tagName.toLowerCase()) { Array.from(container.attributes).filter((attr) => !retainedAttrs.has(attr.name.toLowerCase())).forEach((attr) => container.removeAttribute(attr.name)); Object.keys(attrs).filter((name) => !retainedAttrs.has(name.toLowerCase())).forEach((attr) => container.setAttribute(attr, attrs[attr])); @@ -1193,6 +1221,7 @@ var UploadEntry = class { isAutoUpload() { return this.autoUpload; } + //private onDone(callback) { this._onDone = () => { this.fileEl.removeEventListener(PHX_LIVE_FILE_UPDATED, this._onElUpdated); @@ -1235,7 +1264,7 @@ var UploadEntry = class { // js/phoenix_live_view/live_uploader.js var liveUploaderFileRef = 0; -var LiveUploader = class { +var LiveUploader = class _LiveUploader { static genFileRef(file) { let ref = file._phxRef; if (ref !== void 0) { @@ -1319,8 +1348,8 @@ var LiveUploader = class { this.autoUpload = dom_default.isAutoUpload(inputEl); this.view = view; this.onComplete = onComplete; - this._entries = Array.from(LiveUploader.filesAwaitingPreflight(inputEl) || []).map((file) => new UploadEntry(inputEl, file, view, this.autoUpload)); - LiveUploader.markPreflightInProgress(this._entries); + this._entries = Array.from(_LiveUploader.filesAwaitingPreflight(inputEl) || []).map((file) => new UploadEntry(inputEl, file, view, this.autoUpload)); + _LiveUploader.markPreflightInProgress(this._entries); this.numEntriesInProgress = this._entries.length; } isAutoUpload() { @@ -1557,7 +1586,7 @@ var hooks_default = Hooks; // js/phoenix_live_view/dom_post_morph_restorer.js var DOMPostMorphRestorer = class { constructor(containerBefore, containerAfter, updateType) { - let idsBefore = new Set(); + let idsBefore = /* @__PURE__ */ new Set(); let idsAfter = new Set([...containerAfter.children].map((child) => child.id)); let elementsToModify = []; Array.from(containerBefore.children).forEach((child) => { @@ -1574,6 +1603,12 @@ var DOMPostMorphRestorer = class { this.elementsToModify = elementsToModify; this.elementIdsToAdd = [...idsAfter].filter((id) => !idsBefore.has(id)); } + // We do the following to optimize append/prepend operations: + // 1) Track ids of modified elements & of new elements + // 2) All the modified elements are put back in the correct position in the DOM tree + // by storing the id of their previous sibling + // 3) New elements are going to be put in the right place by morphdom during append. + // For prepend, we move them to the first position in the container perform() { let container = dom_default.byId(this.containerId); this.elementsToModify.forEach((elementToModify) => { @@ -1743,6 +1778,12 @@ var specialElHandlers = { } syncBooleanAttrProp(fromEl, toEl, "selected"); }, + /** + * The "value" attribute is special for the element since it sets + * the initial value. Changing the "value" attribute without changing the + * "value" property will have no effect since it is only used to the set the + * initial value. Similar for the "checked" attribute, and "disabled". + */ INPUT: function(fromEl, toEl) { syncBooleanAttrProp(fromEl, toEl, "checked"); syncBooleanAttrProp(fromEl, toEl, "disabled"); @@ -1838,7 +1879,7 @@ function morphdomFactory(morphAttrs2) { return parent.appendChild(child); }; var childrenOnly = options.childrenOnly === true; - var fromNodesLookup = Object.create(null); + var fromNodesLookup = /* @__PURE__ */ Object.create(null); var keyedRemovalList = []; function addKeyedRemoval(key) { keyedRemovalList.push(key); @@ -1910,7 +1951,12 @@ function morphdomFactory(morphAttrs2) { if (curFromNodeKey = getNodeKey(curFromNodeChild)) { addKeyedRemoval(curFromNodeKey); } else { - removeNode(curFromNodeChild, fromEl, true); + removeNode( + curFromNodeChild, + fromEl, + true + /* skip keyed nodes */ + ); } curFromNodeChild = fromNextSibling; } @@ -1971,7 +2017,12 @@ function morphdomFactory(morphAttrs2) { if (curFromNodeKey) { addKeyedRemoval(curFromNodeKey); } else { - removeNode(curFromNodeChild, fromEl, true); + removeNode( + curFromNodeChild, + fromEl, + true + /* skip keyed nodes */ + ); } curFromNodeChild = matchingFromEl; curFromNodeKey = getNodeKey(curFromNodeChild); @@ -2002,7 +2053,12 @@ function morphdomFactory(morphAttrs2) { if (curFromNodeKey) { addKeyedRemoval(curFromNodeKey); } else { - removeNode(curFromNodeChild, fromEl, true); + removeNode( + curFromNodeChild, + fromEl, + true + /* skip keyed nodes */ + ); } curFromNodeChild = fromNextSibling; } @@ -2163,6 +2219,10 @@ var DOMPatch = class { let externalFormTriggered = null; function morph(targetContainer2, source, withChildren = false) { morphdom_esm_default(targetContainer2, source, { + // normally, we are running with childrenOnly, as the patch HTML for a LV + // does not include the LV attrs (data-phx-session, etc.) + // when we are patching a live component, we do want to patch the root element as well; + // another case is the recursive patch of a stream item that was kept on reset (-> onBeforeNodeAdded) childrenOnly: targetContainer2.getAttribute(PHX_COMPONENT) === null && !withChildren, getNodeKey: (node) => { if (dom_default.isPhxDestroyed(node)) { @@ -2173,9 +2233,11 @@ var DOMPatch = class { } return node.id || node.getAttribute && node.getAttribute(PHX_MAGIC_ID); }, + // skip indexing from children when container is stream skipFromChildren: (from) => { return from.getAttribute(phxUpdate) === PHX_STREAM; }, + // tell morphdom how to add a child addChild: (parent, child) => { let { ref, streamAt } = this.getStreamInsert(child); if (ref === void 0) { @@ -2492,7 +2554,7 @@ var DOMPatch = class { }; // js/phoenix_live_view/rendered.js -var VOID_TAGS = new Set([ +var VOID_TAGS = /* @__PURE__ */ new Set([ "area", "base", "br", @@ -2510,7 +2572,7 @@ var VOID_TAGS = new Set([ "track", "wbr" ]); -var quoteChars = new Set(["'", '"']); +var quoteChars = /* @__PURE__ */ new Set(["'", '"']); var modifyRoot = (html, attrs, clearInnerHTML) => { let i = 0; let insideComment = false; @@ -2604,7 +2666,7 @@ var Rendered = class { } recursiveToString(rendered, components = rendered[COMPONENTS], onlyCids, changeTracking, rootAttrs) { onlyCids = onlyCids ? new Set(onlyCids) : null; - let output = { buffer: "", components, onlyCids, streams: new Set() }; + let output = { buffer: "", components, onlyCids, streams: /* @__PURE__ */ new Set() }; this.toOutputBuffer(rendered, null, output, changeTracking, rootAttrs); return [output.buffer, output.streams]; } @@ -2687,6 +2749,14 @@ var Rendered = class { target.newRender = true; } } + // Merges cid trees together, copying statics from source tree. + // + // The `pruneMagicId` is passed to control pruning the magicId of the + // target. We must always prune the magicId when we are sharing statics + // from another component. If not pruning, we replicate the logic from + // mutableMerge, where we set newRender to true if there is a root + // (effectively forcing the new version to be rendered instead of skipped) + // cloneMerge(target, source, pruneMagicId) { let merged = { ...target, ...source }; for (let key in merged) { @@ -2714,6 +2784,7 @@ var Rendered = class { pruneCIDs(cids) { cids.forEach((cid) => delete this.rendered[COMPONENTS][cid]); } + // private get() { return this.rendered; } @@ -2731,6 +2802,11 @@ var Rendered = class { this.magicId++; return `m${this.magicId}-${this.parentViewId()}`; } + // Converts rendered tree to output buffer. + // + // changeTracking controls if we can apply the PHX_SKIP optimization. + // It is disabled for comprehensions since we must re-render the entire collection + // and no individual element is tracked inside the comprehension. toOutputBuffer(rendered, templates, output, changeTracking, rootAttrs = {}) { if (rendered[DYNAMICS]) { return this.comprehensionToBuffer(rendered, templates, output); @@ -2792,7 +2868,7 @@ var Rendered = class { if (typeof rendered === "number") { let [str, streams] = this.recursiveCIDToString(output.components, rendered, output.onlyCids); output.buffer += str; - output.streams = new Set([...output.streams, ...streams]); + output.streams = /* @__PURE__ */ new Set([...output.streams, ...streams]); } else if (isObject(rendered)) { this.toOutputBuffer(rendered, templates, output, changeTracking, {}); } else { @@ -2825,7 +2901,7 @@ var ViewHook = class { this.__view = view; this.liveSocket = view.liveSocket; this.__callbacks = callbacks; - this.__listeners = new Set(); + this.__listeners = /* @__PURE__ */ new Set(); this.__isDisconnected = false; this.el = el; this.el.phxHookId = this.constructor.makeID(); @@ -2927,7 +3003,7 @@ var serializeForm = (form, metadata, onlyNames = []) => { } return params.toString(); }; -var View = class { +var View = class _View { constructor(el, liveSocket, parentView, flash, liveReferer) { this.isDead = false; this.liveSocket = liveSocket; @@ -2940,7 +3016,7 @@ var View = class { this.childJoins = 0; this.loaderTimer = null; this.pendingDiffs = []; - this.pendingForms = new Set(); + this.pendingForms = /* @__PURE__ */ new Set(); this.redirect = false; this.href = null; this.joinCount = this.parent ? this.parent.joinCount - 1 : 0; @@ -3019,7 +3095,13 @@ var View = class { this.channel.leave().receive("ok", onFinished).receive("error", onFinished).receive("timeout", onFinished); } setContainerClasses(...classes) { - this.el.classList.remove(PHX_CONNECTED_CLASS, PHX_LOADING_CLASS, PHX_ERROR_CLASS, PHX_CLIENT_ERROR_CLASS, PHX_SERVER_ERROR_CLASS); + this.el.classList.remove( + PHX_CONNECTED_CLASS, + PHX_LOADING_CLASS, + PHX_ERROR_CLASS, + PHX_CLIENT_ERROR_CLASS, + PHX_SERVER_ERROR_CLASS + ); this.el.classList.add(...classes); } showLoader(timeout) { @@ -3053,6 +3135,12 @@ var View = class { }) { this.liveSocket.transition(time, onStart, onDone); } + // calls the callback with the view and target element for the given phxTarget + // targets can be: + // * an element itself, then it is simply passed to liveSocket.owner; + // * a CID (Component ID), then we first search the component's element in the DOM + // * a selector, then we search the selector in the DOM and call the callback + // for each element found with the corresponding owner view withinTargets(phxTarget, callback, dom = document, viewEl) { if (phxTarget instanceof HTMLElement || phxTarget instanceof SVGElement) { return this.liveSocket.owner(phxTarget, (view) => callback(view, phxTarget)); @@ -3143,6 +3231,10 @@ var View = class { this.el = dom_default.byId(this.id); this.el.setAttribute(PHX_ROOT_ID, this.root.id); } + // this is invoked for dead and live views, so we must filter by + // by owner to ensure we aren't duplicating hooks across disconnect + // and connected states. This also handles cases where hooks exist + // in a root layout with a LV in the body execNewMounted() { let phxViewportTop = this.binding(PHX_VIEWPORT_TOP); let phxViewportBottom = this.binding(PHX_VIEWPORT_BOTTOM); @@ -3209,7 +3301,7 @@ var View = class { performPatch(patch, pruneCids, isJoinPatch = false) { let removedEls = []; let phxChildrenAdded = false; - let updatedHookIds = new Set(); + let updatedHookIds = /* @__PURE__ */ new Set(); patch.after("added", (el) => { this.liveSocket.triggerDOM("onNodeAdded", [el]); let phxViewportTop = this.binding(PHX_VIEWPORT_TOP); @@ -3282,9 +3374,13 @@ var View = class { rootEl.setAttribute(PHX_SESSION, this.getSession()); rootEl.setAttribute(PHX_STATIC, this.getStatic()); rootEl.setAttribute(PHX_PARENT_ID, this.parent ? this.parent.id : null); - const formsToRecover = dom_default.all(template.content, "form").filter((newForm) => newForm.id && oldForms[newForm.id]).filter((newForm) => !this.pendingForms.has(newForm.id)).filter((newForm) => oldForms[newForm.id].getAttribute(phxChange) === newForm.getAttribute(phxChange)).map((newForm) => { - return [oldForms[newForm.id], newForm]; - }); + const formsToRecover = ( + // we go over all forms in the new DOM; because this is only the HTML for the current + // view, we can be sure that all forms are owned by this view: + dom_default.all(template.content, "form").filter((newForm) => newForm.id && oldForms[newForm.id]).filter((newForm) => !this.pendingForms.has(newForm.id)).filter((newForm) => oldForms[newForm.id].getAttribute(phxChange) === newForm.getAttribute(phxChange)).map((newForm) => { + return [oldForms[newForm.id], newForm]; + }) + ); if (formsToRecover.length === 0) { return callback(); } @@ -3320,7 +3416,7 @@ var View = class { joinChild(el) { let child = this.getChildById(el.id); if (!child) { - let view = new View(el, this.liveSocket, this); + let view = new _View(el, this.liveSocket, this); this.root.children[this.id][view.id] = view; view.join(); this.childJoins++; @@ -4123,12 +4219,15 @@ var LiveSocket = class { this.sessionStorage = opts.sessionStorage || window.sessionStorage; this.boundTopLevelEvents = false; this.serverCloseRef = null; - this.domCallbacks = Object.assign({ - onPatchStart: closure(), - onPatchEnd: closure(), - onNodeAdded: closure(), - onBeforeElUpdated: closure() - }, opts.dom || {}); + this.domCallbacks = Object.assign( + { + onPatchStart: closure(), + onPatchEnd: closure(), + onNodeAdded: closure(), + onBeforeElUpdated: closure() + }, + opts.dom || {} + ); this.transitions = new TransitionSet(); window.addEventListener("pagehide", (_e) => { this.unloaded = true; @@ -4139,6 +4238,7 @@ var LiveSocket = class { } }); } + // public version() { return "0.20.15"; } @@ -4215,6 +4315,7 @@ var LiveSocket = class { execJS(el, encodedJS, eventType = null) { this.owner(el, (view) => js_default.exec(eventType, encodedJS, view, el)); } + // private execJSHookPush(el, phxEvent, data, callback) { this.withinOwners(el, (view) => { js_default.exec("hook", phxEvent, view, el, ["push", { data, callback }]); @@ -4892,7 +4993,7 @@ var LiveSocket = class { }; var TransitionSet = class { constructor() { - this.transitions = new Set(); + this.transitions = /* @__PURE__ */ new Set(); this.pendingOps = []; } reset() { diff --git a/priv/static/phoenix_live_view.cjs.js.map b/priv/static/phoenix_live_view.cjs.js.map index 08bc741d7..559a151bd 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/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 from \"./live_socket\"\nexport {\n LiveSocket\n}\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 = \"data-phx-ref\"\nexport const PHX_REF_SRC = \"data-phx-ref-src\"\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_PAGE_LOADING = \"page-loading\"\nexport const PHX_CONNECTED_CLASS = \"phx-connected\"\nexport const PHX_LOADING_CLASS = \"phx-loading\"\nexport const PHX_NO_FEEDBACK_CLASS = \"phx-no-feedback\"\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_FEEDBACK_FOR = \"feedback-for\"\nexport const PHX_FEEDBACK_GROUP = \"feedback-group\"\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 LOADER_TIMEOUT = 1\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}\n\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\"", "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 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 } else {\n this.redirect(to)\n }\n },\n\n setCookie(name, value){\n document.cookie = `${name}=${value}`\n },\n\n getCookie(name){\n return document.cookie.replace(new RegExp(`(?:(?:^|.*;\\s*)${name}\\s*\\=\\s*([^;]*).*$)|^.*$`), \"$1\")\n },\n\n redirect(toURL, flash){\n if(flash){ Browser.setCookie(\"__phoenix_flash__\", flash + \"; max-age=60000; path=/\") }\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 focusMain(){\n let target = document.querySelector(\"main h1, main, h1\")\n if(target){\n let origTabIndex = target.tabIndex\n target.tabIndex = -1\n target.focus()\n target.tabIndex = origTabIndex\n }\n },\n\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", "import DOM from \"./dom\"\nimport ARIA from \"./aria\"\n\nlet focusStack = []\nlet default_transition_time = 200\n\nlet JS = {\n exec(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(sourceEl, args).forEach(el => {\n this[`exec_${kind}`](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(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(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(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(eventType, phxEvent, view, sourceEl, el, {href, replace}){\n view.liveSocket.historyRedirect(href, replace ? \"replace\" : \"push\")\n },\n\n exec_patch(eventType, phxEvent, view, sourceEl, el, {href, replace}){\n view.liveSocket.pushHistoryPatch(href, replace ? \"replace\" : \"push\", sourceEl)\n },\n\n exec_focus(eventType, phxEvent, view, sourceEl, el){\n window.requestAnimationFrame(() => ARIA.attemptFocus(el))\n },\n\n exec_focus_first(eventType, phxEvent, view, sourceEl, el){\n window.requestAnimationFrame(() => ARIA.focusFirstInteractive(el) || ARIA.focusFirst(el))\n },\n\n exec_push_focus(eventType, phxEvent, view, sourceEl, el){\n window.requestAnimationFrame(() => focusStack.push(el || sourceEl))\n },\n\n exec_pop_focus(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(eventType, phxEvent, view, sourceEl, el, {names, transition, time}){\n this.addOrRemoveClasses(el, names, [], transition, time, view)\n },\n\n exec_remove_class(eventType, phxEvent, view, sourceEl, el, {names, transition, time}){\n this.addOrRemoveClasses(el, [], names, transition, time, view)\n },\n\n exec_toggle_class(eventType, phxEvent, view, sourceEl, el, {to, names, transition, time}){\n this.toggleClasses(el, names, transition, time, view)\n },\n\n exec_toggle_attr(eventType, phxEvent, view, sourceEl, el, {attr: [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 exec_transition(eventType, phxEvent, view, sourceEl, el, {time, transition}){\n this.addOrRemoveClasses(el, [], [], transition, time, view)\n },\n\n exec_toggle(eventType, phxEvent, view, sourceEl, el, {display, ins, outs, time}){\n this.toggle(eventType, view, el, display, ins, outs, time)\n },\n\n exec_show(eventType, phxEvent, view, sourceEl, el, {display, transition, time}){\n this.show(eventType, view, el, display, transition, time)\n },\n\n exec_hide(eventType, phxEvent, view, sourceEl, el, {display, transition, time}){\n this.hide(eventType, view, el, display, transition, time)\n },\n\n exec_set_attr(eventType, phxEvent, view, sourceEl, el, {attr: [attr, val]}){\n this.setOrRemoveAttrs(el, [[attr, val]], [])\n },\n\n exec_remove_attr(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){\n if(!this.isVisible(el)){\n this.toggle(eventType, view, el, display, transition, null, time)\n }\n },\n\n hide(eventType, view, el, display, transition, time){\n if(this.isVisible(el)){\n this.toggle(eventType, view, el, display, null, transition, time)\n }\n },\n\n toggle(eventType, view, el, display, ins, outs, time){\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 el.dispatchEvent(new Event(\"phx:hide-start\"))\n view.transition(time, onStart, () => {\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 } 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 el.dispatchEvent(new Event(\"phx:show-start\"))\n view.transition(time, onStart, () => {\n this.addOrRemoveClasses(el, [], inClasses.concat(inEndClasses))\n el.dispatchEvent(new Event(\"phx:show-end\"))\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){\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)\n })\n },\n\n addOrRemoveClasses(el, adds, removes, transition, time, view){\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 return view.transition(time, onStart, onDone)\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(sourceEl, {to}){\n return to ? DOM.all(document, to) : [sourceEl]\n },\n\n defaultDisplay(el){\n return {tr: \"table-row\", td: \"table-cell\"}[el.tagName.toLowerCase()] || \"block\"\n }\n}\n\nexport default JS", "import {\n CHECKABLE_INPUTS,\n DEBOUNCE_PREV_KEY,\n DEBOUNCE_TRIGGER,\n FOCUSABLE_INPUTS,\n PHX_COMPONENT,\n PHX_EVENT_CLASSES,\n PHX_HAS_FOCUSED,\n PHX_HAS_SUBMITTED,\n PHX_MAIN,\n PHX_NO_FEEDBACK_CLASS,\n PHX_PARENT_ID,\n PHX_PRIVATE,\n PHX_REF,\n PHX_REF_SRC,\n PHX_ROOT_ID,\n PHX_SESSION,\n PHX_STATIC,\n PHX_UPLOAD_REF,\n PHX_VIEW_SELECTOR,\n PHX_STICKY,\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 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 maybeAddPrivateHooks(el, phxViewportTop, phxViewportBottom){\n if(el.hasAttribute && (el.hasAttribute(phxViewportTop) || el.hasAttribute(phxViewportBottom))){\n el.setAttribute(\"data-phx-hook\", \"Phoenix.InfiniteScroll\")\n }\n },\n\n isFeedbackContainer(el, phxFeedbackFor){\n return el.hasAttribute && el.hasAttribute(phxFeedbackFor)\n },\n\n maybeHideFeedback(container, feedbackContainers, phxFeedbackFor, phxFeedbackGroup){\n // because we can have multiple containers with the same phxFeedbackFor value\n // we perform the check only once and store the result;\n // we often have multiple containers, because we push both fromEl and toEl in dompatch\n // when a container is updated\n const feedbackResults = {}\n feedbackContainers.forEach(el => {\n // skip elements that are not in the DOM\n if(!container.contains(el)) return\n const feedback = el.getAttribute(phxFeedbackFor)\n if(!feedback){\n // the container previously had phx-feedback-for, but now it doesn't\n // remove the class from the container (if it exists)\n JS.addOrRemoveClasses(el, [], [PHX_NO_FEEDBACK_CLASS])\n return\n }\n if(feedbackResults[feedback] === true){\n this.hideFeedback(el)\n return\n }\n feedbackResults[feedback] = this.shouldHideFeedback(container, feedback, phxFeedbackGroup)\n if(feedbackResults[feedback] === true){\n this.hideFeedback(el)\n }\n })\n },\n\n hideFeedback(container){\n JS.addOrRemoveClasses(container, [PHX_NO_FEEDBACK_CLASS], [])\n },\n\n shouldHideFeedback(container, nameOrGroup, phxFeedbackGroup){\n const query = `[name=\"${nameOrGroup}\"],\n [name=\"${nameOrGroup}[]\"],\n [${phxFeedbackGroup}=\"${nameOrGroup}\"]`\n let focused = false\n DOM.all(container, query, (input) => {\n if(this.private(input, PHX_HAS_FOCUSED) || this.private(input, PHX_HAS_SUBMITTED)){\n focused = true\n }\n })\n return !focused\n },\n\n feedbackSelector(input, phxFeedbackFor, phxFeedbackGroup){\n let query = `[${phxFeedbackFor}=\"${input.name}\"],\n [${phxFeedbackFor}=\"${input.name.replace(/\\[\\]$/, \"\")}\"]`\n if(input.getAttribute(phxFeedbackGroup)){\n query += `,[${phxFeedbackFor}=\"${input.getAttribute(phxFeedbackGroup)}\"]`\n }\n return query\n },\n\n resetForm(form, phxFeedbackFor, phxFeedbackGroup){\n Array.from(form.elements).forEach(input => {\n let query = this.feedbackSelector(input, phxFeedbackFor, phxFeedbackGroup)\n this.deletePrivate(input, PHX_HAS_FOCUSED)\n this.deletePrivate(input, PHX_HAS_SUBMITTED)\n this.all(document, query, feedbackEl => {\n JS.addOrRemoveClasses(feedbackEl, [PHX_NO_FEEDBACK_CLASS], [])\n })\n })\n },\n\n showError(inputEl, phxFeedbackFor, phxFeedbackGroup){\n if(inputEl.name){\n let query = this.feedbackSelector(inputEl, phxFeedbackFor, phxFeedbackGroup)\n this.all(document, query, (el) => {\n JS.addOrRemoveClasses(el, [], [PHX_NO_FEEDBACK_CLASS])\n })\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_REF, PHX_REF_SRC].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 syncPendingRef(fromEl, toEl, disableWith){\n let ref = fromEl.getAttribute(PHX_REF)\n if(ref === null){ return true }\n let refSrc = fromEl.getAttribute(PHX_REF_SRC)\n\n if(DOM.isUploadInput(fromEl)){ DOM.mergeAttrs(fromEl, toEl, {isIgnored: true}) }\n DOM.putPrivate(fromEl, PHX_REF, toEl)\n return false\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 rect.top >= top(scrollContainer) && rect.left >= 0 && rect.top <= bottom(scrollContainer)\n}\n\nlet isAtViewportBottom = (el, scrollContainer) => {\n let rect = el.getBoundingClientRect()\n return rect.right >= top(scrollContainer) && rect.left >= 0 && rect.bottom <= bottom(scrollContainer)\n}\n\nlet isWithinViewport = (el, scrollContainer) => {\n let rect = el.getBoundingClientRect()\n return rect.top >= top(scrollContainer) && rect.left >= 0 && 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 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 //