From 01423d042942cd0fac33fd6ca98238b76107cc13 Mon Sep 17 00:00:00 2001 From: Chris McCord Date: Thu, 15 Feb 2024 15:32:45 -0500 Subject: [PATCH] Release 0.20.7 --- CHANGELOG.md | 7 +++++++ assets/package-lock.json | 4 ++-- assets/package.json | 2 +- mix.exs | 2 +- package.json | 2 +- priv/static/phoenix_live_view.cjs.js | 4 ++-- priv/static/phoenix_live_view.cjs.js.map | 4 ++-- priv/static/phoenix_live_view.esm.js | 4 ++-- priv/static/phoenix_live_view.esm.js.map | 4 ++-- priv/static/phoenix_live_view.js | 4 ++-- priv/static/phoenix_live_view.min.js | 2 +- 11 files changed, 23 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5438141de..2d88a02f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.20.7 (2024-02-15) + +### Bug fixes + * Fix phx-skip containers leaking into DOM on first patch in some cases (#3117) + * Fix phx-feedback-for failing to be properly updated in some cases (#3122) + + ## 0.20.6 (2024-02-14) ### Bug fixes diff --git a/assets/package-lock.json b/assets/package-lock.json index aed54df41..f9af460ef 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -1,12 +1,12 @@ { "name": "phoenix_live_view", - "version": "0.20.6", + "version": "0.20.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "phoenix_live_view", - "version": "0.20.6", + "version": "0.20.7", "license": "MIT", "dependencies": { "morphdom": "2.7.2" diff --git a/assets/package.json b/assets/package.json index 4b3d8d693..811cf6584 100644 --- a/assets/package.json +++ b/assets/package.json @@ -1,6 +1,6 @@ { "name": "phoenix_live_view", - "version": "0.20.6", + "version": "0.20.7", "description": "The Phoenix LiveView JavaScript client.", "license": "MIT", "repository": {}, diff --git a/mix.exs b/mix.exs index 03010cbb6..fc818f630 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule Phoenix.LiveView.MixProject do use Mix.Project - @version "0.20.6" + @version "0.20.7" def project do [ diff --git a/package.json b/package.json index 528094e98..86c850d9f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "phoenix_live_view", - "version": "0.20.6", + "version": "0.20.7", "description": "The Phoenix LiveView JavaScript client.", "license": "MIT", "module": "./priv/static/phoenix_live_view.esm.js", diff --git a/priv/static/phoenix_live_view.cjs.js b/priv/static/phoenix_live_view.cjs.js index 79c5c30cc..4d1634bcd 100644 --- a/priv/static/phoenix_live_view.cjs.js +++ b/priv/static/phoenix_live_view.cjs.js @@ -2178,8 +2178,6 @@ var DOMPatch = class { }, onBeforeNodeAdded: (el) => { dom_default.maybeAddPrivateHooks(el, phxViewportTop, phxViewportBottom); - if (dom_default.isFeedbackContainer(el, phxFeedbackFor)) - feedbackContainers.push(el); this.trackBefore("added", el); let morphedEl = el; if (!isJoinPatch && this.streamComponentRestore[el.id]) { @@ -2193,6 +2191,8 @@ var DOMPatch = class { if (el.getAttribute) { this.maybeReOrderStream(el, true); } + if (dom_default.isFeedbackContainer(el, phxFeedbackFor)) + feedbackContainers.push(el); if (el instanceof HTMLImageElement && el.srcset) { el.srcset = el.srcset; } else if (el instanceof HTMLVideoElement && el.autoplay) { diff --git a/priv/static/phoenix_live_view.cjs.js.map b/priv/static/phoenix_live_view.cjs.js.map index 32f3b508c..d6f6de38f 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 = null\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 = el || sourceEl)\n },\n\n exec_pop_focus(eventType, phxEvent, view, sourceEl, el){\n window.requestAnimationFrame(() => {\n if(focusStack){ focusStack.focus() }\n focusStack = null\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, 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 return wantsNewTab || isTargetBlank || isDownload\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 findParentCIDs(node, cids){\n let initial = new Set(cids)\n let parentCids =\n cids.reduce((acc, cid) => {\n let selector = `[${PHX_COMPONENT}=\"${cid}\"] [${PHX_COMPONENT}]`\n\n this.filterWithinSameLiveView(this.all(node, selector), node)\n .map(el => parseInt(el.getAttribute(PHX_COMPONENT)))\n .forEach(childCID => acc.delete(childCID))\n\n return acc\n }, initial)\n\n return parentCids.size === 0 ? new Set(cids) : 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\", () => callback())\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(focused.readOnly){ focused.blur() }\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.isFormInput(fromEl) || fromEl.getAttribute(disableWith) !== null){\n if(DOM.isUploadInput(fromEl)){ DOM.mergeAttrs(fromEl, toEl, {isIgnored: true}) }\n DOM.putPrivate(fromEl, PHX_REF, toEl)\n return false\n } else {\n PHX_EVENT_CLASSES.forEach(className => {\n fromEl.classList.contains(className) && toEl.classList.add(className)\n })\n toEl.setAttribute(PHX_REF, ref)\n toEl.setAttribute(PHX_REF_SRC, refSrc)\n return true\n }\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){\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\"\nimport DOM from \"./dom\"\n\nexport default class UploadEntry {\n static isActive(fileEl, file){\n let isNew = file._phxRef === undefined\n let isPreflightInProgress = UploadEntry.isPreflightInProgress(file)\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 || !isPreflightInProgress)\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){\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 }\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 cancel(){\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(!DOM.isAutoUpload(this.fileEl)){ LiveUploader.clearFiles(this.fileEl) }\n }\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){ this.cancel() }\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.putPrivate(inputEl, \"files\", this.activeFiles(inputEl).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.view = view\n this.onComplete = onComplete\n this._entries =\n Array.from(LiveUploader.filesAwaitingPreflight(inputEl) || [])\n .map(file => new UploadEntry(inputEl, file, view))\n\n // prevent sending duplicate preflight requests \n LiveUploader.markPreflightInProgress(this._entries)\n\n this.numEntriesInProgress = this._entries.length\n }\n\n entries(){ return this._entries }\n\n initAdapterUpload(resp, onError, liveSocket){\n this._entries =\n this._entries.map(entry => {\n entry.zipPostFlight(resp)\n entry.onDone(() => {\n this.numEntriesInProgress--\n if(this.numEntriesInProgress === 0){ this.onComplete() }\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 if([\"scroll\", \"auto\"].indexOf(getComputedStyle(el).overflowY) >= 0) return el\n if(document.documentElement === el) return null\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 //