diff --git a/app/assets/stylesheets/pagination.css b/app/assets/stylesheets/pagination.css
new file mode 100644
index 000000000..d0fc4ad16
--- /dev/null
+++ b/app/assets/stylesheets/pagination.css
@@ -0,0 +1,26 @@
+@layer components {
+ .pagination-link {
+ display: block;
+ flex: 1;
+ min-block-size: 1.5rem;
+ pointer-events: none;
+ text-decoration: none;
+
+ &.pagination-link--active-when-observed {
+ block-size: 0;
+ min-block-size: 0;
+ font-size: 0;
+ line-height: 0;
+ overflow: hidden;
+ visibility: hidden;
+ height: 0px;
+ text-indent: -9999px;
+ }
+
+ &[aria-busy="true"] {
+ .spinner {
+ display: block;
+ }
+ }
+ }
+}
diff --git a/app/helpers/columns_helper.rb b/app/helpers/columns_helper.rb
index 402124455..793103aa7 100644
--- a/app/helpers/columns_helper.rb
+++ b/app/helpers/columns_helper.rb
@@ -14,7 +14,7 @@ module ColumnsHelper
"drag-and-drop-refresh": true,
controller: "frame",
action: "turbo:before-frame-render->frame#morphRender turbo:before-morph-element->frame#morphReload"
-
+ options[:refresh] = :morph if src.present?
turbo_frame_tag(id, src: src, data: data, **options, &block)
end
end
diff --git a/app/helpers/pagination_helper.rb b/app/helpers/pagination_helper.rb
index f492b411e..b69e9b02b 100644
--- a/app/helpers/pagination_helper.rb
+++ b/app/helpers/pagination_helper.rb
@@ -5,13 +5,15 @@ module PaginationHelper
def link_to_next_page(namespace, page, activate_when_observed: false, label: default_pagination_label(activate_when_observed), data: {}, **attributes)
if page.before_last? && !params[:previous]
- pagination_link(namespace, page.number + 1, label: label, activate_when_observed: activate_when_observed, data: data, class: "btn txt-small center-block center", **attributes)
+ attributes[:class] = class_names(attributes[:class], "btn txt-small center-block center": !activate_when_observed)
+ pagination_link(namespace, page.number + 1, label: label, activate_when_observed: activate_when_observed, data: data, **attributes)
end
end
def pagination_link(namespace, page_number, activate_when_observed: false, label: default_pagination_label(activate_when_observed), url_params: {}, data: {}, **attributes)
link_to label, url_for(params.permit!.to_h.merge(page: page_number, **url_params)),
"aria-label": "Load page #{page_number}",
+ id: "#{namespace}-pagination-link-#{page_number}",
class: class_names(attributes.delete(:class), "pagination-link", { "pagination-link--active-when-observed" => activate_when_observed }),
data: {
frame: pagination_frame_id_for(namespace, page_number),
@@ -62,9 +64,11 @@ module PaginationHelper
private
def pagination_list(name, tag_element: :div, paginate_on_scroll: false, **properties, &block)
classes = properties.delete(:class)
+ properties[:id] ||= "#{name}-pagination-list"
tag.public_send tag_element,
class: token_list(name, "display-contents", classes),
data: { controller: "pagination", pagination_paginate_on_intersection_value: paginate_on_scroll },
+ **properties,
&block
end
diff --git a/app/javascript/controllers/pagination_controller.js b/app/javascript/controllers/pagination_controller.js
index 01d93d909..cdc6258b5 100644
--- a/app/javascript/controllers/pagination_controller.js
+++ b/app/javascript/controllers/pagination_controller.js
@@ -90,7 +90,6 @@ export default class extends Controller {
#replacePaginationLinkWithFrame(linkElement) {
const turboFrame = this.#buildTurboFrameFor(linkElement)
this.#insertTurboFrameAtPosition(linkElement, turboFrame)
- linkElement.remove()
}
#buildTurboFrameFor(linkElement) {
diff --git a/app/views/cards/display/preview/_bubble.html.erb b/app/views/cards/display/preview/_bubble.html.erb
index 4b88ead9e..571448e55 100644
--- a/app/views/cards/display/preview/_bubble.html.erb
+++ b/app/views/cards/display/preview/_bubble.html.erb
@@ -7,18 +7,18 @@
} do %>
<% end %>
diff --git a/app/views/filters/_settings.html.erb b/app/views/filters/_settings.html.erb
index 9364fef04..1efa116c2 100644
--- a/app/views/filters/_settings.html.erb
+++ b/app/views/filters/_settings.html.erb
@@ -1,4 +1,5 @@
<%= tag.aside \
+ id: "filters-settings",
class: class_names("filters flex align-center gap-half justify-center center margin-block-end", { "filters--expanded": user_filtering.expanded? }),
data: {
controller: "toggle-class filter-settings",
diff --git a/config/importmap.rb b/config/importmap.rb
index f1da52737..c1445db74 100644
--- a/config/importmap.rb
+++ b/config/importmap.rb
@@ -1,7 +1,7 @@
# Pin npm packages by running ./bin/importmap
pin "application"
-pin "@hotwired/turbo-rails", to: "turbo.min.js"
+pin "@hotwired/turbo-rails", to: "turbo.patched.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin "@rails/request.js", to: "@rails--request.js" # @0.0.11
diff --git a/vendor/javascript/turbo.patched.js b/vendor/javascript/turbo.patched.js
new file mode 100644
index 000000000..bcc9fcdcf
--- /dev/null
+++ b/vendor/javascript/turbo.patched.js
@@ -0,0 +1,6050 @@
+/*!
+Turbo 8.0.17
+Copyright © 2025 37signals LLC
+ */
+(function(prototype) {
+ if (typeof prototype.requestSubmit == "function") return;
+ prototype.requestSubmit = function(submitter) {
+ if (submitter) {
+ validateSubmitter(submitter, this);
+ submitter.click();
+ } else {
+ submitter = document.createElement("input");
+ submitter.type = "submit";
+ submitter.hidden = true;
+ this.appendChild(submitter);
+ submitter.click();
+ this.removeChild(submitter);
+ }
+ };
+ function validateSubmitter(submitter, form) {
+ submitter instanceof HTMLElement || raise(TypeError, "parameter 1 is not of type 'HTMLElement'");
+ submitter.type == "submit" || raise(TypeError, "The specified element is not a submit button");
+ submitter.form == form || raise(DOMException, "The specified element is not owned by this form element", "NotFoundError");
+ }
+ function raise(errorConstructor, message, name) {
+ throw new errorConstructor("Failed to execute 'requestSubmit' on 'HTMLFormElement': " + message + ".", name);
+ }
+})(HTMLFormElement.prototype);
+
+const submittersByForm = new WeakMap;
+
+function findSubmitterFromClickTarget(target) {
+ const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
+ const candidate = element ? element.closest("input, button") : null;
+ return candidate?.type == "submit" ? candidate : null;
+}
+
+function clickCaptured(event) {
+ const submitter = findSubmitterFromClickTarget(event.target);
+ if (submitter && submitter.form) {
+ submittersByForm.set(submitter.form, submitter);
+ }
+}
+
+(function() {
+ if ("submitter" in Event.prototype) return;
+ let prototype = window.Event.prototype;
+ if ("SubmitEvent" in window) {
+ const prototypeOfSubmitEvent = window.SubmitEvent.prototype;
+ if (/Apple Computer/.test(navigator.vendor) && !("submitter" in prototypeOfSubmitEvent)) {
+ prototype = prototypeOfSubmitEvent;
+ } else {
+ return;
+ }
+ }
+ addEventListener("click", clickCaptured, true);
+ Object.defineProperty(prototype, "submitter", {
+ get() {
+ if (this.type == "submit" && this.target instanceof HTMLFormElement) {
+ return submittersByForm.get(this.target);
+ }
+ }
+ });
+})();
+
+const FrameLoadingStyle = {
+ eager: "eager",
+ lazy: "lazy"
+};
+
+class FrameElement extends HTMLElement {
+ static delegateConstructor=undefined;
+ loaded=Promise.resolve();
+ static get observedAttributes() {
+ return [ "disabled", "loading", "src" ];
+ }
+ constructor() {
+ super();
+ this.delegate = new FrameElement.delegateConstructor(this);
+ }
+ connectedCallback() {
+ this.delegate.connect();
+ }
+ disconnectedCallback() {
+ this.delegate.disconnect();
+ }
+ reload() {
+ return this.delegate.sourceURLReloaded();
+ }
+ attributeChangedCallback(name) {
+ if (name == "loading") {
+ this.delegate.loadingStyleChanged();
+ } else if (name == "src") {
+ this.delegate.sourceURLChanged();
+ } else if (name == "disabled") {
+ this.delegate.disabledChanged();
+ }
+ }
+ get src() {
+ return this.getAttribute("src");
+ }
+ set src(value) {
+ if (value) {
+ this.setAttribute("src", value);
+ } else {
+ this.removeAttribute("src");
+ }
+ }
+ get refresh() {
+ return this.getAttribute("refresh");
+ }
+ set refresh(value) {
+ if (value) {
+ this.setAttribute("refresh", value);
+ } else {
+ this.removeAttribute("refresh");
+ }
+ }
+ get shouldReloadWithMorph() {
+ return this.src && this.refresh === "morph";
+ }
+ get loading() {
+ return frameLoadingStyleFromString(this.getAttribute("loading") || "");
+ }
+ set loading(value) {
+ if (value) {
+ this.setAttribute("loading", value);
+ } else {
+ this.removeAttribute("loading");
+ }
+ }
+ get disabled() {
+ return this.hasAttribute("disabled");
+ }
+ set disabled(value) {
+ if (value) {
+ this.setAttribute("disabled", "");
+ } else {
+ this.removeAttribute("disabled");
+ }
+ }
+ get autoscroll() {
+ return this.hasAttribute("autoscroll");
+ }
+ set autoscroll(value) {
+ if (value) {
+ this.setAttribute("autoscroll", "");
+ } else {
+ this.removeAttribute("autoscroll");
+ }
+ }
+ get complete() {
+ return !this.delegate.isLoading;
+ }
+ get isActive() {
+ return this.ownerDocument === document && !this.isPreview;
+ }
+ get isPreview() {
+ return this.ownerDocument?.documentElement?.hasAttribute("data-turbo-preview");
+ }
+}
+
+function frameLoadingStyleFromString(style) {
+ switch (style.toLowerCase()) {
+ case "lazy":
+ return FrameLoadingStyle.lazy;
+
+ default:
+ return FrameLoadingStyle.eager;
+ }
+}
+
+const drive = {
+ enabled: true,
+ progressBarDelay: 500,
+ unvisitableExtensions: new Set([ ".7z", ".aac", ".apk", ".avi", ".bmp", ".bz2", ".css", ".csv", ".deb", ".dmg", ".doc", ".docx", ".exe", ".gif", ".gz", ".heic", ".heif", ".ico", ".iso", ".jpeg", ".jpg", ".js", ".json", ".m4a", ".mkv", ".mov", ".mp3", ".mp4", ".mpeg", ".mpg", ".msi", ".ogg", ".ogv", ".pdf", ".pkg", ".png", ".ppt", ".pptx", ".rar", ".rtf", ".svg", ".tar", ".tif", ".tiff", ".txt", ".wav", ".webm", ".webp", ".wma", ".wmv", ".xls", ".xlsx", ".xml", ".zip" ])
+};
+
+function activateScriptElement(element) {
+ if (element.getAttribute("data-turbo-eval") == "false") {
+ return element;
+ } else {
+ const createdScriptElement = document.createElement("script");
+ const cspNonce = getCspNonce();
+ if (cspNonce) {
+ createdScriptElement.nonce = cspNonce;
+ }
+ createdScriptElement.textContent = element.textContent;
+ createdScriptElement.async = false;
+ copyElementAttributes(createdScriptElement, element);
+ return createdScriptElement;
+ }
+}
+
+function copyElementAttributes(destinationElement, sourceElement) {
+ for (const {name: name, value: value} of sourceElement.attributes) {
+ destinationElement.setAttribute(name, value);
+ }
+}
+
+function createDocumentFragment(html) {
+ const template = document.createElement("template");
+ template.innerHTML = html;
+ return template.content;
+}
+
+function dispatch(eventName, {target: target, cancelable: cancelable, detail: detail} = {}) {
+ const event = new CustomEvent(eventName, {
+ cancelable: cancelable,
+ bubbles: true,
+ composed: true,
+ detail: detail
+ });
+ if (target && target.isConnected) {
+ target.dispatchEvent(event);
+ } else {
+ document.documentElement.dispatchEvent(event);
+ }
+ return event;
+}
+
+function cancelEvent(event) {
+ event.preventDefault();
+ event.stopImmediatePropagation();
+}
+
+function nextRepaint() {
+ if (document.visibilityState === "hidden") {
+ return nextEventLoopTick();
+ } else {
+ return nextAnimationFrame();
+ }
+}
+
+function nextAnimationFrame() {
+ return new Promise((resolve => requestAnimationFrame((() => resolve()))));
+}
+
+function nextEventLoopTick() {
+ return new Promise((resolve => setTimeout((() => resolve()), 0)));
+}
+
+function nextMicrotask() {
+ return Promise.resolve();
+}
+
+function parseHTMLDocument(html = "") {
+ return (new DOMParser).parseFromString(html, "text/html");
+}
+
+function unindent(strings, ...values) {
+ const lines = interpolate(strings, values).replace(/^\n/, "").split("\n");
+ const match = lines[0].match(/^\s+/);
+ const indent = match ? match[0].length : 0;
+ return lines.map((line => line.slice(indent))).join("\n");
+}
+
+function interpolate(strings, values) {
+ return strings.reduce(((result, string, i) => {
+ const value = values[i] == undefined ? "" : values[i];
+ return result + string + value;
+ }), "");
+}
+
+function uuid() {
+ return Array.from({
+ length: 36
+ }).map(((_, i) => {
+ if (i == 8 || i == 13 || i == 18 || i == 23) {
+ return "-";
+ } else if (i == 14) {
+ return "4";
+ } else if (i == 19) {
+ return (Math.floor(Math.random() * 4) + 8).toString(16);
+ } else {
+ return Math.floor(Math.random() * 15).toString(16);
+ }
+ })).join("");
+}
+
+function getAttribute(attributeName, ...elements) {
+ for (const value of elements.map((element => element?.getAttribute(attributeName)))) {
+ if (typeof value == "string") return value;
+ }
+ return null;
+}
+
+function hasAttribute(attributeName, ...elements) {
+ return elements.some((element => element && element.hasAttribute(attributeName)));
+}
+
+function markAsBusy(...elements) {
+ for (const element of elements) {
+ if (element.localName == "turbo-frame") {
+ element.setAttribute("busy", "");
+ }
+ element.setAttribute("aria-busy", "true");
+ }
+}
+
+function clearBusyState(...elements) {
+ for (const element of elements) {
+ if (element.localName == "turbo-frame") {
+ element.removeAttribute("busy");
+ }
+ element.removeAttribute("aria-busy");
+ }
+}
+
+function waitForLoad(element, timeoutInMilliseconds = 2e3) {
+ return new Promise((resolve => {
+ const onComplete = () => {
+ element.removeEventListener("error", onComplete);
+ element.removeEventListener("load", onComplete);
+ resolve();
+ };
+ element.addEventListener("load", onComplete, {
+ once: true
+ });
+ element.addEventListener("error", onComplete, {
+ once: true
+ });
+ setTimeout(resolve, timeoutInMilliseconds);
+ }));
+}
+
+function getHistoryMethodForAction(action) {
+ switch (action) {
+ case "replace":
+ return history.replaceState;
+
+ case "advance":
+ case "restore":
+ return history.pushState;
+ }
+}
+
+function isAction(action) {
+ return action == "advance" || action == "replace" || action == "restore";
+}
+
+function getVisitAction(...elements) {
+ const action = getAttribute("data-turbo-action", ...elements);
+ return isAction(action) ? action : null;
+}
+
+function getMetaElement(name) {
+ return document.querySelector(`meta[name="${name}"]`);
+}
+
+function getMetaContent(name) {
+ const element = getMetaElement(name);
+ return element && element.content;
+}
+
+function getCspNonce() {
+ const element = getMetaElement("csp-nonce");
+ if (element) {
+ const {nonce: nonce, content: content} = element;
+ return nonce == "" ? content : nonce;
+ }
+}
+
+function setMetaContent(name, content) {
+ let element = getMetaElement(name);
+ if (!element) {
+ element = document.createElement("meta");
+ element.setAttribute("name", name);
+ document.head.appendChild(element);
+ }
+ element.setAttribute("content", content);
+ return element;
+}
+
+function findClosestRecursively(element, selector) {
+ if (element instanceof Element) {
+ return element.closest(selector) || findClosestRecursively(element.assignedSlot || element.getRootNode()?.host, selector);
+ }
+}
+
+function elementIsFocusable(element) {
+ const inertDisabledOrHidden = "[inert], :disabled, [hidden], details:not([open]), dialog:not([open])";
+ return !!element && element.closest(inertDisabledOrHidden) == null && typeof element.focus == "function";
+}
+
+function queryAutofocusableElement(elementOrDocumentFragment) {
+ return Array.from(elementOrDocumentFragment.querySelectorAll("[autofocus]")).find(elementIsFocusable);
+}
+
+async function around(callback, reader) {
+ const before = reader();
+ callback();
+ await nextAnimationFrame();
+ const after = reader();
+ return [ before, after ];
+}
+
+function doesNotTargetIFrame(name) {
+ if (name === "_blank") {
+ return false;
+ } else if (name) {
+ for (const element of document.getElementsByName(name)) {
+ if (element instanceof HTMLIFrameElement) return false;
+ }
+ return true;
+ } else {
+ return true;
+ }
+}
+
+function findLinkFromClickTarget(target) {
+ const link = findClosestRecursively(target, "a[href], a[xlink\\:href]");
+ if (!link) return null;
+ if (link.hasAttribute("download")) return null;
+ if (link.hasAttribute("target") && link.target !== "_self") return null;
+ return link;
+}
+
+function getLocationForLink(link) {
+ return expandURL(link.getAttribute("href") || "");
+}
+
+function debounce(fn, delay) {
+ let timeoutId = null;
+ return (...args) => {
+ const callback = () => fn.apply(this, args);
+ clearTimeout(timeoutId);
+ timeoutId = setTimeout(callback, delay);
+ };
+}
+
+const submitter = {
+ "aria-disabled": {
+ beforeSubmit: submitter => {
+ submitter.setAttribute("aria-disabled", "true");
+ submitter.addEventListener("click", cancelEvent);
+ },
+ afterSubmit: submitter => {
+ submitter.removeAttribute("aria-disabled");
+ submitter.removeEventListener("click", cancelEvent);
+ }
+ },
+ disabled: {
+ beforeSubmit: submitter => submitter.disabled = true,
+ afterSubmit: submitter => submitter.disabled = false
+ }
+};
+
+class Config {
+ #submitter=null;
+ constructor(config) {
+ Object.assign(this, config);
+ }
+ get submitter() {
+ return this.#submitter;
+ }
+ set submitter(value) {
+ this.#submitter = submitter[value] || value;
+ }
+}
+
+const forms = new Config({
+ mode: "on",
+ submitter: "disabled"
+});
+
+const config = {
+ drive: drive,
+ forms: forms
+};
+
+function expandURL(locatable) {
+ return new URL(locatable.toString(), document.baseURI);
+}
+
+function getAnchor(url) {
+ let anchorMatch;
+ if (url.hash) {
+ return url.hash.slice(1);
+ } else if (anchorMatch = url.href.match(/#(.*)$/)) {
+ return anchorMatch[1];
+ }
+}
+
+function getAction$1(form, submitter) {
+ const action = submitter?.getAttribute("formaction") || form.getAttribute("action") || form.action;
+ return expandURL(action);
+}
+
+function getExtension(url) {
+ return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || "";
+}
+
+function isPrefixedBy(baseURL, url) {
+ const prefix = addTrailingSlash(url.origin + url.pathname);
+ return addTrailingSlash(baseURL.href) === prefix || baseURL.href.startsWith(prefix);
+}
+
+function locationIsVisitable(location, rootLocation) {
+ return isPrefixedBy(location, rootLocation) && !config.drive.unvisitableExtensions.has(getExtension(location));
+}
+
+function getRequestURL(url) {
+ const anchor = getAnchor(url);
+ return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href;
+}
+
+function toCacheKey(url) {
+ return getRequestURL(url);
+}
+
+function urlsAreEqual(left, right) {
+ return expandURL(left).href == expandURL(right).href;
+}
+
+function getPathComponents(url) {
+ return url.pathname.split("/").slice(1);
+}
+
+function getLastPathComponent(url) {
+ return getPathComponents(url).slice(-1)[0];
+}
+
+function addTrailingSlash(value) {
+ return value.endsWith("/") ? value : value + "/";
+}
+
+class FetchResponse {
+ constructor(response) {
+ this.response = response;
+ }
+ get succeeded() {
+ return this.response.ok;
+ }
+ get failed() {
+ return !this.succeeded;
+ }
+ get clientError() {
+ return this.statusCode >= 400 && this.statusCode <= 499;
+ }
+ get serverError() {
+ return this.statusCode >= 500 && this.statusCode <= 599;
+ }
+ get redirected() {
+ return this.response.redirected;
+ }
+ get location() {
+ return expandURL(this.response.url);
+ }
+ get isHTML() {
+ return this.contentType && this.contentType.match(/^(?:text\/([^\s;,]+\b)?html|application\/xhtml\+xml)\b/);
+ }
+ get statusCode() {
+ return this.response.status;
+ }
+ get contentType() {
+ return this.header("Content-Type");
+ }
+ get responseText() {
+ return this.response.clone().text();
+ }
+ get responseHTML() {
+ if (this.isHTML) {
+ return this.response.clone().text();
+ } else {
+ return Promise.resolve(undefined);
+ }
+ }
+ header(name) {
+ return this.response.headers.get(name);
+ }
+}
+
+class LimitedSet extends Set {
+ constructor(maxSize) {
+ super();
+ this.maxSize = maxSize;
+ }
+ add(value) {
+ if (this.size >= this.maxSize) {
+ const iterator = this.values();
+ const oldestValue = iterator.next().value;
+ this.delete(oldestValue);
+ }
+ super.add(value);
+ }
+}
+
+const recentRequests = new LimitedSet(20);
+
+function fetchWithTurboHeaders(url, options = {}) {
+ const modifiedHeaders = new Headers(options.headers || {});
+ const requestUID = uuid();
+ recentRequests.add(requestUID);
+ modifiedHeaders.append("X-Turbo-Request-Id", requestUID);
+ return window.fetch(url, {
+ ...options,
+ headers: modifiedHeaders
+ });
+}
+
+function fetchMethodFromString(method) {
+ switch (method.toLowerCase()) {
+ case "get":
+ return FetchMethod.get;
+
+ case "post":
+ return FetchMethod.post;
+
+ case "put":
+ return FetchMethod.put;
+
+ case "patch":
+ return FetchMethod.patch;
+
+ case "delete":
+ return FetchMethod.delete;
+ }
+}
+
+const FetchMethod = {
+ get: "get",
+ post: "post",
+ put: "put",
+ patch: "patch",
+ delete: "delete"
+};
+
+function fetchEnctypeFromString(encoding) {
+ switch (encoding.toLowerCase()) {
+ case FetchEnctype.multipart:
+ return FetchEnctype.multipart;
+
+ case FetchEnctype.plain:
+ return FetchEnctype.plain;
+
+ default:
+ return FetchEnctype.urlEncoded;
+ }
+}
+
+const FetchEnctype = {
+ urlEncoded: "application/x-www-form-urlencoded",
+ multipart: "multipart/form-data",
+ plain: "text/plain"
+};
+
+class FetchRequest {
+ abortController=new AbortController;
+ #resolveRequestPromise=_value => {};
+ constructor(delegate, method, location, requestBody = new URLSearchParams, target = null, enctype = FetchEnctype.urlEncoded) {
+ const [url, body] = buildResourceAndBody(expandURL(location), method, requestBody, enctype);
+ this.delegate = delegate;
+ this.url = url;
+ this.target = target;
+ this.fetchOptions = {
+ credentials: "same-origin",
+ redirect: "follow",
+ method: method.toUpperCase(),
+ headers: {
+ ...this.defaultHeaders
+ },
+ body: body,
+ signal: this.abortSignal,
+ referrer: this.delegate.referrer?.href
+ };
+ this.enctype = enctype;
+ }
+ get method() {
+ return this.fetchOptions.method;
+ }
+ set method(value) {
+ const fetchBody = this.isSafe ? this.url.searchParams : this.fetchOptions.body || new FormData;
+ const fetchMethod = fetchMethodFromString(value) || FetchMethod.get;
+ this.url.search = "";
+ const [url, body] = buildResourceAndBody(this.url, fetchMethod, fetchBody, this.enctype);
+ this.url = url;
+ this.fetchOptions.body = body;
+ this.fetchOptions.method = fetchMethod.toUpperCase();
+ }
+ get headers() {
+ return this.fetchOptions.headers;
+ }
+ set headers(value) {
+ this.fetchOptions.headers = value;
+ }
+ get body() {
+ if (this.isSafe) {
+ return this.url.searchParams;
+ } else {
+ return this.fetchOptions.body;
+ }
+ }
+ set body(value) {
+ this.fetchOptions.body = value;
+ }
+ get location() {
+ return this.url;
+ }
+ get params() {
+ return this.url.searchParams;
+ }
+ get entries() {
+ return this.body ? Array.from(this.body.entries()) : [];
+ }
+ cancel() {
+ this.abortController.abort();
+ }
+ async perform() {
+ const {fetchOptions: fetchOptions} = this;
+ this.delegate.prepareRequest(this);
+ const event = await this.#allowRequestToBeIntercepted(fetchOptions);
+ try {
+ this.delegate.requestStarted(this);
+ if (event.detail.fetchRequest) {
+ this.response = event.detail.fetchRequest.response;
+ } else {
+ this.response = fetchWithTurboHeaders(this.url.href, fetchOptions);
+ }
+ const response = await this.response;
+ return await this.receive(response);
+ } catch (error) {
+ if (error.name !== "AbortError") {
+ if (this.#willDelegateErrorHandling(error)) {
+ this.delegate.requestErrored(this, error);
+ }
+ throw error;
+ }
+ } finally {
+ this.delegate.requestFinished(this);
+ }
+ }
+ async receive(response) {
+ const fetchResponse = new FetchResponse(response);
+ const event = dispatch("turbo:before-fetch-response", {
+ cancelable: true,
+ detail: {
+ fetchResponse: fetchResponse
+ },
+ target: this.target
+ });
+ if (event.defaultPrevented) {
+ this.delegate.requestPreventedHandlingResponse(this, fetchResponse);
+ } else if (fetchResponse.succeeded) {
+ this.delegate.requestSucceededWithResponse(this, fetchResponse);
+ } else {
+ this.delegate.requestFailedWithResponse(this, fetchResponse);
+ }
+ return fetchResponse;
+ }
+ get defaultHeaders() {
+ return {
+ Accept: "text/html, application/xhtml+xml"
+ };
+ }
+ get isSafe() {
+ return isSafe(this.method);
+ }
+ get abortSignal() {
+ return this.abortController.signal;
+ }
+ acceptResponseType(mimeType) {
+ this.headers["Accept"] = [ mimeType, this.headers["Accept"] ].join(", ");
+ }
+ async #allowRequestToBeIntercepted(fetchOptions) {
+ const requestInterception = new Promise((resolve => this.#resolveRequestPromise = resolve));
+ const event = dispatch("turbo:before-fetch-request", {
+ cancelable: true,
+ detail: {
+ fetchOptions: fetchOptions,
+ url: this.url,
+ resume: this.#resolveRequestPromise
+ },
+ target: this.target
+ });
+ this.url = event.detail.url;
+ if (event.defaultPrevented) await requestInterception;
+ return event;
+ }
+ #willDelegateErrorHandling(error) {
+ const event = dispatch("turbo:fetch-request-error", {
+ target: this.target,
+ cancelable: true,
+ detail: {
+ request: this,
+ error: error
+ }
+ });
+ return !event.defaultPrevented;
+ }
+}
+
+function isSafe(fetchMethod) {
+ return fetchMethodFromString(fetchMethod) == FetchMethod.get;
+}
+
+function buildResourceAndBody(resource, method, requestBody, enctype) {
+ const searchParams = Array.from(requestBody).length > 0 ? new URLSearchParams(entriesExcludingFiles(requestBody)) : resource.searchParams;
+ if (isSafe(method)) {
+ return [ mergeIntoURLSearchParams(resource, searchParams), null ];
+ } else if (enctype == FetchEnctype.urlEncoded) {
+ return [ resource, searchParams ];
+ } else {
+ return [ resource, requestBody ];
+ }
+}
+
+function entriesExcludingFiles(requestBody) {
+ const entries = [];
+ for (const [name, value] of requestBody) {
+ if (value instanceof File) continue; else entries.push([ name, value ]);
+ }
+ return entries;
+}
+
+function mergeIntoURLSearchParams(url, requestBody) {
+ const searchParams = new URLSearchParams(entriesExcludingFiles(requestBody));
+ url.search = searchParams.toString();
+ return url;
+}
+
+class AppearanceObserver {
+ started=false;
+ constructor(delegate, element) {
+ this.delegate = delegate;
+ this.element = element;
+ this.intersectionObserver = new IntersectionObserver(this.intersect);
+ }
+ start() {
+ if (!this.started) {
+ this.started = true;
+ this.intersectionObserver.observe(this.element);
+ }
+ }
+ stop() {
+ if (this.started) {
+ this.started = false;
+ this.intersectionObserver.unobserve(this.element);
+ }
+ }
+ intersect=entries => {
+ const lastEntry = entries.slice(-1)[0];
+ if (lastEntry?.isIntersecting) {
+ this.delegate.elementAppearedInViewport(this.element);
+ }
+ };
+}
+
+class StreamMessage {
+ static contentType="text/vnd.turbo-stream.html";
+ static wrap(message) {
+ if (typeof message == "string") {
+ return new this(createDocumentFragment(message));
+ } else {
+ return message;
+ }
+ }
+ constructor(fragment) {
+ this.fragment = importStreamElements(fragment);
+ }
+}
+
+function importStreamElements(fragment) {
+ for (const element of fragment.querySelectorAll("turbo-stream")) {
+ const streamElement = document.importNode(element, true);
+ for (const inertScriptElement of streamElement.templateElement.content.querySelectorAll("script")) {
+ inertScriptElement.replaceWith(activateScriptElement(inertScriptElement));
+ }
+ element.replaceWith(streamElement);
+ }
+ return fragment;
+}
+
+const PREFETCH_DELAY = 100;
+
+class PrefetchCache {
+ #prefetchTimeout=null;
+ #prefetched=null;
+ get(url) {
+ if (this.#prefetched && this.#prefetched.url === url && this.#prefetched.expire > Date.now()) {
+ return this.#prefetched.request;
+ }
+ }
+ setLater(url, request, ttl) {
+ this.clear();
+ this.#prefetchTimeout = setTimeout((() => {
+ request.perform();
+ this.set(url, request, ttl);
+ this.#prefetchTimeout = null;
+ }), PREFETCH_DELAY);
+ }
+ set(url, request, ttl) {
+ this.#prefetched = {
+ url: url,
+ request: request,
+ expire: new Date((new Date).getTime() + ttl)
+ };
+ }
+ clear() {
+ if (this.#prefetchTimeout) clearTimeout(this.#prefetchTimeout);
+ this.#prefetched = null;
+ }
+}
+
+const cacheTtl = 10 * 1e3;
+
+const prefetchCache = new PrefetchCache;
+
+const FormSubmissionState = {
+ initialized: "initialized",
+ requesting: "requesting",
+ waiting: "waiting",
+ receiving: "receiving",
+ stopping: "stopping",
+ stopped: "stopped"
+};
+
+class FormSubmission {
+ state=FormSubmissionState.initialized;
+ static confirmMethod(message) {
+ return Promise.resolve(confirm(message));
+ }
+ constructor(delegate, formElement, submitter, mustRedirect = false) {
+ const method = getMethod(formElement, submitter);
+ const action = getAction(getFormAction(formElement, submitter), method);
+ const body = buildFormData(formElement, submitter);
+ const enctype = getEnctype(formElement, submitter);
+ this.delegate = delegate;
+ this.formElement = formElement;
+ this.submitter = submitter;
+ this.fetchRequest = new FetchRequest(this, method, action, body, formElement, enctype);
+ this.mustRedirect = mustRedirect;
+ }
+ get method() {
+ return this.fetchRequest.method;
+ }
+ set method(value) {
+ this.fetchRequest.method = value;
+ }
+ get action() {
+ return this.fetchRequest.url.toString();
+ }
+ set action(value) {
+ this.fetchRequest.url = expandURL(value);
+ }
+ get body() {
+ return this.fetchRequest.body;
+ }
+ get enctype() {
+ return this.fetchRequest.enctype;
+ }
+ get isSafe() {
+ return this.fetchRequest.isSafe;
+ }
+ get location() {
+ return this.fetchRequest.url;
+ }
+ async start() {
+ const {initialized: initialized, requesting: requesting} = FormSubmissionState;
+ const confirmationMessage = getAttribute("data-turbo-confirm", this.submitter, this.formElement);
+ if (typeof confirmationMessage === "string") {
+ const confirmMethod = typeof config.forms.confirm === "function" ? config.forms.confirm : FormSubmission.confirmMethod;
+ const answer = await confirmMethod(confirmationMessage, this.formElement, this.submitter);
+ if (!answer) {
+ return;
+ }
+ }
+ if (this.state == initialized) {
+ this.state = requesting;
+ return this.fetchRequest.perform();
+ }
+ }
+ stop() {
+ const {stopping: stopping, stopped: stopped} = FormSubmissionState;
+ if (this.state != stopping && this.state != stopped) {
+ this.state = stopping;
+ this.fetchRequest.cancel();
+ return true;
+ }
+ }
+ prepareRequest(request) {
+ if (!request.isSafe) {
+ const token = getCookieValue(getMetaContent("csrf-param")) || getMetaContent("csrf-token");
+ if (token) {
+ request.headers["X-CSRF-Token"] = token;
+ }
+ }
+ if (this.requestAcceptsTurboStreamResponse(request)) {
+ request.acceptResponseType(StreamMessage.contentType);
+ }
+ }
+ requestStarted(_request) {
+ this.state = FormSubmissionState.waiting;
+ if (this.submitter) config.forms.submitter.beforeSubmit(this.submitter);
+ this.setSubmitsWith();
+ markAsBusy(this.formElement);
+ dispatch("turbo:submit-start", {
+ target: this.formElement,
+ detail: {
+ formSubmission: this
+ }
+ });
+ this.delegate.formSubmissionStarted(this);
+ }
+ requestPreventedHandlingResponse(request, response) {
+ prefetchCache.clear();
+ this.result = {
+ success: response.succeeded,
+ fetchResponse: response
+ };
+ }
+ requestSucceededWithResponse(request, response) {
+ if (response.clientError || response.serverError) {
+ this.delegate.formSubmissionFailedWithResponse(this, response);
+ return;
+ }
+ prefetchCache.clear();
+ if (this.requestMustRedirect(request) && responseSucceededWithoutRedirect(response)) {
+ const error = new Error("Form responses must redirect to another location");
+ this.delegate.formSubmissionErrored(this, error);
+ } else {
+ this.state = FormSubmissionState.receiving;
+ this.result = {
+ success: true,
+ fetchResponse: response
+ };
+ this.delegate.formSubmissionSucceededWithResponse(this, response);
+ }
+ }
+ requestFailedWithResponse(request, response) {
+ this.result = {
+ success: false,
+ fetchResponse: response
+ };
+ this.delegate.formSubmissionFailedWithResponse(this, response);
+ }
+ requestErrored(request, error) {
+ this.result = {
+ success: false,
+ error: error
+ };
+ this.delegate.formSubmissionErrored(this, error);
+ }
+ requestFinished(_request) {
+ this.state = FormSubmissionState.stopped;
+ if (this.submitter) config.forms.submitter.afterSubmit(this.submitter);
+ this.resetSubmitterText();
+ clearBusyState(this.formElement);
+ dispatch("turbo:submit-end", {
+ target: this.formElement,
+ detail: {
+ formSubmission: this,
+ ...this.result
+ }
+ });
+ this.delegate.formSubmissionFinished(this);
+ }
+ setSubmitsWith() {
+ if (!this.submitter || !this.submitsWith) return;
+ if (this.submitter.matches("button")) {
+ this.originalSubmitText = this.submitter.innerHTML;
+ this.submitter.innerHTML = this.submitsWith;
+ } else if (this.submitter.matches("input")) {
+ const input = this.submitter;
+ this.originalSubmitText = input.value;
+ input.value = this.submitsWith;
+ }
+ }
+ resetSubmitterText() {
+ if (!this.submitter || !this.originalSubmitText) return;
+ if (this.submitter.matches("button")) {
+ this.submitter.innerHTML = this.originalSubmitText;
+ } else if (this.submitter.matches("input")) {
+ const input = this.submitter;
+ input.value = this.originalSubmitText;
+ }
+ }
+ requestMustRedirect(request) {
+ return !request.isSafe && this.mustRedirect;
+ }
+ requestAcceptsTurboStreamResponse(request) {
+ return !request.isSafe || hasAttribute("data-turbo-stream", this.submitter, this.formElement);
+ }
+ get submitsWith() {
+ return this.submitter?.getAttribute("data-turbo-submits-with");
+ }
+}
+
+function buildFormData(formElement, submitter) {
+ const formData = new FormData(formElement);
+ const name = submitter?.getAttribute("name");
+ const value = submitter?.getAttribute("value");
+ if (name) {
+ formData.append(name, value || "");
+ }
+ return formData;
+}
+
+function getCookieValue(cookieName) {
+ if (cookieName != null) {
+ const cookies = document.cookie ? document.cookie.split("; ") : [];
+ const cookie = cookies.find((cookie => cookie.startsWith(cookieName)));
+ if (cookie) {
+ const value = cookie.split("=").slice(1).join("=");
+ return value ? decodeURIComponent(value) : undefined;
+ }
+ }
+}
+
+function responseSucceededWithoutRedirect(response) {
+ return response.statusCode == 200 && !response.redirected;
+}
+
+function getFormAction(formElement, submitter) {
+ const formElementAction = typeof formElement.action === "string" ? formElement.action : null;
+ if (submitter?.hasAttribute("formaction")) {
+ return submitter.getAttribute("formaction") || "";
+ } else {
+ return formElement.getAttribute("action") || formElementAction || "";
+ }
+}
+
+function getAction(formAction, fetchMethod) {
+ const action = expandURL(formAction);
+ if (isSafe(fetchMethod)) {
+ action.search = "";
+ }
+ return action;
+}
+
+function getMethod(formElement, submitter) {
+ const method = submitter?.getAttribute("formmethod") || formElement.getAttribute("method") || "";
+ return fetchMethodFromString(method.toLowerCase()) || FetchMethod.get;
+}
+
+function getEnctype(formElement, submitter) {
+ return fetchEnctypeFromString(submitter?.getAttribute("formenctype") || formElement.enctype);
+}
+
+class Snapshot {
+ constructor(element) {
+ this.element = element;
+ }
+ get activeElement() {
+ return this.element.ownerDocument.activeElement;
+ }
+ get children() {
+ return [ ...this.element.children ];
+ }
+ hasAnchor(anchor) {
+ return this.getElementForAnchor(anchor) != null;
+ }
+ getElementForAnchor(anchor) {
+ return anchor ? this.element.querySelector(`[id='${anchor}'], a[name='${anchor}']`) : null;
+ }
+ get isConnected() {
+ return this.element.isConnected;
+ }
+ get firstAutofocusableElement() {
+ return queryAutofocusableElement(this.element);
+ }
+ get permanentElements() {
+ return queryPermanentElementsAll(this.element);
+ }
+ getPermanentElementById(id) {
+ return getPermanentElementById(this.element, id);
+ }
+ getPermanentElementMapForSnapshot(snapshot) {
+ const permanentElementMap = {};
+ for (const currentPermanentElement of this.permanentElements) {
+ const {id: id} = currentPermanentElement;
+ const newPermanentElement = snapshot.getPermanentElementById(id);
+ if (newPermanentElement) {
+ permanentElementMap[id] = [ currentPermanentElement, newPermanentElement ];
+ }
+ }
+ return permanentElementMap;
+ }
+}
+
+function getPermanentElementById(node, id) {
+ return node.querySelector(`#${id}[data-turbo-permanent]`);
+}
+
+function queryPermanentElementsAll(node) {
+ return node.querySelectorAll("[id][data-turbo-permanent]");
+}
+
+class FormSubmitObserver {
+ started=false;
+ constructor(delegate, eventTarget) {
+ this.delegate = delegate;
+ this.eventTarget = eventTarget;
+ }
+ start() {
+ if (!this.started) {
+ this.eventTarget.addEventListener("submit", this.submitCaptured, true);
+ this.started = true;
+ }
+ }
+ stop() {
+ if (this.started) {
+ this.eventTarget.removeEventListener("submit", this.submitCaptured, true);
+ this.started = false;
+ }
+ }
+ submitCaptured=() => {
+ this.eventTarget.removeEventListener("submit", this.submitBubbled, false);
+ this.eventTarget.addEventListener("submit", this.submitBubbled, false);
+ };
+ submitBubbled=event => {
+ if (!event.defaultPrevented) {
+ const form = event.target instanceof HTMLFormElement ? event.target : undefined;
+ const submitter = event.submitter || undefined;
+ if (form && submissionDoesNotDismissDialog(form, submitter) && submissionDoesNotTargetIFrame(form, submitter) && this.delegate.willSubmitForm(form, submitter)) {
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ this.delegate.formSubmitted(form, submitter);
+ }
+ }
+ };
+}
+
+function submissionDoesNotDismissDialog(form, submitter) {
+ const method = submitter?.getAttribute("formmethod") || form.getAttribute("method");
+ return method != "dialog";
+}
+
+function submissionDoesNotTargetIFrame(form, submitter) {
+ const target = submitter?.getAttribute("formtarget") || form.getAttribute("target");
+ return doesNotTargetIFrame(target);
+}
+
+class View {
+ #resolveRenderPromise=_value => {};
+ #resolveInterceptionPromise=_value => {};
+ constructor(delegate, element) {
+ this.delegate = delegate;
+ this.element = element;
+ }
+ scrollToAnchor(anchor) {
+ const element = this.snapshot.getElementForAnchor(anchor);
+ if (element) {
+ this.focusElement(element);
+ this.scrollToElement(element);
+ } else {
+ this.scrollToPosition({
+ x: 0,
+ y: 0
+ });
+ }
+ }
+ scrollToAnchorFromLocation(location) {
+ this.scrollToAnchor(getAnchor(location));
+ }
+ scrollToElement(element) {
+ element.scrollIntoView();
+ }
+ focusElement(element) {
+ if (element instanceof HTMLElement) {
+ if (element.hasAttribute("tabindex")) {
+ element.focus();
+ } else {
+ element.setAttribute("tabindex", "-1");
+ element.focus();
+ element.removeAttribute("tabindex");
+ }
+ }
+ }
+ scrollToPosition({x: x, y: y}) {
+ this.scrollRoot.scrollTo(x, y);
+ }
+ scrollToTop() {
+ this.scrollToPosition({
+ x: 0,
+ y: 0
+ });
+ }
+ get scrollRoot() {
+ return window;
+ }
+ async render(renderer) {
+ const {isPreview: isPreview, shouldRender: shouldRender, willRender: willRender, newSnapshot: snapshot} = renderer;
+ const shouldInvalidate = willRender;
+ if (shouldRender) {
+ try {
+ this.renderPromise = new Promise((resolve => this.#resolveRenderPromise = resolve));
+ this.renderer = renderer;
+ await this.prepareToRenderSnapshot(renderer);
+ const renderInterception = new Promise((resolve => this.#resolveInterceptionPromise = resolve));
+ const options = {
+ resume: this.#resolveInterceptionPromise,
+ render: this.renderer.renderElement,
+ renderMethod: this.renderer.renderMethod
+ };
+ const immediateRender = this.delegate.allowsImmediateRender(snapshot, options);
+ if (!immediateRender) await renderInterception;
+ await this.renderSnapshot(renderer);
+ this.delegate.viewRenderedSnapshot(snapshot, isPreview, this.renderer.renderMethod);
+ this.delegate.preloadOnLoadLinksForView(this.element);
+ this.finishRenderingSnapshot(renderer);
+ } finally {
+ delete this.renderer;
+ this.#resolveRenderPromise(undefined);
+ delete this.renderPromise;
+ }
+ } else if (shouldInvalidate) {
+ this.invalidate(renderer.reloadReason);
+ }
+ }
+ invalidate(reason) {
+ this.delegate.viewInvalidated(reason);
+ }
+ async prepareToRenderSnapshot(renderer) {
+ this.markAsPreview(renderer.isPreview);
+ await renderer.prepareToRender();
+ }
+ markAsPreview(isPreview) {
+ if (isPreview) {
+ this.element.setAttribute("data-turbo-preview", "");
+ } else {
+ this.element.removeAttribute("data-turbo-preview");
+ }
+ }
+ markVisitDirection(direction) {
+ this.element.setAttribute("data-turbo-visit-direction", direction);
+ }
+ unmarkVisitDirection() {
+ this.element.removeAttribute("data-turbo-visit-direction");
+ }
+ async renderSnapshot(renderer) {
+ await renderer.render();
+ }
+ finishRenderingSnapshot(renderer) {
+ renderer.finishRendering();
+ }
+}
+
+class FrameView extends View {
+ missing() {
+ this.element.innerHTML = `Content missing`;
+ }
+ get snapshot() {
+ return new Snapshot(this.element);
+ }
+}
+
+class LinkInterceptor {
+ constructor(delegate, element) {
+ this.delegate = delegate;
+ this.element = element;
+ }
+ start() {
+ this.element.addEventListener("click", this.clickBubbled);
+ document.addEventListener("turbo:click", this.linkClicked);
+ document.addEventListener("turbo:before-visit", this.willVisit);
+ }
+ stop() {
+ this.element.removeEventListener("click", this.clickBubbled);
+ document.removeEventListener("turbo:click", this.linkClicked);
+ document.removeEventListener("turbo:before-visit", this.willVisit);
+ }
+ clickBubbled=event => {
+ if (this.clickEventIsSignificant(event)) {
+ this.clickEvent = event;
+ } else {
+ delete this.clickEvent;
+ }
+ };
+ linkClicked=event => {
+ if (this.clickEvent && this.clickEventIsSignificant(event)) {
+ if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url, event.detail.originalEvent)) {
+ this.clickEvent.preventDefault();
+ event.preventDefault();
+ this.delegate.linkClickIntercepted(event.target, event.detail.url, event.detail.originalEvent);
+ }
+ }
+ delete this.clickEvent;
+ };
+ willVisit=_event => {
+ delete this.clickEvent;
+ };
+ clickEventIsSignificant(event) {
+ const target = event.composed ? event.target?.parentElement : event.target;
+ const element = findLinkFromClickTarget(target) || target;
+ return element instanceof Element && element.closest("turbo-frame, html") == this.element;
+ }
+}
+
+class LinkClickObserver {
+ started=false;
+ constructor(delegate, eventTarget) {
+ this.delegate = delegate;
+ this.eventTarget = eventTarget;
+ }
+ start() {
+ if (!this.started) {
+ this.eventTarget.addEventListener("click", this.clickCaptured, true);
+ this.started = true;
+ }
+ }
+ stop() {
+ if (this.started) {
+ this.eventTarget.removeEventListener("click", this.clickCaptured, true);
+ this.started = false;
+ }
+ }
+ clickCaptured=() => {
+ this.eventTarget.removeEventListener("click", this.clickBubbled, false);
+ this.eventTarget.addEventListener("click", this.clickBubbled, false);
+ };
+ clickBubbled=event => {
+ if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {
+ const target = event.composedPath && event.composedPath()[0] || event.target;
+ const link = findLinkFromClickTarget(target);
+ if (link && doesNotTargetIFrame(link.target)) {
+ const location = getLocationForLink(link);
+ if (this.delegate.willFollowLinkToLocation(link, location, event)) {
+ event.preventDefault();
+ this.delegate.followedLinkToLocation(link, location);
+ }
+ }
+ }
+ };
+ clickEventIsSignificant(event) {
+ return !(event.target && event.target.isContentEditable || event.defaultPrevented || event.which > 1 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey);
+ }
+}
+
+class FormLinkClickObserver {
+ constructor(delegate, element) {
+ this.delegate = delegate;
+ this.linkInterceptor = new LinkClickObserver(this, element);
+ }
+ start() {
+ this.linkInterceptor.start();
+ }
+ stop() {
+ this.linkInterceptor.stop();
+ }
+ canPrefetchRequestToLocation(link, location) {
+ return false;
+ }
+ prefetchAndCacheRequestToLocation(link, location) {
+ return;
+ }
+ willFollowLinkToLocation(link, location, originalEvent) {
+ return this.delegate.willSubmitFormLinkToLocation(link, location, originalEvent) && (link.hasAttribute("data-turbo-method") || link.hasAttribute("data-turbo-stream"));
+ }
+ followedLinkToLocation(link, location) {
+ const form = document.createElement("form");
+ const type = "hidden";
+ for (const [name, value] of location.searchParams) {
+ form.append(Object.assign(document.createElement("input"), {
+ type: type,
+ name: name,
+ value: value
+ }));
+ }
+ const action = Object.assign(location, {
+ search: ""
+ });
+ form.setAttribute("data-turbo", "true");
+ form.setAttribute("action", action.href);
+ form.setAttribute("hidden", "");
+ const method = link.getAttribute("data-turbo-method");
+ if (method) form.setAttribute("method", method);
+ const turboFrame = link.getAttribute("data-turbo-frame");
+ if (turboFrame) form.setAttribute("data-turbo-frame", turboFrame);
+ const turboAction = getVisitAction(link);
+ if (turboAction) form.setAttribute("data-turbo-action", turboAction);
+ const turboConfirm = link.getAttribute("data-turbo-confirm");
+ if (turboConfirm) form.setAttribute("data-turbo-confirm", turboConfirm);
+ const turboStream = link.hasAttribute("data-turbo-stream");
+ if (turboStream) form.setAttribute("data-turbo-stream", "");
+ this.delegate.submittedFormLinkToLocation(link, location, form);
+ document.body.appendChild(form);
+ form.addEventListener("turbo:submit-end", (() => form.remove()), {
+ once: true
+ });
+ requestAnimationFrame((() => form.requestSubmit()));
+ }
+}
+
+class Bardo {
+ static async preservingPermanentElements(delegate, permanentElementMap, callback) {
+ const bardo = new this(delegate, permanentElementMap);
+ bardo.enter();
+ await callback();
+ bardo.leave();
+ }
+ constructor(delegate, permanentElementMap) {
+ this.delegate = delegate;
+ this.permanentElementMap = permanentElementMap;
+ }
+ enter() {
+ for (const id in this.permanentElementMap) {
+ const [currentPermanentElement, newPermanentElement] = this.permanentElementMap[id];
+ this.delegate.enteringBardo(currentPermanentElement, newPermanentElement);
+ this.replaceNewPermanentElementWithPlaceholder(newPermanentElement);
+ }
+ }
+ leave() {
+ for (const id in this.permanentElementMap) {
+ const [currentPermanentElement] = this.permanentElementMap[id];
+ this.replaceCurrentPermanentElementWithClone(currentPermanentElement);
+ this.replacePlaceholderWithPermanentElement(currentPermanentElement);
+ this.delegate.leavingBardo(currentPermanentElement);
+ }
+ }
+ replaceNewPermanentElementWithPlaceholder(permanentElement) {
+ const placeholder = createPlaceholderForPermanentElement(permanentElement);
+ permanentElement.replaceWith(placeholder);
+ }
+ replaceCurrentPermanentElementWithClone(permanentElement) {
+ const clone = permanentElement.cloneNode(true);
+ permanentElement.replaceWith(clone);
+ }
+ replacePlaceholderWithPermanentElement(permanentElement) {
+ const placeholder = this.getPlaceholderById(permanentElement.id);
+ placeholder?.replaceWith(permanentElement);
+ }
+ getPlaceholderById(id) {
+ return this.placeholders.find((element => element.content == id));
+ }
+ get placeholders() {
+ return [ ...document.querySelectorAll("meta[name=turbo-permanent-placeholder][content]") ];
+ }
+}
+
+function createPlaceholderForPermanentElement(permanentElement) {
+ const element = document.createElement("meta");
+ element.setAttribute("name", "turbo-permanent-placeholder");
+ element.setAttribute("content", permanentElement.id);
+ return element;
+}
+
+class Renderer {
+ #activeElement=null;
+ static renderElement(currentElement, newElement) {}
+ constructor(currentSnapshot, newSnapshot, isPreview, willRender = true) {
+ this.currentSnapshot = currentSnapshot;
+ this.newSnapshot = newSnapshot;
+ this.isPreview = isPreview;
+ this.willRender = willRender;
+ this.renderElement = this.constructor.renderElement;
+ this.promise = new Promise(((resolve, reject) => this.resolvingFunctions = {
+ resolve: resolve,
+ reject: reject
+ }));
+ }
+ get shouldRender() {
+ return true;
+ }
+ get shouldAutofocus() {
+ return true;
+ }
+ get reloadReason() {
+ return;
+ }
+ prepareToRender() {
+ return;
+ }
+ render() {}
+ finishRendering() {
+ if (this.resolvingFunctions) {
+ this.resolvingFunctions.resolve();
+ delete this.resolvingFunctions;
+ }
+ }
+ async preservingPermanentElements(callback) {
+ await Bardo.preservingPermanentElements(this, this.permanentElementMap, callback);
+ }
+ focusFirstAutofocusableElement() {
+ if (this.shouldAutofocus) {
+ const element = this.connectedSnapshot.firstAutofocusableElement;
+ if (element) {
+ element.focus();
+ }
+ }
+ }
+ enteringBardo(currentPermanentElement) {
+ if (this.#activeElement) return;
+ if (currentPermanentElement.contains(this.currentSnapshot.activeElement)) {
+ this.#activeElement = this.currentSnapshot.activeElement;
+ }
+ }
+ leavingBardo(currentPermanentElement) {
+ if (currentPermanentElement.contains(this.#activeElement) && this.#activeElement instanceof HTMLElement) {
+ this.#activeElement.focus();
+ this.#activeElement = null;
+ }
+ }
+ get connectedSnapshot() {
+ return this.newSnapshot.isConnected ? this.newSnapshot : this.currentSnapshot;
+ }
+ get currentElement() {
+ return this.currentSnapshot.element;
+ }
+ get newElement() {
+ return this.newSnapshot.element;
+ }
+ get permanentElementMap() {
+ return this.currentSnapshot.getPermanentElementMapForSnapshot(this.newSnapshot);
+ }
+ get renderMethod() {
+ return "replace";
+ }
+}
+
+class FrameRenderer extends Renderer {
+ static renderElement(currentElement, newElement) {
+ const destinationRange = document.createRange();
+ destinationRange.selectNodeContents(currentElement);
+ destinationRange.deleteContents();
+ const frameElement = newElement;
+ const sourceRange = frameElement.ownerDocument?.createRange();
+ if (sourceRange) {
+ sourceRange.selectNodeContents(frameElement);
+ currentElement.appendChild(sourceRange.extractContents());
+ }
+ }
+ constructor(delegate, currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) {
+ super(currentSnapshot, newSnapshot, renderElement, isPreview, willRender);
+ this.delegate = delegate;
+ }
+ get shouldRender() {
+ return true;
+ }
+ async render() {
+ await nextRepaint();
+ this.preservingPermanentElements((() => {
+ this.loadFrameElement();
+ }));
+ this.scrollFrameIntoView();
+ await nextRepaint();
+ this.focusFirstAutofocusableElement();
+ await nextRepaint();
+ this.activateScriptElements();
+ }
+ loadFrameElement() {
+ this.delegate.willRenderFrame(this.currentElement, this.newElement);
+ this.renderElement(this.currentElement, this.newElement);
+ }
+ scrollFrameIntoView() {
+ if (this.currentElement.autoscroll || this.newElement.autoscroll) {
+ const element = this.currentElement.firstElementChild;
+ const block = readScrollLogicalPosition(this.currentElement.getAttribute("data-autoscroll-block"), "end");
+ const behavior = readScrollBehavior(this.currentElement.getAttribute("data-autoscroll-behavior"), "auto");
+ if (element) {
+ element.scrollIntoView({
+ block: block,
+ behavior: behavior
+ });
+ return true;
+ }
+ }
+ return false;
+ }
+ activateScriptElements() {
+ for (const inertScriptElement of this.newScriptElements) {
+ const activatedScriptElement = activateScriptElement(inertScriptElement);
+ inertScriptElement.replaceWith(activatedScriptElement);
+ }
+ }
+ get newScriptElements() {
+ return this.currentElement.querySelectorAll("script");
+ }
+}
+
+function readScrollLogicalPosition(value, defaultValue) {
+ if (value == "end" || value == "start" || value == "center" || value == "nearest") {
+ return value;
+ } else {
+ return defaultValue;
+ }
+}
+
+function readScrollBehavior(value, defaultValue) {
+ if (value == "auto" || value == "smooth") {
+ return value;
+ } else {
+ return defaultValue;
+ }
+}
+
+var Idiomorph = function() {
+ const noOp = () => {};
+ const defaults = {
+ morphStyle: "outerHTML",
+ callbacks: {
+ beforeNodeAdded: noOp,
+ afterNodeAdded: noOp,
+ beforeNodeMorphed: noOp,
+ afterNodeMorphed: noOp,
+ beforeNodeRemoved: noOp,
+ afterNodeRemoved: noOp,
+ beforeAttributeUpdated: noOp
+ },
+ head: {
+ style: "merge",
+ shouldPreserve: elt => elt.getAttribute("im-preserve") === "true",
+ shouldReAppend: elt => elt.getAttribute("im-re-append") === "true",
+ shouldRemove: noOp,
+ afterHeadMorphed: noOp
+ },
+ restoreFocus: true
+ };
+ function morph(oldNode, newContent, config = {}) {
+ oldNode = normalizeElement(oldNode);
+ const newNode = normalizeParent(newContent);
+ const ctx = createMorphContext(oldNode, newNode, config);
+ const morphedNodes = saveAndRestoreFocus(ctx, (() => withHeadBlocking(ctx, oldNode, newNode, (ctx => {
+ if (ctx.morphStyle === "innerHTML") {
+ morphChildren(ctx, oldNode, newNode);
+ return Array.from(oldNode.childNodes);
+ } else {
+ return morphOuterHTML(ctx, oldNode, newNode);
+ }
+ }))));
+ ctx.pantry.remove();
+ return morphedNodes;
+ }
+ function morphOuterHTML(ctx, oldNode, newNode) {
+ const oldParent = normalizeParent(oldNode);
+ let childNodes = Array.from(oldParent.childNodes);
+ const index = childNodes.indexOf(oldNode);
+ const rightMargin = childNodes.length - (index + 1);
+ morphChildren(ctx, oldParent, newNode, oldNode, oldNode.nextSibling);
+ childNodes = Array.from(oldParent.childNodes);
+ return childNodes.slice(index, childNodes.length - rightMargin);
+ }
+ function saveAndRestoreFocus(ctx, fn) {
+ if (!ctx.config.restoreFocus) return fn();
+ let activeElement = document.activeElement;
+ if (!(activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement)) {
+ return fn();
+ }
+ const {id: activeElementId, selectionStart: selectionStart, selectionEnd: selectionEnd} = activeElement;
+ const results = fn();
+ if (activeElementId && activeElementId !== document.activeElement?.id) {
+ activeElement = ctx.target.querySelector(`#${activeElementId}`);
+ activeElement?.focus();
+ }
+ if (activeElement && !activeElement.selectionEnd && selectionEnd) {
+ activeElement.setSelectionRange(selectionStart, selectionEnd);
+ }
+ return results;
+ }
+ const morphChildren = function() {
+ function morphChildren(ctx, oldParent, newParent, insertionPoint = null, endPoint = null) {
+ if (oldParent instanceof HTMLTemplateElement && newParent instanceof HTMLTemplateElement) {
+ oldParent = oldParent.content;
+ newParent = newParent.content;
+ }
+ insertionPoint ||= oldParent.firstChild;
+ for (const newChild of newParent.childNodes) {
+ if (insertionPoint && insertionPoint != endPoint) {
+ const bestMatch = findBestMatch(ctx, newChild, insertionPoint, endPoint);
+ if (bestMatch) {
+ if (bestMatch !== insertionPoint) {
+ removeNodesBetween(ctx, insertionPoint, bestMatch);
+ }
+ morphNode(bestMatch, newChild, ctx);
+ insertionPoint = bestMatch.nextSibling;
+ continue;
+ }
+ }
+ if (newChild instanceof Element && ctx.persistentIds.has(newChild.id)) {
+ const movedChild = moveBeforeById(oldParent, newChild.id, insertionPoint, ctx);
+ morphNode(movedChild, newChild, ctx);
+ insertionPoint = movedChild.nextSibling;
+ continue;
+ }
+ const insertedNode = createNode(oldParent, newChild, insertionPoint, ctx);
+ if (insertedNode) {
+ insertionPoint = insertedNode.nextSibling;
+ }
+ }
+ while (insertionPoint && insertionPoint != endPoint) {
+ const tempNode = insertionPoint;
+ insertionPoint = insertionPoint.nextSibling;
+ removeNode(ctx, tempNode);
+ }
+ }
+ function createNode(oldParent, newChild, insertionPoint, ctx) {
+ if (ctx.callbacks.beforeNodeAdded(newChild) === false) return null;
+ if (ctx.idMap.has(newChild)) {
+ const newEmptyChild = document.createElement(newChild.tagName);
+ oldParent.insertBefore(newEmptyChild, insertionPoint);
+ morphNode(newEmptyChild, newChild, ctx);
+ ctx.callbacks.afterNodeAdded(newEmptyChild);
+ return newEmptyChild;
+ } else {
+ const newClonedChild = document.importNode(newChild, true);
+ oldParent.insertBefore(newClonedChild, insertionPoint);
+ ctx.callbacks.afterNodeAdded(newClonedChild);
+ return newClonedChild;
+ }
+ }
+ const findBestMatch = function() {
+ function findBestMatch(ctx, node, startPoint, endPoint) {
+ let softMatch = null;
+ let nextSibling = node.nextSibling;
+ let siblingSoftMatchCount = 0;
+ let cursor = startPoint;
+ while (cursor && cursor != endPoint) {
+ if (isSoftMatch(cursor, node)) {
+ if (isIdSetMatch(ctx, cursor, node)) {
+ return cursor;
+ }
+ if (softMatch === null) {
+ if (!ctx.idMap.has(cursor)) {
+ softMatch = cursor;
+ }
+ }
+ }
+ if (softMatch === null && nextSibling && isSoftMatch(cursor, nextSibling)) {
+ siblingSoftMatchCount++;
+ nextSibling = nextSibling.nextSibling;
+ if (siblingSoftMatchCount >= 2) {
+ softMatch = undefined;
+ }
+ }
+ if (cursor.contains(document.activeElement)) break;
+ cursor = cursor.nextSibling;
+ }
+ return softMatch || null;
+ }
+ function isIdSetMatch(ctx, oldNode, newNode) {
+ let oldSet = ctx.idMap.get(oldNode);
+ let newSet = ctx.idMap.get(newNode);
+ if (!newSet || !oldSet) return false;
+ for (const id of oldSet) {
+ if (newSet.has(id)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ function isSoftMatch(oldNode, newNode) {
+ const oldElt = oldNode;
+ const newElt = newNode;
+ return oldElt.nodeType === newElt.nodeType && oldElt.tagName === newElt.tagName && (!oldElt.id || oldElt.id === newElt.id);
+ }
+ return findBestMatch;
+ }();
+ function removeNode(ctx, node) {
+ if (ctx.idMap.has(node)) {
+ if (ctx.callbacks.beforeNodeRemoved(node) === false) return;
+ moveBefore(ctx.pantry, node, null);
+ } else {
+ if (ctx.callbacks.beforeNodeRemoved(node) === false) return;
+ node.parentNode?.removeChild(node);
+ ctx.callbacks.afterNodeRemoved(node);
+ }
+ }
+ function removeNodesBetween(ctx, startInclusive, endExclusive) {
+ let cursor = startInclusive;
+ while (cursor && cursor !== endExclusive) {
+ let tempNode = cursor;
+ cursor = cursor.nextSibling;
+ removeNode(ctx, tempNode);
+ }
+ return cursor;
+ }
+ function moveBeforeById(parentNode, id, after, ctx) {
+ const target = ctx.target.querySelector(`#${id}`) || ctx.pantry.querySelector(`#${id}`);
+ removeElementFromAncestorsIdMaps(target, ctx);
+ moveBefore(parentNode, target, after);
+ return target;
+ }
+ function removeElementFromAncestorsIdMaps(element, ctx) {
+ const id = element.id;
+ while (element = element.parentNode) {
+ let idSet = ctx.idMap.get(element);
+ if (idSet) {
+ idSet.delete(id);
+ if (!idSet.size) {
+ ctx.idMap.delete(element);
+ }
+ }
+ }
+ }
+ function moveBefore(parentNode, element, after) {
+ if (parentNode.moveBefore) {
+ try {
+ parentNode.moveBefore(element, after);
+ } catch (e) {
+ parentNode.insertBefore(element, after);
+ }
+ } else {
+ parentNode.insertBefore(element, after);
+ }
+ }
+ return morphChildren;
+ }();
+ const morphNode = function() {
+ function morphNode(oldNode, newContent, ctx) {
+ if (ctx.ignoreActive && oldNode === document.activeElement) {
+ return null;
+ }
+ if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) {
+ return oldNode;
+ }
+ if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) ; else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") {
+ handleHeadElement(oldNode, newContent, ctx);
+ } else {
+ morphAttributes(oldNode, newContent, ctx);
+ if (!ignoreValueOfActiveElement(oldNode, ctx)) {
+ morphChildren(ctx, oldNode, newContent);
+ }
+ }
+ ctx.callbacks.afterNodeMorphed(oldNode, newContent);
+ return oldNode;
+ }
+ function morphAttributes(oldNode, newNode, ctx) {
+ let type = newNode.nodeType;
+ if (type === 1) {
+ const oldElt = oldNode;
+ const newElt = newNode;
+ const oldAttributes = oldElt.attributes;
+ const newAttributes = newElt.attributes;
+ for (const newAttribute of newAttributes) {
+ if (ignoreAttribute(newAttribute.name, oldElt, "update", ctx)) {
+ continue;
+ }
+ if (oldElt.getAttribute(newAttribute.name) !== newAttribute.value) {
+ oldElt.setAttribute(newAttribute.name, newAttribute.value);
+ }
+ }
+ for (let i = oldAttributes.length - 1; 0 <= i; i--) {
+ const oldAttribute = oldAttributes[i];
+ if (!oldAttribute) continue;
+ if (!newElt.hasAttribute(oldAttribute.name)) {
+ if (ignoreAttribute(oldAttribute.name, oldElt, "remove", ctx)) {
+ continue;
+ }
+ oldElt.removeAttribute(oldAttribute.name);
+ }
+ }
+ if (!ignoreValueOfActiveElement(oldElt, ctx)) {
+ syncInputValue(oldElt, newElt, ctx);
+ }
+ }
+ if (type === 8 || type === 3) {
+ if (oldNode.nodeValue !== newNode.nodeValue) {
+ oldNode.nodeValue = newNode.nodeValue;
+ }
+ }
+ }
+ function syncInputValue(oldElement, newElement, ctx) {
+ if (oldElement instanceof HTMLInputElement && newElement instanceof HTMLInputElement && newElement.type !== "file") {
+ let newValue = newElement.value;
+ let oldValue = oldElement.value;
+ syncBooleanAttribute(oldElement, newElement, "checked", ctx);
+ syncBooleanAttribute(oldElement, newElement, "disabled", ctx);
+ if (!newElement.hasAttribute("value")) {
+ if (!ignoreAttribute("value", oldElement, "remove", ctx)) {
+ oldElement.value = "";
+ oldElement.removeAttribute("value");
+ }
+ } else if (oldValue !== newValue) {
+ if (!ignoreAttribute("value", oldElement, "update", ctx)) {
+ oldElement.setAttribute("value", newValue);
+ oldElement.value = newValue;
+ }
+ }
+ } else if (oldElement instanceof HTMLOptionElement && newElement instanceof HTMLOptionElement) {
+ syncBooleanAttribute(oldElement, newElement, "selected", ctx);
+ } else if (oldElement instanceof HTMLTextAreaElement && newElement instanceof HTMLTextAreaElement) {
+ let newValue = newElement.value;
+ let oldValue = oldElement.value;
+ if (ignoreAttribute("value", oldElement, "update", ctx)) {
+ return;
+ }
+ if (newValue !== oldValue) {
+ oldElement.value = newValue;
+ }
+ if (oldElement.firstChild && oldElement.firstChild.nodeValue !== newValue) {
+ oldElement.firstChild.nodeValue = newValue;
+ }
+ }
+ }
+ function syncBooleanAttribute(oldElement, newElement, attributeName, ctx) {
+ const newLiveValue = newElement[attributeName], oldLiveValue = oldElement[attributeName];
+ if (newLiveValue !== oldLiveValue) {
+ const ignoreUpdate = ignoreAttribute(attributeName, oldElement, "update", ctx);
+ if (!ignoreUpdate) {
+ oldElement[attributeName] = newElement[attributeName];
+ }
+ if (newLiveValue) {
+ if (!ignoreUpdate) {
+ oldElement.setAttribute(attributeName, "");
+ }
+ } else {
+ if (!ignoreAttribute(attributeName, oldElement, "remove", ctx)) {
+ oldElement.removeAttribute(attributeName);
+ }
+ }
+ }
+ }
+ function ignoreAttribute(attr, element, updateType, ctx) {
+ if (attr === "value" && ctx.ignoreActiveValue && element === document.activeElement) {
+ return true;
+ }
+ return ctx.callbacks.beforeAttributeUpdated(attr, element, updateType) === false;
+ }
+ function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
+ return !!ctx.ignoreActiveValue && possibleActiveElement === document.activeElement && possibleActiveElement !== document.body;
+ }
+ return morphNode;
+ }();
+ function withHeadBlocking(ctx, oldNode, newNode, callback) {
+ if (ctx.head.block) {
+ const oldHead = oldNode.querySelector("head");
+ const newHead = newNode.querySelector("head");
+ if (oldHead && newHead) {
+ const promises = handleHeadElement(oldHead, newHead, ctx);
+ return Promise.all(promises).then((() => {
+ const newCtx = Object.assign(ctx, {
+ head: {
+ block: false,
+ ignore: true
+ }
+ });
+ return callback(newCtx);
+ }));
+ }
+ }
+ return callback(ctx);
+ }
+ function handleHeadElement(oldHead, newHead, ctx) {
+ let added = [];
+ let removed = [];
+ let preserved = [];
+ let nodesToAppend = [];
+ let srcToNewHeadNodes = new Map;
+ for (const newHeadChild of newHead.children) {
+ srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
+ }
+ for (const currentHeadElt of oldHead.children) {
+ let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
+ let isReAppended = ctx.head.shouldReAppend(currentHeadElt);
+ let isPreserved = ctx.head.shouldPreserve(currentHeadElt);
+ if (inNewContent || isPreserved) {
+ if (isReAppended) {
+ removed.push(currentHeadElt);
+ } else {
+ srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
+ preserved.push(currentHeadElt);
+ }
+ } else {
+ if (ctx.head.style === "append") {
+ if (isReAppended) {
+ removed.push(currentHeadElt);
+ nodesToAppend.push(currentHeadElt);
+ }
+ } else {
+ if (ctx.head.shouldRemove(currentHeadElt) !== false) {
+ removed.push(currentHeadElt);
+ }
+ }
+ }
+ }
+ nodesToAppend.push(...srcToNewHeadNodes.values());
+ let promises = [];
+ for (const newNode of nodesToAppend) {
+ let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild;
+ if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
+ if ("href" in newElt && newElt.href || "src" in newElt && newElt.src) {
+ let resolve;
+ let promise = new Promise((function(_resolve) {
+ resolve = _resolve;
+ }));
+ newElt.addEventListener("load", (function() {
+ resolve();
+ }));
+ promises.push(promise);
+ }
+ oldHead.appendChild(newElt);
+ ctx.callbacks.afterNodeAdded(newElt);
+ added.push(newElt);
+ }
+ }
+ for (const removedElement of removed) {
+ if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
+ oldHead.removeChild(removedElement);
+ ctx.callbacks.afterNodeRemoved(removedElement);
+ }
+ }
+ ctx.head.afterHeadMorphed(oldHead, {
+ added: added,
+ kept: preserved,
+ removed: removed
+ });
+ return promises;
+ }
+ const createMorphContext = function() {
+ function createMorphContext(oldNode, newContent, config) {
+ const {persistentIds: persistentIds, idMap: idMap} = createIdMaps(oldNode, newContent);
+ const mergedConfig = mergeDefaults(config);
+ const morphStyle = mergedConfig.morphStyle || "outerHTML";
+ if (![ "innerHTML", "outerHTML" ].includes(morphStyle)) {
+ throw `Do not understand how to morph style ${morphStyle}`;
+ }
+ return {
+ target: oldNode,
+ newContent: newContent,
+ config: mergedConfig,
+ morphStyle: morphStyle,
+ ignoreActive: mergedConfig.ignoreActive,
+ ignoreActiveValue: mergedConfig.ignoreActiveValue,
+ restoreFocus: mergedConfig.restoreFocus,
+ idMap: idMap,
+ persistentIds: persistentIds,
+ pantry: createPantry(),
+ callbacks: mergedConfig.callbacks,
+ head: mergedConfig.head
+ };
+ }
+ function mergeDefaults(config) {
+ let finalConfig = Object.assign({}, defaults);
+ Object.assign(finalConfig, config);
+ finalConfig.callbacks = Object.assign({}, defaults.callbacks, config.callbacks);
+ finalConfig.head = Object.assign({}, defaults.head, config.head);
+ return finalConfig;
+ }
+ function createPantry() {
+ const pantry = document.createElement("div");
+ pantry.hidden = true;
+ document.body.insertAdjacentElement("afterend", pantry);
+ return pantry;
+ }
+ function findIdElements(root) {
+ let elements = Array.from(root.querySelectorAll("[id]"));
+ if (root.id) {
+ elements.push(root);
+ }
+ return elements;
+ }
+ function populateIdMapWithTree(idMap, persistentIds, root, elements) {
+ for (const elt of elements) {
+ if (persistentIds.has(elt.id)) {
+ let current = elt;
+ while (current) {
+ let idSet = idMap.get(current);
+ if (idSet == null) {
+ idSet = new Set;
+ idMap.set(current, idSet);
+ }
+ idSet.add(elt.id);
+ if (current === root) break;
+ current = current.parentElement;
+ }
+ }
+ }
+ }
+ function createIdMaps(oldContent, newContent) {
+ const oldIdElements = findIdElements(oldContent);
+ const newIdElements = findIdElements(newContent);
+ const persistentIds = createPersistentIds(oldIdElements, newIdElements);
+ let idMap = new Map;
+ populateIdMapWithTree(idMap, persistentIds, oldContent, oldIdElements);
+ const newRoot = newContent.__idiomorphRoot || newContent;
+ populateIdMapWithTree(idMap, persistentIds, newRoot, newIdElements);
+ return {
+ persistentIds: persistentIds,
+ idMap: idMap
+ };
+ }
+ function createPersistentIds(oldIdElements, newIdElements) {
+ let duplicateIds = new Set;
+ let oldIdTagNameMap = new Map;
+ for (const {id: id, tagName: tagName} of oldIdElements) {
+ if (oldIdTagNameMap.has(id)) {
+ duplicateIds.add(id);
+ } else {
+ oldIdTagNameMap.set(id, tagName);
+ }
+ }
+ let persistentIds = new Set;
+ for (const {id: id, tagName: tagName} of newIdElements) {
+ if (persistentIds.has(id)) {
+ duplicateIds.add(id);
+ } else if (oldIdTagNameMap.get(id) === tagName) {
+ persistentIds.add(id);
+ }
+ }
+ for (const id of duplicateIds) {
+ persistentIds.delete(id);
+ }
+ return persistentIds;
+ }
+ return createMorphContext;
+ }();
+ const {normalizeElement: normalizeElement, normalizeParent: normalizeParent} = function() {
+ const generatedByIdiomorph = new WeakSet;
+ function normalizeElement(content) {
+ if (content instanceof Document) {
+ return content.documentElement;
+ } else {
+ return content;
+ }
+ }
+ function normalizeParent(newContent) {
+ if (newContent == null) {
+ return document.createElement("div");
+ } else if (typeof newContent === "string") {
+ return normalizeParent(parseContent(newContent));
+ } else if (generatedByIdiomorph.has(newContent)) {
+ return newContent;
+ } else if (newContent instanceof Node) {
+ if (newContent.parentNode) {
+ return createDuckTypedParent(newContent);
+ } else {
+ const dummyParent = document.createElement("div");
+ dummyParent.append(newContent);
+ return dummyParent;
+ }
+ } else {
+ const dummyParent = document.createElement("div");
+ for (const elt of [ ...newContent ]) {
+ dummyParent.append(elt);
+ }
+ return dummyParent;
+ }
+ }
+ function createDuckTypedParent(newContent) {
+ return {
+ childNodes: [ newContent ],
+ querySelectorAll: s => {
+ const elements = newContent.querySelectorAll(s);
+ return newContent.matches(s) ? [ newContent, ...elements ] : elements;
+ },
+ insertBefore: (n, r) => newContent.parentNode.insertBefore(n, r),
+ moveBefore: (n, r) => newContent.parentNode.moveBefore(n, r),
+ get __idiomorphRoot() {
+ return newContent;
+ }
+ };
+ }
+ function parseContent(newContent) {
+ let parser = new DOMParser;
+ let contentWithSvgsRemoved = newContent.replace(/