UNPKG

@hotwired/turbo-rails

Version:

The speed of a single-page web application without having to write any JavaScript

1,711 lines (1,597 loc) 188 kB
/*! Turbo 8.0.13 Copyright © 2025 37signals LLC */ (function(prototype) { if (typeof prototype.requestSubmit == "function") return; prototype.requestSubmit = function(submitter) { if (submitter) { validateSubmitter(submitter, this); submitter.click(); } else { submitter = document.createElement("input"); submitter.type = "submit"; submitter.hidden = true; this.appendChild(submitter); submitter.click(); this.removeChild(submitter); } }; function validateSubmitter(submitter, form) { submitter instanceof HTMLElement || raise(TypeError, "parameter 1 is not of type 'HTMLElement'"); submitter.type == "submit" || raise(TypeError, "The specified element is not a submit button"); submitter.form == form || raise(DOMException, "The specified element is not owned by this form element", "NotFoundError"); } function raise(errorConstructor, message, name) { throw new errorConstructor("Failed to execute 'requestSubmit' on 'HTMLFormElement': " + message + ".", name); } })(HTMLFormElement.prototype); const submittersByForm = new WeakMap; function findSubmitterFromClickTarget(target) { const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null; const candidate = element ? element.closest("input, button") : null; return candidate?.type == "submit" ? candidate : null; } function clickCaptured(event) { const submitter = findSubmitterFromClickTarget(event.target); if (submitter && submitter.form) { submittersByForm.set(submitter.form, submitter); } } (function() { if ("submitter" in Event.prototype) return; let prototype = window.Event.prototype; if ("SubmitEvent" in window) { const prototypeOfSubmitEvent = window.SubmitEvent.prototype; if (/Apple Computer/.test(navigator.vendor) && !("submitter" in prototypeOfSubmitEvent)) { prototype = prototypeOfSubmitEvent; } else { return; } } addEventListener("click", clickCaptured, true); Object.defineProperty(prototype, "submitter", { get() { if (this.type == "submit" && this.target instanceof HTMLFormElement) { return submittersByForm.get(this.target); } } }); })(); const FrameLoadingStyle = { eager: "eager", lazy: "lazy" }; class FrameElement extends HTMLElement { static delegateConstructor=undefined; loaded=Promise.resolve(); static get observedAttributes() { return [ "disabled", "loading", "src" ]; } constructor() { super(); this.delegate = new FrameElement.delegateConstructor(this); } connectedCallback() { this.delegate.connect(); } disconnectedCallback() { this.delegate.disconnect(); } reload() { return this.delegate.sourceURLReloaded(); } attributeChangedCallback(name) { if (name == "loading") { this.delegate.loadingStyleChanged(); } else if (name == "src") { this.delegate.sourceURLChanged(); } else if (name == "disabled") { this.delegate.disabledChanged(); } } get src() { return this.getAttribute("src"); } set src(value) { if (value) { this.setAttribute("src", value); } else { this.removeAttribute("src"); } } get refresh() { return this.getAttribute("refresh"); } set refresh(value) { if (value) { this.setAttribute("refresh", value); } else { this.removeAttribute("refresh"); } } get shouldReloadWithMorph() { return this.src && this.refresh === "morph"; } get loading() { return frameLoadingStyleFromString(this.getAttribute("loading") || ""); } set loading(value) { if (value) { this.setAttribute("loading", value); } else { this.removeAttribute("loading"); } } get disabled() { return this.hasAttribute("disabled"); } set disabled(value) { if (value) { this.setAttribute("disabled", ""); } else { this.removeAttribute("disabled"); } } get autoscroll() { return this.hasAttribute("autoscroll"); } set autoscroll(value) { if (value) { this.setAttribute("autoscroll", ""); } else { this.removeAttribute("autoscroll"); } } get complete() { return !this.delegate.isLoading; } get isActive() { return this.ownerDocument === document && !this.isPreview; } get isPreview() { return this.ownerDocument?.documentElement?.hasAttribute("data-turbo-preview"); } } function frameLoadingStyleFromString(style) { switch (style.toLowerCase()) { case "lazy": return FrameLoadingStyle.lazy; default: return FrameLoadingStyle.eager; } } const drive = { enabled: true, progressBarDelay: 500, unvisitableExtensions: new Set([ ".7z", ".aac", ".apk", ".avi", ".bmp", ".bz2", ".css", ".csv", ".deb", ".dmg", ".doc", ".docx", ".exe", ".gif", ".gz", ".heic", ".heif", ".ico", ".iso", ".jpeg", ".jpg", ".js", ".json", ".m4a", ".mkv", ".mov", ".mp3", ".mp4", ".mpeg", ".mpg", ".msi", ".ogg", ".ogv", ".pdf", ".pkg", ".png", ".ppt", ".pptx", ".rar", ".rtf", ".svg", ".tar", ".tif", ".tiff", ".txt", ".wav", ".webm", ".webp", ".wma", ".wmv", ".xls", ".xlsx", ".xml", ".zip" ]) }; function activateScriptElement(element) { if (element.getAttribute("data-turbo-eval") == "false") { return element; } else { const createdScriptElement = document.createElement("script"); const cspNonce = getCspNonce(); if (cspNonce) { createdScriptElement.nonce = cspNonce; } createdScriptElement.textContent = element.textContent; createdScriptElement.async = false; copyElementAttributes(createdScriptElement, element); return createdScriptElement; } } function copyElementAttributes(destinationElement, sourceElement) { for (const {name: name, value: value} of sourceElement.attributes) { destinationElement.setAttribute(name, value); } } function createDocumentFragment(html) { const template = document.createElement("template"); template.innerHTML = html; return template.content; } function dispatch(eventName, {target: target, cancelable: cancelable, detail: detail} = {}) { const event = new CustomEvent(eventName, { cancelable: cancelable, bubbles: true, composed: true, detail: detail }); if (target && target.isConnected) { target.dispatchEvent(event); } else { document.documentElement.dispatchEvent(event); } return event; } function cancelEvent(event) { event.preventDefault(); event.stopImmediatePropagation(); } function nextRepaint() { if (document.visibilityState === "hidden") { return nextEventLoopTick(); } else { return nextAnimationFrame(); } } function nextAnimationFrame() { return new Promise((resolve => requestAnimationFrame((() => resolve())))); } function nextEventLoopTick() { return new Promise((resolve => setTimeout((() => resolve()), 0))); } function nextMicrotask() { return Promise.resolve(); } function parseHTMLDocument(html = "") { return (new DOMParser).parseFromString(html, "text/html"); } function unindent(strings, ...values) { const lines = interpolate(strings, values).replace(/^\n/, "").split("\n"); const match = lines[0].match(/^\s+/); const indent = match ? match[0].length : 0; return lines.map((line => line.slice(indent))).join("\n"); } function interpolate(strings, values) { return strings.reduce(((result, string, i) => { const value = values[i] == undefined ? "" : values[i]; return result + string + value; }), ""); } function uuid() { return Array.from({ length: 36 }).map(((_, i) => { if (i == 8 || i == 13 || i == 18 || i == 23) { return "-"; } else if (i == 14) { return "4"; } else if (i == 19) { return (Math.floor(Math.random() * 4) + 8).toString(16); } else { return Math.floor(Math.random() * 15).toString(16); } })).join(""); } function getAttribute(attributeName, ...elements) { for (const value of elements.map((element => element?.getAttribute(attributeName)))) { if (typeof value == "string") return value; } return null; } function hasAttribute(attributeName, ...elements) { return elements.some((element => element && element.hasAttribute(attributeName))); } function markAsBusy(...elements) { for (const element of elements) { if (element.localName == "turbo-frame") { element.setAttribute("busy", ""); } element.setAttribute("aria-busy", "true"); } } function clearBusyState(...elements) { for (const element of elements) { if (element.localName == "turbo-frame") { element.removeAttribute("busy"); } element.removeAttribute("aria-busy"); } } function waitForLoad(element, timeoutInMilliseconds = 2e3) { return new Promise((resolve => { const onComplete = () => { element.removeEventListener("error", onComplete); element.removeEventListener("load", onComplete); resolve(); }; element.addEventListener("load", onComplete, { once: true }); element.addEventListener("error", onComplete, { once: true }); setTimeout(resolve, timeoutInMilliseconds); })); } function getHistoryMethodForAction(action) { switch (action) { case "replace": return history.replaceState; case "advance": case "restore": return history.pushState; } } function isAction(action) { return action == "advance" || action == "replace" || action == "restore"; } function getVisitAction(...elements) { const action = getAttribute("data-turbo-action", ...elements); return isAction(action) ? action : null; } function getMetaElement(name) { return document.querySelector(`meta[name="${name}"]`); } function getMetaContent(name) { const element = getMetaElement(name); return element && element.content; } function getCspNonce() { const element = getMetaElement("csp-nonce"); if (element) { const {nonce: nonce, content: content} = element; return nonce == "" ? content : nonce; } } function setMetaContent(name, content) { let element = getMetaElement(name); if (!element) { element = document.createElement("meta"); element.setAttribute("name", name); document.head.appendChild(element); } element.setAttribute("content", content); return element; } function findClosestRecursively(element, selector) { if (element instanceof Element) { return element.closest(selector) || findClosestRecursively(element.assignedSlot || element.getRootNode()?.host, selector); } } function elementIsFocusable(element) { const inertDisabledOrHidden = "[inert], :disabled, [hidden], details:not([open]), dialog:not([open])"; return !!element && element.closest(inertDisabledOrHidden) == null && typeof element.focus == "function"; } function queryAutofocusableElement(elementOrDocumentFragment) { return Array.from(elementOrDocumentFragment.querySelectorAll("[autofocus]")).find(elementIsFocusable); } async function around(callback, reader) { const before = reader(); callback(); await nextAnimationFrame(); const after = reader(); return [ before, after ]; } function doesNotTargetIFrame(name) { if (name === "_blank") { return false; } else if (name) { for (const element of document.getElementsByName(name)) { if (element instanceof HTMLIFrameElement) return false; } return true; } else { return true; } } function findLinkFromClickTarget(target) { 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 = `<strong class="turbo-frame-error">Content missing</strong>`; } get snapshot() { return new Snapshot(this.element); } } class LinkInterceptor { constructor(delegate, element) { this.delegate = delegate; this.element = element; } start() { this.element.addEventListener("click", this.clickBubbled); document.addEventListener("turbo:click", this.linkClicked); document.addEventListener("turbo:before-visit", this.willVisit); } stop() { this.element.removeEventListener("click", this.clickBubbled); document.removeEventListener("turbo:click", this.linkClicked); document.removeEventListener("turbo:before-visit", this.willVisit); } clickBubbled=event => { if (this.clickEventIsSignificant(event)) { this.clickEvent = event; } else { delete this.clickEvent; } }; linkClicked=event => { if (this.clickEvent && this.clickEventIsSignificant(event)) { if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url, event.detail.originalEvent)) { this.clickEvent.preventDefault(); event.preventDefault(); this.delegate.linkClickIntercepted(event.target, event.detail.url, event.detail.originalEvent); } } delete this.clickEvent; }; willVisit=_event => { delete this.clickEvent; }; clickEventIsSignificant(event) { const target = event.composed ? event.target?.parentElement : event.target; const element = findLinkFromClickTarget(target) || target; return element instanceof Element && element.closest("turbo-frame, html") == this.element; } } class LinkClickObserver { started=false; constructor(delegate, eventTarget) { this.delegate = delegate; this.eventTarget = eventTarget; } start() { if (!this.started) { this.eventTarget.addEventListener("click", this.clickCaptured, true); this.started = true; } } stop() { if (this.started) { this.eventTarget.removeEventListener("click", this.clickCaptured, true); this.started = false; } } clickCaptured=() => { this.eventTarget.removeEventListener("click", this.clickBubbled, false); this.eventTarget.addEventListener("click", this.clickBubbled, false); }; clickBubbled=event => { if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) { const target = event.composedPath && event.composedPath()[0] || event.target; const link = findLinkFromClickTarget(target); if (link && doesNotTargetIFrame(link.target)) { const location = getLocationForLink(link); if (this.delegate.willFollowLinkToLocation(link, location, event)) { event.preventDefault(); this.delegate.followedLinkToLocation(link, location); } } } }; clickEventIsSignificant(event) { return !(event.target && event.target.isContentEditable || event.defaultPrevented || event.which > 1 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey); } } class FormLinkClickObserver { constructor(delegate, element) { this.delegate = delegate; this.linkInterceptor = new LinkClickObserver(this, element); } start() { this.linkInterceptor.start(); } stop() { this.linkInterceptor.stop(); } canPrefetchRequestToLocation(link, location) { return false; } prefetchAndCacheRequestToLocation(link, location) { return; } willFollowLinkToLocation(link, location, originalEvent) { return this.delegate.willSubmitFormLinkToLocation(link, location, originalEvent) && (link.hasAttribute("data-turbo-method") || link.hasAttribute("data-turbo-stream")); } followedLinkToLocation(link, location) { const form = document.createElement("form"); const type = "hidden"; for (const [name, value] of location.searchParams) { form.append(Object.assign(document.createElement("input"), { type: type, name: name, value: value })); } const action = Object.assign(location, { search: "" }); form.setAttribute("data-turbo", "true"); form.setAttribute("action", action.href); form.setAttribute("hidden", ""); const method = link.getAttribute("data-turbo-method"); if (method) form.setAttribute("method", method); const turboFrame = link.getAttribute("data-turbo-frame"); if (turboFrame) form.setAttribute("data-turbo-frame", turboFrame); const turboAction = getVisitAction(link); if (turboAction) form.setAttribute("data-turbo-action", turboAction); const turboConfirm = link.getAttribute("data-turbo-confirm"); if (turboConfirm) form.setAttribute("data-turbo-confirm", turboConfirm); const turboStream = link.hasAttribute("data-turbo-stream"); if (turboStream) form.setAttribute("data-turbo-stream", ""); this.delegate.submittedFormLinkToLocation(link, location, form); document.body.appendChild(form); form.addEventListener("turbo:submit-end", (() => form.remove()), { once: true }); requestAnimationFrame((() => form.requestSubmit())); } } class Bardo { static async preservingPermanentElements(delegate, permanentElementMap, callback) { const bardo = new this(delegate, permanentElementMap); bardo.enter(); await callback(); bardo.leave(); } constructor(delegate, permanentElementMap) { this.delegate = delegate; this.permanentElementMap = permanentElementMap; } enter() { for (const id in this.permanentElementMap) { const [currentPermanentElement, newPermanentElement] = this.permanentElementMap[id]; this.delegate.enteringBardo(currentPermanentElement, newPermanentElement); this.replaceNewPermanentElementWithPlaceholder(newPermanentElement); } } leave() { for (const id in this.permanentElementMap) { const [currentPermanentElement] = this.permanentElementMap[id]; this.replaceCurrentPermanentElementWithClone(currentPermanentElement); this.replacePlaceholderWithPermanentElement(currentPermanentElement); this.delegate.leavingBardo(currentPermanentElement); } } replaceNewPermanentElementWithPlaceholder(permanentElement) { const placeholder = createPlaceholderForPermanentElement(permanentElement); permanentElement.replaceWith(placeholder); } replaceCurrentPermanentElementWithClone(permanentElement) { const clone = permanentElement.cloneNode(true); permanentElement.replaceWith(clone); } replacePlaceholderWithPermanentElement(permanentElement) { const placeholder = this.getPlaceholderById(permanentElement.id); placeholder?.replaceWith(permanentElement); } getPlaceholderById(id) { return this.placeholders.find((element => element.content == id)); } get placeholders() { return [ ...document.querySelectorAll("meta[name=turbo-permanent-placeholder][content]") ]; } } function createPlaceholderForPermanentElement(permanentElement) { const element = document.createElement("meta"); element.setAttribute("name", "turbo-permanent-placeholder"); element.setAttribute("content", permanentElement.id); return element; } class Renderer { #activeElement=null; static renderElement(currentElement, newElement) {} constructor(currentSnapshot, newSnapshot, isPreview, willRender = true) { this.currentSnapshot = currentSnapshot; this.newSnapshot = newSnapshot; this.isPreview = isPreview; this.willRender = willRender; this.renderElement = this.constructor.renderElement; this.promise = new Promise(((resolve, reject) => this.resolvingFunctions = { resolve: resolve, reject: reject })); } get shouldRender() { return true; } get shouldAutofocus() { return true; } get reloadReason() { return; } prepareToRender() { return; } render() {} finishRendering() { if (this.resolvingFunctions) { this.resolvingFunctions.resolve(); delete this.resolvingFunctions; } } async preservingPermanentElements(callback) { await Bardo.preservingPermanentElements(this, this.permanentElementMap, callback); } focusFirstAutofocusableElement() { if (this.shouldAutofocus) { const element = this.connectedSnapshot.firstAutofocusableElement; if (element) { element.focus(); } } } enteringBardo(currentPermanentElement) { if (this.#activeElement) return; if (currentPermanentElement.contains(this.currentSnapshot.activeElement)) { this.#activeElement = this.currentSnapshot.activeElement; } } leavingBardo(currentPermanentElement) { if (currentPermanentElement.contains(this.#activeElement) && this.#activeElement instanceof HTMLElement) { this.#activeElement.focus(); this.#activeElement = null; } } get connectedSnapshot() { return this.newSnapshot.isConnected ? this.newSnapshot : this.currentSnapshot; } get currentElement() { return this.currentSnapshot.element; } get newElement() { return this.newSnapshot.element; } get permanentElementMap() { return this.currentSnapshot.getPermanentElementMapForSnapshot(this.newSnapshot); } get renderMethod() { return "replace"; } } class FrameRenderer extends Renderer { static renderElement(currentElement, newElement) { const destinationRange = document.createRange(); destinationRange.selectNodeContents(currentElement); destinationRange.deleteContents(); const frameElement = newElement; const sourceRange = frameElement.ownerDocument?.createRange(); if (sourceRange) { sourceRange.selectNodeContents(frameElement); currentElement.appendChild(sourceRange.extractContents()); } } constructor(delegate, currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) { super(currentSnapshot, newSnapshot, renderElement, isPreview, willRender); this.delegate = delegate; } get shouldRender() { return true; } async render() { await nextRepaint(); this.preservingPermanentElements((() => { this.loadFrameElement(); })); this.scrollFrameIntoView(); await nextRepaint(); this.focusFirstAutofocusableElement(); await nextRepaint(); this.activateScriptElements(); } loadFrameElement() { this.delegate.willRenderFrame(this.currentElement, this.newElement); this.renderElement(this.currentElement, this.newElement); } scrollFrameIntoView() { if (this.currentElement.autoscroll || this.newElement.autoscroll) { const element = this.currentElement.firstElementChild; const block = readScrollLogicalPosition(this.currentElement.getAttribute("data-autoscroll-block"), "end"); const behavior = readScrollBehavior(this.currentElement.getAttribute("data-autoscroll-behavior"), "auto"); if (element) { element.scrollIntoView({ block: block, behavior: behavior }); return true; } } return false; } activateScriptElements() { for (const inertScriptElement of this.newScriptElements) { const activatedScriptElement = activateScriptElement(inertScriptElement); inertScriptElement.replaceWith(activatedScriptElement); } } get newScriptElements() { return this.currentElement.querySelectorAll("script"); } } function readScrollLogicalPosition(value, defaultValue) { if (value == "end" || value == "start" || value == "center" || value == "nearest") { return value; } else { return defaultValue; } } function readScrollBehavior(value, defaultValue) { if (value == "auto" || value == "smooth") { return value; } else { return defaultValue; } } var Idiomorph = function() { const noOp = () => {}; const defaults = { morphStyle: "outerHTML", callbacks: { beforeNodeAdded: noOp, afterNodeAdded: noOp, beforeNodeMorphed: noOp, afterNodeMorphed: noOp, beforeNodeRemoved: noOp, afterNodeRemoved: noOp, beforeAttributeUpdated: noOp }, head: { style: "merge", shouldPreserve: elt => elt.getAttribute("im-preserve") === "true", shouldReAppend: elt => elt.getAttribute("im-re-append") === "true", shouldRemove: noOp, afterHeadMorphed: noOp }, restoreFocus: true }; f