UNPKG

199 kBJavaScriptView Raw
1/*!
2Turbo 8.0.4
3Copyright © 2024 37signals LLC
4 */
5(function (global, factory) {
6 typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
7 typeof define === 'function' && define.amd ? define(['exports'], factory) :
8 (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Turbo = {}));
9})(this, (function (exports) { 'use strict';
10
11 /**
12 * The MIT License (MIT)
13 *
14 * Copyright (c) 2019 Javan Makhmali
15 *
16 * Permission is hereby granted, free of charge, to any person obtaining a copy
17 * of this software and associated documentation files (the "Software"), to deal
18 * in the Software without restriction, including without limitation the rights
19 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
20 * copies of the Software, and to permit persons to whom the Software is
21 * furnished to do so, subject to the following conditions:
22 *
23 * The above copyright notice and this permission notice shall be included in
24 * all copies or substantial portions of the Software.
25 *
26 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
27 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
28 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
29 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
30 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
31 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
32 * THE SOFTWARE.
33 */
34
35 (function (prototype) {
36 if (typeof prototype.requestSubmit == "function") return
37
38 prototype.requestSubmit = function (submitter) {
39 if (submitter) {
40 validateSubmitter(submitter, this);
41 submitter.click();
42 } else {
43 submitter = document.createElement("input");
44 submitter.type = "submit";
45 submitter.hidden = true;
46 this.appendChild(submitter);
47 submitter.click();
48 this.removeChild(submitter);
49 }
50 };
51
52 function validateSubmitter(submitter, form) {
53 submitter instanceof HTMLElement || raise(TypeError, "parameter 1 is not of type 'HTMLElement'");
54 submitter.type == "submit" || raise(TypeError, "The specified element is not a submit button");
55 submitter.form == form ||
56 raise(DOMException, "The specified element is not owned by this form element", "NotFoundError");
57 }
58
59 function raise(errorConstructor, message, name) {
60 throw new errorConstructor("Failed to execute 'requestSubmit' on 'HTMLFormElement': " + message + ".", name)
61 }
62 })(HTMLFormElement.prototype);
63
64 const submittersByForm = new WeakMap();
65
66 function findSubmitterFromClickTarget(target) {
67 const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
68 const candidate = element ? element.closest("input, button") : null;
69 return candidate?.type == "submit" ? candidate : null
70 }
71
72 function clickCaptured(event) {
73 const submitter = findSubmitterFromClickTarget(event.target);
74
75 if (submitter && submitter.form) {
76 submittersByForm.set(submitter.form, submitter);
77 }
78 }
79
80 (function () {
81 if ("submitter" in Event.prototype) return
82
83 let prototype = window.Event.prototype;
84 // Certain versions of Safari 15 have a bug where they won't
85 // populate the submitter. This hurts TurboDrive's enable/disable detection.
86 // See https://bugs.webkit.org/show_bug.cgi?id=229660
87 if ("SubmitEvent" in window) {
88 const prototypeOfSubmitEvent = window.SubmitEvent.prototype;
89
90 if (/Apple Computer/.test(navigator.vendor) && !("submitter" in prototypeOfSubmitEvent)) {
91 prototype = prototypeOfSubmitEvent;
92 } else {
93 return // polyfill not needed
94 }
95 }
96
97 addEventListener("click", clickCaptured, true);
98
99 Object.defineProperty(prototype, "submitter", {
100 get() {
101 if (this.type == "submit" && this.target instanceof HTMLFormElement) {
102 return submittersByForm.get(this.target)
103 }
104 }
105 });
106 })();
107
108 const FrameLoadingStyle = {
109 eager: "eager",
110 lazy: "lazy"
111 };
112
113 /**
114 * Contains a fragment of HTML which is updated based on navigation within
115 * it (e.g. via links or form submissions).
116 *
117 * @customElement turbo-frame
118 * @example
119 * <turbo-frame id="messages">
120 * <a href="/messages/expanded">
121 * Show all expanded messages in this frame.
122 * </a>
123 *
124 * <form action="/messages">
125 * Show response from this form within this frame.
126 * </form>
127 * </turbo-frame>
128 */
129 class FrameElement extends HTMLElement {
130 static delegateConstructor = undefined
131
132 loaded = Promise.resolve()
133
134 static get observedAttributes() {
135 return ["disabled", "loading", "src"]
136 }
137
138 constructor() {
139 super();
140 this.delegate = new FrameElement.delegateConstructor(this);
141 }
142
143 connectedCallback() {
144 this.delegate.connect();
145 }
146
147 disconnectedCallback() {
148 this.delegate.disconnect();
149 }
150
151 reload() {
152 return this.delegate.sourceURLReloaded()
153 }
154
155 attributeChangedCallback(name) {
156 if (name == "loading") {
157 this.delegate.loadingStyleChanged();
158 } else if (name == "src") {
159 this.delegate.sourceURLChanged();
160 } else if (name == "disabled") {
161 this.delegate.disabledChanged();
162 }
163 }
164
165 /**
166 * Gets the URL to lazily load source HTML from
167 */
168 get src() {
169 return this.getAttribute("src")
170 }
171
172 /**
173 * Sets the URL to lazily load source HTML from
174 */
175 set src(value) {
176 if (value) {
177 this.setAttribute("src", value);
178 } else {
179 this.removeAttribute("src");
180 }
181 }
182
183 /**
184 * Gets the refresh mode for the frame.
185 */
186 get refresh() {
187 return this.getAttribute("refresh")
188 }
189
190 /**
191 * Sets the refresh mode for the frame.
192 */
193 set refresh(value) {
194 if (value) {
195 this.setAttribute("refresh", value);
196 } else {
197 this.removeAttribute("refresh");
198 }
199 }
200
201 /**
202 * Determines if the element is loading
203 */
204 get loading() {
205 return frameLoadingStyleFromString(this.getAttribute("loading") || "")
206 }
207
208 /**
209 * Sets the value of if the element is loading
210 */
211 set loading(value) {
212 if (value) {
213 this.setAttribute("loading", value);
214 } else {
215 this.removeAttribute("loading");
216 }
217 }
218
219 /**
220 * Gets the disabled state of the frame.
221 *
222 * If disabled, no requests will be intercepted by the frame.
223 */
224 get disabled() {
225 return this.hasAttribute("disabled")
226 }
227
228 /**
229 * Sets the disabled state of the frame.
230 *
231 * If disabled, no requests will be intercepted by the frame.
232 */
233 set disabled(value) {
234 if (value) {
235 this.setAttribute("disabled", "");
236 } else {
237 this.removeAttribute("disabled");
238 }
239 }
240
241 /**
242 * Gets the autoscroll state of the frame.
243 *
244 * If true, the frame will be scrolled into view automatically on update.
245 */
246 get autoscroll() {
247 return this.hasAttribute("autoscroll")
248 }
249
250 /**
251 * Sets the autoscroll state of the frame.
252 *
253 * If true, the frame will be scrolled into view automatically on update.
254 */
255 set autoscroll(value) {
256 if (value) {
257 this.setAttribute("autoscroll", "");
258 } else {
259 this.removeAttribute("autoscroll");
260 }
261 }
262
263 /**
264 * Determines if the element has finished loading
265 */
266 get complete() {
267 return !this.delegate.isLoading
268 }
269
270 /**
271 * Gets the active state of the frame.
272 *
273 * If inactive, source changes will not be observed.
274 */
275 get isActive() {
276 return this.ownerDocument === document && !this.isPreview
277 }
278
279 /**
280 * Sets the active state of the frame.
281 *
282 * If inactive, source changes will not be observed.
283 */
284 get isPreview() {
285 return this.ownerDocument?.documentElement?.hasAttribute("data-turbo-preview")
286 }
287 }
288
289 function frameLoadingStyleFromString(style) {
290 switch (style.toLowerCase()) {
291 case "lazy":
292 return FrameLoadingStyle.lazy
293 default:
294 return FrameLoadingStyle.eager
295 }
296 }
297
298 function expandURL(locatable) {
299 return new URL(locatable.toString(), document.baseURI)
300 }
301
302 function getAnchor(url) {
303 let anchorMatch;
304 if (url.hash) {
305 return url.hash.slice(1)
306 // eslint-disable-next-line no-cond-assign
307 } else if ((anchorMatch = url.href.match(/#(.*)$/))) {
308 return anchorMatch[1]
309 }
310 }
311
312 function getAction$1(form, submitter) {
313 const action = submitter?.getAttribute("formaction") || form.getAttribute("action") || form.action;
314
315 return expandURL(action)
316 }
317
318 function getExtension(url) {
319 return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || ""
320 }
321
322 function isHTML(url) {
323 return !!getExtension(url).match(/^(?:|\.(?:htm|html|xhtml|php))$/)
324 }
325
326 function isPrefixedBy(baseURL, url) {
327 const prefix = getPrefix(url);
328 return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix)
329 }
330
331 function locationIsVisitable(location, rootLocation) {
332 return isPrefixedBy(location, rootLocation) && isHTML(location)
333 }
334
335 function getRequestURL(url) {
336 const anchor = getAnchor(url);
337 return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href
338 }
339
340 function toCacheKey(url) {
341 return getRequestURL(url)
342 }
343
344 function urlsAreEqual(left, right) {
345 return expandURL(left).href == expandURL(right).href
346 }
347
348 function getPathComponents(url) {
349 return url.pathname.split("/").slice(1)
350 }
351
352 function getLastPathComponent(url) {
353 return getPathComponents(url).slice(-1)[0]
354 }
355
356 function getPrefix(url) {
357 return addTrailingSlash(url.origin + url.pathname)
358 }
359
360 function addTrailingSlash(value) {
361 return value.endsWith("/") ? value : value + "/"
362 }
363
364 class FetchResponse {
365 constructor(response) {
366 this.response = response;
367 }
368
369 get succeeded() {
370 return this.response.ok
371 }
372
373 get failed() {
374 return !this.succeeded
375 }
376
377 get clientError() {
378 return this.statusCode >= 400 && this.statusCode <= 499
379 }
380
381 get serverError() {
382 return this.statusCode >= 500 && this.statusCode <= 599
383 }
384
385 get redirected() {
386 return this.response.redirected
387 }
388
389 get location() {
390 return expandURL(this.response.url)
391 }
392
393 get isHTML() {
394 return this.contentType && this.contentType.match(/^(?:text\/([^\s;,]+\b)?html|application\/xhtml\+xml)\b/)
395 }
396
397 get statusCode() {
398 return this.response.status
399 }
400
401 get contentType() {
402 return this.header("Content-Type")
403 }
404
405 get responseText() {
406 return this.response.clone().text()
407 }
408
409 get responseHTML() {
410 if (this.isHTML) {
411 return this.response.clone().text()
412 } else {
413 return Promise.resolve(undefined)
414 }
415 }
416
417 header(name) {
418 return this.response.headers.get(name)
419 }
420 }
421
422 function activateScriptElement(element) {
423 if (element.getAttribute("data-turbo-eval") == "false") {
424 return element
425 } else {
426 const createdScriptElement = document.createElement("script");
427 const cspNonce = getMetaContent("csp-nonce");
428 if (cspNonce) {
429 createdScriptElement.nonce = cspNonce;
430 }
431 createdScriptElement.textContent = element.textContent;
432 createdScriptElement.async = false;
433 copyElementAttributes(createdScriptElement, element);
434 return createdScriptElement
435 }
436 }
437
438 function copyElementAttributes(destinationElement, sourceElement) {
439 for (const { name, value } of sourceElement.attributes) {
440 destinationElement.setAttribute(name, value);
441 }
442 }
443
444 function createDocumentFragment(html) {
445 const template = document.createElement("template");
446 template.innerHTML = html;
447 return template.content
448 }
449
450 function dispatch(eventName, { target, cancelable, detail } = {}) {
451 const event = new CustomEvent(eventName, {
452 cancelable,
453 bubbles: true,
454 composed: true,
455 detail
456 });
457
458 if (target && target.isConnected) {
459 target.dispatchEvent(event);
460 } else {
461 document.documentElement.dispatchEvent(event);
462 }
463
464 return event
465 }
466
467 function nextRepaint() {
468 if (document.visibilityState === "hidden") {
469 return nextEventLoopTick()
470 } else {
471 return nextAnimationFrame()
472 }
473 }
474
475 function nextAnimationFrame() {
476 return new Promise((resolve) => requestAnimationFrame(() => resolve()))
477 }
478
479 function nextEventLoopTick() {
480 return new Promise((resolve) => setTimeout(() => resolve(), 0))
481 }
482
483 function nextMicrotask() {
484 return Promise.resolve()
485 }
486
487 function parseHTMLDocument(html = "") {
488 return new DOMParser().parseFromString(html, "text/html")
489 }
490
491 function unindent(strings, ...values) {
492 const lines = interpolate(strings, values).replace(/^\n/, "").split("\n");
493 const match = lines[0].match(/^\s+/);
494 const indent = match ? match[0].length : 0;
495 return lines.map((line) => line.slice(indent)).join("\n")
496 }
497
498 function interpolate(strings, values) {
499 return strings.reduce((result, string, i) => {
500 const value = values[i] == undefined ? "" : values[i];
501 return result + string + value
502 }, "")
503 }
504
505 function uuid() {
506 return Array.from({ length: 36 })
507 .map((_, i) => {
508 if (i == 8 || i == 13 || i == 18 || i == 23) {
509 return "-"
510 } else if (i == 14) {
511 return "4"
512 } else if (i == 19) {
513 return (Math.floor(Math.random() * 4) + 8).toString(16)
514 } else {
515 return Math.floor(Math.random() * 15).toString(16)
516 }
517 })
518 .join("")
519 }
520
521 function getAttribute(attributeName, ...elements) {
522 for (const value of elements.map((element) => element?.getAttribute(attributeName))) {
523 if (typeof value == "string") return value
524 }
525
526 return null
527 }
528
529 function hasAttribute(attributeName, ...elements) {
530 return elements.some((element) => element && element.hasAttribute(attributeName))
531 }
532
533 function markAsBusy(...elements) {
534 for (const element of elements) {
535 if (element.localName == "turbo-frame") {
536 element.setAttribute("busy", "");
537 }
538 element.setAttribute("aria-busy", "true");
539 }
540 }
541
542 function clearBusyState(...elements) {
543 for (const element of elements) {
544 if (element.localName == "turbo-frame") {
545 element.removeAttribute("busy");
546 }
547
548 element.removeAttribute("aria-busy");
549 }
550 }
551
552 function waitForLoad(element, timeoutInMilliseconds = 2000) {
553 return new Promise((resolve) => {
554 const onComplete = () => {
555 element.removeEventListener("error", onComplete);
556 element.removeEventListener("load", onComplete);
557 resolve();
558 };
559
560 element.addEventListener("load", onComplete, { once: true });
561 element.addEventListener("error", onComplete, { once: true });
562 setTimeout(resolve, timeoutInMilliseconds);
563 })
564 }
565
566 function getHistoryMethodForAction(action) {
567 switch (action) {
568 case "replace":
569 return history.replaceState
570 case "advance":
571 case "restore":
572 return history.pushState
573 }
574 }
575
576 function isAction(action) {
577 return action == "advance" || action == "replace" || action == "restore"
578 }
579
580 function getVisitAction(...elements) {
581 const action = getAttribute("data-turbo-action", ...elements);
582
583 return isAction(action) ? action : null
584 }
585
586 function getMetaElement(name) {
587 return document.querySelector(`meta[name="${name}"]`)
588 }
589
590 function getMetaContent(name) {
591 const element = getMetaElement(name);
592 return element && element.content
593 }
594
595 function setMetaContent(name, content) {
596 let element = getMetaElement(name);
597
598 if (!element) {
599 element = document.createElement("meta");
600 element.setAttribute("name", name);
601
602 document.head.appendChild(element);
603 }
604
605 element.setAttribute("content", content);
606
607 return element
608 }
609
610 function findClosestRecursively(element, selector) {
611 if (element instanceof Element) {
612 return (
613 element.closest(selector) || findClosestRecursively(element.assignedSlot || element.getRootNode()?.host, selector)
614 )
615 }
616 }
617
618 function elementIsFocusable(element) {
619 const inertDisabledOrHidden = "[inert], :disabled, [hidden], details:not([open]), dialog:not([open])";
620
621 return !!element && element.closest(inertDisabledOrHidden) == null && typeof element.focus == "function"
622 }
623
624 function queryAutofocusableElement(elementOrDocumentFragment) {
625 return Array.from(elementOrDocumentFragment.querySelectorAll("[autofocus]")).find(elementIsFocusable)
626 }
627
628 async function around(callback, reader) {
629 const before = reader();
630
631 callback();
632
633 await nextAnimationFrame();
634
635 const after = reader();
636
637 return [before, after]
638 }
639
640 function doesNotTargetIFrame(anchor) {
641 if (anchor.hasAttribute("target")) {
642 for (const element of document.getElementsByName(anchor.target)) {
643 if (element instanceof HTMLIFrameElement) return false
644 }
645 }
646
647 return true
648 }
649
650 function findLinkFromClickTarget(target) {
651 return findClosestRecursively(target, "a[href]:not([target^=_]):not([download])")
652 }
653
654 function getLocationForLink(link) {
655 return expandURL(link.getAttribute("href") || "")
656 }
657
658 function debounce(fn, delay) {
659 let timeoutId = null;
660
661 return (...args) => {
662 const callback = () => fn.apply(this, args);
663 clearTimeout(timeoutId);
664 timeoutId = setTimeout(callback, delay);
665 }
666 }
667
668 class LimitedSet extends Set {
669 constructor(maxSize) {
670 super();
671 this.maxSize = maxSize;
672 }
673
674 add(value) {
675 if (this.size >= this.maxSize) {
676 const iterator = this.values();
677 const oldestValue = iterator.next().value;
678 this.delete(oldestValue);
679 }
680 super.add(value);
681 }
682 }
683
684 const recentRequests = new LimitedSet(20);
685
686 const nativeFetch = window.fetch;
687
688 function fetchWithTurboHeaders(url, options = {}) {
689 const modifiedHeaders = new Headers(options.headers || {});
690 const requestUID = uuid();
691 recentRequests.add(requestUID);
692 modifiedHeaders.append("X-Turbo-Request-Id", requestUID);
693
694 return nativeFetch(url, {
695 ...options,
696 headers: modifiedHeaders
697 })
698 }
699
700 function fetchMethodFromString(method) {
701 switch (method.toLowerCase()) {
702 case "get":
703 return FetchMethod.get
704 case "post":
705 return FetchMethod.post
706 case "put":
707 return FetchMethod.put
708 case "patch":
709 return FetchMethod.patch
710 case "delete":
711 return FetchMethod.delete
712 }
713 }
714
715 const FetchMethod = {
716 get: "get",
717 post: "post",
718 put: "put",
719 patch: "patch",
720 delete: "delete"
721 };
722
723 function fetchEnctypeFromString(encoding) {
724 switch (encoding.toLowerCase()) {
725 case FetchEnctype.multipart:
726 return FetchEnctype.multipart
727 case FetchEnctype.plain:
728 return FetchEnctype.plain
729 default:
730 return FetchEnctype.urlEncoded
731 }
732 }
733
734 const FetchEnctype = {
735 urlEncoded: "application/x-www-form-urlencoded",
736 multipart: "multipart/form-data",
737 plain: "text/plain"
738 };
739
740 class FetchRequest {
741 abortController = new AbortController()
742 #resolveRequestPromise = (_value) => {}
743
744 constructor(delegate, method, location, requestBody = new URLSearchParams(), target = null, enctype = FetchEnctype.urlEncoded) {
745 const [url, body] = buildResourceAndBody(expandURL(location), method, requestBody, enctype);
746
747 this.delegate = delegate;
748 this.url = url;
749 this.target = target;
750 this.fetchOptions = {
751 credentials: "same-origin",
752 redirect: "follow",
753 method: method,
754 headers: { ...this.defaultHeaders },
755 body: body,
756 signal: this.abortSignal,
757 referrer: this.delegate.referrer?.href
758 };
759 this.enctype = enctype;
760 }
761
762 get method() {
763 return this.fetchOptions.method
764 }
765
766 set method(value) {
767 const fetchBody = this.isSafe ? this.url.searchParams : this.fetchOptions.body || new FormData();
768 const fetchMethod = fetchMethodFromString(value) || FetchMethod.get;
769
770 this.url.search = "";
771
772 const [url, body] = buildResourceAndBody(this.url, fetchMethod, fetchBody, this.enctype);
773
774 this.url = url;
775 this.fetchOptions.body = body;
776 this.fetchOptions.method = fetchMethod;
777 }
778
779 get headers() {
780 return this.fetchOptions.headers
781 }
782
783 set headers(value) {
784 this.fetchOptions.headers = value;
785 }
786
787 get body() {
788 if (this.isSafe) {
789 return this.url.searchParams
790 } else {
791 return this.fetchOptions.body
792 }
793 }
794
795 set body(value) {
796 this.fetchOptions.body = value;
797 }
798
799 get location() {
800 return this.url
801 }
802
803 get params() {
804 return this.url.searchParams
805 }
806
807 get entries() {
808 return this.body ? Array.from(this.body.entries()) : []
809 }
810
811 cancel() {
812 this.abortController.abort();
813 }
814
815 async perform() {
816 const { fetchOptions } = this;
817 this.delegate.prepareRequest(this);
818 const event = await this.#allowRequestToBeIntercepted(fetchOptions);
819 try {
820 this.delegate.requestStarted(this);
821
822 if (event.detail.fetchRequest) {
823 this.response = event.detail.fetchRequest.response;
824 } else {
825 this.response = fetchWithTurboHeaders(this.url.href, fetchOptions);
826 }
827
828 const response = await this.response;
829 return await this.receive(response)
830 } catch (error) {
831 if (error.name !== "AbortError") {
832 if (this.#willDelegateErrorHandling(error)) {
833 this.delegate.requestErrored(this, error);
834 }
835 throw error
836 }
837 } finally {
838 this.delegate.requestFinished(this);
839 }
840 }
841
842 async receive(response) {
843 const fetchResponse = new FetchResponse(response);
844 const event = dispatch("turbo:before-fetch-response", {
845 cancelable: true,
846 detail: { fetchResponse },
847 target: this.target
848 });
849 if (event.defaultPrevented) {
850 this.delegate.requestPreventedHandlingResponse(this, fetchResponse);
851 } else if (fetchResponse.succeeded) {
852 this.delegate.requestSucceededWithResponse(this, fetchResponse);
853 } else {
854 this.delegate.requestFailedWithResponse(this, fetchResponse);
855 }
856 return fetchResponse
857 }
858
859 get defaultHeaders() {
860 return {
861 Accept: "text/html, application/xhtml+xml"
862 }
863 }
864
865 get isSafe() {
866 return isSafe(this.method)
867 }
868
869 get abortSignal() {
870 return this.abortController.signal
871 }
872
873 acceptResponseType(mimeType) {
874 this.headers["Accept"] = [mimeType, this.headers["Accept"]].join(", ");
875 }
876
877 async #allowRequestToBeIntercepted(fetchOptions) {
878 const requestInterception = new Promise((resolve) => (this.#resolveRequestPromise = resolve));
879 const event = dispatch("turbo:before-fetch-request", {
880 cancelable: true,
881 detail: {
882 fetchOptions,
883 url: this.url,
884 resume: this.#resolveRequestPromise
885 },
886 target: this.target
887 });
888 this.url = event.detail.url;
889 if (event.defaultPrevented) await requestInterception;
890
891 return event
892 }
893
894 #willDelegateErrorHandling(error) {
895 const event = dispatch("turbo:fetch-request-error", {
896 target: this.target,
897 cancelable: true,
898 detail: { request: this, error: error }
899 });
900
901 return !event.defaultPrevented
902 }
903 }
904
905 function isSafe(fetchMethod) {
906 return fetchMethodFromString(fetchMethod) == FetchMethod.get
907 }
908
909 function buildResourceAndBody(resource, method, requestBody, enctype) {
910 const searchParams =
911 Array.from(requestBody).length > 0 ? new URLSearchParams(entriesExcludingFiles(requestBody)) : resource.searchParams;
912
913 if (isSafe(method)) {
914 return [mergeIntoURLSearchParams(resource, searchParams), null]
915 } else if (enctype == FetchEnctype.urlEncoded) {
916 return [resource, searchParams]
917 } else {
918 return [resource, requestBody]
919 }
920 }
921
922 function entriesExcludingFiles(requestBody) {
923 const entries = [];
924
925 for (const [name, value] of requestBody) {
926 if (value instanceof File) continue
927 else entries.push([name, value]);
928 }
929
930 return entries
931 }
932
933 function mergeIntoURLSearchParams(url, requestBody) {
934 const searchParams = new URLSearchParams(entriesExcludingFiles(requestBody));
935
936 url.search = searchParams.toString();
937
938 return url
939 }
940
941 class AppearanceObserver {
942 started = false
943
944 constructor(delegate, element) {
945 this.delegate = delegate;
946 this.element = element;
947 this.intersectionObserver = new IntersectionObserver(this.intersect);
948 }
949
950 start() {
951 if (!this.started) {
952 this.started = true;
953 this.intersectionObserver.observe(this.element);
954 }
955 }
956
957 stop() {
958 if (this.started) {
959 this.started = false;
960 this.intersectionObserver.unobserve(this.element);
961 }
962 }
963
964 intersect = (entries) => {
965 const lastEntry = entries.slice(-1)[0];
966 if (lastEntry?.isIntersecting) {
967 this.delegate.elementAppearedInViewport(this.element);
968 }
969 }
970 }
971
972 class StreamMessage {
973 static contentType = "text/vnd.turbo-stream.html"
974
975 static wrap(message) {
976 if (typeof message == "string") {
977 return new this(createDocumentFragment(message))
978 } else {
979 return message
980 }
981 }
982
983 constructor(fragment) {
984 this.fragment = importStreamElements(fragment);
985 }
986 }
987
988 function importStreamElements(fragment) {
989 for (const element of fragment.querySelectorAll("turbo-stream")) {
990 const streamElement = document.importNode(element, true);
991
992 for (const inertScriptElement of streamElement.templateElement.content.querySelectorAll("script")) {
993 inertScriptElement.replaceWith(activateScriptElement(inertScriptElement));
994 }
995
996 element.replaceWith(streamElement);
997 }
998
999 return fragment
1000 }
1001
1002 const PREFETCH_DELAY = 100;
1003
1004 class PrefetchCache {
1005 #prefetchTimeout = null
1006 #prefetched = null
1007
1008 get(url) {
1009 if (this.#prefetched && this.#prefetched.url === url && this.#prefetched.expire > Date.now()) {
1010 return this.#prefetched.request
1011 }
1012 }
1013
1014 setLater(url, request, ttl) {
1015 this.clear();
1016
1017 this.#prefetchTimeout = setTimeout(() => {
1018 request.perform();
1019 this.set(url, request, ttl);
1020 this.#prefetchTimeout = null;
1021 }, PREFETCH_DELAY);
1022 }
1023
1024 set(url, request, ttl) {
1025 this.#prefetched = { url, request, expire: new Date(new Date().getTime() + ttl) };
1026 }
1027
1028 clear() {
1029 if (this.#prefetchTimeout) clearTimeout(this.#prefetchTimeout);
1030 this.#prefetched = null;
1031 }
1032 }
1033
1034 const cacheTtl = 10 * 1000;
1035 const prefetchCache = new PrefetchCache();
1036
1037 const FormSubmissionState = {
1038 initialized: "initialized",
1039 requesting: "requesting",
1040 waiting: "waiting",
1041 receiving: "receiving",
1042 stopping: "stopping",
1043 stopped: "stopped"
1044 };
1045
1046 class FormSubmission {
1047 state = FormSubmissionState.initialized
1048
1049 static confirmMethod(message, _element, _submitter) {
1050 return Promise.resolve(confirm(message))
1051 }
1052
1053 constructor(delegate, formElement, submitter, mustRedirect = false) {
1054 const method = getMethod(formElement, submitter);
1055 const action = getAction(getFormAction(formElement, submitter), method);
1056 const body = buildFormData(formElement, submitter);
1057 const enctype = getEnctype(formElement, submitter);
1058
1059 this.delegate = delegate;
1060 this.formElement = formElement;
1061 this.submitter = submitter;
1062 this.fetchRequest = new FetchRequest(this, method, action, body, formElement, enctype);
1063 this.mustRedirect = mustRedirect;
1064 }
1065
1066 get method() {
1067 return this.fetchRequest.method
1068 }
1069
1070 set method(value) {
1071 this.fetchRequest.method = value;
1072 }
1073
1074 get action() {
1075 return this.fetchRequest.url.toString()
1076 }
1077
1078 set action(value) {
1079 this.fetchRequest.url = expandURL(value);
1080 }
1081
1082 get body() {
1083 return this.fetchRequest.body
1084 }
1085
1086 get enctype() {
1087 return this.fetchRequest.enctype
1088 }
1089
1090 get isSafe() {
1091 return this.fetchRequest.isSafe
1092 }
1093
1094 get location() {
1095 return this.fetchRequest.url
1096 }
1097
1098 // The submission process
1099
1100 async start() {
1101 const { initialized, requesting } = FormSubmissionState;
1102 const confirmationMessage = getAttribute("data-turbo-confirm", this.submitter, this.formElement);
1103
1104 if (typeof confirmationMessage === "string") {
1105 const answer = await FormSubmission.confirmMethod(confirmationMessage, this.formElement, this.submitter);
1106 if (!answer) {
1107 return
1108 }
1109 }
1110
1111 if (this.state == initialized) {
1112 this.state = requesting;
1113 return this.fetchRequest.perform()
1114 }
1115 }
1116
1117 stop() {
1118 const { stopping, stopped } = FormSubmissionState;
1119 if (this.state != stopping && this.state != stopped) {
1120 this.state = stopping;
1121 this.fetchRequest.cancel();
1122 return true
1123 }
1124 }
1125
1126 // Fetch request delegate
1127
1128 prepareRequest(request) {
1129 if (!request.isSafe) {
1130 const token = getCookieValue(getMetaContent("csrf-param")) || getMetaContent("csrf-token");
1131 if (token) {
1132 request.headers["X-CSRF-Token"] = token;
1133 }
1134 }
1135
1136 if (this.requestAcceptsTurboStreamResponse(request)) {
1137 request.acceptResponseType(StreamMessage.contentType);
1138 }
1139 }
1140
1141 requestStarted(_request) {
1142 this.state = FormSubmissionState.waiting;
1143 this.submitter?.setAttribute("disabled", "");
1144 this.setSubmitsWith();
1145 markAsBusy(this.formElement);
1146 dispatch("turbo:submit-start", {
1147 target: this.formElement,
1148 detail: { formSubmission: this }
1149 });
1150 this.delegate.formSubmissionStarted(this);
1151 }
1152
1153 requestPreventedHandlingResponse(request, response) {
1154 prefetchCache.clear();
1155
1156 this.result = { success: response.succeeded, fetchResponse: response };
1157 }
1158
1159 requestSucceededWithResponse(request, response) {
1160 if (response.clientError || response.serverError) {
1161 this.delegate.formSubmissionFailedWithResponse(this, response);
1162 return
1163 }
1164
1165 prefetchCache.clear();
1166
1167 if (this.requestMustRedirect(request) && responseSucceededWithoutRedirect(response)) {
1168 const error = new Error("Form responses must redirect to another location");
1169 this.delegate.formSubmissionErrored(this, error);
1170 } else {
1171 this.state = FormSubmissionState.receiving;
1172 this.result = { success: true, fetchResponse: response };
1173 this.delegate.formSubmissionSucceededWithResponse(this, response);
1174 }
1175 }
1176
1177 requestFailedWithResponse(request, response) {
1178 this.result = { success: false, fetchResponse: response };
1179 this.delegate.formSubmissionFailedWithResponse(this, response);
1180 }
1181
1182 requestErrored(request, error) {
1183 this.result = { success: false, error };
1184 this.delegate.formSubmissionErrored(this, error);
1185 }
1186
1187 requestFinished(_request) {
1188 this.state = FormSubmissionState.stopped;
1189 this.submitter?.removeAttribute("disabled");
1190 this.resetSubmitterText();
1191 clearBusyState(this.formElement);
1192 dispatch("turbo:submit-end", {
1193 target: this.formElement,
1194 detail: { formSubmission: this, ...this.result }
1195 });
1196 this.delegate.formSubmissionFinished(this);
1197 }
1198
1199 // Private
1200
1201 setSubmitsWith() {
1202 if (!this.submitter || !this.submitsWith) return
1203
1204 if (this.submitter.matches("button")) {
1205 this.originalSubmitText = this.submitter.innerHTML;
1206 this.submitter.innerHTML = this.submitsWith;
1207 } else if (this.submitter.matches("input")) {
1208 const input = this.submitter;
1209 this.originalSubmitText = input.value;
1210 input.value = this.submitsWith;
1211 }
1212 }
1213
1214 resetSubmitterText() {
1215 if (!this.submitter || !this.originalSubmitText) return
1216
1217 if (this.submitter.matches("button")) {
1218 this.submitter.innerHTML = this.originalSubmitText;
1219 } else if (this.submitter.matches("input")) {
1220 const input = this.submitter;
1221 input.value = this.originalSubmitText;
1222 }
1223 }
1224
1225 requestMustRedirect(request) {
1226 return !request.isSafe && this.mustRedirect
1227 }
1228
1229 requestAcceptsTurboStreamResponse(request) {
1230 return !request.isSafe || hasAttribute("data-turbo-stream", this.submitter, this.formElement)
1231 }
1232
1233 get submitsWith() {
1234 return this.submitter?.getAttribute("data-turbo-submits-with")
1235 }
1236 }
1237
1238 function buildFormData(formElement, submitter) {
1239 const formData = new FormData(formElement);
1240 const name = submitter?.getAttribute("name");
1241 const value = submitter?.getAttribute("value");
1242
1243 if (name) {
1244 formData.append(name, value || "");
1245 }
1246
1247 return formData
1248 }
1249
1250 function getCookieValue(cookieName) {
1251 if (cookieName != null) {
1252 const cookies = document.cookie ? document.cookie.split("; ") : [];
1253 const cookie = cookies.find((cookie) => cookie.startsWith(cookieName));
1254 if (cookie) {
1255 const value = cookie.split("=").slice(1).join("=");
1256 return value ? decodeURIComponent(value) : undefined
1257 }
1258 }
1259 }
1260
1261 function responseSucceededWithoutRedirect(response) {
1262 return response.statusCode == 200 && !response.redirected
1263 }
1264
1265 function getFormAction(formElement, submitter) {
1266 const formElementAction = typeof formElement.action === "string" ? formElement.action : null;
1267
1268 if (submitter?.hasAttribute("formaction")) {
1269 return submitter.getAttribute("formaction") || ""
1270 } else {
1271 return formElement.getAttribute("action") || formElementAction || ""
1272 }
1273 }
1274
1275 function getAction(formAction, fetchMethod) {
1276 const action = expandURL(formAction);
1277
1278 if (isSafe(fetchMethod)) {
1279 action.search = "";
1280 }
1281
1282 return action
1283 }
1284
1285 function getMethod(formElement, submitter) {
1286 const method = submitter?.getAttribute("formmethod") || formElement.getAttribute("method") || "";
1287 return fetchMethodFromString(method.toLowerCase()) || FetchMethod.get
1288 }
1289
1290 function getEnctype(formElement, submitter) {
1291 return fetchEnctypeFromString(submitter?.getAttribute("formenctype") || formElement.enctype)
1292 }
1293
1294 class Snapshot {
1295 constructor(element) {
1296 this.element = element;
1297 }
1298
1299 get activeElement() {
1300 return this.element.ownerDocument.activeElement
1301 }
1302
1303 get children() {
1304 return [...this.element.children]
1305 }
1306
1307 hasAnchor(anchor) {
1308 return this.getElementForAnchor(anchor) != null
1309 }
1310
1311 getElementForAnchor(anchor) {
1312 return anchor ? this.element.querySelector(`[id='${anchor}'], a[name='${anchor}']`) : null
1313 }
1314
1315 get isConnected() {
1316 return this.element.isConnected
1317 }
1318
1319 get firstAutofocusableElement() {
1320 return queryAutofocusableElement(this.element)
1321 }
1322
1323 get permanentElements() {
1324 return queryPermanentElementsAll(this.element)
1325 }
1326
1327 getPermanentElementById(id) {
1328 return getPermanentElementById(this.element, id)
1329 }
1330
1331 getPermanentElementMapForSnapshot(snapshot) {
1332 const permanentElementMap = {};
1333
1334 for (const currentPermanentElement of this.permanentElements) {
1335 const { id } = currentPermanentElement;
1336 const newPermanentElement = snapshot.getPermanentElementById(id);
1337 if (newPermanentElement) {
1338 permanentElementMap[id] = [currentPermanentElement, newPermanentElement];
1339 }
1340 }
1341
1342 return permanentElementMap
1343 }
1344 }
1345
1346 function getPermanentElementById(node, id) {
1347 return node.querySelector(`#${id}[data-turbo-permanent]`)
1348 }
1349
1350 function queryPermanentElementsAll(node) {
1351 return node.querySelectorAll("[id][data-turbo-permanent]")
1352 }
1353
1354 class FormSubmitObserver {
1355 started = false
1356
1357 constructor(delegate, eventTarget) {
1358 this.delegate = delegate;
1359 this.eventTarget = eventTarget;
1360 }
1361
1362 start() {
1363 if (!this.started) {
1364 this.eventTarget.addEventListener("submit", this.submitCaptured, true);
1365 this.started = true;
1366 }
1367 }
1368
1369 stop() {
1370 if (this.started) {
1371 this.eventTarget.removeEventListener("submit", this.submitCaptured, true);
1372 this.started = false;
1373 }
1374 }
1375
1376 submitCaptured = () => {
1377 this.eventTarget.removeEventListener("submit", this.submitBubbled, false);
1378 this.eventTarget.addEventListener("submit", this.submitBubbled, false);
1379 }
1380
1381 submitBubbled = (event) => {
1382 if (!event.defaultPrevented) {
1383 const form = event.target instanceof HTMLFormElement ? event.target : undefined;
1384 const submitter = event.submitter || undefined;
1385
1386 if (
1387 form &&
1388 submissionDoesNotDismissDialog(form, submitter) &&
1389 submissionDoesNotTargetIFrame(form, submitter) &&
1390 this.delegate.willSubmitForm(form, submitter)
1391 ) {
1392 event.preventDefault();
1393 event.stopImmediatePropagation();
1394 this.delegate.formSubmitted(form, submitter);
1395 }
1396 }
1397 }
1398 }
1399
1400 function submissionDoesNotDismissDialog(form, submitter) {
1401 const method = submitter?.getAttribute("formmethod") || form.getAttribute("method");
1402
1403 return method != "dialog"
1404 }
1405
1406 function submissionDoesNotTargetIFrame(form, submitter) {
1407 if (submitter?.hasAttribute("formtarget") || form.hasAttribute("target")) {
1408 const target = submitter?.getAttribute("formtarget") || form.target;
1409
1410 for (const element of document.getElementsByName(target)) {
1411 if (element instanceof HTMLIFrameElement) return false
1412 }
1413
1414 return true
1415 } else {
1416 return true
1417 }
1418 }
1419
1420 class View {
1421 #resolveRenderPromise = (_value) => {}
1422 #resolveInterceptionPromise = (_value) => {}
1423
1424 constructor(delegate, element) {
1425 this.delegate = delegate;
1426 this.element = element;
1427 }
1428
1429 // Scrolling
1430
1431 scrollToAnchor(anchor) {
1432 const element = this.snapshot.getElementForAnchor(anchor);
1433 if (element) {
1434 this.scrollToElement(element);
1435 this.focusElement(element);
1436 } else {
1437 this.scrollToPosition({ x: 0, y: 0 });
1438 }
1439 }
1440
1441 scrollToAnchorFromLocation(location) {
1442 this.scrollToAnchor(getAnchor(location));
1443 }
1444
1445 scrollToElement(element) {
1446 element.scrollIntoView();
1447 }
1448
1449 focusElement(element) {
1450 if (element instanceof HTMLElement) {
1451 if (element.hasAttribute("tabindex")) {
1452 element.focus();
1453 } else {
1454 element.setAttribute("tabindex", "-1");
1455 element.focus();
1456 element.removeAttribute("tabindex");
1457 }
1458 }
1459 }
1460
1461 scrollToPosition({ x, y }) {
1462 this.scrollRoot.scrollTo(x, y);
1463 }
1464
1465 scrollToTop() {
1466 this.scrollToPosition({ x: 0, y: 0 });
1467 }
1468
1469 get scrollRoot() {
1470 return window
1471 }
1472
1473 // Rendering
1474
1475 async render(renderer) {
1476 const { isPreview, shouldRender, willRender, newSnapshot: snapshot } = renderer;
1477
1478 // A workaround to ignore tracked element mismatch reloads when performing
1479 // a promoted Visit from a frame navigation
1480 const shouldInvalidate = willRender;
1481
1482 if (shouldRender) {
1483 try {
1484 this.renderPromise = new Promise((resolve) => (this.#resolveRenderPromise = resolve));
1485 this.renderer = renderer;
1486 await this.prepareToRenderSnapshot(renderer);
1487
1488 const renderInterception = new Promise((resolve) => (this.#resolveInterceptionPromise = resolve));
1489 const options = { resume: this.#resolveInterceptionPromise, render: this.renderer.renderElement, renderMethod: this.renderer.renderMethod };
1490 const immediateRender = this.delegate.allowsImmediateRender(snapshot, options);
1491 if (!immediateRender) await renderInterception;
1492
1493 await this.renderSnapshot(renderer);
1494 this.delegate.viewRenderedSnapshot(snapshot, isPreview, this.renderer.renderMethod);
1495 this.delegate.preloadOnLoadLinksForView(this.element);
1496 this.finishRenderingSnapshot(renderer);
1497 } finally {
1498 delete this.renderer;
1499 this.#resolveRenderPromise(undefined);
1500 delete this.renderPromise;
1501 }
1502 } else if (shouldInvalidate) {
1503 this.invalidate(renderer.reloadReason);
1504 }
1505 }
1506
1507 invalidate(reason) {
1508 this.delegate.viewInvalidated(reason);
1509 }
1510
1511 async prepareToRenderSnapshot(renderer) {
1512 this.markAsPreview(renderer.isPreview);
1513 await renderer.prepareToRender();
1514 }
1515
1516 markAsPreview(isPreview) {
1517 if (isPreview) {
1518 this.element.setAttribute("data-turbo-preview", "");
1519 } else {
1520 this.element.removeAttribute("data-turbo-preview");
1521 }
1522 }
1523
1524 markVisitDirection(direction) {
1525 this.element.setAttribute("data-turbo-visit-direction", direction);
1526 }
1527
1528 unmarkVisitDirection() {
1529 this.element.removeAttribute("data-turbo-visit-direction");
1530 }
1531
1532 async renderSnapshot(renderer) {
1533 await renderer.render();
1534 }
1535
1536 finishRenderingSnapshot(renderer) {
1537 renderer.finishRendering();
1538 }
1539 }
1540
1541 class FrameView extends View {
1542 missing() {
1543 this.element.innerHTML = `<strong class="turbo-frame-error">Content missing</strong>`;
1544 }
1545
1546 get snapshot() {
1547 return new Snapshot(this.element)
1548 }
1549 }
1550
1551 class LinkInterceptor {
1552 constructor(delegate, element) {
1553 this.delegate = delegate;
1554 this.element = element;
1555 }
1556
1557 start() {
1558 this.element.addEventListener("click", this.clickBubbled);
1559 document.addEventListener("turbo:click", this.linkClicked);
1560 document.addEventListener("turbo:before-visit", this.willVisit);
1561 }
1562
1563 stop() {
1564 this.element.removeEventListener("click", this.clickBubbled);
1565 document.removeEventListener("turbo:click", this.linkClicked);
1566 document.removeEventListener("turbo:before-visit", this.willVisit);
1567 }
1568
1569 clickBubbled = (event) => {
1570 if (this.respondsToEventTarget(event.target)) {
1571 this.clickEvent = event;
1572 } else {
1573 delete this.clickEvent;
1574 }
1575 }
1576
1577 linkClicked = (event) => {
1578 if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) {
1579 if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url, event.detail.originalEvent)) {
1580 this.clickEvent.preventDefault();
1581 event.preventDefault();
1582 this.delegate.linkClickIntercepted(event.target, event.detail.url, event.detail.originalEvent);
1583 }
1584 }
1585 delete this.clickEvent;
1586 }
1587
1588 willVisit = (_event) => {
1589 delete this.clickEvent;
1590 }
1591
1592 respondsToEventTarget(target) {
1593 const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
1594 return element && element.closest("turbo-frame, html") == this.element
1595 }
1596 }
1597
1598 class LinkClickObserver {
1599 started = false
1600
1601 constructor(delegate, eventTarget) {
1602 this.delegate = delegate;
1603 this.eventTarget = eventTarget;
1604 }
1605
1606 start() {
1607 if (!this.started) {
1608 this.eventTarget.addEventListener("click", this.clickCaptured, true);
1609 this.started = true;
1610 }
1611 }
1612
1613 stop() {
1614 if (this.started) {
1615 this.eventTarget.removeEventListener("click", this.clickCaptured, true);
1616 this.started = false;
1617 }
1618 }
1619
1620 clickCaptured = () => {
1621 this.eventTarget.removeEventListener("click", this.clickBubbled, false);
1622 this.eventTarget.addEventListener("click", this.clickBubbled, false);
1623 }
1624
1625 clickBubbled = (event) => {
1626 if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {
1627 const target = (event.composedPath && event.composedPath()[0]) || event.target;
1628 const link = findLinkFromClickTarget(target);
1629 if (link && doesNotTargetIFrame(link)) {
1630 const location = getLocationForLink(link);
1631 if (this.delegate.willFollowLinkToLocation(link, location, event)) {
1632 event.preventDefault();
1633 this.delegate.followedLinkToLocation(link, location);
1634 }
1635 }
1636 }
1637 }
1638
1639 clickEventIsSignificant(event) {
1640 return !(
1641 (event.target && event.target.isContentEditable) ||
1642 event.defaultPrevented ||
1643 event.which > 1 ||
1644 event.altKey ||
1645 event.ctrlKey ||
1646 event.metaKey ||
1647 event.shiftKey
1648 )
1649 }
1650 }
1651
1652 class FormLinkClickObserver {
1653 constructor(delegate, element) {
1654 this.delegate = delegate;
1655 this.linkInterceptor = new LinkClickObserver(this, element);
1656 }
1657
1658 start() {
1659 this.linkInterceptor.start();
1660 }
1661
1662 stop() {
1663 this.linkInterceptor.stop();
1664 }
1665
1666 // Link hover observer delegate
1667
1668 canPrefetchRequestToLocation(link, location) {
1669 return false
1670 }
1671
1672 prefetchAndCacheRequestToLocation(link, location) {
1673 return
1674 }
1675
1676 // Link click observer delegate
1677
1678 willFollowLinkToLocation(link, location, originalEvent) {
1679 return (
1680 this.delegate.willSubmitFormLinkToLocation(link, location, originalEvent) &&
1681 (link.hasAttribute("data-turbo-method") || link.hasAttribute("data-turbo-stream"))
1682 )
1683 }
1684
1685 followedLinkToLocation(link, location) {
1686 const form = document.createElement("form");
1687
1688 const type = "hidden";
1689 for (const [name, value] of location.searchParams) {
1690 form.append(Object.assign(document.createElement("input"), { type, name, value }));
1691 }
1692
1693 const action = Object.assign(location, { search: "" });
1694 form.setAttribute("data-turbo", "true");
1695 form.setAttribute("action", action.href);
1696 form.setAttribute("hidden", "");
1697
1698 const method = link.getAttribute("data-turbo-method");
1699 if (method) form.setAttribute("method", method);
1700
1701 const turboFrame = link.getAttribute("data-turbo-frame");
1702 if (turboFrame) form.setAttribute("data-turbo-frame", turboFrame);
1703
1704 const turboAction = getVisitAction(link);
1705 if (turboAction) form.setAttribute("data-turbo-action", turboAction);
1706
1707 const turboConfirm = link.getAttribute("data-turbo-confirm");
1708 if (turboConfirm) form.setAttribute("data-turbo-confirm", turboConfirm);
1709
1710 const turboStream = link.hasAttribute("data-turbo-stream");
1711 if (turboStream) form.setAttribute("data-turbo-stream", "");
1712
1713 this.delegate.submittedFormLinkToLocation(link, location, form);
1714
1715 document.body.appendChild(form);
1716 form.addEventListener("turbo:submit-end", () => form.remove(), { once: true });
1717 requestAnimationFrame(() => form.requestSubmit());
1718 }
1719 }
1720
1721 class Bardo {
1722 static async preservingPermanentElements(delegate, permanentElementMap, callback) {
1723 const bardo = new this(delegate, permanentElementMap);
1724 bardo.enter();
1725 await callback();
1726 bardo.leave();
1727 }
1728
1729 constructor(delegate, permanentElementMap) {
1730 this.delegate = delegate;
1731 this.permanentElementMap = permanentElementMap;
1732 }
1733
1734 enter() {
1735 for (const id in this.permanentElementMap) {
1736 const [currentPermanentElement, newPermanentElement] = this.permanentElementMap[id];
1737 this.delegate.enteringBardo(currentPermanentElement, newPermanentElement);
1738 this.replaceNewPermanentElementWithPlaceholder(newPermanentElement);
1739 }
1740 }
1741
1742 leave() {
1743 for (const id in this.permanentElementMap) {
1744 const [currentPermanentElement] = this.permanentElementMap[id];
1745 this.replaceCurrentPermanentElementWithClone(currentPermanentElement);
1746 this.replacePlaceholderWithPermanentElement(currentPermanentElement);
1747 this.delegate.leavingBardo(currentPermanentElement);
1748 }
1749 }
1750
1751 replaceNewPermanentElementWithPlaceholder(permanentElement) {
1752 const placeholder = createPlaceholderForPermanentElement(permanentElement);
1753 permanentElement.replaceWith(placeholder);
1754 }
1755
1756 replaceCurrentPermanentElementWithClone(permanentElement) {
1757 const clone = permanentElement.cloneNode(true);
1758 permanentElement.replaceWith(clone);
1759 }
1760
1761 replacePlaceholderWithPermanentElement(permanentElement) {
1762 const placeholder = this.getPlaceholderById(permanentElement.id);
1763 placeholder?.replaceWith(permanentElement);
1764 }
1765
1766 getPlaceholderById(id) {
1767 return this.placeholders.find((element) => element.content == id)
1768 }
1769
1770 get placeholders() {
1771 return [...document.querySelectorAll("meta[name=turbo-permanent-placeholder][content]")]
1772 }
1773 }
1774
1775 function createPlaceholderForPermanentElement(permanentElement) {
1776 const element = document.createElement("meta");
1777 element.setAttribute("name", "turbo-permanent-placeholder");
1778 element.setAttribute("content", permanentElement.id);
1779 return element
1780 }
1781
1782 class Renderer {
1783 #activeElement = null
1784
1785 constructor(currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) {
1786 this.currentSnapshot = currentSnapshot;
1787 this.newSnapshot = newSnapshot;
1788 this.isPreview = isPreview;
1789 this.willRender = willRender;
1790 this.renderElement = renderElement;
1791 this.promise = new Promise((resolve, reject) => (this.resolvingFunctions = { resolve, reject }));
1792 }
1793
1794 get shouldRender() {
1795 return true
1796 }
1797
1798 get reloadReason() {
1799 return
1800 }
1801
1802 prepareToRender() {
1803 return
1804 }
1805
1806 render() {
1807 // Abstract method
1808 }
1809
1810 finishRendering() {
1811 if (this.resolvingFunctions) {
1812 this.resolvingFunctions.resolve();
1813 delete this.resolvingFunctions;
1814 }
1815 }
1816
1817 async preservingPermanentElements(callback) {
1818 await Bardo.preservingPermanentElements(this, this.permanentElementMap, callback);
1819 }
1820
1821 focusFirstAutofocusableElement() {
1822 const element = this.connectedSnapshot.firstAutofocusableElement;
1823 if (element) {
1824 element.focus();
1825 }
1826 }
1827
1828 // Bardo delegate
1829
1830 enteringBardo(currentPermanentElement) {
1831 if (this.#activeElement) return
1832
1833 if (currentPermanentElement.contains(this.currentSnapshot.activeElement)) {
1834 this.#activeElement = this.currentSnapshot.activeElement;
1835 }
1836 }
1837
1838 leavingBardo(currentPermanentElement) {
1839 if (currentPermanentElement.contains(this.#activeElement) && this.#activeElement instanceof HTMLElement) {
1840 this.#activeElement.focus();
1841
1842 this.#activeElement = null;
1843 }
1844 }
1845
1846 get connectedSnapshot() {
1847 return this.newSnapshot.isConnected ? this.newSnapshot : this.currentSnapshot
1848 }
1849
1850 get currentElement() {
1851 return this.currentSnapshot.element
1852 }
1853
1854 get newElement() {
1855 return this.newSnapshot.element
1856 }
1857
1858 get permanentElementMap() {
1859 return this.currentSnapshot.getPermanentElementMapForSnapshot(this.newSnapshot)
1860 }
1861
1862 get renderMethod() {
1863 return "replace"
1864 }
1865 }
1866
1867 class FrameRenderer extends Renderer {
1868 static renderElement(currentElement, newElement) {
1869 const destinationRange = document.createRange();
1870 destinationRange.selectNodeContents(currentElement);
1871 destinationRange.deleteContents();
1872
1873 const frameElement = newElement;
1874 const sourceRange = frameElement.ownerDocument?.createRange();
1875 if (sourceRange) {
1876 sourceRange.selectNodeContents(frameElement);
1877 currentElement.appendChild(sourceRange.extractContents());
1878 }
1879 }
1880
1881 constructor(delegate, currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) {
1882 super(currentSnapshot, newSnapshot, renderElement, isPreview, willRender);
1883 this.delegate = delegate;
1884 }
1885
1886 get shouldRender() {
1887 return true
1888 }
1889
1890 async render() {
1891 await nextRepaint();
1892 this.preservingPermanentElements(() => {
1893 this.loadFrameElement();
1894 });
1895 this.scrollFrameIntoView();
1896 await nextRepaint();
1897 this.focusFirstAutofocusableElement();
1898 await nextRepaint();
1899 this.activateScriptElements();
1900 }
1901
1902 loadFrameElement() {
1903 this.delegate.willRenderFrame(this.currentElement, this.newElement);
1904 this.renderElement(this.currentElement, this.newElement);
1905 }
1906
1907 scrollFrameIntoView() {
1908 if (this.currentElement.autoscroll || this.newElement.autoscroll) {
1909 const element = this.currentElement.firstElementChild;
1910 const block = readScrollLogicalPosition(this.currentElement.getAttribute("data-autoscroll-block"), "end");
1911 const behavior = readScrollBehavior(this.currentElement.getAttribute("data-autoscroll-behavior"), "auto");
1912
1913 if (element) {
1914 element.scrollIntoView({ block, behavior });
1915 return true
1916 }
1917 }
1918 return false
1919 }
1920
1921 activateScriptElements() {
1922 for (const inertScriptElement of this.newScriptElements) {
1923 const activatedScriptElement = activateScriptElement(inertScriptElement);
1924 inertScriptElement.replaceWith(activatedScriptElement);
1925 }
1926 }
1927
1928 get newScriptElements() {
1929 return this.currentElement.querySelectorAll("script")
1930 }
1931 }
1932
1933 function readScrollLogicalPosition(value, defaultValue) {
1934 if (value == "end" || value == "start" || value == "center" || value == "nearest") {
1935 return value
1936 } else {
1937 return defaultValue
1938 }
1939 }
1940
1941 function readScrollBehavior(value, defaultValue) {
1942 if (value == "auto" || value == "smooth") {
1943 return value
1944 } else {
1945 return defaultValue
1946 }
1947 }
1948
1949 class ProgressBar {
1950 static animationDuration = 300 /*ms*/
1951
1952 static get defaultCSS() {
1953 return unindent`
1954 .turbo-progress-bar {
1955 position: fixed;
1956 display: block;
1957 top: 0;
1958 left: 0;
1959 height: 3px;
1960 background: #0076ff;
1961 z-index: 2147483647;
1962 transition:
1963 width ${ProgressBar.animationDuration}ms ease-out,
1964 opacity ${ProgressBar.animationDuration / 2}ms ${ProgressBar.animationDuration / 2}ms ease-in;
1965 transform: translate3d(0, 0, 0);
1966 }
1967 `
1968 }
1969
1970 hiding = false
1971 value = 0
1972 visible = false
1973
1974 constructor() {
1975 this.stylesheetElement = this.createStylesheetElement();
1976 this.progressElement = this.createProgressElement();
1977 this.installStylesheetElement();
1978 this.setValue(0);
1979 }
1980
1981 show() {
1982 if (!this.visible) {
1983 this.visible = true;
1984 this.installProgressElement();
1985 this.startTrickling();
1986 }
1987 }
1988
1989 hide() {
1990 if (this.visible && !this.hiding) {
1991 this.hiding = true;
1992 this.fadeProgressElement(() => {
1993 this.uninstallProgressElement();
1994 this.stopTrickling();
1995 this.visible = false;
1996 this.hiding = false;
1997 });
1998 }
1999 }
2000
2001 setValue(value) {
2002 this.value = value;
2003 this.refresh();
2004 }
2005
2006 // Private
2007
2008 installStylesheetElement() {
2009 document.head.insertBefore(this.stylesheetElement, document.head.firstChild);
2010 }
2011
2012 installProgressElement() {
2013 this.progressElement.style.width = "0";
2014 this.progressElement.style.opacity = "1";
2015 document.documentElement.insertBefore(this.progressElement, document.body);
2016 this.refresh();
2017 }
2018
2019 fadeProgressElement(callback) {
2020 this.progressElement.style.opacity = "0";
2021 setTimeout(callback, ProgressBar.animationDuration * 1.5);
2022 }
2023
2024 uninstallProgressElement() {
2025 if (this.progressElement.parentNode) {
2026 document.documentElement.removeChild(this.progressElement);
2027 }
2028 }
2029
2030 startTrickling() {
2031 if (!this.trickleInterval) {
2032 this.trickleInterval = window.setInterval(this.trickle, ProgressBar.animationDuration);
2033 }
2034 }
2035
2036 stopTrickling() {
2037 window.clearInterval(this.trickleInterval);
2038 delete this.trickleInterval;
2039 }
2040
2041 trickle = () => {
2042 this.setValue(this.value + Math.random() / 100);
2043 }
2044
2045 refresh() {
2046 requestAnimationFrame(() => {
2047 this.progressElement.style.width = `${10 + this.value * 90}%`;
2048 });
2049 }
2050
2051 createStylesheetElement() {
2052 const element = document.createElement("style");
2053 element.type = "text/css";
2054 element.textContent = ProgressBar.defaultCSS;
2055 if (this.cspNonce) {
2056 element.nonce = this.cspNonce;
2057 }
2058 return element
2059 }
2060
2061 createProgressElement() {
2062 const element = document.createElement("div");
2063 element.className = "turbo-progress-bar";
2064 return element
2065 }
2066
2067 get cspNonce() {
2068 return getMetaContent("csp-nonce")
2069 }
2070 }
2071
2072 class HeadSnapshot extends Snapshot {
2073 detailsByOuterHTML = this.children
2074 .filter((element) => !elementIsNoscript(element))
2075 .map((element) => elementWithoutNonce(element))
2076 .reduce((result, element) => {
2077 const { outerHTML } = element;
2078 const details =
2079 outerHTML in result
2080 ? result[outerHTML]
2081 : {
2082 type: elementType(element),
2083 tracked: elementIsTracked(element),
2084 elements: []
2085 };
2086 return {
2087 ...result,
2088 [outerHTML]: {
2089 ...details,
2090 elements: [...details.elements, element]
2091 }
2092 }
2093 }, {})
2094
2095 get trackedElementSignature() {
2096 return Object.keys(this.detailsByOuterHTML)
2097 .filter((outerHTML) => this.detailsByOuterHTML[outerHTML].tracked)
2098 .join("")
2099 }
2100
2101 getScriptElementsNotInSnapshot(snapshot) {
2102 return this.getElementsMatchingTypeNotInSnapshot("script", snapshot)
2103 }
2104
2105 getStylesheetElementsNotInSnapshot(snapshot) {
2106 return this.getElementsMatchingTypeNotInSnapshot("stylesheet", snapshot)
2107 }
2108
2109 getElementsMatchingTypeNotInSnapshot(matchedType, snapshot) {
2110 return Object.keys(this.detailsByOuterHTML)
2111 .filter((outerHTML) => !(outerHTML in snapshot.detailsByOuterHTML))
2112 .map((outerHTML) => this.detailsByOuterHTML[outerHTML])
2113 .filter(({ type }) => type == matchedType)
2114 .map(({ elements: [element] }) => element)
2115 }
2116
2117 get provisionalElements() {
2118 return Object.keys(this.detailsByOuterHTML).reduce((result, outerHTML) => {
2119 const { type, tracked, elements } = this.detailsByOuterHTML[outerHTML];
2120 if (type == null && !tracked) {
2121 return [...result, ...elements]
2122 } else if (elements.length > 1) {
2123 return [...result, ...elements.slice(1)]
2124 } else {
2125 return result
2126 }
2127 }, [])
2128 }
2129
2130 getMetaValue(name) {
2131 const element = this.findMetaElementByName(name);
2132 return element ? element.getAttribute("content") : null
2133 }
2134
2135 findMetaElementByName(name) {
2136 return Object.keys(this.detailsByOuterHTML).reduce((result, outerHTML) => {
2137 const {
2138 elements: [element]
2139 } = this.detailsByOuterHTML[outerHTML];
2140 return elementIsMetaElementWithName(element, name) ? element : result
2141 }, undefined | undefined)
2142 }
2143 }
2144
2145 function elementType(element) {
2146 if (elementIsScript(element)) {
2147 return "script"
2148 } else if (elementIsStylesheet(element)) {
2149 return "stylesheet"
2150 }
2151 }
2152
2153 function elementIsTracked(element) {
2154 return element.getAttribute("data-turbo-track") == "reload"
2155 }
2156
2157 function elementIsScript(element) {
2158 const tagName = element.localName;
2159 return tagName == "script"
2160 }
2161
2162 function elementIsNoscript(element) {
2163 const tagName = element.localName;
2164 return tagName == "noscript"
2165 }
2166
2167 function elementIsStylesheet(element) {
2168 const tagName = element.localName;
2169 return tagName == "style" || (tagName == "link" && element.getAttribute("rel") == "stylesheet")
2170 }
2171
2172 function elementIsMetaElementWithName(element, name) {
2173 const tagName = element.localName;
2174 return tagName == "meta" && element.getAttribute("name") == name
2175 }
2176
2177 function elementWithoutNonce(element) {
2178 if (element.hasAttribute("nonce")) {
2179 element.setAttribute("nonce", "");
2180 }
2181
2182 return element
2183 }
2184
2185 class PageSnapshot extends Snapshot {
2186 static fromHTMLString(html = "") {
2187 return this.fromDocument(parseHTMLDocument(html))
2188 }
2189
2190 static fromElement(element) {
2191 return this.fromDocument(element.ownerDocument)
2192 }
2193
2194 static fromDocument({ documentElement, body, head }) {
2195 return new this(documentElement, body, new HeadSnapshot(head))
2196 }
2197
2198 constructor(documentElement, body, headSnapshot) {
2199 super(body);
2200 this.documentElement = documentElement;
2201 this.headSnapshot = headSnapshot;
2202 }
2203
2204 clone() {
2205 const clonedElement = this.element.cloneNode(true);
2206
2207 const selectElements = this.element.querySelectorAll("select");
2208 const clonedSelectElements = clonedElement.querySelectorAll("select");
2209
2210 for (const [index, source] of selectElements.entries()) {
2211 const clone = clonedSelectElements[index];
2212 for (const option of clone.selectedOptions) option.selected = false;
2213 for (const option of source.selectedOptions) clone.options[option.index].selected = true;
2214 }
2215
2216 for (const clonedPasswordInput of clonedElement.querySelectorAll('input[type="password"]')) {
2217 clonedPasswordInput.value = "";
2218 }
2219
2220 return new PageSnapshot(this.documentElement, clonedElement, this.headSnapshot)
2221 }
2222
2223 get lang() {
2224 return this.documentElement.getAttribute("lang")
2225 }
2226
2227 get headElement() {
2228 return this.headSnapshot.element
2229 }
2230
2231 get rootLocation() {
2232 const root = this.getSetting("root") ?? "/";
2233 return expandURL(root)
2234 }
2235
2236 get cacheControlValue() {
2237 return this.getSetting("cache-control")
2238 }
2239
2240 get isPreviewable() {
2241 return this.cacheControlValue != "no-preview"
2242 }
2243
2244 get isCacheable() {
2245 return this.cacheControlValue != "no-cache"
2246 }
2247
2248 get isVisitable() {
2249 return this.getSetting("visit-control") != "reload"
2250 }
2251
2252 get prefersViewTransitions() {
2253 return this.headSnapshot.getMetaValue("view-transition") === "same-origin"
2254 }
2255
2256 get shouldMorphPage() {
2257 return this.getSetting("refresh-method") === "morph"
2258 }
2259
2260 get shouldPreserveScrollPosition() {
2261 return this.getSetting("refresh-scroll") === "preserve"
2262 }
2263
2264 // Private
2265
2266 getSetting(name) {
2267 return this.headSnapshot.getMetaValue(`turbo-${name}`)
2268 }
2269 }
2270
2271 class ViewTransitioner {
2272 #viewTransitionStarted = false
2273 #lastOperation = Promise.resolve()
2274
2275 renderChange(useViewTransition, render) {
2276 if (useViewTransition && this.viewTransitionsAvailable && !this.#viewTransitionStarted) {
2277 this.#viewTransitionStarted = true;
2278 this.#lastOperation = this.#lastOperation.then(async () => {
2279 await document.startViewTransition(render).finished;
2280 });
2281 } else {
2282 this.#lastOperation = this.#lastOperation.then(render);
2283 }
2284
2285 return this.#lastOperation
2286 }
2287
2288 get viewTransitionsAvailable() {
2289 return document.startViewTransition
2290 }
2291 }
2292
2293 const defaultOptions = {
2294 action: "advance",
2295 historyChanged: false,
2296 visitCachedSnapshot: () => {},
2297 willRender: true,
2298 updateHistory: true,
2299 shouldCacheSnapshot: true,
2300 acceptsStreamResponse: false
2301 };
2302
2303 const TimingMetric = {
2304 visitStart: "visitStart",
2305 requestStart: "requestStart",
2306 requestEnd: "requestEnd",
2307 visitEnd: "visitEnd"
2308 };
2309
2310 const VisitState = {
2311 initialized: "initialized",
2312 started: "started",
2313 canceled: "canceled",
2314 failed: "failed",
2315 completed: "completed"
2316 };
2317
2318 const SystemStatusCode = {
2319 networkFailure: 0,
2320 timeoutFailure: -1,
2321 contentTypeMismatch: -2
2322 };
2323
2324 const Direction = {
2325 advance: "forward",
2326 restore: "back",
2327 replace: "none"
2328 };
2329
2330 class Visit {
2331 identifier = uuid() // Required by turbo-ios
2332 timingMetrics = {}
2333
2334 followedRedirect = false
2335 historyChanged = false
2336 scrolled = false
2337 shouldCacheSnapshot = true
2338 acceptsStreamResponse = false
2339 snapshotCached = false
2340 state = VisitState.initialized
2341 viewTransitioner = new ViewTransitioner()
2342
2343 constructor(delegate, location, restorationIdentifier, options = {}) {
2344 this.delegate = delegate;
2345 this.location = location;
2346 this.restorationIdentifier = restorationIdentifier || uuid();
2347
2348 const {
2349 action,
2350 historyChanged,
2351 referrer,
2352 snapshot,
2353 snapshotHTML,
2354 response,
2355 visitCachedSnapshot,
2356 willRender,
2357 updateHistory,
2358 shouldCacheSnapshot,
2359 acceptsStreamResponse,
2360 direction
2361 } = {
2362 ...defaultOptions,
2363 ...options
2364 };
2365 this.action = action;
2366 this.historyChanged = historyChanged;
2367 this.referrer = referrer;
2368 this.snapshot = snapshot;
2369 this.snapshotHTML = snapshotHTML;
2370 this.response = response;
2371 this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
2372 this.isPageRefresh = this.view.isPageRefresh(this);
2373 this.visitCachedSnapshot = visitCachedSnapshot;
2374 this.willRender = willRender;
2375 this.updateHistory = updateHistory;
2376 this.scrolled = !willRender;
2377 this.shouldCacheSnapshot = shouldCacheSnapshot;
2378 this.acceptsStreamResponse = acceptsStreamResponse;
2379 this.direction = direction || Direction[action];
2380 }
2381
2382 get adapter() {
2383 return this.delegate.adapter
2384 }
2385
2386 get view() {
2387 return this.delegate.view
2388 }
2389
2390 get history() {
2391 return this.delegate.history
2392 }
2393
2394 get restorationData() {
2395 return this.history.getRestorationDataForIdentifier(this.restorationIdentifier)
2396 }
2397
2398 get silent() {
2399 return this.isSamePage
2400 }
2401
2402 start() {
2403 if (this.state == VisitState.initialized) {
2404 this.recordTimingMetric(TimingMetric.visitStart);
2405 this.state = VisitState.started;
2406 this.adapter.visitStarted(this);
2407 this.delegate.visitStarted(this);
2408 }
2409 }
2410
2411 cancel() {
2412 if (this.state == VisitState.started) {
2413 if (this.request) {
2414 this.request.cancel();
2415 }
2416 this.cancelRender();
2417 this.state = VisitState.canceled;
2418 }
2419 }
2420
2421 complete() {
2422 if (this.state == VisitState.started) {
2423 this.recordTimingMetric(TimingMetric.visitEnd);
2424 this.adapter.visitCompleted(this);
2425 this.state = VisitState.completed;
2426 this.followRedirect();
2427
2428 if (!this.followedRedirect) {
2429 this.delegate.visitCompleted(this);
2430 }
2431 }
2432 }
2433
2434 fail() {
2435 if (this.state == VisitState.started) {
2436 this.state = VisitState.failed;
2437 this.adapter.visitFailed(this);
2438 this.delegate.visitCompleted(this);
2439 }
2440 }
2441
2442 changeHistory() {
2443 if (!this.historyChanged && this.updateHistory) {
2444 const actionForHistory = this.location.href === this.referrer?.href ? "replace" : this.action;
2445 const method = getHistoryMethodForAction(actionForHistory);
2446 this.history.update(method, this.location, this.restorationIdentifier);
2447 this.historyChanged = true;
2448 }
2449 }
2450
2451 issueRequest() {
2452 if (this.hasPreloadedResponse()) {
2453 this.simulateRequest();
2454 } else if (this.shouldIssueRequest() && !this.request) {
2455 this.request = new FetchRequest(this, FetchMethod.get, this.location);
2456 this.request.perform();
2457 }
2458 }
2459
2460 simulateRequest() {
2461 if (this.response) {
2462 this.startRequest();
2463 this.recordResponse();
2464 this.finishRequest();
2465 }
2466 }
2467
2468 startRequest() {
2469 this.recordTimingMetric(TimingMetric.requestStart);
2470 this.adapter.visitRequestStarted(this);
2471 }
2472
2473 recordResponse(response = this.response) {
2474 this.response = response;
2475 if (response) {
2476 const { statusCode } = response;
2477 if (isSuccessful(statusCode)) {
2478 this.adapter.visitRequestCompleted(this);
2479 } else {
2480 this.adapter.visitRequestFailedWithStatusCode(this, statusCode);
2481 }
2482 }
2483 }
2484
2485 finishRequest() {
2486 this.recordTimingMetric(TimingMetric.requestEnd);
2487 this.adapter.visitRequestFinished(this);
2488 }
2489
2490 loadResponse() {
2491 if (this.response) {
2492 const { statusCode, responseHTML } = this.response;
2493 this.render(async () => {
2494 if (this.shouldCacheSnapshot) this.cacheSnapshot();
2495 if (this.view.renderPromise) await this.view.renderPromise;
2496
2497 if (isSuccessful(statusCode) && responseHTML != null) {
2498 const snapshot = PageSnapshot.fromHTMLString(responseHTML);
2499 await this.renderPageSnapshot(snapshot, false);
2500
2501 this.adapter.visitRendered(this);
2502 this.complete();
2503 } else {
2504 await this.view.renderError(PageSnapshot.fromHTMLString(responseHTML), this);
2505 this.adapter.visitRendered(this);
2506 this.fail();
2507 }
2508 });
2509 }
2510 }
2511
2512 getCachedSnapshot() {
2513 const snapshot = this.view.getCachedSnapshotForLocation(this.location) || this.getPreloadedSnapshot();
2514
2515 if (snapshot && (!getAnchor(this.location) || snapshot.hasAnchor(getAnchor(this.location)))) {
2516 if (this.action == "restore" || snapshot.isPreviewable) {
2517 return snapshot
2518 }
2519 }
2520 }
2521
2522 getPreloadedSnapshot() {
2523 if (this.snapshotHTML) {
2524 return PageSnapshot.fromHTMLString(this.snapshotHTML)
2525 }
2526 }
2527
2528 hasCachedSnapshot() {
2529 return this.getCachedSnapshot() != null
2530 }
2531
2532 loadCachedSnapshot() {
2533 const snapshot = this.getCachedSnapshot();
2534 if (snapshot) {
2535 const isPreview = this.shouldIssueRequest();
2536 this.render(async () => {
2537 this.cacheSnapshot();
2538 if (this.isSamePage || this.isPageRefresh) {
2539 this.adapter.visitRendered(this);
2540 } else {
2541 if (this.view.renderPromise) await this.view.renderPromise;
2542
2543 await this.renderPageSnapshot(snapshot, isPreview);
2544
2545 this.adapter.visitRendered(this);
2546 if (!isPreview) {
2547 this.complete();
2548 }
2549 }
2550 });
2551 }
2552 }
2553
2554 followRedirect() {
2555 if (this.redirectedToLocation && !this.followedRedirect && this.response?.redirected) {
2556 this.adapter.visitProposedToLocation(this.redirectedToLocation, {
2557 action: "replace",
2558 response: this.response,
2559 shouldCacheSnapshot: false,
2560 willRender: false
2561 });
2562 this.followedRedirect = true;
2563 }
2564 }
2565
2566 goToSamePageAnchor() {
2567 if (this.isSamePage) {
2568 this.render(async () => {
2569 this.cacheSnapshot();
2570 this.performScroll();
2571 this.changeHistory();
2572 this.adapter.visitRendered(this);
2573 });
2574 }
2575 }
2576
2577 // Fetch request delegate
2578
2579 prepareRequest(request) {
2580 if (this.acceptsStreamResponse) {
2581 request.acceptResponseType(StreamMessage.contentType);
2582 }
2583 }
2584
2585 requestStarted() {
2586 this.startRequest();
2587 }
2588
2589 requestPreventedHandlingResponse(_request, _response) {}
2590
2591 async requestSucceededWithResponse(request, response) {
2592 const responseHTML = await response.responseHTML;
2593 const { redirected, statusCode } = response;
2594 if (responseHTML == undefined) {
2595 this.recordResponse({
2596 statusCode: SystemStatusCode.contentTypeMismatch,
2597 redirected
2598 });
2599 } else {
2600 this.redirectedToLocation = response.redirected ? response.location : undefined;
2601 this.recordResponse({ statusCode: statusCode, responseHTML, redirected });
2602 }
2603 }
2604
2605 async requestFailedWithResponse(request, response) {
2606 const responseHTML = await response.responseHTML;
2607 const { redirected, statusCode } = response;
2608 if (responseHTML == undefined) {
2609 this.recordResponse({
2610 statusCode: SystemStatusCode.contentTypeMismatch,
2611 redirected
2612 });
2613 } else {
2614 this.recordResponse({ statusCode: statusCode, responseHTML, redirected });
2615 }
2616 }
2617
2618 requestErrored(_request, _error) {
2619 this.recordResponse({
2620 statusCode: SystemStatusCode.networkFailure,
2621 redirected: false
2622 });
2623 }
2624
2625 requestFinished() {
2626 this.finishRequest();
2627 }
2628
2629 // Scrolling
2630
2631 performScroll() {
2632 if (!this.scrolled && !this.view.forceReloaded && !this.view.shouldPreserveScrollPosition(this)) {
2633 if (this.action == "restore") {
2634 this.scrollToRestoredPosition() || this.scrollToAnchor() || this.view.scrollToTop();
2635 } else {
2636 this.scrollToAnchor() || this.view.scrollToTop();
2637 }
2638 if (this.isSamePage) {
2639 this.delegate.visitScrolledToSamePageLocation(this.view.lastRenderedLocation, this.location);
2640 }
2641
2642 this.scrolled = true;
2643 }
2644 }
2645
2646 scrollToRestoredPosition() {
2647 const { scrollPosition } = this.restorationData;
2648 if (scrollPosition) {
2649 this.view.scrollToPosition(scrollPosition);
2650 return true
2651 }
2652 }
2653
2654 scrollToAnchor() {
2655 const anchor = getAnchor(this.location);
2656 if (anchor != null) {
2657 this.view.scrollToAnchor(anchor);
2658 return true
2659 }
2660 }
2661
2662 // Instrumentation
2663
2664 recordTimingMetric(metric) {
2665 this.timingMetrics[metric] = new Date().getTime();
2666 }
2667
2668 getTimingMetrics() {
2669 return { ...this.timingMetrics }
2670 }
2671
2672 // Private
2673
2674 getHistoryMethodForAction(action) {
2675 switch (action) {
2676 case "replace":
2677 return history.replaceState
2678 case "advance":
2679 case "restore":
2680 return history.pushState
2681 }
2682 }
2683
2684 hasPreloadedResponse() {
2685 return typeof this.response == "object"
2686 }
2687
2688 shouldIssueRequest() {
2689 if (this.isSamePage) {
2690 return false
2691 } else if (this.action == "restore") {
2692 return !this.hasCachedSnapshot()
2693 } else {
2694 return this.willRender
2695 }
2696 }
2697
2698 cacheSnapshot() {
2699 if (!this.snapshotCached) {
2700 this.view.cacheSnapshot(this.snapshot).then((snapshot) => snapshot && this.visitCachedSnapshot(snapshot));
2701 this.snapshotCached = true;
2702 }
2703 }
2704
2705 async render(callback) {
2706 this.cancelRender();
2707 this.frame = await nextRepaint();
2708 await callback();
2709 delete this.frame;
2710 }
2711
2712 async renderPageSnapshot(snapshot, isPreview) {
2713 await this.viewTransitioner.renderChange(this.view.shouldTransitionTo(snapshot), async () => {
2714 await this.view.renderPage(snapshot, isPreview, this.willRender, this);
2715 this.performScroll();
2716 });
2717 }
2718
2719 cancelRender() {
2720 if (this.frame) {
2721 cancelAnimationFrame(this.frame);
2722 delete this.frame;
2723 }
2724 }
2725 }
2726
2727 function isSuccessful(statusCode) {
2728 return statusCode >= 200 && statusCode < 300
2729 }
2730
2731 class BrowserAdapter {
2732 progressBar = new ProgressBar()
2733
2734 constructor(session) {
2735 this.session = session;
2736 }
2737
2738 visitProposedToLocation(location, options) {
2739 if (locationIsVisitable(location, this.navigator.rootLocation)) {
2740 this.navigator.startVisit(location, options?.restorationIdentifier || uuid(), options);
2741 } else {
2742 window.location.href = location.toString();
2743 }
2744 }
2745
2746 visitStarted(visit) {
2747 this.location = visit.location;
2748 visit.loadCachedSnapshot();
2749 visit.issueRequest();
2750 visit.goToSamePageAnchor();
2751 }
2752
2753 visitRequestStarted(visit) {
2754 this.progressBar.setValue(0);
2755 if (visit.hasCachedSnapshot() || visit.action != "restore") {
2756 this.showVisitProgressBarAfterDelay();
2757 } else {
2758 this.showProgressBar();
2759 }
2760 }
2761
2762 visitRequestCompleted(visit) {
2763 visit.loadResponse();
2764 }
2765
2766 visitRequestFailedWithStatusCode(visit, statusCode) {
2767 switch (statusCode) {
2768 case SystemStatusCode.networkFailure:
2769 case SystemStatusCode.timeoutFailure:
2770 case SystemStatusCode.contentTypeMismatch:
2771 return this.reload({
2772 reason: "request_failed",
2773 context: {
2774 statusCode
2775 }
2776 })
2777 default:
2778 return visit.loadResponse()
2779 }
2780 }
2781
2782 visitRequestFinished(_visit) {}
2783
2784 visitCompleted(_visit) {
2785 this.progressBar.setValue(1);
2786 this.hideVisitProgressBar();
2787 }
2788
2789 pageInvalidated(reason) {
2790 this.reload(reason);
2791 }
2792
2793 visitFailed(_visit) {
2794 this.progressBar.setValue(1);
2795 this.hideVisitProgressBar();
2796 }
2797
2798 visitRendered(_visit) {}
2799
2800 // Form Submission Delegate
2801
2802 formSubmissionStarted(_formSubmission) {
2803 this.progressBar.setValue(0);
2804 this.showFormProgressBarAfterDelay();
2805 }
2806
2807 formSubmissionFinished(_formSubmission) {
2808 this.progressBar.setValue(1);
2809 this.hideFormProgressBar();
2810 }
2811
2812 // Private
2813
2814 showVisitProgressBarAfterDelay() {
2815 this.visitProgressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay);
2816 }
2817
2818 hideVisitProgressBar() {
2819 this.progressBar.hide();
2820 if (this.visitProgressBarTimeout != null) {
2821 window.clearTimeout(this.visitProgressBarTimeout);
2822 delete this.visitProgressBarTimeout;
2823 }
2824 }
2825
2826 showFormProgressBarAfterDelay() {
2827 if (this.formProgressBarTimeout == null) {
2828 this.formProgressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay);
2829 }
2830 }
2831
2832 hideFormProgressBar() {
2833 this.progressBar.hide();
2834 if (this.formProgressBarTimeout != null) {
2835 window.clearTimeout(this.formProgressBarTimeout);
2836 delete this.formProgressBarTimeout;
2837 }
2838 }
2839
2840 showProgressBar = () => {
2841 this.progressBar.show();
2842 }
2843
2844 reload(reason) {
2845 dispatch("turbo:reload", { detail: reason });
2846
2847 window.location.href = this.location?.toString() || window.location.href;
2848 }
2849
2850 get navigator() {
2851 return this.session.navigator
2852 }
2853 }
2854
2855 class CacheObserver {
2856 selector = "[data-turbo-temporary]"
2857 deprecatedSelector = "[data-turbo-cache=false]"
2858
2859 started = false
2860
2861 start() {
2862 if (!this.started) {
2863 this.started = true;
2864 addEventListener("turbo:before-cache", this.removeTemporaryElements, false);
2865 }
2866 }
2867
2868 stop() {
2869 if (this.started) {
2870 this.started = false;
2871 removeEventListener("turbo:before-cache", this.removeTemporaryElements, false);
2872 }
2873 }
2874
2875 removeTemporaryElements = (_event) => {
2876 for (const element of this.temporaryElements) {
2877 element.remove();
2878 }
2879 }
2880
2881 get temporaryElements() {
2882 return [...document.querySelectorAll(this.selector), ...this.temporaryElementsWithDeprecation]
2883 }
2884
2885 get temporaryElementsWithDeprecation() {
2886 const elements = document.querySelectorAll(this.deprecatedSelector);
2887
2888 if (elements.length) {
2889 console.warn(
2890 `The ${this.deprecatedSelector} selector is deprecated and will be removed in a future version. Use ${this.selector} instead.`
2891 );
2892 }
2893
2894 return [...elements]
2895 }
2896 }
2897
2898 class FrameRedirector {
2899 constructor(session, element) {
2900 this.session = session;
2901 this.element = element;
2902 this.linkInterceptor = new LinkInterceptor(this, element);
2903 this.formSubmitObserver = new FormSubmitObserver(this, element);
2904 }
2905
2906 start() {
2907 this.linkInterceptor.start();
2908 this.formSubmitObserver.start();
2909 }
2910
2911 stop() {
2912 this.linkInterceptor.stop();
2913 this.formSubmitObserver.stop();
2914 }
2915
2916 // Link interceptor delegate
2917
2918 shouldInterceptLinkClick(element, _location, _event) {
2919 return this.#shouldRedirect(element)
2920 }
2921
2922 linkClickIntercepted(element, url, event) {
2923 const frame = this.#findFrameElement(element);
2924 if (frame) {
2925 frame.delegate.linkClickIntercepted(element, url, event);
2926 }
2927 }
2928
2929 // Form submit observer delegate
2930
2931 willSubmitForm(element, submitter) {
2932 return (
2933 element.closest("turbo-frame") == null &&
2934 this.#shouldSubmit(element, submitter) &&
2935 this.#shouldRedirect(element, submitter)
2936 )
2937 }
2938
2939 formSubmitted(element, submitter) {
2940 const frame = this.#findFrameElement(element, submitter);
2941 if (frame) {
2942 frame.delegate.formSubmitted(element, submitter);
2943 }
2944 }
2945
2946 #shouldSubmit(form, submitter) {
2947 const action = getAction$1(form, submitter);
2948 const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
2949 const rootLocation = expandURL(meta?.content ?? "/");
2950
2951 return this.#shouldRedirect(form, submitter) && locationIsVisitable(action, rootLocation)
2952 }
2953
2954 #shouldRedirect(element, submitter) {
2955 const isNavigatable =
2956 element instanceof HTMLFormElement
2957 ? this.session.submissionIsNavigatable(element, submitter)
2958 : this.session.elementIsNavigatable(element);
2959
2960 if (isNavigatable) {
2961 const frame = this.#findFrameElement(element, submitter);
2962 return frame ? frame != element.closest("turbo-frame") : false
2963 } else {
2964 return false
2965 }
2966 }
2967
2968 #findFrameElement(element, submitter) {
2969 const id = submitter?.getAttribute("data-turbo-frame") || element.getAttribute("data-turbo-frame");
2970 if (id && id != "_top") {
2971 const frame = this.element.querySelector(`#${id}:not([disabled])`);
2972 if (frame instanceof FrameElement) {
2973 return frame
2974 }
2975 }
2976 }
2977 }
2978
2979 class History {
2980 location
2981 restorationIdentifier = uuid()
2982 restorationData = {}
2983 started = false
2984 pageLoaded = false
2985 currentIndex = 0
2986
2987 constructor(delegate) {
2988 this.delegate = delegate;
2989 }
2990
2991 start() {
2992 if (!this.started) {
2993 addEventListener("popstate", this.onPopState, false);
2994 addEventListener("load", this.onPageLoad, false);
2995 this.currentIndex = history.state?.turbo?.restorationIndex || 0;
2996 this.started = true;
2997 this.replace(new URL(window.location.href));
2998 }
2999 }
3000
3001 stop() {
3002 if (this.started) {
3003 removeEventListener("popstate", this.onPopState, false);
3004 removeEventListener("load", this.onPageLoad, false);
3005 this.started = false;
3006 }
3007 }
3008
3009 push(location, restorationIdentifier) {
3010 this.update(history.pushState, location, restorationIdentifier);
3011 }
3012
3013 replace(location, restorationIdentifier) {
3014 this.update(history.replaceState, location, restorationIdentifier);
3015 }
3016
3017 update(method, location, restorationIdentifier = uuid()) {
3018 if (method === history.pushState) ++this.currentIndex;
3019
3020 const state = { turbo: { restorationIdentifier, restorationIndex: this.currentIndex } };
3021 method.call(history, state, "", location.href);
3022 this.location = location;
3023 this.restorationIdentifier = restorationIdentifier;
3024 }
3025
3026 // Restoration data
3027
3028 getRestorationDataForIdentifier(restorationIdentifier) {
3029 return this.restorationData[restorationIdentifier] || {}
3030 }
3031
3032 updateRestorationData(additionalData) {
3033 const { restorationIdentifier } = this;
3034 const restorationData = this.restorationData[restorationIdentifier];
3035 this.restorationData[restorationIdentifier] = {
3036 ...restorationData,
3037 ...additionalData
3038 };
3039 }
3040
3041 // Scroll restoration
3042
3043 assumeControlOfScrollRestoration() {
3044 if (!this.previousScrollRestoration) {
3045 this.previousScrollRestoration = history.scrollRestoration ?? "auto";
3046 history.scrollRestoration = "manual";
3047 }
3048 }
3049
3050 relinquishControlOfScrollRestoration() {
3051 if (this.previousScrollRestoration) {
3052 history.scrollRestoration = this.previousScrollRestoration;
3053 delete this.previousScrollRestoration;
3054 }
3055 }
3056
3057 // Event handlers
3058
3059 onPopState = (event) => {
3060 if (this.shouldHandlePopState()) {
3061 const { turbo } = event.state || {};
3062 if (turbo) {
3063 this.location = new URL(window.location.href);
3064 const { restorationIdentifier, restorationIndex } = turbo;
3065 this.restorationIdentifier = restorationIdentifier;
3066 const direction = restorationIndex > this.currentIndex ? "forward" : "back";
3067 this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(this.location, restorationIdentifier, direction);
3068 this.currentIndex = restorationIndex;
3069 }
3070 }
3071 }
3072
3073 onPageLoad = async (_event) => {
3074 await nextMicrotask();
3075 this.pageLoaded = true;
3076 }
3077
3078 // Private
3079
3080 shouldHandlePopState() {
3081 // Safari dispatches a popstate event after window's load event, ignore it
3082 return this.pageIsLoaded()
3083 }
3084
3085 pageIsLoaded() {
3086 return this.pageLoaded || document.readyState == "complete"
3087 }
3088 }
3089
3090 class LinkPrefetchObserver {
3091 started = false
3092 #prefetchedLink = null
3093
3094 constructor(delegate, eventTarget) {
3095 this.delegate = delegate;
3096 this.eventTarget = eventTarget;
3097 }
3098
3099 start() {
3100 if (this.started) return
3101
3102 if (this.eventTarget.readyState === "loading") {
3103 this.eventTarget.addEventListener("DOMContentLoaded", this.#enable, { once: true });
3104 } else {
3105 this.#enable();
3106 }
3107 }
3108
3109 stop() {
3110 if (!this.started) return
3111
3112 this.eventTarget.removeEventListener("mouseenter", this.#tryToPrefetchRequest, {
3113 capture: true,
3114 passive: true
3115 });
3116 this.eventTarget.removeEventListener("mouseleave", this.#cancelRequestIfObsolete, {
3117 capture: true,
3118 passive: true
3119 });
3120
3121 this.eventTarget.removeEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true);
3122 this.started = false;
3123 }
3124
3125 #enable = () => {
3126 this.eventTarget.addEventListener("mouseenter", this.#tryToPrefetchRequest, {
3127 capture: true,
3128 passive: true
3129 });
3130 this.eventTarget.addEventListener("mouseleave", this.#cancelRequestIfObsolete, {
3131 capture: true,
3132 passive: true
3133 });
3134
3135 this.eventTarget.addEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true);
3136 this.started = true;
3137 }
3138
3139 #tryToPrefetchRequest = (event) => {
3140 if (getMetaContent("turbo-prefetch") === "false") return
3141
3142 const target = event.target;
3143 const isLink = target.matches && target.matches("a[href]:not([target^=_]):not([download])");
3144
3145 if (isLink && this.#isPrefetchable(target)) {
3146 const link = target;
3147 const location = getLocationForLink(link);
3148
3149 if (this.delegate.canPrefetchRequestToLocation(link, location)) {
3150 this.#prefetchedLink = link;
3151
3152 const fetchRequest = new FetchRequest(
3153 this,
3154 FetchMethod.get,
3155 location,
3156 new URLSearchParams(),
3157 target
3158 );
3159
3160 prefetchCache.setLater(location.toString(), fetchRequest, this.#cacheTtl);
3161 }
3162 }
3163 }
3164
3165 #cancelRequestIfObsolete = (event) => {
3166 if (event.target === this.#prefetchedLink) this.#cancelPrefetchRequest();
3167 }
3168
3169 #cancelPrefetchRequest = () => {
3170 prefetchCache.clear();
3171 this.#prefetchedLink = null;
3172 }
3173
3174 #tryToUsePrefetchedRequest = (event) => {
3175 if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "get") {
3176 const cached = prefetchCache.get(event.detail.url.toString());
3177
3178 if (cached) {
3179 // User clicked link, use cache response
3180 event.detail.fetchRequest = cached;
3181 }
3182
3183 prefetchCache.clear();
3184 }
3185 }
3186
3187 prepareRequest(request) {
3188 const link = request.target;
3189
3190 request.headers["X-Sec-Purpose"] = "prefetch";
3191
3192 const turboFrame = link.closest("turbo-frame");
3193 const turboFrameTarget = link.getAttribute("data-turbo-frame") || turboFrame?.getAttribute("target") || turboFrame?.id;
3194
3195 if (turboFrameTarget && turboFrameTarget !== "_top") {
3196 request.headers["Turbo-Frame"] = turboFrameTarget;
3197 }
3198 }
3199
3200 // Fetch request interface
3201
3202 requestSucceededWithResponse() {}
3203
3204 requestStarted(fetchRequest) {}
3205
3206 requestErrored(fetchRequest) {}
3207
3208 requestFinished(fetchRequest) {}
3209
3210 requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}
3211
3212 requestFailedWithResponse(fetchRequest, fetchResponse) {}
3213
3214 get #cacheTtl() {
3215 return Number(getMetaContent("turbo-prefetch-cache-time")) || cacheTtl
3216 }
3217
3218 #isPrefetchable(link) {
3219 const href = link.getAttribute("href");
3220
3221 if (!href) return false
3222
3223 if (unfetchableLink(link)) return false
3224 if (linkToTheSamePage(link)) return false
3225 if (linkOptsOut(link)) return false
3226 if (nonSafeLink(link)) return false
3227 if (eventPrevented(link)) return false
3228
3229 return true
3230 }
3231 }
3232
3233 const unfetchableLink = (link) => {
3234 return link.origin !== document.location.origin || !["http:", "https:"].includes(link.protocol) || link.hasAttribute("target")
3235 };
3236
3237 const linkToTheSamePage = (link) => {
3238 return (link.pathname + link.search === document.location.pathname + document.location.search) || link.href.startsWith("#")
3239 };
3240
3241 const linkOptsOut = (link) => {
3242 if (link.getAttribute("data-turbo-prefetch") === "false") return true
3243 if (link.getAttribute("data-turbo") === "false") return true
3244
3245 const turboPrefetchParent = findClosestRecursively(link, "[data-turbo-prefetch]");
3246 if (turboPrefetchParent && turboPrefetchParent.getAttribute("data-turbo-prefetch") === "false") return true
3247
3248 return false
3249 };
3250
3251 const nonSafeLink = (link) => {
3252 const turboMethod = link.getAttribute("data-turbo-method");
3253 if (turboMethod && turboMethod.toLowerCase() !== "get") return true
3254
3255 if (isUJS(link)) return true
3256 if (link.hasAttribute("data-turbo-confirm")) return true
3257 if (link.hasAttribute("data-turbo-stream")) return true
3258
3259 return false
3260 };
3261
3262 const isUJS = (link) => {
3263 return link.hasAttribute("data-remote") || link.hasAttribute("data-behavior") || link.hasAttribute("data-confirm") || link.hasAttribute("data-method")
3264 };
3265
3266 const eventPrevented = (link) => {
3267 const event = dispatch("turbo:before-prefetch", { target: link, cancelable: true });
3268 return event.defaultPrevented
3269 };
3270
3271 class Navigator {
3272 constructor(delegate) {
3273 this.delegate = delegate;
3274 }
3275
3276 proposeVisit(location, options = {}) {
3277 if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
3278 this.delegate.visitProposedToLocation(location, options);
3279 }
3280 }
3281
3282 startVisit(locatable, restorationIdentifier, options = {}) {
3283 this.stop();
3284 this.currentVisit = new Visit(this, expandURL(locatable), restorationIdentifier, {
3285 referrer: this.location,
3286 ...options
3287 });
3288 this.currentVisit.start();
3289 }
3290
3291 submitForm(form, submitter) {
3292 this.stop();
3293 this.formSubmission = new FormSubmission(this, form, submitter, true);
3294
3295 this.formSubmission.start();
3296 }
3297
3298 stop() {
3299 if (this.formSubmission) {
3300 this.formSubmission.stop();
3301 delete this.formSubmission;
3302 }
3303
3304 if (this.currentVisit) {
3305 this.currentVisit.cancel();
3306 delete this.currentVisit;
3307 }
3308 }
3309
3310 get adapter() {
3311 return this.delegate.adapter
3312 }
3313
3314 get view() {
3315 return this.delegate.view
3316 }
3317
3318 get rootLocation() {
3319 return this.view.snapshot.rootLocation
3320 }
3321
3322 get history() {
3323 return this.delegate.history
3324 }
3325
3326 // Form submission delegate
3327
3328 formSubmissionStarted(formSubmission) {
3329 // Not all adapters implement formSubmissionStarted
3330 if (typeof this.adapter.formSubmissionStarted === "function") {
3331 this.adapter.formSubmissionStarted(formSubmission);
3332 }
3333 }
3334
3335 async formSubmissionSucceededWithResponse(formSubmission, fetchResponse) {
3336 if (formSubmission == this.formSubmission) {
3337 const responseHTML = await fetchResponse.responseHTML;
3338 if (responseHTML) {
3339 const shouldCacheSnapshot = formSubmission.isSafe;
3340 if (!shouldCacheSnapshot) {
3341 this.view.clearSnapshotCache();
3342 }
3343
3344 const { statusCode, redirected } = fetchResponse;
3345 const action = this.#getActionForFormSubmission(formSubmission, fetchResponse);
3346 const visitOptions = {
3347 action,
3348 shouldCacheSnapshot,
3349 response: { statusCode, responseHTML, redirected }
3350 };
3351 this.proposeVisit(fetchResponse.location, visitOptions);
3352 }
3353 }
3354 }
3355
3356 async formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
3357 const responseHTML = await fetchResponse.responseHTML;
3358
3359 if (responseHTML) {
3360 const snapshot = PageSnapshot.fromHTMLString(responseHTML);
3361 if (fetchResponse.serverError) {
3362 await this.view.renderError(snapshot, this.currentVisit);
3363 } else {
3364 await this.view.renderPage(snapshot, false, true, this.currentVisit);
3365 }
3366 if(!snapshot.shouldPreserveScrollPosition) {
3367 this.view.scrollToTop();
3368 }
3369 this.view.clearSnapshotCache();
3370 }
3371 }
3372
3373 formSubmissionErrored(formSubmission, error) {
3374 console.error(error);
3375 }
3376
3377 formSubmissionFinished(formSubmission) {
3378 // Not all adapters implement formSubmissionFinished
3379 if (typeof this.adapter.formSubmissionFinished === "function") {
3380 this.adapter.formSubmissionFinished(formSubmission);
3381 }
3382 }
3383
3384 // Visit delegate
3385
3386 visitStarted(visit) {
3387 this.delegate.visitStarted(visit);
3388 }
3389
3390 visitCompleted(visit) {
3391 this.delegate.visitCompleted(visit);
3392 }
3393
3394 locationWithActionIsSamePage(location, action) {
3395 const anchor = getAnchor(location);
3396 const currentAnchor = getAnchor(this.view.lastRenderedLocation);
3397 const isRestorationToTop = action === "restore" && typeof anchor === "undefined";
3398
3399 return (
3400 action !== "replace" &&
3401 getRequestURL(location) === getRequestURL(this.view.lastRenderedLocation) &&
3402 (isRestorationToTop || (anchor != null && anchor !== currentAnchor))
3403 )
3404 }
3405
3406 visitScrolledToSamePageLocation(oldURL, newURL) {
3407 this.delegate.visitScrolledToSamePageLocation(oldURL, newURL);
3408 }
3409
3410 // Visits
3411
3412 get location() {
3413 return this.history.location
3414 }
3415
3416 get restorationIdentifier() {
3417 return this.history.restorationIdentifier
3418 }
3419
3420 #getActionForFormSubmission(formSubmission, fetchResponse) {
3421 const { submitter, formElement } = formSubmission;
3422 return getVisitAction(submitter, formElement) || this.#getDefaultAction(fetchResponse)
3423 }
3424
3425 #getDefaultAction(fetchResponse) {
3426 const sameLocationRedirect = fetchResponse.redirected && fetchResponse.location.href === this.location?.href;
3427 return sameLocationRedirect ? "replace" : "advance"
3428 }
3429 }
3430
3431 const PageStage = {
3432 initial: 0,
3433 loading: 1,
3434 interactive: 2,
3435 complete: 3
3436 };
3437
3438 class PageObserver {
3439 stage = PageStage.initial
3440 started = false
3441
3442 constructor(delegate) {
3443 this.delegate = delegate;
3444 }
3445
3446 start() {
3447 if (!this.started) {
3448 if (this.stage == PageStage.initial) {
3449 this.stage = PageStage.loading;
3450 }
3451 document.addEventListener("readystatechange", this.interpretReadyState, false);
3452 addEventListener("pagehide", this.pageWillUnload, false);
3453 this.started = true;
3454 }
3455 }
3456
3457 stop() {
3458 if (this.started) {
3459 document.removeEventListener("readystatechange", this.interpretReadyState, false);
3460 removeEventListener("pagehide", this.pageWillUnload, false);
3461 this.started = false;
3462 }
3463 }
3464
3465 interpretReadyState = () => {
3466 const { readyState } = this;
3467 if (readyState == "interactive") {
3468 this.pageIsInteractive();
3469 } else if (readyState == "complete") {
3470 this.pageIsComplete();
3471 }
3472 }
3473
3474 pageIsInteractive() {
3475 if (this.stage == PageStage.loading) {
3476 this.stage = PageStage.interactive;
3477 this.delegate.pageBecameInteractive();
3478 }
3479 }
3480
3481 pageIsComplete() {
3482 this.pageIsInteractive();
3483 if (this.stage == PageStage.interactive) {
3484 this.stage = PageStage.complete;
3485 this.delegate.pageLoaded();
3486 }
3487 }
3488
3489 pageWillUnload = () => {
3490 this.delegate.pageWillUnload();
3491 }
3492
3493 get readyState() {
3494 return document.readyState
3495 }
3496 }
3497
3498 class ScrollObserver {
3499 started = false
3500
3501 constructor(delegate) {
3502 this.delegate = delegate;
3503 }
3504
3505 start() {
3506 if (!this.started) {
3507 addEventListener("scroll", this.onScroll, false);
3508 this.onScroll();
3509 this.started = true;
3510 }
3511 }
3512
3513 stop() {
3514 if (this.started) {
3515 removeEventListener("scroll", this.onScroll, false);
3516 this.started = false;
3517 }
3518 }
3519
3520 onScroll = () => {
3521 this.updatePosition({ x: window.pageXOffset, y: window.pageYOffset });
3522 }
3523
3524 // Private
3525
3526 updatePosition(position) {
3527 this.delegate.scrollPositionChanged(position);
3528 }
3529 }
3530
3531 class StreamMessageRenderer {
3532 render({ fragment }) {
3533 Bardo.preservingPermanentElements(this, getPermanentElementMapForFragment(fragment), () => {
3534 withAutofocusFromFragment(fragment, () => {
3535 withPreservedFocus(() => {
3536 document.documentElement.appendChild(fragment);
3537 });
3538 });
3539 });
3540 }
3541
3542 // Bardo delegate
3543
3544 enteringBardo(currentPermanentElement, newPermanentElement) {
3545 newPermanentElement.replaceWith(currentPermanentElement.cloneNode(true));
3546 }
3547
3548 leavingBardo() {}
3549 }
3550
3551 function getPermanentElementMapForFragment(fragment) {
3552 const permanentElementsInDocument = queryPermanentElementsAll(document.documentElement);
3553 const permanentElementMap = {};
3554 for (const permanentElementInDocument of permanentElementsInDocument) {
3555 const { id } = permanentElementInDocument;
3556
3557 for (const streamElement of fragment.querySelectorAll("turbo-stream")) {
3558 const elementInStream = getPermanentElementById(streamElement.templateElement.content, id);
3559
3560 if (elementInStream) {
3561 permanentElementMap[id] = [permanentElementInDocument, elementInStream];
3562 }
3563 }
3564 }
3565
3566 return permanentElementMap
3567 }
3568
3569 async function withAutofocusFromFragment(fragment, callback) {
3570 const generatedID = `turbo-stream-autofocus-${uuid()}`;
3571 const turboStreams = fragment.querySelectorAll("turbo-stream");
3572 const elementWithAutofocus = firstAutofocusableElementInStreams(turboStreams);
3573 let willAutofocusId = null;
3574
3575 if (elementWithAutofocus) {
3576 if (elementWithAutofocus.id) {
3577 willAutofocusId = elementWithAutofocus.id;
3578 } else {
3579 willAutofocusId = generatedID;
3580 }
3581
3582 elementWithAutofocus.id = willAutofocusId;
3583 }
3584
3585 callback();
3586 await nextRepaint();
3587
3588 const hasNoActiveElement = document.activeElement == null || document.activeElement == document.body;
3589
3590 if (hasNoActiveElement && willAutofocusId) {
3591 const elementToAutofocus = document.getElementById(willAutofocusId);
3592
3593 if (elementIsFocusable(elementToAutofocus)) {
3594 elementToAutofocus.focus();
3595 }
3596 if (elementToAutofocus && elementToAutofocus.id == generatedID) {
3597 elementToAutofocus.removeAttribute("id");
3598 }
3599 }
3600 }
3601
3602 async function withPreservedFocus(callback) {
3603 const [activeElementBeforeRender, activeElementAfterRender] = await around(callback, () => document.activeElement);
3604
3605 const restoreFocusTo = activeElementBeforeRender && activeElementBeforeRender.id;
3606
3607 if (restoreFocusTo) {
3608 const elementToFocus = document.getElementById(restoreFocusTo);
3609
3610 if (elementIsFocusable(elementToFocus) && elementToFocus != activeElementAfterRender) {
3611 elementToFocus.focus();
3612 }
3613 }
3614 }
3615
3616 function firstAutofocusableElementInStreams(nodeListOfStreamElements) {
3617 for (const streamElement of nodeListOfStreamElements) {
3618 const elementWithAutofocus = queryAutofocusableElement(streamElement.templateElement.content);
3619
3620 if (elementWithAutofocus) return elementWithAutofocus
3621 }
3622
3623 return null
3624 }
3625
3626 class StreamObserver {
3627 sources = new Set()
3628 #started = false
3629
3630 constructor(delegate) {
3631 this.delegate = delegate;
3632 }
3633
3634 start() {
3635 if (!this.#started) {
3636 this.#started = true;
3637 addEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
3638 }
3639 }
3640
3641 stop() {
3642 if (this.#started) {
3643 this.#started = false;
3644 removeEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
3645 }
3646 }
3647
3648 connectStreamSource(source) {
3649 if (!this.streamSourceIsConnected(source)) {
3650 this.sources.add(source);
3651 source.addEventListener("message", this.receiveMessageEvent, false);
3652 }
3653 }
3654
3655 disconnectStreamSource(source) {
3656 if (this.streamSourceIsConnected(source)) {
3657 this.sources.delete(source);
3658 source.removeEventListener("message", this.receiveMessageEvent, false);
3659 }
3660 }
3661
3662 streamSourceIsConnected(source) {
3663 return this.sources.has(source)
3664 }
3665
3666 inspectFetchResponse = (event) => {
3667 const response = fetchResponseFromEvent(event);
3668 if (response && fetchResponseIsStream(response)) {
3669 event.preventDefault();
3670 this.receiveMessageResponse(response);
3671 }
3672 }
3673
3674 receiveMessageEvent = (event) => {
3675 if (this.#started && typeof event.data == "string") {
3676 this.receiveMessageHTML(event.data);
3677 }
3678 }
3679
3680 async receiveMessageResponse(response) {
3681 const html = await response.responseHTML;
3682 if (html) {
3683 this.receiveMessageHTML(html);
3684 }
3685 }
3686
3687 receiveMessageHTML(html) {
3688 this.delegate.receivedMessageFromStream(StreamMessage.wrap(html));
3689 }
3690 }
3691
3692 function fetchResponseFromEvent(event) {
3693 const fetchResponse = event.detail?.fetchResponse;
3694 if (fetchResponse instanceof FetchResponse) {
3695 return fetchResponse
3696 }
3697 }
3698
3699 function fetchResponseIsStream(response) {
3700 const contentType = response.contentType ?? "";
3701 return contentType.startsWith(StreamMessage.contentType)
3702 }
3703
3704 class ErrorRenderer extends Renderer {
3705 static renderElement(currentElement, newElement) {
3706 const { documentElement, body } = document;
3707
3708 documentElement.replaceChild(newElement, body);
3709 }
3710
3711 async render() {
3712 this.replaceHeadAndBody();
3713 this.activateScriptElements();
3714 }
3715
3716 replaceHeadAndBody() {
3717 const { documentElement, head } = document;
3718 documentElement.replaceChild(this.newHead, head);
3719 this.renderElement(this.currentElement, this.newElement);
3720 }
3721
3722 activateScriptElements() {
3723 for (const replaceableElement of this.scriptElements) {
3724 const parentNode = replaceableElement.parentNode;
3725 if (parentNode) {
3726 const element = activateScriptElement(replaceableElement);
3727 parentNode.replaceChild(element, replaceableElement);
3728 }
3729 }
3730 }
3731
3732 get newHead() {
3733 return this.newSnapshot.headSnapshot.element
3734 }
3735
3736 get scriptElements() {
3737 return document.documentElement.querySelectorAll("script")
3738 }
3739 }
3740
3741 // base IIFE to define idiomorph
3742 var Idiomorph = (function () {
3743
3744 //=============================================================================
3745 // AND NOW IT BEGINS...
3746 //=============================================================================
3747 let EMPTY_SET = new Set();
3748
3749 // default configuration values, updatable by users now
3750 let defaults = {
3751 morphStyle: "outerHTML",
3752 callbacks : {
3753 beforeNodeAdded: noOp,
3754 afterNodeAdded: noOp,
3755 beforeNodeMorphed: noOp,
3756 afterNodeMorphed: noOp,
3757 beforeNodeRemoved: noOp,
3758 afterNodeRemoved: noOp,
3759 beforeAttributeUpdated: noOp,
3760
3761 },
3762 head: {
3763 style: 'merge',
3764 shouldPreserve: function (elt) {
3765 return elt.getAttribute("im-preserve") === "true";
3766 },
3767 shouldReAppend: function (elt) {
3768 return elt.getAttribute("im-re-append") === "true";
3769 },
3770 shouldRemove: noOp,
3771 afterHeadMorphed: noOp,
3772 }
3773 };
3774
3775 //=============================================================================
3776 // Core Morphing Algorithm - morph, morphNormalizedContent, morphOldNodeTo, morphChildren
3777 //=============================================================================
3778 function morph(oldNode, newContent, config = {}) {
3779
3780 if (oldNode instanceof Document) {
3781 oldNode = oldNode.documentElement;
3782 }
3783
3784 if (typeof newContent === 'string') {
3785 newContent = parseContent(newContent);
3786 }
3787
3788 let normalizedContent = normalizeContent(newContent);
3789
3790 let ctx = createMorphContext(oldNode, normalizedContent, config);
3791
3792 return morphNormalizedContent(oldNode, normalizedContent, ctx);
3793 }
3794
3795 function morphNormalizedContent(oldNode, normalizedNewContent, ctx) {
3796 if (ctx.head.block) {
3797 let oldHead = oldNode.querySelector('head');
3798 let newHead = normalizedNewContent.querySelector('head');
3799 if (oldHead && newHead) {
3800 let promises = handleHeadElement(newHead, oldHead, ctx);
3801 // when head promises resolve, call morph again, ignoring the head tag
3802 Promise.all(promises).then(function () {
3803 morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, {
3804 head: {
3805 block: false,
3806 ignore: true
3807 }
3808 }));
3809 });
3810 return;
3811 }
3812 }
3813
3814 if (ctx.morphStyle === "innerHTML") {
3815
3816 // innerHTML, so we are only updating the children
3817 morphChildren(normalizedNewContent, oldNode, ctx);
3818 return oldNode.children;
3819
3820 } else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) {
3821 // otherwise find the best element match in the new content, morph that, and merge its siblings
3822 // into either side of the best match
3823 let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx);
3824
3825 // stash the siblings that will need to be inserted on either side of the best match
3826 let previousSibling = bestMatch?.previousSibling;
3827 let nextSibling = bestMatch?.nextSibling;
3828
3829 // morph it
3830 let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx);
3831
3832 if (bestMatch) {
3833 // if there was a best match, merge the siblings in too and return the
3834 // whole bunch
3835 return insertSiblings(previousSibling, morphedNode, nextSibling);
3836 } else {
3837 // otherwise nothing was added to the DOM
3838 return []
3839 }
3840 } else {
3841 throw "Do not understand how to morph style " + ctx.morphStyle;
3842 }
3843 }
3844
3845
3846 /**
3847 * @param possibleActiveElement
3848 * @param ctx
3849 * @returns {boolean}
3850 */
3851 function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
3852 return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement && possibleActiveElement !== document.body;
3853 }
3854
3855 /**
3856 * @param oldNode root node to merge content into
3857 * @param newContent new content to merge
3858 * @param ctx the merge context
3859 * @returns {Element} the element that ended up in the DOM
3860 */
3861 function morphOldNodeTo(oldNode, newContent, ctx) {
3862 if (ctx.ignoreActive && oldNode === document.activeElement) ; else if (newContent == null) {
3863 if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
3864
3865 oldNode.remove();
3866 ctx.callbacks.afterNodeRemoved(oldNode);
3867 return null;
3868 } else if (!isSoftMatch(oldNode, newContent)) {
3869 if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
3870 if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode;
3871
3872 oldNode.parentElement.replaceChild(newContent, oldNode);
3873 ctx.callbacks.afterNodeAdded(newContent);
3874 ctx.callbacks.afterNodeRemoved(oldNode);
3875 return newContent;
3876 } else {
3877 if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return oldNode;
3878
3879 if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) ; else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") {
3880 handleHeadElement(newContent, oldNode, ctx);
3881 } else {
3882 syncNodeFrom(newContent, oldNode, ctx);
3883 if (!ignoreValueOfActiveElement(oldNode, ctx)) {
3884 morphChildren(newContent, oldNode, ctx);
3885 }
3886 }
3887 ctx.callbacks.afterNodeMorphed(oldNode, newContent);
3888 return oldNode;
3889 }
3890 }
3891
3892 /**
3893 * This is the core algorithm for matching up children. The idea is to use id sets to try to match up
3894 * nodes as faithfully as possible. We greedily match, which allows us to keep the algorithm fast, but
3895 * by using id sets, we are able to better match up with content deeper in the DOM.
3896 *
3897 * Basic algorithm is, for each node in the new content:
3898 *
3899 * - if we have reached the end of the old parent, append the new content
3900 * - if the new content has an id set match with the current insertion point, morph
3901 * - search for an id set match
3902 * - if id set match found, morph
3903 * - otherwise search for a "soft" match
3904 * - if a soft match is found, morph
3905 * - otherwise, prepend the new node before the current insertion point
3906 *
3907 * The two search algorithms terminate if competing node matches appear to outweigh what can be achieved
3908 * with the current node. See findIdSetMatch() and findSoftMatch() for details.
3909 *
3910 * @param {Element} newParent the parent element of the new content
3911 * @param {Element } oldParent the old content that we are merging the new content into
3912 * @param ctx the merge context
3913 */
3914 function morphChildren(newParent, oldParent, ctx) {
3915
3916 let nextNewChild = newParent.firstChild;
3917 let insertionPoint = oldParent.firstChild;
3918 let newChild;
3919
3920 // run through all the new content
3921 while (nextNewChild) {
3922
3923 newChild = nextNewChild;
3924 nextNewChild = newChild.nextSibling;
3925
3926 // if we are at the end of the exiting parent's children, just append
3927 if (insertionPoint == null) {
3928 if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
3929
3930 oldParent.appendChild(newChild);
3931 ctx.callbacks.afterNodeAdded(newChild);
3932 removeIdsFromConsideration(ctx, newChild);
3933 continue;
3934 }
3935
3936 // if the current node has an id set match then morph
3937 if (isIdSetMatch(newChild, insertionPoint, ctx)) {
3938 morphOldNodeTo(insertionPoint, newChild, ctx);
3939 insertionPoint = insertionPoint.nextSibling;
3940 removeIdsFromConsideration(ctx, newChild);
3941 continue;
3942 }
3943
3944 // otherwise search forward in the existing old children for an id set match
3945 let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx);
3946
3947 // if we found a potential match, remove the nodes until that point and morph
3948 if (idSetMatch) {
3949 insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx);
3950 morphOldNodeTo(idSetMatch, newChild, ctx);
3951 removeIdsFromConsideration(ctx, newChild);
3952 continue;
3953 }
3954
3955 // no id set match found, so scan forward for a soft match for the current node
3956 let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx);
3957
3958 // if we found a soft match for the current node, morph
3959 if (softMatch) {
3960 insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx);
3961 morphOldNodeTo(softMatch, newChild, ctx);
3962 removeIdsFromConsideration(ctx, newChild);
3963 continue;
3964 }
3965
3966 // abandon all hope of morphing, just insert the new child before the insertion point
3967 // and move on
3968 if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
3969
3970 oldParent.insertBefore(newChild, insertionPoint);
3971 ctx.callbacks.afterNodeAdded(newChild);
3972 removeIdsFromConsideration(ctx, newChild);
3973 }
3974
3975 // remove any remaining old nodes that didn't match up with new content
3976 while (insertionPoint !== null) {
3977
3978 let tempNode = insertionPoint;
3979 insertionPoint = insertionPoint.nextSibling;
3980 removeNode(tempNode, ctx);
3981 }
3982 }
3983
3984 //=============================================================================
3985 // Attribute Syncing Code
3986 //=============================================================================
3987
3988 /**
3989 * @param attr {String} the attribute to be mutated
3990 * @param to {Element} the element that is going to be updated
3991 * @param updateType {("update"|"remove")}
3992 * @param ctx the merge context
3993 * @returns {boolean} true if the attribute should be ignored, false otherwise
3994 */
3995 function ignoreAttribute(attr, to, updateType, ctx) {
3996 if(attr === 'value' && ctx.ignoreActiveValue && to === document.activeElement){
3997 return true;
3998 }
3999 return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false;
4000 }
4001
4002 /**
4003 * syncs a given node with another node, copying over all attributes and
4004 * inner element state from the 'from' node to the 'to' node
4005 *
4006 * @param {Element} from the element to copy attributes & state from
4007 * @param {Element} to the element to copy attributes & state to
4008 * @param ctx the merge context
4009 */
4010 function syncNodeFrom(from, to, ctx) {
4011 let type = from.nodeType;
4012
4013 // if is an element type, sync the attributes from the
4014 // new node into the new node
4015 if (type === 1 /* element type */) {
4016 const fromAttributes = from.attributes;
4017 const toAttributes = to.attributes;
4018 for (const fromAttribute of fromAttributes) {
4019 if (ignoreAttribute(fromAttribute.name, to, 'update', ctx)) {
4020 continue;
4021 }
4022 if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) {
4023 to.setAttribute(fromAttribute.name, fromAttribute.value);
4024 }
4025 }
4026 // iterate backwards to avoid skipping over items when a delete occurs
4027 for (let i = toAttributes.length - 1; 0 <= i; i--) {
4028 const toAttribute = toAttributes[i];
4029 if (ignoreAttribute(toAttribute.name, to, 'remove', ctx)) {
4030 continue;
4031 }
4032 if (!from.hasAttribute(toAttribute.name)) {
4033 to.removeAttribute(toAttribute.name);
4034 }
4035 }
4036 }
4037
4038 // sync text nodes
4039 if (type === 8 /* comment */ || type === 3 /* text */) {
4040 if (to.nodeValue !== from.nodeValue) {
4041 to.nodeValue = from.nodeValue;
4042 }
4043 }
4044
4045 if (!ignoreValueOfActiveElement(to, ctx)) {
4046 // sync input values
4047 syncInputValue(from, to, ctx);
4048 }
4049 }
4050
4051 /**
4052 * @param from {Element} element to sync the value from
4053 * @param to {Element} element to sync the value to
4054 * @param attributeName {String} the attribute name
4055 * @param ctx the merge context
4056 */
4057 function syncBooleanAttribute(from, to, attributeName, ctx) {
4058 if (from[attributeName] !== to[attributeName]) {
4059 let ignoreUpdate = ignoreAttribute(attributeName, to, 'update', ctx);
4060 if (!ignoreUpdate) {
4061 to[attributeName] = from[attributeName];
4062 }
4063 if (from[attributeName]) {
4064 if (!ignoreUpdate) {
4065 to.setAttribute(attributeName, from[attributeName]);
4066 }
4067 } else {
4068 if (!ignoreAttribute(attributeName, to, 'remove', ctx)) {
4069 to.removeAttribute(attributeName);
4070 }
4071 }
4072 }
4073 }
4074
4075 /**
4076 * NB: many bothans died to bring us information:
4077 *
4078 * https://github.com/patrick-steele-idem/morphdom/blob/master/src/specialElHandlers.js
4079 * https://github.com/choojs/nanomorph/blob/master/lib/morph.jsL113
4080 *
4081 * @param from {Element} the element to sync the input value from
4082 * @param to {Element} the element to sync the input value to
4083 * @param ctx the merge context
4084 */
4085 function syncInputValue(from, to, ctx) {
4086 if (from instanceof HTMLInputElement &&
4087 to instanceof HTMLInputElement &&
4088 from.type !== 'file') {
4089
4090 let fromValue = from.value;
4091 let toValue = to.value;
4092
4093 // sync boolean attributes
4094 syncBooleanAttribute(from, to, 'checked', ctx);
4095 syncBooleanAttribute(from, to, 'disabled', ctx);
4096
4097 if (!from.hasAttribute('value')) {
4098 if (!ignoreAttribute('value', to, 'remove', ctx)) {
4099 to.value = '';
4100 to.removeAttribute('value');
4101 }
4102 } else if (fromValue !== toValue) {
4103 if (!ignoreAttribute('value', to, 'update', ctx)) {
4104 to.setAttribute('value', fromValue);
4105 to.value = fromValue;
4106 }
4107 }
4108 } else if (from instanceof HTMLOptionElement) {
4109 syncBooleanAttribute(from, to, 'selected', ctx);
4110 } else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) {
4111 let fromValue = from.value;
4112 let toValue = to.value;
4113 if (ignoreAttribute('value', to, 'update', ctx)) {
4114 return;
4115 }
4116 if (fromValue !== toValue) {
4117 to.value = fromValue;
4118 }
4119 if (to.firstChild && to.firstChild.nodeValue !== fromValue) {
4120 to.firstChild.nodeValue = fromValue;
4121 }
4122 }
4123 }
4124
4125 //=============================================================================
4126 // the HEAD tag can be handled specially, either w/ a 'merge' or 'append' style
4127 //=============================================================================
4128 function handleHeadElement(newHeadTag, currentHead, ctx) {
4129
4130 let added = [];
4131 let removed = [];
4132 let preserved = [];
4133 let nodesToAppend = [];
4134
4135 let headMergeStyle = ctx.head.style;
4136
4137 // put all new head elements into a Map, by their outerHTML
4138 let srcToNewHeadNodes = new Map();
4139 for (const newHeadChild of newHeadTag.children) {
4140 srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
4141 }
4142
4143 // for each elt in the current head
4144 for (const currentHeadElt of currentHead.children) {
4145
4146 // If the current head element is in the map
4147 let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
4148 let isReAppended = ctx.head.shouldReAppend(currentHeadElt);
4149 let isPreserved = ctx.head.shouldPreserve(currentHeadElt);
4150 if (inNewContent || isPreserved) {
4151 if (isReAppended) {
4152 // remove the current version and let the new version replace it and re-execute
4153 removed.push(currentHeadElt);
4154 } else {
4155 // this element already exists and should not be re-appended, so remove it from
4156 // the new content map, preserving it in the DOM
4157 srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
4158 preserved.push(currentHeadElt);
4159 }
4160 } else {
4161 if (headMergeStyle === "append") {
4162 // we are appending and this existing element is not new content
4163 // so if and only if it is marked for re-append do we do anything
4164 if (isReAppended) {
4165 removed.push(currentHeadElt);
4166 nodesToAppend.push(currentHeadElt);
4167 }
4168 } else {
4169 // if this is a merge, we remove this content since it is not in the new head
4170 if (ctx.head.shouldRemove(currentHeadElt) !== false) {
4171 removed.push(currentHeadElt);
4172 }
4173 }
4174 }
4175 }
4176
4177 // Push the remaining new head elements in the Map into the
4178 // nodes to append to the head tag
4179 nodesToAppend.push(...srcToNewHeadNodes.values());
4180
4181 let promises = [];
4182 for (const newNode of nodesToAppend) {
4183 let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild;
4184 if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
4185 if (newElt.href || newElt.src) {
4186 let resolve = null;
4187 let promise = new Promise(function (_resolve) {
4188 resolve = _resolve;
4189 });
4190 newElt.addEventListener('load', function () {
4191 resolve();
4192 });
4193 promises.push(promise);
4194 }
4195 currentHead.appendChild(newElt);
4196 ctx.callbacks.afterNodeAdded(newElt);
4197 added.push(newElt);
4198 }
4199 }
4200
4201 // remove all removed elements, after we have appended the new elements to avoid
4202 // additional network requests for things like style sheets
4203 for (const removedElement of removed) {
4204 if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
4205 currentHead.removeChild(removedElement);
4206 ctx.callbacks.afterNodeRemoved(removedElement);
4207 }
4208 }
4209
4210 ctx.head.afterHeadMorphed(currentHead, {added: added, kept: preserved, removed: removed});
4211 return promises;
4212 }
4213
4214 function noOp() {
4215 }
4216
4217 /*
4218 Deep merges the config object and the Idiomoroph.defaults object to
4219 produce a final configuration object
4220 */
4221 function mergeDefaults(config) {
4222 let finalConfig = {};
4223 // copy top level stuff into final config
4224 Object.assign(finalConfig, defaults);
4225 Object.assign(finalConfig, config);
4226
4227 // copy callbacks into final config (do this to deep merge the callbacks)
4228 finalConfig.callbacks = {};
4229 Object.assign(finalConfig.callbacks, defaults.callbacks);
4230 Object.assign(finalConfig.callbacks, config.callbacks);
4231
4232 // copy head config into final config (do this to deep merge the head)
4233 finalConfig.head = {};
4234 Object.assign(finalConfig.head, defaults.head);
4235 Object.assign(finalConfig.head, config.head);
4236 return finalConfig;
4237 }
4238
4239 function createMorphContext(oldNode, newContent, config) {
4240 config = mergeDefaults(config);
4241 return {
4242 target: oldNode,
4243 newContent: newContent,
4244 config: config,
4245 morphStyle: config.morphStyle,
4246 ignoreActive: config.ignoreActive,
4247 ignoreActiveValue: config.ignoreActiveValue,
4248 idMap: createIdMap(oldNode, newContent),
4249 deadIds: new Set(),
4250 callbacks: config.callbacks,
4251 head: config.head
4252 }
4253 }
4254
4255 function isIdSetMatch(node1, node2, ctx) {
4256 if (node1 == null || node2 == null) {
4257 return false;
4258 }
4259 if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) {
4260 if (node1.id !== "" && node1.id === node2.id) {
4261 return true;
4262 } else {
4263 return getIdIntersectionCount(ctx, node1, node2) > 0;
4264 }
4265 }
4266 return false;
4267 }
4268
4269 function isSoftMatch(node1, node2) {
4270 if (node1 == null || node2 == null) {
4271 return false;
4272 }
4273 return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName
4274 }
4275
4276 function removeNodesBetween(startInclusive, endExclusive, ctx) {
4277 while (startInclusive !== endExclusive) {
4278 let tempNode = startInclusive;
4279 startInclusive = startInclusive.nextSibling;
4280 removeNode(tempNode, ctx);
4281 }
4282 removeIdsFromConsideration(ctx, endExclusive);
4283 return endExclusive.nextSibling;
4284 }
4285
4286 //=============================================================================
4287 // Scans forward from the insertionPoint in the old parent looking for a potential id match
4288 // for the newChild. We stop if we find a potential id match for the new child OR
4289 // if the number of potential id matches we are discarding is greater than the
4290 // potential id matches for the new child
4291 //=============================================================================
4292 function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
4293
4294 // max id matches we are willing to discard in our search
4295 let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent);
4296
4297 let potentialMatch = null;
4298
4299 // only search forward if there is a possibility of an id match
4300 if (newChildPotentialIdCount > 0) {
4301 let potentialMatch = insertionPoint;
4302 // if there is a possibility of an id match, scan forward
4303 // keep track of the potential id match count we are discarding (the
4304 // newChildPotentialIdCount must be greater than this to make it likely
4305 // worth it)
4306 let otherMatchCount = 0;
4307 while (potentialMatch != null) {
4308
4309 // If we have an id match, return the current potential match
4310 if (isIdSetMatch(newChild, potentialMatch, ctx)) {
4311 return potentialMatch;
4312 }
4313
4314 // computer the other potential matches of this new content
4315 otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent);
4316 if (otherMatchCount > newChildPotentialIdCount) {
4317 // if we have more potential id matches in _other_ content, we
4318 // do not have a good candidate for an id match, so return null
4319 return null;
4320 }
4321
4322 // advanced to the next old content child
4323 potentialMatch = potentialMatch.nextSibling;
4324 }
4325 }
4326 return potentialMatch;
4327 }
4328
4329 //=============================================================================
4330 // Scans forward from the insertionPoint in the old parent looking for a potential soft match
4331 // for the newChild. We stop if we find a potential soft match for the new child OR
4332 // if we find a potential id match in the old parents children OR if we find two
4333 // potential soft matches for the next two pieces of new content
4334 //=============================================================================
4335 function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
4336
4337 let potentialSoftMatch = insertionPoint;
4338 let nextSibling = newChild.nextSibling;
4339 let siblingSoftMatchCount = 0;
4340
4341 while (potentialSoftMatch != null) {
4342
4343 if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) {
4344 // the current potential soft match has a potential id set match with the remaining new
4345 // content so bail out of looking
4346 return null;
4347 }
4348
4349 // if we have a soft match with the current node, return it
4350 if (isSoftMatch(newChild, potentialSoftMatch)) {
4351 return potentialSoftMatch;
4352 }
4353
4354 if (isSoftMatch(nextSibling, potentialSoftMatch)) {
4355 // the next new node has a soft match with this node, so
4356 // increment the count of future soft matches
4357 siblingSoftMatchCount++;
4358 nextSibling = nextSibling.nextSibling;
4359
4360 // If there are two future soft matches, bail to allow the siblings to soft match
4361 // so that we don't consume future soft matches for the sake of the current node
4362 if (siblingSoftMatchCount >= 2) {
4363 return null;
4364 }
4365 }
4366
4367 // advanced to the next old content child
4368 potentialSoftMatch = potentialSoftMatch.nextSibling;
4369 }
4370
4371 return potentialSoftMatch;
4372 }
4373
4374 function parseContent(newContent) {
4375 let parser = new DOMParser();
4376
4377 // remove svgs to avoid false-positive matches on head, etc.
4378 let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
4379
4380 // if the newContent contains a html, head or body tag, we can simply parse it w/o wrapping
4381 if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) {
4382 let content = parser.parseFromString(newContent, "text/html");
4383 // if it is a full HTML document, return the document itself as the parent container
4384 if (contentWithSvgsRemoved.match(/<\/html>/)) {
4385 content.generatedByIdiomorph = true;
4386 return content;
4387 } else {
4388 // otherwise return the html element as the parent container
4389 let htmlElement = content.firstChild;
4390 if (htmlElement) {
4391 htmlElement.generatedByIdiomorph = true;
4392 return htmlElement;
4393 } else {
4394 return null;
4395 }
4396 }
4397 } else {
4398 // if it is partial HTML, wrap it in a template tag to provide a parent element and also to help
4399 // deal with touchy tags like tr, tbody, etc.
4400 let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html");
4401 let content = responseDoc.body.querySelector('template').content;
4402 content.generatedByIdiomorph = true;
4403 return content
4404 }
4405 }
4406
4407 function normalizeContent(newContent) {
4408 if (newContent == null) {
4409 // noinspection UnnecessaryLocalVariableJS
4410 const dummyParent = document.createElement('div');
4411 return dummyParent;
4412 } else if (newContent.generatedByIdiomorph) {
4413 // the template tag created by idiomorph parsing can serve as a dummy parent
4414 return newContent;
4415 } else if (newContent instanceof Node) {
4416 // a single node is added as a child to a dummy parent
4417 const dummyParent = document.createElement('div');
4418 dummyParent.append(newContent);
4419 return dummyParent;
4420 } else {
4421 // all nodes in the array or HTMLElement collection are consolidated under
4422 // a single dummy parent element
4423 const dummyParent = document.createElement('div');
4424 for (const elt of [...newContent]) {
4425 dummyParent.append(elt);
4426 }
4427 return dummyParent;
4428 }
4429 }
4430
4431 function insertSiblings(previousSibling, morphedNode, nextSibling) {
4432 let stack = [];
4433 let added = [];
4434 while (previousSibling != null) {
4435 stack.push(previousSibling);
4436 previousSibling = previousSibling.previousSibling;
4437 }
4438 while (stack.length > 0) {
4439 let node = stack.pop();
4440 added.push(node); // push added preceding siblings on in order and insert
4441 morphedNode.parentElement.insertBefore(node, morphedNode);
4442 }
4443 added.push(morphedNode);
4444 while (nextSibling != null) {
4445 stack.push(nextSibling);
4446 added.push(nextSibling); // here we are going in order, so push on as we scan, rather than add
4447 nextSibling = nextSibling.nextSibling;
4448 }
4449 while (stack.length > 0) {
4450 morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling);
4451 }
4452 return added;
4453 }
4454
4455 function findBestNodeMatch(newContent, oldNode, ctx) {
4456 let currentElement;
4457 currentElement = newContent.firstChild;
4458 let bestElement = currentElement;
4459 let score = 0;
4460 while (currentElement) {
4461 let newScore = scoreElement(currentElement, oldNode, ctx);
4462 if (newScore > score) {
4463 bestElement = currentElement;
4464 score = newScore;
4465 }
4466 currentElement = currentElement.nextSibling;
4467 }
4468 return bestElement;
4469 }
4470
4471 function scoreElement(node1, node2, ctx) {
4472 if (isSoftMatch(node1, node2)) {
4473 return .5 + getIdIntersectionCount(ctx, node1, node2);
4474 }
4475 return 0;
4476 }
4477
4478 function removeNode(tempNode, ctx) {
4479 removeIdsFromConsideration(ctx, tempNode);
4480 if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return;
4481
4482 tempNode.remove();
4483 ctx.callbacks.afterNodeRemoved(tempNode);
4484 }
4485
4486 //=============================================================================
4487 // ID Set Functions
4488 //=============================================================================
4489
4490 function isIdInConsideration(ctx, id) {
4491 return !ctx.deadIds.has(id);
4492 }
4493
4494 function idIsWithinNode(ctx, id, targetNode) {
4495 let idSet = ctx.idMap.get(targetNode) || EMPTY_SET;
4496 return idSet.has(id);
4497 }
4498
4499 function removeIdsFromConsideration(ctx, node) {
4500 let idSet = ctx.idMap.get(node) || EMPTY_SET;
4501 for (const id of idSet) {
4502 ctx.deadIds.add(id);
4503 }
4504 }
4505
4506 function getIdIntersectionCount(ctx, node1, node2) {
4507 let sourceSet = ctx.idMap.get(node1) || EMPTY_SET;
4508 let matchCount = 0;
4509 for (const id of sourceSet) {
4510 // a potential match is an id in the source and potentialIdsSet, but
4511 // that has not already been merged into the DOM
4512 if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) {
4513 ++matchCount;
4514 }
4515 }
4516 return matchCount;
4517 }
4518
4519 /**
4520 * A bottom up algorithm that finds all elements with ids inside of the node
4521 * argument and populates id sets for those nodes and all their parents, generating
4522 * a set of ids contained within all nodes for the entire hierarchy in the DOM
4523 *
4524 * @param node {Element}
4525 * @param {Map<Node, Set<String>>} idMap
4526 */
4527 function populateIdMapForNode(node, idMap) {
4528 let nodeParent = node.parentElement;
4529 // find all elements with an id property
4530 let idElements = node.querySelectorAll('[id]');
4531 for (const elt of idElements) {
4532 let current = elt;
4533 // walk up the parent hierarchy of that element, adding the id
4534 // of element to the parent's id set
4535 while (current !== nodeParent && current != null) {
4536 let idSet = idMap.get(current);
4537 // if the id set doesn't exist, create it and insert it in the map
4538 if (idSet == null) {
4539 idSet = new Set();
4540 idMap.set(current, idSet);
4541 }
4542 idSet.add(elt.id);
4543 current = current.parentElement;
4544 }
4545 }
4546 }
4547
4548 /**
4549 * This function computes a map of nodes to all ids contained within that node (inclusive of the
4550 * node). This map can be used to ask if two nodes have intersecting sets of ids, which allows
4551 * for a looser definition of "matching" than tradition id matching, and allows child nodes
4552 * to contribute to a parent nodes matching.
4553 *
4554 * @param {Element} oldContent the old content that will be morphed
4555 * @param {Element} newContent the new content to morph to
4556 * @returns {Map<Node, Set<String>>} a map of nodes to id sets for the
4557 */
4558 function createIdMap(oldContent, newContent) {
4559 let idMap = new Map();
4560 populateIdMapForNode(oldContent, idMap);
4561 populateIdMapForNode(newContent, idMap);
4562 return idMap;
4563 }
4564
4565 //=============================================================================
4566 // This is what ends up becoming the Idiomorph global object
4567 //=============================================================================
4568 return {
4569 morph,
4570 defaults
4571 }
4572 })();
4573
4574 class PageRenderer extends Renderer {
4575 static renderElement(currentElement, newElement) {
4576 if (document.body && newElement instanceof HTMLBodyElement) {
4577 document.body.replaceWith(newElement);
4578 } else {
4579 document.documentElement.appendChild(newElement);
4580 }
4581 }
4582
4583 get shouldRender() {
4584 return this.newSnapshot.isVisitable && this.trackedElementsAreIdentical
4585 }
4586
4587 get reloadReason() {
4588 if (!this.newSnapshot.isVisitable) {
4589 return {
4590 reason: "turbo_visit_control_is_reload"
4591 }
4592 }
4593
4594 if (!this.trackedElementsAreIdentical) {
4595 return {
4596 reason: "tracked_element_mismatch"
4597 }
4598 }
4599 }
4600
4601 async prepareToRender() {
4602 this.#setLanguage();
4603 await this.mergeHead();
4604 }
4605
4606 async render() {
4607 if (this.willRender) {
4608 await this.replaceBody();
4609 }
4610 }
4611
4612 finishRendering() {
4613 super.finishRendering();
4614 if (!this.isPreview) {
4615 this.focusFirstAutofocusableElement();
4616 }
4617 }
4618
4619 get currentHeadSnapshot() {
4620 return this.currentSnapshot.headSnapshot
4621 }
4622
4623 get newHeadSnapshot() {
4624 return this.newSnapshot.headSnapshot
4625 }
4626
4627 get newElement() {
4628 return this.newSnapshot.element
4629 }
4630
4631 #setLanguage() {
4632 const { documentElement } = this.currentSnapshot;
4633 const { lang } = this.newSnapshot;
4634
4635 if (lang) {
4636 documentElement.setAttribute("lang", lang);
4637 } else {
4638 documentElement.removeAttribute("lang");
4639 }
4640 }
4641
4642 async mergeHead() {
4643 const mergedHeadElements = this.mergeProvisionalElements();
4644 const newStylesheetElements = this.copyNewHeadStylesheetElements();
4645 this.copyNewHeadScriptElements();
4646
4647 await mergedHeadElements;
4648 await newStylesheetElements;
4649
4650 if (this.willRender) {
4651 this.removeUnusedDynamicStylesheetElements();
4652 }
4653 }
4654
4655 async replaceBody() {
4656 await this.preservingPermanentElements(async () => {
4657 this.activateNewBody();
4658 await this.assignNewBody();
4659 });
4660 }
4661
4662 get trackedElementsAreIdentical() {
4663 return this.currentHeadSnapshot.trackedElementSignature == this.newHeadSnapshot.trackedElementSignature
4664 }
4665
4666 async copyNewHeadStylesheetElements() {
4667 const loadingElements = [];
4668
4669 for (const element of this.newHeadStylesheetElements) {
4670 loadingElements.push(waitForLoad(element));
4671
4672 document.head.appendChild(element);
4673 }
4674
4675 await Promise.all(loadingElements);
4676 }
4677
4678 copyNewHeadScriptElements() {
4679 for (const element of this.newHeadScriptElements) {
4680 document.head.appendChild(activateScriptElement(element));
4681 }
4682 }
4683
4684 removeUnusedDynamicStylesheetElements() {
4685 for (const element of this.unusedDynamicStylesheetElements) {
4686 document.head.removeChild(element);
4687 }
4688 }
4689
4690 async mergeProvisionalElements() {
4691 const newHeadElements = [...this.newHeadProvisionalElements];
4692
4693 for (const element of this.currentHeadProvisionalElements) {
4694 if (!this.isCurrentElementInElementList(element, newHeadElements)) {
4695 document.head.removeChild(element);
4696 }
4697 }
4698
4699 for (const element of newHeadElements) {
4700 document.head.appendChild(element);
4701 }
4702 }
4703
4704 isCurrentElementInElementList(element, elementList) {
4705 for (const [index, newElement] of elementList.entries()) {
4706 // if title element...
4707 if (element.tagName == "TITLE") {
4708 if (newElement.tagName != "TITLE") {
4709 continue
4710 }
4711 if (element.innerHTML == newElement.innerHTML) {
4712 elementList.splice(index, 1);
4713 return true
4714 }
4715 }
4716
4717 // if any other element...
4718 if (newElement.isEqualNode(element)) {
4719 elementList.splice(index, 1);
4720 return true
4721 }
4722 }
4723
4724 return false
4725 }
4726
4727 removeCurrentHeadProvisionalElements() {
4728 for (const element of this.currentHeadProvisionalElements) {
4729 document.head.removeChild(element);
4730 }
4731 }
4732
4733 copyNewHeadProvisionalElements() {
4734 for (const element of this.newHeadProvisionalElements) {
4735 document.head.appendChild(element);
4736 }
4737 }
4738
4739 activateNewBody() {
4740 document.adoptNode(this.newElement);
4741 this.activateNewBodyScriptElements();
4742 }
4743
4744 activateNewBodyScriptElements() {
4745 for (const inertScriptElement of this.newBodyScriptElements) {
4746 const activatedScriptElement = activateScriptElement(inertScriptElement);
4747 inertScriptElement.replaceWith(activatedScriptElement);
4748 }
4749 }
4750
4751 async assignNewBody() {
4752 await this.renderElement(this.currentElement, this.newElement);
4753 }
4754
4755 get unusedDynamicStylesheetElements() {
4756 return this.oldHeadStylesheetElements.filter((element) => {
4757 return element.getAttribute("data-turbo-track") === "dynamic"
4758 })
4759 }
4760
4761 get oldHeadStylesheetElements() {
4762 return this.currentHeadSnapshot.getStylesheetElementsNotInSnapshot(this.newHeadSnapshot)
4763 }
4764
4765 get newHeadStylesheetElements() {
4766 return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot)
4767 }
4768
4769 get newHeadScriptElements() {
4770 return this.newHeadSnapshot.getScriptElementsNotInSnapshot(this.currentHeadSnapshot)
4771 }
4772
4773 get currentHeadProvisionalElements() {
4774 return this.currentHeadSnapshot.provisionalElements
4775 }
4776
4777 get newHeadProvisionalElements() {
4778 return this.newHeadSnapshot.provisionalElements
4779 }
4780
4781 get newBodyScriptElements() {
4782 return this.newElement.querySelectorAll("script")
4783 }
4784 }
4785
4786 class MorphRenderer extends PageRenderer {
4787 async render() {
4788 if (this.willRender) await this.#morphBody();
4789 }
4790
4791 get renderMethod() {
4792 return "morph"
4793 }
4794
4795 // Private
4796
4797 async #morphBody() {
4798 this.#morphElements(this.currentElement, this.newElement);
4799 this.#reloadRemoteFrames();
4800
4801 dispatch("turbo:morph", {
4802 detail: {
4803 currentElement: this.currentElement,
4804 newElement: this.newElement
4805 }
4806 });
4807 }
4808
4809 #morphElements(currentElement, newElement, morphStyle = "outerHTML") {
4810 this.isMorphingTurboFrame = this.#isFrameReloadedWithMorph(currentElement);
4811
4812 Idiomorph.morph(currentElement, newElement, {
4813 morphStyle: morphStyle,
4814 callbacks: {
4815 beforeNodeAdded: this.#shouldAddElement,
4816 beforeNodeMorphed: this.#shouldMorphElement,
4817 beforeAttributeUpdated: this.#shouldUpdateAttribute,
4818 beforeNodeRemoved: this.#shouldRemoveElement,
4819 afterNodeMorphed: this.#didMorphElement
4820 }
4821 });
4822 }
4823
4824 #shouldAddElement = (node) => {
4825 return !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id))
4826 }
4827
4828 #shouldMorphElement = (oldNode, newNode) => {
4829 if (oldNode instanceof HTMLElement) {
4830 if (!oldNode.hasAttribute("data-turbo-permanent") && (this.isMorphingTurboFrame || !this.#isFrameReloadedWithMorph(oldNode))) {
4831 const event = dispatch("turbo:before-morph-element", {
4832 cancelable: true,
4833 target: oldNode,
4834 detail: {
4835 newElement: newNode
4836 }
4837 });
4838
4839 return !event.defaultPrevented
4840 } else {
4841 return false
4842 }
4843 }
4844 }
4845
4846 #shouldUpdateAttribute = (attributeName, target, mutationType) => {
4847 const event = dispatch("turbo:before-morph-attribute", { cancelable: true, target, detail: { attributeName, mutationType } });
4848
4849 return !event.defaultPrevented
4850 }
4851
4852 #didMorphElement = (oldNode, newNode) => {
4853 if (newNode instanceof HTMLElement) {
4854 dispatch("turbo:morph-element", {
4855 target: oldNode,
4856 detail: {
4857 newElement: newNode
4858 }
4859 });
4860 }
4861 }
4862
4863 #shouldRemoveElement = (node) => {
4864 return this.#shouldMorphElement(node)
4865 }
4866
4867 #reloadRemoteFrames() {
4868 this.#remoteFrames().forEach((frame) => {
4869 if (this.#isFrameReloadedWithMorph(frame)) {
4870 this.#renderFrameWithMorph(frame);
4871 frame.reload();
4872 }
4873 });
4874 }
4875
4876 #renderFrameWithMorph(frame) {
4877 frame.addEventListener("turbo:before-frame-render", (event) => {
4878 event.detail.render = this.#morphFrameUpdate;
4879 }, { once: true });
4880 }
4881
4882 #morphFrameUpdate = (currentElement, newElement) => {
4883 dispatch("turbo:before-frame-morph", {
4884 target: currentElement,
4885 detail: { currentElement, newElement }
4886 });
4887 this.#morphElements(currentElement, newElement.children, "innerHTML");
4888 }
4889
4890 #isFrameReloadedWithMorph(element) {
4891 return element.src && element.refresh === "morph"
4892 }
4893
4894 #remoteFrames() {
4895 return Array.from(document.querySelectorAll('turbo-frame[src]')).filter(frame => {
4896 return !frame.closest('[data-turbo-permanent]')
4897 })
4898 }
4899 }
4900
4901 class SnapshotCache {
4902 keys = []
4903 snapshots = {}
4904
4905 constructor(size) {
4906 this.size = size;
4907 }
4908
4909 has(location) {
4910 return toCacheKey(location) in this.snapshots
4911 }
4912
4913 get(location) {
4914 if (this.has(location)) {
4915 const snapshot = this.read(location);
4916 this.touch(location);
4917 return snapshot
4918 }
4919 }
4920
4921 put(location, snapshot) {
4922 this.write(location, snapshot);
4923 this.touch(location);
4924 return snapshot
4925 }
4926
4927 clear() {
4928 this.snapshots = {};
4929 }
4930
4931 // Private
4932
4933 read(location) {
4934 return this.snapshots[toCacheKey(location)]
4935 }
4936
4937 write(location, snapshot) {
4938 this.snapshots[toCacheKey(location)] = snapshot;
4939 }
4940
4941 touch(location) {
4942 const key = toCacheKey(location);
4943 const index = this.keys.indexOf(key);
4944 if (index > -1) this.keys.splice(index, 1);
4945 this.keys.unshift(key);
4946 this.trim();
4947 }
4948
4949 trim() {
4950 for (const key of this.keys.splice(this.size)) {
4951 delete this.snapshots[key];
4952 }
4953 }
4954 }
4955
4956 class PageView extends View {
4957 snapshotCache = new SnapshotCache(10)
4958 lastRenderedLocation = new URL(location.href)
4959 forceReloaded = false
4960
4961 shouldTransitionTo(newSnapshot) {
4962 return this.snapshot.prefersViewTransitions && newSnapshot.prefersViewTransitions
4963 }
4964
4965 renderPage(snapshot, isPreview = false, willRender = true, visit) {
4966 const shouldMorphPage = this.isPageRefresh(visit) && this.snapshot.shouldMorphPage;
4967 const rendererClass = shouldMorphPage ? MorphRenderer : PageRenderer;
4968
4969 const renderer = new rendererClass(this.snapshot, snapshot, PageRenderer.renderElement, isPreview, willRender);
4970
4971 if (!renderer.shouldRender) {
4972 this.forceReloaded = true;
4973 } else {
4974 visit?.changeHistory();
4975 }
4976
4977 return this.render(renderer)
4978 }
4979
4980 renderError(snapshot, visit) {
4981 visit?.changeHistory();
4982 const renderer = new ErrorRenderer(this.snapshot, snapshot, ErrorRenderer.renderElement, false);
4983 return this.render(renderer)
4984 }
4985
4986 clearSnapshotCache() {
4987 this.snapshotCache.clear();
4988 }
4989
4990 async cacheSnapshot(snapshot = this.snapshot) {
4991 if (snapshot.isCacheable) {
4992 this.delegate.viewWillCacheSnapshot();
4993 const { lastRenderedLocation: location } = this;
4994 await nextEventLoopTick();
4995 const cachedSnapshot = snapshot.clone();
4996 this.snapshotCache.put(location, cachedSnapshot);
4997 return cachedSnapshot
4998 }
4999 }
5000
5001 getCachedSnapshotForLocation(location) {
5002 return this.snapshotCache.get(location)
5003 }
5004
5005 isPageRefresh(visit) {
5006 return !visit || (this.lastRenderedLocation.pathname === visit.location.pathname && visit.action === "replace")
5007 }
5008
5009 shouldPreserveScrollPosition(visit) {
5010 return this.isPageRefresh(visit) && this.snapshot.shouldPreserveScrollPosition
5011 }
5012
5013 get snapshot() {
5014 return PageSnapshot.fromElement(this.element)
5015 }
5016 }
5017
5018 class Preloader {
5019 selector = "a[data-turbo-preload]"
5020
5021 constructor(delegate, snapshotCache) {
5022 this.delegate = delegate;
5023 this.snapshotCache = snapshotCache;
5024 }
5025
5026 start() {
5027 if (document.readyState === "loading") {
5028 document.addEventListener("DOMContentLoaded", this.#preloadAll);
5029 } else {
5030 this.preloadOnLoadLinksForView(document.body);
5031 }
5032 }
5033
5034 stop() {
5035 document.removeEventListener("DOMContentLoaded", this.#preloadAll);
5036 }
5037
5038 preloadOnLoadLinksForView(element) {
5039 for (const link of element.querySelectorAll(this.selector)) {
5040 if (this.delegate.shouldPreloadLink(link)) {
5041 this.preloadURL(link);
5042 }
5043 }
5044 }
5045
5046 async preloadURL(link) {
5047 const location = new URL(link.href);
5048
5049 if (this.snapshotCache.has(location)) {
5050 return
5051 }
5052
5053 const fetchRequest = new FetchRequest(this, FetchMethod.get, location, new URLSearchParams(), link);
5054 await fetchRequest.perform();
5055 }
5056
5057 // Fetch request delegate
5058
5059 prepareRequest(fetchRequest) {
5060 fetchRequest.headers["X-Sec-Purpose"] = "prefetch";
5061 }
5062
5063 async requestSucceededWithResponse(fetchRequest, fetchResponse) {
5064 try {
5065 const responseHTML = await fetchResponse.responseHTML;
5066 const snapshot = PageSnapshot.fromHTMLString(responseHTML);
5067
5068 this.snapshotCache.put(fetchRequest.url, snapshot);
5069 } catch (_) {
5070 // If we cannot preload that is ok!
5071 }
5072 }
5073
5074 requestStarted(fetchRequest) {}
5075
5076 requestErrored(fetchRequest) {}
5077
5078 requestFinished(fetchRequest) {}
5079
5080 requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}
5081
5082 requestFailedWithResponse(fetchRequest, fetchResponse) {}
5083
5084 #preloadAll = () => {
5085 this.preloadOnLoadLinksForView(document.body);
5086 }
5087 }
5088
5089 class Cache {
5090 constructor(session) {
5091 this.session = session;
5092 }
5093
5094 clear() {
5095 this.session.clearCache();
5096 }
5097
5098 resetCacheControl() {
5099 this.#setCacheControl("");
5100 }
5101
5102 exemptPageFromCache() {
5103 this.#setCacheControl("no-cache");
5104 }
5105
5106 exemptPageFromPreview() {
5107 this.#setCacheControl("no-preview");
5108 }
5109
5110 #setCacheControl(value) {
5111 setMetaContent("turbo-cache-control", value);
5112 }
5113 }
5114
5115 class Session {
5116 navigator = new Navigator(this)
5117 history = new History(this)
5118 view = new PageView(this, document.documentElement)
5119 adapter = new BrowserAdapter(this)
5120
5121 pageObserver = new PageObserver(this)
5122 cacheObserver = new CacheObserver()
5123 linkPrefetchObserver = new LinkPrefetchObserver(this, document)
5124 linkClickObserver = new LinkClickObserver(this, window)
5125 formSubmitObserver = new FormSubmitObserver(this, document)
5126 scrollObserver = new ScrollObserver(this)
5127 streamObserver = new StreamObserver(this)
5128 formLinkClickObserver = new FormLinkClickObserver(this, document.documentElement)
5129 frameRedirector = new FrameRedirector(this, document.documentElement)
5130 streamMessageRenderer = new StreamMessageRenderer()
5131 cache = new Cache(this)
5132
5133 drive = true
5134 enabled = true
5135 progressBarDelay = 500
5136 started = false
5137 formMode = "on"
5138 #pageRefreshDebouncePeriod = 150
5139
5140 constructor(recentRequests) {
5141 this.recentRequests = recentRequests;
5142 this.preloader = new Preloader(this, this.view.snapshotCache);
5143 this.debouncedRefresh = this.refresh;
5144 this.pageRefreshDebouncePeriod = this.pageRefreshDebouncePeriod;
5145 }
5146
5147 start() {
5148 if (!this.started) {
5149 this.pageObserver.start();
5150 this.cacheObserver.start();
5151 this.linkPrefetchObserver.start();
5152 this.formLinkClickObserver.start();
5153 this.linkClickObserver.start();
5154 this.formSubmitObserver.start();
5155 this.scrollObserver.start();
5156 this.streamObserver.start();
5157 this.frameRedirector.start();
5158 this.history.start();
5159 this.preloader.start();
5160 this.started = true;
5161 this.enabled = true;
5162 }
5163 }
5164
5165 disable() {
5166 this.enabled = false;
5167 }
5168
5169 stop() {
5170 if (this.started) {
5171 this.pageObserver.stop();
5172 this.cacheObserver.stop();
5173 this.linkPrefetchObserver.stop();
5174 this.formLinkClickObserver.stop();
5175 this.linkClickObserver.stop();
5176 this.formSubmitObserver.stop();
5177 this.scrollObserver.stop();
5178 this.streamObserver.stop();
5179 this.frameRedirector.stop();
5180 this.history.stop();
5181 this.preloader.stop();
5182 this.started = false;
5183 }
5184 }
5185
5186 registerAdapter(adapter) {
5187 this.adapter = adapter;
5188 }
5189
5190 visit(location, options = {}) {
5191 const frameElement = options.frame ? document.getElementById(options.frame) : null;
5192
5193 if (frameElement instanceof FrameElement) {
5194 const action = options.action || getVisitAction(frameElement);
5195
5196 frameElement.delegate.proposeVisitIfNavigatedWithAction(frameElement, action);
5197 frameElement.src = location.toString();
5198 } else {
5199 this.navigator.proposeVisit(expandURL(location), options);
5200 }
5201 }
5202
5203 refresh(url, requestId) {
5204 const isRecentRequest = requestId && this.recentRequests.has(requestId);
5205 if (!isRecentRequest) {
5206 this.visit(url, { action: "replace", shouldCacheSnapshot: false });
5207 }
5208 }
5209
5210 connectStreamSource(source) {
5211 this.streamObserver.connectStreamSource(source);
5212 }
5213
5214 disconnectStreamSource(source) {
5215 this.streamObserver.disconnectStreamSource(source);
5216 }
5217
5218 renderStreamMessage(message) {
5219 this.streamMessageRenderer.render(StreamMessage.wrap(message));
5220 }
5221
5222 clearCache() {
5223 this.view.clearSnapshotCache();
5224 }
5225
5226 setProgressBarDelay(delay) {
5227 this.progressBarDelay = delay;
5228 }
5229
5230 setFormMode(mode) {
5231 this.formMode = mode;
5232 }
5233
5234 get location() {
5235 return this.history.location
5236 }
5237
5238 get restorationIdentifier() {
5239 return this.history.restorationIdentifier
5240 }
5241
5242 get pageRefreshDebouncePeriod() {
5243 return this.#pageRefreshDebouncePeriod
5244 }
5245
5246 set pageRefreshDebouncePeriod(value) {
5247 this.refresh = debounce(this.debouncedRefresh.bind(this), value);
5248 this.#pageRefreshDebouncePeriod = value;
5249 }
5250
5251 // Preloader delegate
5252
5253 shouldPreloadLink(element) {
5254 const isUnsafe = element.hasAttribute("data-turbo-method");
5255 const isStream = element.hasAttribute("data-turbo-stream");
5256 const frameTarget = element.getAttribute("data-turbo-frame");
5257 const frame = frameTarget == "_top" ?
5258 null :
5259 document.getElementById(frameTarget) || findClosestRecursively(element, "turbo-frame:not([disabled])");
5260
5261 if (isUnsafe || isStream || frame instanceof FrameElement) {
5262 return false
5263 } else {
5264 const location = new URL(element.href);
5265
5266 return this.elementIsNavigatable(element) && locationIsVisitable(location, this.snapshot.rootLocation)
5267 }
5268 }
5269
5270 // History delegate
5271
5272 historyPoppedToLocationWithRestorationIdentifierAndDirection(location, restorationIdentifier, direction) {
5273 if (this.enabled) {
5274 this.navigator.startVisit(location, restorationIdentifier, {
5275 action: "restore",
5276 historyChanged: true,
5277 direction
5278 });
5279 } else {
5280 this.adapter.pageInvalidated({
5281 reason: "turbo_disabled"
5282 });
5283 }
5284 }
5285
5286 // Scroll observer delegate
5287
5288 scrollPositionChanged(position) {
5289 this.history.updateRestorationData({ scrollPosition: position });
5290 }
5291
5292 // Form click observer delegate
5293
5294 willSubmitFormLinkToLocation(link, location) {
5295 return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation)
5296 }
5297
5298 submittedFormLinkToLocation() {}
5299
5300 // Link hover observer delegate
5301
5302 canPrefetchRequestToLocation(link, location) {
5303 return (
5304 this.elementIsNavigatable(link) &&
5305 locationIsVisitable(location, this.snapshot.rootLocation)
5306 )
5307 }
5308
5309 // Link click observer delegate
5310
5311 willFollowLinkToLocation(link, location, event) {
5312 return (
5313 this.elementIsNavigatable(link) &&
5314 locationIsVisitable(location, this.snapshot.rootLocation) &&
5315 this.applicationAllowsFollowingLinkToLocation(link, location, event)
5316 )
5317 }
5318
5319 followedLinkToLocation(link, location) {
5320 const action = this.getActionForLink(link);
5321 const acceptsStreamResponse = link.hasAttribute("data-turbo-stream");
5322
5323 this.visit(location.href, { action, acceptsStreamResponse });
5324 }
5325
5326 // Navigator delegate
5327
5328 allowsVisitingLocationWithAction(location, action) {
5329 return this.locationWithActionIsSamePage(location, action) || this.applicationAllowsVisitingLocation(location)
5330 }
5331
5332 visitProposedToLocation(location, options) {
5333 extendURLWithDeprecatedProperties(location);
5334 this.adapter.visitProposedToLocation(location, options);
5335 }
5336
5337 // Visit delegate
5338
5339 visitStarted(visit) {
5340 if (!visit.acceptsStreamResponse) {
5341 markAsBusy(document.documentElement);
5342 this.view.markVisitDirection(visit.direction);
5343 }
5344 extendURLWithDeprecatedProperties(visit.location);
5345 if (!visit.silent) {
5346 this.notifyApplicationAfterVisitingLocation(visit.location, visit.action);
5347 }
5348 }
5349
5350 visitCompleted(visit) {
5351 this.view.unmarkVisitDirection();
5352 clearBusyState(document.documentElement);
5353 this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
5354 }
5355
5356 locationWithActionIsSamePage(location, action) {
5357 return this.navigator.locationWithActionIsSamePage(location, action)
5358 }
5359
5360 visitScrolledToSamePageLocation(oldURL, newURL) {
5361 this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
5362 }
5363
5364 // Form submit observer delegate
5365
5366 willSubmitForm(form, submitter) {
5367 const action = getAction$1(form, submitter);
5368
5369 return (
5370 this.submissionIsNavigatable(form, submitter) &&
5371 locationIsVisitable(expandURL(action), this.snapshot.rootLocation)
5372 )
5373 }
5374
5375 formSubmitted(form, submitter) {
5376 this.navigator.submitForm(form, submitter);
5377 }
5378
5379 // Page observer delegate
5380
5381 pageBecameInteractive() {
5382 this.view.lastRenderedLocation = this.location;
5383 this.notifyApplicationAfterPageLoad();
5384 }
5385
5386 pageLoaded() {
5387 this.history.assumeControlOfScrollRestoration();
5388 }
5389
5390 pageWillUnload() {
5391 this.history.relinquishControlOfScrollRestoration();
5392 }
5393
5394 // Stream observer delegate
5395
5396 receivedMessageFromStream(message) {
5397 this.renderStreamMessage(message);
5398 }
5399
5400 // Page view delegate
5401
5402 viewWillCacheSnapshot() {
5403 if (!this.navigator.currentVisit?.silent) {
5404 this.notifyApplicationBeforeCachingSnapshot();
5405 }
5406 }
5407
5408 allowsImmediateRender({ element }, options) {
5409 const event = this.notifyApplicationBeforeRender(element, options);
5410 const {
5411 defaultPrevented,
5412 detail: { render }
5413 } = event;
5414
5415 if (this.view.renderer && render) {
5416 this.view.renderer.renderElement = render;
5417 }
5418
5419 return !defaultPrevented
5420 }
5421
5422 viewRenderedSnapshot(_snapshot, _isPreview, renderMethod) {
5423 this.view.lastRenderedLocation = this.history.location;
5424 this.notifyApplicationAfterRender(renderMethod);
5425 }
5426
5427 preloadOnLoadLinksForView(element) {
5428 this.preloader.preloadOnLoadLinksForView(element);
5429 }
5430
5431 viewInvalidated(reason) {
5432 this.adapter.pageInvalidated(reason);
5433 }
5434
5435 // Frame element
5436
5437 frameLoaded(frame) {
5438 this.notifyApplicationAfterFrameLoad(frame);
5439 }
5440
5441 frameRendered(fetchResponse, frame) {
5442 this.notifyApplicationAfterFrameRender(fetchResponse, frame);
5443 }
5444
5445 // Application events
5446
5447 applicationAllowsFollowingLinkToLocation(link, location, ev) {
5448 const event = this.notifyApplicationAfterClickingLinkToLocation(link, location, ev);
5449 return !event.defaultPrevented
5450 }
5451
5452 applicationAllowsVisitingLocation(location) {
5453 const event = this.notifyApplicationBeforeVisitingLocation(location);
5454 return !event.defaultPrevented
5455 }
5456
5457 notifyApplicationAfterClickingLinkToLocation(link, location, event) {
5458 return dispatch("turbo:click", {
5459 target: link,
5460 detail: { url: location.href, originalEvent: event },
5461 cancelable: true
5462 })
5463 }
5464
5465 notifyApplicationBeforeVisitingLocation(location) {
5466 return dispatch("turbo:before-visit", {
5467 detail: { url: location.href },
5468 cancelable: true
5469 })
5470 }
5471
5472 notifyApplicationAfterVisitingLocation(location, action) {
5473 return dispatch("turbo:visit", { detail: { url: location.href, action } })
5474 }
5475
5476 notifyApplicationBeforeCachingSnapshot() {
5477 return dispatch("turbo:before-cache")
5478 }
5479
5480 notifyApplicationBeforeRender(newBody, options) {
5481 return dispatch("turbo:before-render", {
5482 detail: { newBody, ...options },
5483 cancelable: true
5484 })
5485 }
5486
5487 notifyApplicationAfterRender(renderMethod) {
5488 return dispatch("turbo:render", { detail: { renderMethod } })
5489 }
5490
5491 notifyApplicationAfterPageLoad(timing = {}) {
5492 return dispatch("turbo:load", {
5493 detail: { url: this.location.href, timing }
5494 })
5495 }
5496
5497 notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL) {
5498 dispatchEvent(
5499 new HashChangeEvent("hashchange", {
5500 oldURL: oldURL.toString(),
5501 newURL: newURL.toString()
5502 })
5503 );
5504 }
5505
5506 notifyApplicationAfterFrameLoad(frame) {
5507 return dispatch("turbo:frame-load", { target: frame })
5508 }
5509
5510 notifyApplicationAfterFrameRender(fetchResponse, frame) {
5511 return dispatch("turbo:frame-render", {
5512 detail: { fetchResponse },
5513 target: frame,
5514 cancelable: true
5515 })
5516 }
5517
5518 // Helpers
5519
5520 submissionIsNavigatable(form, submitter) {
5521 if (this.formMode == "off") {
5522 return false
5523 } else {
5524 const submitterIsNavigatable = submitter ? this.elementIsNavigatable(submitter) : true;
5525
5526 if (this.formMode == "optin") {
5527 return submitterIsNavigatable && form.closest('[data-turbo="true"]') != null
5528 } else {
5529 return submitterIsNavigatable && this.elementIsNavigatable(form)
5530 }
5531 }
5532 }
5533
5534 elementIsNavigatable(element) {
5535 const container = findClosestRecursively(element, "[data-turbo]");
5536 const withinFrame = findClosestRecursively(element, "turbo-frame");
5537
5538 // Check if Drive is enabled on the session or we're within a Frame.
5539 if (this.drive || withinFrame) {
5540 // Element is navigatable by default, unless `data-turbo="false"`.
5541 if (container) {
5542 return container.getAttribute("data-turbo") != "false"
5543 } else {
5544 return true
5545 }
5546 } else {
5547 // Element isn't navigatable by default, unless `data-turbo="true"`.
5548 if (container) {
5549 return container.getAttribute("data-turbo") == "true"
5550 } else {
5551 return false
5552 }
5553 }
5554 }
5555
5556 // Private
5557
5558 getActionForLink(link) {
5559 return getVisitAction(link) || "advance"
5560 }
5561
5562 get snapshot() {
5563 return this.view.snapshot
5564 }
5565 }
5566
5567 // Older versions of the Turbo Native adapters referenced the
5568 // `Location#absoluteURL` property in their implementations of
5569 // the `Adapter#visitProposedToLocation()` and `#visitStarted()`
5570 // methods. The Location class has since been removed in favor
5571 // of the DOM URL API, and accordingly all Adapter methods now
5572 // receive URL objects.
5573 //
5574 // We alias #absoluteURL to #toString() here to avoid crashing
5575 // older adapters which do not expect URL objects. We should
5576 // consider removing this support at some point in the future.
5577
5578 function extendURLWithDeprecatedProperties(url) {
5579 Object.defineProperties(url, deprecatedLocationPropertyDescriptors);
5580 }
5581
5582 const deprecatedLocationPropertyDescriptors = {
5583 absoluteURL: {
5584 get() {
5585 return this.toString()
5586 }
5587 }
5588 };
5589
5590 const session = new Session(recentRequests);
5591 const { cache, navigator: navigator$1 } = session;
5592
5593 /**
5594 * Starts the main session.
5595 * This initialises any necessary observers such as those to monitor
5596 * link interactions.
5597 */
5598 function start() {
5599 session.start();
5600 }
5601
5602 /**
5603 * Registers an adapter for the main session.
5604 *
5605 * @param adapter Adapter to register
5606 */
5607 function registerAdapter(adapter) {
5608 session.registerAdapter(adapter);
5609 }
5610
5611 /**
5612 * Performs an application visit to the given location.
5613 *
5614 * @param location Location to visit (a URL or path)
5615 * @param options Options to apply
5616 * @param options.action Type of history navigation to apply ("restore",
5617 * "replace" or "advance")
5618 * @param options.historyChanged Specifies whether the browser history has
5619 * already been changed for this visit or not
5620 * @param options.referrer Specifies the referrer of this visit such that
5621 * navigations to the same page will not result in a new history entry.
5622 * @param options.snapshotHTML Cached snapshot to render
5623 * @param options.response Response of the specified location
5624 */
5625 function visit(location, options) {
5626 session.visit(location, options);
5627 }
5628
5629 /**
5630 * Connects a stream source to the main session.
5631 *
5632 * @param source Stream source to connect
5633 */
5634 function connectStreamSource(source) {
5635 session.connectStreamSource(source);
5636 }
5637
5638 /**
5639 * Disconnects a stream source from the main session.
5640 *
5641 * @param source Stream source to disconnect
5642 */
5643 function disconnectStreamSource(source) {
5644 session.disconnectStreamSource(source);
5645 }
5646
5647 /**
5648 * Renders a stream message to the main session by appending it to the
5649 * current document.
5650 *
5651 * @param message Message to render
5652 */
5653 function renderStreamMessage(message) {
5654 session.renderStreamMessage(message);
5655 }
5656
5657 /**
5658 * Removes all entries from the Turbo Drive page cache.
5659 * Call this when state has changed on the server that may affect cached pages.
5660 *
5661 * @deprecated since version 7.2.0 in favor of `Turbo.cache.clear()`
5662 */
5663 function clearCache() {
5664 console.warn(
5665 "Please replace `Turbo.clearCache()` with `Turbo.cache.clear()`. The top-level function is deprecated and will be removed in a future version of Turbo.`"
5666 );
5667 session.clearCache();
5668 }
5669
5670 /**
5671 * Sets the delay after which the progress bar will appear during navigation.
5672 *
5673 * The progress bar appears after 500ms by default.
5674 *
5675 * Note that this method has no effect when used with the iOS or Android
5676 * adapters.
5677 *
5678 * @param delay Time to delay in milliseconds
5679 */
5680 function setProgressBarDelay(delay) {
5681 session.setProgressBarDelay(delay);
5682 }
5683
5684 function setConfirmMethod(confirmMethod) {
5685 FormSubmission.confirmMethod = confirmMethod;
5686 }
5687
5688 function setFormMode(mode) {
5689 session.setFormMode(mode);
5690 }
5691
5692 var Turbo = /*#__PURE__*/Object.freeze({
5693 __proto__: null,
5694 navigator: navigator$1,
5695 session: session,
5696 cache: cache,
5697 PageRenderer: PageRenderer,
5698 PageSnapshot: PageSnapshot,
5699 FrameRenderer: FrameRenderer,
5700 fetch: fetchWithTurboHeaders,
5701 start: start,
5702 registerAdapter: registerAdapter,
5703 visit: visit,
5704 connectStreamSource: connectStreamSource,
5705 disconnectStreamSource: disconnectStreamSource,
5706 renderStreamMessage: renderStreamMessage,
5707 clearCache: clearCache,
5708 setProgressBarDelay: setProgressBarDelay,
5709 setConfirmMethod: setConfirmMethod,
5710 setFormMode: setFormMode
5711 });
5712
5713 class TurboFrameMissingError extends Error {}
5714
5715 class FrameController {
5716 fetchResponseLoaded = (_fetchResponse) => Promise.resolve()
5717 #currentFetchRequest = null
5718 #resolveVisitPromise = () => {}
5719 #connected = false
5720 #hasBeenLoaded = false
5721 #ignoredAttributes = new Set()
5722 action = null
5723
5724 constructor(element) {
5725 this.element = element;
5726 this.view = new FrameView(this, this.element);
5727 this.appearanceObserver = new AppearanceObserver(this, this.element);
5728 this.formLinkClickObserver = new FormLinkClickObserver(this, this.element);
5729 this.linkInterceptor = new LinkInterceptor(this, this.element);
5730 this.restorationIdentifier = uuid();
5731 this.formSubmitObserver = new FormSubmitObserver(this, this.element);
5732 }
5733
5734 // Frame delegate
5735
5736 connect() {
5737 if (!this.#connected) {
5738 this.#connected = true;
5739 if (this.loadingStyle == FrameLoadingStyle.lazy) {
5740 this.appearanceObserver.start();
5741 } else {
5742 this.#loadSourceURL();
5743 }
5744 this.formLinkClickObserver.start();
5745 this.linkInterceptor.start();
5746 this.formSubmitObserver.start();
5747 }
5748 }
5749
5750 disconnect() {
5751 if (this.#connected) {
5752 this.#connected = false;
5753 this.appearanceObserver.stop();
5754 this.formLinkClickObserver.stop();
5755 this.linkInterceptor.stop();
5756 this.formSubmitObserver.stop();
5757 }
5758 }
5759
5760 disabledChanged() {
5761 if (this.loadingStyle == FrameLoadingStyle.eager) {
5762 this.#loadSourceURL();
5763 }
5764 }
5765
5766 sourceURLChanged() {
5767 if (this.#isIgnoringChangesTo("src")) return
5768
5769 if (this.element.isConnected) {
5770 this.complete = false;
5771 }
5772
5773 if (this.loadingStyle == FrameLoadingStyle.eager || this.#hasBeenLoaded) {
5774 this.#loadSourceURL();
5775 }
5776 }
5777
5778 sourceURLReloaded() {
5779 const { src } = this.element;
5780 this.element.removeAttribute("complete");
5781 this.element.src = null;
5782 this.element.src = src;
5783 return this.element.loaded
5784 }
5785
5786 loadingStyleChanged() {
5787 if (this.loadingStyle == FrameLoadingStyle.lazy) {
5788 this.appearanceObserver.start();
5789 } else {
5790 this.appearanceObserver.stop();
5791 this.#loadSourceURL();
5792 }
5793 }
5794
5795 async #loadSourceURL() {
5796 if (this.enabled && this.isActive && !this.complete && this.sourceURL) {
5797 this.element.loaded = this.#visit(expandURL(this.sourceURL));
5798 this.appearanceObserver.stop();
5799 await this.element.loaded;
5800 this.#hasBeenLoaded = true;
5801 }
5802 }
5803
5804 async loadResponse(fetchResponse) {
5805 if (fetchResponse.redirected || (fetchResponse.succeeded && fetchResponse.isHTML)) {
5806 this.sourceURL = fetchResponse.response.url;
5807 }
5808
5809 try {
5810 const html = await fetchResponse.responseHTML;
5811 if (html) {
5812 const document = parseHTMLDocument(html);
5813 const pageSnapshot = PageSnapshot.fromDocument(document);
5814
5815 if (pageSnapshot.isVisitable) {
5816 await this.#loadFrameResponse(fetchResponse, document);
5817 } else {
5818 await this.#handleUnvisitableFrameResponse(fetchResponse);
5819 }
5820 }
5821 } finally {
5822 this.fetchResponseLoaded = () => Promise.resolve();
5823 }
5824 }
5825
5826 // Appearance observer delegate
5827
5828 elementAppearedInViewport(element) {
5829 this.proposeVisitIfNavigatedWithAction(element, getVisitAction(element));
5830 this.#loadSourceURL();
5831 }
5832
5833 // Form link click observer delegate
5834
5835 willSubmitFormLinkToLocation(link) {
5836 return this.#shouldInterceptNavigation(link)
5837 }
5838
5839 submittedFormLinkToLocation(link, _location, form) {
5840 const frame = this.#findFrameElement(link);
5841 if (frame) form.setAttribute("data-turbo-frame", frame.id);
5842 }
5843
5844 // Link interceptor delegate
5845
5846 shouldInterceptLinkClick(element, _location, _event) {
5847 return this.#shouldInterceptNavigation(element)
5848 }
5849
5850 linkClickIntercepted(element, location) {
5851 this.#navigateFrame(element, location);
5852 }
5853
5854 // Form submit observer delegate
5855
5856 willSubmitForm(element, submitter) {
5857 return element.closest("turbo-frame") == this.element && this.#shouldInterceptNavigation(element, submitter)
5858 }
5859
5860 formSubmitted(element, submitter) {
5861 if (this.formSubmission) {
5862 this.formSubmission.stop();
5863 }
5864
5865 this.formSubmission = new FormSubmission(this, element, submitter);
5866 const { fetchRequest } = this.formSubmission;
5867 this.prepareRequest(fetchRequest);
5868 this.formSubmission.start();
5869 }
5870
5871 // Fetch request delegate
5872
5873 prepareRequest(request) {
5874 request.headers["Turbo-Frame"] = this.id;
5875
5876 if (this.currentNavigationElement?.hasAttribute("data-turbo-stream")) {
5877 request.acceptResponseType(StreamMessage.contentType);
5878 }
5879 }
5880
5881 requestStarted(_request) {
5882 markAsBusy(this.element);
5883 }
5884
5885 requestPreventedHandlingResponse(_request, _response) {
5886 this.#resolveVisitPromise();
5887 }
5888
5889 async requestSucceededWithResponse(request, response) {
5890 await this.loadResponse(response);
5891 this.#resolveVisitPromise();
5892 }
5893
5894 async requestFailedWithResponse(request, response) {
5895 await this.loadResponse(response);
5896 this.#resolveVisitPromise();
5897 }
5898
5899 requestErrored(request, error) {
5900 console.error(error);
5901 this.#resolveVisitPromise();
5902 }
5903
5904 requestFinished(_request) {
5905 clearBusyState(this.element);
5906 }
5907
5908 // Form submission delegate
5909
5910 formSubmissionStarted({ formElement }) {
5911 markAsBusy(formElement, this.#findFrameElement(formElement));
5912 }
5913
5914 formSubmissionSucceededWithResponse(formSubmission, response) {
5915 const frame = this.#findFrameElement(formSubmission.formElement, formSubmission.submitter);
5916
5917 frame.delegate.proposeVisitIfNavigatedWithAction(frame, getVisitAction(formSubmission.submitter, formSubmission.formElement, frame));
5918 frame.delegate.loadResponse(response);
5919
5920 if (!formSubmission.isSafe) {
5921 session.clearCache();
5922 }
5923 }
5924
5925 formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
5926 this.element.delegate.loadResponse(fetchResponse);
5927 session.clearCache();
5928 }
5929
5930 formSubmissionErrored(formSubmission, error) {
5931 console.error(error);
5932 }
5933
5934 formSubmissionFinished({ formElement }) {
5935 clearBusyState(formElement, this.#findFrameElement(formElement));
5936 }
5937
5938 // View delegate
5939
5940 allowsImmediateRender({ element: newFrame }, options) {
5941 const event = dispatch("turbo:before-frame-render", {
5942 target: this.element,
5943 detail: { newFrame, ...options },
5944 cancelable: true
5945 });
5946 const {
5947 defaultPrevented,
5948 detail: { render }
5949 } = event;
5950
5951 if (this.view.renderer && render) {
5952 this.view.renderer.renderElement = render;
5953 }
5954
5955 return !defaultPrevented
5956 }
5957
5958 viewRenderedSnapshot(_snapshot, _isPreview, _renderMethod) {}
5959
5960 preloadOnLoadLinksForView(element) {
5961 session.preloadOnLoadLinksForView(element);
5962 }
5963
5964 viewInvalidated() {}
5965
5966 // Frame renderer delegate
5967
5968 willRenderFrame(currentElement, _newElement) {
5969 this.previousFrameElement = currentElement.cloneNode(true);
5970 }
5971
5972 visitCachedSnapshot = ({ element }) => {
5973 const frame = element.querySelector("#" + this.element.id);
5974
5975 if (frame && this.previousFrameElement) {
5976 frame.replaceChildren(...this.previousFrameElement.children);
5977 }
5978
5979 delete this.previousFrameElement;
5980 }
5981
5982 // Private
5983
5984 async #loadFrameResponse(fetchResponse, document) {
5985 const newFrameElement = await this.extractForeignFrameElement(document.body);
5986
5987 if (newFrameElement) {
5988 const snapshot = new Snapshot(newFrameElement);
5989 const renderer = new FrameRenderer(this, this.view.snapshot, snapshot, FrameRenderer.renderElement, false, false);
5990 if (this.view.renderPromise) await this.view.renderPromise;
5991 this.changeHistory();
5992
5993 await this.view.render(renderer);
5994 this.complete = true;
5995 session.frameRendered(fetchResponse, this.element);
5996 session.frameLoaded(this.element);
5997 await this.fetchResponseLoaded(fetchResponse);
5998 } else if (this.#willHandleFrameMissingFromResponse(fetchResponse)) {
5999 this.#handleFrameMissingFromResponse(fetchResponse);
6000 }
6001 }
6002
6003 async #visit(url) {
6004 const request = new FetchRequest(this, FetchMethod.get, url, new URLSearchParams(), this.element);
6005
6006 this.#currentFetchRequest?.cancel();
6007 this.#currentFetchRequest = request;
6008
6009 return new Promise((resolve) => {
6010 this.#resolveVisitPromise = () => {
6011 this.#resolveVisitPromise = () => {};
6012 this.#currentFetchRequest = null;
6013 resolve();
6014 };
6015 request.perform();
6016 })
6017 }
6018
6019 #navigateFrame(element, url, submitter) {
6020 const frame = this.#findFrameElement(element, submitter);
6021
6022 frame.delegate.proposeVisitIfNavigatedWithAction(frame, getVisitAction(submitter, element, frame));
6023
6024 this.#withCurrentNavigationElement(element, () => {
6025 frame.src = url;
6026 });
6027 }
6028
6029 proposeVisitIfNavigatedWithAction(frame, action = null) {
6030 this.action = action;
6031
6032 if (this.action) {
6033 const pageSnapshot = PageSnapshot.fromElement(frame).clone();
6034 const { visitCachedSnapshot } = frame.delegate;
6035
6036 frame.delegate.fetchResponseLoaded = async (fetchResponse) => {
6037 if (frame.src) {
6038 const { statusCode, redirected } = fetchResponse;
6039 const responseHTML = await fetchResponse.responseHTML;
6040 const response = { statusCode, redirected, responseHTML };
6041 const options = {
6042 response,
6043 visitCachedSnapshot,
6044 willRender: false,
6045 updateHistory: false,
6046 restorationIdentifier: this.restorationIdentifier,
6047 snapshot: pageSnapshot
6048 };
6049
6050 if (this.action) options.action = this.action;
6051
6052 session.visit(frame.src, options);
6053 }
6054 };
6055 }
6056 }
6057
6058 changeHistory() {
6059 if (this.action) {
6060 const method = getHistoryMethodForAction(this.action);
6061 session.history.update(method, expandURL(this.element.src || ""), this.restorationIdentifier);
6062 }
6063 }
6064
6065 async #handleUnvisitableFrameResponse(fetchResponse) {
6066 console.warn(
6067 `The response (${fetchResponse.statusCode}) from <turbo-frame id="${this.element.id}"> is performing a full page visit due to turbo-visit-control.`
6068 );
6069
6070 await this.#visitResponse(fetchResponse.response);
6071 }
6072
6073 #willHandleFrameMissingFromResponse(fetchResponse) {
6074 this.element.setAttribute("complete", "");
6075
6076 const response = fetchResponse.response;
6077 const visit = async (url, options) => {
6078 if (url instanceof Response) {
6079 this.#visitResponse(url);
6080 } else {
6081 session.visit(url, options);
6082 }
6083 };
6084
6085 const event = dispatch("turbo:frame-missing", {
6086 target: this.element,
6087 detail: { response, visit },
6088 cancelable: true
6089 });
6090
6091 return !event.defaultPrevented
6092 }
6093
6094 #handleFrameMissingFromResponse(fetchResponse) {
6095 this.view.missing();
6096 this.#throwFrameMissingError(fetchResponse);
6097 }
6098
6099 #throwFrameMissingError(fetchResponse) {
6100 const message = `The response (${fetchResponse.statusCode}) did not contain the expected <turbo-frame id="${this.element.id}"> and will be ignored. To perform a full page visit instead, set turbo-visit-control to reload.`;
6101 throw new TurboFrameMissingError(message)
6102 }
6103
6104 async #visitResponse(response) {
6105 const wrapped = new FetchResponse(response);
6106 const responseHTML = await wrapped.responseHTML;
6107 const { location, redirected, statusCode } = wrapped;
6108
6109 return session.visit(location, { response: { redirected, statusCode, responseHTML } })
6110 }
6111
6112 #findFrameElement(element, submitter) {
6113 const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
6114 return getFrameElementById(id) ?? this.element
6115 }
6116
6117 async extractForeignFrameElement(container) {
6118 let element;
6119 const id = CSS.escape(this.id);
6120
6121 try {
6122 element = activateElement(container.querySelector(`turbo-frame#${id}`), this.sourceURL);
6123 if (element) {
6124 return element
6125 }
6126
6127 element = activateElement(container.querySelector(`turbo-frame[src][recurse~=${id}]`), this.sourceURL);
6128 if (element) {
6129 await element.loaded;
6130 return await this.extractForeignFrameElement(element)
6131 }
6132 } catch (error) {
6133 console.error(error);
6134 return new FrameElement()
6135 }
6136
6137 return null
6138 }
6139
6140 #formActionIsVisitable(form, submitter) {
6141 const action = getAction$1(form, submitter);
6142
6143 return locationIsVisitable(expandURL(action), this.rootLocation)
6144 }
6145
6146 #shouldInterceptNavigation(element, submitter) {
6147 const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
6148
6149 if (element instanceof HTMLFormElement && !this.#formActionIsVisitable(element, submitter)) {
6150 return false
6151 }
6152
6153 if (!this.enabled || id == "_top") {
6154 return false
6155 }
6156
6157 if (id) {
6158 const frameElement = getFrameElementById(id);
6159 if (frameElement) {
6160 return !frameElement.disabled
6161 }
6162 }
6163
6164 if (!session.elementIsNavigatable(element)) {
6165 return false
6166 }
6167
6168 if (submitter && !session.elementIsNavigatable(submitter)) {
6169 return false
6170 }
6171
6172 return true
6173 }
6174
6175 // Computed properties
6176
6177 get id() {
6178 return this.element.id
6179 }
6180
6181 get enabled() {
6182 return !this.element.disabled
6183 }
6184
6185 get sourceURL() {
6186 if (this.element.src) {
6187 return this.element.src
6188 }
6189 }
6190
6191 set sourceURL(sourceURL) {
6192 this.#ignoringChangesToAttribute("src", () => {
6193 this.element.src = sourceURL ?? null;
6194 });
6195 }
6196
6197 get loadingStyle() {
6198 return this.element.loading
6199 }
6200
6201 get isLoading() {
6202 return this.formSubmission !== undefined || this.#resolveVisitPromise() !== undefined
6203 }
6204
6205 get complete() {
6206 return this.element.hasAttribute("complete")
6207 }
6208
6209 set complete(value) {
6210 if (value) {
6211 this.element.setAttribute("complete", "");
6212 } else {
6213 this.element.removeAttribute("complete");
6214 }
6215 }
6216
6217 get isActive() {
6218 return this.element.isActive && this.#connected
6219 }
6220
6221 get rootLocation() {
6222 const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
6223 const root = meta?.content ?? "/";
6224 return expandURL(root)
6225 }
6226
6227 #isIgnoringChangesTo(attributeName) {
6228 return this.#ignoredAttributes.has(attributeName)
6229 }
6230
6231 #ignoringChangesToAttribute(attributeName, callback) {
6232 this.#ignoredAttributes.add(attributeName);
6233 callback();
6234 this.#ignoredAttributes.delete(attributeName);
6235 }
6236
6237 #withCurrentNavigationElement(element, callback) {
6238 this.currentNavigationElement = element;
6239 callback();
6240 delete this.currentNavigationElement;
6241 }
6242 }
6243
6244 function getFrameElementById(id) {
6245 if (id != null) {
6246 const element = document.getElementById(id);
6247 if (element instanceof FrameElement) {
6248 return element
6249 }
6250 }
6251 }
6252
6253 function activateElement(element, currentURL) {
6254 if (element) {
6255 const src = element.getAttribute("src");
6256 if (src != null && currentURL != null && urlsAreEqual(src, currentURL)) {
6257 throw new Error(`Matching <turbo-frame id="${element.id}"> element has a source URL which references itself`)
6258 }
6259 if (element.ownerDocument !== document) {
6260 element = document.importNode(element, true);
6261 }
6262
6263 if (element instanceof FrameElement) {
6264 element.connectedCallback();
6265 element.disconnectedCallback();
6266 return element
6267 }
6268 }
6269 }
6270
6271 const StreamActions = {
6272 after() {
6273 this.targetElements.forEach((e) => e.parentElement?.insertBefore(this.templateContent, e.nextSibling));
6274 },
6275
6276 append() {
6277 this.removeDuplicateTargetChildren();
6278 this.targetElements.forEach((e) => e.append(this.templateContent));
6279 },
6280
6281 before() {
6282 this.targetElements.forEach((e) => e.parentElement?.insertBefore(this.templateContent, e));
6283 },
6284
6285 prepend() {
6286 this.removeDuplicateTargetChildren();
6287 this.targetElements.forEach((e) => e.prepend(this.templateContent));
6288 },
6289
6290 remove() {
6291 this.targetElements.forEach((e) => e.remove());
6292 },
6293
6294 replace() {
6295 this.targetElements.forEach((e) => e.replaceWith(this.templateContent));
6296 },
6297
6298 update() {
6299 this.targetElements.forEach((targetElement) => {
6300 targetElement.innerHTML = "";
6301 targetElement.append(this.templateContent);
6302 });
6303 },
6304
6305 refresh() {
6306 session.refresh(this.baseURI, this.requestId);
6307 }
6308 };
6309
6310 // <turbo-stream action=replace target=id><template>...
6311
6312 /**
6313 * Renders updates to the page from a stream of messages.
6314 *
6315 * Using the `action` attribute, this can be configured one of four ways:
6316 *
6317 * - `append` - appends the result to the container
6318 * - `prepend` - prepends the result to the container
6319 * - `replace` - replaces the contents of the container
6320 * - `remove` - removes the container
6321 * - `before` - inserts the result before the target
6322 * - `after` - inserts the result after the target
6323 *
6324 * @customElement turbo-stream
6325 * @example
6326 * <turbo-stream action="append" target="dom_id">
6327 * <template>
6328 * Content to append to container designated with the dom_id.
6329 * </template>
6330 * </turbo-stream>
6331 */
6332 class StreamElement extends HTMLElement {
6333 static async renderElement(newElement) {
6334 await newElement.performAction();
6335 }
6336
6337 async connectedCallback() {
6338 try {
6339 await this.render();
6340 } catch (error) {
6341 console.error(error);
6342 } finally {
6343 this.disconnect();
6344 }
6345 }
6346
6347 async render() {
6348 return (this.renderPromise ??= (async () => {
6349 const event = this.beforeRenderEvent;
6350
6351 if (this.dispatchEvent(event)) {
6352 await nextRepaint();
6353 await event.detail.render(this);
6354 }
6355 })())
6356 }
6357
6358 disconnect() {
6359 try {
6360 this.remove();
6361 // eslint-disable-next-line no-empty
6362 } catch {}
6363 }
6364
6365 /**
6366 * Removes duplicate children (by ID)
6367 */
6368 removeDuplicateTargetChildren() {
6369 this.duplicateChildren.forEach((c) => c.remove());
6370 }
6371
6372 /**
6373 * Gets the list of duplicate children (i.e. those with the same ID)
6374 */
6375 get duplicateChildren() {
6376 const existingChildren = this.targetElements.flatMap((e) => [...e.children]).filter((c) => !!c.id);
6377 const newChildrenIds = [...(this.templateContent?.children || [])].filter((c) => !!c.id).map((c) => c.id);
6378
6379 return existingChildren.filter((c) => newChildrenIds.includes(c.id))
6380 }
6381
6382 /**
6383 * Gets the action function to be performed.
6384 */
6385 get performAction() {
6386 if (this.action) {
6387 const actionFunction = StreamActions[this.action];
6388 if (actionFunction) {
6389 return actionFunction
6390 }
6391 this.#raise("unknown action");
6392 }
6393 this.#raise("action attribute is missing");
6394 }
6395
6396 /**
6397 * Gets the target elements which the template will be rendered to.
6398 */
6399 get targetElements() {
6400 if (this.target) {
6401 return this.targetElementsById
6402 } else if (this.targets) {
6403 return this.targetElementsByQuery
6404 } else {
6405 this.#raise("target or targets attribute is missing");
6406 }
6407 }
6408
6409 /**
6410 * Gets the contents of the main `<template>`.
6411 */
6412 get templateContent() {
6413 return this.templateElement.content.cloneNode(true)
6414 }
6415
6416 /**
6417 * Gets the main `<template>` used for rendering
6418 */
6419 get templateElement() {
6420 if (this.firstElementChild === null) {
6421 const template = this.ownerDocument.createElement("template");
6422 this.appendChild(template);
6423 return template
6424 } else if (this.firstElementChild instanceof HTMLTemplateElement) {
6425 return this.firstElementChild
6426 }
6427 this.#raise("first child element must be a <template> element");
6428 }
6429
6430 /**
6431 * Gets the current action.
6432 */
6433 get action() {
6434 return this.getAttribute("action")
6435 }
6436
6437 /**
6438 * Gets the current target (an element ID) to which the result will
6439 * be rendered.
6440 */
6441 get target() {
6442 return this.getAttribute("target")
6443 }
6444
6445 /**
6446 * Gets the current "targets" selector (a CSS selector)
6447 */
6448 get targets() {
6449 return this.getAttribute("targets")
6450 }
6451
6452 /**
6453 * Reads the request-id attribute
6454 */
6455 get requestId() {
6456 return this.getAttribute("request-id")
6457 }
6458
6459 #raise(message) {
6460 throw new Error(`${this.description}: ${message}`)
6461 }
6462
6463 get description() {
6464 return (this.outerHTML.match(/<[^>]+>/) ?? [])[0] ?? "<turbo-stream>"
6465 }
6466
6467 get beforeRenderEvent() {
6468 return new CustomEvent("turbo:before-stream-render", {
6469 bubbles: true,
6470 cancelable: true,
6471 detail: { newStream: this, render: StreamElement.renderElement }
6472 })
6473 }
6474
6475 get targetElementsById() {
6476 const element = this.ownerDocument?.getElementById(this.target);
6477
6478 if (element !== null) {
6479 return [element]
6480 } else {
6481 return []
6482 }
6483 }
6484
6485 get targetElementsByQuery() {
6486 const elements = this.ownerDocument?.querySelectorAll(this.targets);
6487
6488 if (elements.length !== 0) {
6489 return Array.prototype.slice.call(elements)
6490 } else {
6491 return []
6492 }
6493 }
6494 }
6495
6496 class StreamSourceElement extends HTMLElement {
6497 streamSource = null
6498
6499 connectedCallback() {
6500 this.streamSource = this.src.match(/^ws{1,2}:/) ? new WebSocket(this.src) : new EventSource(this.src);
6501
6502 connectStreamSource(this.streamSource);
6503 }
6504
6505 disconnectedCallback() {
6506 if (this.streamSource) {
6507 this.streamSource.close();
6508
6509 disconnectStreamSource(this.streamSource);
6510 }
6511 }
6512
6513 get src() {
6514 return this.getAttribute("src") || ""
6515 }
6516 }
6517
6518 FrameElement.delegateConstructor = FrameController;
6519
6520 if (customElements.get("turbo-frame") === undefined) {
6521 customElements.define("turbo-frame", FrameElement);
6522 }
6523
6524 if (customElements.get("turbo-stream") === undefined) {
6525 customElements.define("turbo-stream", StreamElement);
6526 }
6527
6528 if (customElements.get("turbo-stream-source") === undefined) {
6529 customElements.define("turbo-stream-source", StreamSourceElement);
6530 }
6531
6532 (() => {
6533 let element = document.currentScript;
6534 if (!element) return
6535 if (element.hasAttribute("data-turbo-suppress-warning")) return
6536
6537 element = element.parentElement;
6538 while (element) {
6539 if (element == document.body) {
6540 return console.warn(
6541 unindent`
6542 You are loading Turbo from a <script> element inside the <body> element. This is probably not what you meant to do!
6543
6544 Load your application’s JavaScript bundle inside the <head> element instead. <script> elements in <body> are evaluated with each page change.
6545
6546 For more information, see: https://turbo.hotwired.dev/handbook/building#working-with-script-elements
6547
6548 ——
6549 Suppress this warning by adding a "data-turbo-suppress-warning" attribute to: %s
6550 `,
6551 element.outerHTML
6552 )
6553 }
6554
6555 element = element.parentElement;
6556 }
6557 })();
6558
6559 window.Turbo = { ...Turbo, StreamActions };
6560 start();
6561
6562 exports.FetchEnctype = FetchEnctype;
6563 exports.FetchMethod = FetchMethod;
6564 exports.FetchRequest = FetchRequest;
6565 exports.FetchResponse = FetchResponse;
6566 exports.FrameElement = FrameElement;
6567 exports.FrameLoadingStyle = FrameLoadingStyle;
6568 exports.FrameRenderer = FrameRenderer;
6569 exports.PageRenderer = PageRenderer;
6570 exports.PageSnapshot = PageSnapshot;
6571 exports.StreamActions = StreamActions;
6572 exports.StreamElement = StreamElement;
6573 exports.StreamSourceElement = StreamSourceElement;
6574 exports.cache = cache;
6575 exports.clearCache = clearCache;
6576 exports.connectStreamSource = connectStreamSource;
6577 exports.disconnectStreamSource = disconnectStreamSource;
6578 exports.fetch = fetchWithTurboHeaders;
6579 exports.fetchEnctypeFromString = fetchEnctypeFromString;
6580 exports.fetchMethodFromString = fetchMethodFromString;
6581 exports.isSafe = isSafe;
6582 exports.navigator = navigator$1;
6583 exports.registerAdapter = registerAdapter;
6584 exports.renderStreamMessage = renderStreamMessage;
6585 exports.session = session;
6586 exports.setConfirmMethod = setConfirmMethod;
6587 exports.setFormMode = setFormMode;
6588 exports.setProgressBarDelay = setProgressBarDelay;
6589 exports.start = start;
6590 exports.visit = visit;
6591
6592 Object.defineProperty(exports, '__esModule', { value: true });
6593
6594}));