From 7ca0e7c56a6164479b542716632e653453a0c6d6 Mon Sep 17 00:00:00 2001 From: Leif Metcalf Date: Thu, 21 Nov 2024 23:35:09 +1300 Subject: [PATCH] _ --- .formatter.exs | 2 +- assets/js/app.js | 28 +- assets/vendor/phoenix_live_view.js | 5479 ----------------- config/config.exs | 4 +- config/dev.exs | 1 + config/test.exs | 1 + lib/munch/osm.ex | 66 + lib/munch/osm/restaurant.ex | 12 + lib/munch/postgres_types.ex | 4 + lib/munch/restaurants.ex | 2 +- lib/munch/restaurants/restaurant.ex | 13 +- lib/munch_web/components/core_components.ex | 2 +- lib/munch_web/live/list_live/form.ex | 7 +- lib/munch_web/live/restaurant_live/edit.ex | 68 + lib/munch_web/live/restaurant_live/form.ex | 97 - lib/munch_web/live/restaurant_live/new.ex | 69 + .../live/restaurant_live/select_component.ex | 10 +- lib/munch_web/live/user_live/profile_form.ex | 10 +- lib/munch_web/router.ex | 4 +- mix.exs | 28 +- mix.lock | 28 +- osm/download_osm.exs | 43 + osm/style.lua | 34 + .../20241008001216_create_restaurants.exs | 12 +- ...0241008002020_create_users_auth_tables.exs | 6 +- .../20241008002021_create_lists.exs | 2 +- ...1112014159_create_featured_restaurants.exs | 4 +- priv/repo/seeds.exs | 40 - 28 files changed, 392 insertions(+), 5684 deletions(-) delete mode 100644 assets/vendor/phoenix_live_view.js create mode 100644 lib/munch/osm.ex create mode 100644 lib/munch/osm/restaurant.ex create mode 100644 lib/munch/postgres_types.ex create mode 100644 lib/munch_web/live/restaurant_live/edit.ex delete mode 100644 lib/munch_web/live/restaurant_live/form.ex create mode 100644 lib/munch_web/live/restaurant_live/new.ex create mode 100644 osm/download_osm.exs create mode 100644 osm/style.lua diff --git a/.formatter.exs b/.formatter.exs index ef8840c..66a0767 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -2,5 +2,5 @@ import_deps: [:ecto, :ecto_sql, :phoenix], subdirectories: ["priv/*/migrations"], plugins: [Phoenix.LiveView.HTMLFormatter], - inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"] + inputs: ["*.{heex,ex,exs}", "{config,lib,test,osm}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"] ] diff --git a/assets/js/app.js b/assets/js/app.js index 9168074..e390105 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -18,8 +18,8 @@ // Include phoenix_html to handle method=PUT/DELETE in forms and buttons. import "phoenix_html" // Establish Phoenix Socket and LiveView configuration. -import { Socket } from "../../../../" -import { LiveSocket } from "../vendor/phoenix_live_view" +import { Socket } from "phoenix" +import { LiveSocket } from "phoenix_live_view" import topbar from "../vendor/topbar" import Sortable from "../vendor/Sortable" @@ -39,17 +39,25 @@ let Hooks = { }); } + }, + Modal: { + mounted() { + this.handleEvent(`show-${this.el.id}`, () => { + this.el.showModal() + }) + this.handleEvent(`close-${this.el.id}`, () => { + this.el.close() + }) + this.el.addEventListener("munch:show-modal", () => { + this.el.showModal() + }) + this.el.addEventListener("munch:close-modal", () => { + this.el.close() + }) + } } } -window.addEventListener("munch:show-modal", (e) => { - e.target.showModal() -}) - -window.addEventListener("munch:close-modal", (e) => { - e.target.close() -}) - let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") let liveSocket = new LiveSocket("/live", Socket, { longPollFallbackMs: 2500, diff --git a/assets/vendor/phoenix_live_view.js b/assets/vendor/phoenix_live_view.js deleted file mode 100644 index 0f4ca5f..0000000 --- a/assets/vendor/phoenix_live_view.js +++ /dev/null @@ -1,5479 +0,0 @@ -// js/phoenix_live_view/constants.js -var CONSECUTIVE_RELOADS = "consecutive-reloads"; -var MAX_RELOADS = 10; -var RELOAD_JITTER_MIN = 5e3; -var RELOAD_JITTER_MAX = 1e4; -var FAILSAFE_JITTER = 3e4; -var PHX_EVENT_CLASSES = [ - "phx-click-loading", - "phx-change-loading", - "phx-submit-loading", - "phx-keydown-loading", - "phx-keyup-loading", - "phx-blur-loading", - "phx-focus-loading", - "phx-hook-loading" -]; -var PHX_COMPONENT = "data-phx-component"; -var PHX_LIVE_LINK = "data-phx-link"; -var PHX_TRACK_STATIC = "track-static"; -var PHX_LINK_STATE = "data-phx-link-state"; -var PHX_REF_LOADING = "data-phx-ref-loading"; -var PHX_REF_SRC = "data-phx-ref-src"; -var PHX_REF_LOCK = "data-phx-ref-lock"; -var PHX_TRACK_UPLOADS = "track-uploads"; -var PHX_UPLOAD_REF = "data-phx-upload-ref"; -var PHX_PREFLIGHTED_REFS = "data-phx-preflighted-refs"; -var PHX_DONE_REFS = "data-phx-done-refs"; -var PHX_DROP_TARGET = "drop-target"; -var PHX_ACTIVE_ENTRY_REFS = "data-phx-active-refs"; -var PHX_LIVE_FILE_UPDATED = "phx:live-file:updated"; -var PHX_SKIP = "data-phx-skip"; -var PHX_MAGIC_ID = "data-phx-id"; -var PHX_PRUNE = "data-phx-prune"; -var PHX_CONNECTED_CLASS = "phx-connected"; -var PHX_LOADING_CLASS = "phx-loading"; -var PHX_ERROR_CLASS = "phx-error"; -var PHX_CLIENT_ERROR_CLASS = "phx-client-error"; -var PHX_SERVER_ERROR_CLASS = "phx-server-error"; -var PHX_PARENT_ID = "data-phx-parent-id"; -var PHX_MAIN = "data-phx-main"; -var PHX_ROOT_ID = "data-phx-root-id"; -var PHX_VIEWPORT_TOP = "viewport-top"; -var PHX_VIEWPORT_BOTTOM = "viewport-bottom"; -var PHX_TRIGGER_ACTION = "trigger-action"; -var PHX_HAS_FOCUSED = "phx-has-focused"; -var FOCUSABLE_INPUTS = ["text", "textarea", "number", "email", "password", "search", "tel", "url", "date", "time", "datetime-local", "color", "range"]; -var CHECKABLE_INPUTS = ["checkbox", "radio"]; -var PHX_HAS_SUBMITTED = "phx-has-submitted"; -var PHX_SESSION = "data-phx-session"; -var PHX_VIEW_SELECTOR = `[${PHX_SESSION}]`; -var PHX_STICKY = "data-phx-sticky"; -var PHX_STATIC = "data-phx-static"; -var PHX_READONLY = "data-phx-readonly"; -var PHX_DISABLED = "data-phx-disabled"; -var PHX_DISABLE_WITH = "disable-with"; -var PHX_DISABLE_WITH_RESTORE = "data-phx-disable-with-restore"; -var PHX_HOOK = "hook"; -var PHX_DEBOUNCE = "debounce"; -var PHX_THROTTLE = "throttle"; -var PHX_UPDATE = "update"; -var PHX_STREAM = "stream"; -var PHX_STREAM_REF = "data-phx-stream"; -var PHX_KEY = "key"; -var PHX_PRIVATE = "phxPrivate"; -var PHX_AUTO_RECOVER = "auto-recover"; -var PHX_LV_DEBUG = "phx:live-socket:debug"; -var PHX_LV_PROFILE = "phx:live-socket:profiling"; -var PHX_LV_LATENCY_SIM = "phx:live-socket:latency-sim"; -var PHX_PROGRESS = "progress"; -var PHX_MOUNTED = "mounted"; -var PHX_RELOAD_STATUS = "__phoenix_reload_status__"; -var LOADER_TIMEOUT = 1; -var MAX_CHILD_JOIN_ATTEMPTS = 3; -var BEFORE_UNLOAD_LOADER_TIMEOUT = 200; -var BINDING_PREFIX = "phx-"; -var PUSH_TIMEOUT = 3e4; -var DEBOUNCE_TRIGGER = "debounce-trigger"; -var THROTTLED = "throttled"; -var DEBOUNCE_PREV_KEY = "debounce-prev-key"; -var DEFAULTS = { - debounce: 300, - throttle: 300 -}; -var PHX_PENDING_ATTRS = [PHX_REF_LOADING, PHX_REF_SRC, PHX_REF_LOCK]; -var DYNAMICS = "d"; -var STATIC = "s"; -var ROOT = "r"; -var COMPONENTS = "c"; -var EVENTS = "e"; -var REPLY = "r"; -var TITLE = "t"; -var TEMPLATES = "p"; -var STREAM = "stream"; - -// js/phoenix_live_view/entry_uploader.js -var EntryUploader = class { - constructor(entry, chunkSize, liveSocket) { - this.liveSocket = liveSocket; - this.entry = entry; - this.offset = 0; - this.chunkSize = chunkSize; - this.chunkTimer = null; - this.errored = false; - this.uploadChannel = liveSocket.channel(`lvu:${entry.ref}`, { token: entry.metadata() }); - } - error(reason) { - if (this.errored) { - return; - } - this.uploadChannel.leave(); - this.errored = true; - clearTimeout(this.chunkTimer); - this.entry.error(reason); - } - upload() { - this.uploadChannel.onError((reason) => this.error(reason)); - this.uploadChannel.join().receive("ok", (_data) => this.readNextChunk()).receive("error", (reason) => this.error(reason)); - } - isDone() { - return this.offset >= this.entry.file.size; - } - readNextChunk() { - let reader = new window.FileReader(); - let blob = this.entry.file.slice(this.offset, this.chunkSize + this.offset); - reader.onload = (e) => { - if (e.target.error === null) { - this.offset += e.target.result.byteLength; - this.pushChunk(e.target.result); - } else { - return logError("Read error: " + e.target.error); - } - }; - reader.readAsArrayBuffer(blob); - } - pushChunk(chunk) { - if (!this.uploadChannel.isJoined()) { - return; - } - this.uploadChannel.push("chunk", chunk).receive("ok", () => { - this.entry.progress(this.offset / this.entry.file.size * 100); - if (!this.isDone()) { - this.chunkTimer = setTimeout(() => this.readNextChunk(), this.liveSocket.getLatencySim() || 0); - } - }).receive("error", ({ reason }) => this.error(reason)); - } -}; - -// js/phoenix_live_view/utils.js -var logError = (msg, obj) => console.error && console.error(msg, obj); -var isCid = (cid) => { - let type = typeof cid; - return type === "number" || type === "string" && /^(0|[1-9]\d*)$/.test(cid); -}; -function detectDuplicateIds() { - 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)) { - console.error(`Multiple IDs detected: ${elems[i].id}. Ensure unique element ids.`); - } else { - ids.add(elems[i].id); - } - } -} -var debug = (view, kind, msg, obj) => { - if (view.liveSocket.isDebugEnabled()) { - console.log(`${view.id} ${kind}: ${msg} - `, obj); - } -}; -var closure = (val) => typeof val === "function" ? val : function() { - return val; -}; -var clone = (obj) => { - return JSON.parse(JSON.stringify(obj)); -}; -var closestPhxBinding = (el, binding, borderEl) => { - do { - if (el.matches(`[${binding}]`) && !el.disabled) { - return el; - } - el = el.parentElement || el.parentNode; - } while (el !== null && el.nodeType === 1 && !(borderEl && borderEl.isSameNode(el) || el.matches(PHX_VIEW_SELECTOR))); - return null; -}; -var isObject = (obj) => { - return obj !== null && typeof obj === "object" && !(obj instanceof Array); -}; -var isEqualObj = (obj1, obj2) => JSON.stringify(obj1) === JSON.stringify(obj2); -var isEmpty = (obj) => { - for (let x in obj) { - return false; - } - return true; -}; -var maybe = (el, callback) => el && callback(el); -var channelUploader = function(entries, onError, resp, liveSocket) { - entries.forEach((entry) => { - let entryUploader = new EntryUploader(entry, resp.config.chunk_size, liveSocket); - entryUploader.upload(); - }); -}; - -// js/phoenix_live_view/browser.js -var Browser = { - canPushState() { - return typeof history.pushState !== "undefined"; - }, - dropLocal(localStorage, namespace, subkey) { - return localStorage.removeItem(this.localKey(namespace, subkey)); - }, - updateLocal(localStorage, namespace, subkey, initial, func) { - let current = this.getLocal(localStorage, namespace, subkey); - let key = this.localKey(namespace, subkey); - let newVal = current === null ? initial : func(current); - localStorage.setItem(key, JSON.stringify(newVal)); - return newVal; - }, - getLocal(localStorage, namespace, subkey) { - return JSON.parse(localStorage.getItem(this.localKey(namespace, subkey))); - }, - updateCurrentState(callback) { - if (!this.canPushState()) { - return; - } - history.replaceState(callback(history.state || {}), "", window.location.href); - }, - pushState(kind, meta, to) { - if (this.canPushState()) { - if (to !== window.location.href) { - if (meta.type == "redirect" && meta.scroll) { - let currentState = history.state || {}; - currentState.scroll = meta.scroll; - history.replaceState(currentState, "", window.location.href); - } - delete meta.scroll; - history[kind + "State"](meta, "", to || null); - window.requestAnimationFrame(() => { - let hashEl = this.getHashTargetEl(window.location.hash); - if (hashEl) { - hashEl.scrollIntoView(); - } else if (meta.type === "redirect") { - window.scroll(0, 0); - } - }); - } - } else { - this.redirect(to); - } - }, - setCookie(name, value, maxAgeSeconds) { - let expires = typeof maxAgeSeconds === "number" ? ` max-age=${maxAgeSeconds};` : ""; - document.cookie = `${name}=${value};${expires} path=/`; - }, - getCookie(name) { - return document.cookie.replace(new RegExp(`(?:(?:^|.*;s*)${name}s*=s*([^;]*).*$)|^.*$`), "$1"); - }, - deleteCookie(name) { - document.cookie = `${name}=; max-age=-1; path=/`; - }, - redirect(toURL, flash) { - if (flash) { - this.setCookie("__phoenix_flash__", flash, 60); - } - window.location = toURL; - }, - localKey(namespace, subkey) { - return `${namespace}-${subkey}`; - }, - getHashTargetEl(maybeHash) { - let hash = maybeHash.toString().substring(1); - if (hash === "") { - return; - } - return document.getElementById(hash) || document.querySelector(`a[name="${hash}"]`); - } -}; -var browser_default = Browser; - -// js/phoenix_live_view/aria.js -var ARIA = { - anyOf(instance, classes) { - return classes.find((name) => instance instanceof name); - }, - isFocusable(el, interactiveOnly) { - return el instanceof HTMLAnchorElement && el.rel !== "ignore" || el instanceof HTMLAreaElement && el.href !== void 0 || !el.disabled && this.anyOf(el, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement, HTMLButtonElement]) || el instanceof HTMLIFrameElement || (el.tabIndex > 0 || !interactiveOnly && el.getAttribute("tabindex") !== null && el.getAttribute("aria-hidden") !== "true"); - }, - attemptFocus(el, interactiveOnly) { - if (this.isFocusable(el, interactiveOnly)) { - try { - el.focus(); - } catch (e) { - } - } - return !!document.activeElement && document.activeElement.isSameNode(el); - }, - focusFirstInteractive(el) { - let child = el.firstElementChild; - while (child) { - if (this.attemptFocus(child, true) || this.focusFirstInteractive(child, true)) { - return true; - } - child = child.nextElementSibling; - } - }, - focusFirst(el) { - let child = el.firstElementChild; - while (child) { - if (this.attemptFocus(child) || this.focusFirst(child)) { - return true; - } - child = child.nextElementSibling; - } - }, - focusLast(el) { - let child = el.lastElementChild; - while (child) { - if (this.attemptFocus(child) || this.focusLast(child)) { - return true; - } - child = child.previousElementSibling; - } - } -}; -var aria_default = ARIA; - -// js/phoenix_live_view/js.js -var focusStack = []; -var default_transition_time = 200; -var JS = { - // private - exec(e, eventType, phxEvent, view, sourceEl, defaults) { - let [defaultKind, defaultArgs] = defaults || [null, { callback: defaults && defaults.callback }]; - let commands = phxEvent.charAt(0) === "[" ? JSON.parse(phxEvent) : [[defaultKind, defaultArgs]]; - commands.forEach(([kind, args]) => { - if (kind === defaultKind) { - args = { ...defaultArgs, ...args }; - args.callback = args.callback || defaultArgs.callback; - } - this.filterToEls(view.liveSocket, sourceEl, args).forEach((el) => { - this[`exec_${kind}`](e, eventType, phxEvent, view, sourceEl, el, args); - }); - }); - }, - 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(e, eventType, phxEvent, view, sourceEl, el, { attr, to }) { - let nodes = to ? dom_default.all(document, to) : [sourceEl]; - nodes.forEach((node) => { - let encodedJS = node.getAttribute(attr); - if (!encodedJS) { - throw new Error(`expected ${attr} to contain JS command on "${to}"`); - } - view.liveSocket.execJS(node, encodedJS, eventType); - }); - }, - exec_dispatch(e, eventType, phxEvent, view, sourceEl, el, { to, event, detail, bubbles }) { - detail = detail || {}; - detail.dispatcher = sourceEl; - dom_default.dispatchEvent(el, event, { detail, bubbles }); - }, - exec_push(e, eventType, phxEvent, view, sourceEl, el, args) { - let { event, data, target, page_loading, loading, value, dispatcher, callback } = args; - let pushOpts = { loading, value, target, page_loading: !!page_loading }; - let targetSrc = eventType === "change" && dispatcher ? dispatcher : sourceEl; - let phxTarget = target || targetSrc.getAttribute(view.binding("target")) || targetSrc; - view.withinTargets(phxTarget, (targetView, targetCtx) => { - if (!targetView.isConnected()) { - return; - } - if (eventType === "change") { - let { newCid, _target } = args; - _target = _target || (dom_default.isFormInput(sourceEl) ? sourceEl.name : void 0); - if (_target) { - pushOpts._target = _target; - } - targetView.pushInput(sourceEl, targetCtx, newCid, event || phxEvent, pushOpts, callback); - } else if (eventType === "submit") { - let { submitter } = args; - targetView.submitForm(sourceEl, targetCtx, event || phxEvent, submitter, pushOpts, callback); - } else { - targetView.pushEvent(eventType, sourceEl, targetCtx, event || phxEvent, data, pushOpts, callback); - } - }); - }, - exec_navigate(e, eventType, phxEvent, view, sourceEl, el, { href, replace }) { - view.liveSocket.historyRedirect(e, href, replace ? "replace" : "push", null, sourceEl); - }, - exec_patch(e, eventType, phxEvent, view, sourceEl, el, { href, replace }) { - view.liveSocket.pushHistoryPatch(e, href, replace ? "replace" : "push", sourceEl); - }, - exec_focus(e, eventType, phxEvent, view, sourceEl, el) { - window.requestAnimationFrame(() => aria_default.attemptFocus(el)); - }, - exec_focus_first(e, eventType, phxEvent, view, sourceEl, el) { - window.requestAnimationFrame(() => aria_default.focusFirstInteractive(el) || aria_default.focusFirst(el)); - }, - exec_push_focus(e, eventType, phxEvent, view, sourceEl, el) { - window.requestAnimationFrame(() => focusStack.push(el || sourceEl)); - }, - exec_pop_focus(e, eventType, phxEvent, view, sourceEl, el) { - window.requestAnimationFrame(() => { - const el2 = focusStack.pop(); - if (el2) { - el2.focus(); - } - }); - }, - exec_add_class(e, eventType, phxEvent, view, sourceEl, el, { names, transition, time, blocking }) { - this.addOrRemoveClasses(el, names, [], transition, time, view, blocking); - }, - exec_remove_class(e, eventType, phxEvent, view, sourceEl, el, { names, transition, time, blocking }) { - this.addOrRemoveClasses(el, [], names, transition, time, view, blocking); - }, - exec_toggle_class(e, eventType, phxEvent, view, sourceEl, el, { to, names, transition, time, blocking }) { - this.toggleClasses(el, names, transition, time, view, blocking); - }, - exec_toggle_attr(e, eventType, phxEvent, view, sourceEl, el, { attr: [attr, val1, val2] }) { - this.toggleAttr(el, attr, val1, val2); - }, - exec_transition(e, eventType, phxEvent, view, sourceEl, el, { time, transition, blocking }) { - this.addOrRemoveClasses(el, [], [], transition, time, view, blocking); - }, - exec_toggle(e, eventType, phxEvent, view, sourceEl, el, { display, ins, outs, time, blocking }) { - this.toggle(eventType, view, el, display, ins, outs, time, blocking); - }, - exec_show(e, eventType, phxEvent, view, sourceEl, el, { display, transition, time, blocking }) { - this.show(eventType, view, el, display, transition, time, blocking); - }, - exec_hide(e, eventType, phxEvent, view, sourceEl, el, { display, transition, time, blocking }) { - this.hide(eventType, view, el, display, transition, time, blocking); - }, - exec_set_attr(e, eventType, phxEvent, view, sourceEl, el, { attr: [attr, val] }) { - this.setOrRemoveAttrs(el, [[attr, val]], []); - }, - exec_remove_attr(e, eventType, phxEvent, view, sourceEl, el, { attr }) { - this.setOrRemoveAttrs(el, [], [attr]); - }, - // utils for commands - show(eventType, view, el, display, transition, time, blocking) { - if (!this.isVisible(el)) { - this.toggle(eventType, view, el, display, transition, null, time, blocking); - } - }, - hide(eventType, view, el, display, transition, time, blocking) { - if (this.isVisible(el)) { - this.toggle(eventType, view, el, display, null, transition, time, blocking); - } - }, - toggle(eventType, view, el, display, ins, outs, time, blocking) { - time = time || default_transition_time; - let [inClasses, inStartClasses, inEndClasses] = ins || [[], [], []]; - let [outClasses, outStartClasses, outEndClasses] = outs || [[], [], []]; - if (inClasses.length > 0 || outClasses.length > 0) { - if (this.isVisible(el)) { - let onStart = () => { - this.addOrRemoveClasses(el, outStartClasses, inClasses.concat(inStartClasses).concat(inEndClasses)); - window.requestAnimationFrame(() => { - this.addOrRemoveClasses(el, outClasses, []); - window.requestAnimationFrame(() => this.addOrRemoveClasses(el, outEndClasses, outStartClasses)); - }); - }; - let onEnd = () => { - this.addOrRemoveClasses(el, [], outClasses.concat(outEndClasses)); - dom_default.putSticky(el, "toggle", (currentEl) => currentEl.style.display = "none"); - el.dispatchEvent(new Event("phx:hide-end")); - }; - el.dispatchEvent(new Event("phx:hide-start")); - if (blocking === false) { - onStart(); - setTimeout(onEnd, time); - } else { - view.transition(time, onStart, onEnd); - } - } else { - if (eventType === "remove") { - return; - } - let onStart = () => { - this.addOrRemoveClasses(el, inStartClasses, outClasses.concat(outStartClasses).concat(outEndClasses)); - let stickyDisplay = display || this.defaultDisplay(el); - dom_default.putSticky(el, "toggle", (currentEl) => currentEl.style.display = stickyDisplay); - window.requestAnimationFrame(() => { - this.addOrRemoveClasses(el, inClasses, []); - window.requestAnimationFrame(() => this.addOrRemoveClasses(el, inEndClasses, inStartClasses)); - }); - }; - let onEnd = () => { - this.addOrRemoveClasses(el, [], inClasses.concat(inEndClasses)); - el.dispatchEvent(new Event("phx:show-end")); - }; - el.dispatchEvent(new Event("phx:show-start")); - if (blocking === false) { - onStart(); - setTimeout(onEnd, time); - } else { - view.transition(time, onStart, onEnd); - } - } - } else { - if (this.isVisible(el)) { - window.requestAnimationFrame(() => { - el.dispatchEvent(new Event("phx:hide-start")); - dom_default.putSticky(el, "toggle", (currentEl) => currentEl.style.display = "none"); - el.dispatchEvent(new Event("phx:hide-end")); - }); - } else { - window.requestAnimationFrame(() => { - el.dispatchEvent(new Event("phx:show-start")); - let stickyDisplay = display || this.defaultDisplay(el); - dom_default.putSticky(el, "toggle", (currentEl) => currentEl.style.display = stickyDisplay); - el.dispatchEvent(new Event("phx:show-end")); - }); - } - } - }, - toggleClasses(el, classes, transition, time, view, blocking) { - window.requestAnimationFrame(() => { - let [prevAdds, prevRemoves] = dom_default.getSticky(el, "classes", [[], []]); - let newAdds = classes.filter((name) => prevAdds.indexOf(name) < 0 && !el.classList.contains(name)); - let newRemoves = classes.filter((name) => prevRemoves.indexOf(name) < 0 && el.classList.contains(name)); - this.addOrRemoveClasses(el, newAdds, newRemoves, transition, time, view, blocking); - }); - }, - toggleAttr(el, attr, val1, val2) { - if (el.hasAttribute(attr)) { - if (val2 !== void 0) { - if (el.getAttribute(attr) === val1) { - this.setOrRemoveAttrs(el, [[attr, val2]], []); - } else { - this.setOrRemoveAttrs(el, [[attr, val1]], []); - } - } else { - this.setOrRemoveAttrs(el, [], [attr]); - } - } else { - this.setOrRemoveAttrs(el, [[attr, val1]], []); - } - }, - addOrRemoveClasses(el, adds, removes, transition, time, view, blocking) { - time = time || default_transition_time; - let [transitionRun, transitionStart, transitionEnd] = transition || [[], [], []]; - if (transitionRun.length > 0) { - let onStart = () => { - this.addOrRemoveClasses(el, transitionStart, [].concat(transitionRun).concat(transitionEnd)); - window.requestAnimationFrame(() => { - this.addOrRemoveClasses(el, transitionRun, []); - window.requestAnimationFrame(() => this.addOrRemoveClasses(el, transitionEnd, transitionStart)); - }); - }; - let onDone = () => this.addOrRemoveClasses(el, adds.concat(transitionEnd), removes.concat(transitionRun).concat(transitionStart)); - if (blocking === false) { - onStart(); - setTimeout(onDone, time); - } else { - view.transition(time, onStart, onDone); - } - return; - } - window.requestAnimationFrame(() => { - let [prevAdds, prevRemoves] = dom_default.getSticky(el, "classes", [[], []]); - let keepAdds = adds.filter((name) => prevAdds.indexOf(name) < 0 && !el.classList.contains(name)); - let keepRemoves = removes.filter((name) => prevRemoves.indexOf(name) < 0 && el.classList.contains(name)); - let newAdds = prevAdds.filter((name) => removes.indexOf(name) < 0).concat(keepAdds); - let newRemoves = prevRemoves.filter((name) => adds.indexOf(name) < 0).concat(keepRemoves); - dom_default.putSticky(el, "classes", (currentEl) => { - currentEl.classList.remove(...newRemoves); - currentEl.classList.add(...newAdds); - return [newAdds, newRemoves]; - }); - }); - }, - setOrRemoveAttrs(el, sets, removes) { - let [prevSets, prevRemoves] = dom_default.getSticky(el, "attrs", [[], []]); - let alteredAttrs = sets.map(([attr, _val]) => attr).concat(removes); - let newSets = prevSets.filter(([attr, _val]) => !alteredAttrs.includes(attr)).concat(sets); - let newRemoves = prevRemoves.filter((attr) => !alteredAttrs.includes(attr)).concat(removes); - dom_default.putSticky(el, "attrs", (currentEl) => { - newRemoves.forEach((attr) => currentEl.removeAttribute(attr)); - newSets.forEach(([attr, val]) => currentEl.setAttribute(attr, val)); - return [newSets, newRemoves]; - }); - }, - hasAllClasses(el, classes) { - return classes.every((name) => el.classList.contains(name)); - }, - isToggledOut(el, outClasses) { - return !this.isVisible(el) || this.hasAllClasses(el, outClasses); - }, - filterToEls(liveSocket, sourceEl, { to }) { - let defaultQuery = () => { - if (typeof to === "string") { - return document.querySelectorAll(to); - } else if (to.closest) { - let toEl = sourceEl.closest(to.closest); - return toEl ? [toEl] : []; - } else if (to.inner) { - return sourceEl.querySelectorAll(to.inner); - } - }; - return to ? liveSocket.jsQuerySelectorAll(sourceEl, to, defaultQuery) : [sourceEl]; - }, - defaultDisplay(el) { - return { tr: "table-row", td: "table-cell" }[el.tagName.toLowerCase()] || "block"; - }, - transitionClasses(val) { - if (!val) { - return null; - } - let [trans, tStart, tEnd] = Array.isArray(val) ? val : [val.split(" "), [], []]; - trans = Array.isArray(trans) ? trans : trans.split(" "); - tStart = Array.isArray(tStart) ? tStart : tStart.split(" "); - tEnd = Array.isArray(tEnd) ? tEnd : tEnd.split(" "); - return [trans, tStart, tEnd]; - } -}; -var js_default = JS; - -// js/phoenix_live_view/dom.js -var DOM = { - byId(id) { - return document.getElementById(id) || logError(`no id found for ${id}`); - }, - removeClass(el, className) { - el.classList.remove(className); - if (el.classList.length === 0) { - el.removeAttribute("class"); - } - }, - all(node, query, callback) { - if (!node) { - return []; - } - let array = Array.from(node.querySelectorAll(query)); - return callback ? array.forEach(callback) : array; - }, - childNodeLength(html) { - let template = document.createElement("template"); - template.innerHTML = html; - return template.content.childElementCount; - }, - isUploadInput(el) { - return el.type === "file" && el.getAttribute(PHX_UPLOAD_REF) !== null; - }, - isAutoUpload(inputEl) { - return inputEl.hasAttribute("data-phx-auto-upload"); - }, - findUploadInputs(node) { - const formId = node.id; - const inputsOutsideForm = this.all(document, `input[type="file"][${PHX_UPLOAD_REF}][form="${formId}"]`); - return this.all(node, `input[type="file"][${PHX_UPLOAD_REF}]`).concat(inputsOutsideForm); - }, - findComponentNodeList(node, cid) { - return this.filterWithinSameLiveView(this.all(node, `[${PHX_COMPONENT}="${cid}"]`), node); - }, - isPhxDestroyed(node) { - return node.id && DOM.private(node, "destroyed") ? true : false; - }, - wantsNewTab(e) { - let wantsNewTab = e.ctrlKey || e.shiftKey || e.metaKey || e.button && e.button === 1; - let isDownload = e.target instanceof HTMLAnchorElement && e.target.hasAttribute("download"); - let isTargetBlank = e.target.hasAttribute("target") && e.target.getAttribute("target").toLowerCase() === "_blank"; - let isTargetNamedTab = e.target.hasAttribute("target") && !e.target.getAttribute("target").startsWith("_"); - return wantsNewTab || isTargetBlank || isDownload || isTargetNamedTab; - }, - isUnloadableFormSubmit(e) { - let isDialogSubmit = e.target && e.target.getAttribute("method") === "dialog" || e.submitter && e.submitter.getAttribute("formmethod") === "dialog"; - if (isDialogSubmit) { - return false; - } else { - return !e.defaultPrevented && !this.wantsNewTab(e); - } - }, - isNewPageClick(e, currentLocation) { - let href = e.target instanceof HTMLAnchorElement ? e.target.getAttribute("href") : null; - let url; - if (e.defaultPrevented || href === null || this.wantsNewTab(e)) { - return false; - } - if (href.startsWith("mailto:") || href.startsWith("tel:")) { - return false; - } - if (e.target.isContentEditable) { - return false; - } - try { - url = new URL(href); - } catch (e2) { - try { - url = new URL(href, currentLocation); - } catch (e3) { - return true; - } - } - if (url.host === currentLocation.host && url.protocol === currentLocation.protocol) { - if (url.pathname === currentLocation.pathname && url.search === currentLocation.search) { - return url.hash === "" && !url.href.endsWith("#"); - } - } - return url.protocol.startsWith("http"); - }, - markPhxChildDestroyed(el) { - if (this.isPhxChild(el)) { - el.setAttribute(PHX_SESSION, ""); - } - this.putPrivate(el, "destroyed", true); - }, - findPhxChildrenInFragment(html, parentId) { - let template = document.createElement("template"); - template.innerHTML = html; - return this.findPhxChildren(template.content, parentId); - }, - isIgnored(el, phxUpdate) { - return (el.getAttribute(phxUpdate) || el.getAttribute("data-phx-update")) === "ignore"; - }, - isPhxUpdate(el, phxUpdate, updateTypes) { - return el.getAttribute && updateTypes.indexOf(el.getAttribute(phxUpdate)) >= 0; - }, - findPhxSticky(el) { - return this.all(el, `[${PHX_STICKY}]`); - }, - findPhxChildren(el, parentId) { - return this.all(el, `${PHX_VIEW_SELECTOR}[${PHX_PARENT_ID}="${parentId}"]`); - }, - findExistingParentCIDs(node, cids) { - 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); - this.all(parent, `[${PHX_COMPONENT}]`).map((el) => parseInt(el.getAttribute(PHX_COMPONENT))).forEach((childCID) => childrenCids.add(childCID)); - }); - }); - childrenCids.forEach((childCid) => parentCids.delete(childCid)); - return parentCids; - }, - filterWithinSameLiveView(nodes, parent) { - if (parent.querySelector(PHX_VIEW_SELECTOR)) { - return nodes.filter((el) => this.withinSameLiveView(el, parent)); - } else { - return nodes; - } - }, - withinSameLiveView(node, parent) { - while (node = node.parentNode) { - if (node.isSameNode(parent)) { - return true; - } - if (node.getAttribute(PHX_SESSION) !== null) { - return false; - } - } - }, - private(el, key) { - return el[PHX_PRIVATE] && el[PHX_PRIVATE][key]; - }, - deletePrivate(el, key) { - el[PHX_PRIVATE] && delete el[PHX_PRIVATE][key]; - }, - putPrivate(el, key, value) { - if (!el[PHX_PRIVATE]) { - el[PHX_PRIVATE] = {}; - } - el[PHX_PRIVATE][key] = value; - }, - updatePrivate(el, key, defaultVal, updateFunc) { - let existing = this.private(el, key); - if (existing === void 0) { - this.putPrivate(el, key, updateFunc(defaultVal)); - } else { - this.putPrivate(el, key, updateFunc(existing)); - } - }, - syncPendingAttrs(fromEl, toEl) { - if (!fromEl.hasAttribute(PHX_REF_SRC)) { - return; - } - PHX_EVENT_CLASSES.forEach((className) => { - fromEl.classList.contains(className) && toEl.classList.add(className); - }); - PHX_PENDING_ATTRS.filter((attr) => fromEl.hasAttribute(attr)).forEach((attr) => { - toEl.setAttribute(attr, fromEl.getAttribute(attr)); - }); - }, - copyPrivates(target, source) { - if (source[PHX_PRIVATE]) { - target[PHX_PRIVATE] = source[PHX_PRIVATE]; - } - }, - putTitle(str) { - let titleEl = document.querySelector("title"); - if (titleEl) { - let { prefix, suffix } = titleEl.dataset; - document.title = `${prefix || ""}${str}${suffix || ""}`; - } else { - document.title = str; - } - }, - debounce(el, event, phxDebounce, defaultDebounce, phxThrottle, defaultThrottle, asyncFilter, callback) { - let debounce = el.getAttribute(phxDebounce); - let throttle = el.getAttribute(phxThrottle); - if (debounce === "") { - debounce = defaultDebounce; - } - if (throttle === "") { - throttle = defaultThrottle; - } - let value = debounce || throttle; - switch (value) { - case null: - return callback(); - case "blur": - if (this.once(el, "debounce-blur")) { - el.addEventListener("blur", () => { - if (asyncFilter()) { - callback(); - } - }); - } - return; - default: - let timeout = parseInt(value); - let trigger = () => throttle ? this.deletePrivate(el, THROTTLED) : callback(); - let currentCycle = this.incCycle(el, DEBOUNCE_TRIGGER, trigger); - if (isNaN(timeout)) { - return logError(`invalid throttle/debounce value: ${value}`); - } - if (throttle) { - let newKeyDown = false; - if (event.type === "keydown") { - let prevKey = this.private(el, DEBOUNCE_PREV_KEY); - this.putPrivate(el, DEBOUNCE_PREV_KEY, event.key); - newKeyDown = prevKey !== event.key; - } - if (!newKeyDown && this.private(el, THROTTLED)) { - return false; - } else { - callback(); - const t = setTimeout(() => { - if (asyncFilter()) { - this.triggerCycle(el, DEBOUNCE_TRIGGER); - } - }, timeout); - this.putPrivate(el, THROTTLED, t); - } - } else { - setTimeout(() => { - if (asyncFilter()) { - this.triggerCycle(el, DEBOUNCE_TRIGGER, currentCycle); - } - }, timeout); - } - let form = el.form; - if (form && this.once(form, "bind-debounce")) { - form.addEventListener("submit", () => { - Array.from(new FormData(form).entries(), ([name]) => { - let input = form.querySelector(`[name="${name}"]`); - this.incCycle(input, DEBOUNCE_TRIGGER); - this.deletePrivate(input, THROTTLED); - }); - }); - } - if (this.once(el, "bind-debounce")) { - el.addEventListener("blur", () => { - clearTimeout(this.private(el, THROTTLED)); - this.triggerCycle(el, DEBOUNCE_TRIGGER); - }); - } - } - }, - triggerCycle(el, key, currentCycle) { - let [cycle, trigger] = this.private(el, key); - if (!currentCycle) { - currentCycle = cycle; - } - if (currentCycle === cycle) { - this.incCycle(el, key); - trigger(); - } - }, - once(el, key) { - if (this.private(el, key) === true) { - return false; - } - this.putPrivate(el, key, true); - return true; - }, - incCycle(el, key, trigger = function() { - }) { - let [currentCycle] = this.private(el, key) || [0, trigger]; - currentCycle++; - this.putPrivate(el, key, [currentCycle, trigger]); - return currentCycle; - }, - // maintains or adds privately used hook information - // fromEl and toEl can be the same element in the case of a newly added node - // fromEl and toEl can be any HTML node type, so we need to check if it's an element node - maintainPrivateHooks(fromEl, toEl, phxViewportTop, phxViewportBottom) { - if (fromEl.hasAttribute && fromEl.hasAttribute("data-phx-hook") && !toEl.hasAttribute("data-phx-hook")) { - toEl.setAttribute("data-phx-hook", fromEl.getAttribute("data-phx-hook")); - } - if (toEl.hasAttribute && (toEl.hasAttribute(phxViewportTop) || toEl.hasAttribute(phxViewportBottom))) { - toEl.setAttribute("data-phx-hook", "Phoenix.InfiniteScroll"); - } - }, - putCustomElHook(el, hook) { - if (el.isConnected) { - el.setAttribute("data-phx-hook", ""); - } else { - console.error(` - hook attached to non-connected DOM element - ensure you are calling createHook within your connectedCallback. ${el.outerHTML} - `); - } - this.putPrivate(el, "custom-el-hook", hook); - }, - getCustomElHook(el) { - return this.private(el, "custom-el-hook"); - }, - isUsedInput(el) { - return el.nodeType === Node.ELEMENT_NODE && (this.private(el, PHX_HAS_FOCUSED) || this.private(el, PHX_HAS_SUBMITTED)); - }, - resetForm(form) { - Array.from(form.elements).forEach((input) => { - this.deletePrivate(input, PHX_HAS_FOCUSED); - this.deletePrivate(input, PHX_HAS_SUBMITTED); - }); - }, - isPhxChild(node) { - return node.getAttribute && node.getAttribute(PHX_PARENT_ID); - }, - isPhxSticky(node) { - return node.getAttribute && node.getAttribute(PHX_STICKY) !== null; - }, - isChildOfAny(el, parents) { - return !!parents.find((parent) => parent.contains(el)); - }, - firstPhxChild(el) { - return this.isPhxChild(el) ? el : this.all(el, `[${PHX_PARENT_ID}]`)[0]; - }, - dispatchEvent(target, name, opts = {}) { - let defaultBubble = true; - let isUploadTarget = target.nodeName === "INPUT" && target.type === "file"; - if (isUploadTarget && name === "click") { - defaultBubble = false; - } - let bubbles = opts.bubbles === void 0 ? defaultBubble : !!opts.bubbles; - let eventOpts = { bubbles, cancelable: true, detail: opts.detail || {} }; - let event = name === "click" ? new MouseEvent("click", eventOpts) : new CustomEvent(name, eventOpts); - target.dispatchEvent(event); - }, - cloneNode(node, html) { - if (typeof html === "undefined") { - return node.cloneNode(true); - } else { - let cloned = node.cloneNode(false); - cloned.innerHTML = html; - 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; - let sourceAttrs = source.attributes; - for (let i = sourceAttrs.length - 1; i >= 0; i--) { - let name = sourceAttrs[i].name; - if (!exclude.has(name)) { - const sourceValue = source.getAttribute(name); - if (target.getAttribute(name) !== sourceValue && (!isIgnored || isIgnored && name.startsWith("data-"))) { - target.setAttribute(name, sourceValue); - } - } else { - if (name === "value" && target.value === source.value) { - target.setAttribute("value", source.getAttribute(name)); - } - } - } - let targetAttrs = target.attributes; - for (let i = targetAttrs.length - 1; i >= 0; i--) { - let name = targetAttrs[i].name; - if (isIgnored) { - if (name.startsWith("data-") && !source.hasAttribute(name) && !PHX_PENDING_ATTRS.includes(name)) { - target.removeAttribute(name); - } - } else { - if (!source.hasAttribute(name)) { - target.removeAttribute(name); - } - } - } - }, - mergeFocusedInput(target, source) { - if (!(target instanceof HTMLSelectElement)) { - DOM.mergeAttrs(target, source, { exclude: ["value"] }); - } - if (source.readOnly) { - target.setAttribute("readonly", true); - } else { - target.removeAttribute("readonly"); - } - }, - hasSelectionRange(el) { - return el.setSelectionRange && (el.type === "text" || el.type === "textarea"); - }, - restoreFocus(focused, selectionStart, selectionEnd) { - if (focused instanceof HTMLSelectElement) { - focused.focus(); - } - if (!DOM.isTextualInput(focused)) { - return; - } - let wasFocused = focused.matches(":focus"); - if (!wasFocused) { - focused.focus(); - } - if (this.hasSelectionRange(focused)) { - focused.setSelectionRange(selectionStart, selectionEnd); - } - }, - isFormInput(el) { - return /^(?:input|select|textarea)$/i.test(el.tagName) && el.type !== "button"; - }, - syncAttrsToProps(el) { - if (el instanceof HTMLInputElement && CHECKABLE_INPUTS.indexOf(el.type.toLocaleLowerCase()) >= 0) { - el.checked = el.getAttribute("checked") !== null; - } - }, - isTextualInput(el) { - return FOCUSABLE_INPUTS.indexOf(el.type) >= 0; - }, - isNowTriggerFormExternal(el, phxTriggerExternal) { - return el.getAttribute && el.getAttribute(phxTriggerExternal) !== null; - }, - cleanChildNodes(container, phxUpdate) { - if (DOM.isPhxUpdate(container, phxUpdate, ["append", "prepend"])) { - let toRemove = []; - container.childNodes.forEach((childNode) => { - if (!childNode.id) { - let isEmptyTextNode = childNode.nodeType === Node.TEXT_NODE && childNode.nodeValue.trim() === ""; - if (!isEmptyTextNode && childNode.nodeType !== Node.COMMENT_NODE) { - logError(`only HTML element tags with an id are allowed inside containers with phx-update. - -removing illegal node: "${(childNode.outerHTML || childNode.nodeValue).trim()}" - -`); - } - toRemove.push(childNode); - } - }); - toRemove.forEach((childNode) => childNode.remove()); - } - }, - replaceRootContainer(container, tagName, attrs) { - 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])); - return container; - } else { - let newContainer = document.createElement(tagName); - Object.keys(attrs).forEach((attr) => newContainer.setAttribute(attr, attrs[attr])); - retainedAttrs.forEach((attr) => newContainer.setAttribute(attr, container.getAttribute(attr))); - newContainer.innerHTML = container.innerHTML; - container.replaceWith(newContainer); - return newContainer; - } - }, - getSticky(el, name, defaultVal) { - let op = (DOM.private(el, "sticky") || []).find(([existingName]) => name === existingName); - if (op) { - let [_name, _op, stashedResult] = op; - return stashedResult; - } else { - return typeof defaultVal === "function" ? defaultVal() : defaultVal; - } - }, - deleteSticky(el, name) { - this.updatePrivate(el, "sticky", [], (ops) => { - return ops.filter(([existingName, _]) => existingName !== name); - }); - }, - putSticky(el, name, op) { - let stashedResult = op(el); - this.updatePrivate(el, "sticky", [], (ops) => { - let existingIndex = ops.findIndex(([existingName]) => name === existingName); - if (existingIndex >= 0) { - ops[existingIndex] = [name, op, stashedResult]; - } else { - ops.push([name, op, stashedResult]); - } - return ops; - }); - }, - applyStickyOperations(el) { - let ops = DOM.private(el, "sticky"); - if (!ops) { - return; - } - ops.forEach(([name, op, _stashed]) => this.putSticky(el, name, op)); - } -}; -var dom_default = DOM; - -// js/phoenix_live_view/upload_entry.js -var UploadEntry = class { - static isActive(fileEl, file) { - let isNew = file._phxRef === void 0; - let activeRefs = fileEl.getAttribute(PHX_ACTIVE_ENTRY_REFS).split(","); - let isActive = activeRefs.indexOf(LiveUploader.genFileRef(file)) >= 0; - return file.size > 0 && (isNew || isActive); - } - static isPreflighted(fileEl, file) { - let preflightedRefs = fileEl.getAttribute(PHX_PREFLIGHTED_REFS).split(","); - let isPreflighted = preflightedRefs.indexOf(LiveUploader.genFileRef(file)) >= 0; - return isPreflighted && this.isActive(fileEl, file); - } - static isPreflightInProgress(file) { - return file._preflightInProgress === true; - } - static markPreflightInProgress(file) { - file._preflightInProgress = true; - } - constructor(fileEl, file, view, autoUpload) { - this.ref = LiveUploader.genFileRef(file); - this.fileEl = fileEl; - this.file = file; - this.view = view; - this.meta = null; - this._isCancelled = false; - this._isDone = false; - this._progress = 0; - this._lastProgressSent = -1; - this._onDone = function() { - }; - this._onElUpdated = this.onElUpdated.bind(this); - this.fileEl.addEventListener(PHX_LIVE_FILE_UPDATED, this._onElUpdated); - this.autoUpload = autoUpload; - } - metadata() { - return this.meta; - } - progress(progress) { - this._progress = Math.floor(progress); - if (this._progress > this._lastProgressSent) { - if (this._progress >= 100) { - this._progress = 100; - this._lastProgressSent = 100; - this._isDone = true; - this.view.pushFileProgress(this.fileEl, this.ref, 100, () => { - LiveUploader.untrackFile(this.fileEl, this.file); - this._onDone(); - }); - } else { - this._lastProgressSent = this._progress; - this.view.pushFileProgress(this.fileEl, this.ref, this._progress); - } - } - } - isCancelled() { - return this._isCancelled; - } - cancel() { - this.file._preflightInProgress = false; - this._isCancelled = true; - this._isDone = true; - this._onDone(); - } - isDone() { - return this._isDone; - } - error(reason = "failed") { - this.fileEl.removeEventListener(PHX_LIVE_FILE_UPDATED, this._onElUpdated); - this.view.pushFileProgress(this.fileEl, this.ref, { error: reason }); - if (!this.isAutoUpload()) { - LiveUploader.clearFiles(this.fileEl); - } - } - isAutoUpload() { - return this.autoUpload; - } - //private - onDone(callback) { - this._onDone = () => { - this.fileEl.removeEventListener(PHX_LIVE_FILE_UPDATED, this._onElUpdated); - callback(); - }; - } - onElUpdated() { - let activeRefs = this.fileEl.getAttribute(PHX_ACTIVE_ENTRY_REFS).split(","); - if (activeRefs.indexOf(this.ref) === -1) { - LiveUploader.untrackFile(this.fileEl, this.file); - this.cancel(); - } - } - toPreflightPayload() { - return { - last_modified: this.file.lastModified, - name: this.file.name, - relative_path: this.file.webkitRelativePath, - size: this.file.size, - type: this.file.type, - ref: this.ref, - meta: typeof this.file.meta === "function" ? this.file.meta() : void 0 - }; - } - uploader(uploaders) { - if (this.meta.uploader) { - let callback = uploaders[this.meta.uploader] || logError(`no uploader configured for ${this.meta.uploader}`); - return { name: this.meta.uploader, callback }; - } else { - return { name: "channel", callback: channelUploader }; - } - } - zipPostFlight(resp) { - this.meta = resp.entries[this.ref]; - if (!this.meta) { - logError(`no preflight upload response returned with ref ${this.ref}`, { input: this.fileEl, response: resp }); - } - } -}; - -// js/phoenix_live_view/live_uploader.js -var liveUploaderFileRef = 0; -var LiveUploader = class _LiveUploader { - static genFileRef(file) { - let ref = file._phxRef; - if (ref !== void 0) { - return ref; - } else { - file._phxRef = (liveUploaderFileRef++).toString(); - return file._phxRef; - } - } - static getEntryDataURL(inputEl, ref, callback) { - let file = this.activeFiles(inputEl).find((file2) => this.genFileRef(file2) === ref); - callback(URL.createObjectURL(file)); - } - static hasUploadsInProgress(formEl) { - let active = 0; - dom_default.findUploadInputs(formEl).forEach((input) => { - if (input.getAttribute(PHX_PREFLIGHTED_REFS) !== input.getAttribute(PHX_DONE_REFS)) { - active++; - } - }); - return active > 0; - } - static serializeUploads(inputEl) { - let files = this.activeFiles(inputEl); - let fileData = {}; - files.forEach((file) => { - let entry = { path: inputEl.name }; - let uploadRef = inputEl.getAttribute(PHX_UPLOAD_REF); - fileData[uploadRef] = fileData[uploadRef] || []; - entry.ref = this.genFileRef(file); - entry.last_modified = file.lastModified; - entry.name = file.name || entry.ref; - entry.relative_path = file.webkitRelativePath; - entry.type = file.type; - entry.size = file.size; - if (typeof file.meta === "function") { - entry.meta = file.meta(); - } - fileData[uploadRef].push(entry); - }); - return fileData; - } - static clearFiles(inputEl) { - inputEl.value = null; - inputEl.removeAttribute(PHX_UPLOAD_REF); - dom_default.putPrivate(inputEl, "files", []); - } - static untrackFile(inputEl, file) { - dom_default.putPrivate(inputEl, "files", dom_default.private(inputEl, "files").filter((f) => !Object.is(f, file))); - } - static trackFiles(inputEl, files, dataTransfer) { - if (inputEl.getAttribute("multiple") !== null) { - let newFiles = files.filter((file) => !this.activeFiles(inputEl).find((f) => Object.is(f, file))); - dom_default.updatePrivate(inputEl, "files", [], (existing) => existing.concat(newFiles)); - inputEl.value = null; - } else { - if (dataTransfer && dataTransfer.files.length > 0) { - inputEl.files = dataTransfer.files; - } - dom_default.putPrivate(inputEl, "files", files); - } - } - static activeFileInputs(formEl) { - let fileInputs = dom_default.findUploadInputs(formEl); - return Array.from(fileInputs).filter((el) => el.files && this.activeFiles(el).length > 0); - } - static activeFiles(input) { - return (dom_default.private(input, "files") || []).filter((f) => UploadEntry.isActive(input, f)); - } - static inputsAwaitingPreflight(formEl) { - let fileInputs = dom_default.findUploadInputs(formEl); - return Array.from(fileInputs).filter((input) => this.filesAwaitingPreflight(input).length > 0); - } - static filesAwaitingPreflight(input) { - return this.activeFiles(input).filter((f) => !UploadEntry.isPreflighted(input, f) && !UploadEntry.isPreflightInProgress(f)); - } - static markPreflightInProgress(entries) { - entries.forEach((entry) => UploadEntry.markPreflightInProgress(entry.file)); - } - constructor(inputEl, view, onComplete) { - 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.numEntriesInProgress = this._entries.length; - } - isAutoUpload() { - return this.autoUpload; - } - entries() { - return this._entries; - } - initAdapterUpload(resp, onError, liveSocket) { - this._entries = this._entries.map((entry) => { - if (entry.isCancelled()) { - this.numEntriesInProgress--; - if (this.numEntriesInProgress === 0) { - this.onComplete(); - } - } else { - entry.zipPostFlight(resp); - entry.onDone(() => { - this.numEntriesInProgress--; - if (this.numEntriesInProgress === 0) { - this.onComplete(); - } - }); - } - return entry; - }); - let groupedEntries = this._entries.reduce((acc, entry) => { - if (!entry.meta) { - return acc; - } - let { name, callback } = entry.uploader(liveSocket.uploaders); - acc[name] = acc[name] || { callback, entries: [] }; - acc[name].entries.push(entry); - return acc; - }, {}); - for (let name in groupedEntries) { - let { callback, entries } = groupedEntries[name]; - callback(entries, onError, resp, liveSocket); - } - } -}; - -// js/phoenix_live_view/hooks.js -var Hooks = { - LiveFileUpload: { - activeRefs() { - return this.el.getAttribute(PHX_ACTIVE_ENTRY_REFS); - }, - preflightedRefs() { - return this.el.getAttribute(PHX_PREFLIGHTED_REFS); - }, - mounted() { - this.preflightedWas = this.preflightedRefs(); - }, - updated() { - let newPreflights = this.preflightedRefs(); - if (this.preflightedWas !== newPreflights) { - this.preflightedWas = newPreflights; - if (newPreflights === "") { - this.__view().cancelSubmit(this.el.form); - } - } - if (this.activeRefs() === "") { - this.el.value = null; - } - this.el.dispatchEvent(new CustomEvent(PHX_LIVE_FILE_UPDATED)); - } - }, - LiveImgPreview: { - mounted() { - this.ref = this.el.getAttribute("data-phx-entry-ref"); - this.inputEl = document.getElementById(this.el.getAttribute(PHX_UPLOAD_REF)); - LiveUploader.getEntryDataURL(this.inputEl, this.ref, (url) => { - this.url = url; - this.el.src = url; - }); - }, - destroyed() { - URL.revokeObjectURL(this.url); - } - }, - FocusWrap: { - mounted() { - this.focusStart = this.el.firstElementChild; - this.focusEnd = this.el.lastElementChild; - this.focusStart.addEventListener("focus", () => aria_default.focusLast(this.el)); - this.focusEnd.addEventListener("focus", () => aria_default.focusFirst(this.el)); - this.el.addEventListener("phx:show-end", () => this.el.focus()); - if (window.getComputedStyle(this.el).display !== "none") { - aria_default.focusFirst(this.el); - } - } - } -}; -var findScrollContainer = (el) => { - if (["HTML", "BODY"].indexOf(el.nodeName.toUpperCase()) >= 0) - return null; - if (["scroll", "auto"].indexOf(getComputedStyle(el).overflowY) >= 0) - return el; - return findScrollContainer(el.parentElement); -}; -var scrollTop = (scrollContainer) => { - if (scrollContainer) { - return scrollContainer.scrollTop; - } else { - return document.documentElement.scrollTop || document.body.scrollTop; - } -}; -var bottom = (scrollContainer) => { - if (scrollContainer) { - return scrollContainer.getBoundingClientRect().bottom; - } else { - return window.innerHeight || document.documentElement.clientHeight; - } -}; -var top = (scrollContainer) => { - if (scrollContainer) { - return scrollContainer.getBoundingClientRect().top; - } else { - return 0; - } -}; -var isAtViewportTop = (el, scrollContainer) => { - let rect = el.getBoundingClientRect(); - return Math.ceil(rect.top) >= top(scrollContainer) && Math.ceil(rect.left) >= 0 && Math.floor(rect.top) <= bottom(scrollContainer); -}; -var isAtViewportBottom = (el, scrollContainer) => { - let rect = el.getBoundingClientRect(); - return Math.ceil(rect.bottom) >= top(scrollContainer) && Math.ceil(rect.left) >= 0 && Math.floor(rect.bottom) <= bottom(scrollContainer); -}; -var isWithinViewport = (el, scrollContainer) => { - let rect = el.getBoundingClientRect(); - return Math.ceil(rect.top) >= top(scrollContainer) && Math.ceil(rect.left) >= 0 && Math.floor(rect.top) <= bottom(scrollContainer); -}; -Hooks.InfiniteScroll = { - mounted() { - this.scrollContainer = findScrollContainer(this.el); - let scrollBefore = scrollTop(this.scrollContainer); - let topOverran = false; - let throttleInterval = 500; - let pendingOp = null; - let onTopOverrun = this.throttle(throttleInterval, (topEvent, firstChild) => { - pendingOp = () => true; - this.liveSocket.execJSHookPush(this.el, topEvent, { id: firstChild.id, _overran: true }, () => { - pendingOp = null; - }); - }); - let onFirstChildAtTop = this.throttle(throttleInterval, (topEvent, firstChild) => { - pendingOp = () => firstChild.scrollIntoView({ block: "start" }); - this.liveSocket.execJSHookPush(this.el, topEvent, { id: firstChild.id }, () => { - pendingOp = null; - window.requestAnimationFrame(() => { - if (!isWithinViewport(firstChild, this.scrollContainer)) { - firstChild.scrollIntoView({ block: "start" }); - } - }); - }); - }); - let onLastChildAtBottom = this.throttle(throttleInterval, (bottomEvent, lastChild) => { - pendingOp = () => lastChild.scrollIntoView({ block: "end" }); - this.liveSocket.execJSHookPush(this.el, bottomEvent, { id: lastChild.id }, () => { - pendingOp = null; - window.requestAnimationFrame(() => { - if (!isWithinViewport(lastChild, this.scrollContainer)) { - lastChild.scrollIntoView({ block: "end" }); - } - }); - }); - }); - this.onScroll = (_e) => { - let scrollNow = scrollTop(this.scrollContainer); - if (pendingOp) { - scrollBefore = scrollNow; - return pendingOp(); - } - let rect = this.el.getBoundingClientRect(); - let topEvent = this.el.getAttribute(this.liveSocket.binding("viewport-top")); - let bottomEvent = this.el.getAttribute(this.liveSocket.binding("viewport-bottom")); - let lastChild = this.el.lastElementChild; - let firstChild = this.el.firstElementChild; - let isScrollingUp = scrollNow < scrollBefore; - let isScrollingDown = scrollNow > scrollBefore; - if (isScrollingUp && topEvent && !topOverran && rect.top >= 0) { - topOverran = true; - onTopOverrun(topEvent, firstChild); - } else if (isScrollingDown && topOverran && rect.top <= 0) { - topOverran = false; - } - if (topEvent && isScrollingUp && isAtViewportTop(firstChild, this.scrollContainer)) { - onFirstChildAtTop(topEvent, firstChild); - } else if (bottomEvent && isScrollingDown && isAtViewportBottom(lastChild, this.scrollContainer)) { - onLastChildAtBottom(bottomEvent, lastChild); - } - scrollBefore = scrollNow; - }; - if (this.scrollContainer) { - this.scrollContainer.addEventListener("scroll", this.onScroll); - } else { - window.addEventListener("scroll", this.onScroll); - } - }, - destroyed() { - if (this.scrollContainer) { - this.scrollContainer.removeEventListener("scroll", this.onScroll); - } else { - window.removeEventListener("scroll", this.onScroll); - } - }, - throttle(interval, callback) { - let lastCallAt = 0; - let timer; - return (...args) => { - let now = Date.now(); - let remainingTime = interval - (now - lastCallAt); - if (remainingTime <= 0 || remainingTime > interval) { - if (timer) { - clearTimeout(timer); - timer = null; - } - lastCallAt = now; - callback(...args); - } else if (!timer) { - timer = setTimeout(() => { - lastCallAt = Date.now(); - timer = null; - callback(...args); - }, remainingTime); - } - }; - } -}; -var hooks_default = Hooks; - -// js/phoenix_live_view/element_ref.js -var ElementRef = class { - constructor(el) { - this.el = el; - this.loadingRef = el.hasAttribute(PHX_REF_LOADING) ? parseInt(el.getAttribute(PHX_REF_LOADING), 10) : null; - this.lockRef = el.hasAttribute(PHX_REF_LOCK) ? parseInt(el.getAttribute(PHX_REF_LOCK), 10) : null; - } - // public - maybeUndo(ref, phxEvent, eachCloneCallback) { - if (!this.isWithin(ref)) { - return; - } - this.undoLocks(ref, phxEvent, eachCloneCallback); - this.undoLoading(ref, phxEvent); - if (this.isFullyResolvedBy(ref)) { - this.el.removeAttribute(PHX_REF_SRC); - } - } - // private - isWithin(ref) { - return !(this.loadingRef !== null && this.loadingRef > ref && (this.lockRef !== null && this.lockRef > ref)); - } - // Check for cloned PHX_REF_LOCK element that has been morphed behind - // the scenes while this element was locked in the DOM. - // When we apply the cloned tree to the active DOM element, we must - // - // 1. execute pending mounted hooks for nodes now in the DOM - // 2. undo any ref inside the cloned tree that has since been ack'd - undoLocks(ref, phxEvent, eachCloneCallback) { - if (!this.isLockUndoneBy(ref)) { - return; - } - let clonedTree = dom_default.private(this.el, PHX_REF_LOCK); - if (clonedTree) { - eachCloneCallback(clonedTree); - dom_default.deletePrivate(this.el, PHX_REF_LOCK); - } - this.el.removeAttribute(PHX_REF_LOCK); - let opts = { detail: { ref, event: phxEvent }, bubbles: true, cancelable: false }; - this.el.dispatchEvent(new CustomEvent(`phx:undo-lock:${this.lockRef}`, opts)); - } - undoLoading(ref, phxEvent) { - if (!this.isLoadingUndoneBy(ref)) { - if (this.canUndoLoading(ref) && this.el.classList.contains("phx-submit-loading")) { - this.el.classList.remove("phx-change-loading"); - } - return; - } - if (this.canUndoLoading(ref)) { - this.el.removeAttribute(PHX_REF_LOADING); - let disabledVal = this.el.getAttribute(PHX_DISABLED); - let readOnlyVal = this.el.getAttribute(PHX_READONLY); - if (readOnlyVal !== null) { - this.el.readOnly = readOnlyVal === "true" ? true : false; - this.el.removeAttribute(PHX_READONLY); - } - if (disabledVal !== null) { - this.el.disabled = disabledVal === "true" ? true : false; - this.el.removeAttribute(PHX_DISABLED); - } - let disableRestore = this.el.getAttribute(PHX_DISABLE_WITH_RESTORE); - if (disableRestore !== null) { - this.el.innerText = disableRestore; - this.el.removeAttribute(PHX_DISABLE_WITH_RESTORE); - } - let opts = { detail: { ref, event: phxEvent }, bubbles: true, cancelable: false }; - this.el.dispatchEvent(new CustomEvent(`phx:undo-loading:${this.loadingRef}`, opts)); - } - PHX_EVENT_CLASSES.forEach((name) => { - if (name !== "phx-submit-loading" || this.canUndoLoading(ref)) { - dom_default.removeClass(this.el, name); - } - }); - } - isLoadingUndoneBy(ref) { - return this.loadingRef === null ? false : this.loadingRef <= ref; - } - isLockUndoneBy(ref) { - return this.lockRef === null ? false : this.lockRef <= ref; - } - isFullyResolvedBy(ref) { - return (this.loadingRef === null || this.loadingRef <= ref) && (this.lockRef === null || this.lockRef <= ref); - } - // only remove the phx-submit-loading class if we are not locked - canUndoLoading(ref) { - return this.lockRef === null || this.lockRef <= ref; - } -}; - -// js/phoenix_live_view/dom_post_morph_restorer.js -var DOMPostMorphRestorer = class { - constructor(containerBefore, containerAfter, updateType) { - let idsBefore = /* @__PURE__ */ new Set(); - let idsAfter = new Set([...containerAfter.children].map((child) => child.id)); - let elementsToModify = []; - Array.from(containerBefore.children).forEach((child) => { - if (child.id) { - idsBefore.add(child.id); - if (idsAfter.has(child.id)) { - let previousElementId = child.previousElementSibling && child.previousElementSibling.id; - elementsToModify.push({ elementId: child.id, previousElementId }); - } - } - }); - this.containerId = containerAfter.id; - this.updateType = updateType; - 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) => { - if (elementToModify.previousElementId) { - maybe(document.getElementById(elementToModify.previousElementId), (previousElem) => { - maybe(document.getElementById(elementToModify.elementId), (elem) => { - let isInRightPlace = elem.previousElementSibling && elem.previousElementSibling.id == previousElem.id; - if (!isInRightPlace) { - previousElem.insertAdjacentElement("afterend", elem); - } - }); - }); - } else { - maybe(document.getElementById(elementToModify.elementId), (elem) => { - let isInRightPlace = elem.previousElementSibling == null; - if (!isInRightPlace) { - container.insertAdjacentElement("afterbegin", elem); - } - }); - } - }); - if (this.updateType == "prepend") { - this.elementIdsToAdd.reverse().forEach((elemId) => { - maybe(document.getElementById(elemId), (elem) => container.insertAdjacentElement("afterbegin", elem)); - }); - } - } -}; - -// node_modules/morphdom/dist/morphdom-esm.js -var DOCUMENT_FRAGMENT_NODE = 11; -function morphAttrs(fromNode, toNode) { - var toNodeAttrs = toNode.attributes; - var attr; - var attrName; - var attrNamespaceURI; - var attrValue; - var fromValue; - if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE || fromNode.nodeType === DOCUMENT_FRAGMENT_NODE) { - return; - } - for (var i = toNodeAttrs.length - 1; i >= 0; i--) { - attr = toNodeAttrs[i]; - attrName = attr.name; - attrNamespaceURI = attr.namespaceURI; - attrValue = attr.value; - if (attrNamespaceURI) { - attrName = attr.localName || attrName; - fromValue = fromNode.getAttributeNS(attrNamespaceURI, attrName); - if (fromValue !== attrValue) { - if (attr.prefix === "xmlns") { - attrName = attr.name; - } - fromNode.setAttributeNS(attrNamespaceURI, attrName, attrValue); - } - } else { - fromValue = fromNode.getAttribute(attrName); - if (fromValue !== attrValue) { - fromNode.setAttribute(attrName, attrValue); - } - } - } - var fromNodeAttrs = fromNode.attributes; - for (var d = fromNodeAttrs.length - 1; d >= 0; d--) { - attr = fromNodeAttrs[d]; - attrName = attr.name; - attrNamespaceURI = attr.namespaceURI; - if (attrNamespaceURI) { - attrName = attr.localName || attrName; - if (!toNode.hasAttributeNS(attrNamespaceURI, attrName)) { - fromNode.removeAttributeNS(attrNamespaceURI, attrName); - } - } else { - if (!toNode.hasAttribute(attrName)) { - fromNode.removeAttribute(attrName); - } - } - } -} -var range; -var NS_XHTML = "http://www.w3.org/1999/xhtml"; -var doc = typeof document === "undefined" ? void 0 : document; -var HAS_TEMPLATE_SUPPORT = !!doc && "content" in doc.createElement("template"); -var HAS_RANGE_SUPPORT = !!doc && doc.createRange && "createContextualFragment" in doc.createRange(); -function createFragmentFromTemplate(str) { - var template = doc.createElement("template"); - template.innerHTML = str; - return template.content.childNodes[0]; -} -function createFragmentFromRange(str) { - if (!range) { - range = doc.createRange(); - range.selectNode(doc.body); - } - var fragment = range.createContextualFragment(str); - return fragment.childNodes[0]; -} -function createFragmentFromWrap(str) { - var fragment = doc.createElement("body"); - fragment.innerHTML = str; - return fragment.childNodes[0]; -} -function toElement(str) { - str = str.trim(); - if (HAS_TEMPLATE_SUPPORT) { - return createFragmentFromTemplate(str); - } else if (HAS_RANGE_SUPPORT) { - return createFragmentFromRange(str); - } - return createFragmentFromWrap(str); -} -function compareNodeNames(fromEl, toEl) { - var fromNodeName = fromEl.nodeName; - var toNodeName = toEl.nodeName; - var fromCodeStart, toCodeStart; - if (fromNodeName === toNodeName) { - return true; - } - fromCodeStart = fromNodeName.charCodeAt(0); - toCodeStart = toNodeName.charCodeAt(0); - if (fromCodeStart <= 90 && toCodeStart >= 97) { - return fromNodeName === toNodeName.toUpperCase(); - } else if (toCodeStart <= 90 && fromCodeStart >= 97) { - return toNodeName === fromNodeName.toUpperCase(); - } else { - return false; - } -} -function createElementNS(name, namespaceURI) { - return !namespaceURI || namespaceURI === NS_XHTML ? doc.createElement(name) : doc.createElementNS(namespaceURI, name); -} -function moveChildren(fromEl, toEl) { - var curChild = fromEl.firstChild; - while (curChild) { - var nextChild = curChild.nextSibling; - toEl.appendChild(curChild); - curChild = nextChild; - } - return toEl; -} -function syncBooleanAttrProp(fromEl, toEl, name) { - if (fromEl[name] !== toEl[name]) { - fromEl[name] = toEl[name]; - if (fromEl[name]) { - fromEl.setAttribute(name, ""); - } else { - fromEl.removeAttribute(name); - } - } -} -var specialElHandlers = { - OPTION: function(fromEl, toEl) { - var parentNode = fromEl.parentNode; - if (parentNode) { - var parentName = parentNode.nodeName.toUpperCase(); - if (parentName === "OPTGROUP") { - parentNode = parentNode.parentNode; - parentName = parentNode && parentNode.nodeName.toUpperCase(); - } - if (parentName === "SELECT" && !parentNode.hasAttribute("multiple")) { - if (fromEl.hasAttribute("selected") && !toEl.selected) { - fromEl.setAttribute("selected", "selected"); - fromEl.removeAttribute("selected"); - } - parentNode.selectedIndex = -1; - } - } - 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"); - if (fromEl.value !== toEl.value) { - fromEl.value = toEl.value; - } - if (!toEl.hasAttribute("value")) { - fromEl.removeAttribute("value"); - } - }, - TEXTAREA: function(fromEl, toEl) { - var newValue = toEl.value; - if (fromEl.value !== newValue) { - fromEl.value = newValue; - } - var firstChild = fromEl.firstChild; - if (firstChild) { - var oldValue = firstChild.nodeValue; - if (oldValue == newValue || !newValue && oldValue == fromEl.placeholder) { - return; - } - firstChild.nodeValue = newValue; - } - }, - SELECT: function(fromEl, toEl) { - if (!toEl.hasAttribute("multiple")) { - var selectedIndex = -1; - var i = 0; - var curChild = fromEl.firstChild; - var optgroup; - var nodeName; - while (curChild) { - nodeName = curChild.nodeName && curChild.nodeName.toUpperCase(); - if (nodeName === "OPTGROUP") { - optgroup = curChild; - curChild = optgroup.firstChild; - } else { - if (nodeName === "OPTION") { - if (curChild.hasAttribute("selected")) { - selectedIndex = i; - break; - } - i++; - } - curChild = curChild.nextSibling; - if (!curChild && optgroup) { - curChild = optgroup.nextSibling; - optgroup = null; - } - } - } - fromEl.selectedIndex = selectedIndex; - } - } -}; -var ELEMENT_NODE = 1; -var DOCUMENT_FRAGMENT_NODE$1 = 11; -var TEXT_NODE = 3; -var COMMENT_NODE = 8; -function noop() { -} -function defaultGetNodeKey(node) { - if (node) { - return node.getAttribute && node.getAttribute("id") || node.id; - } -} -function morphdomFactory(morphAttrs2) { - return function morphdom2(fromNode, toNode, options) { - if (!options) { - options = {}; - } - if (typeof toNode === "string") { - if (fromNode.nodeName === "#document" || fromNode.nodeName === "HTML" || fromNode.nodeName === "BODY") { - var toNodeHtml = toNode; - toNode = doc.createElement("html"); - toNode.innerHTML = toNodeHtml; - } else { - toNode = toElement(toNode); - } - } else if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE$1) { - toNode = toNode.firstElementChild; - } - var getNodeKey = options.getNodeKey || defaultGetNodeKey; - var onBeforeNodeAdded = options.onBeforeNodeAdded || noop; - var onNodeAdded = options.onNodeAdded || noop; - var onBeforeElUpdated = options.onBeforeElUpdated || noop; - var onElUpdated = options.onElUpdated || noop; - var onBeforeNodeDiscarded = options.onBeforeNodeDiscarded || noop; - var onNodeDiscarded = options.onNodeDiscarded || noop; - var onBeforeElChildrenUpdated = options.onBeforeElChildrenUpdated || noop; - var skipFromChildren = options.skipFromChildren || noop; - var addChild = options.addChild || function(parent, child) { - return parent.appendChild(child); - }; - var childrenOnly = options.childrenOnly === true; - var fromNodesLookup = /* @__PURE__ */ Object.create(null); - var keyedRemovalList = []; - function addKeyedRemoval(key) { - keyedRemovalList.push(key); - } - function walkDiscardedChildNodes(node, skipKeyedNodes) { - if (node.nodeType === ELEMENT_NODE) { - var curChild = node.firstChild; - while (curChild) { - var key = void 0; - if (skipKeyedNodes && (key = getNodeKey(curChild))) { - addKeyedRemoval(key); - } else { - onNodeDiscarded(curChild); - if (curChild.firstChild) { - walkDiscardedChildNodes(curChild, skipKeyedNodes); - } - } - curChild = curChild.nextSibling; - } - } - } - function removeNode(node, parentNode, skipKeyedNodes) { - if (onBeforeNodeDiscarded(node) === false) { - return; - } - if (parentNode) { - parentNode.removeChild(node); - } - onNodeDiscarded(node); - walkDiscardedChildNodes(node, skipKeyedNodes); - } - function indexTree(node) { - if (node.nodeType === ELEMENT_NODE || node.nodeType === DOCUMENT_FRAGMENT_NODE$1) { - var curChild = node.firstChild; - while (curChild) { - var key = getNodeKey(curChild); - if (key) { - fromNodesLookup[key] = curChild; - } - indexTree(curChild); - curChild = curChild.nextSibling; - } - } - } - indexTree(fromNode); - function handleNodeAdded(el) { - onNodeAdded(el); - var curChild = el.firstChild; - while (curChild) { - var nextSibling = curChild.nextSibling; - var key = getNodeKey(curChild); - if (key) { - var unmatchedFromEl = fromNodesLookup[key]; - if (unmatchedFromEl && compareNodeNames(curChild, unmatchedFromEl)) { - curChild.parentNode.replaceChild(unmatchedFromEl, curChild); - morphEl(unmatchedFromEl, curChild); - } else { - handleNodeAdded(curChild); - } - } else { - handleNodeAdded(curChild); - } - curChild = nextSibling; - } - } - function cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey) { - while (curFromNodeChild) { - var fromNextSibling = curFromNodeChild.nextSibling; - if (curFromNodeKey = getNodeKey(curFromNodeChild)) { - addKeyedRemoval(curFromNodeKey); - } else { - removeNode( - curFromNodeChild, - fromEl, - true - /* skip keyed nodes */ - ); - } - curFromNodeChild = fromNextSibling; - } - } - function morphEl(fromEl, toEl, childrenOnly2) { - var toElKey = getNodeKey(toEl); - if (toElKey) { - delete fromNodesLookup[toElKey]; - } - if (!childrenOnly2) { - var beforeUpdateResult = onBeforeElUpdated(fromEl, toEl); - if (beforeUpdateResult === false) { - return; - } else if (beforeUpdateResult instanceof HTMLElement) { - fromEl = beforeUpdateResult; - indexTree(fromEl); - } - morphAttrs2(fromEl, toEl); - onElUpdated(fromEl); - if (onBeforeElChildrenUpdated(fromEl, toEl) === false) { - return; - } - } - if (fromEl.nodeName !== "TEXTAREA") { - morphChildren(fromEl, toEl); - } else { - specialElHandlers.TEXTAREA(fromEl, toEl); - } - } - function morphChildren(fromEl, toEl) { - var skipFrom = skipFromChildren(fromEl, toEl); - var curToNodeChild = toEl.firstChild; - var curFromNodeChild = fromEl.firstChild; - var curToNodeKey; - var curFromNodeKey; - var fromNextSibling; - var toNextSibling; - var matchingFromEl; - outer: - while (curToNodeChild) { - toNextSibling = curToNodeChild.nextSibling; - curToNodeKey = getNodeKey(curToNodeChild); - while (!skipFrom && curFromNodeChild) { - fromNextSibling = curFromNodeChild.nextSibling; - if (curToNodeChild.isSameNode && curToNodeChild.isSameNode(curFromNodeChild)) { - curToNodeChild = toNextSibling; - curFromNodeChild = fromNextSibling; - continue outer; - } - curFromNodeKey = getNodeKey(curFromNodeChild); - var curFromNodeType = curFromNodeChild.nodeType; - var isCompatible = void 0; - if (curFromNodeType === curToNodeChild.nodeType) { - if (curFromNodeType === ELEMENT_NODE) { - if (curToNodeKey) { - if (curToNodeKey !== curFromNodeKey) { - if (matchingFromEl = fromNodesLookup[curToNodeKey]) { - if (fromNextSibling === matchingFromEl) { - isCompatible = false; - } else { - fromEl.insertBefore(matchingFromEl, curFromNodeChild); - if (curFromNodeKey) { - addKeyedRemoval(curFromNodeKey); - } else { - removeNode( - curFromNodeChild, - fromEl, - true - /* skip keyed nodes */ - ); - } - curFromNodeChild = matchingFromEl; - curFromNodeKey = getNodeKey(curFromNodeChild); - } - } else { - isCompatible = false; - } - } - } else if (curFromNodeKey) { - isCompatible = false; - } - isCompatible = isCompatible !== false && compareNodeNames(curFromNodeChild, curToNodeChild); - if (isCompatible) { - morphEl(curFromNodeChild, curToNodeChild); - } - } else if (curFromNodeType === TEXT_NODE || curFromNodeType == COMMENT_NODE) { - isCompatible = true; - if (curFromNodeChild.nodeValue !== curToNodeChild.nodeValue) { - curFromNodeChild.nodeValue = curToNodeChild.nodeValue; - } - } - } - if (isCompatible) { - curToNodeChild = toNextSibling; - curFromNodeChild = fromNextSibling; - continue outer; - } - if (curFromNodeKey) { - addKeyedRemoval(curFromNodeKey); - } else { - removeNode( - curFromNodeChild, - fromEl, - true - /* skip keyed nodes */ - ); - } - curFromNodeChild = fromNextSibling; - } - if (curToNodeKey && (matchingFromEl = fromNodesLookup[curToNodeKey]) && compareNodeNames(matchingFromEl, curToNodeChild)) { - if (!skipFrom) { - addChild(fromEl, matchingFromEl); - } - morphEl(matchingFromEl, curToNodeChild); - } else { - var onBeforeNodeAddedResult = onBeforeNodeAdded(curToNodeChild); - if (onBeforeNodeAddedResult !== false) { - if (onBeforeNodeAddedResult) { - curToNodeChild = onBeforeNodeAddedResult; - } - if (curToNodeChild.actualize) { - curToNodeChild = curToNodeChild.actualize(fromEl.ownerDocument || doc); - } - addChild(fromEl, curToNodeChild); - handleNodeAdded(curToNodeChild); - } - } - curToNodeChild = toNextSibling; - curFromNodeChild = fromNextSibling; - } - cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey); - var specialElHandler = specialElHandlers[fromEl.nodeName]; - if (specialElHandler) { - specialElHandler(fromEl, toEl); - } - } - var morphedNode = fromNode; - var morphedNodeType = morphedNode.nodeType; - var toNodeType = toNode.nodeType; - if (!childrenOnly) { - if (morphedNodeType === ELEMENT_NODE) { - if (toNodeType === ELEMENT_NODE) { - if (!compareNodeNames(fromNode, toNode)) { - onNodeDiscarded(fromNode); - morphedNode = moveChildren(fromNode, createElementNS(toNode.nodeName, toNode.namespaceURI)); - } - } else { - morphedNode = toNode; - } - } else if (morphedNodeType === TEXT_NODE || morphedNodeType === COMMENT_NODE) { - if (toNodeType === morphedNodeType) { - if (morphedNode.nodeValue !== toNode.nodeValue) { - morphedNode.nodeValue = toNode.nodeValue; - } - return morphedNode; - } else { - morphedNode = toNode; - } - } - } - if (morphedNode === toNode) { - onNodeDiscarded(fromNode); - } else { - if (toNode.isSameNode && toNode.isSameNode(morphedNode)) { - return; - } - morphEl(morphedNode, toNode, childrenOnly); - if (keyedRemovalList) { - for (var i = 0, len = keyedRemovalList.length; i < len; i++) { - var elToRemove = fromNodesLookup[keyedRemovalList[i]]; - if (elToRemove) { - removeNode(elToRemove, elToRemove.parentNode, false); - } - } - } - } - if (!childrenOnly && morphedNode !== fromNode && fromNode.parentNode) { - if (morphedNode.actualize) { - morphedNode = morphedNode.actualize(fromNode.ownerDocument || doc); - } - fromNode.parentNode.replaceChild(morphedNode, fromNode); - } - return morphedNode; - }; -} -var morphdom = morphdomFactory(morphAttrs); -var morphdom_esm_default = morphdom; - -// js/phoenix_live_view/dom_patch.js -var DOMPatch = class { - static patchWithClonedTree(container, clonedTree, liveSocket) { - let activeElement = liveSocket.getActiveElement(); - let phxUpdate = liveSocket.binding(PHX_UPDATE); - morphdom_esm_default(container, clonedTree, { - childrenOnly: false, - onBeforeElUpdated: (fromEl, toEl) => { - dom_default.syncPendingAttrs(fromEl, toEl); - if (!container.isSameNode(fromEl) && fromEl.hasAttribute(PHX_REF_LOCK)) { - return false; - } - if (dom_default.isIgnored(fromEl, phxUpdate)) { - return false; - } - if (activeElement && activeElement.isSameNode(fromEl) && dom_default.isFormInput(fromEl)) { - dom_default.mergeFocusedInput(fromEl, toEl); - return false; - } - } - }); - } - constructor(view, container, id, html, streams, targetCID) { - this.view = view; - this.liveSocket = view.liveSocket; - this.container = container; - this.id = id; - this.rootID = view.root.id; - this.html = html; - this.streams = streams; - this.streamInserts = {}; - this.streamComponentRestore = {}; - this.targetCID = targetCID; - this.cidPatch = isCid(this.targetCID); - this.pendingRemoves = []; - this.phxRemove = this.liveSocket.binding("remove"); - this.targetContainer = this.isCIDPatch() ? this.targetCIDContainer(html) : container; - this.callbacks = { - beforeadded: [], - beforeupdated: [], - beforephxChildAdded: [], - afteradded: [], - afterupdated: [], - afterdiscarded: [], - afterphxChildAdded: [], - aftertransitionsDiscarded: [] - }; - } - before(kind, callback) { - this.callbacks[`before${kind}`].push(callback); - } - after(kind, callback) { - this.callbacks[`after${kind}`].push(callback); - } - trackBefore(kind, ...args) { - this.callbacks[`before${kind}`].forEach((callback) => callback(...args)); - } - trackAfter(kind, ...args) { - this.callbacks[`after${kind}`].forEach((callback) => callback(...args)); - } - markPrunableContentForRemoval() { - let phxUpdate = this.liveSocket.binding(PHX_UPDATE); - dom_default.all(this.container, `[${phxUpdate}=append] > *, [${phxUpdate}=prepend] > *`, (el) => { - el.setAttribute(PHX_PRUNE, ""); - }); - } - perform(isJoinPatch) { - let { view, liveSocket, html, container, targetContainer } = this; - if (this.isCIDPatch() && !targetContainer) { - return; - } - let focused = liveSocket.getActiveElement(); - let { selectionStart, selectionEnd } = focused && dom_default.hasSelectionRange(focused) ? focused : {}; - let phxUpdate = liveSocket.binding(PHX_UPDATE); - let phxViewportTop = liveSocket.binding(PHX_VIEWPORT_TOP); - let phxViewportBottom = liveSocket.binding(PHX_VIEWPORT_BOTTOM); - let phxTriggerExternal = liveSocket.binding(PHX_TRIGGER_ACTION); - let added = []; - let updates = []; - let appendPrependUpdates = []; - let externalFormTriggered = null; - function morph(targetContainer2, source, withChildren = false) { - let morphCallbacks = { - // 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)) { - return null; - } - if (isJoinPatch) { - return node.id; - } - 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) { - return parent.appendChild(child); - } - this.setStreamRef(child, ref); - if (streamAt === 0) { - parent.insertAdjacentElement("afterbegin", child); - } else if (streamAt === -1) { - let lastChild = parent.lastElementChild; - if (lastChild && !lastChild.hasAttribute(PHX_STREAM_REF)) { - let nonStreamChild = Array.from(parent.children).find((c) => !c.hasAttribute(PHX_STREAM_REF)); - parent.insertBefore(child, nonStreamChild); - } else { - parent.appendChild(child); - } - } else if (streamAt > 0) { - let sibling = Array.from(parent.children)[streamAt]; - parent.insertBefore(child, sibling); - } - }, - onBeforeNodeAdded: (el) => { - dom_default.maintainPrivateHooks(el, el, phxViewportTop, phxViewportBottom); - this.trackBefore("added", el); - let morphedEl = el; - if (this.streamComponentRestore[el.id]) { - morphedEl = this.streamComponentRestore[el.id]; - delete this.streamComponentRestore[el.id]; - morph.call(this, morphedEl, el, true); - } - return morphedEl; - }, - onNodeAdded: (el) => { - if (el.getAttribute) { - this.maybeReOrderStream(el, true); - } - if (el instanceof HTMLImageElement && el.srcset) { - el.srcset = el.srcset; - } else if (el instanceof HTMLVideoElement && el.autoplay) { - el.play(); - } - if (dom_default.isNowTriggerFormExternal(el, phxTriggerExternal)) { - externalFormTriggered = el; - } - if (dom_default.isPhxChild(el) && view.ownsElement(el) || dom_default.isPhxSticky(el) && view.ownsElement(el.parentNode)) { - this.trackAfter("phxChildAdded", el); - } - added.push(el); - }, - onNodeDiscarded: (el) => this.onNodeDiscarded(el), - onBeforeNodeDiscarded: (el) => { - if (el.getAttribute && el.getAttribute(PHX_PRUNE) !== null) { - return true; - } - if (el.parentElement !== null && el.id && dom_default.isPhxUpdate(el.parentElement, phxUpdate, [PHX_STREAM, "append", "prepend"])) { - return false; - } - if (this.maybePendingRemove(el)) { - return false; - } - if (this.skipCIDSibling(el)) { - return false; - } - return true; - }, - onElUpdated: (el) => { - if (dom_default.isNowTriggerFormExternal(el, phxTriggerExternal)) { - externalFormTriggered = el; - } - updates.push(el); - this.maybeReOrderStream(el, false); - }, - onBeforeElUpdated: (fromEl, toEl) => { - if (fromEl.id && fromEl.isSameNode(targetContainer2) && fromEl.id !== toEl.id) { - morphCallbacks.onNodeDiscarded(fromEl); - fromEl.replaceWith(toEl); - return morphCallbacks.onNodeAdded(toEl); - } - dom_default.syncPendingAttrs(fromEl, toEl); - dom_default.maintainPrivateHooks(fromEl, toEl, phxViewportTop, phxViewportBottom); - dom_default.cleanChildNodes(toEl, phxUpdate); - if (this.skipCIDSibling(toEl)) { - this.maybeReOrderStream(fromEl); - return false; - } - if (dom_default.isPhxSticky(fromEl)) { - [PHX_SESSION, PHX_STATIC, PHX_ROOT_ID].map((attr) => [attr, fromEl.getAttribute(attr), toEl.getAttribute(attr)]).forEach(([attr, fromVal, toVal]) => { - if (toVal && fromVal !== toVal) { - fromEl.setAttribute(attr, toVal); - } - }); - return false; - } - if (dom_default.isIgnored(fromEl, phxUpdate) || fromEl.form && fromEl.form.isSameNode(externalFormTriggered)) { - this.trackBefore("updated", fromEl, toEl); - dom_default.mergeAttrs(fromEl, toEl, { isIgnored: dom_default.isIgnored(fromEl, phxUpdate) }); - updates.push(fromEl); - dom_default.applyStickyOperations(fromEl); - return false; - } - if (fromEl.type === "number" && (fromEl.validity && fromEl.validity.badInput)) { - return false; - } - let isFocusedFormEl = focused && fromEl.isSameNode(focused) && dom_default.isFormInput(fromEl); - let focusedSelectChanged = isFocusedFormEl && this.isChangedSelect(fromEl, toEl); - if (fromEl.hasAttribute(PHX_REF_SRC)) { - if (dom_default.isUploadInput(fromEl)) { - dom_default.mergeAttrs(fromEl, toEl, { isIgnored: true }); - this.trackBefore("updated", fromEl, toEl); - updates.push(fromEl); - } - dom_default.applyStickyOperations(fromEl); - let isLocked = fromEl.hasAttribute(PHX_REF_LOCK); - let clone2 = isLocked ? dom_default.private(fromEl, PHX_REF_LOCK) || fromEl.cloneNode(true) : null; - if (clone2) { - dom_default.putPrivate(fromEl, PHX_REF_LOCK, clone2); - if (!isFocusedFormEl) { - fromEl = clone2; - } - } - } - if (dom_default.isPhxChild(toEl)) { - let prevSession = fromEl.getAttribute(PHX_SESSION); - dom_default.mergeAttrs(fromEl, toEl, { exclude: [PHX_STATIC] }); - if (prevSession !== "") { - fromEl.setAttribute(PHX_SESSION, prevSession); - } - fromEl.setAttribute(PHX_ROOT_ID, this.rootID); - dom_default.applyStickyOperations(fromEl); - return false; - } - dom_default.copyPrivates(toEl, fromEl); - if (isFocusedFormEl && fromEl.type !== "hidden" && !focusedSelectChanged) { - this.trackBefore("updated", fromEl, toEl); - dom_default.mergeFocusedInput(fromEl, toEl); - dom_default.syncAttrsToProps(fromEl); - updates.push(fromEl); - dom_default.applyStickyOperations(fromEl); - return false; - } else { - if (focusedSelectChanged) { - fromEl.blur(); - } - if (dom_default.isPhxUpdate(toEl, phxUpdate, ["append", "prepend"])) { - appendPrependUpdates.push(new DOMPostMorphRestorer(fromEl, toEl, toEl.getAttribute(phxUpdate))); - } - dom_default.syncAttrsToProps(toEl); - dom_default.applyStickyOperations(toEl); - this.trackBefore("updated", fromEl, toEl); - return fromEl; - } - } - }; - morphdom_esm_default(targetContainer2, source, morphCallbacks); - } - this.trackBefore("added", container); - this.trackBefore("updated", container, container); - liveSocket.time("morphdom", () => { - this.streams.forEach(([ref, inserts, deleteIds, reset]) => { - inserts.forEach(([key, streamAt, limit]) => { - this.streamInserts[key] = { ref, streamAt, limit, reset }; - }); - if (reset !== void 0) { - dom_default.all(container, `[${PHX_STREAM_REF}="${ref}"]`, (child) => { - this.removeStreamChildElement(child); - }); - } - deleteIds.forEach((id) => { - let child = container.querySelector(`[id="${id}"]`); - if (child) { - this.removeStreamChildElement(child); - } - }); - }); - if (isJoinPatch) { - dom_default.all(this.container, `[${phxUpdate}=${PHX_STREAM}]`, (el) => { - this.liveSocket.owner(el, (view2) => { - if (view2 === this.view) { - Array.from(el.children).forEach((child) => { - this.removeStreamChildElement(child); - }); - } - }); - }); - } - morph.call(this, targetContainer, html); - }); - if (liveSocket.isDebugEnabled()) { - detectDuplicateIds(); - Array.from(document.querySelectorAll("input[name=id]")).forEach((node) => { - if (node.form) { - console.error('Detected an input with name="id" inside a form! This will cause problems when patching the DOM.\n', node); - } - }); - } - if (appendPrependUpdates.length > 0) { - liveSocket.time("post-morph append/prepend restoration", () => { - appendPrependUpdates.forEach((update) => update.perform()); - }); - } - liveSocket.silenceEvents(() => dom_default.restoreFocus(focused, selectionStart, selectionEnd)); - dom_default.dispatchEvent(document, "phx:update"); - added.forEach((el) => this.trackAfter("added", el)); - updates.forEach((el) => this.trackAfter("updated", el)); - this.transitionPendingRemoves(); - if (externalFormTriggered) { - liveSocket.unload(); - Object.getPrototypeOf(externalFormTriggered).submit.call(externalFormTriggered); - } - return true; - } - onNodeDiscarded(el) { - if (dom_default.isPhxChild(el) || dom_default.isPhxSticky(el)) { - this.liveSocket.destroyViewByEl(el); - } - this.trackAfter("discarded", el); - } - maybePendingRemove(node) { - if (node.getAttribute && node.getAttribute(this.phxRemove) !== null) { - this.pendingRemoves.push(node); - return true; - } else { - return false; - } - } - removeStreamChildElement(child) { - if (this.streamInserts[child.id]) { - this.streamComponentRestore[child.id] = child; - child.remove(); - } else { - if (!this.maybePendingRemove(child)) { - child.remove(); - this.onNodeDiscarded(child); - } - } - } - getStreamInsert(el) { - let insert = el.id ? this.streamInserts[el.id] : {}; - return insert || {}; - } - setStreamRef(el, ref) { - dom_default.putSticky(el, PHX_STREAM_REF, (el2) => el2.setAttribute(PHX_STREAM_REF, ref)); - } - maybeReOrderStream(el, isNew) { - let { ref, streamAt, reset } = this.getStreamInsert(el); - if (streamAt === void 0) { - return; - } - this.setStreamRef(el, ref); - if (!reset && !isNew) { - return; - } - if (!el.parentElement) { - return; - } - if (streamAt === 0) { - el.parentElement.insertBefore(el, el.parentElement.firstElementChild); - } else if (streamAt > 0) { - let children = Array.from(el.parentElement.children); - let oldIndex = children.indexOf(el); - if (streamAt >= children.length - 1) { - el.parentElement.appendChild(el); - } else { - let sibling = children[streamAt]; - if (oldIndex > streamAt) { - el.parentElement.insertBefore(el, sibling); - } else { - el.parentElement.insertBefore(el, sibling.nextElementSibling); - } - } - } - this.maybeLimitStream(el); - } - maybeLimitStream(el) { - let { limit } = this.getStreamInsert(el); - let children = limit !== null && Array.from(el.parentElement.children); - if (limit && limit < 0 && children.length > limit * -1) { - children.slice(0, children.length + limit).forEach((child) => this.removeStreamChildElement(child)); - } else if (limit && limit >= 0 && children.length > limit) { - children.slice(limit).forEach((child) => this.removeStreamChildElement(child)); - } - } - transitionPendingRemoves() { - let { pendingRemoves, liveSocket } = this; - if (pendingRemoves.length > 0) { - liveSocket.transitionRemoves(pendingRemoves, false, () => { - pendingRemoves.forEach((el) => { - let child = dom_default.firstPhxChild(el); - if (child) { - liveSocket.destroyViewByEl(child); - } - el.remove(); - }); - this.trackAfter("transitionsDiscarded", pendingRemoves); - }); - } - } - isChangedSelect(fromEl, toEl) { - if (!(fromEl instanceof HTMLSelectElement) || fromEl.multiple) { - return false; - } - if (fromEl.options.length !== toEl.options.length) { - return true; - } - toEl.value = fromEl.value; - return !fromEl.isEqualNode(toEl); - } - isCIDPatch() { - return this.cidPatch; - } - skipCIDSibling(el) { - return el.nodeType === Node.ELEMENT_NODE && el.hasAttribute(PHX_SKIP); - } - targetCIDContainer(html) { - if (!this.isCIDPatch()) { - return; - } - let [first, ...rest] = dom_default.findComponentNodeList(this.container, this.targetCID); - if (rest.length === 0 && dom_default.childNodeLength(html) === 1) { - return first; - } else { - return first && first.parentNode; - } - } - indexOf(parent, child) { - return Array.from(parent.children).indexOf(child); - } -}; - -// js/phoenix_live_view/rendered.js -var VOID_TAGS = /* @__PURE__ */ new Set([ - "area", - "base", - "br", - "col", - "command", - "embed", - "hr", - "img", - "input", - "keygen", - "link", - "meta", - "param", - "source", - "track", - "wbr" -]); -var quoteChars = /* @__PURE__ */ new Set(["'", '"']); -var modifyRoot = (html, attrs, clearInnerHTML) => { - let i = 0; - let insideComment = false; - let beforeTag, afterTag, tag, tagNameEndsAt, id, newHTML; - let lookahead = html.match(/^(\s*(?:\s*)*)<([^\s\/>]+)/); - if (lookahead === null) { - throw new Error(`malformed html ${html}`); - } - i = lookahead[0].length; - beforeTag = lookahead[1]; - tag = lookahead[2]; - tagNameEndsAt = i; - for (i; i < html.length; i++) { - if (html.charAt(i) === ">") { - break; - } - if (html.charAt(i) === "=") { - let isId = html.slice(i - 3, i) === " id"; - i++; - let char = html.charAt(i); - if (quoteChars.has(char)) { - let attrStartsAt = i; - i++; - for (i; i < html.length; i++) { - if (html.charAt(i) === char) { - break; - } - } - if (isId) { - id = html.slice(attrStartsAt + 1, i); - break; - } - } - } - } - let closeAt = html.length - 1; - insideComment = false; - while (closeAt >= beforeTag.length + tag.length) { - let char = html.charAt(closeAt); - if (insideComment) { - if (char === "-" && html.slice(closeAt - 3, closeAt) === "" && html.slice(closeAt - 2, closeAt) === "--") { - insideComment = true; - closeAt -= 3; - } else if (char === ">") { - break; - } else { - closeAt -= 1; - } - } - afterTag = html.slice(closeAt + 1, html.length); - let attrsStr = Object.keys(attrs).map((attr) => attrs[attr] === true ? attr : `${attr}="${attrs[attr]}"`).join(" "); - if (clearInnerHTML) { - let idAttrStr = id ? ` id="${id}"` : ""; - if (VOID_TAGS.has(tag)) { - newHTML = `<${tag}${idAttrStr}${attrsStr === "" ? "" : " "}${attrsStr}/>`; - } else { - newHTML = `<${tag}${idAttrStr}${attrsStr === "" ? "" : " "}${attrsStr}>`; - } - } else { - let rest = html.slice(tagNameEndsAt, closeAt + 1); - newHTML = `<${tag}${attrsStr === "" ? "" : " "}${attrsStr}${rest}`; - } - return [newHTML, beforeTag, afterTag]; -}; -var Rendered = class { - static extract(diff) { - let { [REPLY]: reply, [EVENTS]: events, [TITLE]: title } = diff; - delete diff[REPLY]; - delete diff[EVENTS]; - delete diff[TITLE]; - return { diff, title, reply: reply || null, events: events || [] }; - } - constructor(viewId, rendered) { - this.viewId = viewId; - this.rendered = {}; - this.magicId = 0; - this.mergeDiff(rendered); - } - parentViewId() { - return this.viewId; - } - toString(onlyCids) { - let [str, streams] = this.recursiveToString(this.rendered, this.rendered[COMPONENTS], onlyCids, true, {}); - return [str, streams]; - } - recursiveToString(rendered, components = rendered[COMPONENTS], onlyCids, changeTracking, rootAttrs) { - onlyCids = onlyCids ? new Set(onlyCids) : null; - let output = { buffer: "", components, onlyCids, streams: /* @__PURE__ */ new Set() }; - this.toOutputBuffer(rendered, null, output, changeTracking, rootAttrs); - return [output.buffer, output.streams]; - } - componentCIDs(diff) { - return Object.keys(diff[COMPONENTS] || {}).map((i) => parseInt(i)); - } - isComponentOnlyDiff(diff) { - if (!diff[COMPONENTS]) { - return false; - } - return Object.keys(diff).length === 1; - } - getComponent(diff, cid) { - return diff[COMPONENTS][cid]; - } - resetRender(cid) { - if (this.rendered[COMPONENTS][cid]) { - this.rendered[COMPONENTS][cid].reset = true; - } - } - mergeDiff(diff) { - let newc = diff[COMPONENTS]; - let cache = {}; - delete diff[COMPONENTS]; - this.rendered = this.mutableMerge(this.rendered, diff); - this.rendered[COMPONENTS] = this.rendered[COMPONENTS] || {}; - if (newc) { - let oldc = this.rendered[COMPONENTS]; - for (let cid in newc) { - newc[cid] = this.cachedFindComponent(cid, newc[cid], oldc, newc, cache); - } - for (let cid in newc) { - oldc[cid] = newc[cid]; - } - diff[COMPONENTS] = newc; - } - } - cachedFindComponent(cid, cdiff, oldc, newc, cache) { - if (cache[cid]) { - return cache[cid]; - } else { - let ndiff, stat, scid = cdiff[STATIC]; - if (isCid(scid)) { - let tdiff; - if (scid > 0) { - tdiff = this.cachedFindComponent(scid, newc[scid], oldc, newc, cache); - } else { - tdiff = oldc[-scid]; - } - stat = tdiff[STATIC]; - ndiff = this.cloneMerge(tdiff, cdiff, true); - ndiff[STATIC] = stat; - } else { - ndiff = cdiff[STATIC] !== void 0 || oldc[cid] === void 0 ? cdiff : this.cloneMerge(oldc[cid], cdiff, false); - } - cache[cid] = ndiff; - return ndiff; - } - } - mutableMerge(target, source) { - if (source[STATIC] !== void 0) { - return source; - } else { - this.doMutableMerge(target, source); - return target; - } - } - doMutableMerge(target, source) { - for (let key in source) { - let val = source[key]; - let targetVal = target[key]; - let isObjVal = isObject(val); - if (isObjVal && val[STATIC] === void 0 && isObject(targetVal)) { - this.doMutableMerge(targetVal, val); - } else { - target[key] = val; - } - } - if (target[ROOT]) { - 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) { - let val = source[key]; - let targetVal = target[key]; - if (isObject(val) && val[STATIC] === void 0 && isObject(targetVal)) { - merged[key] = this.cloneMerge(targetVal, val, pruneMagicId); - } else if (val === void 0 && isObject(targetVal)) { - merged[key] = this.cloneMerge(targetVal, {}, pruneMagicId); - } - } - if (pruneMagicId) { - delete merged.magicId; - delete merged.newRender; - } else if (target[ROOT]) { - merged.newRender = true; - } - return merged; - } - componentToString(cid) { - let [str, streams] = this.recursiveCIDToString(this.rendered[COMPONENTS], cid, null); - let [strippedHTML, _before, _after] = modifyRoot(str, {}); - return [strippedHTML, streams]; - } - pruneCIDs(cids) { - cids.forEach((cid) => delete this.rendered[COMPONENTS][cid]); - } - // private - get() { - return this.rendered; - } - isNewFingerprint(diff = {}) { - return !!diff[STATIC]; - } - templateStatic(part, templates) { - if (typeof part === "number") { - return templates[part]; - } else { - return part; - } - } - nextMagicID() { - 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); - } - let { [STATIC]: statics } = rendered; - statics = this.templateStatic(statics, templates); - let isRoot = rendered[ROOT]; - let prevBuffer = output.buffer; - if (isRoot) { - output.buffer = ""; - } - if (changeTracking && isRoot && !rendered.magicId) { - rendered.newRender = true; - rendered.magicId = this.nextMagicID(); - } - output.buffer += statics[0]; - for (let i = 1; i < statics.length; i++) { - this.dynamicToBuffer(rendered[i - 1], templates, output, changeTracking); - output.buffer += statics[i]; - } - if (isRoot) { - let skip = false; - let attrs; - if (changeTracking || rendered.magicId) { - skip = changeTracking && !rendered.newRender; - attrs = { [PHX_MAGIC_ID]: rendered.magicId, ...rootAttrs }; - } else { - attrs = rootAttrs; - } - if (skip) { - attrs[PHX_SKIP] = true; - } - let [newRoot, commentBefore, commentAfter] = modifyRoot(output.buffer, attrs, skip); - rendered.newRender = false; - output.buffer = prevBuffer + commentBefore + newRoot + commentAfter; - } - } - comprehensionToBuffer(rendered, templates, output) { - let { [DYNAMICS]: dynamics, [STATIC]: statics, [STREAM]: stream } = rendered; - let [_ref, _inserts, deleteIds, reset] = stream || [null, {}, [], null]; - statics = this.templateStatic(statics, templates); - let compTemplates = templates || rendered[TEMPLATES]; - for (let d = 0; d < dynamics.length; d++) { - let dynamic = dynamics[d]; - output.buffer += statics[0]; - for (let i = 1; i < statics.length; i++) { - let changeTracking = false; - this.dynamicToBuffer(dynamic[i - 1], compTemplates, output, changeTracking); - output.buffer += statics[i]; - } - } - if (stream !== void 0 && (rendered[DYNAMICS].length > 0 || deleteIds.length > 0 || reset)) { - delete rendered[STREAM]; - rendered[DYNAMICS] = []; - output.streams.add(stream); - } - } - dynamicToBuffer(rendered, templates, output, changeTracking) { - if (typeof rendered === "number") { - let [str, streams] = this.recursiveCIDToString(output.components, rendered, output.onlyCids); - output.buffer += str; - output.streams = /* @__PURE__ */ new Set([...output.streams, ...streams]); - } else if (isObject(rendered)) { - this.toOutputBuffer(rendered, templates, output, changeTracking, {}); - } else { - output.buffer += rendered; - } - } - recursiveCIDToString(components, cid, onlyCids) { - let component = components[cid] || logError(`no component for CID ${cid}`, components); - let attrs = { [PHX_COMPONENT]: cid }; - let skip = onlyCids && !onlyCids.has(cid); - component.newRender = !skip; - component.magicId = `c${cid}-${this.parentViewId()}`; - let changeTracking = !component.reset; - let [html, streams] = this.recursiveToString(component, components, onlyCids, changeTracking, attrs); - delete component.reset; - return [html, streams]; - } -}; - -// js/phoenix_live_view/view_hook.js -var HOOK_ID = "hookId"; -var viewHookID = 1; -var ViewHook = class { - static makeID() { - return viewHookID++; - } - static elementID(el) { - return dom_default.private(el, HOOK_ID); - } - constructor(view, el, callbacks) { - this.el = el; - this.__attachView(view); - this.__callbacks = callbacks; - this.__listeners = /* @__PURE__ */ new Set(); - this.__isDisconnected = false; - dom_default.putPrivate(this.el, HOOK_ID, this.constructor.makeID()); - for (let key in this.__callbacks) { - this[key] = this.__callbacks[key]; - } - } - __attachView(view) { - if (view) { - this.__view = () => view; - this.liveSocket = view.liveSocket; - } else { - this.__view = () => { - throw new Error(`hook not yet attached to a live view: ${this.el.outerHTML}`); - }; - this.liveSocket = null; - } - } - __mounted() { - this.mounted && this.mounted(); - } - __updated() { - this.updated && this.updated(); - } - __beforeUpdate() { - this.beforeUpdate && this.beforeUpdate(); - } - __destroyed() { - this.destroyed && this.destroyed(); - } - __reconnected() { - if (this.__isDisconnected) { - this.__isDisconnected = false; - this.reconnected && this.reconnected(); - } - } - __disconnected() { - this.__isDisconnected = true; - this.disconnected && this.disconnected(); - } - /** - * Binds the hook to JS commands. - * - * @param {ViewHook} hook - The ViewHook instance to bind. - * - * @returns {Object} An object with methods to manipulate the DOM and execute JavaScript. - */ - js() { - let hook = this; - return { - /** - * Executes encoded JavaScript in the context of the hook element. - * - * @param {string} encodedJS - The encoded JavaScript string to execute. - */ - exec(encodedJS) { - hook.__view().liveSocket.execJS(hook.el, encodedJS, "hook"); - }, - /** - * Shows an element. - * - * @param {HTMLElement} el - The element to show. - * @param {Object} [opts={}] - Optional settings. - * @param {string} [opts.display] - The CSS display value to set. Defaults "block". - * @param {string} [opts.transition] - The CSS transition classes to set when showing. - * @param {number} [opts.time] - The transition duration in milliseconds. Defaults 200. - * @param {boolean} [opts.blocking] - The boolean flag to block the UI during the transition. - * Defaults `true`. - */ - show(el, opts = {}) { - let owner = hook.__view().liveSocket.owner(el); - js_default.show("hook", owner, el, opts.display, opts.transition, opts.time, opts.blocking); - }, - /** - * Hides an element. - * - * @param {HTMLElement} el - The element to hide. - * @param {Object} [opts={}] - Optional settings. - * @param {string} [opts.transition] - The CSS transition classes to set when hiding. - * @param {number} [opts.time] - The transition duration in milliseconds. Defaults 200. - * @param {boolean} [opts.blocking] - The boolean flag to block the UI during the transition. - * Defaults `true`. - */ - hide(el, opts = {}) { - let owner = hook.__view().liveSocket.owner(el); - js_default.hide("hook", owner, el, null, opts.transition, opts.time, opts.blocking); - }, - /** - * Toggles the visibility of an element. - * - * @param {HTMLElement} el - The element to toggle. - * @param {Object} [opts={}] - Optional settings. - * @param {string} [opts.display] - The CSS display value to set. Defaults "block". - * @param {string} [opts.in] - The CSS transition classes for showing. - * Accepts either the string of classes to apply when toggling in, or - * a 3-tuple containing the transition class, the class to apply - * to start the transition, and the ending transition class, such as: - * - * ["ease-out duration-300", "opacity-0", "opacity-100"] - * - * @param {string} [opts.out] - The CSS transition classes for hiding. - * Accepts either string of classes to apply when toggling out, or - * a 3-tuple containing the transition class, the class to apply - * to start the transition, and the ending transition class, such as: - * - * ["ease-out duration-300", "opacity-100", "opacity-0"] - * - * @param {number} [opts.time] - The transition duration in milliseconds. - * - * @param {boolean} [opts.blocking] - The boolean flag to block the UI during the transition. - * Defaults `true`. - */ - toggle(el, opts = {}) { - let owner = hook.__view().liveSocket.owner(el); - opts.in = js_default.transitionClasses(opts.in); - opts.out = js_default.transitionClasses(opts.out); - js_default.toggle("hook", owner, el, opts.display, opts.in, opts.out, opts.time, opts.blocking); - }, - /** - * Adds CSS classes to an element. - * - * @param {HTMLElement} el - The element to add classes to. - * @param {string|string[]} names - The class name(s) to add. - * @param {Object} [opts={}] - Optional settings. - * @param {string} [opts.transition] - The CSS transition property to set. - * Accepts a string of classes to apply when adding classes or - * a 3-tuple containing the transition class, the class to apply - * to start the transition, and the ending transition class, such as: - * - * ["ease-out duration-300", "opacity-0", "opacity-100"] - * - * @param {number} [opts.time] - The transition duration in milliseconds. - * @param {boolean} [opts.blocking] - The boolean flag to block the UI during the transition. - * Defaults `true`. - */ - addClass(el, names, opts = {}) { - names = Array.isArray(names) ? names : names.split(" "); - let owner = hook.__view().liveSocket.owner(el); - js_default.addOrRemoveClasses(el, names, [], opts.transition, opts.time, owner, opts.blocking); - }, - /** - * Removes CSS classes from an element. - * - * @param {HTMLElement} el - The element to remove classes from. - * @param {string|string[]} names - The class name(s) to remove. - * @param {Object} [opts={}] - Optional settings. - * @param {string} [opts.transition] - The CSS transition classes to set. - * Accepts a string of classes to apply when removing classes or - * a 3-tuple containing the transition class, the class to apply - * to start the transition, and the ending transition class, such as: - * - * ["ease-out duration-300", "opacity-100", "opacity-0"] - * - * @param {number} [opts.time] - The transition duration in milliseconds. - * @param {boolean} [opts.blocking] - The boolean flag to block the UI during the transition. - * Defaults `true`. - */ - removeClass(el, names, opts = {}) { - opts.transition = js_default.transitionClasses(opts.transition); - names = Array.isArray(names) ? names : names.split(" "); - let owner = hook.__view().liveSocket.owner(el); - js_default.addOrRemoveClasses(el, [], names, opts.transition, opts.time, owner, opts.blocking); - }, - /** - * Toggles CSS classes on an element. - * - * @param {HTMLElement} el - The element to toggle classes on. - * @param {string|string[]} names - The class name(s) to toggle. - * @param {Object} [opts={}] - Optional settings. - * @param {string} [opts.transition] - The CSS transition classes to set. - * Accepts a string of classes to apply when toggling classes or - * a 3-tuple containing the transition class, the class to apply - * to start the transition, and the ending transition class, such as: - * - * ["ease-out duration-300", "opacity-100", "opacity-0"] - * - * @param {number} [opts.time] - The transition duration in milliseconds. - * @param {boolean} [opts.blocking] - The boolean flag to block the UI during the transition. - * Defaults `true`. - */ - toggleClass(el, names, opts = {}) { - opts.transition = js_default.transitionClasses(opts.transition); - names = Array.isArray(names) ? names : names.split(" "); - let owner = hook.__view().liveSocket.owner(el); - js_default.toggleClasses(el, names, opts.transition, opts.time, owner, opts.blocking); - }, - /** - * Applies a CSS transition to an element. - * - * @param {HTMLElement} el - The element to apply the transition to. - * @param {string|string[]} transition - The transition class(es) to apply. - * Accepts a string of classes to apply when transitioning or - * a 3-tuple containing the transition class, the class to apply - * to start the transition, and the ending transition class, such as: - * - * ["ease-out duration-300", "opacity-100", "opacity-0"] - * - * @param {Object} [opts={}] - Optional settings. - * @param {number} [opts.time] - The transition duration in milliseconds. - * @param {boolean} [opts.blocking] - The boolean flag to block the UI during the transition. - * Defaults `true`. - */ - transition(el, transition, opts = {}) { - let owner = hook.__view().liveSocket.owner(el); - js_default.addOrRemoveClasses(el, [], [], js_default.transitionClasses(transition), opts.time, owner, opts.blocking); - }, - /** - * Sets an attribute on an element. - * - * @param {HTMLElement} el - The element to set the attribute on. - * @param {string} attr - The attribute name to set. - * @param {string} val - The value to set for the attribute. - */ - setAttribute(el, attr, val) { - js_default.setOrRemoveAttrs(el, [[attr, val]], []); - }, - /** - * Removes an attribute from an element. - * - * @param {HTMLElement} el - The element to remove the attribute from. - * @param {string} attr - The attribute name to remove. - */ - removeAttribute(el, attr) { - js_default.setOrRemoveAttrs(el, [], [attr]); - }, - /** - * Toggles an attribute on an element between two values. - * - * @param {HTMLElement} el - The element to toggle the attribute on. - * @param {string} attr - The attribute name to toggle. - * @param {string} val1 - The first value to toggle between. - * @param {string} val2 - The second value to toggle between. - */ - toggleAttribute(el, attr, val1, val2) { - js_default.toggleAttr(el, attr, val1, val2); - } - }; - } - pushEvent(event, payload = {}, onReply = function() { - }) { - return this.__view().pushHookEvent(this.el, null, event, payload, onReply); - } - pushEventTo(phxTarget, event, payload = {}, onReply = function() { - }) { - return this.__view().withinTargets(phxTarget, (view, targetCtx) => { - return view.pushHookEvent(this.el, targetCtx, event, payload, onReply); - }); - } - handleEvent(event, callback) { - let callbackRef = (customEvent, bypass) => bypass ? event : callback(customEvent.detail); - window.addEventListener(`phx:${event}`, callbackRef); - this.__listeners.add(callbackRef); - return callbackRef; - } - removeHandleEvent(callbackRef) { - let event = callbackRef(null, true); - window.removeEventListener(`phx:${event}`, callbackRef); - this.__listeners.delete(callbackRef); - } - upload(name, files) { - return this.__view().dispatchUploads(null, name, files); - } - uploadTo(phxTarget, name, files) { - return this.__view().withinTargets(phxTarget, (view, targetCtx) => { - view.dispatchUploads(targetCtx, name, files); - }); - } - __cleanup__() { - this.__listeners.forEach((callbackRef) => this.removeHandleEvent(callbackRef)); - } -}; - -// js/phoenix_live_view/view.js -var prependFormDataKey = (key, prefix) => { - let isArray = key.endsWith("[]"); - let baseKey = isArray ? key.slice(0, -2) : key; - baseKey = baseKey.replace(/([^\[\]]+)(\]?$)/, `${prefix}$1$2`); - if (isArray) { - baseKey += "[]"; - } - return baseKey; -}; -var serializeForm = (form, metadata, onlyNames = []) => { - const { submitter, ...meta } = metadata; - let injectedElement; - if (submitter && submitter.name) { - const input = document.createElement("input"); - input.type = "hidden"; - const formId = submitter.getAttribute("form"); - if (formId) { - input.setAttribute("form", formId); - } - input.name = submitter.name; - input.value = submitter.value; - submitter.parentElement.insertBefore(input, submitter); - injectedElement = input; - } - const formData = new FormData(form); - const toRemove = []; - formData.forEach((val, key, _index) => { - if (val instanceof File) { - toRemove.push(key); - } - }); - toRemove.forEach((key) => formData.delete(key)); - const params = new URLSearchParams(); - let elements = Array.from(form.elements); - for (let [key, val] of formData.entries()) { - if (onlyNames.length === 0 || onlyNames.indexOf(key) >= 0) { - let inputs = elements.filter((input) => input.name === key); - let isUnused = !inputs.some((input) => dom_default.private(input, PHX_HAS_FOCUSED) || dom_default.private(input, PHX_HAS_SUBMITTED)); - let hidden = inputs.every((input) => input.type === "hidden"); - if (isUnused && !(submitter && submitter.name == key) && !hidden) { - params.append(prependFormDataKey(key, "_unused_"), ""); - } - params.append(key, val); - } - } - if (submitter && injectedElement) { - submitter.parentElement.removeChild(injectedElement); - } - for (let metaKey in meta) { - params.append(metaKey, meta[metaKey]); - } - return params.toString(); -}; -var View = class _View { - static closestView(el) { - let liveViewEl = el.closest(PHX_VIEW_SELECTOR); - return liveViewEl ? dom_default.private(liveViewEl, "view") : null; - } - constructor(el, liveSocket, parentView, flash, liveReferer) { - this.isDead = false; - this.liveSocket = liveSocket; - this.flash = flash; - this.parent = parentView; - this.root = parentView ? parentView.root : this; - this.el = el; - dom_default.putPrivate(this.el, "view", this); - this.id = this.el.id; - this.ref = 0; - this.lastAckRef = null; - this.childJoins = 0; - this.loaderTimer = null; - this.pendingDiffs = []; - this.pendingForms = /* @__PURE__ */ new Set(); - this.redirect = false; - this.href = null; - this.joinCount = this.parent ? this.parent.joinCount - 1 : 0; - this.joinAttempts = 0; - this.joinPending = true; - this.destroyed = false; - this.joinCallback = function(onDone) { - onDone && onDone(); - }; - this.stopCallback = function() { - }; - this.pendingJoinOps = this.parent ? null : []; - this.viewHooks = {}; - this.formSubmits = []; - this.children = this.parent ? null : {}; - this.root.children[this.id] = {}; - this.formsForRecovery = {}; - this.channel = this.liveSocket.channel(`lv:${this.id}`, () => { - let url = this.href && this.expandURL(this.href); - return { - redirect: this.redirect ? url : void 0, - url: this.redirect ? void 0 : url || void 0, - params: this.connectParams(liveReferer), - session: this.getSession(), - static: this.getStatic(), - flash: this.flash - }; - }); - } - setHref(href) { - this.href = href; - } - setRedirect(href) { - this.redirect = true; - this.href = href; - } - isMain() { - return this.el.hasAttribute(PHX_MAIN); - } - connectParams(liveReferer) { - let params = this.liveSocket.params(this.el); - let manifest = dom_default.all(document, `[${this.binding(PHX_TRACK_STATIC)}]`).map((node) => node.src || node.href).filter((url) => typeof url === "string"); - if (manifest.length > 0) { - params["_track_static"] = manifest; - } - params["_mounts"] = this.joinCount; - params["_mount_attempts"] = this.joinAttempts; - params["_live_referer"] = liveReferer; - this.joinAttempts++; - return params; - } - isConnected() { - return this.channel.canPush(); - } - getSession() { - return this.el.getAttribute(PHX_SESSION); - } - getStatic() { - let val = this.el.getAttribute(PHX_STATIC); - return val === "" ? null : val; - } - destroy(callback = function() { - }) { - this.destroyAllChildren(); - this.destroyed = true; - delete this.root.children[this.id]; - if (this.parent) { - delete this.root.children[this.parent.id][this.id]; - } - clearTimeout(this.loaderTimer); - let onFinished = () => { - callback(); - for (let id in this.viewHooks) { - this.destroyHook(this.viewHooks[id]); - } - }; - dom_default.markPhxChildDestroyed(this.el); - this.log("destroyed", () => ["the child has been removed from the parent"]); - 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.add(...classes); - } - showLoader(timeout) { - clearTimeout(this.loaderTimer); - if (timeout) { - this.loaderTimer = setTimeout(() => this.showLoader(), timeout); - } else { - for (let id in this.viewHooks) { - this.viewHooks[id].__disconnected(); - } - this.setContainerClasses(PHX_LOADING_CLASS); - } - } - execAll(binding) { - dom_default.all(this.el, `[${binding}]`, (el) => this.liveSocket.execJS(el, el.getAttribute(binding))); - } - hideLoader() { - clearTimeout(this.loaderTimer); - this.setContainerClasses(PHX_CONNECTED_CLASS); - this.execAll(this.binding("connected")); - } - triggerReconnected() { - for (let id in this.viewHooks) { - this.viewHooks[id].__reconnected(); - } - } - log(kind, msgCallback) { - this.liveSocket.log(this, kind, msgCallback); - } - transition(time, onStart, onDone = function() { - }) { - 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)); - } - if (isCid(phxTarget)) { - let targets = dom_default.findComponentNodeList(viewEl || this.el, phxTarget); - if (targets.length === 0) { - logError(`no component found matching phx-target of ${phxTarget}`); - } else { - callback(this, parseInt(phxTarget)); - } - } else { - let targets = Array.from(dom.querySelectorAll(phxTarget)); - if (targets.length === 0) { - logError(`nothing found matching the phx-target selector "${phxTarget}"`); - } - targets.forEach((target) => this.liveSocket.owner(target, (view) => callback(view, target))); - } - } - applyDiff(type, rawDiff, callback) { - this.log(type, () => ["", clone(rawDiff)]); - let { diff, reply, events, title } = Rendered.extract(rawDiff); - callback({ diff, reply, events }); - if (typeof title === "string") { - window.requestAnimationFrame(() => dom_default.putTitle(title)); - } - } - onJoin(resp) { - let { rendered, container, liveview_version } = resp; - if (container) { - let [tag, attrs] = container; - this.el = dom_default.replaceRootContainer(this.el, tag, attrs); - } - this.childJoins = 0; - this.joinPending = true; - this.flash = null; - if (this.root === this) { - this.formsForRecovery = this.getFormsForRecovery(); - } - if (this.isMain()) { - this.liveSocket.replaceRootHistory(); - } - if (liveview_version !== this.liveSocket.version()) { - console.error(`LiveView asset version mismatch. JavaScript version ${this.liveSocket.version()} vs. server ${liveview_version}. To avoid issues, please ensure that your assets use the same version as the server.`); - } - browser_default.dropLocal(this.liveSocket.localStorage, window.location.pathname, CONSECUTIVE_RELOADS); - this.applyDiff("mount", rendered, ({ diff, events }) => { - this.rendered = new Rendered(this.id, diff); - let [html, streams] = this.renderContainer(null, "join"); - this.dropPendingRefs(); - this.joinCount++; - this.joinAttempts = 0; - this.maybeRecoverForms(html, () => { - this.onJoinComplete(resp, html, streams, events); - }); - }); - } - dropPendingRefs() { - dom_default.all(document, `[${PHX_REF_SRC}="${this.refSrc()}"]`, (el) => { - el.removeAttribute(PHX_REF_LOADING); - el.removeAttribute(PHX_REF_SRC); - el.removeAttribute(PHX_REF_LOCK); - }); - } - onJoinComplete({ live_patch }, html, streams, events) { - if (this.joinCount > 1 || this.parent && !this.parent.isJoinPending()) { - return this.applyJoinPatch(live_patch, html, streams, events); - } - let newChildren = dom_default.findPhxChildrenInFragment(html, this.id).filter((toEl) => { - let fromEl = toEl.id && this.el.querySelector(`[id="${toEl.id}"]`); - let phxStatic = fromEl && fromEl.getAttribute(PHX_STATIC); - if (phxStatic) { - toEl.setAttribute(PHX_STATIC, phxStatic); - } - if (fromEl) { - fromEl.setAttribute(PHX_ROOT_ID, this.root.id); - } - return this.joinChild(toEl); - }); - if (newChildren.length === 0) { - if (this.parent) { - this.root.pendingJoinOps.push([this, () => this.applyJoinPatch(live_patch, html, streams, events)]); - this.parent.ackJoin(this); - } else { - this.onAllChildJoinsComplete(); - this.applyJoinPatch(live_patch, html, streams, events); - } - } else { - this.root.pendingJoinOps.push([this, () => this.applyJoinPatch(live_patch, html, streams, events)]); - } - } - attachTrueDocEl() { - 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(parent = this.el) { - let phxViewportTop = this.binding(PHX_VIEWPORT_TOP); - let phxViewportBottom = this.binding(PHX_VIEWPORT_BOTTOM); - dom_default.all(parent, `[${phxViewportTop}], [${phxViewportBottom}]`, (hookEl) => { - if (this.ownsElement(hookEl)) { - dom_default.maintainPrivateHooks(hookEl, hookEl, phxViewportTop, phxViewportBottom); - this.maybeAddNewHook(hookEl); - } - }); - dom_default.all(parent, `[${this.binding(PHX_HOOK)}], [data-phx-${PHX_HOOK}]`, (hookEl) => { - if (this.ownsElement(hookEl)) { - this.maybeAddNewHook(hookEl); - } - }); - dom_default.all(parent, `[${this.binding(PHX_MOUNTED)}]`, (el) => { - if (this.ownsElement(el)) { - this.maybeMounted(el); - } - }); - } - applyJoinPatch(live_patch, html, streams, events) { - this.attachTrueDocEl(); - let patch = new DOMPatch(this, this.el, this.id, html, streams, null); - patch.markPrunableContentForRemoval(); - this.performPatch(patch, false, true); - this.joinNewChildren(); - this.execNewMounted(); - this.joinPending = false; - this.liveSocket.dispatchEvents(events); - this.applyPendingUpdates(); - if (live_patch) { - let { kind, to } = live_patch; - this.liveSocket.historyPatch(to, kind); - } - this.hideLoader(); - if (this.joinCount > 1) { - this.triggerReconnected(); - } - this.stopCallback(); - } - triggerBeforeUpdateHook(fromEl, toEl) { - this.liveSocket.triggerDOM("onBeforeElUpdated", [fromEl, toEl]); - let hook = this.getHook(fromEl); - let isIgnored = hook && dom_default.isIgnored(fromEl, this.binding(PHX_UPDATE)); - if (hook && !fromEl.isEqualNode(toEl) && !(isIgnored && isEqualObj(fromEl.dataset, toEl.dataset))) { - hook.__beforeUpdate(); - return hook; - } - } - maybeMounted(el) { - let phxMounted = el.getAttribute(this.binding(PHX_MOUNTED)); - let hasBeenInvoked = phxMounted && dom_default.private(el, "mounted"); - if (phxMounted && !hasBeenInvoked) { - this.liveSocket.execJS(el, phxMounted); - dom_default.putPrivate(el, "mounted", true); - } - } - maybeAddNewHook(el) { - let newHook = this.addHook(el); - if (newHook) { - newHook.__mounted(); - } - } - performPatch(patch, pruneCids, isJoinPatch = false) { - let removedEls = []; - let phxChildrenAdded = false; - let updatedHookIds = /* @__PURE__ */ new Set(); - this.liveSocket.triggerDOM("onPatchStart", [patch.targetContainer]); - patch.after("added", (el) => { - this.liveSocket.triggerDOM("onNodeAdded", [el]); - let phxViewportTop = this.binding(PHX_VIEWPORT_TOP); - let phxViewportBottom = this.binding(PHX_VIEWPORT_BOTTOM); - dom_default.maintainPrivateHooks(el, el, phxViewportTop, phxViewportBottom); - this.maybeAddNewHook(el); - if (el.getAttribute) { - this.maybeMounted(el); - } - }); - patch.after("phxChildAdded", (el) => { - if (dom_default.isPhxSticky(el)) { - this.liveSocket.joinRootViews(); - } else { - phxChildrenAdded = true; - } - }); - patch.before("updated", (fromEl, toEl) => { - let hook = this.triggerBeforeUpdateHook(fromEl, toEl); - if (hook) { - updatedHookIds.add(fromEl.id); - } - }); - patch.after("updated", (el) => { - if (updatedHookIds.has(el.id)) { - this.getHook(el).__updated(); - } - }); - patch.after("discarded", (el) => { - if (el.nodeType === Node.ELEMENT_NODE) { - removedEls.push(el); - } - }); - patch.after("transitionsDiscarded", (els) => this.afterElementsRemoved(els, pruneCids)); - patch.perform(isJoinPatch); - this.afterElementsRemoved(removedEls, pruneCids); - this.liveSocket.triggerDOM("onPatchEnd", [patch.targetContainer]); - return phxChildrenAdded; - } - afterElementsRemoved(elements, pruneCids) { - let destroyedCIDs = []; - elements.forEach((parent) => { - let components = dom_default.all(parent, `[${PHX_COMPONENT}]`); - let hooks = dom_default.all(parent, `[${this.binding(PHX_HOOK)}], [data-phx-hook]`); - components.concat(parent).forEach((el) => { - let cid = this.componentID(el); - if (isCid(cid) && destroyedCIDs.indexOf(cid) === -1) { - destroyedCIDs.push(cid); - } - }); - hooks.concat(parent).forEach((hookEl) => { - let hook = this.getHook(hookEl); - hook && this.destroyHook(hook); - }); - }); - if (pruneCids) { - this.maybePushComponentsDestroyed(destroyedCIDs); - } - } - joinNewChildren() { - dom_default.findPhxChildren(this.el, this.id).forEach((el) => this.joinChild(el)); - } - maybeRecoverForms(html, callback) { - const phxChange = this.binding("change"); - const oldForms = this.root.formsForRecovery; - let template = document.createElement("template"); - template.innerHTML = html; - const rootEl = template.content.firstElementChild; - rootEl.id = this.id; - rootEl.setAttribute(PHX_ROOT_ID, this.root.id); - 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 = ( - // 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(); - } - formsToRecover.forEach(([oldForm, newForm], i) => { - this.pendingForms.add(newForm.id); - this.pushFormRecovery(oldForm, newForm, template.content.firstElementChild, () => { - this.pendingForms.delete(newForm.id); - if (i === formsToRecover.length - 1) { - callback(); - } - }); - }); - } - getChildById(id) { - return this.root.children[this.id][id]; - } - getDescendentByEl(el) { - if (el.id === this.id) { - return this; - } else { - return this.children[el.getAttribute(PHX_PARENT_ID)]?.[el.id]; - } - } - destroyDescendent(id) { - for (let parentId in this.root.children) { - for (let childId in this.root.children[parentId]) { - if (childId === id) { - return this.root.children[parentId][childId].destroy(); - } - } - } - } - joinChild(el) { - let child = this.getChildById(el.id); - if (!child) { - let view = new _View(el, this.liveSocket, this); - this.root.children[this.id][view.id] = view; - view.join(); - this.childJoins++; - return true; - } - } - isJoinPending() { - return this.joinPending; - } - ackJoin(_child) { - this.childJoins--; - if (this.childJoins === 0) { - if (this.parent) { - this.parent.ackJoin(this); - } else { - this.onAllChildJoinsComplete(); - } - } - } - onAllChildJoinsComplete() { - this.pendingForms.clear(); - this.formsForRecovery = {}; - this.joinCallback(() => { - this.pendingJoinOps.forEach(([view, op]) => { - if (!view.isDestroyed()) { - op(); - } - }); - this.pendingJoinOps = []; - }); - } - update(diff, events) { - if (this.isJoinPending() || this.liveSocket.hasPendingLink() && this.root.isMain()) { - return this.pendingDiffs.push({ diff, events }); - } - this.rendered.mergeDiff(diff); - let phxChildrenAdded = false; - if (this.rendered.isComponentOnlyDiff(diff)) { - this.liveSocket.time("component patch complete", () => { - let parentCids = dom_default.findExistingParentCIDs(this.el, this.rendered.componentCIDs(diff)); - parentCids.forEach((parentCID) => { - if (this.componentPatch(this.rendered.getComponent(diff, parentCID), parentCID)) { - phxChildrenAdded = true; - } - }); - }); - } else if (!isEmpty(diff)) { - this.liveSocket.time("full patch complete", () => { - let [html, streams] = this.renderContainer(diff, "update"); - let patch = new DOMPatch(this, this.el, this.id, html, streams, null); - phxChildrenAdded = this.performPatch(patch, true); - }); - } - this.liveSocket.dispatchEvents(events); - if (phxChildrenAdded) { - this.joinNewChildren(); - } - } - renderContainer(diff, kind) { - return this.liveSocket.time(`toString diff (${kind})`, () => { - let tag = this.el.tagName; - let cids = diff ? this.rendered.componentCIDs(diff) : null; - let [html, streams] = this.rendered.toString(cids); - return [`<${tag}>${html}`, streams]; - }); - } - componentPatch(diff, cid) { - if (isEmpty(diff)) - return false; - let [html, streams] = this.rendered.componentToString(cid); - let patch = new DOMPatch(this, this.el, this.id, html, streams, cid); - let childrenAdded = this.performPatch(patch, true); - return childrenAdded; - } - getHook(el) { - return this.viewHooks[ViewHook.elementID(el)]; - } - addHook(el) { - let hookElId = ViewHook.elementID(el); - if (hookElId && !this.viewHooks[hookElId]) { - let hook = dom_default.getCustomElHook(el) || logError(`no hook found for custom element: ${el.id}`); - this.viewHooks[hookElId] = hook; - hook.__attachView(this); - return hook; - } else if (hookElId || !el.getAttribute) { - return; - } else { - let hookName = el.getAttribute(`data-phx-${PHX_HOOK}`) || el.getAttribute(this.binding(PHX_HOOK)); - if (hookName && !this.ownsElement(el)) { - return; - } - let callbacks = this.liveSocket.getHookCallbacks(hookName); - if (callbacks) { - if (!el.id) { - logError(`no DOM ID for hook "${hookName}". Hooks require a unique ID on each element.`, el); - } - let hook = new ViewHook(this, el, callbacks); - this.viewHooks[ViewHook.elementID(hook.el)] = hook; - return hook; - } else if (hookName !== null) { - logError(`unknown hook found for "${hookName}"`, el); - } - } - } - destroyHook(hook) { - hook.__destroyed(); - hook.__cleanup__(); - delete this.viewHooks[ViewHook.elementID(hook.el)]; - } - applyPendingUpdates() { - this.pendingDiffs.forEach(({ diff, events }) => this.update(diff, events)); - this.pendingDiffs = []; - this.eachChild((child) => child.applyPendingUpdates()); - } - eachChild(callback) { - let children = this.root.children[this.id] || {}; - for (let id in children) { - callback(this.getChildById(id)); - } - } - onChannel(event, cb) { - this.liveSocket.onChannel(this.channel, event, (resp) => { - if (this.isJoinPending()) { - this.root.pendingJoinOps.push([this, () => cb(resp)]); - } else { - this.liveSocket.requestDOMUpdate(() => cb(resp)); - } - }); - } - bindChannel() { - this.liveSocket.onChannel(this.channel, "diff", (rawDiff) => { - this.liveSocket.requestDOMUpdate(() => { - this.applyDiff("update", rawDiff, ({ diff, events }) => this.update(diff, events)); - }); - }); - this.onChannel("redirect", ({ to, flash }) => this.onRedirect({ to, flash })); - this.onChannel("live_patch", (redir) => this.onLivePatch(redir)); - this.onChannel("live_redirect", (redir) => this.onLiveRedirect(redir)); - this.channel.onError((reason) => this.onError(reason)); - this.channel.onClose((reason) => this.onClose(reason)); - } - destroyAllChildren() { - this.eachChild((child) => child.destroy()); - } - onLiveRedirect(redir) { - let { to, kind, flash } = redir; - let url = this.expandURL(to); - let e = new CustomEvent("phx:server-navigate", { detail: { to, kind, flash } }); - this.liveSocket.historyRedirect(e, url, kind, flash); - } - onLivePatch(redir) { - let { to, kind } = redir; - this.href = this.expandURL(to); - this.liveSocket.historyPatch(to, kind); - } - expandURL(to) { - return to.startsWith("/") ? `${window.location.protocol}//${window.location.host}${to}` : to; - } - onRedirect({ to, flash, reloadToken }) { - this.liveSocket.redirect(to, flash, reloadToken); - } - isDestroyed() { - return this.destroyed; - } - joinDead() { - this.isDead = true; - } - joinPush() { - this.joinPush = this.joinPush || this.channel.join(); - return this.joinPush; - } - join(callback) { - this.showLoader(this.liveSocket.loaderTimeout); - this.bindChannel(); - if (this.isMain()) { - this.stopCallback = this.liveSocket.withPageLoading({ to: this.href, kind: "initial" }); - } - this.joinCallback = (onDone) => { - onDone = onDone || function() { - }; - callback ? callback(this.joinCount, onDone) : onDone(); - }; - this.wrapPush(() => this.channel.join(), { - ok: (resp) => this.liveSocket.requestDOMUpdate(() => this.onJoin(resp)), - error: (error) => this.onJoinError(error), - timeout: () => this.onJoinError({ reason: "timeout" }) - }); - } - onJoinError(resp) { - if (resp.reason === "reload") { - this.log("error", () => [`failed mount with ${resp.status}. Falling back to page reload`, resp]); - this.onRedirect({ to: this.root.href, reloadToken: resp.token }); - return; - } else if (resp.reason === "unauthorized" || resp.reason === "stale") { - this.log("error", () => ["unauthorized live_redirect. Falling back to page request", resp]); - this.onRedirect({ to: this.root.href }); - return; - } - if (resp.redirect || resp.live_redirect) { - this.joinPending = false; - this.channel.leave(); - } - if (resp.redirect) { - return this.onRedirect(resp.redirect); - } - if (resp.live_redirect) { - return this.onLiveRedirect(resp.live_redirect); - } - this.log("error", () => ["unable to join", resp]); - if (this.isMain()) { - this.displayError([PHX_LOADING_CLASS, PHX_ERROR_CLASS, PHX_SERVER_ERROR_CLASS]); - if (this.liveSocket.isConnected()) { - this.liveSocket.reloadWithJitter(this); - } - } else { - if (this.joinAttempts >= MAX_CHILD_JOIN_ATTEMPTS) { - this.root.displayError([PHX_LOADING_CLASS, PHX_ERROR_CLASS, PHX_SERVER_ERROR_CLASS]); - this.log("error", () => [`giving up trying to mount after ${MAX_CHILD_JOIN_ATTEMPTS} tries`, resp]); - this.destroy(); - } - let trueChildEl = dom_default.byId(this.el.id); - if (trueChildEl) { - dom_default.mergeAttrs(trueChildEl, this.el); - this.displayError([PHX_LOADING_CLASS, PHX_ERROR_CLASS, PHX_SERVER_ERROR_CLASS]); - this.el = trueChildEl; - } else { - this.destroy(); - } - } - } - onClose(reason) { - if (this.isDestroyed()) { - return; - } - if (this.isMain() && this.liveSocket.hasPendingLink() && reason !== "leave") { - return this.liveSocket.reloadWithJitter(this); - } - this.destroyAllChildren(); - this.liveSocket.dropActiveElement(this); - if (document.activeElement) { - document.activeElement.blur(); - } - if (this.liveSocket.isUnloaded()) { - this.showLoader(BEFORE_UNLOAD_LOADER_TIMEOUT); - } - } - onError(reason) { - this.onClose(reason); - if (this.liveSocket.isConnected()) { - this.log("error", () => ["view crashed", reason]); - } - if (!this.liveSocket.isUnloaded()) { - if (this.liveSocket.isConnected()) { - this.displayError([PHX_LOADING_CLASS, PHX_ERROR_CLASS, PHX_SERVER_ERROR_CLASS]); - } else { - this.displayError([PHX_LOADING_CLASS, PHX_ERROR_CLASS, PHX_CLIENT_ERROR_CLASS]); - } - } - } - displayError(classes) { - if (this.isMain()) { - dom_default.dispatchEvent(window, "phx:page-loading-start", { detail: { to: this.href, kind: "error" } }); - } - this.showLoader(); - this.setContainerClasses(...classes); - this.execAll(this.binding("disconnected")); - } - wrapPush(callerPush, receives) { - let latency = this.liveSocket.getLatencySim(); - let withLatency = latency ? (cb) => setTimeout(() => !this.isDestroyed() && cb(), latency) : (cb) => !this.isDestroyed() && cb(); - withLatency(() => { - callerPush().receive("ok", (resp) => withLatency(() => receives.ok && receives.ok(resp))).receive("error", (reason) => withLatency(() => receives.error && receives.error(reason))).receive("timeout", () => withLatency(() => receives.timeout && receives.timeout())); - }); - } - pushWithReply(refGenerator, event, payload) { - if (!this.isConnected()) { - return Promise.reject({ error: "noconnection" }); - } - let [ref, [el], opts] = refGenerator ? refGenerator() : [null, [], {}]; - let oldJoinCount = this.joinCount; - let onLoadingDone = function() { - }; - if (opts.page_loading) { - onLoadingDone = this.liveSocket.withPageLoading({ kind: "element", target: el }); - } - if (typeof payload.cid !== "number") { - delete payload.cid; - } - return new Promise((resolve, reject) => { - this.wrapPush(() => this.channel.push(event, payload, PUSH_TIMEOUT), { - ok: (resp) => { - if (ref !== null) { - this.lastAckRef = ref; - } - let finish = (hookReply) => { - if (resp.redirect) { - this.onRedirect(resp.redirect); - } - if (resp.live_patch) { - this.onLivePatch(resp.live_patch); - } - if (resp.live_redirect) { - this.onLiveRedirect(resp.live_redirect); - } - onLoadingDone(); - resolve({ resp, reply: hookReply }); - }; - if (resp.diff) { - this.liveSocket.requestDOMUpdate(() => { - this.applyDiff("update", resp.diff, ({ diff, reply, events }) => { - if (ref !== null) { - this.undoRefs(ref, payload.event); - } - this.update(diff, events); - finish(reply); - }); - }); - } else { - if (ref !== null) { - this.undoRefs(ref, payload.event); - } - finish(null); - } - }, - error: (reason) => reject({ error: reason }), - timeout: () => { - reject({ timeout: true }); - if (this.joinCount === oldJoinCount) { - this.liveSocket.reloadWithJitter(this, () => { - this.log("timeout", () => ["received timeout while communicating with server. Falling back to hard refresh for recovery"]); - }); - } - } - }); - }); - } - undoRefs(ref, phxEvent, onlyEls) { - if (!this.isConnected()) { - return; - } - let selector = `[${PHX_REF_SRC}="${this.refSrc()}"]`; - if (onlyEls) { - onlyEls = new Set(onlyEls); - dom_default.all(document, selector, (parent) => { - if (onlyEls && !onlyEls.has(parent)) { - return; - } - dom_default.all(parent, selector, (child) => this.undoElRef(child, ref, phxEvent)); - this.undoElRef(parent, ref, phxEvent); - }); - } else { - dom_default.all(document, selector, (el) => this.undoElRef(el, ref, phxEvent)); - } - } - undoElRef(el, ref, phxEvent) { - let elRef = new ElementRef(el); - elRef.maybeUndo(ref, phxEvent, (clonedTree) => { - let hook = this.triggerBeforeUpdateHook(el, clonedTree); - DOMPatch.patchWithClonedTree(el, clonedTree, this.liveSocket); - dom_default.all(el, `[${PHX_REF_SRC}="${this.refSrc()}"]`, (child) => this.undoElRef(child, ref, phxEvent)); - this.execNewMounted(el); - if (hook) { - hook.__updated(); - } - }); - } - refSrc() { - return this.el.id; - } - putRef(elements, phxEvent, eventType, opts = {}) { - let newRef = this.ref++; - let disableWith = this.binding(PHX_DISABLE_WITH); - if (opts.loading) { - let loadingEls = dom_default.all(document, opts.loading).map((el) => { - return { el, lock: true, loading: true }; - }); - elements = elements.concat(loadingEls); - } - for (let { el, lock, loading } of elements) { - if (!lock && !loading) { - throw new Error("putRef requires lock or loading"); - } - el.setAttribute(PHX_REF_SRC, this.refSrc()); - if (loading) { - el.setAttribute(PHX_REF_LOADING, newRef); - } - if (lock) { - el.setAttribute(PHX_REF_LOCK, newRef); - } - if (!loading || opts.submitter && !(el === opts.submitter || el === opts.form)) { - continue; - } - let lockCompletePromise = new Promise((resolve) => { - el.addEventListener(`phx:undo-lock:${newRef}`, () => resolve(detail), { once: true }); - }); - let loadingCompletePromise = new Promise((resolve) => { - el.addEventListener(`phx:undo-loading:${newRef}`, () => resolve(detail), { once: true }); - }); - el.classList.add(`phx-${eventType}-loading`); - let disableText = el.getAttribute(disableWith); - if (disableText !== null) { - if (!el.getAttribute(PHX_DISABLE_WITH_RESTORE)) { - el.setAttribute(PHX_DISABLE_WITH_RESTORE, el.innerText); - } - if (disableText !== "") { - el.innerText = disableText; - } - el.setAttribute(PHX_DISABLED, el.getAttribute(PHX_DISABLED) || el.disabled); - el.setAttribute("disabled", ""); - } - let detail = { - event: phxEvent, - eventType, - ref: newRef, - isLoading: loading, - isLocked: lock, - lockElements: elements.filter(({ lock: lock2 }) => lock2).map(({ el: el2 }) => el2), - loadingElements: elements.filter(({ loading: loading2 }) => loading2).map(({ el: el2 }) => el2), - unlock: (els) => { - els = Array.isArray(els) ? els : [els]; - this.undoRefs(newRef, phxEvent, els); - }, - lockComplete: lockCompletePromise, - loadingComplete: loadingCompletePromise, - lock: (lockEl) => { - return new Promise((resolve) => { - if (this.isAcked(newRef)) { - return resolve(detail); - } - lockEl.setAttribute(PHX_REF_LOCK, newRef); - lockEl.setAttribute(PHX_REF_SRC, this.refSrc()); - lockEl.addEventListener(`phx:lock-stop:${newRef}`, () => resolve(detail), { once: true }); - }); - } - }; - el.dispatchEvent(new CustomEvent(`phx:push`, { - detail, - bubbles: true, - cancelable: false - })); - if (phxEvent) { - el.dispatchEvent(new CustomEvent(`phx:push:${phxEvent}`, { - detail, - bubbles: true, - cancelable: false - })); - } - } - return [newRef, elements.map(({ el }) => el), opts]; - } - isAcked(ref) { - return this.lastAckRef !== null && this.lastAckRef >= ref; - } - componentID(el) { - let cid = el.getAttribute && el.getAttribute(PHX_COMPONENT); - return cid ? parseInt(cid) : null; - } - targetComponentID(target, targetCtx, opts = {}) { - if (isCid(targetCtx)) { - return targetCtx; - } - let cidOrSelector = opts.target || target.getAttribute(this.binding("target")); - if (isCid(cidOrSelector)) { - return parseInt(cidOrSelector); - } else if (targetCtx && (cidOrSelector !== null || opts.target)) { - return this.closestComponentID(targetCtx); - } else { - return null; - } - } - closestComponentID(targetCtx) { - if (isCid(targetCtx)) { - return targetCtx; - } else if (targetCtx) { - return maybe(targetCtx.closest(`[${PHX_COMPONENT}]`), (el) => this.ownsElement(el) && this.componentID(el)); - } else { - return null; - } - } - pushHookEvent(el, targetCtx, event, payload, onReply) { - if (!this.isConnected()) { - this.log("hook", () => ["unable to push hook event. LiveView not connected", event, payload]); - return false; - } - let [ref, els, opts] = this.putRef([{ el, loading: true, lock: true }], event, "hook"); - this.pushWithReply(() => [ref, els, opts], "event", { - type: "hook", - event, - value: payload, - cid: this.closestComponentID(targetCtx) - }).then(({ resp: _resp, reply: hookReply }) => onReply(hookReply, ref)); - return ref; - } - extractMeta(el, meta, value) { - let prefix = this.binding("value-"); - for (let i = 0; i < el.attributes.length; i++) { - if (!meta) { - meta = {}; - } - let name = el.attributes[i].name; - if (name.startsWith(prefix)) { - meta[name.replace(prefix, "")] = el.getAttribute(name); - } - } - if (el.value !== void 0 && !(el instanceof HTMLFormElement)) { - if (!meta) { - meta = {}; - } - meta.value = el.value; - if (el.tagName === "INPUT" && CHECKABLE_INPUTS.indexOf(el.type) >= 0 && !el.checked) { - delete meta.value; - } - } - if (value) { - if (!meta) { - meta = {}; - } - for (let key in value) { - meta[key] = value[key]; - } - } - return meta; - } - pushEvent(type, el, targetCtx, phxEvent, meta, opts = {}, onReply) { - this.pushWithReply(() => this.putRef([{ el, loading: true, lock: true }], phxEvent, type, opts), "event", { - type, - event: phxEvent, - value: this.extractMeta(el, meta, opts.value), - cid: this.targetComponentID(el, targetCtx, opts) - }).then(({ resp, reply }) => onReply && onReply(reply)); - } - pushFileProgress(fileEl, entryRef, progress, onReply = function() { - }) { - this.liveSocket.withinOwners(fileEl.form, (view, targetCtx) => { - view.pushWithReply(null, "progress", { - event: fileEl.getAttribute(view.binding(PHX_PROGRESS)), - ref: fileEl.getAttribute(PHX_UPLOAD_REF), - entry_ref: entryRef, - progress, - cid: view.targetComponentID(fileEl.form, targetCtx) - }).then(({ resp }) => onReply(resp)); - }); - } - pushInput(inputEl, targetCtx, forceCid, phxEvent, opts, callback) { - if (!inputEl.form) { - throw new Error("form events require the input to be inside a form"); - } - let uploads; - let cid = isCid(forceCid) ? forceCid : this.targetComponentID(inputEl.form, targetCtx, opts); - let refGenerator = () => { - return this.putRef([ - { el: inputEl, loading: true, lock: true }, - { el: inputEl.form, loading: true, lock: true } - ], phxEvent, "change", opts); - }; - let formData; - let meta = this.extractMeta(inputEl.form); - if (inputEl instanceof HTMLButtonElement) { - meta.submitter = inputEl; - } - if (inputEl.getAttribute(this.binding("change"))) { - formData = serializeForm(inputEl.form, { _target: opts._target, ...meta }, [inputEl.name]); - } else { - formData = serializeForm(inputEl.form, { _target: opts._target, ...meta }); - } - if (dom_default.isUploadInput(inputEl) && inputEl.files && inputEl.files.length > 0) { - LiveUploader.trackFiles(inputEl, Array.from(inputEl.files)); - } - uploads = LiveUploader.serializeUploads(inputEl); - let event = { - type: "form", - event: phxEvent, - value: formData, - uploads, - cid - }; - this.pushWithReply(refGenerator, "event", event).then(({ resp }) => { - if (dom_default.isUploadInput(inputEl) && dom_default.isAutoUpload(inputEl)) { - if (LiveUploader.filesAwaitingPreflight(inputEl).length > 0) { - let [ref, _els] = refGenerator(); - this.undoRefs(ref, phxEvent, [inputEl.form]); - this.uploadFiles(inputEl.form, phxEvent, targetCtx, ref, cid, (_uploads) => { - callback && callback(resp); - this.triggerAwaitingSubmit(inputEl.form, phxEvent); - this.undoRefs(ref, phxEvent); - }); - } - } else { - callback && callback(resp); - } - }); - } - triggerAwaitingSubmit(formEl, phxEvent) { - let awaitingSubmit = this.getScheduledSubmit(formEl); - if (awaitingSubmit) { - let [_el, _ref, _opts, callback] = awaitingSubmit; - this.cancelSubmit(formEl, phxEvent); - callback(); - } - } - getScheduledSubmit(formEl) { - return this.formSubmits.find(([el, _ref, _opts, _callback]) => el.isSameNode(formEl)); - } - scheduleSubmit(formEl, ref, opts, callback) { - if (this.getScheduledSubmit(formEl)) { - return true; - } - this.formSubmits.push([formEl, ref, opts, callback]); - } - cancelSubmit(formEl, phxEvent) { - this.formSubmits = this.formSubmits.filter(([el, ref, _callback]) => { - if (el.isSameNode(formEl)) { - this.undoRefs(ref, phxEvent); - return false; - } else { - return true; - } - }); - } - disableForm(formEl, phxEvent, opts = {}) { - let filterIgnored = (el) => { - let userIgnored = closestPhxBinding(el, `${this.binding(PHX_UPDATE)}=ignore`, el.form); - return !(userIgnored || closestPhxBinding(el, "data-phx-update=ignore", el.form)); - }; - let filterDisables = (el) => { - return el.hasAttribute(this.binding(PHX_DISABLE_WITH)); - }; - let filterButton = (el) => el.tagName == "BUTTON"; - let filterInput = (el) => ["INPUT", "TEXTAREA", "SELECT"].includes(el.tagName); - let formElements = Array.from(formEl.elements); - let disables = formElements.filter(filterDisables); - let buttons = formElements.filter(filterButton).filter(filterIgnored); - let inputs = formElements.filter(filterInput).filter(filterIgnored); - buttons.forEach((button) => { - button.setAttribute(PHX_DISABLED, button.disabled); - button.disabled = true; - }); - inputs.forEach((input) => { - input.setAttribute(PHX_READONLY, input.readOnly); - input.readOnly = true; - if (input.files) { - input.setAttribute(PHX_DISABLED, input.disabled); - input.disabled = true; - } - }); - let formEls = disables.concat(buttons).concat(inputs).map((el) => { - return { el, loading: true, lock: true }; - }); - let els = [{ el: formEl, loading: true, lock: false }].concat(formEls).reverse(); - return this.putRef(els, phxEvent, "submit", opts); - } - pushFormSubmit(formEl, targetCtx, phxEvent, submitter, opts, onReply) { - let refGenerator = () => this.disableForm(formEl, phxEvent, { - ...opts, - form: formEl, - submitter - }); - let cid = this.targetComponentID(formEl, targetCtx); - if (LiveUploader.hasUploadsInProgress(formEl)) { - let [ref, _els] = refGenerator(); - let push = () => this.pushFormSubmit(formEl, targetCtx, phxEvent, submitter, opts, onReply); - return this.scheduleSubmit(formEl, ref, opts, push); - } else if (LiveUploader.inputsAwaitingPreflight(formEl).length > 0) { - let [ref, els] = refGenerator(); - let proxyRefGen = () => [ref, els, opts]; - this.uploadFiles(formEl, phxEvent, targetCtx, ref, cid, (uploads) => { - if (LiveUploader.inputsAwaitingPreflight(formEl).length > 0) { - return this.undoRefs(ref, phxEvent); - } - let meta = this.extractMeta(formEl); - let formData = serializeForm(formEl, { submitter, ...meta }); - this.pushWithReply(proxyRefGen, "event", { - type: "form", - event: phxEvent, - value: formData, - cid - }).then(({ resp }) => onReply(resp)); - }); - } else if (!(formEl.hasAttribute(PHX_REF_SRC) && formEl.classList.contains("phx-submit-loading"))) { - let meta = this.extractMeta(formEl); - let formData = serializeForm(formEl, { submitter, ...meta }); - this.pushWithReply(refGenerator, "event", { - type: "form", - event: phxEvent, - value: formData, - cid - }).then(({ resp }) => onReply(resp)); - } - } - uploadFiles(formEl, phxEvent, targetCtx, ref, cid, onComplete) { - let joinCountAtUpload = this.joinCount; - let inputEls = LiveUploader.activeFileInputs(formEl); - let numFileInputsInProgress = inputEls.length; - inputEls.forEach((inputEl) => { - let uploader = new LiveUploader(inputEl, this, () => { - numFileInputsInProgress--; - if (numFileInputsInProgress === 0) { - onComplete(); - } - }); - let entries = uploader.entries().map((entry) => entry.toPreflightPayload()); - if (entries.length === 0) { - numFileInputsInProgress--; - return; - } - let payload = { - ref: inputEl.getAttribute(PHX_UPLOAD_REF), - entries, - cid: this.targetComponentID(inputEl.form, targetCtx) - }; - this.log("upload", () => ["sending preflight request", payload]); - this.pushWithReply(null, "allow_upload", payload).then(({ resp }) => { - this.log("upload", () => ["got preflight response", resp]); - uploader.entries().forEach((entry) => { - if (resp.entries && !resp.entries[entry.ref]) { - this.handleFailedEntryPreflight(entry.ref, "failed preflight", uploader); - } - }); - if (resp.error || Object.keys(resp.entries).length === 0) { - this.undoRefs(ref, phxEvent); - let errors = resp.error || []; - errors.map(([entry_ref, reason]) => { - this.handleFailedEntryPreflight(entry_ref, reason, uploader); - }); - } else { - let onError = (callback) => { - this.channel.onError(() => { - if (this.joinCount === joinCountAtUpload) { - callback(); - } - }); - }; - uploader.initAdapterUpload(resp, onError, this.liveSocket); - } - }); - }); - } - handleFailedEntryPreflight(uploadRef, reason, uploader) { - if (uploader.isAutoUpload()) { - let entry = uploader.entries().find((entry2) => entry2.ref === uploadRef.toString()); - if (entry) { - entry.cancel(); - } - } else { - uploader.entries().map((entry) => entry.cancel()); - } - this.log("upload", () => [`error for entry ${uploadRef}`, reason]); - } - dispatchUploads(targetCtx, name, filesOrBlobs) { - let targetElement = this.targetCtxElement(targetCtx) || this.el; - let inputs = dom_default.findUploadInputs(targetElement).filter((el) => el.name === name); - if (inputs.length === 0) { - logError(`no live file inputs found matching the name "${name}"`); - } else if (inputs.length > 1) { - logError(`duplicate live file inputs found matching the name "${name}"`); - } else { - dom_default.dispatchEvent(inputs[0], PHX_TRACK_UPLOADS, { detail: { files: filesOrBlobs } }); - } - } - targetCtxElement(targetCtx) { - if (isCid(targetCtx)) { - let [target] = dom_default.findComponentNodeList(this.el, targetCtx); - return target; - } else if (targetCtx) { - return targetCtx; - } else { - return null; - } - } - pushFormRecovery(oldForm, newForm, templateDom, callback) { - const phxChange = this.binding("change"); - const phxTarget = newForm.getAttribute(this.binding("target")) || newForm; - const phxEvent = newForm.getAttribute(this.binding(PHX_AUTO_RECOVER)) || newForm.getAttribute(this.binding("change")); - const inputs = Array.from(oldForm.elements).filter((el) => dom_default.isFormInput(el) && el.name && !el.hasAttribute(phxChange)); - if (inputs.length === 0) { - return; - } - inputs.forEach((input2) => input2.hasAttribute(PHX_UPLOAD_REF) && LiveUploader.clearFiles(input2)); - let input = inputs.find((el) => el.type !== "hidden") || inputs[0]; - let pending = 0; - this.withinTargets(phxTarget, (targetView, targetCtx) => { - const cid = this.targetComponentID(newForm, targetCtx); - pending++; - targetView.pushInput(input, targetCtx, cid, phxEvent, { _target: input.name }, () => { - pending--; - if (pending === 0) { - callback(); - } - }); - }, templateDom, templateDom); - } - pushLinkPatch(e, href, targetEl, callback) { - let linkRef = this.liveSocket.setPendingLink(href); - let loading = e.isTrusted && e.type !== "popstate"; - let refGen = targetEl ? () => this.putRef([{ el: targetEl, loading, lock: true }], null, "click") : null; - let fallback = () => this.liveSocket.redirect(window.location.href); - let url = href.startsWith("/") ? `${location.protocol}//${location.host}${href}` : href; - this.pushWithReply(refGen, "live_patch", { url }).then( - ({ resp }) => { - this.liveSocket.requestDOMUpdate(() => { - if (resp.link_redirect) { - this.liveSocket.replaceMain(href, null, callback, linkRef); - } else { - if (this.liveSocket.commitPendingLink(linkRef)) { - this.href = href; - } - this.applyPendingUpdates(); - callback && callback(linkRef); - } - }); - }, - ({ error: _error, timeout: _timeout }) => fallback() - ); - } - getFormsForRecovery() { - if (this.joinCount === 0) { - return {}; - } - let phxChange = this.binding("change"); - return dom_default.all(this.el, `form[${phxChange}]`).filter((form) => form.id).filter((form) => form.elements.length > 0).filter((form) => form.getAttribute(this.binding(PHX_AUTO_RECOVER)) !== "ignore").map((form) => form.cloneNode(true)).reduce((acc, form) => { - acc[form.id] = form; - return acc; - }, {}); - } - maybePushComponentsDestroyed(destroyedCIDs) { - let willDestroyCIDs = destroyedCIDs.filter((cid) => { - return dom_default.findComponentNodeList(this.el, cid).length === 0; - }); - if (willDestroyCIDs.length > 0) { - willDestroyCIDs.forEach((cid) => this.rendered.resetRender(cid)); - this.pushWithReply(null, "cids_will_destroy", { cids: willDestroyCIDs }).then(() => { - this.liveSocket.requestDOMUpdate(() => { - let completelyDestroyCIDs = willDestroyCIDs.filter((cid) => { - return dom_default.findComponentNodeList(this.el, cid).length === 0; - }); - if (completelyDestroyCIDs.length > 0) { - this.pushWithReply(null, "cids_destroyed", { cids: completelyDestroyCIDs }).then(({ resp }) => { - this.rendered.pruneCIDs(resp.cids); - }); - } - }); - }); - } - } - ownsElement(el) { - let parentViewEl = el.closest(PHX_VIEW_SELECTOR); - return el.getAttribute(PHX_PARENT_ID) === this.id || parentViewEl && parentViewEl.id === this.id || !parentViewEl && this.isDead; - } - submitForm(form, targetCtx, phxEvent, submitter, opts = {}) { - dom_default.putPrivate(form, PHX_HAS_SUBMITTED, true); - const inputs = Array.from(form.elements); - inputs.forEach((input) => dom_default.putPrivate(input, PHX_HAS_SUBMITTED, true)); - this.liveSocket.blurActiveElement(this); - this.pushFormSubmit(form, targetCtx, phxEvent, submitter, opts, () => { - this.liveSocket.restorePreviouslyActiveFocus(); - }); - } - binding(kind) { - return this.liveSocket.binding(kind); - } -}; - -// js/phoenix_live_view/live_socket.js -var isUsedInput = (el) => dom_default.isUsedInput(el); -var LiveSocket = class { - constructor(url, phxSocket, opts = {}) { - this.unloaded = false; - if (!phxSocket || phxSocket.constructor.name === "Object") { - throw new Error(` - a phoenix Socket must be provided as the second argument to the LiveSocket constructor. For example: - - import {Socket} from "phoenix" - import {LiveSocket} from "phoenix_live_view" - let liveSocket = new LiveSocket("/live", Socket, {...}) - `); - } - this.socket = new phxSocket(url, opts); - this.bindingPrefix = opts.bindingPrefix || BINDING_PREFIX; - this.opts = opts; - this.params = closure(opts.params || {}); - this.viewLogger = opts.viewLogger; - this.metadataCallbacks = opts.metadata || {}; - this.defaults = Object.assign(clone(DEFAULTS), opts.defaults || {}); - this.activeElement = null; - this.prevActive = null; - this.silenced = false; - this.main = null; - this.outgoingMainEl = null; - this.clickStartedAtTarget = null; - this.linkRef = 1; - this.roots = {}; - this.href = window.location.href; - this.pendingLink = null; - this.currentLocation = clone(window.location); - this.hooks = opts.hooks || {}; - this.uploaders = opts.uploaders || {}; - this.loaderTimeout = opts.loaderTimeout || LOADER_TIMEOUT; - this.reloadWithJitterTimer = null; - this.maxReloads = opts.maxReloads || MAX_RELOADS; - this.reloadJitterMin = opts.reloadJitterMin || RELOAD_JITTER_MIN; - this.reloadJitterMax = opts.reloadJitterMax || RELOAD_JITTER_MAX; - this.failsafeJitter = opts.failsafeJitter || FAILSAFE_JITTER; - this.localStorage = opts.localStorage || window.localStorage; - this.sessionStorage = opts.sessionStorage || window.sessionStorage; - this.boundTopLevelEvents = false; - this.boundEventNames = /* @__PURE__ */ new Set(); - this.serverCloseRef = null; - this.domCallbacks = Object.assign( - { - jsQuerySelectorAll: null, - onPatchStart: closure(), - onPatchEnd: closure(), - onNodeAdded: closure(), - onBeforeElUpdated: closure() - }, - opts.dom || {} - ); - this.transitions = new TransitionSet(); - window.addEventListener("pagehide", (_e) => { - this.unloaded = true; - }); - this.socket.onOpen(() => { - if (this.isUnloaded()) { - window.location.reload(); - } - }); - } - // public - version() { - return "1.0.0-rc.7"; - } - isProfileEnabled() { - return this.sessionStorage.getItem(PHX_LV_PROFILE) === "true"; - } - isDebugEnabled() { - return this.sessionStorage.getItem(PHX_LV_DEBUG) === "true"; - } - isDebugDisabled() { - return this.sessionStorage.getItem(PHX_LV_DEBUG) === "false"; - } - enableDebug() { - this.sessionStorage.setItem(PHX_LV_DEBUG, "true"); - } - enableProfiling() { - this.sessionStorage.setItem(PHX_LV_PROFILE, "true"); - } - disableDebug() { - this.sessionStorage.setItem(PHX_LV_DEBUG, "false"); - } - disableProfiling() { - this.sessionStorage.removeItem(PHX_LV_PROFILE); - } - enableLatencySim(upperBoundMs) { - this.enableDebug(); - console.log("latency simulator enabled for the duration of this browser session. Call disableLatencySim() to disable"); - this.sessionStorage.setItem(PHX_LV_LATENCY_SIM, upperBoundMs); - } - disableLatencySim() { - this.sessionStorage.removeItem(PHX_LV_LATENCY_SIM); - } - getLatencySim() { - let str = this.sessionStorage.getItem(PHX_LV_LATENCY_SIM); - return str ? parseInt(str) : null; - } - getSocket() { - return this.socket; - } - connect() { - if (window.location.hostname === "localhost" && !this.isDebugDisabled()) { - this.enableDebug(); - } - let doConnect = () => { - this.resetReloadStatus(); - if (this.joinRootViews()) { - this.bindTopLevelEvents(); - this.socket.connect(); - } else if (this.main) { - this.socket.connect(); - } else { - this.bindTopLevelEvents({ dead: true }); - } - this.joinDeadView(); - }; - if (["complete", "loaded", "interactive"].indexOf(document.readyState) >= 0) { - doConnect(); - } else { - document.addEventListener("DOMContentLoaded", () => doConnect()); - } - } - disconnect(callback) { - clearTimeout(this.reloadWithJitterTimer); - if (this.serverCloseRef) { - this.socket.off(this.serverCloseRef); - this.serverCloseRef = null; - } - this.socket.disconnect(callback); - } - replaceTransport(transport) { - clearTimeout(this.reloadWithJitterTimer); - this.socket.replaceTransport(transport); - this.connect(); - } - execJS(el, encodedJS, eventType = null) { - let e = new CustomEvent("phx:exec", { detail: { sourceElement: el } }); - this.owner(el, (view) => js_default.exec(e, eventType, encodedJS, view, el)); - } - // private - execJSHookPush(el, phxEvent, data, callback) { - this.withinOwners(el, (view) => { - let e = new CustomEvent("phx:exec", { detail: { sourceElement: el } }); - js_default.exec(e, "hook", phxEvent, view, el, ["push", { data, callback }]); - }); - } - unload() { - if (this.unloaded) { - return; - } - if (this.main && this.isConnected()) { - this.log(this.main, "socket", () => ["disconnect for page nav"]); - } - this.unloaded = true; - this.destroyAllViews(); - this.disconnect(); - } - triggerDOM(kind, args) { - this.domCallbacks[kind](...args); - } - time(name, func) { - if (!this.isProfileEnabled() || !console.time) { - return func(); - } - console.time(name); - let result = func(); - console.timeEnd(name); - return result; - } - log(view, kind, msgCallback) { - if (this.viewLogger) { - let [msg, obj] = msgCallback(); - this.viewLogger(view, kind, msg, obj); - } else if (this.isDebugEnabled()) { - let [msg, obj] = msgCallback(); - debug(view, kind, msg, obj); - } - } - requestDOMUpdate(callback) { - this.transitions.after(callback); - } - transition(time, onStart, onDone = function() { - }) { - this.transitions.addTransition(time, onStart, onDone); - } - onChannel(channel, event, cb) { - channel.on(event, (data) => { - let latency = this.getLatencySim(); - if (!latency) { - cb(data); - } else { - setTimeout(() => cb(data), latency); - } - }); - } - reloadWithJitter(view, log) { - clearTimeout(this.reloadWithJitterTimer); - this.disconnect(); - let minMs = this.reloadJitterMin; - let maxMs = this.reloadJitterMax; - let afterMs = Math.floor(Math.random() * (maxMs - minMs + 1)) + minMs; - let tries = browser_default.updateLocal(this.localStorage, window.location.pathname, CONSECUTIVE_RELOADS, 0, (count) => count + 1); - if (tries >= this.maxReloads) { - afterMs = this.failsafeJitter; - } - this.reloadWithJitterTimer = setTimeout(() => { - if (view.isDestroyed() || view.isConnected()) { - return; - } - view.destroy(); - log ? log() : this.log(view, "join", () => [`encountered ${tries} consecutive reloads`]); - if (tries >= this.maxReloads) { - this.log(view, "join", () => [`exceeded ${this.maxReloads} consecutive reloads. Entering failsafe mode`]); - } - if (this.hasPendingLink()) { - window.location = this.pendingLink; - } else { - window.location.reload(); - } - }, afterMs); - } - getHookCallbacks(name) { - return name && name.startsWith("Phoenix.") ? hooks_default[name.split(".")[1]] : this.hooks[name]; - } - isUnloaded() { - return this.unloaded; - } - isConnected() { - return this.socket.isConnected(); - } - getBindingPrefix() { - return this.bindingPrefix; - } - binding(kind) { - return `${this.getBindingPrefix()}${kind}`; - } - channel(topic, params) { - return this.socket.channel(topic, params); - } - joinDeadView() { - let body = document.body; - if (body && !this.isPhxView(body) && !this.isPhxView(document.firstElementChild)) { - let view = this.newRootView(body); - view.setHref(this.getHref()); - view.joinDead(); - if (!this.main) { - this.main = view; - } - window.requestAnimationFrame(() => view.execNewMounted()); - } - } - joinRootViews() { - let rootsFound = false; - dom_default.all(document, `${PHX_VIEW_SELECTOR}:not([${PHX_PARENT_ID}])`, (rootEl) => { - if (!this.getRootById(rootEl.id)) { - let view = this.newRootView(rootEl); - view.setHref(this.getHref()); - view.join(); - if (rootEl.hasAttribute(PHX_MAIN)) { - this.main = view; - } - } - rootsFound = true; - }); - return rootsFound; - } - redirect(to, flash, reloadToken) { - if (reloadToken) { - browser_default.setCookie(PHX_RELOAD_STATUS, reloadToken, 60); - } - this.unload(); - browser_default.redirect(to, flash); - } - replaceMain(href, flash, callback = null, linkRef = this.setPendingLink(href)) { - let liveReferer = this.currentLocation.href; - this.outgoingMainEl = this.outgoingMainEl || this.main.el; - let removeEls = dom_default.all(this.outgoingMainEl, `[${this.binding("remove")}]`); - let newMainEl = dom_default.cloneNode(this.outgoingMainEl, ""); - this.main.showLoader(this.loaderTimeout); - this.main.destroy(); - this.main = this.newRootView(newMainEl, flash, liveReferer); - this.main.setRedirect(href); - this.transitionRemoves(removeEls, true); - this.main.join((joinCount, onDone) => { - if (joinCount === 1 && this.commitPendingLink(linkRef)) { - this.requestDOMUpdate(() => { - removeEls.forEach((el) => el.remove()); - dom_default.findPhxSticky(document).forEach((el) => newMainEl.appendChild(el)); - this.outgoingMainEl.replaceWith(newMainEl); - this.outgoingMainEl = null; - callback && callback(linkRef); - onDone(); - }); - } - }); - } - transitionRemoves(elements, skipSticky, callback) { - let removeAttr = this.binding("remove"); - if (skipSticky) { - const stickies = dom_default.findPhxSticky(document) || []; - elements = elements.filter((el) => !dom_default.isChildOfAny(el, stickies)); - } - let silenceEvents = (e) => { - e.preventDefault(); - e.stopImmediatePropagation(); - }; - elements.forEach((el) => { - for (let event of this.boundEventNames) { - el.addEventListener(event, silenceEvents, true); - } - this.execJS(el, el.getAttribute(removeAttr), "remove"); - }); - this.requestDOMUpdate(() => { - elements.forEach((el) => { - for (let event of this.boundEventNames) { - el.removeEventListener(event, silenceEvents, true); - } - }); - callback && callback(); - }); - } - isPhxView(el) { - return el.getAttribute && el.getAttribute(PHX_SESSION) !== null; - } - newRootView(el, flash, liveReferer) { - let view = new View(el, this, null, flash, liveReferer); - this.roots[view.id] = view; - return view; - } - owner(childEl, callback) { - let view = maybe(childEl.closest(PHX_VIEW_SELECTOR), (el) => this.getViewByEl(el)) || this.main; - return view && callback ? callback(view) : view; - } - withinOwners(childEl, callback) { - this.owner(childEl, (view) => callback(view, childEl)); - } - getViewByEl(el) { - let rootId = el.getAttribute(PHX_ROOT_ID); - return maybe(this.getRootById(rootId), (root) => root.getDescendentByEl(el)); - } - getRootById(id) { - return this.roots[id]; - } - destroyAllViews() { - for (let id in this.roots) { - this.roots[id].destroy(); - delete this.roots[id]; - } - this.main = null; - } - destroyViewByEl(el) { - let root = this.getRootById(el.getAttribute(PHX_ROOT_ID)); - if (root && root.id === el.id) { - root.destroy(); - delete this.roots[root.id]; - } else if (root) { - root.destroyDescendent(el.id); - } - } - getActiveElement() { - return document.activeElement; - } - dropActiveElement(view) { - if (this.prevActive && view.ownsElement(this.prevActive)) { - this.prevActive = null; - } - } - restorePreviouslyActiveFocus() { - if (this.prevActive && this.prevActive !== document.body) { - this.prevActive.focus(); - } - } - blurActiveElement() { - this.prevActive = this.getActiveElement(); - if (this.prevActive !== document.body) { - this.prevActive.blur(); - } - } - bindTopLevelEvents({ dead } = {}) { - if (this.boundTopLevelEvents) { - return; - } - this.boundTopLevelEvents = true; - this.serverCloseRef = this.socket.onClose((event) => { - if (event && event.code === 1e3 && this.main) { - return this.reloadWithJitter(this.main); - } - }); - document.body.addEventListener("click", function() { - }); - window.addEventListener("pageshow", (e) => { - if (e.persisted) { - this.getSocket().disconnect(); - this.withPageLoading({ to: window.location.href, kind: "redirect" }); - window.location.reload(); - } - }, true); - if (!dead) { - this.bindNav(); - } - this.bindClicks(); - if (!dead) { - this.bindForms(); - } - this.bind({ keyup: "keyup", keydown: "keydown" }, (e, type, view, targetEl, phxEvent, phxTarget) => { - let matchKey = targetEl.getAttribute(this.binding(PHX_KEY)); - let pressedKey = e.key && e.key.toLowerCase(); - if (matchKey && matchKey.toLowerCase() !== pressedKey) { - return; - } - let data = { key: e.key, ...this.eventMeta(type, e, targetEl) }; - js_default.exec(e, type, phxEvent, view, targetEl, ["push", { data }]); - }); - this.bind({ blur: "focusout", focus: "focusin" }, (e, type, view, targetEl, phxEvent, phxTarget) => { - if (!phxTarget) { - let data = { key: e.key, ...this.eventMeta(type, e, targetEl) }; - js_default.exec(e, type, phxEvent, view, targetEl, ["push", { data }]); - } - }); - this.bind({ blur: "blur", focus: "focus" }, (e, type, view, targetEl, phxEvent, phxTarget) => { - if (phxTarget === "window") { - let data = this.eventMeta(type, e, targetEl); - js_default.exec(e, type, phxEvent, view, targetEl, ["push", { data }]); - } - }); - this.on("dragover", (e) => e.preventDefault()); - this.on("drop", (e) => { - e.preventDefault(); - let dropTargetId = maybe(closestPhxBinding(e.target, this.binding(PHX_DROP_TARGET)), (trueTarget) => { - return trueTarget.getAttribute(this.binding(PHX_DROP_TARGET)); - }); - let dropTarget = dropTargetId && document.getElementById(dropTargetId); - let files = Array.from(e.dataTransfer.files || []); - if (!dropTarget || dropTarget.disabled || files.length === 0 || !(dropTarget.files instanceof FileList)) { - return; - } - LiveUploader.trackFiles(dropTarget, files, e.dataTransfer); - dropTarget.dispatchEvent(new Event("input", { bubbles: true })); - }); - this.on(PHX_TRACK_UPLOADS, (e) => { - let uploadTarget = e.target; - if (!dom_default.isUploadInput(uploadTarget)) { - return; - } - let files = Array.from(e.detail.files || []).filter((f) => f instanceof File || f instanceof Blob); - LiveUploader.trackFiles(uploadTarget, files); - uploadTarget.dispatchEvent(new Event("input", { bubbles: true })); - }); - } - eventMeta(eventName, e, targetEl) { - let callback = this.metadataCallbacks[eventName]; - return callback ? callback(e, targetEl) : {}; - } - setPendingLink(href) { - this.linkRef++; - this.pendingLink = href; - this.resetReloadStatus(); - return this.linkRef; - } - // anytime we are navigating or connecting, drop reload cookie in case - // we issue the cookie but the next request was interrupted and the server never dropped it - resetReloadStatus() { - browser_default.deleteCookie(PHX_RELOAD_STATUS); - } - commitPendingLink(linkRef) { - if (this.linkRef !== linkRef) { - return false; - } else { - this.href = this.pendingLink; - this.pendingLink = null; - return true; - } - } - getHref() { - return this.href; - } - hasPendingLink() { - return !!this.pendingLink; - } - bind(events, callback) { - for (let event in events) { - let browserEventName = events[event]; - this.on(browserEventName, (e) => { - let binding = this.binding(event); - let windowBinding = this.binding(`window-${event}`); - let targetPhxEvent = e.target.getAttribute && e.target.getAttribute(binding); - if (targetPhxEvent) { - this.debounce(e.target, e, browserEventName, () => { - this.withinOwners(e.target, (view) => { - callback(e, event, view, e.target, targetPhxEvent, null); - }); - }); - } else { - dom_default.all(document, `[${windowBinding}]`, (el) => { - let phxEvent = el.getAttribute(windowBinding); - this.debounce(el, e, browserEventName, () => { - this.withinOwners(el, (view) => { - callback(e, event, view, el, phxEvent, "window"); - }); - }); - }); - } - }); - } - } - bindClicks() { - this.on("mousedown", (e) => this.clickStartedAtTarget = e.target); - this.bindClick("click", "click"); - } - bindClick(eventName, bindingName) { - let click = this.binding(bindingName); - window.addEventListener(eventName, (e) => { - let target = null; - if (e.detail === 0) - this.clickStartedAtTarget = e.target; - let clickStartedAtTarget = this.clickStartedAtTarget || e.target; - target = closestPhxBinding(e.target, click); - this.dispatchClickAway(e, clickStartedAtTarget); - this.clickStartedAtTarget = null; - let phxEvent = target && target.getAttribute(click); - if (!phxEvent) { - if (dom_default.isNewPageClick(e, window.location)) { - this.unload(); - } - return; - } - if (target.getAttribute("href") === "#") { - e.preventDefault(); - } - if (target.hasAttribute(PHX_REF_SRC)) { - return; - } - this.debounce(target, e, "click", () => { - this.withinOwners(target, (view) => { - js_default.exec(e, "click", phxEvent, view, target, ["push", { data: this.eventMeta("click", e, target) }]); - }); - }); - }, false); - } - dispatchClickAway(e, clickStartedAt) { - let phxClickAway = this.binding("click-away"); - dom_default.all(document, `[${phxClickAway}]`, (el) => { - if (!(el.isSameNode(clickStartedAt) || el.contains(clickStartedAt))) { - this.withinOwners(el, (view) => { - let phxEvent = el.getAttribute(phxClickAway); - if (js_default.isVisible(el) && js_default.isInViewport(el)) { - js_default.exec(e, "click", phxEvent, view, el, ["push", { data: this.eventMeta("click", e, e.target) }]); - } - }); - } - }); - } - bindNav() { - if (!browser_default.canPushState()) { - return; - } - if (history.scrollRestoration) { - history.scrollRestoration = "manual"; - } - let scrollTimer = null; - window.addEventListener("scroll", (_e) => { - clearTimeout(scrollTimer); - scrollTimer = setTimeout(() => { - browser_default.updateCurrentState((state) => Object.assign(state, { scroll: window.scrollY })); - }, 100); - }); - window.addEventListener("popstate", (event) => { - if (!this.registerNewLocation(window.location)) { - return; - } - let { type, id, root, scroll } = event.state || {}; - let href = window.location.href; - dom_default.dispatchEvent(window, "phx:navigate", { detail: { href, patch: type === "patch", pop: true } }); - this.requestDOMUpdate(() => { - if (this.main.isConnected() && (type === "patch" && id === this.main.id)) { - this.main.pushLinkPatch(event, href, null, () => { - this.maybeScroll(scroll); - }); - } else { - this.replaceMain(href, null, () => { - if (root) { - this.replaceRootHistory(); - } - this.maybeScroll(scroll); - }); - } - }); - }, false); - window.addEventListener("click", (e) => { - let target = closestPhxBinding(e.target, PHX_LIVE_LINK); - let type = target && target.getAttribute(PHX_LIVE_LINK); - if (!type || !this.isConnected() || !this.main || dom_default.wantsNewTab(e)) { - return; - } - let href = target.href instanceof SVGAnimatedString ? target.href.baseVal : target.href; - let linkState = target.getAttribute(PHX_LINK_STATE); - e.preventDefault(); - e.stopImmediatePropagation(); - if (this.pendingLink === href) { - return; - } - this.requestDOMUpdate(() => { - if (type === "patch") { - this.pushHistoryPatch(e, href, linkState, target); - } else if (type === "redirect") { - this.historyRedirect(e, href, linkState, null, target); - } else { - throw new Error(`expected ${PHX_LIVE_LINK} to be "patch" or "redirect", got: ${type}`); - } - let phxClick = target.getAttribute(this.binding("click")); - if (phxClick) { - this.requestDOMUpdate(() => this.execJS(target, phxClick, "click")); - } - }); - }, false); - } - maybeScroll(scroll) { - if (typeof scroll === "number") { - requestAnimationFrame(() => { - window.scrollTo(0, scroll); - }); - } - } - dispatchEvent(event, payload = {}) { - dom_default.dispatchEvent(window, `phx:${event}`, { detail: payload }); - } - dispatchEvents(events) { - events.forEach(([event, payload]) => this.dispatchEvent(event, payload)); - } - withPageLoading(info, callback) { - dom_default.dispatchEvent(window, "phx:page-loading-start", { detail: info }); - let done = () => dom_default.dispatchEvent(window, "phx:page-loading-stop", { detail: info }); - return callback ? callback(done) : done; - } - pushHistoryPatch(e, href, linkState, targetEl) { - if (!this.isConnected() || !this.main.isMain()) { - return browser_default.redirect(href); - } - this.withPageLoading({ to: href, kind: "patch" }, (done) => { - this.main.pushLinkPatch(e, href, targetEl, (linkRef) => { - this.historyPatch(href, linkState, linkRef); - done(); - }); - }); - } - historyPatch(href, linkState, linkRef = this.setPendingLink(href)) { - if (!this.commitPendingLink(linkRef)) { - return; - } - browser_default.pushState(linkState, { type: "patch", id: this.main.id }, href); - dom_default.dispatchEvent(window, "phx:navigate", { detail: { patch: true, href, pop: false } }); - this.registerNewLocation(window.location); - } - historyRedirect(e, href, linkState, flash, targetEl) { - if (targetEl && e.isTrusted && e.type !== "popstate") { - targetEl.classList.add("phx-click-loading"); - } - if (!this.isConnected() || !this.main.isMain()) { - return browser_default.redirect(href, flash); - } - if (/^\/$|^\/[^\/]+.*$/.test(href)) { - let { protocol, host } = window.location; - href = `${protocol}//${host}${href}`; - } - let scroll = window.scrollY; - this.withPageLoading({ to: href, kind: "redirect" }, (done) => { - this.replaceMain(href, flash, (linkRef) => { - if (linkRef === this.linkRef) { - browser_default.pushState(linkState, { type: "redirect", id: this.main.id, scroll }, href); - dom_default.dispatchEvent(window, "phx:navigate", { detail: { href, patch: false, pop: false } }); - this.registerNewLocation(window.location); - } - done(); - }); - }); - } - replaceRootHistory() { - browser_default.pushState("replace", { root: true, type: "patch", id: this.main.id }); - } - registerNewLocation(newLocation) { - let { pathname, search } = this.currentLocation; - if (pathname + search === newLocation.pathname + newLocation.search) { - return false; - } else { - this.currentLocation = clone(newLocation); - return true; - } - } - bindForms() { - let iterations = 0; - let externalFormSubmitted = false; - this.on("submit", (e) => { - let phxSubmit = e.target.getAttribute(this.binding("submit")); - let phxChange = e.target.getAttribute(this.binding("change")); - if (!externalFormSubmitted && phxChange && !phxSubmit) { - externalFormSubmitted = true; - e.preventDefault(); - this.withinOwners(e.target, (view) => { - view.disableForm(e.target); - window.requestAnimationFrame(() => { - if (dom_default.isUnloadableFormSubmit(e)) { - this.unload(); - } - e.target.submit(); - }); - }); - } - }); - this.on("submit", (e) => { - let phxEvent = e.target.getAttribute(this.binding("submit")); - if (!phxEvent) { - if (dom_default.isUnloadableFormSubmit(e)) { - this.unload(); - } - return; - } - e.preventDefault(); - e.target.disabled = true; - this.withinOwners(e.target, (view) => { - js_default.exec(e, "submit", phxEvent, view, e.target, ["push", { submitter: e.submitter }]); - }); - }); - for (let type of ["change", "input"]) { - this.on(type, (e) => { - if (e instanceof CustomEvent && e.target.form === void 0) { - throw new Error(`dispatching a custom ${type} event is only supported on input elements inside a form`); - } - let phxChange = this.binding("change"); - let input = e.target; - if (e.isComposing) { - const key = `composition-listener-${type}`; - if (!dom_default.private(input, key)) { - dom_default.putPrivate(input, key, true); - input.addEventListener("compositionend", () => { - input.dispatchEvent(new Event(type, { bubbles: true })); - dom_default.deletePrivate(input, key); - }, { once: true }); - } - return; - } - let inputEvent = input.getAttribute(phxChange); - let formEvent = input.form && input.form.getAttribute(phxChange); - let phxEvent = inputEvent || formEvent; - if (!phxEvent) { - return; - } - if (input.type === "number" && input.validity && input.validity.badInput) { - return; - } - let dispatcher = inputEvent ? input : input.form; - let currentIterations = iterations; - iterations++; - let { at, type: lastType } = dom_default.private(input, "prev-iteration") || {}; - if (at === currentIterations - 1 && type === "change" && lastType === "input") { - return; - } - dom_default.putPrivate(input, "prev-iteration", { at: currentIterations, type }); - this.debounce(input, e, type, () => { - this.withinOwners(dispatcher, (view) => { - dom_default.putPrivate(input, PHX_HAS_FOCUSED, true); - js_default.exec(e, "change", phxEvent, view, input, ["push", { _target: e.target.name, dispatcher }]); - }); - }); - }); - } - this.on("reset", (e) => { - let form = e.target; - dom_default.resetForm(form); - let input = Array.from(form.elements).find((el) => el.type === "reset"); - if (input) { - window.requestAnimationFrame(() => { - input.dispatchEvent(new Event("input", { bubbles: true, cancelable: false })); - }); - } - }); - } - debounce(el, event, eventType, callback) { - if (eventType === "blur" || eventType === "focusout") { - return callback(); - } - let phxDebounce = this.binding(PHX_DEBOUNCE); - let phxThrottle = this.binding(PHX_THROTTLE); - let defaultDebounce = this.defaults.debounce.toString(); - let defaultThrottle = this.defaults.throttle.toString(); - this.withinOwners(el, (view) => { - let asyncFilter = () => !view.isDestroyed() && document.body.contains(el); - dom_default.debounce(el, event, phxDebounce, defaultDebounce, phxThrottle, defaultThrottle, asyncFilter, () => { - callback(); - }); - }); - } - silenceEvents(callback) { - this.silenced = true; - callback(); - this.silenced = false; - } - on(event, callback) { - this.boundEventNames.add(event); - window.addEventListener(event, (e) => { - if (!this.silenced) { - callback(e); - } - }); - } - jsQuerySelectorAll(sourceEl, query, defaultQuery) { - let all = this.domCallbacks.jsQuerySelectorAll; - return all ? all(sourceEl, query, defaultQuery) : defaultQuery(); - } -}; -var TransitionSet = class { - constructor() { - this.transitions = /* @__PURE__ */ new Set(); - this.pendingOps = []; - } - reset() { - this.transitions.forEach((timer) => { - clearTimeout(timer); - this.transitions.delete(timer); - }); - this.flushPendingOps(); - } - after(callback) { - if (this.size() === 0) { - callback(); - } else { - this.pushPendingOp(callback); - } - } - addTransition(time, onStart, onDone) { - onStart(); - let timer = setTimeout(() => { - this.transitions.delete(timer); - onDone(); - this.flushPendingOps(); - }, time); - this.transitions.add(timer); - } - pushPendingOp(op) { - this.pendingOps.push(op); - } - size() { - return this.transitions.size; - } - flushPendingOps() { - if (this.size() > 0) { - return; - } - let op = this.pendingOps.shift(); - if (op) { - op(); - this.flushPendingOps(); - } - } -}; - -// js/phoenix_live_view/index.js -var createHook = (el, callbacks = {}) => { - let existingHook = dom_default.getCustomElHook(el); - if (existingHook) { - return existingHook; - } - let hook = new ViewHook(View.closestView(el), el, callbacks); - dom_default.putCustomElHook(el, hook); - return hook; -}; -export { - LiveSocket, - createHook, - isUsedInput -}; -//# sourceMappingURL=phoenix_live_view.esm.js.map diff --git a/config/config.exs b/config/config.exs index ab19505..f71ae1f 100644 --- a/config/config.exs +++ b/config/config.exs @@ -9,7 +9,9 @@ import Config config :munch, ecto_repos: [Munch.Repo], - generators: [timestamp_type: :utc_datetime] + generators: [timestamp_type: :utc_datetime, binary_id: true] + +config :munch, Munch.Repo, types: Munch.PostgresTypes # Configures the endpoint config :munch, MunchWeb.Endpoint, diff --git a/config/dev.exs b/config/dev.exs index 942b78b..de476eb 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -5,6 +5,7 @@ config :munch, Munch.Repo, username: "postgres", password: "postgres", hostname: "localhost", + port: 5432, database: "munch_dev", stacktrace: true, show_sensitive_data_on_connection_error: true, diff --git a/config/test.exs b/config/test.exs index fc8c08f..f2ae508 100644 --- a/config/test.exs +++ b/config/test.exs @@ -12,6 +12,7 @@ config :munch, Munch.Repo, username: "postgres", password: "postgres", hostname: "localhost", + port: 5432, database: "munch_test#{System.get_env("MIX_TEST_PARTITION")}", pool: Ecto.Adapters.SQL.Sandbox, pool_size: System.schedulers_online() * 2 diff --git a/lib/munch/osm.ex b/lib/munch/osm.ex new file mode 100644 index 0000000..eb416a2 --- /dev/null +++ b/lib/munch/osm.ex @@ -0,0 +1,66 @@ +defmodule Munch.Osm do + @moduledoc """ + The OSM context. + """ + + import Ecto.Query, warn: false + alias Munch.Repo + alias Munch.Restaurants + + def api_get_object(osm_type, osm_id) do + resp = Req.get!("https://api.openstreetmap.org/api/0.6/#{osm_type}/#{osm_id}.json") + + decoded = Jason.decode!(resp.body) + + case decoded do + %{"elements" => [%{"lat" => lattitude, "lon" => longitude, "tags" => tags}]} -> + %{lattitude: lattitude, longitude: longitude, tags: tags} + + _ -> + raise "Invalid response from OpenStreetMaps API: #{decoded}" + end + end + + def pull_restaurant(osm_type, osm_id) do + object = api_get_object(osm_type, osm_id) + %{"name" => name} = object.tags + + Repo.insert!(%Restaurants.Restaurant{ + osm_type: osm_type, + osm_id: osm_id, + name: name, + location: %Geo.Point{coordinates: {object.longitude, object.lattitude}, srid: 4326}, + note: "", + iso_country_subdivision: "", + secondary_subdivision: "" + }) + end + + def copy_osm_restaurants() do + osm_restaurants = Repo.all(Munch.Osm.Restaurant) + + restaurants = + osm_restaurants + |> Enum.map(fn osm_restaurant -> + %Restaurants.Restaurant{ + osm_type: osm_restaurant.osm_type, + osm_id: osm_restaurant.osm_id, + name: osm_restaurant.tags["name"], + location: osm_restaurant.location, + note: "", + iso_country_subdivision: nil, + secondary_subdivision: nil + } + end) + |> IO.inspect() + + restaurants + |> Enum.map(fn restaurant -> + Repo.insert!(restaurant) + end) + end + + def sync_restaurant(_restaurant) do + IO.inspect("TODO: Implement sync with OSM") + end +end diff --git a/lib/munch/osm/restaurant.ex b/lib/munch/osm/restaurant.ex new file mode 100644 index 0000000..7c3a33a --- /dev/null +++ b/lib/munch/osm/restaurant.ex @@ -0,0 +1,12 @@ +defmodule Munch.Osm.Restaurant do + use Ecto.Schema + + @schema_prefix "osm" + @primary_key false + schema "restaurants" do + field :osm_type, Ecto.Enum, values: [node: "N", way: "W", relation: "R"], primary_key: true + field :osm_id, :integer, primary_key: true + field :location, Geo.PostGIS.Geometry + field :tags, :map + end +end diff --git a/lib/munch/postgres_types.ex b/lib/munch/postgres_types.ex new file mode 100644 index 0000000..2777671 --- /dev/null +++ b/lib/munch/postgres_types.ex @@ -0,0 +1,4 @@ +Postgrex.Types.define( + Munch.PostgresTypes, + [Geo.PostGIS.Extension] ++ Ecto.Adapters.Postgres.extensions() +) diff --git a/lib/munch/restaurants.ex b/lib/munch/restaurants.ex index b0bd84d..7e5d426 100644 --- a/lib/munch/restaurants.ex +++ b/lib/munch/restaurants.ex @@ -37,7 +37,7 @@ defmodule Munch.Restaurants do from r in Restaurant, where: ^Enum.reduce(words, dynamic(true), fn word, acc -> - dynamic([r], ^acc and (ilike(r.name, ^"%#{word}%") or ilike(r.address, ^"%#{word}%"))) + dynamic([r], ^acc and ilike(r.name, ^"%#{word}%")) end) ) end diff --git a/lib/munch/restaurants/restaurant.ex b/lib/munch/restaurants/restaurant.ex index 366b2c6..6cfc4cf 100644 --- a/lib/munch/restaurants/restaurant.ex +++ b/lib/munch/restaurants/restaurant.ex @@ -5,11 +5,13 @@ defmodule Munch.Restaurants.Restaurant do @primary_key {:id, :binary_id, autogenerate: true} @foreign_key_type :binary_id schema "restaurants" do + field :osm_type, Ecto.Enum, values: [node: "N", way: "W", relation: "R"] + field :osm_id, :integer field :name, :string - field :address, :string - field :country, :string - field :city, :string - field :neighbourhood, :string + field :location, Geo.PostGIS.Geometry + field :note, :string + field :iso_country_subdivision, :string + field :secondary_subdivision, :string timestamps(type: :utc_datetime) end @@ -17,7 +19,6 @@ defmodule Munch.Restaurants.Restaurant do @doc false def changeset(restaurant, attrs) do restaurant - |> cast(attrs, [:name, :address, :country, :city, :neighbourhood]) - |> validate_required([:name, :address, :country, :city, :neighbourhood]) + |> cast(attrs, [:note]) end end diff --git a/lib/munch_web/components/core_components.ex b/lib/munch_web/components/core_components.ex index bb8e5ac..5988357 100644 --- a/lib/munch_web/components/core_components.ex +++ b/lib/munch_web/components/core_components.ex @@ -323,7 +323,7 @@ defmodule MunchWeb.CoreComponents do def modal(assigns) do ~H""" - + <%= render_slot(@inner_block) %> """ diff --git a/lib/munch_web/live/list_live/form.ex b/lib/munch_web/live/list_live/form.ex index 7be1a33..74cf11f 100644 --- a/lib/munch_web/live/list_live/form.ex +++ b/lib/munch_web/live/list_live/form.ex @@ -114,7 +114,7 @@ defmodule MunchWeb.ListLive.Form do end defp save_list(socket, :new, list_params) do - case Lists.create_list(list_params |> Map.put("user_id", socket.assigns.current_user.id)) do + case Lists.create_list(socket.assigns.current_user, list_params) do {:ok, list} -> {:noreply, socket @@ -130,14 +130,15 @@ defmodule MunchWeb.ListLive.Form do defp return_path("show", list), do: ~p"/list/#{list}" @impl true - def handle_info({:restaurant_selected, restaurant_id}, socket) do + def handle_info({:restaurant_selected, nil, restaurant_id}, socket) do changeset = Lists.changeset_prepend_restaurant(socket.assigns.form.source, restaurant_id) {:noreply, socket |> assign(:form, to_form(changeset)) - |> fill_restaurants()} + |> fill_restaurants() + |> push_event("close-modal-restaurant-select-modal", %{})} end def fill_restaurants(socket) do diff --git a/lib/munch_web/live/restaurant_live/edit.ex b/lib/munch_web/live/restaurant_live/edit.ex new file mode 100644 index 0000000..7862b57 --- /dev/null +++ b/lib/munch_web/live/restaurant_live/edit.ex @@ -0,0 +1,68 @@ +defmodule MunchWeb.RestaurantLive.Edit do + use MunchWeb, :live_view + + alias Munch.Restaurants + alias Munch.Osm + + @impl true + def render(assigns) do + ~H""" + <.header> + Edit restaurant + + + <.simple_form for={@form} id="restaurant-form" phx-change="validate" phx-submit="save"> + <.input field={@form[:note]} type="text" label="Note" /> + <:actions> + <.button phx-disable-with="Saving...">Save Restaurant + + + + <.button phx-click="trigger_sync">Sync OSM Data + + <.back navigate={return_path(@return_to, @restaurant)}>Back + """ + end + + @impl true + def mount(params = %{"id" => id}, _session, socket) do + restaurant = Restaurants.get_restaurant!(id) + + {:ok, + socket + |> assign(:return_to, return_to(params["return_to"])) + |> assign(:page_title, "Edit Restaurant") + |> assign(:restaurant, restaurant) + |> assign(:form, to_form(Restaurants.change_restaurant(restaurant)))} + end + + defp return_to("show"), do: "show" + defp return_to(_), do: "index" + + @impl true + def handle_event("validate", %{"restaurant" => restaurant_params}, socket) do + changeset = Restaurants.change_restaurant(socket.assigns.restaurant, restaurant_params) + {:noreply, assign(socket, form: to_form(changeset, action: :validate))} + end + + def handle_event("save", %{"restaurant" => restaurant_params}, socket) do + case Restaurants.update_restaurant(socket.assigns.restaurant, restaurant_params) do + {:ok, restaurant} -> + {:noreply, + socket + |> put_flash(:info, "Restaurant updated successfully") + |> push_navigate(to: return_path(socket.assigns.return_to, restaurant))} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, form: to_form(changeset))} + end + end + + def handle_event("trigger_sync", _params, socket) do + Osm.sync_restaurant(socket.assigns.restaurant) + {:noreply, socket} + end + + defp return_path("index", _restaurant), do: ~p"/restaurants" + defp return_path("show", restaurant), do: ~p"/restaurant/#{restaurant}" +end diff --git a/lib/munch_web/live/restaurant_live/form.ex b/lib/munch_web/live/restaurant_live/form.ex deleted file mode 100644 index 9ffcde0..0000000 --- a/lib/munch_web/live/restaurant_live/form.ex +++ /dev/null @@ -1,97 +0,0 @@ -defmodule MunchWeb.RestaurantLive.Form do - use MunchWeb, :live_view - - alias Munch.Restaurants - alias Munch.Restaurants.Restaurant - - @impl true - def render(assigns) do - ~H""" - <.header> - <%= @page_title %> - <:subtitle>Use this form to manage restaurant records in your database. - - - <.simple_form for={@form} id="restaurant-form" phx-change="validate" phx-submit="save"> - <.input field={@form[:name]} type="text" label="Name" /> - <.input field={@form[:address]} type="text" label="Address" /> - <.input field={@form[:country]} type="text" label="Country" /> - <.input field={@form[:city]} type="text" label="City" /> - <.input field={@form[:neighbourhood]} type="text" label="Neighbourhood" /> - <:actions> - <.button phx-disable-with="Saving...">Save Restaurant - - - - <.back navigate={return_path(@return_to, @restaurant)}>Back - """ - end - - @impl true - def mount(params, _session, socket) do - {:ok, - socket - |> assign(:return_to, return_to(params["return_to"])) - |> apply_action(socket.assigns.live_action, params)} - end - - defp return_to("show"), do: "show" - defp return_to(_), do: "index" - - defp apply_action(socket, :edit, %{"id" => id}) do - restaurant = Restaurants.get_restaurant!(id) - - socket - |> assign(:page_title, "Edit Restaurant") - |> assign(:restaurant, restaurant) - |> assign(:form, to_form(Restaurants.change_restaurant(restaurant))) - end - - defp apply_action(socket, :new, _params) do - restaurant = %Restaurant{} - - socket - |> assign(:page_title, "New Restaurant") - |> assign(:restaurant, restaurant) - |> assign(:form, to_form(Restaurants.change_restaurant(restaurant))) - end - - @impl true - def handle_event("validate", %{"restaurant" => restaurant_params}, socket) do - changeset = Restaurants.change_restaurant(socket.assigns.restaurant, restaurant_params) - {:noreply, assign(socket, form: to_form(changeset, action: :validate))} - end - - def handle_event("save", %{"restaurant" => restaurant_params}, socket) do - save_restaurant(socket, socket.assigns.live_action, restaurant_params) - end - - defp save_restaurant(socket, :edit, restaurant_params) do - case Restaurants.update_restaurant(socket.assigns.restaurant, restaurant_params) do - {:ok, restaurant} -> - {:noreply, - socket - |> put_flash(:info, "Restaurant updated successfully") - |> push_navigate(to: return_path(socket.assigns.return_to, restaurant))} - - {:error, %Ecto.Changeset{} = changeset} -> - {:noreply, assign(socket, form: to_form(changeset))} - end - end - - defp save_restaurant(socket, :new, restaurant_params) do - case Restaurants.create_restaurant(restaurant_params) do - {:ok, restaurant} -> - {:noreply, - socket - |> put_flash(:info, "Restaurant created successfully") - |> push_navigate(to: return_path(socket.assigns.return_to, restaurant))} - - {:error, %Ecto.Changeset{} = changeset} -> - {:noreply, assign(socket, form: to_form(changeset))} - end - end - - defp return_path("index", _restaurant), do: ~p"/restaurants" - defp return_path("show", restaurant), do: ~p"/restaurant/#{restaurant}" -end diff --git a/lib/munch_web/live/restaurant_live/new.ex b/lib/munch_web/live/restaurant_live/new.ex new file mode 100644 index 0000000..b8dc9c2 --- /dev/null +++ b/lib/munch_web/live/restaurant_live/new.ex @@ -0,0 +1,69 @@ +defmodule MunchWeb.RestaurantLive.New do + use MunchWeb, :live_view + + alias Munch.Restaurants + alias Munch.Restaurants.Restaurant + alias Munch.Osm + + @impl true + def render(assigns) do + ~H""" + <.header> + <%= @page_title %> + + +

+ New restaurants and other changes to the OpenStreetMap map should be + imported automatically every 24 hours. You can import a restaurant + ahead of schedule by entering its osm_type and osm_id below. +

+ <.simple_form for={@form} id="osm-form" phx-submit="save"> + <.input + field={@form[:osm_type]} + type="select" + label="OSM Type" + options={["node", "way", "relation"]} + /> + <.input field={@form[:osm_id]} type="text" label="OSM ID" inputmode="numeric" /> + <:actions> + <.button phx-disable-with="Importing...">Import + + + + <.back navigate={return_path(@return_to, @restaurant)}>Back + """ + end + + @impl true + def mount(params, _session, socket) do + restaurant = %Restaurant{} + + {:ok, + socket + |> assign(:return_to, return_to(params["return_to"])) + |> assign(:page_title, "New Restaurant") + |> assign(:restaurant, restaurant) + |> assign(:form, to_form(Restaurants.change_restaurant(restaurant))) + |> assign(:osm_form, to_form(%{}))} + end + + defp return_to("show"), do: "show" + defp return_to(_), do: "index" + + @impl true + def handle_event("save", %{"osm_type" => osm_type, "osm_id" => osm_id}, socket) do + case Osm.pull_restaurant(osm_type, osm_id) do + {:ok, restaurant} -> + {:noreply, + socket + |> put_flash(:info, "Restaurant imported") + |> push_navigate(to: ~p"/restaurant/#{restaurant}")} + + {:error, error} -> + {:noreply, socket |> put_flash(:error, error)} + end + end + + defp return_path("index", _restaurant), do: ~p"/restaurants" + defp return_path("show", restaurant), do: ~p"/restaurant/#{restaurant}" +end diff --git a/lib/munch_web/live/restaurant_live/select_component.ex b/lib/munch_web/live/restaurant_live/select_component.ex index 2afca72..1645fb2 100644 --- a/lib/munch_web/live/restaurant_live/select_component.ex +++ b/lib/munch_web/live/restaurant_live/select_component.ex @@ -7,6 +7,7 @@ defmodule MunchWeb.RestaurantLive.SelectComponent do {:ok, socket |> assign(:restaurants, nil) + |> assign(:tag, nil) |> assign(:form, to_form(%{}))} end @@ -17,7 +18,7 @@ defmodule MunchWeb.RestaurantLive.SelectComponent do for={@form} id="restaurant-select-form" phx-change="validate" - phx-submit={JS.push("save") |> @submit_action.()} + phx-submit="save" phx-target={@myself} > - <%= "#{restaurant.name} (#{restaurant.address})" %> + <%= "#{restaurant.name}" %>
  • @@ -72,12 +73,11 @@ defmodule MunchWeb.RestaurantLive.SelectComponent do end def handle_event("save", %{"restaurant_id" => restaurant_id}, socket) do - send(self(), {:restaurant_selected, restaurant_id}) + send(self(), {:restaurant_selected, socket.assigns.tag, restaurant_id}) {:noreply, socket |> assign(:form, to_form(%{})) |> assign(:restaurants, nil)} end - def handle_event("save", params, socket) do - IO.inspect(params) + def handle_event("save", _params, socket) do {:noreply, socket} end end diff --git a/lib/munch_web/live/user_live/profile_form.ex b/lib/munch_web/live/user_live/profile_form.ex index 90018d2..ace301c 100644 --- a/lib/munch_web/live/user_live/profile_form.ex +++ b/lib/munch_web/live/user_live/profile_form.ex @@ -16,7 +16,7 @@ defmodule MunchWeb.UserLive.ProfileForm do <% end %> <%= if length(@featured_restaurants) < 4 do %>
    - <.restaurant_card_add /> + <.restaurant_card_add tag={length(@featured_restaurants)} />
    <% end %> <%= for _ <- 0..(2 - length(@featured_restaurants)) do %> @@ -41,8 +41,12 @@ defmodule MunchWeb.UserLive.ProfileForm do end @impl true - def handle_info({:restaurant_selected, restaurant_id}, socket) do - IO.inspect(restaurant_id) + def handle_info({:restaurant_selected, position, restaurant_id}, socket) do + Profile.create_featured_restaurant(socket.assigns.user, %{ + position: position, + restaurant_id: restaurant_id + }) + {:noreply, socket} end end diff --git a/lib/munch_web/router.ex b/lib/munch_web/router.ex index 9e15275..0068864 100644 --- a/lib/munch_web/router.ex +++ b/lib/munch_web/router.ex @@ -62,8 +62,8 @@ defmodule MunchWeb.Router do live "/users/settings/confirm_email/:token", UserLive.Settings, :confirm_email live "/users/edit", UserLive.ProfileForm, :edit - live "/restaurants/new", RestaurantLive.Form, :new - live "/restaurant/:id/edit", RestaurantLive.Form, :edit + live "/restaurants/new", RestaurantLive.New, :new + live "/restaurant/:id/edit", RestaurantLive.Edit, :edit live "/lists/new", ListLive.Form, :new live "/list/:id/edit", ListLive.Form, :edit diff --git a/mix.exs b/mix.exs index 5e8edb4..72820c2 100644 --- a/mix.exs +++ b/mix.exs @@ -33,32 +33,34 @@ defmodule Munch.MixProject do defp deps do [ {:argon2_elixir, "~> 4.1"}, - {:phoenix, path: "../..", override: true}, - {:phoenix_ecto, "~> 4.5"}, - {:ecto_sql, "~> 3.10"}, - {:postgrex, ">= 0.0.0"}, + {:phoenix, github: "phoenixframework/phoenix", override: true}, + {:phoenix_ecto, "~> 4.6"}, + {:ecto_sql, "~> 3.12"}, + {:postgrex, "~> 0.19"}, {:phoenix_html, "~> 4.1"}, - {:phoenix_live_reload, "~> 1.2", only: :dev}, - {:phoenix_live_view, "~> 1.0.0-rc.7"}, - {:floki, ">= 0.30.0", only: :test}, - {:phoenix_live_dashboard, "~> 0.8.3"}, + {:phoenix_live_reload, "~> 1.5", only: :dev}, + {:phoenix_live_view, github: "phoenixframework/phoenix_live_view", override: true}, + {:floki, "~> 0.36", only: :test}, + {:phoenix_live_dashboard, "~> 0.8"}, {:esbuild, "~> 0.8", runtime: Mix.env() == :dev}, {:tailwind, "~> 0.2", runtime: Mix.env() == :dev}, {:heroicons, github: "tailwindlabs/heroicons", - tag: "v2.1.1", + tag: "v2.2.0", sparse: "optimized", app: false, compile: false, depth: 1}, - {:swoosh, "~> 1.16"}, - {:req, "~> 0.5.4"}, + {:swoosh, "~> 1.17"}, + {:req, "~> 0.5"}, {:telemetry_metrics, "~> 1.0"}, {:telemetry_poller, "~> 1.0"}, {:gettext, "~> 0.26"}, {:jason, "~> 1.2"}, - {:dns_cluster, "~> 0.1.1"}, - {:bandit, "~> 1.5"} + {:dns_cluster, "~> 0.1"}, + {:bandit, "~> 1.5"}, + {:geo, "~> 4.0"}, + {:geo_postgis, "~> 3.7"} ] end diff --git a/mix.lock b/mix.lock index dc6a253..5fe1f8d 100644 --- a/mix.lock +++ b/mix.lock @@ -1,44 +1,50 @@ %{ "argon2_elixir": {:hex, :argon2_elixir, "4.1.0", "2f242afe47c373663cb404eb75e792f749507075ed737b49685a9f2edcb401df", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ecb6f2ca2cca34b28e546224661bf2a85714516d2713c7313c5ffe8bdade7cf"}, - "bandit": {:hex, :bandit, "1.5.7", "6856b1e1df4f2b0cb3df1377eab7891bec2da6a7fd69dc78594ad3e152363a50", [:mix], [{:hpax, "~> 1.0.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f2dd92ae87d2cbea2fa9aa1652db157b6cba6c405cb44d4f6dd87abba41371cd"}, - "castore": {:hex, :castore, "1.0.9", "5cc77474afadf02c7c017823f460a17daa7908e991b0cc917febc90e466a375c", [:mix], [], "hexpm", "5ea956504f1ba6f2b4eb707061d8e17870de2bee95fb59d512872c2ef06925e7"}, + "bandit": {:hex, :bandit, "1.6.0", "9cb6c67c27cecab2d0c93968cb957fa8decccb7275193c8bf33f97397b3ac25d", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "fd2491e564a7c5e11ff8496ebf530c342c742452c59de17ac0fb1f814a0ab01a"}, + "castore": {:hex, :castore, "1.0.10", "43bbeeac820f16c89f79721af1b3e092399b3a1ecc8df1a472738fd853574911", [:mix], [], "hexpm", "1b0b7ea14d889d9ea21202c43a4fa015eb913021cb535e8ed91946f4b77a8848"}, "comeonin": {:hex, :comeonin, "5.5.0", "364d00df52545c44a139bad919d7eacb55abf39e86565878e17cebb787977368", [:mix], [], "hexpm", "6287fc3ba0aad34883cbe3f7949fc1d1e738e5ccdce77165bc99490aa69f47fb"}, "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, - "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, + "decimal": {:hex, :decimal, "2.2.0", "df3d06bb9517e302b1bd265c1e7f16cda51547ad9d99892049340841f3e15836", [:mix], [], "hexpm", "af8daf87384b51b7e611fb1a1f2c4d4876b65ef968fa8bd3adf44cff401c7f21"}, "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"}, "ecto": {:hex, :ecto, "3.12.4", "267c94d9f2969e6acc4dd5e3e3af5b05cdae89a4d549925f3008b2b7eb0b93c3", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ef04e4101688a67d061e1b10d7bc1fbf00d1d13c17eef08b71d070ff9188f747"}, "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, - "elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"}, + "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "esbuild": {:hex, :esbuild, "0.8.2", "5f379dfa383ef482b738e7771daf238b2d1cfb0222bef9d3b20d4c8f06c7a7ac", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "558a8a08ed78eb820efbfda1de196569d8bfa9b51e8371a1934fbb31345feda7"}, "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"}, "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, "floki": {:hex, :floki, "0.36.3", "1102f93b16a55bc5383b85ae3ec470f82dee056eaeff9195e8afdf0ef2a43c30", [:mix], [], "hexpm", "fe0158bff509e407735f6d40b3ee0d7deb47f3f3ee7c6c182ad28599f9f6b27a"}, - "gettext": {:hex, :gettext, "0.26.1", "38e14ea5dcf962d1fc9f361b63ea07c0ce715a8ef1f9e82d3dfb8e67e0416715", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "01ce56f188b9dc28780a52783d6529ad2bc7124f9744e571e1ee4ea88bf08734"}, - "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]}, + "geo": {:hex, :geo, "4.0.1", "f4ae3fd912b0536bfe9ec3bce15eb554197ab0739c01297c8534c20dcedd561c", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "32eb624feff75d043bbdd43f67e3869c5fc729e221333271b07cdc98ba98563d"}, + "geo_postgis": {:hex, :geo_postgis, "3.7.1", "614f25b42334a615bd54bb09c22030b1aac7bac8f829bd823ab1faccf093a324", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:geo, "~> 3.6 or ~> 4.0", [hex: :geo, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "c20d823c600d35b7fe9ddd5be03052bb7136c57d6f1775dbd46871545e405280"}, + "gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"}, + "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]}, "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, + "oauth2": {:hex, :oauth2, "2.1.0", "beb657f393814a3a7a8a15bd5e5776ecae341fd344df425342a3b6f1904c2989", [:mix], [{:tesla, "~> 1.5", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "8ac07f85b3307dd1acfeb0ec852f64161b22f57d0ce0c15e616a1dfc8ebe2b41"}, + "phoenix": {:git, "https://github.com/phoenixframework/phoenix.git", "222d9aba8595f81148d454d60d2072d11a969a09", []}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.3", "f686701b0499a07f2e3b122d84d52ff8a31f5def386e03706c916f6feddf69ef", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "909502956916a657a197f94cc1206d9a65247538de8a5e186f7537c895d95764"}, "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"}, - "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.4-rc.0", "3af26124c9bea60253db50360912cb59668580ede2e702e7324536fb9d1cb523", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0-rc", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "66885624771f1a59f5b0ed6c24cbcec70fb2544356cb8085fd85320e6593720a"}, + "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.5", "d5f44d7dbd7cfacaa617b70c5a14b2b598d6f93b9caa8e350c51d56cd4350a9b", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "1d73920515554d7d6c548aee0bf10a4780568b029d042eccb336db29ea0dad70"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.0-rc.7", "d2abca526422adea88896769529addb6443390b1d4f1ff9cbe694312d8875fb2", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b82a4575f6f3eb5b97922ec6874b0c52b3ca0cc5dcb4b14ddc478cbfa135dd01"}, + "phoenix_live_view": {:git, "https://github.com/phoenixframework/phoenix_live_view.git", "83900448dfde9b1ed575da9cd31037c7d8f9f95e", []}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, - "postgrex": {:hex, :postgrex, "0.19.2", "34d6884a332c7bf1e367fc8b9a849d23b43f7da5c6e263def92784d03f9da468", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "618988886ab7ae8561ebed9a3c7469034bf6a88b8995785a3378746a4b9835ec"}, + "postgrex": {:hex, :postgrex, "0.19.3", "a0bda6e3bc75ec07fca5b0a89bffd242ca209a4822a9533e7d3e84ee80707e19", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d31c28053655b78f47f948c85bb1cf86a9c1f8ead346ba1aa0d0df017fa05b61"}, "req": {:hex, :req, "0.5.7", "b722680e03d531a2947282adff474362a48a02aa54b131196fbf7acaff5e4cee", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "c6035374615120a8923e8089d0c21a3496cf9eda2d287b806081b8f323ceee29"}, + "saxy": {:hex, :saxy, "1.6.0", "02cb4e9bd045f25ac0c70fae8164754878327ee393c338a090288210b02317ee", [:mix], [], "hexpm", "ef42eb4ac983ca77d650fbdb68368b26570f6cc5895f0faa04d34a6f384abad3"}, "swoosh": {:hex, :swoosh, "1.17.3", "5cda7bff6bc1121cc5b58db8ed90ef33261b373425ae3e32dd599688037a0482", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "14ad57cfbb70af57323e17f569f5840a33c01f8ebc531dd3846beef3c9c95e55"}, "tailwind": {:hex, :tailwind, "0.2.4", "5706ec47182d4e7045901302bf3a333e80f3d1af65c442ba9a9eed152fb26c2e", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "c6e4a82b8727bab593700c998a4d98cf3d8025678bfde059aed71d0000c3e463"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"}, "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"}, - "thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"}, + "tesla": {:hex, :tesla, "1.13.2", "85afa342eb2ac0fee830cf649dbd19179b6b359bec4710d02a3d5d587f016910", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "960609848f1ef654c3cdfad68453cd84a5febecb6ed9fed9416e36cd9cd724f9"}, + "thousand_island": {:hex, :thousand_island, "1.3.6", "835a626a8a6f6a1e681b63e1132a8427e87ce443aaf4888fbf63b2df77539b97", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0ed8798084c8c49a223840b20598b022e4eb8c9f390fb6701864c307fc9aa2cd"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, - "websock_adapter": {:hex, :websock_adapter, "0.5.7", "65fa74042530064ef0570b75b43f5c49bb8b235d6515671b3d250022cb8a1f9e", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d0f478ee64deddfec64b800673fd6e0c8888b079d9f3444dd96d2a98383bdbd1"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, } diff --git a/osm/download_osm.exs b/osm/download_osm.exs new file mode 100644 index 0000000..e496489 --- /dev/null +++ b/osm/download_osm.exs @@ -0,0 +1,43 @@ +# script for importing osm data into the database + +osm_dir = "/home/leif/Documents/munch/osm" + +IO.inspect(osm_dir) + +# change the area line to +# area["ISO3166-2"="AU-NSW"]; +# when it's ready +overpass_query = """ +area["name"="Dunedin"]; +( + nwr[amenity=restaurant](area); + nwr[amenity=fast_food](area); + nwr[amenity=cafe](area); + nwr[amenity=bar](area); + nwr[amenity=pub](area); + nwr[amenity=food_court](area); + nwr[amenity=biergarten](area); +); +out; +""" + +overpass_uri = "https://overpass-api.de/api/interpreter" + +resp = Req.post!(overpass_uri, form: [data: overpass_query]) + +File.write!(Path.join(osm_dir, "export.osm"), resp.body) + +database_config = Munch.Repo.config() + +System.cmd("osm2pgsql", [ + "--style=#{Path.join(osm_dir, "style.lua")}", + "--output=flex", + "--schema=osm", + "--database=#{database_config[:database]}", + "--user=#{database_config[:username]}", + "--host=#{database_config[:hostname]}", + "--port=#{database_config[:port]}", + Path.join(osm_dir, "export.osm") +]) + +Munch.Osm.copy_osm_restaurants() diff --git a/osm/style.lua b/osm/style.lua new file mode 100644 index 0000000..5a879c5 --- /dev/null +++ b/osm/style.lua @@ -0,0 +1,34 @@ +local restaurants = osm2pgsql.define_table({ + name = 'restaurants', + ids = { + type = 'any', + type_column = 'osm_type', + id_column = 'osm_id', + create_index = 'unique', + }, + columns = { + { column = 'location', type = 'point'}, + { column = 'tags', type = 'jsonb' }, + } +}) + +local function process_object(object, point) + restaurants:insert({ + location = point, + tags = object.tags, + }) +end + +function osm2pgsql.process_node(object) + process_object(object, object:as_point()) +end + +function osm2pgsql.process_way(object) + -- TODO: figure out why :as_polygon() fails on some closed ways + process_object(object, object:as_polygon():centroid()) +end + +function osm2pgsql.process_relation(object) + assert(object.tags.type == 'multipolygon') + process_object(object, object:as_multipolygon():centroid()) +end diff --git a/priv/repo/migrations/20241008001216_create_restaurants.exs b/priv/repo/migrations/20241008001216_create_restaurants.exs index 96c456e..6559be8 100644 --- a/priv/repo/migrations/20241008001216_create_restaurants.exs +++ b/priv/repo/migrations/20241008001216_create_restaurants.exs @@ -4,11 +4,13 @@ defmodule Munch.Repo.Migrations.CreateRestaurants do def change do create table(:restaurants, primary_key: false) do add :id, :binary_id, primary_key: true - add :name, :string, null: false - add :address, :string, null: false - add :country, :string, null: false - add :city, :string, null: false - add :neighbourhood, :string, null: false + add :osm_type, :text + add :osm_id, :bigint + add :name, :text, null: false + add :location, :geometry, null: false + add :note, :text + add :iso_country_subdivision, :text + add :secondary_subdivision, :text timestamps(type: :utc_datetime) end diff --git a/priv/repo/migrations/20241008002020_create_users_auth_tables.exs b/priv/repo/migrations/20241008002020_create_users_auth_tables.exs index 476ea5c..4577dff 100644 --- a/priv/repo/migrations/20241008002020_create_users_auth_tables.exs +++ b/priv/repo/migrations/20241008002020_create_users_auth_tables.exs @@ -7,7 +7,7 @@ defmodule Munch.Repo.Migrations.CreateUsersAuthTables do create table(:users, primary_key: false) do add :id, :binary_id, primary_key: true add :email, :citext, null: false - add :hashed_password, :string, null: false + add :hashed_password, :text, null: false add :confirmed_at, :utc_datetime timestamps(type: :utc_datetime) @@ -19,8 +19,8 @@ defmodule Munch.Repo.Migrations.CreateUsersAuthTables do add :id, :binary_id, primary_key: true add :user_id, references(:users, type: :binary_id, on_delete: :delete_all), null: false add :token, :binary, null: false - add :context, :string, null: false - add :sent_to, :string + add :context, :text, null: false + add :sent_to, :text timestamps(type: :utc_datetime, updated_at: false) end diff --git a/priv/repo/migrations/20241008002021_create_lists.exs b/priv/repo/migrations/20241008002021_create_lists.exs index 981fb97..e0b592f 100644 --- a/priv/repo/migrations/20241008002021_create_lists.exs +++ b/priv/repo/migrations/20241008002021_create_lists.exs @@ -4,7 +4,7 @@ defmodule Munch.Repo.Migrations.CreateLists do def change do create table(:lists, primary_key: false) do add :id, :binary_id, primary_key: true - add :name, :string, null: false + add :name, :text, null: false add :user_id, references(:users, on_delete: :delete_all, type: :binary_id), null: false timestamps(type: :utc_datetime) diff --git a/priv/repo/migrations/20241112014159_create_featured_restaurants.exs b/priv/repo/migrations/20241112014159_create_featured_restaurants.exs index 66c073f..a98686e 100644 --- a/priv/repo/migrations/20241112014159_create_featured_restaurants.exs +++ b/priv/repo/migrations/20241112014159_create_featured_restaurants.exs @@ -5,9 +5,9 @@ defmodule Munch.Repo.Migrations.CreateFeaturedRestaurants do create table(:featured_restaurants, primary_key: false) do add :id, :binary_id, primary_key: true add :position, :integer, null: false - add :user_id, references(:users, on_delete: :nothing, type: :binary_id), null: false + add :user_id, references(:users, on_delete: :delete_all, type: :binary_id), null: false - add :restaurant_id, references(:restaurants, on_delete: :nothing, type: :binary_id), + add :restaurant_id, references(:restaurants, on_delete: :delete_all, type: :binary_id), null: false timestamps(type: :utc_datetime) diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 353f09d..1fa12fd 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -9,43 +9,3 @@ # # We recommend using the bang functions (`insert!`, `update!` # and so on) as they will fail if something goes wrong. - -Munch.Repo.insert!(%Munch.Restaurants.Restaurant{ - name: "Mappen", - address: "shop 11/537-551 George St, Sydney NSW 2000, Australia", - country: "Australia", - city: "Sydney", - neighbourhood: "Haymarket" -}) - -Munch.Repo.insert!(%Munch.Restaurants.Restaurant{ - name: "Chaco Ramen", - address: "238 Crown St, Darlinghurst NSW 2010, Australia", - country: "Australia", - city: "Sydney", - neighbourhood: "Darlinghurst" -}) - -Munch.Repo.insert!(%Munch.Restaurants.Restaurant{ - name: "Emperor's Garden Cakes & Bakery", - address: "75 Dixon St, Haymarket NSW 2000, Australia", - country: "Australia", - city: "Sydney", - neighbourhood: "Haymarket" -}) - -Munch.Repo.insert!(%Munch.Restaurants.Restaurant{ - name: "Ice Kirin Bar", - address: "486/488 Kent St, Sydney NSW 2000, Australia", - country: "Australia", - city: "Sydney", - neighbourhood: "Haymarket" -}) - -Munch.Repo.insert!(%Munch.Restaurants.Restaurant{ - name: "Pho Pasteur", - address: "709 George St, Haymarket NSW 2000, Australia", - country: "Australia", - city: "Sydney", - neighbourhood: "Haymarket" -})