diff --git a/Gemfile.lock b/Gemfile.lock
index 137d07264..57a5571a6 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -565,7 +565,7 @@ GEM
tiktoken_ruby (0.0.12-x86_64-linux-musl)
timeout (0.4.3)
tsort (0.2.0)
- turbo-rails (2.0.17)
+ turbo-rails (2.0.20)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
tzinfo (2.0.6)
diff --git a/config/importmap.rb b/config/importmap.rb
index c1445db74..f1da52737 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.patched.js"
+pin "@hotwired/turbo-rails", to: "turbo.min.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
deleted file mode 100644
index bcc9fcdcf..000000000
--- a/vendor/javascript/turbo.patched.js
+++ /dev/null
@@ -1,6050 +0,0 @@
-/*!
-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(/