/*! Turbo 8.0.12 Copyright © 2024 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) { return findClosestRecursively(target, "a[href]:not([target^=_]):not([download])"); } 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 = getPrefix(url); return baseURL.href === expandURL(prefix).href || 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 getPrefix(url) { return addTrailingSlash(url.origin + url.pathname); } 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); const nativeFetch = window.fetch; function fetchWithTurboHeaders(url, options = {}) { const modifiedHeaders = new Headers(options.headers || {}); const requestUID = uuid(); recentRequests.add(requestUID); modifiedHeaders.append("X-Turbo-Request-Id", requestUID); return nativeFetch(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.scrollToElement(element); this.focusElement(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() { let EMPTY_SET = new Set; let defaults = { morphStyle: "outerHTML", callbacks: { beforeNodeAdded: noOp, afterNodeAdded: noOp, beforeNodeMorphed: noOp, afterNodeMorphed: noOp, beforeNodeRemoved: noOp, afterNodeRemoved: noOp, beforeAttributeUpdated: noOp }, head: { style: "merge", shouldPreserve: function(elt) { return elt.getAttribute("im-preserve") === "true"; }, shouldReAppend: function(elt) { return elt.getAttribute("im-re-append") === "true"; }, shouldRemove: noOp, afterHeadMorphed: noOp } }; function morph(oldNode, newContent, config = {}) { if (oldNode instanceof Document) { oldNode = oldNode.documentElement; } if (typeof newContent === "string") { newContent = parseContent(newContent); } let normalizedContent = normalizeContent(newContent); let ctx = createMorphContext(oldNode, normalizedContent, config); return morphNormalizedContent(oldNode, normalizedContent, ctx); } function morphNormalizedContent(oldNode, normalizedNewContent, ctx) { if (ctx.head.block) { let oldHead = oldNode.querySelector("head"); let newHead = normalizedNewContent.querySelector("head"); if (oldHead && newHead) { let promises = handleHeadElement(newHead, oldHead, ctx); Promise.all(promises).then((function() { morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, { head: { block: false, ignore: true } })); })); return; } } if (ctx.morphStyle === "innerHTML") { morphChildren(normalizedNewContent, oldNode, ctx); return oldNode.children; } else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) { let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx); let previousSibling = bestMatch?.previousSibling; let nextSibling = bestMatch?.nextSibling; let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx); if (bestMatch) { return insertSiblings(previousSibling, morphedNode, nextSibling); } else { return []; } } else { throw "Do not understand how to morph style " + ctx.morphStyle; } } function ignoreValueOfActiveElement(possibleActiveElement, ctx) { return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement && possibleActiveElement !== document.body; } function morphOldNodeTo(oldNode, newContent, ctx) { if (ctx.ignoreActive && oldNode === document.activeElement) ; else if (newContent == null) { if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode; oldNode.remove(); ctx.callbacks.afterNodeRemoved(oldNode); return null; } else if (!isSoftMatch(oldNode, newContent)) { if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode; if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode; oldNode.parentElement.replaceChild(newContent, oldNode); ctx.callbacks.afterNodeAdded(newContent); ctx.callbacks.afterNodeRemoved(oldNode); return newContent; } else { 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(newContent, oldNode, ctx); } else { syncNodeFrom(newContent, oldNode, ctx); if (!ignoreValueOfActiveElement(oldNode, ctx)) { morphChildren(newContent, oldNode, ctx); } } ctx.callbacks.afterNodeMorphed(oldNode, newContent); return oldNode; } } function morphChildren(newParent, oldParent, ctx) { let nextNewChild = newParent.firstChild; let insertionPoint = oldParent.firstChild; let newChild; while (nextNewChild) { newChild = nextNewChild; nextNewChild = newChild.nextSibling; if (insertionPoint == null) { if (ctx.callbacks.beforeNodeAdded(newChild) === false) return; oldParent.appendChild(newChild); ctx.callbacks.afterNodeAdded(newChild); removeIdsFromConsideration(ctx, newChild); continue; } if (isIdSetMatch(newChild, insertionPoint, ctx)) { morphOldNodeTo(insertionPoint, newChild, ctx); insertionPoint = insertionPoint.nextSibling; removeIdsFromConsideration(ctx, newChild); continue; } let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx); if (idSetMatch) { insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx); morphOldNodeTo(idSetMatch, newChild, ctx); removeIdsFromConsideration(ctx, newChild); continue; } let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx); if (softMatch) { insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx); morphOldNodeTo(softMatch, newChild, ctx); removeIdsFromConsideration(ctx, newChild); continue; } if (ctx.callbacks.beforeNodeAdded(newChild) === false) return; oldParent.insertBefore(newChild, insertionPoint); ctx.callbacks.afterNodeAdded(newChild); removeIdsFromConsideration(ctx, newChild); } while (insertionPoint !== null) { let tempNode = insertionPoint; insertionPoint = insertionPoint.nextSibling; removeNode(tempNode, ctx); } } function ignoreAttribute(attr, to, updateType, ctx) { if (attr === "value" && ctx.ignoreActiveValue && to === document.activeElement) { return true; } return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false; } function syncNodeFrom(from, to, ctx) { let type = from.nodeType; if (type === 1) { const fromAttributes = from.attributes; const toAttributes = to.attributes; for (const fromAttribute of fromAttributes) { if (ignoreAttribute(fromAttribute.name, to, "update", ctx)) { continue; } if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) { to.setAttribute(fromAttribute.name, fromAttribute.value); } } for (let i = toAttributes.length - 1; 0 <= i; i--) { const toAttribute = toAttributes[i]; if (ignoreAttribute(toAttribute.name, to, "remove", ctx)) { continue; } if (!from.hasAttribute(toAttribute.name)) { to.removeAttribute(toAttribute.name); } } } if (type === 8 || type === 3) { if (to.nodeValue !== from.nodeValue) { to.nodeValue = from.nodeValue; } } if (!ignoreValueOfActiveElement(to, ctx)) { syncInputValue(from, to, ctx); } } function syncBooleanAttribute(from, to, attributeName, ctx) { if (from[attributeName] !== to[attributeName]) { let ignoreUpdate = ignoreAttribute(attributeName, to, "update", ctx); if (!ignoreUpdate) { to[attributeName] = from[attributeName]; } if (from[attributeName]) { if (!ignoreUpdate) { to.setAttribute(attributeName, from[attributeName]); } } else { if (!ignoreAttribute(attributeName, to, "remove", ctx)) { to.removeAttribute(attributeName); } } } } function syncInputValue(from, to, ctx) { if (from instanceof HTMLInputElement && to instanceof HTMLInputElement && from.type !== "file") { let fromValue = from.value; let toValue = to.value; syncBooleanAttribute(from, to, "checked", ctx); syncBooleanAttribute(from, to, "disabled", ctx); if (!from.hasAttribute("value")) { if (!ignoreAttribute("value", to, "remove", ctx)) { to.value = ""; to.removeAttribute("value"); } } else if (fromValue !== toValue) { if (!ignoreAttribute("value", to, "update", ctx)) { to.setAttribute("value", fromValue); to.value = fromValue; } } } else if (from instanceof HTMLOptionElement) { syncBooleanAttribute(from, to, "selected", ctx); } else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) { let fromValue = from.value; let toValue = to.value; if (ignoreAttribute("value", to, "update", ctx)) { return; } if (fromValue !== toValue) { to.value = fromValue; } if (to.firstChild && to.firstChild.nodeValue !== fromValue) { to.firstChild.nodeValue = fromValue; } } } function handleHeadElement(newHeadTag, currentHead, ctx) { let added = []; let removed = []; let preserved = []; let nodesToAppend = []; let headMergeStyle = ctx.head.style; let srcToNewHeadNodes = new Map; for (const newHeadChild of newHeadTag.children) { srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild); } for (const currentHeadElt of currentHead.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 (headMergeStyle === "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 (newElt.href || newElt.src) { let resolve = null; let promise = new Promise((function(_resolve) { resolve = _resolve; })); newElt.addEventListener("load", (function() { resolve(); })); promises.push(promise); } currentHead.appendChild(newElt); ctx.callbacks.afterNodeAdded(newElt); added.push(newElt); } } for (const removedElement of removed) { if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) { currentHead.removeChild(removedElement); ctx.callbacks.afterNodeRemoved(removedElement); } } ctx.head.afterHeadMorphed(currentHead, { added: added, kept: preserved, removed: removed }); return promises; } function noOp() {} function mergeDefaults(config) { let finalConfig = {}; Object.assign(finalConfig, defaults); Object.assign(finalConfig, config); finalConfig.callbacks = {}; Object.assign(finalConfig.callbacks, defaults.callbacks); Object.assign(finalConfig.callbacks, config.callbacks); finalConfig.head = {}; Object.assign(finalConfig.head, defaults.head); Object.assign(finalConfig.head, config.head); return finalConfig; } function createMorphContext(oldNode, newContent, config) { config = mergeDefaults(config); return { target: oldNode, newContent: newContent, config: config, morphStyle: config.morphStyle, ignoreActive: config.ignoreActive, ignoreActiveValue: config.ignoreActiveValue, idMap: createIdMap(oldNode, newContent), deadIds: new Set, callbacks: config.callbacks, head: config.head }; } function isIdSetMatch(node1, node2, ctx) { if (node1 == null || node2 == null) { return false; } if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) { if (node1.id !== "" && node1.id === node2.id) { return true; } else { return getIdIntersectionCount(ctx, node1, node2) > 0; } } return false; } function isSoftMatch(node1, node2) { if (node1 == null || node2 == null) { return false; } return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName; } function removeNodesBetween(startInclusive, endExclusive, ctx) { while (startInclusive !== endExclusive) { let tempNode = startInclusive; startInclusive = startInclusive.nextSibling; removeNode(tempNode, ctx); } removeIdsFromConsideration(ctx, endExclusive); return endExclusive.nextSibling; } function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) { let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent); let potentialMatch = null; if (newChildPotentialIdCount > 0) { let potentialMatch = insertionPoint; let otherMatchCount = 0; while (potentialMatch != null) { if (isIdSetMatch(newChild, potentialMatch, ctx)) { return potentialMatch; } otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent); if (otherMatchCount > newChildPotentialIdCount) { return null; } potentialMatch = potentialMatch.nextSibling; } } return potentialMatch; } function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) { let potentialSoftMatch = insertionPoint; let nextSibling = newChild.nextSibling; let siblingSoftMatchCount = 0; while (potentialSoftMatch != null) { if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) { return null; } if (isSoftMatch(newChild, potentialSoftMatch)) { return potentialSoftMatch; } if (isSoftMatch(nextSibling, potentialSoftMatch)) { siblingSoftMatchCount++; nextSibling = nextSibling.nextSibling; if (siblingSoftMatchCount >= 2) { return null; } } potentialSoftMatch = potentialSoftMatch.nextSibling; } return potentialSoftMatch; } function parseContent(newContent) { let parser = new DOMParser; let contentWithSvgsRemoved = newContent.replace(/]*>|>)([\s\S]*?)<\/svg>/gim, ""); if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) { let content = parser.parseFromString(newContent, "text/html"); if (contentWithSvgsRemoved.match(/<\/html>/)) { content.generatedByIdiomorph = true; return content; } else { let htmlElement = content.firstChild; if (htmlElement) { htmlElement.generatedByIdiomorph = true; return htmlElement; } else { return null; } } } else { let responseDoc = parser.parseFromString("", "text/html"); let content = responseDoc.body.querySelector("template").content; content.generatedByIdiomorph = true; return content; } } function normalizeContent(newContent) { if (newContent == null) { const dummyParent = document.createElement("div"); return dummyParent; } else if (newContent.generatedByIdiomorph) { return newContent; } else if (newContent instanceof Node) { 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 insertSiblings(previousSibling, morphedNode, nextSibling) { let stack = []; let added = []; while (previousSibling != null) { stack.push(previousSibling); previousSibling = previousSibling.previousSibling; } while (stack.length > 0) { let node = stack.pop(); added.push(node); morphedNode.parentElement.insertBefore(node, morphedNode); } added.push(morphedNode); while (nextSibling != null) { stack.push(nextSibling); added.push(nextSibling); nextSibling = nextSibling.nextSibling; } while (stack.length > 0) { morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling); } return added; } function findBestNodeMatch(newContent, oldNode, ctx) { let currentElement; currentElement = newContent.firstChild; let bestElement = currentElement; let score = 0; while (currentElement) { let newScore = scoreElement(currentElement, oldNode, ctx); if (newScore > score) { bestElement = currentElement; score = newScore; } currentElement = currentElement.nextSibling; } return bestElement; } function scoreElement(node1, node2, ctx) { if (isSoftMatch(node1, node2)) { return .5 + getIdIntersectionCount(ctx, node1, node2); } return 0; } function removeNode(tempNode, ctx) { removeIdsFromConsideration(ctx, tempNode); if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return; tempNode.remove(); ctx.callbacks.afterNodeRemoved(tempNode); } function isIdInConsideration(ctx, id) { return !ctx.deadIds.has(id); } function idIsWithinNode(ctx, id, targetNode) { let idSet = ctx.idMap.get(targetNode) || EMPTY_SET; return idSet.has(id); } function removeIdsFromConsideration(ctx, node) { let idSet = ctx.idMap.get(node) || EMPTY_SET; for (const id of idSet) { ctx.deadIds.add(id); } } function getIdIntersectionCount(ctx, node1, node2) { let sourceSet = ctx.idMap.get(node1) || EMPTY_SET; let matchCount = 0; for (const id of sourceSet) { if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) { ++matchCount; } } return matchCount; } function populateIdMapForNode(node, idMap) { let nodeParent = node.parentElement; let idElements = node.querySelectorAll("[id]"); for (const elt of idElements) { let current = elt; while (current !== nodeParent && current != null) { let idSet = idMap.get(current); if (idSet == null) { idSet = new Set; idMap.set(current, idSet); } idSet.add(elt.id); current = current.parentElement; } } } function createIdMap(oldContent, newContent) { let idMap = new Map; populateIdMapForNode(oldContent, idMap); populateIdMapForNode(newContent, idMap); return idMap; } return { morph: morph, defaults: defaults }; }(); function morphElements(currentElement, newElement, {callbacks: callbacks, ...options} = {}) { Idiomorph.morph(currentElement, newElement, { ...options, callbacks: new DefaultIdiomorphCallbacks(callbacks) }); } function morphChildren(currentElement, newElement) { morphElements(currentElement, newElement.children, { morphStyle: "innerHTML" }); } class DefaultIdiomorphCallbacks { #beforeNodeMorphed; constructor({beforeNodeMorphed: beforeNodeMorphed} = {}) { this.#beforeNodeMorphed = beforeNodeMorphed || (() => true); } beforeNodeAdded=node => !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id)); beforeNodeMorphed=(currentElement, newElement) => { if (currentElement instanceof Element) { if (!currentElement.hasAttribute("data-turbo-permanent") && this.#beforeNodeMorphed(currentElement, newElement)) { const event = dispatch("turbo:before-morph-element", { cancelable: true, target: currentElement, detail: { currentElement: currentElement, newElement: newElement } }); return !event.defaultPrevented; } else { return false; } } }; beforeAttributeUpdated=(attributeName, target, mutationType) => { const event = dispatch("turbo:before-morph-attribute", { cancelable: true, target: target, detail: { attributeName: attributeName, mutationType: mutationType } }); return !event.defaultPrevented; }; beforeNodeRemoved=node => this.beforeNodeMorphed(node); afterNodeMorphed=(currentElement, newElement) => { if (currentElement instanceof Element) { dispatch("turbo:morph-element", { target: currentElement, detail: { currentElement: currentElement, newElement: newElement } }); } }; } class MorphingFrameRenderer extends FrameRenderer { static renderElement(currentElement, newElement) { dispatch("turbo:before-frame-morph", { target: currentElement, detail: { currentElement: currentElement, newElement: newElement } }); morphChildren(currentElement, newElement); } async preservingPermanentElements(callback) { return await callback(); } } class ProgressBar { static animationDuration=300; static get defaultCSS() { return unindent` .turbo-progress-bar { position: fixed; display: block; top: 0; left: 0; height: 3px; background: #0076ff; z-index: 2147483647; transition: width ${ProgressBar.animationDuration}ms ease-out, opacity ${ProgressBar.animationDuration / 2}ms ${ProgressBar.animationDuration / 2}ms ease-in; transform: translate3d(0, 0, 0); } `; } hiding=false; value=0; visible=false; constructor() { this.stylesheetElement = this.createStylesheetElement(); this.progressElement = this.createProgressElement(); this.installStylesheetElement(); this.setValue(0); } show() { if (!this.visible) { this.visible = true; this.installProgressElement(); this.startTrickling(); } } hide() { if (this.visible && !this.hiding) { this.hiding = true; this.fadeProgressElement((() => { this.uninstallProgressElement(); this.stopTrickling(); this.visible = false; this.hiding = false; })); } } setValue(value) { this.value = value; this.refresh(); } installStylesheetElement() { document.head.insertBefore(this.stylesheetElement, document.head.firstChild); } installProgressElement() { this.progressElement.style.width = "0"; this.progressElement.style.opacity = "1"; document.documentElement.insertBefore(this.progressElement, document.body); this.refresh(); } fadeProgressElement(callback) { this.progressElement.style.opacity = "0"; setTimeout(callback, ProgressBar.animationDuration * 1.5); } uninstallProgressElement() { if (this.progressElement.parentNode) { document.documentElement.removeChild(this.progressElement); } } startTrickling() { if (!this.trickleInterval) { this.trickleInterval = window.setInterval(this.trickle, ProgressBar.animationDuration); } } stopTrickling() { window.clearInterval(this.trickleInterval); delete this.trickleInterval; } trickle=() => { this.setValue(this.value + Math.random() / 100); }; refresh() { requestAnimationFrame((() => { this.progressElement.style.width = `${10 + this.value * 90}%`; })); } createStylesheetElement() { const element = document.createElement("style"); element.type = "text/css"; element.textContent = ProgressBar.defaultCSS; const cspNonce = getCspNonce(); if (cspNonce) { element.nonce = cspNonce; } return element; } createProgressElement() { const element = document.createElement("div"); element.className = "turbo-progress-bar"; return element; } } class HeadSnapshot extends Snapshot { detailsByOuterHTML=this.children.filter((element => !elementIsNoscript(element))).map((element => elementWithoutNonce(element))).reduce(((result, element) => { const {outerHTML: outerHTML} = element; const details = outerHTML in result ? result[outerHTML] : { type: elementType(element), tracked: elementIsTracked(element), elements: [] }; return { ...result, [outerHTML]: { ...details, elements: [ ...details.elements, element ] } }; }), {}); get trackedElementSignature() { return Object.keys(this.detailsByOuterHTML).filter((outerHTML => this.detailsByOuterHTML[outerHTML].tracked)).join(""); } getScriptElementsNotInSnapshot(snapshot) { return this.getElementsMatchingTypeNotInSnapshot("script", snapshot); } getStylesheetElementsNotInSnapshot(snapshot) { return this.getElementsMatchingTypeNotInSnapshot("stylesheet", snapshot); } getElementsMatchingTypeNotInSnapshot(matchedType, snapshot) { return Object.keys(this.detailsByOuterHTML).filter((outerHTML => !(outerHTML in snapshot.detailsByOuterHTML))).map((outerHTML => this.detailsByOuterHTML[outerHTML])).filter((({type: type}) => type == matchedType)).map((({elements: [element]}) => element)); } get provisionalElements() { return Object.keys(this.detailsByOuterHTML).reduce(((result, outerHTML) => { const {type: type, tracked: tracked, elements: elements} = this.detailsByOuterHTML[outerHTML]; if (type == null && !tracked) { return [ ...result, ...elements ]; } else if (elements.length > 1) { return [ ...result, ...elements.slice(1) ]; } else { return result; } }), []); } getMetaValue(name) { const element = this.findMetaElementByName(name); return element ? element.getAttribute("content") : null; } findMetaElementByName(name) { return Object.keys(this.detailsByOuterHTML).reduce(((result, outerHTML) => { const {elements: [element]} = this.detailsByOuterHTML[outerHTML]; return elementIsMetaElementWithName(element, name) ? element : result; }), undefined | undefined); } } function elementType(element) { if (elementIsScript(element)) { return "script"; } else if (elementIsStylesheet(element)) { return "stylesheet"; } } function elementIsTracked(element) { return element.getAttribute("data-turbo-track") == "reload"; } function elementIsScript(element) { const tagName = element.localName; return tagName == "script"; } function elementIsNoscript(element) { const tagName = element.localName; return tagName == "noscript"; } function elementIsStylesheet(element) { const tagName = element.localName; return tagName == "style" || tagName == "link" && element.getAttribute("rel") == "stylesheet"; } function elementIsMetaElementWithName(element, name) { const tagName = element.localName; return tagName == "meta" && element.getAttribute("name") == name; } function elementWithoutNonce(element) { if (element.hasAttribute("nonce")) { element.setAttribute("nonce", ""); } return element; } class PageSnapshot extends Snapshot { static fromHTMLString(html = "") { return this.fromDocument(parseHTMLDocument(html)); } static fromElement(element) { return this.fromDocument(element.ownerDocument); } static fromDocument({documentElement: documentElement, body: body, head: head}) { return new this(documentElement, body, new HeadSnapshot(head)); } constructor(documentElement, body, headSnapshot) { super(body); this.documentElement = documentElement; this.headSnapshot = headSnapshot; } clone() { const clonedElement = this.element.cloneNode(true); const selectElements = this.element.querySelectorAll("select"); const clonedSelectElements = clonedElement.querySelectorAll("select"); for (const [index, source] of selectElements.entries()) { const clone = clonedSelectElements[index]; for (const option of clone.selectedOptions) option.selected = false; for (const option of source.selectedOptions) clone.options[option.index].selected = true; } for (const clonedPasswordInput of clonedElement.querySelectorAll('input[type="password"]')) { clonedPasswordInput.value = ""; } return new PageSnapshot(this.documentElement, clonedElement, this.headSnapshot); } get lang() { return this.documentElement.getAttribute("lang"); } get headElement() { return this.headSnapshot.element; } get rootLocation() { const root = this.getSetting("root") ?? "/"; return expandURL(root); } get cacheControlValue() { return this.getSetting("cache-control"); } get isPreviewable() { return this.cacheControlValue != "no-preview"; } get isCacheable() { return this.cacheControlValue != "no-cache"; } get isVisitable() { return this.getSetting("visit-control") != "reload"; } get prefersViewTransitions() { return this.headSnapshot.getMetaValue("view-transition") === "same-origin"; } get shouldMorphPage() { return this.getSetting("refresh-method") === "morph"; } get shouldPreserveScrollPosition() { return this.getSetting("refresh-scroll") === "preserve"; } getSetting(name) { return this.headSnapshot.getMetaValue(`turbo-${name}`); } } class ViewTransitioner { #viewTransitionStarted=false; #lastOperation=Promise.resolve(); renderChange(useViewTransition, render) { if (useViewTransition && this.viewTransitionsAvailable && !this.#viewTransitionStarted) { this.#viewTransitionStarted = true; this.#lastOperation = this.#lastOperation.then((async () => { await document.startViewTransition(render).finished; })); } else { this.#lastOperation = this.#lastOperation.then(render); } return this.#lastOperation; } get viewTransitionsAvailable() { return document.startViewTransition; } } const defaultOptions = { action: "advance", historyChanged: false, visitCachedSnapshot: () => {}, willRender: true, updateHistory: true, shouldCacheSnapshot: true, acceptsStreamResponse: false }; const TimingMetric = { visitStart: "visitStart", requestStart: "requestStart", requestEnd: "requestEnd", visitEnd: "visitEnd" }; const VisitState = { initialized: "initialized", started: "started", canceled: "canceled", failed: "failed", completed: "completed" }; const SystemStatusCode = { networkFailure: 0, timeoutFailure: -1, contentTypeMismatch: -2 }; const Direction = { advance: "forward", restore: "back", replace: "none" }; class Visit { identifier=uuid(); timingMetrics={}; followedRedirect=false; historyChanged=false; scrolled=false; shouldCacheSnapshot=true; acceptsStreamResponse=false; snapshotCached=false; state=VisitState.initialized; viewTransitioner=new ViewTransitioner; constructor(delegate, location, restorationIdentifier, options = {}) { this.delegate = delegate; this.location = location; this.restorationIdentifier = restorationIdentifier || uuid(); const {action: action, historyChanged: historyChanged, referrer: referrer, snapshot: snapshot, snapshotHTML: snapshotHTML, response: response, visitCachedSnapshot: visitCachedSnapshot, willRender: willRender, updateHistory: updateHistory, shouldCacheSnapshot: shouldCacheSnapshot, acceptsStreamResponse: acceptsStreamResponse, direction: direction} = { ...defaultOptions, ...options }; this.action = action; this.historyChanged = historyChanged; this.referrer = referrer; this.snapshot = snapshot; this.snapshotHTML = snapshotHTML; this.response = response; this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action); this.isPageRefresh = this.view.isPageRefresh(this); this.visitCachedSnapshot = visitCachedSnapshot; this.willRender = willRender; this.updateHistory = updateHistory; this.scrolled = !willRender; this.shouldCacheSnapshot = shouldCacheSnapshot; this.acceptsStreamResponse = acceptsStreamResponse; this.direction = direction || Direction[action]; } get adapter() { return this.delegate.adapter; } get view() { return this.delegate.view; } get history() { return this.delegate.history; } get restorationData() { return this.history.getRestorationDataForIdentifier(this.restorationIdentifier); } get silent() { return this.isSamePage; } start() { if (this.state == VisitState.initialized) { this.recordTimingMetric(TimingMetric.visitStart); this.state = VisitState.started; this.adapter.visitStarted(this); this.delegate.visitStarted(this); } } cancel() { if (this.state == VisitState.started) { if (this.request) { this.request.cancel(); } this.cancelRender(); this.state = VisitState.canceled; } } complete() { if (this.state == VisitState.started) { this.recordTimingMetric(TimingMetric.visitEnd); this.adapter.visitCompleted(this); this.state = VisitState.completed; this.followRedirect(); if (!this.followedRedirect) { this.delegate.visitCompleted(this); } } } fail() { if (this.state == VisitState.started) { this.state = VisitState.failed; this.adapter.visitFailed(this); this.delegate.visitCompleted(this); } } changeHistory() { if (!this.historyChanged && this.updateHistory) { const actionForHistory = this.location.href === this.referrer?.href ? "replace" : this.action; const method = getHistoryMethodForAction(actionForHistory); this.history.update(method, this.location, this.restorationIdentifier); this.historyChanged = true; } } issueRequest() { if (this.hasPreloadedResponse()) { this.simulateRequest(); } else if (this.shouldIssueRequest() && !this.request) { this.request = new FetchRequest(this, FetchMethod.get, this.location); this.request.perform(); } } simulateRequest() { if (this.response) { this.startRequest(); this.recordResponse(); this.finishRequest(); } } startRequest() { this.recordTimingMetric(TimingMetric.requestStart); this.adapter.visitRequestStarted(this); } recordResponse(response = this.response) { this.response = response; if (response) { const {statusCode: statusCode} = response; if (isSuccessful(statusCode)) { this.adapter.visitRequestCompleted(this); } else { this.adapter.visitRequestFailedWithStatusCode(this, statusCode); } } } finishRequest() { this.recordTimingMetric(TimingMetric.requestEnd); this.adapter.visitRequestFinished(this); } loadResponse() { if (this.response) { const {statusCode: statusCode, responseHTML: responseHTML} = this.response; this.render((async () => { if (this.shouldCacheSnapshot) this.cacheSnapshot(); if (this.view.renderPromise) await this.view.renderPromise; if (isSuccessful(statusCode) && responseHTML != null) { const snapshot = PageSnapshot.fromHTMLString(responseHTML); await this.renderPageSnapshot(snapshot, false); this.adapter.visitRendered(this); this.complete(); } else { await this.view.renderError(PageSnapshot.fromHTMLString(responseHTML), this); this.adapter.visitRendered(this); this.fail(); } })); } } getCachedSnapshot() { const snapshot = this.view.getCachedSnapshotForLocation(this.location) || this.getPreloadedSnapshot(); if (snapshot && (!getAnchor(this.location) || snapshot.hasAnchor(getAnchor(this.location)))) { if (this.action == "restore" || snapshot.isPreviewable) { return snapshot; } } } getPreloadedSnapshot() { if (this.snapshotHTML) { return PageSnapshot.fromHTMLString(this.snapshotHTML); } } hasCachedSnapshot() { return this.getCachedSnapshot() != null; } loadCachedSnapshot() { const snapshot = this.getCachedSnapshot(); if (snapshot) { const isPreview = this.shouldIssueRequest(); this.render((async () => { this.cacheSnapshot(); if (this.isSamePage || this.isPageRefresh) { this.adapter.visitRendered(this); } else { if (this.view.renderPromise) await this.view.renderPromise; await this.renderPageSnapshot(snapshot, isPreview); this.adapter.visitRendered(this); if (!isPreview) { this.complete(); } } })); } } followRedirect() { if (this.redirectedToLocation && !this.followedRedirect && this.response?.redirected) { this.adapter.visitProposedToLocation(this.redirectedToLocation, { action: "replace", response: this.response, shouldCacheSnapshot: false, willRender: false }); this.followedRedirect = true; } } goToSamePageAnchor() { if (this.isSamePage) { this.render((async () => { this.cacheSnapshot(); this.performScroll(); this.changeHistory(); this.adapter.visitRendered(this); })); } } prepareRequest(request) { if (this.acceptsStreamResponse) { request.acceptResponseType(StreamMessage.contentType); } } requestStarted() { this.startRequest(); } requestPreventedHandlingResponse(_request, _response) {} async requestSucceededWithResponse(request, response) { const responseHTML = await response.responseHTML; const {redirected: redirected, statusCode: statusCode} = response; if (responseHTML == undefined) { this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch, redirected: redirected }); } else { this.redirectedToLocation = response.redirected ? response.location : undefined; this.recordResponse({ statusCode: statusCode, responseHTML: responseHTML, redirected: redirected }); } } async requestFailedWithResponse(request, response) { const responseHTML = await response.responseHTML; const {redirected: redirected, statusCode: statusCode} = response; if (responseHTML == undefined) { this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch, redirected: redirected }); } else { this.recordResponse({ statusCode: statusCode, responseHTML: responseHTML, redirected: redirected }); } } requestErrored(_request, _error) { this.recordResponse({ statusCode: SystemStatusCode.networkFailure, redirected: false }); } requestFinished() { this.finishRequest(); } performScroll() { if (!this.scrolled && !this.view.forceReloaded && !this.view.shouldPreserveScrollPosition(this)) { if (this.action == "restore") { this.scrollToRestoredPosition() || this.scrollToAnchor() || this.view.scrollToTop(); } else { this.scrollToAnchor() || this.view.scrollToTop(); } if (this.isSamePage) { this.delegate.visitScrolledToSamePageLocation(this.view.lastRenderedLocation, this.location); } this.scrolled = true; } } scrollToRestoredPosition() { const {scrollPosition: scrollPosition} = this.restorationData; if (scrollPosition) { this.view.scrollToPosition(scrollPosition); return true; } } scrollToAnchor() { const anchor = getAnchor(this.location); if (anchor != null) { this.view.scrollToAnchor(anchor); return true; } } recordTimingMetric(metric) { this.timingMetrics[metric] = (new Date).getTime(); } getTimingMetrics() { return { ...this.timingMetrics }; } getHistoryMethodForAction(action) { switch (action) { case "replace": return history.replaceState; case "advance": case "restore": return history.pushState; } } hasPreloadedResponse() { return typeof this.response == "object"; } shouldIssueRequest() { if (this.isSamePage) { return false; } else if (this.action == "restore") { return !this.hasCachedSnapshot(); } else { return this.willRender; } } cacheSnapshot() { if (!this.snapshotCached) { this.view.cacheSnapshot(this.snapshot).then((snapshot => snapshot && this.visitCachedSnapshot(snapshot))); this.snapshotCached = true; } } async render(callback) { this.cancelRender(); await new Promise((resolve => { this.frame = document.visibilityState === "hidden" ? setTimeout((() => resolve()), 0) : requestAnimationFrame((() => resolve())); })); await callback(); delete this.frame; } async renderPageSnapshot(snapshot, isPreview) { await this.viewTransitioner.renderChange(this.view.shouldTransitionTo(snapshot), (async () => { await this.view.renderPage(snapshot, isPreview, this.willRender, this); this.performScroll(); })); } cancelRender() { if (this.frame) { cancelAnimationFrame(this.frame); delete this.frame; } } } function isSuccessful(statusCode) { return statusCode >= 200 && statusCode < 300; } class BrowserAdapter { progressBar=new ProgressBar; constructor(session) { this.session = session; } visitProposedToLocation(location, options) { if (locationIsVisitable(location, this.navigator.rootLocation)) { this.navigator.startVisit(location, options?.restorationIdentifier || uuid(), options); } else { window.location.href = location.toString(); } } visitStarted(visit) { this.location = visit.location; visit.loadCachedSnapshot(); visit.issueRequest(); visit.goToSamePageAnchor(); } visitRequestStarted(visit) { this.progressBar.setValue(0); if (visit.hasCachedSnapshot() || visit.action != "restore") { this.showVisitProgressBarAfterDelay(); } else { this.showProgressBar(); } } visitRequestCompleted(visit) { visit.loadResponse(); } visitRequestFailedWithStatusCode(visit, statusCode) { switch (statusCode) { case SystemStatusCode.networkFailure: case SystemStatusCode.timeoutFailure: case SystemStatusCode.contentTypeMismatch: return this.reload({ reason: "request_failed", context: { statusCode: statusCode } }); default: return visit.loadResponse(); } } visitRequestFinished(_visit) {} visitCompleted(_visit) { this.progressBar.setValue(1); this.hideVisitProgressBar(); } pageInvalidated(reason) { this.reload(reason); } visitFailed(_visit) { this.progressBar.setValue(1); this.hideVisitProgressBar(); } visitRendered(_visit) {} formSubmissionStarted(_formSubmission) { this.progressBar.setValue(0); this.showFormProgressBarAfterDelay(); } formSubmissionFinished(_formSubmission) { this.progressBar.setValue(1); this.hideFormProgressBar(); } showVisitProgressBarAfterDelay() { this.visitProgressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay); } hideVisitProgressBar() { this.progressBar.hide(); if (this.visitProgressBarTimeout != null) { window.clearTimeout(this.visitProgressBarTimeout); delete this.visitProgressBarTimeout; } } showFormProgressBarAfterDelay() { if (this.formProgressBarTimeout == null) { this.formProgressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay); } } hideFormProgressBar() { this.progressBar.hide(); if (this.formProgressBarTimeout != null) { window.clearTimeout(this.formProgressBarTimeout); delete this.formProgressBarTimeout; } } showProgressBar=() => { this.progressBar.show(); }; reload(reason) { dispatch("turbo:reload", { detail: reason }); window.location.href = this.location?.toString() || window.location.href; } get navigator() { return this.session.navigator; } } class CacheObserver { selector="[data-turbo-temporary]"; deprecatedSelector="[data-turbo-cache=false]"; started=false; start() { if (!this.started) { this.started = true; addEventListener("turbo:before-cache", this.removeTemporaryElements, false); } } stop() { if (this.started) { this.started = false; removeEventListener("turbo:before-cache", this.removeTemporaryElements, false); } } removeTemporaryElements=_event => { for (const element of this.temporaryElements) { element.remove(); } }; get temporaryElements() { return [ ...document.querySelectorAll(this.selector), ...this.temporaryElementsWithDeprecation ]; } get temporaryElementsWithDeprecation() { const elements = document.querySelectorAll(this.deprecatedSelector); if (elements.length) { console.warn(`The ${this.deprecatedSelector} selector is deprecated and will be removed in a future version. Use ${this.selector} instead.`); } return [ ...elements ]; } } class FrameRedirector { constructor(session, element) { this.session = session; this.element = element; this.linkInterceptor = new LinkInterceptor(this, element); this.formSubmitObserver = new FormSubmitObserver(this, element); } start() { this.linkInterceptor.start(); this.formSubmitObserver.start(); } stop() { this.linkInterceptor.stop(); this.formSubmitObserver.stop(); } shouldInterceptLinkClick(element, _location, _event) { return this.#shouldRedirect(element); } linkClickIntercepted(element, url, event) { const frame = this.#findFrameElement(element); if (frame) { frame.delegate.linkClickIntercepted(element, url, event); } } willSubmitForm(element, submitter) { return element.closest("turbo-frame") == null && this.#shouldSubmit(element, submitter) && this.#shouldRedirect(element, submitter); } formSubmitted(element, submitter) { const frame = this.#findFrameElement(element, submitter); if (frame) { frame.delegate.formSubmitted(element, submitter); } } #shouldSubmit(form, submitter) { const action = getAction$1(form, submitter); const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`); const rootLocation = expandURL(meta?.content ?? "/"); return this.#shouldRedirect(form, submitter) && locationIsVisitable(action, rootLocation); } #shouldRedirect(element, submitter) { const isNavigatable = element instanceof HTMLFormElement ? this.session.submissionIsNavigatable(element, submitter) : this.session.elementIsNavigatable(element); if (isNavigatable) { const frame = this.#findFrameElement(element, submitter); return frame ? frame != element.closest("turbo-frame") : false; } else { return false; } } #findFrameElement(element, submitter) { const id = submitter?.getAttribute("data-turbo-frame") || element.getAttribute("data-turbo-frame"); if (id && id != "_top") { const frame = this.element.querySelector(`#${id}:not([disabled])`); if (frame instanceof FrameElement) { return frame; } } } } class History { location; restorationIdentifier=uuid(); restorationData={}; started=false; pageLoaded=false; currentIndex=0; constructor(delegate) { this.delegate = delegate; } start() { if (!this.started) { addEventListener("popstate", this.onPopState, false); addEventListener("load", this.onPageLoad, false); this.currentIndex = history.state?.turbo?.restorationIndex || 0; this.started = true; this.replace(new URL(window.location.href)); } } stop() { if (this.started) { removeEventListener("popstate", this.onPopState, false); removeEventListener("load", this.onPageLoad, false); this.started = false; } } push(location, restorationIdentifier) { this.update(history.pushState, location, restorationIdentifier); } replace(location, restorationIdentifier) { this.update(history.replaceState, location, restorationIdentifier); } update(method, location, restorationIdentifier = uuid()) { if (method === history.pushState) ++this.currentIndex; const state = { turbo: { restorationIdentifier: restorationIdentifier, restorationIndex: this.currentIndex } }; method.call(history, state, "", location.href); this.location = location; this.restorationIdentifier = restorationIdentifier; } getRestorationDataForIdentifier(restorationIdentifier) { return this.restorationData[restorationIdentifier] || {}; } updateRestorationData(additionalData) { const {restorationIdentifier: restorationIdentifier} = this; const restorationData = this.restorationData[restorationIdentifier]; this.restorationData[restorationIdentifier] = { ...restorationData, ...additionalData }; } assumeControlOfScrollRestoration() { if (!this.previousScrollRestoration) { this.previousScrollRestoration = history.scrollRestoration ?? "auto"; history.scrollRestoration = "manual"; } } relinquishControlOfScrollRestoration() { if (this.previousScrollRestoration) { history.scrollRestoration = this.previousScrollRestoration; delete this.previousScrollRestoration; } } onPopState=event => { if (this.shouldHandlePopState()) { const {turbo: turbo} = event.state || {}; if (turbo) { this.location = new URL(window.location.href); const {restorationIdentifier: restorationIdentifier, restorationIndex: restorationIndex} = turbo; this.restorationIdentifier = restorationIdentifier; const direction = restorationIndex > this.currentIndex ? "forward" : "back"; this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(this.location, restorationIdentifier, direction); this.currentIndex = restorationIndex; } } }; onPageLoad=async _event => { await nextMicrotask(); this.pageLoaded = true; }; shouldHandlePopState() { return this.pageIsLoaded(); } pageIsLoaded() { return this.pageLoaded || document.readyState == "complete"; } } class LinkPrefetchObserver { started=false; #prefetchedLink=null; constructor(delegate, eventTarget) { this.delegate = delegate; this.eventTarget = eventTarget; } start() { if (this.started) return; if (this.eventTarget.readyState === "loading") { this.eventTarget.addEventListener("DOMContentLoaded", this.#enable, { once: true }); } else { this.#enable(); } } stop() { if (!this.started) return; this.eventTarget.removeEventListener("mouseenter", this.#tryToPrefetchRequest, { capture: true, passive: true }); this.eventTarget.removeEventListener("mouseleave", this.#cancelRequestIfObsolete, { capture: true, passive: true }); this.eventTarget.removeEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true); this.started = false; } #enable=() => { this.eventTarget.addEventListener("mouseenter", this.#tryToPrefetchRequest, { capture: true, passive: true }); this.eventTarget.addEventListener("mouseleave", this.#cancelRequestIfObsolete, { capture: true, passive: true }); this.eventTarget.addEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true); this.started = true; }; #tryToPrefetchRequest=event => { if (getMetaContent("turbo-prefetch") === "false") return; const target = event.target; const isLink = target.matches && target.matches("a[href]:not([target^=_]):not([download])"); if (isLink && this.#isPrefetchable(target)) { const link = target; const location = getLocationForLink(link); if (this.delegate.canPrefetchRequestToLocation(link, location)) { this.#prefetchedLink = link; const fetchRequest = new FetchRequest(this, FetchMethod.get, location, new URLSearchParams, target); prefetchCache.setLater(location.toString(), fetchRequest, this.#cacheTtl); } } }; #cancelRequestIfObsolete=event => { if (event.target === this.#prefetchedLink) this.#cancelPrefetchRequest(); }; #cancelPrefetchRequest=() => { prefetchCache.clear(); this.#prefetchedLink = null; }; #tryToUsePrefetchedRequest=event => { if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "GET") { const cached = prefetchCache.get(event.detail.url.toString()); if (cached) { event.detail.fetchRequest = cached; } prefetchCache.clear(); } }; prepareRequest(request) { const link = request.target; request.headers["X-Sec-Purpose"] = "prefetch"; const turboFrame = link.closest("turbo-frame"); const turboFrameTarget = link.getAttribute("data-turbo-frame") || turboFrame?.getAttribute("target") || turboFrame?.id; if (turboFrameTarget && turboFrameTarget !== "_top") { request.headers["Turbo-Frame"] = turboFrameTarget; } } requestSucceededWithResponse() {} requestStarted(fetchRequest) {} requestErrored(fetchRequest) {} requestFinished(fetchRequest) {} requestPreventedHandlingResponse(fetchRequest, fetchResponse) {} requestFailedWithResponse(fetchRequest, fetchResponse) {} get #cacheTtl() { return Number(getMetaContent("turbo-prefetch-cache-time")) || cacheTtl; } #isPrefetchable(link) { const href = link.getAttribute("href"); if (!href) return false; if (unfetchableLink(link)) return false; if (linkToTheSamePage(link)) return false; if (linkOptsOut(link)) return false; if (nonSafeLink(link)) return false; if (eventPrevented(link)) return false; return true; } } const unfetchableLink = link => link.origin !== document.location.origin || ![ "http:", "https:" ].includes(link.protocol) || link.hasAttribute("target"); const linkToTheSamePage = link => link.pathname + link.search === document.location.pathname + document.location.search || link.href.startsWith("#"); const linkOptsOut = link => { if (link.getAttribute("data-turbo-prefetch") === "false") return true; if (link.getAttribute("data-turbo") === "false") return true; const turboPrefetchParent = findClosestRecursively(link, "[data-turbo-prefetch]"); if (turboPrefetchParent && turboPrefetchParent.getAttribute("data-turbo-prefetch") === "false") return true; return false; }; const nonSafeLink = link => { const turboMethod = link.getAttribute("data-turbo-method"); if (turboMethod && turboMethod.toLowerCase() !== "get") return true; if (isUJS(link)) return true; if (link.hasAttribute("data-turbo-confirm")) return true; if (link.hasAttribute("data-turbo-stream")) return true; return false; }; const isUJS = link => link.hasAttribute("data-remote") || link.hasAttribute("data-behavior") || link.hasAttribute("data-confirm") || link.hasAttribute("data-method"); const eventPrevented = link => { const event = dispatch("turbo:before-prefetch", { target: link, cancelable: true }); return event.defaultPrevented; }; class Navigator { constructor(delegate) { this.delegate = delegate; } proposeVisit(location, options = {}) { if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) { this.delegate.visitProposedToLocation(location, options); } } startVisit(locatable, restorationIdentifier, options = {}) { this.stop(); this.currentVisit = new Visit(this, expandURL(locatable), restorationIdentifier, { referrer: this.location, ...options }); this.currentVisit.start(); } submitForm(form, submitter) { this.stop(); this.formSubmission = new FormSubmission(this, form, submitter, true); this.formSubmission.start(); } stop() { if (this.formSubmission) { this.formSubmission.stop(); delete this.formSubmission; } if (this.currentVisit) { this.currentVisit.cancel(); delete this.currentVisit; } } get adapter() { return this.delegate.adapter; } get view() { return this.delegate.view; } get rootLocation() { return this.view.snapshot.rootLocation; } get history() { return this.delegate.history; } formSubmissionStarted(formSubmission) { if (typeof this.adapter.formSubmissionStarted === "function") { this.adapter.formSubmissionStarted(formSubmission); } } async formSubmissionSucceededWithResponse(formSubmission, fetchResponse) { if (formSubmission == this.formSubmission) { const responseHTML = await fetchResponse.responseHTML; if (responseHTML) { const shouldCacheSnapshot = formSubmission.isSafe; if (!shouldCacheSnapshot) { this.view.clearSnapshotCache(); } const {statusCode: statusCode, redirected: redirected} = fetchResponse; const action = this.#getActionForFormSubmission(formSubmission, fetchResponse); const visitOptions = { action: action, shouldCacheSnapshot: shouldCacheSnapshot, response: { statusCode: statusCode, responseHTML: responseHTML, redirected: redirected } }; this.proposeVisit(fetchResponse.location, visitOptions); } } } async formSubmissionFailedWithResponse(formSubmission, fetchResponse) { const responseHTML = await fetchResponse.responseHTML; if (responseHTML) { const snapshot = PageSnapshot.fromHTMLString(responseHTML); if (fetchResponse.serverError) { await this.view.renderError(snapshot, this.currentVisit); } else { await this.view.renderPage(snapshot, false, true, this.currentVisit); } if (!snapshot.shouldPreserveScrollPosition) { this.view.scrollToTop(); } this.view.clearSnapshotCache(); } } formSubmissionErrored(formSubmission, error) { console.error(error); } formSubmissionFinished(formSubmission) { if (typeof this.adapter.formSubmissionFinished === "function") { this.adapter.formSubmissionFinished(formSubmission); } } visitStarted(visit) { this.delegate.visitStarted(visit); } visitCompleted(visit) { this.delegate.visitCompleted(visit); delete this.currentVisit; } locationWithActionIsSamePage(location, action) { const anchor = getAnchor(location); const currentAnchor = getAnchor(this.view.lastRenderedLocation); const isRestorationToTop = action === "restore" && typeof anchor === "undefined"; return action !== "replace" && getRequestURL(location) === getRequestURL(this.view.lastRenderedLocation) && (isRestorationToTop || anchor != null && anchor !== currentAnchor); } visitScrolledToSamePageLocation(oldURL, newURL) { this.delegate.visitScrolledToSamePageLocation(oldURL, newURL); } get location() { return this.history.location; } get restorationIdentifier() { return this.history.restorationIdentifier; } #getActionForFormSubmission(formSubmission, fetchResponse) { const {submitter: submitter, formElement: formElement} = formSubmission; return getVisitAction(submitter, formElement) || this.#getDefaultAction(fetchResponse); } #getDefaultAction(fetchResponse) { const sameLocationRedirect = fetchResponse.redirected && fetchResponse.location.href === this.location?.href; return sameLocationRedirect ? "replace" : "advance"; } } const PageStage = { initial: 0, loading: 1, interactive: 2, complete: 3 }; class PageObserver { stage=PageStage.initial; started=false; constructor(delegate) { this.delegate = delegate; } start() { if (!this.started) { if (this.stage == PageStage.initial) { this.stage = PageStage.loading; } document.addEventListener("readystatechange", this.interpretReadyState, false); addEventListener("pagehide", this.pageWillUnload, false); this.started = true; } } stop() { if (this.started) { document.removeEventListener("readystatechange", this.interpretReadyState, false); removeEventListener("pagehide", this.pageWillUnload, false); this.started = false; } } interpretReadyState=() => { const {readyState: readyState} = this; if (readyState == "interactive") { this.pageIsInteractive(); } else if (readyState == "complete") { this.pageIsComplete(); } }; pageIsInteractive() { if (this.stage == PageStage.loading) { this.stage = PageStage.interactive; this.delegate.pageBecameInteractive(); } } pageIsComplete() { this.pageIsInteractive(); if (this.stage == PageStage.interactive) { this.stage = PageStage.complete; this.delegate.pageLoaded(); } } pageWillUnload=() => { this.delegate.pageWillUnload(); }; get readyState() { return document.readyState; } } class ScrollObserver { started=false; constructor(delegate) { this.delegate = delegate; } start() { if (!this.started) { addEventListener("scroll", this.onScroll, false); this.onScroll(); this.started = true; } } stop() { if (this.started) { removeEventListener("scroll", this.onScroll, false); this.started = false; } } onScroll=() => { this.updatePosition({ x: window.pageXOffset, y: window.pageYOffset }); }; updatePosition(position) { this.delegate.scrollPositionChanged(position); } } class StreamMessageRenderer { render({fragment: fragment}) { Bardo.preservingPermanentElements(this, getPermanentElementMapForFragment(fragment), (() => { withAutofocusFromFragment(fragment, (() => { withPreservedFocus((() => { document.documentElement.appendChild(fragment); })); })); })); } enteringBardo(currentPermanentElement, newPermanentElement) { newPermanentElement.replaceWith(currentPermanentElement.cloneNode(true)); } leavingBardo() {} } function getPermanentElementMapForFragment(fragment) { const permanentElementsInDocument = queryPermanentElementsAll(document.documentElement); const permanentElementMap = {}; for (const permanentElementInDocument of permanentElementsInDocument) { const {id: id} = permanentElementInDocument; for (const streamElement of fragment.querySelectorAll("turbo-stream")) { const elementInStream = getPermanentElementById(streamElement.templateElement.content, id); if (elementInStream) { permanentElementMap[id] = [ permanentElementInDocument, elementInStream ]; } } } return permanentElementMap; } async function withAutofocusFromFragment(fragment, callback) { const generatedID = `turbo-stream-autofocus-${uuid()}`; const turboStreams = fragment.querySelectorAll("turbo-stream"); const elementWithAutofocus = firstAutofocusableElementInStreams(turboStreams); let willAutofocusId = null; if (elementWithAutofocus) { if (elementWithAutofocus.id) { willAutofocusId = elementWithAutofocus.id; } else { willAutofocusId = generatedID; } elementWithAutofocus.id = willAutofocusId; } callback(); await nextRepaint(); const hasNoActiveElement = document.activeElement == null || document.activeElement == document.body; if (hasNoActiveElement && willAutofocusId) { const elementToAutofocus = document.getElementById(willAutofocusId); if (elementIsFocusable(elementToAutofocus)) { elementToAutofocus.focus(); } if (elementToAutofocus && elementToAutofocus.id == generatedID) { elementToAutofocus.removeAttribute("id"); } } } async function withPreservedFocus(callback) { const [activeElementBeforeRender, activeElementAfterRender] = await around(callback, (() => document.activeElement)); const restoreFocusTo = activeElementBeforeRender && activeElementBeforeRender.id; if (restoreFocusTo) { const elementToFocus = document.getElementById(restoreFocusTo); if (elementIsFocusable(elementToFocus) && elementToFocus != activeElementAfterRender) { elementToFocus.focus(); } } } function firstAutofocusableElementInStreams(nodeListOfStreamElements) { for (const streamElement of nodeListOfStreamElements) { const elementWithAutofocus = queryAutofocusableElement(streamElement.templateElement.content); if (elementWithAutofocus) return elementWithAutofocus; } return null; } class StreamObserver { sources=new Set; #started=false; constructor(delegate) { this.delegate = delegate; } start() { if (!this.#started) { this.#started = true; addEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false); } } stop() { if (this.#started) { this.#started = false; removeEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false); } } connectStreamSource(source) { if (!this.streamSourceIsConnected(source)) { this.sources.add(source); source.addEventListener("message", this.receiveMessageEvent, false); } } disconnectStreamSource(source) { if (this.streamSourceIsConnected(source)) { this.sources.delete(source); source.removeEventListener("message", this.receiveMessageEvent, false); } } streamSourceIsConnected(source) { return this.sources.has(source); } inspectFetchResponse=event => { const response = fetchResponseFromEvent(event); if (response && fetchResponseIsStream(response)) { event.preventDefault(); this.receiveMessageResponse(response); } }; receiveMessageEvent=event => { if (this.#started && typeof event.data == "string") { this.receiveMessageHTML(event.data); } }; async receiveMessageResponse(response) { const html = await response.responseHTML; if (html) { this.receiveMessageHTML(html); } } receiveMessageHTML(html) { this.delegate.receivedMessageFromStream(StreamMessage.wrap(html)); } } function fetchResponseFromEvent(event) { const fetchResponse = event.detail?.fetchResponse; if (fetchResponse instanceof FetchResponse) { return fetchResponse; } } function fetchResponseIsStream(response) { const contentType = response.contentType ?? ""; return contentType.startsWith(StreamMessage.contentType); } class ErrorRenderer extends Renderer { static renderElement(currentElement, newElement) { const {documentElement: documentElement, body: body} = document; documentElement.replaceChild(newElement, body); } async render() { this.replaceHeadAndBody(); this.activateScriptElements(); } replaceHeadAndBody() { const {documentElement: documentElement, head: head} = document; documentElement.replaceChild(this.newHead, head); this.renderElement(this.currentElement, this.newElement); } activateScriptElements() { for (const replaceableElement of this.scriptElements) { const parentNode = replaceableElement.parentNode; if (parentNode) { const element = activateScriptElement(replaceableElement); parentNode.replaceChild(element, replaceableElement); } } } get newHead() { return this.newSnapshot.headSnapshot.element; } get scriptElements() { return document.documentElement.querySelectorAll("script"); } } class PageRenderer extends Renderer { static renderElement(currentElement, newElement) { if (document.body && newElement instanceof HTMLBodyElement) { document.body.replaceWith(newElement); } else { document.documentElement.appendChild(newElement); } } get shouldRender() { return this.newSnapshot.isVisitable && this.trackedElementsAreIdentical; } get reloadReason() { if (!this.newSnapshot.isVisitable) { return { reason: "turbo_visit_control_is_reload" }; } if (!this.trackedElementsAreIdentical) { return { reason: "tracked_element_mismatch" }; } } async prepareToRender() { this.#setLanguage(); await this.mergeHead(); } async render() { if (this.willRender) { await this.replaceBody(); } } finishRendering() { super.finishRendering(); if (!this.isPreview) { this.focusFirstAutofocusableElement(); } } get currentHeadSnapshot() { return this.currentSnapshot.headSnapshot; } get newHeadSnapshot() { return this.newSnapshot.headSnapshot; } get newElement() { return this.newSnapshot.element; } #setLanguage() { const {documentElement: documentElement} = this.currentSnapshot; const {lang: lang} = this.newSnapshot; if (lang) { documentElement.setAttribute("lang", lang); } else { documentElement.removeAttribute("lang"); } } async mergeHead() { const mergedHeadElements = this.mergeProvisionalElements(); const newStylesheetElements = this.copyNewHeadStylesheetElements(); this.copyNewHeadScriptElements(); await mergedHeadElements; await newStylesheetElements; if (this.willRender) { this.removeUnusedDynamicStylesheetElements(); } } async replaceBody() { await this.preservingPermanentElements((async () => { this.activateNewBody(); await this.assignNewBody(); })); } get trackedElementsAreIdentical() { return this.currentHeadSnapshot.trackedElementSignature == this.newHeadSnapshot.trackedElementSignature; } async copyNewHeadStylesheetElements() { const loadingElements = []; for (const element of this.newHeadStylesheetElements) { loadingElements.push(waitForLoad(element)); document.head.appendChild(element); } await Promise.all(loadingElements); } copyNewHeadScriptElements() { for (const element of this.newHeadScriptElements) { document.head.appendChild(activateScriptElement(element)); } } removeUnusedDynamicStylesheetElements() { for (const element of this.unusedDynamicStylesheetElements) { document.head.removeChild(element); } } async mergeProvisionalElements() { const newHeadElements = [ ...this.newHeadProvisionalElements ]; for (const element of this.currentHeadProvisionalElements) { if (!this.isCurrentElementInElementList(element, newHeadElements)) { document.head.removeChild(element); } } for (const element of newHeadElements) { document.head.appendChild(element); } } isCurrentElementInElementList(element, elementList) { for (const [index, newElement] of elementList.entries()) { if (element.tagName == "TITLE") { if (newElement.tagName != "TITLE") { continue; } if (element.innerHTML == newElement.innerHTML) { elementList.splice(index, 1); return true; } } if (newElement.isEqualNode(element)) { elementList.splice(index, 1); return true; } } return false; } removeCurrentHeadProvisionalElements() { for (const element of this.currentHeadProvisionalElements) { document.head.removeChild(element); } } copyNewHeadProvisionalElements() { for (const element of this.newHeadProvisionalElements) { document.head.appendChild(element); } } activateNewBody() { document.adoptNode(this.newElement); this.activateNewBodyScriptElements(); } activateNewBodyScriptElements() { for (const inertScriptElement of this.newBodyScriptElements) { const activatedScriptElement = activateScriptElement(inertScriptElement); inertScriptElement.replaceWith(activatedScriptElement); } } async assignNewBody() { await this.renderElement(this.currentElement, this.newElement); } get unusedDynamicStylesheetElements() { return this.oldHeadStylesheetElements.filter((element => element.getAttribute("data-turbo-track") === "dynamic")); } get oldHeadStylesheetElements() { return this.currentHeadSnapshot.getStylesheetElementsNotInSnapshot(this.newHeadSnapshot); } get newHeadStylesheetElements() { return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot); } get newHeadScriptElements() { return this.newHeadSnapshot.getScriptElementsNotInSnapshot(this.currentHeadSnapshot); } get currentHeadProvisionalElements() { return this.currentHeadSnapshot.provisionalElements; } get newHeadProvisionalElements() { return this.newHeadSnapshot.provisionalElements; } get newBodyScriptElements() { return this.newElement.querySelectorAll("script"); } } class MorphingPageRenderer extends PageRenderer { static renderElement(currentElement, newElement) { morphElements(currentElement, newElement, { callbacks: { beforeNodeMorphed: element => !canRefreshFrame(element) } }); for (const frame of currentElement.querySelectorAll("turbo-frame")) { if (canRefreshFrame(frame)) frame.reload(); } dispatch("turbo:morph", { detail: { currentElement: currentElement, newElement: newElement } }); } async preservingPermanentElements(callback) { return await callback(); } get renderMethod() { return "morph"; } get shouldAutofocus() { return false; } } function canRefreshFrame(frame) { return frame instanceof FrameElement && frame.src && frame.refresh === "morph" && !frame.closest("[data-turbo-permanent]"); } class SnapshotCache { keys=[]; snapshots={}; constructor(size) { this.size = size; } has(location) { return toCacheKey(location) in this.snapshots; } get(location) { if (this.has(location)) { const snapshot = this.read(location); this.touch(location); return snapshot; } } put(location, snapshot) { this.write(location, snapshot); this.touch(location); return snapshot; } clear() { this.snapshots = {}; } read(location) { return this.snapshots[toCacheKey(location)]; } write(location, snapshot) { this.snapshots[toCacheKey(location)] = snapshot; } touch(location) { const key = toCacheKey(location); const index = this.keys.indexOf(key); if (index > -1) this.keys.splice(index, 1); this.keys.unshift(key); this.trim(); } trim() { for (const key of this.keys.splice(this.size)) { delete this.snapshots[key]; } } } class PageView extends View { snapshotCache=new SnapshotCache(10); lastRenderedLocation=new URL(location.href); forceReloaded=false; shouldTransitionTo(newSnapshot) { return this.snapshot.prefersViewTransitions && newSnapshot.prefersViewTransitions; } renderPage(snapshot, isPreview = false, willRender = true, visit) { const shouldMorphPage = this.isPageRefresh(visit) && this.snapshot.shouldMorphPage; const rendererClass = shouldMorphPage ? MorphingPageRenderer : PageRenderer; const renderer = new rendererClass(this.snapshot, snapshot, isPreview, willRender); if (!renderer.shouldRender) { this.forceReloaded = true; } else { visit?.changeHistory(); } return this.render(renderer); } renderError(snapshot, visit) { visit?.changeHistory(); const renderer = new ErrorRenderer(this.snapshot, snapshot, false); return this.render(renderer); } clearSnapshotCache() { this.snapshotCache.clear(); } async cacheSnapshot(snapshot = this.snapshot) { if (snapshot.isCacheable) { this.delegate.viewWillCacheSnapshot(); const {lastRenderedLocation: location} = this; await nextEventLoopTick(); const cachedSnapshot = snapshot.clone(); this.snapshotCache.put(location, cachedSnapshot); return cachedSnapshot; } } getCachedSnapshotForLocation(location) { return this.snapshotCache.get(location); } isPageRefresh(visit) { return !visit || this.lastRenderedLocation.pathname === visit.location.pathname && visit.action === "replace"; } shouldPreserveScrollPosition(visit) { return this.isPageRefresh(visit) && this.snapshot.shouldPreserveScrollPosition; } get snapshot() { return PageSnapshot.fromElement(this.element); } } class Preloader { selector="a[data-turbo-preload]"; constructor(delegate, snapshotCache) { this.delegate = delegate; this.snapshotCache = snapshotCache; } start() { if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", this.#preloadAll); } else { this.preloadOnLoadLinksForView(document.body); } } stop() { document.removeEventListener("DOMContentLoaded", this.#preloadAll); } preloadOnLoadLinksForView(element) { for (const link of element.querySelectorAll(this.selector)) { if (this.delegate.shouldPreloadLink(link)) { this.preloadURL(link); } } } async preloadURL(link) { const location = new URL(link.href); if (this.snapshotCache.has(location)) { return; } const fetchRequest = new FetchRequest(this, FetchMethod.get, location, new URLSearchParams, link); await fetchRequest.perform(); } prepareRequest(fetchRequest) { fetchRequest.headers["X-Sec-Purpose"] = "prefetch"; } async requestSucceededWithResponse(fetchRequest, fetchResponse) { try { const responseHTML = await fetchResponse.responseHTML; const snapshot = PageSnapshot.fromHTMLString(responseHTML); this.snapshotCache.put(fetchRequest.url, snapshot); } catch (_) {} } requestStarted(fetchRequest) {} requestErrored(fetchRequest) {} requestFinished(fetchRequest) {} requestPreventedHandlingResponse(fetchRequest, fetchResponse) {} requestFailedWithResponse(fetchRequest, fetchResponse) {} #preloadAll=() => { this.preloadOnLoadLinksForView(document.body); }; } class Cache { constructor(session) { this.session = session; } clear() { this.session.clearCache(); } resetCacheControl() { this.#setCacheControl(""); } exemptPageFromCache() { this.#setCacheControl("no-cache"); } exemptPageFromPreview() { this.#setCacheControl("no-preview"); } #setCacheControl(value) { setMetaContent("turbo-cache-control", value); } } class Session { navigator=new Navigator(this); history=new History(this); view=new PageView(this, document.documentElement); adapter=new BrowserAdapter(this); pageObserver=new PageObserver(this); cacheObserver=new CacheObserver; linkPrefetchObserver=new LinkPrefetchObserver(this, document); linkClickObserver=new LinkClickObserver(this, window); formSubmitObserver=new FormSubmitObserver(this, document); scrollObserver=new ScrollObserver(this); streamObserver=new StreamObserver(this); formLinkClickObserver=new FormLinkClickObserver(this, document.documentElement); frameRedirector=new FrameRedirector(this, document.documentElement); streamMessageRenderer=new StreamMessageRenderer; cache=new Cache(this); enabled=true; started=false; #pageRefreshDebouncePeriod=150; constructor(recentRequests) { this.recentRequests = recentRequests; this.preloader = new Preloader(this, this.view.snapshotCache); this.debouncedRefresh = this.refresh; this.pageRefreshDebouncePeriod = this.pageRefreshDebouncePeriod; } start() { if (!this.started) { this.pageObserver.start(); this.cacheObserver.start(); this.linkPrefetchObserver.start(); this.formLinkClickObserver.start(); this.linkClickObserver.start(); this.formSubmitObserver.start(); this.scrollObserver.start(); this.streamObserver.start(); this.frameRedirector.start(); this.history.start(); this.preloader.start(); this.started = true; this.enabled = true; } } disable() { this.enabled = false; } stop() { if (this.started) { this.pageObserver.stop(); this.cacheObserver.stop(); this.linkPrefetchObserver.stop(); this.formLinkClickObserver.stop(); this.linkClickObserver.stop(); this.formSubmitObserver.stop(); this.scrollObserver.stop(); this.streamObserver.stop(); this.frameRedirector.stop(); this.history.stop(); this.preloader.stop(); this.started = false; } } registerAdapter(adapter) { this.adapter = adapter; } visit(location, options = {}) { const frameElement = options.frame ? document.getElementById(options.frame) : null; if (frameElement instanceof FrameElement) { const action = options.action || getVisitAction(frameElement); frameElement.delegate.proposeVisitIfNavigatedWithAction(frameElement, action); frameElement.src = location.toString(); } else { this.navigator.proposeVisit(expandURL(location), options); } } refresh(url, requestId) { const isRecentRequest = requestId && this.recentRequests.has(requestId); if (!isRecentRequest && !this.navigator.currentVisit) { this.visit(url, { action: "replace", shouldCacheSnapshot: false }); } } connectStreamSource(source) { this.streamObserver.connectStreamSource(source); } disconnectStreamSource(source) { this.streamObserver.disconnectStreamSource(source); } renderStreamMessage(message) { this.streamMessageRenderer.render(StreamMessage.wrap(message)); } clearCache() { this.view.clearSnapshotCache(); } setProgressBarDelay(delay) { console.warn("Please replace `session.setProgressBarDelay(delay)` with `session.progressBarDelay = delay`. The function is deprecated and will be removed in a future version of Turbo.`"); this.progressBarDelay = delay; } set progressBarDelay(delay) { config.drive.progressBarDelay = delay; } get progressBarDelay() { return config.drive.progressBarDelay; } set drive(value) { config.drive.enabled = value; } get drive() { return config.drive.enabled; } set formMode(value) { config.forms.mode = value; } get formMode() { return config.forms.mode; } get location() { return this.history.location; } get restorationIdentifier() { return this.history.restorationIdentifier; } get pageRefreshDebouncePeriod() { return this.#pageRefreshDebouncePeriod; } set pageRefreshDebouncePeriod(value) { this.refresh = debounce(this.debouncedRefresh.bind(this), value); this.#pageRefreshDebouncePeriod = value; } shouldPreloadLink(element) { const isUnsafe = element.hasAttribute("data-turbo-method"); const isStream = element.hasAttribute("data-turbo-stream"); const frameTarget = element.getAttribute("data-turbo-frame"); const frame = frameTarget == "_top" ? null : document.getElementById(frameTarget) || findClosestRecursively(element, "turbo-frame:not([disabled])"); if (isUnsafe || isStream || frame instanceof FrameElement) { return false; } else { const location = new URL(element.href); return this.elementIsNavigatable(element) && locationIsVisitable(location, this.snapshot.rootLocation); } } historyPoppedToLocationWithRestorationIdentifierAndDirection(location, restorationIdentifier, direction) { if (this.enabled) { this.navigator.startVisit(location, restorationIdentifier, { action: "restore", historyChanged: true, direction: direction }); } else { this.adapter.pageInvalidated({ reason: "turbo_disabled" }); } } scrollPositionChanged(position) { this.history.updateRestorationData({ scrollPosition: position }); } willSubmitFormLinkToLocation(link, location) { return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation); } submittedFormLinkToLocation() {} canPrefetchRequestToLocation(link, location) { return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation); } willFollowLinkToLocation(link, location, event) { return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation) && this.applicationAllowsFollowingLinkToLocation(link, location, event); } followedLinkToLocation(link, location) { const action = this.getActionForLink(link); const acceptsStreamResponse = link.hasAttribute("data-turbo-stream"); this.visit(location.href, { action: action, acceptsStreamResponse: acceptsStreamResponse }); } allowsVisitingLocationWithAction(location, action) { return this.locationWithActionIsSamePage(location, action) || this.applicationAllowsVisitingLocation(location); } visitProposedToLocation(location, options) { extendURLWithDeprecatedProperties(location); this.adapter.visitProposedToLocation(location, options); } visitStarted(visit) { if (!visit.acceptsStreamResponse) { markAsBusy(document.documentElement); this.view.markVisitDirection(visit.direction); } extendURLWithDeprecatedProperties(visit.location); if (!visit.silent) { this.notifyApplicationAfterVisitingLocation(visit.location, visit.action); } } visitCompleted(visit) { this.view.unmarkVisitDirection(); clearBusyState(document.documentElement); this.notifyApplicationAfterPageLoad(visit.getTimingMetrics()); } locationWithActionIsSamePage(location, action) { return this.navigator.locationWithActionIsSamePage(location, action); } visitScrolledToSamePageLocation(oldURL, newURL) { this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL); } willSubmitForm(form, submitter) { const action = getAction$1(form, submitter); return this.submissionIsNavigatable(form, submitter) && locationIsVisitable(expandURL(action), this.snapshot.rootLocation); } formSubmitted(form, submitter) { this.navigator.submitForm(form, submitter); } pageBecameInteractive() { this.view.lastRenderedLocation = this.location; this.notifyApplicationAfterPageLoad(); } pageLoaded() { this.history.assumeControlOfScrollRestoration(); } pageWillUnload() { this.history.relinquishControlOfScrollRestoration(); } receivedMessageFromStream(message) { this.renderStreamMessage(message); } viewWillCacheSnapshot() { if (!this.navigator.currentVisit?.silent) { this.notifyApplicationBeforeCachingSnapshot(); } } allowsImmediateRender({element: element}, options) { const event = this.notifyApplicationBeforeRender(element, options); const {defaultPrevented: defaultPrevented, detail: {render: render}} = event; if (this.view.renderer && render) { this.view.renderer.renderElement = render; } return !defaultPrevented; } viewRenderedSnapshot(_snapshot, _isPreview, renderMethod) { this.view.lastRenderedLocation = this.history.location; this.notifyApplicationAfterRender(renderMethod); } preloadOnLoadLinksForView(element) { this.preloader.preloadOnLoadLinksForView(element); } viewInvalidated(reason) { this.adapter.pageInvalidated(reason); } frameLoaded(frame) { this.notifyApplicationAfterFrameLoad(frame); } frameRendered(fetchResponse, frame) { this.notifyApplicationAfterFrameRender(fetchResponse, frame); } applicationAllowsFollowingLinkToLocation(link, location, ev) { const event = this.notifyApplicationAfterClickingLinkToLocation(link, location, ev); return !event.defaultPrevented; } applicationAllowsVisitingLocation(location) { const event = this.notifyApplicationBeforeVisitingLocation(location); return !event.defaultPrevented; } notifyApplicationAfterClickingLinkToLocation(link, location, event) { return dispatch("turbo:click", { target: link, detail: { url: location.href, originalEvent: event }, cancelable: true }); } notifyApplicationBeforeVisitingLocation(location) { return dispatch("turbo:before-visit", { detail: { url: location.href }, cancelable: true }); } notifyApplicationAfterVisitingLocation(location, action) { return dispatch("turbo:visit", { detail: { url: location.href, action: action } }); } notifyApplicationBeforeCachingSnapshot() { return dispatch("turbo:before-cache"); } notifyApplicationBeforeRender(newBody, options) { return dispatch("turbo:before-render", { detail: { newBody: newBody, ...options }, cancelable: true }); } notifyApplicationAfterRender(renderMethod) { return dispatch("turbo:render", { detail: { renderMethod: renderMethod } }); } notifyApplicationAfterPageLoad(timing = {}) { return dispatch("turbo:load", { detail: { url: this.location.href, timing: timing } }); } notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL) { dispatchEvent(new HashChangeEvent("hashchange", { oldURL: oldURL.toString(), newURL: newURL.toString() })); } notifyApplicationAfterFrameLoad(frame) { return dispatch("turbo:frame-load", { target: frame }); } notifyApplicationAfterFrameRender(fetchResponse, frame) { return dispatch("turbo:frame-render", { detail: { fetchResponse: fetchResponse }, target: frame, cancelable: true }); } submissionIsNavigatable(form, submitter) { if (config.forms.mode == "off") { return false; } else { const submitterIsNavigatable = submitter ? this.elementIsNavigatable(submitter) : true; if (config.forms.mode == "optin") { return submitterIsNavigatable && form.closest('[data-turbo="true"]') != null; } else { return submitterIsNavigatable && this.elementIsNavigatable(form); } } } elementIsNavigatable(element) { const container = findClosestRecursively(element, "[data-turbo]"); const withinFrame = findClosestRecursively(element, "turbo-frame"); if (config.drive.enabled || withinFrame) { if (container) { return container.getAttribute("data-turbo") != "false"; } else { return true; } } else { if (container) { return container.getAttribute("data-turbo") == "true"; } else { return false; } } } getActionForLink(link) { return getVisitAction(link) || "advance"; } get snapshot() { return this.view.snapshot; } } function extendURLWithDeprecatedProperties(url) { Object.defineProperties(url, deprecatedLocationPropertyDescriptors); } const deprecatedLocationPropertyDescriptors = { absoluteURL: { get() { return this.toString(); } } }; const session = new Session(recentRequests); const {cache: cache, navigator: navigator$1} = session; function start() { session.start(); } function registerAdapter(adapter) { session.registerAdapter(adapter); } function visit(location, options) { session.visit(location, options); } function connectStreamSource(source) { session.connectStreamSource(source); } function disconnectStreamSource(source) { session.disconnectStreamSource(source); } function renderStreamMessage(message) { session.renderStreamMessage(message); } function clearCache() { console.warn("Please replace `Turbo.clearCache()` with `Turbo.cache.clear()`. The top-level function is deprecated and will be removed in a future version of Turbo.`"); session.clearCache(); } function setProgressBarDelay(delay) { console.warn("Please replace `Turbo.setProgressBarDelay(delay)` with `Turbo.config.drive.progressBarDelay = delay`. The top-level function is deprecated and will be removed in a future version of Turbo.`"); config.drive.progressBarDelay = delay; } function setConfirmMethod(confirmMethod) { console.warn("Please replace `Turbo.setConfirmMethod(confirmMethod)` with `Turbo.config.forms.confirm = confirmMethod`. The top-level function is deprecated and will be removed in a future version of Turbo.`"); config.forms.confirm = confirmMethod; } function setFormMode(mode) { console.warn("Please replace `Turbo.setFormMode(mode)` with `Turbo.config.forms.mode = mode`. The top-level function is deprecated and will be removed in a future version of Turbo.`"); config.forms.mode = mode; } var Turbo = Object.freeze({ __proto__: null, navigator: navigator$1, session: session, cache: cache, PageRenderer: PageRenderer, PageSnapshot: PageSnapshot, FrameRenderer: FrameRenderer, fetch: fetchWithTurboHeaders, config: config, start: start, registerAdapter: registerAdapter, visit: visit, connectStreamSource: connectStreamSource, disconnectStreamSource: disconnectStreamSource, renderStreamMessage: renderStreamMessage, clearCache: clearCache, setProgressBarDelay: setProgressBarDelay, setConfirmMethod: setConfirmMethod, setFormMode: setFormMode }); class TurboFrameMissingError extends Error {} class FrameController { fetchResponseLoaded=_fetchResponse => Promise.resolve(); #currentFetchRequest=null; #resolveVisitPromise=() => {}; #connected=false; #hasBeenLoaded=false; #ignoredAttributes=new Set; #shouldMorphFrame=false; action=null; constructor(element) { this.element = element; this.view = new FrameView(this, this.element); this.appearanceObserver = new AppearanceObserver(this, this.element); this.formLinkClickObserver = new FormLinkClickObserver(this, this.element); this.linkInterceptor = new LinkInterceptor(this, this.element); this.restorationIdentifier = uuid(); this.formSubmitObserver = new FormSubmitObserver(this, this.element); } connect() { if (!this.#connected) { this.#connected = true; if (this.loadingStyle == FrameLoadingStyle.lazy) { this.appearanceObserver.start(); } else { this.#loadSourceURL(); } this.formLinkClickObserver.start(); this.linkInterceptor.start(); this.formSubmitObserver.start(); } } disconnect() { if (this.#connected) { this.#connected = false; this.appearanceObserver.stop(); this.formLinkClickObserver.stop(); this.linkInterceptor.stop(); this.formSubmitObserver.stop(); } } disabledChanged() { if (this.loadingStyle == FrameLoadingStyle.eager) { this.#loadSourceURL(); } } sourceURLChanged() { if (this.#isIgnoringChangesTo("src")) return; if (this.element.isConnected) { this.complete = false; } if (this.loadingStyle == FrameLoadingStyle.eager || this.#hasBeenLoaded) { this.#loadSourceURL(); } } sourceURLReloaded() { const {refresh: refresh, src: src} = this.element; this.#shouldMorphFrame = src && refresh === "morph"; this.element.removeAttribute("complete"); this.element.src = null; this.element.src = src; return this.element.loaded; } loadingStyleChanged() { if (this.loadingStyle == FrameLoadingStyle.lazy) { this.appearanceObserver.start(); } else { this.appearanceObserver.stop(); this.#loadSourceURL(); } } async #loadSourceURL() { if (this.enabled && this.isActive && !this.complete && this.sourceURL) { this.element.loaded = this.#visit(expandURL(this.sourceURL)); this.appearanceObserver.stop(); await this.element.loaded; this.#hasBeenLoaded = true; } } async loadResponse(fetchResponse) { if (fetchResponse.redirected || fetchResponse.succeeded && fetchResponse.isHTML) { this.sourceURL = fetchResponse.response.url; } try { const html = await fetchResponse.responseHTML; if (html) { const document = parseHTMLDocument(html); const pageSnapshot = PageSnapshot.fromDocument(document); if (pageSnapshot.isVisitable) { await this.#loadFrameResponse(fetchResponse, document); } else { await this.#handleUnvisitableFrameResponse(fetchResponse); } } } finally { this.#shouldMorphFrame = false; this.fetchResponseLoaded = () => Promise.resolve(); } } elementAppearedInViewport(element) { this.proposeVisitIfNavigatedWithAction(element, getVisitAction(element)); this.#loadSourceURL(); } willSubmitFormLinkToLocation(link) { return this.#shouldInterceptNavigation(link); } submittedFormLinkToLocation(link, _location, form) { const frame = this.#findFrameElement(link); if (frame) form.setAttribute("data-turbo-frame", frame.id); } shouldInterceptLinkClick(element, _location, _event) { return this.#shouldInterceptNavigation(element); } linkClickIntercepted(element, location) { this.#navigateFrame(element, location); } willSubmitForm(element, submitter) { return element.closest("turbo-frame") == this.element && this.#shouldInterceptNavigation(element, submitter); } formSubmitted(element, submitter) { if (this.formSubmission) { this.formSubmission.stop(); } this.formSubmission = new FormSubmission(this, element, submitter); const {fetchRequest: fetchRequest} = this.formSubmission; this.prepareRequest(fetchRequest); this.formSubmission.start(); } prepareRequest(request) { request.headers["Turbo-Frame"] = this.id; if (this.currentNavigationElement?.hasAttribute("data-turbo-stream")) { request.acceptResponseType(StreamMessage.contentType); } } requestStarted(_request) { markAsBusy(this.element); } requestPreventedHandlingResponse(_request, _response) { this.#resolveVisitPromise(); } async requestSucceededWithResponse(request, response) { await this.loadResponse(response); this.#resolveVisitPromise(); } async requestFailedWithResponse(request, response) { await this.loadResponse(response); this.#resolveVisitPromise(); } requestErrored(request, error) { console.error(error); this.#resolveVisitPromise(); } requestFinished(_request) { clearBusyState(this.element); } formSubmissionStarted({formElement: formElement}) { markAsBusy(formElement, this.#findFrameElement(formElement)); } formSubmissionSucceededWithResponse(formSubmission, response) { const frame = this.#findFrameElement(formSubmission.formElement, formSubmission.submitter); frame.delegate.proposeVisitIfNavigatedWithAction(frame, getVisitAction(formSubmission.submitter, formSubmission.formElement, frame)); frame.delegate.loadResponse(response); if (!formSubmission.isSafe) { session.clearCache(); } } formSubmissionFailedWithResponse(formSubmission, fetchResponse) { this.element.delegate.loadResponse(fetchResponse); session.clearCache(); } formSubmissionErrored(formSubmission, error) { console.error(error); } formSubmissionFinished({formElement: formElement}) { clearBusyState(formElement, this.#findFrameElement(formElement)); } allowsImmediateRender({element: newFrame}, options) { const event = dispatch("turbo:before-frame-render", { target: this.element, detail: { newFrame: newFrame, ...options }, cancelable: true }); const {defaultPrevented: defaultPrevented, detail: {render: render}} = event; if (this.view.renderer && render) { this.view.renderer.renderElement = render; } return !defaultPrevented; } viewRenderedSnapshot(_snapshot, _isPreview, _renderMethod) {} preloadOnLoadLinksForView(element) { session.preloadOnLoadLinksForView(element); } viewInvalidated() {} willRenderFrame(currentElement, _newElement) { this.previousFrameElement = currentElement.cloneNode(true); } visitCachedSnapshot=({element: element}) => { const frame = element.querySelector("#" + this.element.id); if (frame && this.previousFrameElement) { frame.replaceChildren(...this.previousFrameElement.children); } delete this.previousFrameElement; }; async #loadFrameResponse(fetchResponse, document) { const newFrameElement = await this.extractForeignFrameElement(document.body); const rendererClass = this.#shouldMorphFrame ? MorphingFrameRenderer : FrameRenderer; if (newFrameElement) { const snapshot = new Snapshot(newFrameElement); const renderer = new rendererClass(this, this.view.snapshot, snapshot, false, false); if (this.view.renderPromise) await this.view.renderPromise; this.changeHistory(); await this.view.render(renderer); this.complete = true; session.frameRendered(fetchResponse, this.element); session.frameLoaded(this.element); await this.fetchResponseLoaded(fetchResponse); } else if (this.#willHandleFrameMissingFromResponse(fetchResponse)) { this.#handleFrameMissingFromResponse(fetchResponse); } } async #visit(url) { const request = new FetchRequest(this, FetchMethod.get, url, new URLSearchParams, this.element); this.#currentFetchRequest?.cancel(); this.#currentFetchRequest = request; return new Promise((resolve => { this.#resolveVisitPromise = () => { this.#resolveVisitPromise = () => {}; this.#currentFetchRequest = null; resolve(); }; request.perform(); })); } #navigateFrame(element, url, submitter) { const frame = this.#findFrameElement(element, submitter); frame.delegate.proposeVisitIfNavigatedWithAction(frame, getVisitAction(submitter, element, frame)); this.#withCurrentNavigationElement(element, (() => { frame.src = url; })); } proposeVisitIfNavigatedWithAction(frame, action = null) { this.action = action; if (this.action) { const pageSnapshot = PageSnapshot.fromElement(frame).clone(); const {visitCachedSnapshot: visitCachedSnapshot} = frame.delegate; frame.delegate.fetchResponseLoaded = async fetchResponse => { if (frame.src) { const {statusCode: statusCode, redirected: redirected} = fetchResponse; const responseHTML = await fetchResponse.responseHTML; const response = { statusCode: statusCode, redirected: redirected, responseHTML: responseHTML }; const options = { response: response, visitCachedSnapshot: visitCachedSnapshot, willRender: false, updateHistory: false, restorationIdentifier: this.restorationIdentifier, snapshot: pageSnapshot }; if (this.action) options.action = this.action; session.visit(frame.src, options); } }; } } changeHistory() { if (this.action) { const method = getHistoryMethodForAction(this.action); session.history.update(method, expandURL(this.element.src || ""), this.restorationIdentifier); } } async #handleUnvisitableFrameResponse(fetchResponse) { console.warn(`The response (${fetchResponse.statusCode}) from is performing a full page visit due to turbo-visit-control.`); await this.#visitResponse(fetchResponse.response); } #willHandleFrameMissingFromResponse(fetchResponse) { this.element.setAttribute("complete", ""); const response = fetchResponse.response; const visit = async (url, options) => { if (url instanceof Response) { this.#visitResponse(url); } else { session.visit(url, options); } }; const event = dispatch("turbo:frame-missing", { target: this.element, detail: { response: response, visit: visit }, cancelable: true }); return !event.defaultPrevented; } #handleFrameMissingFromResponse(fetchResponse) { this.view.missing(); this.#throwFrameMissingError(fetchResponse); } #throwFrameMissingError(fetchResponse) { const message = `The response (${fetchResponse.statusCode}) did not contain the expected and will be ignored. To perform a full page visit instead, set turbo-visit-control to reload.`; throw new TurboFrameMissingError(message); } async #visitResponse(response) { const wrapped = new FetchResponse(response); const responseHTML = await wrapped.responseHTML; const {location: location, redirected: redirected, statusCode: statusCode} = wrapped; return session.visit(location, { response: { redirected: redirected, statusCode: statusCode, responseHTML: responseHTML } }); } #findFrameElement(element, submitter) { const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target"); return getFrameElementById(id) ?? this.element; } async extractForeignFrameElement(container) { let element; const id = CSS.escape(this.id); try { element = activateElement(container.querySelector(`turbo-frame#${id}`), this.sourceURL); if (element) { return element; } element = activateElement(container.querySelector(`turbo-frame[src][recurse~=${id}]`), this.sourceURL); if (element) { await element.loaded; return await this.extractForeignFrameElement(element); } } catch (error) { console.error(error); return new FrameElement; } return null; } #formActionIsVisitable(form, submitter) { const action = getAction$1(form, submitter); return locationIsVisitable(expandURL(action), this.rootLocation); } #shouldInterceptNavigation(element, submitter) { const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target"); if (element instanceof HTMLFormElement && !this.#formActionIsVisitable(element, submitter)) { return false; } if (!this.enabled || id == "_top") { return false; } if (id) { const frameElement = getFrameElementById(id); if (frameElement) { return !frameElement.disabled; } } if (!session.elementIsNavigatable(element)) { return false; } if (submitter && !session.elementIsNavigatable(submitter)) { return false; } return true; } get id() { return this.element.id; } get enabled() { return !this.element.disabled; } get sourceURL() { if (this.element.src) { return this.element.src; } } set sourceURL(sourceURL) { this.#ignoringChangesToAttribute("src", (() => { this.element.src = sourceURL ?? null; })); } get loadingStyle() { return this.element.loading; } get isLoading() { return this.formSubmission !== undefined || this.#resolveVisitPromise() !== undefined; } get complete() { return this.element.hasAttribute("complete"); } set complete(value) { if (value) { this.element.setAttribute("complete", ""); } else { this.element.removeAttribute("complete"); } } get isActive() { return this.element.isActive && this.#connected; } get rootLocation() { const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`); const root = meta?.content ?? "/"; return expandURL(root); } #isIgnoringChangesTo(attributeName) { return this.#ignoredAttributes.has(attributeName); } #ignoringChangesToAttribute(attributeName, callback) { this.#ignoredAttributes.add(attributeName); callback(); this.#ignoredAttributes.delete(attributeName); } #withCurrentNavigationElement(element, callback) { this.currentNavigationElement = element; callback(); delete this.currentNavigationElement; } } function getFrameElementById(id) { if (id != null) { const element = document.getElementById(id); if (element instanceof FrameElement) { return element; } } } function activateElement(element, currentURL) { if (element) { const src = element.getAttribute("src"); if (src != null && currentURL != null && urlsAreEqual(src, currentURL)) { throw new Error(`Matching element has a source URL which references itself`); } if (element.ownerDocument !== document) { element = document.importNode(element, true); } if (element instanceof FrameElement) { element.connectedCallback(); element.disconnectedCallback(); return element; } } } const StreamActions = { after() { this.targetElements.forEach((e => e.parentElement?.insertBefore(this.templateContent, e.nextSibling))); }, append() { this.removeDuplicateTargetChildren(); this.targetElements.forEach((e => e.append(this.templateContent))); }, before() { this.targetElements.forEach((e => e.parentElement?.insertBefore(this.templateContent, e))); }, prepend() { this.removeDuplicateTargetChildren(); this.targetElements.forEach((e => e.prepend(this.templateContent))); }, remove() { this.targetElements.forEach((e => e.remove())); }, replace() { const method = this.getAttribute("method"); this.targetElements.forEach((targetElement => { if (method === "morph") { morphElements(targetElement, this.templateContent); } else { targetElement.replaceWith(this.templateContent); } })); }, update() { const method = this.getAttribute("method"); this.targetElements.forEach((targetElement => { if (method === "morph") { morphChildren(targetElement, this.templateContent); } else { targetElement.innerHTML = ""; targetElement.append(this.templateContent); } })); }, refresh() { session.refresh(this.baseURI, this.requestId); } }; class StreamElement extends HTMLElement { static async renderElement(newElement) { await newElement.performAction(); } async connectedCallback() { try { await this.render(); } catch (error) { console.error(error); } finally { this.disconnect(); } } async render() { return this.renderPromise ??= (async () => { const event = this.beforeRenderEvent; if (this.dispatchEvent(event)) { await nextRepaint(); await event.detail.render(this); } })(); } disconnect() { try { this.remove(); } catch {} } removeDuplicateTargetChildren() { this.duplicateChildren.forEach((c => c.remove())); } get duplicateChildren() { const existingChildren = this.targetElements.flatMap((e => [ ...e.children ])).filter((c => !!c.id)); const newChildrenIds = [ ...this.templateContent?.children || [] ].filter((c => !!c.id)).map((c => c.id)); return existingChildren.filter((c => newChildrenIds.includes(c.id))); } get performAction() { if (this.action) { const actionFunction = StreamActions[this.action]; if (actionFunction) { return actionFunction; } this.#raise("unknown action"); } this.#raise("action attribute is missing"); } get targetElements() { if (this.target) { return this.targetElementsById; } else if (this.targets) { return this.targetElementsByQuery; } else { this.#raise("target or targets attribute is missing"); } } get templateContent() { return this.templateElement.content.cloneNode(true); } get templateElement() { if (this.firstElementChild === null) { const template = this.ownerDocument.createElement("template"); this.appendChild(template); return template; } else if (this.firstElementChild instanceof HTMLTemplateElement) { return this.firstElementChild; } this.#raise("first child element must be a