From 33d1b1c3a3cba73f65e38ae03fa7ee1a0fc78272 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 12 Apr 2024 10:19:25 +0200 Subject: [PATCH 01/15] add turbo --- annotations/build.gradle.kts | 1 + .../projectcheckins/annotations/GetHtml.java | 5 + .../assets/javascripts/turbo8.0.4/turbo.js | 5864 +++++++++++++++++ .../javascripts/turbo8.0.4/turbo.min.js | 29 + .../javascripts/turbo8.0.4/turbo.min.js.map | 1 + .../src/main/resources/views/answer/edit.html | 2 +- http/src/main/resources/views/layout.html | 5 +- .../main/resources/views/profile/edit.html | 2 +- .../main/resources/views/question/show.html | 2 +- .../main/resources/views/team/_create.html | 2 +- .../src/main/resources/application.properties | 2 +- .../org/projectcheckins/assets/TurboTest.java | 21 + .../processor/PostFormAnnotationMapper.java | 5 +- .../PostFormAnnotationMapperTest.java | 3 +- .../security/http/SecurityController.java | 3 +- .../main/resources/views/security/login.html | 2 +- .../main/resources/views/security/signUp.html | 2 +- .../main/resources/views/fieldset/form.html | 7 +- 18 files changed, 5945 insertions(+), 13 deletions(-) create mode 100644 assets/src/main/resources/assets/javascripts/turbo8.0.4/turbo.js create mode 100644 assets/src/main/resources/assets/javascripts/turbo8.0.4/turbo.min.js create mode 100644 assets/src/main/resources/assets/javascripts/turbo8.0.4/turbo.min.js.map create mode 100644 netty/src/test/java/org/projectcheckins/assets/TurboTest.java diff --git a/annotations/build.gradle.kts b/annotations/build.gradle.kts index ad90b739..8f8d0c10 100644 --- a/annotations/build.gradle.kts +++ b/annotations/build.gradle.kts @@ -3,4 +3,5 @@ plugins { } dependencies { api("io.micronaut:micronaut-http") + api("io.micronaut.views:micronaut-views-core") } \ No newline at end of file diff --git a/annotations/src/main/java/org/projectcheckins/annotations/GetHtml.java b/annotations/src/main/java/org/projectcheckins/annotations/GetHtml.java index a43563e6..3bec9742 100644 --- a/annotations/src/main/java/org/projectcheckins/annotations/GetHtml.java +++ b/annotations/src/main/java/org/projectcheckins/annotations/GetHtml.java @@ -4,6 +4,7 @@ import io.micronaut.scheduling.TaskExecutors; import java.lang.annotation.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; +import io.micronaut.views.turbo.TurboStreamAction; @Documented @Retention(RUNTIME) @@ -19,4 +20,8 @@ boolean hidden() default true; String executesOn() default TaskExecutors.BLOCKING; + + TurboStreamAction turboAction() default TurboStreamAction.UPDATE; + + String turboView() default ""; } diff --git a/assets/src/main/resources/assets/javascripts/turbo8.0.4/turbo.js b/assets/src/main/resources/assets/javascripts/turbo8.0.4/turbo.js new file mode 100644 index 00000000..9bd18201 --- /dev/null +++ b/assets/src/main/resources/assets/javascripts/turbo8.0.4/turbo.js @@ -0,0 +1,5864 @@ +/*! +Turbo 8.0.4 +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 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; + } +} + +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 isHTML(url) { + return !!getExtension(url).match(/^(?:|\.(?:htm|html|xhtml|php))$/); +} + +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) && isHTML(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); + } +} + +function activateScriptElement(element) { + if (element.getAttribute("data-turbo-eval") == "false") { + return element; + } else { + const createdScriptElement = document.createElement("script"); + const cspNonce = getMetaContent("csp-nonce"); + 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 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 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(anchor) { + if (anchor.hasAttribute("target")) { + for (const element of document.getElementsByName(anchor.target)) { + if (element instanceof HTMLIFrameElement) return false; + } + } + 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); + }; +} + +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, + 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; + } + 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, _element, _submitter) { + 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 answer = await FormSubmission.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; + this.submitter?.setAttribute("disabled", ""); + 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; + this.submitter?.removeAttribute("disabled"); + 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) { + if (submitter?.hasAttribute("formtarget") || form.hasAttribute("target")) { + const target = submitter?.getAttribute("formtarget") || form.target; + for (const element of document.getElementsByName(target)) { + if (element instanceof HTMLIFrameElement) return false; + } + return true; + } else { + return true; + } +} + +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.respondsToEventTarget(event.target)) { + this.clickEvent = event; + } else { + delete this.clickEvent; + } + }; + linkClicked=event => { + if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) { + 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; + }; + respondsToEventTarget(target) { + const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null; + return 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)) { + 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; + constructor(currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) { + this.currentSnapshot = currentSnapshot; + this.newSnapshot = newSnapshot; + this.isPreview = isPreview; + this.willRender = willRender; + this.renderElement = renderElement; + this.promise = new Promise(((resolve, reject) => this.resolvingFunctions = { + resolve: resolve, + reject: reject + })); + } + get shouldRender() { + 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() { + 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; + } +} + +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; + if (this.cspNonce) { + element.nonce = this.cspNonce; + } + return element; + } + createProgressElement() { + const element = document.createElement("div"); + element.className = "turbo-progress-bar"; + return element; + } + get cspNonce() { + return getMetaContent("csp-nonce"); + } +} + +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(); + this.frame = await nextRepaint(); + 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); + } + 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"); + } +} + +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 + }; +}(); + +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 MorphRenderer extends PageRenderer { + async render() { + if (this.willRender) await this.#morphBody(); + } + get renderMethod() { + return "morph"; + } + async #morphBody() { + this.#morphElements(this.currentElement, this.newElement); + this.#reloadRemoteFrames(); + dispatch("turbo:morph", { + detail: { + currentElement: this.currentElement, + newElement: this.newElement + } + }); + } + #morphElements(currentElement, newElement, morphStyle = "outerHTML") { + this.isMorphingTurboFrame = this.#isFrameReloadedWithMorph(currentElement); + Idiomorph.morph(currentElement, newElement, { + morphStyle: morphStyle, + callbacks: { + beforeNodeAdded: this.#shouldAddElement, + beforeNodeMorphed: this.#shouldMorphElement, + beforeAttributeUpdated: this.#shouldUpdateAttribute, + beforeNodeRemoved: this.#shouldRemoveElement, + afterNodeMorphed: this.#didMorphElement + } + }); + } + #shouldAddElement=node => !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id)); + #shouldMorphElement=(oldNode, newNode) => { + if (oldNode instanceof HTMLElement) { + if (!oldNode.hasAttribute("data-turbo-permanent") && (this.isMorphingTurboFrame || !this.#isFrameReloadedWithMorph(oldNode))) { + const event = dispatch("turbo:before-morph-element", { + cancelable: true, + target: oldNode, + detail: { + newElement: newNode + } + }); + return !event.defaultPrevented; + } else { + return false; + } + } + }; + #shouldUpdateAttribute=(attributeName, target, mutationType) => { + const event = dispatch("turbo:before-morph-attribute", { + cancelable: true, + target: target, + detail: { + attributeName: attributeName, + mutationType: mutationType + } + }); + return !event.defaultPrevented; + }; + #didMorphElement=(oldNode, newNode) => { + if (newNode instanceof HTMLElement) { + dispatch("turbo:morph-element", { + target: oldNode, + detail: { + newElement: newNode + } + }); + } + }; + #shouldRemoveElement=node => this.#shouldMorphElement(node); + #reloadRemoteFrames() { + this.#remoteFrames().forEach((frame => { + if (this.#isFrameReloadedWithMorph(frame)) { + this.#renderFrameWithMorph(frame); + frame.reload(); + } + })); + } + #renderFrameWithMorph(frame) { + frame.addEventListener("turbo:before-frame-render", (event => { + event.detail.render = this.#morphFrameUpdate; + }), { + once: true + }); + } + #morphFrameUpdate=(currentElement, newElement) => { + dispatch("turbo:before-frame-morph", { + target: currentElement, + detail: { + currentElement: currentElement, + newElement: newElement + } + }); + this.#morphElements(currentElement, newElement.children, "innerHTML"); + }; + #isFrameReloadedWithMorph(element) { + return element.src && element.refresh === "morph"; + } + #remoteFrames() { + return Array.from(document.querySelectorAll("turbo-frame[src]")).filter((frame => !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 ? MorphRenderer : PageRenderer; + const renderer = new rendererClass(this.snapshot, snapshot, PageRenderer.renderElement, 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, ErrorRenderer.renderElement, 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); + drive=true; + enabled=true; + progressBarDelay=500; + started=false; + formMode="on"; + #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.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) { + this.progressBarDelay = delay; + } + setFormMode(mode) { + this.formMode = 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 (this.formMode == "off") { + return false; + } else { + const submitterIsNavigatable = submitter ? this.elementIsNavigatable(submitter) : true; + if (this.formMode == "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 (this.drive || 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) { + session.setProgressBarDelay(delay); +} + +function setConfirmMethod(confirmMethod) { + FormSubmission.confirmMethod = confirmMethod; +} + +function setFormMode(mode) { + session.setFormMode(mode); +} + +var Turbo = Object.freeze({ + __proto__: null, + navigator: navigator$1, + session: session, + cache: cache, + PageRenderer: PageRenderer, + PageSnapshot: PageSnapshot, + FrameRenderer: FrameRenderer, + fetch: fetchWithTurboHeaders, + 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; + 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 {src: src} = this.element; + 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.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); + if (newFrameElement) { + const snapshot = new Snapshot(newFrameElement); + const renderer = new FrameRenderer(this, this.view.snapshot, snapshot, FrameRenderer.renderElement, 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() { + this.targetElements.forEach((e => e.replaceWith(this.templateContent))); + }, + update() { + this.targetElements.forEach((targetElement => { + 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