turbo-rails 1.5.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +3 -6
- data/app/assets/javascripts/turbo.js +1894 -725
- data/app/assets/javascripts/turbo.min.js +9 -5
- data/app/assets/javascripts/turbo.min.js.map +1 -1
- data/app/channels/turbo/streams/broadcasts.rb +18 -4
- data/app/controllers/concerns/turbo/request_id_tracking.rb +12 -0
- data/app/helpers/turbo/drive_helper.rb +63 -4
- data/app/helpers/turbo/frames_helper.rb +4 -1
- data/app/helpers/turbo/streams/action_helper.rb +5 -1
- data/app/javascript/turbo/index.js +2 -0
- data/app/jobs/turbo/streams/action_broadcast_job.rb +2 -2
- data/app/jobs/turbo/streams/broadcast_job.rb +1 -1
- data/app/jobs/turbo/streams/broadcast_stream_job.rb +7 -0
- data/app/models/concerns/turbo/broadcastable.rb +87 -20
- data/app/models/turbo/debouncer.rb +24 -0
- data/app/models/turbo/streams/tag_builder.rb +20 -0
- data/app/models/turbo/thread_debouncer.rb +28 -0
- data/config/routes.rb +0 -1
- data/lib/install/turbo_with_importmap.rb +1 -1
- data/lib/turbo/broadcastable/test_helper.rb +1 -1
- data/lib/turbo/engine.rb +6 -0
- data/lib/turbo/version.rb +1 -1
- data/lib/turbo-rails.rb +10 -0
- metadata +6 -2
@@ -1,19 +1,7 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
const BuiltInHTMLElement = HTMLElement;
|
6
|
-
const wrapperForTheName = {
|
7
|
-
HTMLElement: function HTMLElement() {
|
8
|
-
return Reflect.construct(BuiltInHTMLElement, [], this.constructor);
|
9
|
-
}
|
10
|
-
};
|
11
|
-
window.HTMLElement = wrapperForTheName["HTMLElement"];
|
12
|
-
HTMLElement.prototype = BuiltInHTMLElement.prototype;
|
13
|
-
HTMLElement.prototype.constructor = HTMLElement;
|
14
|
-
Object.setPrototypeOf(HTMLElement, BuiltInHTMLElement);
|
15
|
-
})();
|
16
|
-
|
1
|
+
/*!
|
2
|
+
Turbo 8.0.0
|
3
|
+
Copyright © 2024 37signals LLC
|
4
|
+
*/
|
17
5
|
(function(prototype) {
|
18
6
|
if (typeof prototype.requestSubmit == "function") return;
|
19
7
|
prototype.requestSubmit = function(submitter) {
|
@@ -44,7 +32,7 @@ const submittersByForm = new WeakMap;
|
|
44
32
|
function findSubmitterFromClickTarget(target) {
|
45
33
|
const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
|
46
34
|
const candidate = element ? element.closest("input, button") : null;
|
47
|
-
return
|
35
|
+
return candidate?.type == "submit" ? candidate : null;
|
48
36
|
}
|
49
37
|
|
50
38
|
function clickCaptured(event) {
|
@@ -57,10 +45,13 @@ function clickCaptured(event) {
|
|
57
45
|
(function() {
|
58
46
|
if ("submitter" in Event.prototype) return;
|
59
47
|
let prototype = window.Event.prototype;
|
60
|
-
if ("SubmitEvent" in window
|
61
|
-
|
62
|
-
|
63
|
-
|
48
|
+
if ("SubmitEvent" in window) {
|
49
|
+
const prototypeOfSubmitEvent = window.SubmitEvent.prototype;
|
50
|
+
if (/Apple Computer/.test(navigator.vendor) && !("submitter" in prototypeOfSubmitEvent)) {
|
51
|
+
prototype = prototypeOfSubmitEvent;
|
52
|
+
} else {
|
53
|
+
return;
|
54
|
+
}
|
64
55
|
}
|
65
56
|
addEventListener("click", clickCaptured, true);
|
66
57
|
Object.defineProperty(prototype, "submitter", {
|
@@ -72,20 +63,19 @@ function clickCaptured(event) {
|
|
72
63
|
});
|
73
64
|
})();
|
74
65
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
FrameLoadingStyle["lazy"] = "lazy";
|
80
|
-
})(FrameLoadingStyle || (FrameLoadingStyle = {}));
|
66
|
+
const FrameLoadingStyle = {
|
67
|
+
eager: "eager",
|
68
|
+
lazy: "lazy"
|
69
|
+
};
|
81
70
|
|
82
71
|
class FrameElement extends HTMLElement {
|
72
|
+
static delegateConstructor=undefined;
|
73
|
+
loaded=Promise.resolve();
|
83
74
|
static get observedAttributes() {
|
84
75
|
return [ "disabled", "complete", "loading", "src" ];
|
85
76
|
}
|
86
77
|
constructor() {
|
87
78
|
super();
|
88
|
-
this.loaded = Promise.resolve();
|
89
79
|
this.delegate = new FrameElement.delegateConstructor(this);
|
90
80
|
}
|
91
81
|
connectedCallback() {
|
@@ -118,6 +108,16 @@ class FrameElement extends HTMLElement {
|
|
118
108
|
this.removeAttribute("src");
|
119
109
|
}
|
120
110
|
}
|
111
|
+
get refresh() {
|
112
|
+
return this.getAttribute("refresh");
|
113
|
+
}
|
114
|
+
set refresh(value) {
|
115
|
+
if (value) {
|
116
|
+
this.setAttribute("refresh", value);
|
117
|
+
} else {
|
118
|
+
this.removeAttribute("refresh");
|
119
|
+
}
|
120
|
+
}
|
121
121
|
get loading() {
|
122
122
|
return frameLoadingStyleFromString(this.getAttribute("loading") || "");
|
123
123
|
}
|
@@ -155,8 +155,7 @@ class FrameElement extends HTMLElement {
|
|
155
155
|
return this.ownerDocument === document && !this.isPreview;
|
156
156
|
}
|
157
157
|
get isPreview() {
|
158
|
-
|
159
|
-
return (_b = (_a = this.ownerDocument) === null || _a === void 0 ? void 0 : _a.documentElement) === null || _b === void 0 ? void 0 : _b.hasAttribute("data-turbo-preview");
|
158
|
+
return this.ownerDocument?.documentElement?.hasAttribute("data-turbo-preview");
|
160
159
|
}
|
161
160
|
}
|
162
161
|
|
@@ -183,8 +182,8 @@ function getAnchor(url) {
|
|
183
182
|
}
|
184
183
|
}
|
185
184
|
|
186
|
-
function getAction(form, submitter) {
|
187
|
-
const action =
|
185
|
+
function getAction$1(form, submitter) {
|
186
|
+
const action = submitter?.getAttribute("formaction") || form.getAttribute("action") || form.action;
|
188
187
|
return expandURL(action);
|
189
188
|
}
|
190
189
|
|
@@ -323,6 +322,14 @@ function dispatch(eventName, {target: target, cancelable: cancelable, detail: de
|
|
323
322
|
return event;
|
324
323
|
}
|
325
324
|
|
325
|
+
function nextRepaint() {
|
326
|
+
if (document.visibilityState === "hidden") {
|
327
|
+
return nextEventLoopTick();
|
328
|
+
} else {
|
329
|
+
return nextAnimationFrame();
|
330
|
+
}
|
331
|
+
}
|
332
|
+
|
326
333
|
function nextAnimationFrame() {
|
327
334
|
return new Promise((resolve => requestAnimationFrame((() => resolve()))));
|
328
335
|
}
|
@@ -370,7 +377,7 @@ function uuid() {
|
|
370
377
|
}
|
371
378
|
|
372
379
|
function getAttribute(attributeName, ...elements) {
|
373
|
-
for (const value of elements.map((element => element
|
380
|
+
for (const value of elements.map((element => element?.getAttribute(attributeName)))) {
|
374
381
|
if (typeof value == "string") return value;
|
375
382
|
}
|
376
383
|
return null;
|
@@ -456,21 +463,83 @@ function setMetaContent(name, content) {
|
|
456
463
|
}
|
457
464
|
|
458
465
|
function findClosestRecursively(element, selector) {
|
459
|
-
var _a;
|
460
466
|
if (element instanceof Element) {
|
461
|
-
return element.closest(selector) || findClosestRecursively(element.assignedSlot ||
|
467
|
+
return element.closest(selector) || findClosestRecursively(element.assignedSlot || element.getRootNode()?.host, selector);
|
468
|
+
}
|
469
|
+
}
|
470
|
+
|
471
|
+
function elementIsFocusable(element) {
|
472
|
+
const inertDisabledOrHidden = "[inert], :disabled, [hidden], details:not([open]), dialog:not([open])";
|
473
|
+
return !!element && element.closest(inertDisabledOrHidden) == null && typeof element.focus == "function";
|
474
|
+
}
|
475
|
+
|
476
|
+
function queryAutofocusableElement(elementOrDocumentFragment) {
|
477
|
+
return Array.from(elementOrDocumentFragment.querySelectorAll("[autofocus]")).find(elementIsFocusable);
|
478
|
+
}
|
479
|
+
|
480
|
+
async function around(callback, reader) {
|
481
|
+
const before = reader();
|
482
|
+
callback();
|
483
|
+
await nextAnimationFrame();
|
484
|
+
const after = reader();
|
485
|
+
return [ before, after ];
|
486
|
+
}
|
487
|
+
|
488
|
+
function doesNotTargetIFrame(anchor) {
|
489
|
+
if (anchor.hasAttribute("target")) {
|
490
|
+
for (const element of document.getElementsByName(anchor.target)) {
|
491
|
+
if (element instanceof HTMLIFrameElement) return false;
|
492
|
+
}
|
493
|
+
}
|
494
|
+
return true;
|
495
|
+
}
|
496
|
+
|
497
|
+
function findLinkFromClickTarget(target) {
|
498
|
+
return findClosestRecursively(target, "a[href]:not([target^=_]):not([download])");
|
499
|
+
}
|
500
|
+
|
501
|
+
function getLocationForLink(link) {
|
502
|
+
return expandURL(link.getAttribute("href") || "");
|
503
|
+
}
|
504
|
+
|
505
|
+
function debounce(fn, delay) {
|
506
|
+
let timeoutId = null;
|
507
|
+
return (...args) => {
|
508
|
+
const callback = () => fn.apply(this, args);
|
509
|
+
clearTimeout(timeoutId);
|
510
|
+
timeoutId = setTimeout(callback, delay);
|
511
|
+
};
|
512
|
+
}
|
513
|
+
|
514
|
+
class LimitedSet extends Set {
|
515
|
+
constructor(maxSize) {
|
516
|
+
super();
|
517
|
+
this.maxSize = maxSize;
|
518
|
+
}
|
519
|
+
add(value) {
|
520
|
+
if (this.size >= this.maxSize) {
|
521
|
+
const iterator = this.values();
|
522
|
+
const oldestValue = iterator.next().value;
|
523
|
+
this.delete(oldestValue);
|
524
|
+
}
|
525
|
+
super.add(value);
|
462
526
|
}
|
463
527
|
}
|
464
528
|
|
465
|
-
|
529
|
+
const recentRequests = new LimitedSet(20);
|
466
530
|
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
531
|
+
const nativeFetch = window.fetch;
|
532
|
+
|
533
|
+
function fetchWithTurboHeaders(url, options = {}) {
|
534
|
+
const modifiedHeaders = new Headers(options.headers || {});
|
535
|
+
const requestUID = uuid();
|
536
|
+
recentRequests.add(requestUID);
|
537
|
+
modifiedHeaders.append("X-Turbo-Request-Id", requestUID);
|
538
|
+
return nativeFetch(url, {
|
539
|
+
...options,
|
540
|
+
headers: modifiedHeaders
|
541
|
+
});
|
542
|
+
}
|
474
543
|
|
475
544
|
function fetchMethodFromString(method) {
|
476
545
|
switch (method.toLowerCase()) {
|
@@ -491,16 +560,81 @@ function fetchMethodFromString(method) {
|
|
491
560
|
}
|
492
561
|
}
|
493
562
|
|
563
|
+
const FetchMethod = {
|
564
|
+
get: "get",
|
565
|
+
post: "post",
|
566
|
+
put: "put",
|
567
|
+
patch: "patch",
|
568
|
+
delete: "delete"
|
569
|
+
};
|
570
|
+
|
571
|
+
function fetchEnctypeFromString(encoding) {
|
572
|
+
switch (encoding.toLowerCase()) {
|
573
|
+
case FetchEnctype.multipart:
|
574
|
+
return FetchEnctype.multipart;
|
575
|
+
|
576
|
+
case FetchEnctype.plain:
|
577
|
+
return FetchEnctype.plain;
|
578
|
+
|
579
|
+
default:
|
580
|
+
return FetchEnctype.urlEncoded;
|
581
|
+
}
|
582
|
+
}
|
583
|
+
|
584
|
+
const FetchEnctype = {
|
585
|
+
urlEncoded: "application/x-www-form-urlencoded",
|
586
|
+
multipart: "multipart/form-data",
|
587
|
+
plain: "text/plain"
|
588
|
+
};
|
589
|
+
|
494
590
|
class FetchRequest {
|
495
|
-
|
496
|
-
|
497
|
-
|
591
|
+
abortController=new AbortController;
|
592
|
+
#resolveRequestPromise=_value => {};
|
593
|
+
constructor(delegate, method, location, requestBody = new URLSearchParams, target = null, enctype = FetchEnctype.urlEncoded) {
|
594
|
+
const [url, body] = buildResourceAndBody(expandURL(location), method, requestBody, enctype);
|
498
595
|
this.delegate = delegate;
|
499
|
-
this.
|
500
|
-
this.headers = this.defaultHeaders;
|
501
|
-
this.body = body;
|
502
|
-
this.url = location;
|
596
|
+
this.url = url;
|
503
597
|
this.target = target;
|
598
|
+
this.fetchOptions = {
|
599
|
+
credentials: "same-origin",
|
600
|
+
redirect: "follow",
|
601
|
+
method: method,
|
602
|
+
headers: {
|
603
|
+
...this.defaultHeaders
|
604
|
+
},
|
605
|
+
body: body,
|
606
|
+
signal: this.abortSignal,
|
607
|
+
referrer: this.delegate.referrer?.href
|
608
|
+
};
|
609
|
+
this.enctype = enctype;
|
610
|
+
}
|
611
|
+
get method() {
|
612
|
+
return this.fetchOptions.method;
|
613
|
+
}
|
614
|
+
set method(value) {
|
615
|
+
const fetchBody = this.isSafe ? this.url.searchParams : this.fetchOptions.body || new FormData;
|
616
|
+
const fetchMethod = fetchMethodFromString(value) || FetchMethod.get;
|
617
|
+
this.url.search = "";
|
618
|
+
const [url, body] = buildResourceAndBody(this.url, fetchMethod, fetchBody, this.enctype);
|
619
|
+
this.url = url;
|
620
|
+
this.fetchOptions.body = body;
|
621
|
+
this.fetchOptions.method = fetchMethod;
|
622
|
+
}
|
623
|
+
get headers() {
|
624
|
+
return this.fetchOptions.headers;
|
625
|
+
}
|
626
|
+
set headers(value) {
|
627
|
+
this.fetchOptions.headers = value;
|
628
|
+
}
|
629
|
+
get body() {
|
630
|
+
if (this.isSafe) {
|
631
|
+
return this.url.searchParams;
|
632
|
+
} else {
|
633
|
+
return this.fetchOptions.body;
|
634
|
+
}
|
635
|
+
}
|
636
|
+
set body(value) {
|
637
|
+
this.fetchOptions.body = value;
|
504
638
|
}
|
505
639
|
get location() {
|
506
640
|
return this.url;
|
@@ -517,14 +651,19 @@ class FetchRequest {
|
|
517
651
|
async perform() {
|
518
652
|
const {fetchOptions: fetchOptions} = this;
|
519
653
|
this.delegate.prepareRequest(this);
|
520
|
-
await this
|
654
|
+
const event = await this.#allowRequestToBeIntercepted(fetchOptions);
|
521
655
|
try {
|
522
656
|
this.delegate.requestStarted(this);
|
523
|
-
|
657
|
+
if (event.detail.fetchRequest) {
|
658
|
+
this.response = event.detail.fetchRequest.response;
|
659
|
+
} else {
|
660
|
+
this.response = fetchWithTurboHeaders(this.url.href, fetchOptions);
|
661
|
+
}
|
662
|
+
const response = await this.response;
|
524
663
|
return await this.receive(response);
|
525
664
|
} catch (error) {
|
526
665
|
if (error.name !== "AbortError") {
|
527
|
-
if (this
|
666
|
+
if (this.#willDelegateErrorHandling(error)) {
|
528
667
|
this.delegate.requestErrored(this, error);
|
529
668
|
}
|
530
669
|
throw error;
|
@@ -551,25 +690,13 @@ class FetchRequest {
|
|
551
690
|
}
|
552
691
|
return fetchResponse;
|
553
692
|
}
|
554
|
-
get fetchOptions() {
|
555
|
-
var _a;
|
556
|
-
return {
|
557
|
-
method: FetchMethod[this.method].toUpperCase(),
|
558
|
-
credentials: "same-origin",
|
559
|
-
headers: this.headers,
|
560
|
-
redirect: "follow",
|
561
|
-
body: this.isSafe ? null : this.body,
|
562
|
-
signal: this.abortSignal,
|
563
|
-
referrer: (_a = this.delegate.referrer) === null || _a === void 0 ? void 0 : _a.href
|
564
|
-
};
|
565
|
-
}
|
566
693
|
get defaultHeaders() {
|
567
694
|
return {
|
568
695
|
Accept: "text/html, application/xhtml+xml"
|
569
696
|
};
|
570
697
|
}
|
571
698
|
get isSafe() {
|
572
|
-
return this.method
|
699
|
+
return isSafe(this.method);
|
573
700
|
}
|
574
701
|
get abortSignal() {
|
575
702
|
return this.abortController.signal;
|
@@ -577,20 +704,22 @@ class FetchRequest {
|
|
577
704
|
acceptResponseType(mimeType) {
|
578
705
|
this.headers["Accept"] = [ mimeType, this.headers["Accept"] ].join(", ");
|
579
706
|
}
|
580
|
-
async allowRequestToBeIntercepted(fetchOptions) {
|
581
|
-
const requestInterception = new Promise((resolve => this
|
707
|
+
async #allowRequestToBeIntercepted(fetchOptions) {
|
708
|
+
const requestInterception = new Promise((resolve => this.#resolveRequestPromise = resolve));
|
582
709
|
const event = dispatch("turbo:before-fetch-request", {
|
583
710
|
cancelable: true,
|
584
711
|
detail: {
|
585
712
|
fetchOptions: fetchOptions,
|
586
713
|
url: this.url,
|
587
|
-
resume: this
|
714
|
+
resume: this.#resolveRequestPromise
|
588
715
|
},
|
589
716
|
target: this.target
|
590
717
|
});
|
718
|
+
this.url = event.detail.url;
|
591
719
|
if (event.defaultPrevented) await requestInterception;
|
720
|
+
return event;
|
592
721
|
}
|
593
|
-
willDelegateErrorHandling(error) {
|
722
|
+
#willDelegateErrorHandling(error) {
|
594
723
|
const event = dispatch("turbo:fetch-request-error", {
|
595
724
|
target: this.target,
|
596
725
|
cancelable: true,
|
@@ -603,15 +732,38 @@ class FetchRequest {
|
|
603
732
|
}
|
604
733
|
}
|
605
734
|
|
735
|
+
function isSafe(fetchMethod) {
|
736
|
+
return fetchMethodFromString(fetchMethod) == FetchMethod.get;
|
737
|
+
}
|
738
|
+
|
739
|
+
function buildResourceAndBody(resource, method, requestBody, enctype) {
|
740
|
+
const searchParams = Array.from(requestBody).length > 0 ? new URLSearchParams(entriesExcludingFiles(requestBody)) : resource.searchParams;
|
741
|
+
if (isSafe(method)) {
|
742
|
+
return [ mergeIntoURLSearchParams(resource, searchParams), null ];
|
743
|
+
} else if (enctype == FetchEnctype.urlEncoded) {
|
744
|
+
return [ resource, searchParams ];
|
745
|
+
} else {
|
746
|
+
return [ resource, requestBody ];
|
747
|
+
}
|
748
|
+
}
|
749
|
+
|
750
|
+
function entriesExcludingFiles(requestBody) {
|
751
|
+
const entries = [];
|
752
|
+
for (const [name, value] of requestBody) {
|
753
|
+
if (value instanceof File) continue; else entries.push([ name, value ]);
|
754
|
+
}
|
755
|
+
return entries;
|
756
|
+
}
|
757
|
+
|
758
|
+
function mergeIntoURLSearchParams(url, requestBody) {
|
759
|
+
const searchParams = new URLSearchParams(entriesExcludingFiles(requestBody));
|
760
|
+
url.search = searchParams.toString();
|
761
|
+
return url;
|
762
|
+
}
|
763
|
+
|
606
764
|
class AppearanceObserver {
|
765
|
+
started=false;
|
607
766
|
constructor(delegate, element) {
|
608
|
-
this.started = false;
|
609
|
-
this.intersect = entries => {
|
610
|
-
const lastEntry = entries.slice(-1)[0];
|
611
|
-
if (lastEntry === null || lastEntry === void 0 ? void 0 : lastEntry.isIntersecting) {
|
612
|
-
this.delegate.elementAppearedInViewport(this.element);
|
613
|
-
}
|
614
|
-
};
|
615
767
|
this.delegate = delegate;
|
616
768
|
this.element = element;
|
617
769
|
this.intersectionObserver = new IntersectionObserver(this.intersect);
|
@@ -628,9 +780,16 @@ class AppearanceObserver {
|
|
628
780
|
this.intersectionObserver.unobserve(this.element);
|
629
781
|
}
|
630
782
|
}
|
783
|
+
intersect=entries => {
|
784
|
+
const lastEntry = entries.slice(-1)[0];
|
785
|
+
if (lastEntry?.isIntersecting) {
|
786
|
+
this.delegate.elementAppearedInViewport(this.element);
|
787
|
+
}
|
788
|
+
};
|
631
789
|
}
|
632
790
|
|
633
791
|
class StreamMessage {
|
792
|
+
static contentType="text/vnd.turbo-stream.html";
|
634
793
|
static wrap(message) {
|
635
794
|
if (typeof message == "string") {
|
636
795
|
return new this(createDocumentFragment(message));
|
@@ -643,8 +802,6 @@ class StreamMessage {
|
|
643
802
|
}
|
644
803
|
}
|
645
804
|
|
646
|
-
StreamMessage.contentType = "text/vnd.turbo-stream.html";
|
647
|
-
|
648
805
|
function importStreamElements(fragment) {
|
649
806
|
for (const element of fragment.querySelectorAll("turbo-stream")) {
|
650
807
|
const streamElement = document.importNode(element, true);
|
@@ -656,85 +813,89 @@ function importStreamElements(fragment) {
|
|
656
813
|
return fragment;
|
657
814
|
}
|
658
815
|
|
659
|
-
|
660
|
-
|
661
|
-
(function(FormSubmissionState) {
|
662
|
-
FormSubmissionState[FormSubmissionState["initialized"] = 0] = "initialized";
|
663
|
-
FormSubmissionState[FormSubmissionState["requesting"] = 1] = "requesting";
|
664
|
-
FormSubmissionState[FormSubmissionState["waiting"] = 2] = "waiting";
|
665
|
-
FormSubmissionState[FormSubmissionState["receiving"] = 3] = "receiving";
|
666
|
-
FormSubmissionState[FormSubmissionState["stopping"] = 4] = "stopping";
|
667
|
-
FormSubmissionState[FormSubmissionState["stopped"] = 5] = "stopped";
|
668
|
-
})(FormSubmissionState || (FormSubmissionState = {}));
|
669
|
-
|
670
|
-
var FormEnctype;
|
816
|
+
const PREFETCH_DELAY = 100;
|
671
817
|
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
818
|
+
class PrefetchCache {
|
819
|
+
#prefetchTimeout=null;
|
820
|
+
#prefetched=null;
|
821
|
+
get(url) {
|
822
|
+
if (this.#prefetched && this.#prefetched.url === url && this.#prefetched.expire > Date.now()) {
|
823
|
+
return this.#prefetched.request;
|
824
|
+
}
|
825
|
+
}
|
826
|
+
setLater(url, request, ttl) {
|
827
|
+
this.clear();
|
828
|
+
this.#prefetchTimeout = setTimeout((() => {
|
829
|
+
request.perform();
|
830
|
+
this.set(url, request, ttl);
|
831
|
+
this.#prefetchTimeout = null;
|
832
|
+
}), PREFETCH_DELAY);
|
833
|
+
}
|
834
|
+
set(url, request, ttl) {
|
835
|
+
this.#prefetched = {
|
836
|
+
url: url,
|
837
|
+
request: request,
|
838
|
+
expire: new Date((new Date).getTime() + ttl)
|
839
|
+
};
|
840
|
+
}
|
841
|
+
clear() {
|
842
|
+
if (this.#prefetchTimeout) clearTimeout(this.#prefetchTimeout);
|
843
|
+
this.#prefetched = null;
|
844
|
+
}
|
845
|
+
}
|
677
846
|
|
678
|
-
|
679
|
-
switch (encoding.toLowerCase()) {
|
680
|
-
case FormEnctype.multipart:
|
681
|
-
return FormEnctype.multipart;
|
847
|
+
const cacheTtl = 10 * 1e3;
|
682
848
|
|
683
|
-
|
684
|
-
return FormEnctype.plain;
|
849
|
+
const prefetchCache = new PrefetchCache;
|
685
850
|
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
851
|
+
const FormSubmissionState = {
|
852
|
+
initialized: "initialized",
|
853
|
+
requesting: "requesting",
|
854
|
+
waiting: "waiting",
|
855
|
+
receiving: "receiving",
|
856
|
+
stopping: "stopping",
|
857
|
+
stopped: "stopped"
|
858
|
+
};
|
690
859
|
|
691
860
|
class FormSubmission {
|
861
|
+
state=FormSubmissionState.initialized;
|
692
862
|
static confirmMethod(message, _element, _submitter) {
|
693
863
|
return Promise.resolve(confirm(message));
|
694
864
|
}
|
695
865
|
constructor(delegate, formElement, submitter, mustRedirect = false) {
|
696
|
-
|
866
|
+
const method = getMethod(formElement, submitter);
|
867
|
+
const action = getAction(getFormAction(formElement, submitter), method);
|
868
|
+
const body = buildFormData(formElement, submitter);
|
869
|
+
const enctype = getEnctype(formElement, submitter);
|
697
870
|
this.delegate = delegate;
|
698
871
|
this.formElement = formElement;
|
699
872
|
this.submitter = submitter;
|
700
|
-
this.
|
701
|
-
this.location = expandURL(this.action);
|
702
|
-
if (this.method == FetchMethod.get) {
|
703
|
-
mergeFormDataEntries(this.location, [ ...this.body.entries() ]);
|
704
|
-
}
|
705
|
-
this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body, this.formElement);
|
873
|
+
this.fetchRequest = new FetchRequest(this, method, action, body, formElement, enctype);
|
706
874
|
this.mustRedirect = mustRedirect;
|
707
875
|
}
|
708
876
|
get method() {
|
709
|
-
|
710
|
-
|
711
|
-
|
877
|
+
return this.fetchRequest.method;
|
878
|
+
}
|
879
|
+
set method(value) {
|
880
|
+
this.fetchRequest.method = value;
|
712
881
|
}
|
713
882
|
get action() {
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
} else {
|
719
|
-
return this.formElement.getAttribute("action") || formElementAction || "";
|
720
|
-
}
|
883
|
+
return this.fetchRequest.url.toString();
|
884
|
+
}
|
885
|
+
set action(value) {
|
886
|
+
this.fetchRequest.url = expandURL(value);
|
721
887
|
}
|
722
888
|
get body() {
|
723
|
-
|
724
|
-
return new URLSearchParams(this.stringFormData);
|
725
|
-
} else {
|
726
|
-
return this.formData;
|
727
|
-
}
|
889
|
+
return this.fetchRequest.body;
|
728
890
|
}
|
729
891
|
get enctype() {
|
730
|
-
|
731
|
-
return formEnctypeFromString(((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formenctype")) || this.formElement.enctype);
|
892
|
+
return this.fetchRequest.enctype;
|
732
893
|
}
|
733
894
|
get isSafe() {
|
734
895
|
return this.fetchRequest.isSafe;
|
735
896
|
}
|
736
|
-
get
|
737
|
-
return
|
897
|
+
get location() {
|
898
|
+
return this.fetchRequest.url;
|
738
899
|
}
|
739
900
|
async start() {
|
740
901
|
const {initialized: initialized, requesting: requesting} = FormSubmissionState;
|
@@ -770,10 +931,10 @@ class FormSubmission {
|
|
770
931
|
}
|
771
932
|
}
|
772
933
|
requestStarted(_request) {
|
773
|
-
var _a;
|
774
934
|
this.state = FormSubmissionState.waiting;
|
775
|
-
|
935
|
+
this.submitter?.setAttribute("disabled", "");
|
776
936
|
this.setSubmitsWith();
|
937
|
+
markAsBusy(this.formElement);
|
777
938
|
dispatch("turbo:submit-start", {
|
778
939
|
target: this.formElement,
|
779
940
|
detail: {
|
@@ -783,6 +944,7 @@ class FormSubmission {
|
|
783
944
|
this.delegate.formSubmissionStarted(this);
|
784
945
|
}
|
785
946
|
requestPreventedHandlingResponse(request, response) {
|
947
|
+
prefetchCache.clear();
|
786
948
|
this.result = {
|
787
949
|
success: response.succeeded,
|
788
950
|
fetchResponse: response
|
@@ -791,7 +953,10 @@ class FormSubmission {
|
|
791
953
|
requestSucceededWithResponse(request, response) {
|
792
954
|
if (response.clientError || response.serverError) {
|
793
955
|
this.delegate.formSubmissionFailedWithResponse(this, response);
|
794
|
-
|
956
|
+
return;
|
957
|
+
}
|
958
|
+
prefetchCache.clear();
|
959
|
+
if (this.requestMustRedirect(request) && responseSucceededWithoutRedirect(response)) {
|
795
960
|
const error = new Error("Form responses must redirect to another location");
|
796
961
|
this.delegate.formSubmissionErrored(this, error);
|
797
962
|
} else {
|
@@ -818,15 +983,16 @@ class FormSubmission {
|
|
818
983
|
this.delegate.formSubmissionErrored(this, error);
|
819
984
|
}
|
820
985
|
requestFinished(_request) {
|
821
|
-
var _a;
|
822
986
|
this.state = FormSubmissionState.stopped;
|
823
|
-
|
987
|
+
this.submitter?.removeAttribute("disabled");
|
824
988
|
this.resetSubmitterText();
|
989
|
+
clearBusyState(this.formElement);
|
825
990
|
dispatch("turbo:submit-end", {
|
826
991
|
target: this.formElement,
|
827
|
-
detail:
|
828
|
-
formSubmission: this
|
829
|
-
|
992
|
+
detail: {
|
993
|
+
formSubmission: this,
|
994
|
+
...this.result
|
995
|
+
}
|
830
996
|
});
|
831
997
|
this.delegate.formSubmissionFinished(this);
|
832
998
|
}
|
@@ -857,15 +1023,14 @@ class FormSubmission {
|
|
857
1023
|
return !request.isSafe || hasAttribute("data-turbo-stream", this.submitter, this.formElement);
|
858
1024
|
}
|
859
1025
|
get submitsWith() {
|
860
|
-
|
861
|
-
return (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("data-turbo-submits-with");
|
1026
|
+
return this.submitter?.getAttribute("data-turbo-submits-with");
|
862
1027
|
}
|
863
1028
|
}
|
864
1029
|
|
865
1030
|
function buildFormData(formElement, submitter) {
|
866
1031
|
const formData = new FormData(formElement);
|
867
|
-
const name = submitter
|
868
|
-
const value = submitter
|
1032
|
+
const name = submitter?.getAttribute("name");
|
1033
|
+
const value = submitter?.getAttribute("value");
|
869
1034
|
if (name) {
|
870
1035
|
formData.append(name, value || "");
|
871
1036
|
}
|
@@ -887,14 +1052,30 @@ function responseSucceededWithoutRedirect(response) {
|
|
887
1052
|
return response.statusCode == 200 && !response.redirected;
|
888
1053
|
}
|
889
1054
|
|
890
|
-
function
|
891
|
-
const
|
892
|
-
|
893
|
-
|
894
|
-
|
1055
|
+
function getFormAction(formElement, submitter) {
|
1056
|
+
const formElementAction = typeof formElement.action === "string" ? formElement.action : null;
|
1057
|
+
if (submitter?.hasAttribute("formaction")) {
|
1058
|
+
return submitter.getAttribute("formaction") || "";
|
1059
|
+
} else {
|
1060
|
+
return formElement.getAttribute("action") || formElementAction || "";
|
895
1061
|
}
|
896
|
-
|
897
|
-
|
1062
|
+
}
|
1063
|
+
|
1064
|
+
function getAction(formAction, fetchMethod) {
|
1065
|
+
const action = expandURL(formAction);
|
1066
|
+
if (isSafe(fetchMethod)) {
|
1067
|
+
action.search = "";
|
1068
|
+
}
|
1069
|
+
return action;
|
1070
|
+
}
|
1071
|
+
|
1072
|
+
function getMethod(formElement, submitter) {
|
1073
|
+
const method = submitter?.getAttribute("formmethod") || formElement.getAttribute("method") || "";
|
1074
|
+
return fetchMethodFromString(method.toLowerCase()) || FetchMethod.get;
|
1075
|
+
}
|
1076
|
+
|
1077
|
+
function getEnctype(formElement, submitter) {
|
1078
|
+
return fetchEnctypeFromString(submitter?.getAttribute("formenctype") || formElement.enctype);
|
898
1079
|
}
|
899
1080
|
|
900
1081
|
class Snapshot {
|
@@ -917,11 +1098,7 @@ class Snapshot {
|
|
917
1098
|
return this.element.isConnected;
|
918
1099
|
}
|
919
1100
|
get firstAutofocusableElement() {
|
920
|
-
|
921
|
-
for (const element of this.element.querySelectorAll("[autofocus]")) {
|
922
|
-
if (element.closest(inertDisabledOrHidden) == null) return element; else continue;
|
923
|
-
}
|
924
|
-
return null;
|
1101
|
+
return queryAutofocusableElement(this.element);
|
925
1102
|
}
|
926
1103
|
get permanentElements() {
|
927
1104
|
return queryPermanentElementsAll(this.element);
|
@@ -951,23 +1128,8 @@ function queryPermanentElementsAll(node) {
|
|
951
1128
|
}
|
952
1129
|
|
953
1130
|
class FormSubmitObserver {
|
1131
|
+
started=false;
|
954
1132
|
constructor(delegate, eventTarget) {
|
955
|
-
this.started = false;
|
956
|
-
this.submitCaptured = () => {
|
957
|
-
this.eventTarget.removeEventListener("submit", this.submitBubbled, false);
|
958
|
-
this.eventTarget.addEventListener("submit", this.submitBubbled, false);
|
959
|
-
};
|
960
|
-
this.submitBubbled = event => {
|
961
|
-
if (!event.defaultPrevented) {
|
962
|
-
const form = event.target instanceof HTMLFormElement ? event.target : undefined;
|
963
|
-
const submitter = event.submitter || undefined;
|
964
|
-
if (form && submissionDoesNotDismissDialog(form, submitter) && submissionDoesNotTargetIFrame(form, submitter) && this.delegate.willSubmitForm(form, submitter)) {
|
965
|
-
event.preventDefault();
|
966
|
-
event.stopImmediatePropagation();
|
967
|
-
this.delegate.formSubmitted(form, submitter);
|
968
|
-
}
|
969
|
-
}
|
970
|
-
};
|
971
1133
|
this.delegate = delegate;
|
972
1134
|
this.eventTarget = eventTarget;
|
973
1135
|
}
|
@@ -983,16 +1145,31 @@ class FormSubmitObserver {
|
|
983
1145
|
this.started = false;
|
984
1146
|
}
|
985
1147
|
}
|
1148
|
+
submitCaptured=() => {
|
1149
|
+
this.eventTarget.removeEventListener("submit", this.submitBubbled, false);
|
1150
|
+
this.eventTarget.addEventListener("submit", this.submitBubbled, false);
|
1151
|
+
};
|
1152
|
+
submitBubbled=event => {
|
1153
|
+
if (!event.defaultPrevented) {
|
1154
|
+
const form = event.target instanceof HTMLFormElement ? event.target : undefined;
|
1155
|
+
const submitter = event.submitter || undefined;
|
1156
|
+
if (form && submissionDoesNotDismissDialog(form, submitter) && submissionDoesNotTargetIFrame(form, submitter) && this.delegate.willSubmitForm(form, submitter)) {
|
1157
|
+
event.preventDefault();
|
1158
|
+
event.stopImmediatePropagation();
|
1159
|
+
this.delegate.formSubmitted(form, submitter);
|
1160
|
+
}
|
1161
|
+
}
|
1162
|
+
};
|
986
1163
|
}
|
987
1164
|
|
988
1165
|
function submissionDoesNotDismissDialog(form, submitter) {
|
989
|
-
const method =
|
1166
|
+
const method = submitter?.getAttribute("formmethod") || form.getAttribute("method");
|
990
1167
|
return method != "dialog";
|
991
1168
|
}
|
992
1169
|
|
993
1170
|
function submissionDoesNotTargetIFrame(form, submitter) {
|
994
|
-
if (
|
995
|
-
const target =
|
1171
|
+
if (submitter?.hasAttribute("formtarget") || form.hasAttribute("target")) {
|
1172
|
+
const target = submitter?.getAttribute("formtarget") || form.target;
|
996
1173
|
for (const element of document.getElementsByName(target)) {
|
997
1174
|
if (element instanceof HTMLIFrameElement) return false;
|
998
1175
|
}
|
@@ -1003,9 +1180,9 @@ function submissionDoesNotTargetIFrame(form, submitter) {
|
|
1003
1180
|
}
|
1004
1181
|
|
1005
1182
|
class View {
|
1183
|
+
#resolveRenderPromise=_value => {};
|
1184
|
+
#resolveInterceptionPromise=_value => {};
|
1006
1185
|
constructor(delegate, element) {
|
1007
|
-
this.resolveRenderPromise = _value => {};
|
1008
|
-
this.resolveInterceptionPromise = _value => {};
|
1009
1186
|
this.delegate = delegate;
|
1010
1187
|
this.element = element;
|
1011
1188
|
}
|
@@ -1051,29 +1228,31 @@ class View {
|
|
1051
1228
|
return window;
|
1052
1229
|
}
|
1053
1230
|
async render(renderer) {
|
1054
|
-
const {isPreview: isPreview, shouldRender: shouldRender, newSnapshot: snapshot} = renderer;
|
1231
|
+
const {isPreview: isPreview, shouldRender: shouldRender, willRender: willRender, newSnapshot: snapshot} = renderer;
|
1232
|
+
const shouldInvalidate = willRender;
|
1055
1233
|
if (shouldRender) {
|
1056
1234
|
try {
|
1057
|
-
this.renderPromise = new Promise((resolve => this
|
1235
|
+
this.renderPromise = new Promise((resolve => this.#resolveRenderPromise = resolve));
|
1058
1236
|
this.renderer = renderer;
|
1059
1237
|
await this.prepareToRenderSnapshot(renderer);
|
1060
|
-
const renderInterception = new Promise((resolve => this
|
1238
|
+
const renderInterception = new Promise((resolve => this.#resolveInterceptionPromise = resolve));
|
1061
1239
|
const options = {
|
1062
|
-
resume: this
|
1063
|
-
render: this.renderer.renderElement
|
1240
|
+
resume: this.#resolveInterceptionPromise,
|
1241
|
+
render: this.renderer.renderElement,
|
1242
|
+
renderMethod: this.renderer.renderMethod
|
1064
1243
|
};
|
1065
1244
|
const immediateRender = this.delegate.allowsImmediateRender(snapshot, options);
|
1066
1245
|
if (!immediateRender) await renderInterception;
|
1067
1246
|
await this.renderSnapshot(renderer);
|
1068
|
-
this.delegate.viewRenderedSnapshot(snapshot, isPreview);
|
1247
|
+
this.delegate.viewRenderedSnapshot(snapshot, isPreview, this.renderer.renderMethod);
|
1069
1248
|
this.delegate.preloadOnLoadLinksForView(this.element);
|
1070
1249
|
this.finishRenderingSnapshot(renderer);
|
1071
1250
|
} finally {
|
1072
1251
|
delete this.renderer;
|
1073
|
-
this
|
1252
|
+
this.#resolveRenderPromise(undefined);
|
1074
1253
|
delete this.renderPromise;
|
1075
1254
|
}
|
1076
|
-
} else {
|
1255
|
+
} else if (shouldInvalidate) {
|
1077
1256
|
this.invalidate(renderer.reloadReason);
|
1078
1257
|
}
|
1079
1258
|
}
|
@@ -1091,6 +1270,12 @@ class View {
|
|
1091
1270
|
this.element.removeAttribute("data-turbo-preview");
|
1092
1271
|
}
|
1093
1272
|
}
|
1273
|
+
markVisitDirection(direction) {
|
1274
|
+
this.element.setAttribute("data-turbo-visit-direction", direction);
|
1275
|
+
}
|
1276
|
+
unmarkVisitDirection() {
|
1277
|
+
this.element.removeAttribute("data-turbo-visit-direction");
|
1278
|
+
}
|
1094
1279
|
async renderSnapshot(renderer) {
|
1095
1280
|
await renderer.render();
|
1096
1281
|
}
|
@@ -1110,26 +1295,6 @@ class FrameView extends View {
|
|
1110
1295
|
|
1111
1296
|
class LinkInterceptor {
|
1112
1297
|
constructor(delegate, element) {
|
1113
|
-
this.clickBubbled = event => {
|
1114
|
-
if (this.respondsToEventTarget(event.target)) {
|
1115
|
-
this.clickEvent = event;
|
1116
|
-
} else {
|
1117
|
-
delete this.clickEvent;
|
1118
|
-
}
|
1119
|
-
};
|
1120
|
-
this.linkClicked = event => {
|
1121
|
-
if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) {
|
1122
|
-
if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url, event.detail.originalEvent)) {
|
1123
|
-
this.clickEvent.preventDefault();
|
1124
|
-
event.preventDefault();
|
1125
|
-
this.delegate.linkClickIntercepted(event.target, event.detail.url, event.detail.originalEvent);
|
1126
|
-
}
|
1127
|
-
}
|
1128
|
-
delete this.clickEvent;
|
1129
|
-
};
|
1130
|
-
this.willVisit = _event => {
|
1131
|
-
delete this.clickEvent;
|
1132
|
-
};
|
1133
1298
|
this.delegate = delegate;
|
1134
1299
|
this.element = element;
|
1135
1300
|
}
|
@@ -1143,6 +1308,26 @@ class LinkInterceptor {
|
|
1143
1308
|
document.removeEventListener("turbo:click", this.linkClicked);
|
1144
1309
|
document.removeEventListener("turbo:before-visit", this.willVisit);
|
1145
1310
|
}
|
1311
|
+
clickBubbled=event => {
|
1312
|
+
if (this.respondsToEventTarget(event.target)) {
|
1313
|
+
this.clickEvent = event;
|
1314
|
+
} else {
|
1315
|
+
delete this.clickEvent;
|
1316
|
+
}
|
1317
|
+
};
|
1318
|
+
linkClicked=event => {
|
1319
|
+
if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) {
|
1320
|
+
if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url, event.detail.originalEvent)) {
|
1321
|
+
this.clickEvent.preventDefault();
|
1322
|
+
event.preventDefault();
|
1323
|
+
this.delegate.linkClickIntercepted(event.target, event.detail.url, event.detail.originalEvent);
|
1324
|
+
}
|
1325
|
+
}
|
1326
|
+
delete this.clickEvent;
|
1327
|
+
};
|
1328
|
+
willVisit=_event => {
|
1329
|
+
delete this.clickEvent;
|
1330
|
+
};
|
1146
1331
|
respondsToEventTarget(target) {
|
1147
1332
|
const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
|
1148
1333
|
return element && element.closest("turbo-frame, html") == this.element;
|
@@ -1150,25 +1335,8 @@ class LinkInterceptor {
|
|
1150
1335
|
}
|
1151
1336
|
|
1152
1337
|
class LinkClickObserver {
|
1338
|
+
started=false;
|
1153
1339
|
constructor(delegate, eventTarget) {
|
1154
|
-
this.started = false;
|
1155
|
-
this.clickCaptured = () => {
|
1156
|
-
this.eventTarget.removeEventListener("click", this.clickBubbled, false);
|
1157
|
-
this.eventTarget.addEventListener("click", this.clickBubbled, false);
|
1158
|
-
};
|
1159
|
-
this.clickBubbled = event => {
|
1160
|
-
if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {
|
1161
|
-
const target = event.composedPath && event.composedPath()[0] || event.target;
|
1162
|
-
const link = this.findLinkFromClickTarget(target);
|
1163
|
-
if (link && doesNotTargetIFrame(link)) {
|
1164
|
-
const location = this.getLocationForLink(link);
|
1165
|
-
if (this.delegate.willFollowLinkToLocation(link, location, event)) {
|
1166
|
-
event.preventDefault();
|
1167
|
-
this.delegate.followedLinkToLocation(link, location);
|
1168
|
-
}
|
1169
|
-
}
|
1170
|
-
}
|
1171
|
-
};
|
1172
1340
|
this.delegate = delegate;
|
1173
1341
|
this.eventTarget = eventTarget;
|
1174
1342
|
}
|
@@ -1184,26 +1352,26 @@ class LinkClickObserver {
|
|
1184
1352
|
this.started = false;
|
1185
1353
|
}
|
1186
1354
|
}
|
1355
|
+
clickCaptured=() => {
|
1356
|
+
this.eventTarget.removeEventListener("click", this.clickBubbled, false);
|
1357
|
+
this.eventTarget.addEventListener("click", this.clickBubbled, false);
|
1358
|
+
};
|
1359
|
+
clickBubbled=event => {
|
1360
|
+
if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {
|
1361
|
+
const target = event.composedPath && event.composedPath()[0] || event.target;
|
1362
|
+
const link = findLinkFromClickTarget(target);
|
1363
|
+
if (link && doesNotTargetIFrame(link)) {
|
1364
|
+
const location = getLocationForLink(link);
|
1365
|
+
if (this.delegate.willFollowLinkToLocation(link, location, event)) {
|
1366
|
+
event.preventDefault();
|
1367
|
+
this.delegate.followedLinkToLocation(link, location);
|
1368
|
+
}
|
1369
|
+
}
|
1370
|
+
}
|
1371
|
+
};
|
1187
1372
|
clickEventIsSignificant(event) {
|
1188
1373
|
return !(event.target && event.target.isContentEditable || event.defaultPrevented || event.which > 1 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey);
|
1189
1374
|
}
|
1190
|
-
findLinkFromClickTarget(target) {
|
1191
|
-
return findClosestRecursively(target, "a[href]:not([target^=_]):not([download])");
|
1192
|
-
}
|
1193
|
-
getLocationForLink(link) {
|
1194
|
-
return expandURL(link.getAttribute("href") || "");
|
1195
|
-
}
|
1196
|
-
}
|
1197
|
-
|
1198
|
-
function doesNotTargetIFrame(anchor) {
|
1199
|
-
if (anchor.hasAttribute("target")) {
|
1200
|
-
for (const element of document.getElementsByName(anchor.target)) {
|
1201
|
-
if (element instanceof HTMLIFrameElement) return false;
|
1202
|
-
}
|
1203
|
-
return true;
|
1204
|
-
} else {
|
1205
|
-
return true;
|
1206
|
-
}
|
1207
1375
|
}
|
1208
1376
|
|
1209
1377
|
class FormLinkClickObserver {
|
@@ -1217,8 +1385,14 @@ class FormLinkClickObserver {
|
|
1217
1385
|
stop() {
|
1218
1386
|
this.linkInterceptor.stop();
|
1219
1387
|
}
|
1388
|
+
canPrefetchRequestToLocation(link, location) {
|
1389
|
+
return false;
|
1390
|
+
}
|
1391
|
+
prefetchAndCacheRequestToLocation(link, location) {
|
1392
|
+
return;
|
1393
|
+
}
|
1220
1394
|
willFollowLinkToLocation(link, location, originalEvent) {
|
1221
|
-
return this.delegate.willSubmitFormLinkToLocation(link, location, originalEvent) && link.hasAttribute("data-turbo-method");
|
1395
|
+
return this.delegate.willSubmitFormLinkToLocation(link, location, originalEvent) && (link.hasAttribute("data-turbo-method") || link.hasAttribute("data-turbo-stream"));
|
1222
1396
|
}
|
1223
1397
|
followedLinkToLocation(link, location) {
|
1224
1398
|
const form = document.createElement("form");
|
@@ -1291,7 +1465,7 @@ class Bardo {
|
|
1291
1465
|
}
|
1292
1466
|
replacePlaceholderWithPermanentElement(permanentElement) {
|
1293
1467
|
const placeholder = this.getPlaceholderById(permanentElement.id);
|
1294
|
-
placeholder
|
1468
|
+
placeholder?.replaceWith(permanentElement);
|
1295
1469
|
}
|
1296
1470
|
getPlaceholderById(id) {
|
1297
1471
|
return this.placeholders.find((element => element.content == id));
|
@@ -1309,8 +1483,8 @@ function createPlaceholderForPermanentElement(permanentElement) {
|
|
1309
1483
|
}
|
1310
1484
|
|
1311
1485
|
class Renderer {
|
1486
|
+
#activeElement=null;
|
1312
1487
|
constructor(currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) {
|
1313
|
-
this.activeElement = null;
|
1314
1488
|
this.currentSnapshot = currentSnapshot;
|
1315
1489
|
this.newSnapshot = newSnapshot;
|
1316
1490
|
this.isPreview = isPreview;
|
@@ -1330,6 +1504,7 @@ class Renderer {
|
|
1330
1504
|
prepareToRender() {
|
1331
1505
|
return;
|
1332
1506
|
}
|
1507
|
+
render() {}
|
1333
1508
|
finishRendering() {
|
1334
1509
|
if (this.resolvingFunctions) {
|
1335
1510
|
this.resolvingFunctions.resolve();
|
@@ -1341,20 +1516,20 @@ class Renderer {
|
|
1341
1516
|
}
|
1342
1517
|
focusFirstAutofocusableElement() {
|
1343
1518
|
const element = this.connectedSnapshot.firstAutofocusableElement;
|
1344
|
-
if (
|
1519
|
+
if (element) {
|
1345
1520
|
element.focus();
|
1346
1521
|
}
|
1347
1522
|
}
|
1348
1523
|
enteringBardo(currentPermanentElement) {
|
1349
|
-
if (this
|
1524
|
+
if (this.#activeElement) return;
|
1350
1525
|
if (currentPermanentElement.contains(this.currentSnapshot.activeElement)) {
|
1351
|
-
this
|
1526
|
+
this.#activeElement = this.currentSnapshot.activeElement;
|
1352
1527
|
}
|
1353
1528
|
}
|
1354
1529
|
leavingBardo(currentPermanentElement) {
|
1355
|
-
if (currentPermanentElement.contains(this
|
1356
|
-
this
|
1357
|
-
this
|
1530
|
+
if (currentPermanentElement.contains(this.#activeElement) && this.#activeElement instanceof HTMLElement) {
|
1531
|
+
this.#activeElement.focus();
|
1532
|
+
this.#activeElement = null;
|
1358
1533
|
}
|
1359
1534
|
}
|
1360
1535
|
get connectedSnapshot() {
|
@@ -1369,20 +1544,18 @@ class Renderer {
|
|
1369
1544
|
get permanentElementMap() {
|
1370
1545
|
return this.currentSnapshot.getPermanentElementMapForSnapshot(this.newSnapshot);
|
1371
1546
|
}
|
1372
|
-
|
1373
|
-
|
1374
|
-
|
1375
|
-
return element && typeof element.focus == "function";
|
1547
|
+
get renderMethod() {
|
1548
|
+
return "replace";
|
1549
|
+
}
|
1376
1550
|
}
|
1377
1551
|
|
1378
1552
|
class FrameRenderer extends Renderer {
|
1379
1553
|
static renderElement(currentElement, newElement) {
|
1380
|
-
var _a;
|
1381
1554
|
const destinationRange = document.createRange();
|
1382
1555
|
destinationRange.selectNodeContents(currentElement);
|
1383
1556
|
destinationRange.deleteContents();
|
1384
1557
|
const frameElement = newElement;
|
1385
|
-
const sourceRange =
|
1558
|
+
const sourceRange = frameElement.ownerDocument?.createRange();
|
1386
1559
|
if (sourceRange) {
|
1387
1560
|
sourceRange.selectNodeContents(frameElement);
|
1388
1561
|
currentElement.appendChild(sourceRange.extractContents());
|
@@ -1396,14 +1569,14 @@ class FrameRenderer extends Renderer {
|
|
1396
1569
|
return true;
|
1397
1570
|
}
|
1398
1571
|
async render() {
|
1399
|
-
await
|
1572
|
+
await nextRepaint();
|
1400
1573
|
this.preservingPermanentElements((() => {
|
1401
1574
|
this.loadFrameElement();
|
1402
1575
|
}));
|
1403
1576
|
this.scrollFrameIntoView();
|
1404
|
-
await
|
1577
|
+
await nextRepaint();
|
1405
1578
|
this.focusFirstAutofocusableElement();
|
1406
|
-
await
|
1579
|
+
await nextRepaint();
|
1407
1580
|
this.activateScriptElements();
|
1408
1581
|
}
|
1409
1582
|
loadFrameElement() {
|
@@ -1453,6 +1626,7 @@ function readScrollBehavior(value, defaultValue) {
|
|
1453
1626
|
}
|
1454
1627
|
|
1455
1628
|
class ProgressBar {
|
1629
|
+
static animationDuration=300;
|
1456
1630
|
static get defaultCSS() {
|
1457
1631
|
return unindent`
|
1458
1632
|
.turbo-progress-bar {
|
@@ -1470,13 +1644,10 @@ class ProgressBar {
|
|
1470
1644
|
}
|
1471
1645
|
`;
|
1472
1646
|
}
|
1647
|
+
hiding=false;
|
1648
|
+
value=0;
|
1649
|
+
visible=false;
|
1473
1650
|
constructor() {
|
1474
|
-
this.hiding = false;
|
1475
|
-
this.value = 0;
|
1476
|
-
this.visible = false;
|
1477
|
-
this.trickle = () => {
|
1478
|
-
this.setValue(this.value + Math.random() / 100);
|
1479
|
-
};
|
1480
1651
|
this.stylesheetElement = this.createStylesheetElement();
|
1481
1652
|
this.progressElement = this.createProgressElement();
|
1482
1653
|
this.installStylesheetElement();
|
@@ -1531,6 +1702,9 @@ class ProgressBar {
|
|
1531
1702
|
window.clearInterval(this.trickleInterval);
|
1532
1703
|
delete this.trickleInterval;
|
1533
1704
|
}
|
1705
|
+
trickle=() => {
|
1706
|
+
this.setValue(this.value + Math.random() / 100);
|
1707
|
+
};
|
1534
1708
|
refresh() {
|
1535
1709
|
requestAnimationFrame((() => {
|
1536
1710
|
this.progressElement.style.width = `${10 + this.value * 90}%`;
|
@@ -1555,25 +1729,22 @@ class ProgressBar {
|
|
1555
1729
|
}
|
1556
1730
|
}
|
1557
1731
|
|
1558
|
-
ProgressBar.animationDuration = 300;
|
1559
|
-
|
1560
1732
|
class HeadSnapshot extends Snapshot {
|
1561
|
-
|
1562
|
-
|
1563
|
-
|
1564
|
-
|
1565
|
-
|
1566
|
-
|
1567
|
-
|
1568
|
-
|
1569
|
-
|
1570
|
-
|
1571
|
-
|
1572
|
-
|
1573
|
-
|
1574
|
-
|
1575
|
-
|
1576
|
-
}
|
1733
|
+
detailsByOuterHTML=this.children.filter((element => !elementIsNoscript(element))).map((element => elementWithoutNonce(element))).reduce(((result, element) => {
|
1734
|
+
const {outerHTML: outerHTML} = element;
|
1735
|
+
const details = outerHTML in result ? result[outerHTML] : {
|
1736
|
+
type: elementType(element),
|
1737
|
+
tracked: elementIsTracked(element),
|
1738
|
+
elements: []
|
1739
|
+
};
|
1740
|
+
return {
|
1741
|
+
...result,
|
1742
|
+
[outerHTML]: {
|
1743
|
+
...details,
|
1744
|
+
elements: [ ...details.elements, element ]
|
1745
|
+
}
|
1746
|
+
};
|
1747
|
+
}), {});
|
1577
1748
|
get trackedElementSignature() {
|
1578
1749
|
return Object.keys(this.detailsByOuterHTML).filter((outerHTML => this.detailsByOuterHTML[outerHTML].tracked)).join("");
|
1579
1750
|
}
|
@@ -1606,7 +1777,7 @@ class HeadSnapshot extends Snapshot {
|
|
1606
1777
|
return Object.keys(this.detailsByOuterHTML).reduce(((result, outerHTML) => {
|
1607
1778
|
const {elements: [element]} = this.detailsByOuterHTML[outerHTML];
|
1608
1779
|
return elementIsMetaElementWithName(element, name) ? element : result;
|
1609
|
-
}), undefined);
|
1780
|
+
}), undefined | undefined);
|
1610
1781
|
}
|
1611
1782
|
}
|
1612
1783
|
|
@@ -1656,11 +1827,12 @@ class PageSnapshot extends Snapshot {
|
|
1656
1827
|
static fromElement(element) {
|
1657
1828
|
return this.fromDocument(element.ownerDocument);
|
1658
1829
|
}
|
1659
|
-
static fromDocument({
|
1660
|
-
return new this(body, new HeadSnapshot(head));
|
1830
|
+
static fromDocument({documentElement: documentElement, body: body, head: head}) {
|
1831
|
+
return new this(documentElement, body, new HeadSnapshot(head));
|
1661
1832
|
}
|
1662
|
-
constructor(
|
1663
|
-
super(
|
1833
|
+
constructor(documentElement, body, headSnapshot) {
|
1834
|
+
super(body);
|
1835
|
+
this.documentElement = documentElement;
|
1664
1836
|
this.headSnapshot = headSnapshot;
|
1665
1837
|
}
|
1666
1838
|
clone() {
|
@@ -1675,14 +1847,16 @@ class PageSnapshot extends Snapshot {
|
|
1675
1847
|
for (const clonedPasswordInput of clonedElement.querySelectorAll('input[type="password"]')) {
|
1676
1848
|
clonedPasswordInput.value = "";
|
1677
1849
|
}
|
1678
|
-
return new PageSnapshot(clonedElement, this.headSnapshot);
|
1850
|
+
return new PageSnapshot(this.documentElement, clonedElement, this.headSnapshot);
|
1851
|
+
}
|
1852
|
+
get lang() {
|
1853
|
+
return this.documentElement.getAttribute("lang");
|
1679
1854
|
}
|
1680
1855
|
get headElement() {
|
1681
1856
|
return this.headSnapshot.element;
|
1682
1857
|
}
|
1683
1858
|
get rootLocation() {
|
1684
|
-
|
1685
|
-
const root = (_a = this.getSetting("root")) !== null && _a !== void 0 ? _a : "/";
|
1859
|
+
const root = this.getSetting("root") ?? "/";
|
1686
1860
|
return expandURL(root);
|
1687
1861
|
}
|
1688
1862
|
get cacheControlValue() {
|
@@ -1697,29 +1871,38 @@ class PageSnapshot extends Snapshot {
|
|
1697
1871
|
get isVisitable() {
|
1698
1872
|
return this.getSetting("visit-control") != "reload";
|
1699
1873
|
}
|
1874
|
+
get prefersViewTransitions() {
|
1875
|
+
return this.headSnapshot.getMetaValue("view-transition") === "same-origin";
|
1876
|
+
}
|
1877
|
+
get shouldMorphPage() {
|
1878
|
+
return this.getSetting("refresh-method") === "morph";
|
1879
|
+
}
|
1880
|
+
get shouldPreserveScrollPosition() {
|
1881
|
+
return this.getSetting("refresh-scroll") === "preserve";
|
1882
|
+
}
|
1700
1883
|
getSetting(name) {
|
1701
1884
|
return this.headSnapshot.getMetaValue(`turbo-${name}`);
|
1702
1885
|
}
|
1703
1886
|
}
|
1704
1887
|
|
1705
|
-
|
1706
|
-
|
1707
|
-
(
|
1708
|
-
|
1709
|
-
|
1710
|
-
|
1711
|
-
|
1712
|
-
|
1713
|
-
|
1714
|
-
|
1715
|
-
|
1716
|
-
|
1717
|
-
|
1718
|
-
|
1719
|
-
|
1720
|
-
|
1721
|
-
|
1722
|
-
}
|
1888
|
+
class ViewTransitioner {
|
1889
|
+
#viewTransitionStarted=false;
|
1890
|
+
#lastOperation=Promise.resolve();
|
1891
|
+
renderChange(useViewTransition, render) {
|
1892
|
+
if (useViewTransition && this.viewTransitionsAvailable && !this.#viewTransitionStarted) {
|
1893
|
+
this.#viewTransitionStarted = true;
|
1894
|
+
this.#lastOperation = this.#lastOperation.then((async () => {
|
1895
|
+
await document.startViewTransition(render).finished;
|
1896
|
+
}));
|
1897
|
+
} else {
|
1898
|
+
this.#lastOperation = this.#lastOperation.then(render);
|
1899
|
+
}
|
1900
|
+
return this.#lastOperation;
|
1901
|
+
}
|
1902
|
+
get viewTransitionsAvailable() {
|
1903
|
+
return document.startViewTransition;
|
1904
|
+
}
|
1905
|
+
}
|
1723
1906
|
|
1724
1907
|
const defaultOptions = {
|
1725
1908
|
action: "advance",
|
@@ -1731,29 +1914,52 @@ const defaultOptions = {
|
|
1731
1914
|
acceptsStreamResponse: false
|
1732
1915
|
};
|
1733
1916
|
|
1734
|
-
|
1917
|
+
const TimingMetric = {
|
1918
|
+
visitStart: "visitStart",
|
1919
|
+
requestStart: "requestStart",
|
1920
|
+
requestEnd: "requestEnd",
|
1921
|
+
visitEnd: "visitEnd"
|
1922
|
+
};
|
1923
|
+
|
1924
|
+
const VisitState = {
|
1925
|
+
initialized: "initialized",
|
1926
|
+
started: "started",
|
1927
|
+
canceled: "canceled",
|
1928
|
+
failed: "failed",
|
1929
|
+
completed: "completed"
|
1930
|
+
};
|
1931
|
+
|
1932
|
+
const SystemStatusCode = {
|
1933
|
+
networkFailure: 0,
|
1934
|
+
timeoutFailure: -1,
|
1935
|
+
contentTypeMismatch: -2
|
1936
|
+
};
|
1735
1937
|
|
1736
|
-
|
1737
|
-
|
1738
|
-
|
1739
|
-
|
1740
|
-
}
|
1938
|
+
const Direction = {
|
1939
|
+
advance: "forward",
|
1940
|
+
restore: "back",
|
1941
|
+
replace: "none"
|
1942
|
+
};
|
1741
1943
|
|
1742
1944
|
class Visit {
|
1945
|
+
identifier=uuid();
|
1946
|
+
timingMetrics={};
|
1947
|
+
followedRedirect=false;
|
1948
|
+
historyChanged=false;
|
1949
|
+
scrolled=false;
|
1950
|
+
shouldCacheSnapshot=true;
|
1951
|
+
acceptsStreamResponse=false;
|
1952
|
+
snapshotCached=false;
|
1953
|
+
state=VisitState.initialized;
|
1954
|
+
viewTransitioner=new ViewTransitioner;
|
1743
1955
|
constructor(delegate, location, restorationIdentifier, options = {}) {
|
1744
|
-
this.identifier = uuid();
|
1745
|
-
this.timingMetrics = {};
|
1746
|
-
this.followedRedirect = false;
|
1747
|
-
this.historyChanged = false;
|
1748
|
-
this.scrolled = false;
|
1749
|
-
this.shouldCacheSnapshot = true;
|
1750
|
-
this.acceptsStreamResponse = false;
|
1751
|
-
this.snapshotCached = false;
|
1752
|
-
this.state = VisitState.initialized;
|
1753
1956
|
this.delegate = delegate;
|
1754
1957
|
this.location = location;
|
1755
1958
|
this.restorationIdentifier = restorationIdentifier || uuid();
|
1756
|
-
const {action: action, historyChanged: historyChanged, referrer: referrer, snapshot: snapshot, snapshotHTML: snapshotHTML, response: response, visitCachedSnapshot: visitCachedSnapshot, willRender: willRender, updateHistory: updateHistory, shouldCacheSnapshot: shouldCacheSnapshot, acceptsStreamResponse: acceptsStreamResponse} =
|
1959
|
+
const {action: action, historyChanged: historyChanged, referrer: referrer, snapshot: snapshot, snapshotHTML: snapshotHTML, response: response, visitCachedSnapshot: visitCachedSnapshot, willRender: willRender, updateHistory: updateHistory, shouldCacheSnapshot: shouldCacheSnapshot, acceptsStreamResponse: acceptsStreamResponse, direction: direction} = {
|
1960
|
+
...defaultOptions,
|
1961
|
+
...options
|
1962
|
+
};
|
1757
1963
|
this.action = action;
|
1758
1964
|
this.historyChanged = historyChanged;
|
1759
1965
|
this.referrer = referrer;
|
@@ -1761,12 +1967,14 @@ class Visit {
|
|
1761
1967
|
this.snapshotHTML = snapshotHTML;
|
1762
1968
|
this.response = response;
|
1763
1969
|
this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
|
1970
|
+
this.isPageRefresh = this.view.isPageRefresh(this);
|
1764
1971
|
this.visitCachedSnapshot = visitCachedSnapshot;
|
1765
1972
|
this.willRender = willRender;
|
1766
1973
|
this.updateHistory = updateHistory;
|
1767
1974
|
this.scrolled = !willRender;
|
1768
1975
|
this.shouldCacheSnapshot = shouldCacheSnapshot;
|
1769
1976
|
this.acceptsStreamResponse = acceptsStreamResponse;
|
1977
|
+
this.direction = direction || Direction[action];
|
1770
1978
|
}
|
1771
1979
|
get adapter() {
|
1772
1980
|
return this.delegate.adapter;
|
@@ -1815,12 +2023,12 @@ class Visit {
|
|
1815
2023
|
if (this.state == VisitState.started) {
|
1816
2024
|
this.state = VisitState.failed;
|
1817
2025
|
this.adapter.visitFailed(this);
|
2026
|
+
this.delegate.visitCompleted(this);
|
1818
2027
|
}
|
1819
2028
|
}
|
1820
2029
|
changeHistory() {
|
1821
|
-
var _a;
|
1822
2030
|
if (!this.historyChanged && this.updateHistory) {
|
1823
|
-
const actionForHistory = this.location.href ===
|
2031
|
+
const actionForHistory = this.location.href === this.referrer?.href ? "replace" : this.action;
|
1824
2032
|
const method = getHistoryMethodForAction(actionForHistory);
|
1825
2033
|
this.history.update(method, this.location, this.restorationIdentifier);
|
1826
2034
|
this.historyChanged = true;
|
@@ -1867,8 +2075,8 @@ class Visit {
|
|
1867
2075
|
if (this.shouldCacheSnapshot) this.cacheSnapshot();
|
1868
2076
|
if (this.view.renderPromise) await this.view.renderPromise;
|
1869
2077
|
if (isSuccessful(statusCode) && responseHTML != null) {
|
1870
|
-
|
1871
|
-
this.
|
2078
|
+
const snapshot = PageSnapshot.fromHTMLString(responseHTML);
|
2079
|
+
await this.renderPageSnapshot(snapshot, false);
|
1872
2080
|
this.adapter.visitRendered(this);
|
1873
2081
|
this.complete();
|
1874
2082
|
} else {
|
@@ -1901,12 +2109,11 @@ class Visit {
|
|
1901
2109
|
const isPreview = this.shouldIssueRequest();
|
1902
2110
|
this.render((async () => {
|
1903
2111
|
this.cacheSnapshot();
|
1904
|
-
if (this.isSamePage) {
|
2112
|
+
if (this.isSamePage || this.isPageRefresh) {
|
1905
2113
|
this.adapter.visitRendered(this);
|
1906
2114
|
} else {
|
1907
2115
|
if (this.view.renderPromise) await this.view.renderPromise;
|
1908
|
-
await this.
|
1909
|
-
this.performScroll();
|
2116
|
+
await this.renderPageSnapshot(snapshot, isPreview);
|
1910
2117
|
this.adapter.visitRendered(this);
|
1911
2118
|
if (!isPreview) {
|
1912
2119
|
this.complete();
|
@@ -1916,8 +2123,7 @@ class Visit {
|
|
1916
2123
|
}
|
1917
2124
|
}
|
1918
2125
|
followRedirect() {
|
1919
|
-
|
1920
|
-
if (this.redirectedToLocation && !this.followedRedirect && ((_a = this.response) === null || _a === void 0 ? void 0 : _a.redirected)) {
|
2126
|
+
if (this.redirectedToLocation && !this.followedRedirect && this.response?.redirected) {
|
1921
2127
|
this.adapter.visitProposedToLocation(this.redirectedToLocation, {
|
1922
2128
|
action: "replace",
|
1923
2129
|
response: this.response,
|
@@ -1989,7 +2195,7 @@ class Visit {
|
|
1989
2195
|
this.finishRequest();
|
1990
2196
|
}
|
1991
2197
|
performScroll() {
|
1992
|
-
if (!this.scrolled && !this.view.forceReloaded) {
|
2198
|
+
if (!this.scrolled && !this.view.forceReloaded && !this.view.shouldPreserveScrollPosition(this)) {
|
1993
2199
|
if (this.action == "restore") {
|
1994
2200
|
this.scrollToRestoredPosition() || this.scrollToAnchor() || this.view.scrollToTop();
|
1995
2201
|
} else {
|
@@ -2019,7 +2225,9 @@ class Visit {
|
|
2019
2225
|
this.timingMetrics[metric] = (new Date).getTime();
|
2020
2226
|
}
|
2021
2227
|
getTimingMetrics() {
|
2022
|
-
return
|
2228
|
+
return {
|
2229
|
+
...this.timingMetrics
|
2230
|
+
};
|
2023
2231
|
}
|
2024
2232
|
getHistoryMethodForAction(action) {
|
2025
2233
|
switch (action) {
|
@@ -2051,12 +2259,16 @@ class Visit {
|
|
2051
2259
|
}
|
2052
2260
|
async render(callback) {
|
2053
2261
|
this.cancelRender();
|
2054
|
-
await
|
2055
|
-
this.frame = requestAnimationFrame((() => resolve()));
|
2056
|
-
}));
|
2262
|
+
this.frame = await nextRepaint();
|
2057
2263
|
await callback();
|
2058
2264
|
delete this.frame;
|
2059
2265
|
}
|
2266
|
+
async renderPageSnapshot(snapshot, isPreview) {
|
2267
|
+
await this.viewTransitioner.renderChange(this.view.shouldTransitionTo(snapshot), (async () => {
|
2268
|
+
await this.view.renderPage(snapshot, isPreview, this.willRender, this);
|
2269
|
+
this.performScroll();
|
2270
|
+
}));
|
2271
|
+
}
|
2060
2272
|
cancelRender() {
|
2061
2273
|
if (this.frame) {
|
2062
2274
|
cancelAnimationFrame(this.frame);
|
@@ -2070,15 +2282,16 @@ function isSuccessful(statusCode) {
|
|
2070
2282
|
}
|
2071
2283
|
|
2072
2284
|
class BrowserAdapter {
|
2285
|
+
progressBar=new ProgressBar;
|
2073
2286
|
constructor(session) {
|
2074
|
-
this.progressBar = new ProgressBar;
|
2075
|
-
this.showProgressBar = () => {
|
2076
|
-
this.progressBar.show();
|
2077
|
-
};
|
2078
2287
|
this.session = session;
|
2079
2288
|
}
|
2080
2289
|
visitProposedToLocation(location, options) {
|
2081
|
-
|
2290
|
+
if (locationIsVisitable(location, this.navigator.rootLocation)) {
|
2291
|
+
this.navigator.startVisit(location, options?.restorationIdentifier || uuid(), options);
|
2292
|
+
} else {
|
2293
|
+
window.location.href = location.toString();
|
2294
|
+
}
|
2082
2295
|
}
|
2083
2296
|
visitStarted(visit) {
|
2084
2297
|
this.location = visit.location;
|
@@ -2113,15 +2326,18 @@ class BrowserAdapter {
|
|
2113
2326
|
return visit.loadResponse();
|
2114
2327
|
}
|
2115
2328
|
}
|
2116
|
-
visitRequestFinished(_visit) {
|
2329
|
+
visitRequestFinished(_visit) {}
|
2330
|
+
visitCompleted(_visit) {
|
2117
2331
|
this.progressBar.setValue(1);
|
2118
2332
|
this.hideVisitProgressBar();
|
2119
2333
|
}
|
2120
|
-
visitCompleted(_visit) {}
|
2121
2334
|
pageInvalidated(reason) {
|
2122
2335
|
this.reload(reason);
|
2123
2336
|
}
|
2124
|
-
visitFailed(_visit) {
|
2337
|
+
visitFailed(_visit) {
|
2338
|
+
this.progressBar.setValue(1);
|
2339
|
+
this.hideVisitProgressBar();
|
2340
|
+
}
|
2125
2341
|
visitRendered(_visit) {}
|
2126
2342
|
formSubmissionStarted(_formSubmission) {
|
2127
2343
|
this.progressBar.setValue(0);
|
@@ -2153,12 +2369,14 @@ class BrowserAdapter {
|
|
2153
2369
|
delete this.formProgressBarTimeout;
|
2154
2370
|
}
|
2155
2371
|
}
|
2372
|
+
showProgressBar=() => {
|
2373
|
+
this.progressBar.show();
|
2374
|
+
};
|
2156
2375
|
reload(reason) {
|
2157
|
-
var _a;
|
2158
2376
|
dispatch("turbo:reload", {
|
2159
2377
|
detail: reason
|
2160
2378
|
});
|
2161
|
-
window.location.href =
|
2379
|
+
window.location.href = this.location?.toString() || window.location.href;
|
2162
2380
|
}
|
2163
2381
|
get navigator() {
|
2164
2382
|
return this.session.navigator;
|
@@ -2166,16 +2384,9 @@ class BrowserAdapter {
|
|
2166
2384
|
}
|
2167
2385
|
|
2168
2386
|
class CacheObserver {
|
2169
|
-
|
2170
|
-
|
2171
|
-
|
2172
|
-
this.started = false;
|
2173
|
-
this.removeTemporaryElements = _event => {
|
2174
|
-
for (const element of this.temporaryElements) {
|
2175
|
-
element.remove();
|
2176
|
-
}
|
2177
|
-
};
|
2178
|
-
}
|
2387
|
+
selector="[data-turbo-temporary]";
|
2388
|
+
deprecatedSelector="[data-turbo-cache=false]";
|
2389
|
+
started=false;
|
2179
2390
|
start() {
|
2180
2391
|
if (!this.started) {
|
2181
2392
|
this.started = true;
|
@@ -2188,6 +2399,11 @@ class CacheObserver {
|
|
2188
2399
|
removeEventListener("turbo:before-cache", this.removeTemporaryElements, false);
|
2189
2400
|
}
|
2190
2401
|
}
|
2402
|
+
removeTemporaryElements=_event => {
|
2403
|
+
for (const element of this.temporaryElements) {
|
2404
|
+
element.remove();
|
2405
|
+
}
|
2406
|
+
};
|
2191
2407
|
get temporaryElements() {
|
2192
2408
|
return [ ...document.querySelectorAll(this.selector), ...this.temporaryElementsWithDeprecation ];
|
2193
2409
|
}
|
@@ -2216,41 +2432,40 @@ class FrameRedirector {
|
|
2216
2432
|
this.formSubmitObserver.stop();
|
2217
2433
|
}
|
2218
2434
|
shouldInterceptLinkClick(element, _location, _event) {
|
2219
|
-
return this
|
2435
|
+
return this.#shouldRedirect(element);
|
2220
2436
|
}
|
2221
2437
|
linkClickIntercepted(element, url, event) {
|
2222
|
-
const frame = this
|
2438
|
+
const frame = this.#findFrameElement(element);
|
2223
2439
|
if (frame) {
|
2224
2440
|
frame.delegate.linkClickIntercepted(element, url, event);
|
2225
2441
|
}
|
2226
2442
|
}
|
2227
2443
|
willSubmitForm(element, submitter) {
|
2228
|
-
return element.closest("turbo-frame") == null && this
|
2444
|
+
return element.closest("turbo-frame") == null && this.#shouldSubmit(element, submitter) && this.#shouldRedirect(element, submitter);
|
2229
2445
|
}
|
2230
2446
|
formSubmitted(element, submitter) {
|
2231
|
-
const frame = this
|
2447
|
+
const frame = this.#findFrameElement(element, submitter);
|
2232
2448
|
if (frame) {
|
2233
2449
|
frame.delegate.formSubmitted(element, submitter);
|
2234
2450
|
}
|
2235
2451
|
}
|
2236
|
-
shouldSubmit(form, submitter) {
|
2237
|
-
|
2238
|
-
const action = getAction(form, submitter);
|
2452
|
+
#shouldSubmit(form, submitter) {
|
2453
|
+
const action = getAction$1(form, submitter);
|
2239
2454
|
const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
|
2240
|
-
const rootLocation = expandURL(
|
2241
|
-
return this
|
2455
|
+
const rootLocation = expandURL(meta?.content ?? "/");
|
2456
|
+
return this.#shouldRedirect(form, submitter) && locationIsVisitable(action, rootLocation);
|
2242
2457
|
}
|
2243
|
-
shouldRedirect(element, submitter) {
|
2458
|
+
#shouldRedirect(element, submitter) {
|
2244
2459
|
const isNavigatable = element instanceof HTMLFormElement ? this.session.submissionIsNavigatable(element, submitter) : this.session.elementIsNavigatable(element);
|
2245
2460
|
if (isNavigatable) {
|
2246
|
-
const frame = this
|
2461
|
+
const frame = this.#findFrameElement(element, submitter);
|
2247
2462
|
return frame ? frame != element.closest("turbo-frame") : false;
|
2248
2463
|
} else {
|
2249
2464
|
return false;
|
2250
2465
|
}
|
2251
2466
|
}
|
2252
|
-
findFrameElement(element, submitter) {
|
2253
|
-
const id =
|
2467
|
+
#findFrameElement(element, submitter) {
|
2468
|
+
const id = submitter?.getAttribute("data-turbo-frame") || element.getAttribute("data-turbo-frame");
|
2254
2469
|
if (id && id != "_top") {
|
2255
2470
|
const frame = this.element.querySelector(`#${id}:not([disabled])`);
|
2256
2471
|
if (frame instanceof FrameElement) {
|
@@ -2261,32 +2476,20 @@ class FrameRedirector {
|
|
2261
2476
|
}
|
2262
2477
|
|
2263
2478
|
class History {
|
2479
|
+
location;
|
2480
|
+
restorationIdentifier=uuid();
|
2481
|
+
restorationData={};
|
2482
|
+
started=false;
|
2483
|
+
pageLoaded=false;
|
2484
|
+
currentIndex=0;
|
2264
2485
|
constructor(delegate) {
|
2265
|
-
this.restorationIdentifier = uuid();
|
2266
|
-
this.restorationData = {};
|
2267
|
-
this.started = false;
|
2268
|
-
this.pageLoaded = false;
|
2269
|
-
this.onPopState = event => {
|
2270
|
-
if (this.shouldHandlePopState()) {
|
2271
|
-
const {turbo: turbo} = event.state || {};
|
2272
|
-
if (turbo) {
|
2273
|
-
this.location = new URL(window.location.href);
|
2274
|
-
const {restorationIdentifier: restorationIdentifier} = turbo;
|
2275
|
-
this.restorationIdentifier = restorationIdentifier;
|
2276
|
-
this.delegate.historyPoppedToLocationWithRestorationIdentifier(this.location, restorationIdentifier);
|
2277
|
-
}
|
2278
|
-
}
|
2279
|
-
};
|
2280
|
-
this.onPageLoad = async _event => {
|
2281
|
-
await nextMicrotask();
|
2282
|
-
this.pageLoaded = true;
|
2283
|
-
};
|
2284
2486
|
this.delegate = delegate;
|
2285
2487
|
}
|
2286
2488
|
start() {
|
2287
2489
|
if (!this.started) {
|
2288
2490
|
addEventListener("popstate", this.onPopState, false);
|
2289
2491
|
addEventListener("load", this.onPageLoad, false);
|
2492
|
+
this.currentIndex = history.state?.turbo?.restorationIndex || 0;
|
2290
2493
|
this.started = true;
|
2291
2494
|
this.replace(new URL(window.location.href));
|
2292
2495
|
}
|
@@ -2305,9 +2508,11 @@ class History {
|
|
2305
2508
|
this.update(history.replaceState, location, restorationIdentifier);
|
2306
2509
|
}
|
2307
2510
|
update(method, location, restorationIdentifier = uuid()) {
|
2511
|
+
if (method === history.pushState) ++this.currentIndex;
|
2308
2512
|
const state = {
|
2309
2513
|
turbo: {
|
2310
|
-
restorationIdentifier: restorationIdentifier
|
2514
|
+
restorationIdentifier: restorationIdentifier,
|
2515
|
+
restorationIndex: this.currentIndex
|
2311
2516
|
}
|
2312
2517
|
};
|
2313
2518
|
method.call(history, state, "", location.href);
|
@@ -2320,12 +2525,14 @@ class History {
|
|
2320
2525
|
updateRestorationData(additionalData) {
|
2321
2526
|
const {restorationIdentifier: restorationIdentifier} = this;
|
2322
2527
|
const restorationData = this.restorationData[restorationIdentifier];
|
2323
|
-
this.restorationData[restorationIdentifier] =
|
2528
|
+
this.restorationData[restorationIdentifier] = {
|
2529
|
+
...restorationData,
|
2530
|
+
...additionalData
|
2531
|
+
};
|
2324
2532
|
}
|
2325
2533
|
assumeControlOfScrollRestoration() {
|
2326
|
-
var _a;
|
2327
2534
|
if (!this.previousScrollRestoration) {
|
2328
|
-
this.previousScrollRestoration =
|
2535
|
+
this.previousScrollRestoration = history.scrollRestoration ?? "auto";
|
2329
2536
|
history.scrollRestoration = "manual";
|
2330
2537
|
}
|
2331
2538
|
}
|
@@ -2335,6 +2542,23 @@ class History {
|
|
2335
2542
|
delete this.previousScrollRestoration;
|
2336
2543
|
}
|
2337
2544
|
}
|
2545
|
+
onPopState=event => {
|
2546
|
+
if (this.shouldHandlePopState()) {
|
2547
|
+
const {turbo: turbo} = event.state || {};
|
2548
|
+
if (turbo) {
|
2549
|
+
this.location = new URL(window.location.href);
|
2550
|
+
const {restorationIdentifier: restorationIdentifier, restorationIndex: restorationIndex} = turbo;
|
2551
|
+
this.restorationIdentifier = restorationIdentifier;
|
2552
|
+
const direction = restorationIndex > this.currentIndex ? "forward" : "back";
|
2553
|
+
this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(this.location, restorationIdentifier, direction);
|
2554
|
+
this.currentIndex = restorationIndex;
|
2555
|
+
}
|
2556
|
+
}
|
2557
|
+
};
|
2558
|
+
onPageLoad=async _event => {
|
2559
|
+
await nextMicrotask();
|
2560
|
+
this.pageLoaded = true;
|
2561
|
+
};
|
2338
2562
|
shouldHandlePopState() {
|
2339
2563
|
return this.pageIsLoaded();
|
2340
2564
|
}
|
@@ -2343,24 +2567,152 @@ class History {
|
|
2343
2567
|
}
|
2344
2568
|
}
|
2345
2569
|
|
2570
|
+
class LinkPrefetchObserver {
|
2571
|
+
started=false;
|
2572
|
+
#prefetchedLink=null;
|
2573
|
+
constructor(delegate, eventTarget) {
|
2574
|
+
this.delegate = delegate;
|
2575
|
+
this.eventTarget = eventTarget;
|
2576
|
+
}
|
2577
|
+
start() {
|
2578
|
+
if (this.started) return;
|
2579
|
+
if (this.eventTarget.readyState === "loading") {
|
2580
|
+
this.eventTarget.addEventListener("DOMContentLoaded", this.#enable, {
|
2581
|
+
once: true
|
2582
|
+
});
|
2583
|
+
} else {
|
2584
|
+
this.#enable();
|
2585
|
+
}
|
2586
|
+
}
|
2587
|
+
stop() {
|
2588
|
+
if (!this.started) return;
|
2589
|
+
this.eventTarget.removeEventListener("mouseenter", this.#tryToPrefetchRequest, {
|
2590
|
+
capture: true,
|
2591
|
+
passive: true
|
2592
|
+
});
|
2593
|
+
this.eventTarget.removeEventListener("mouseleave", this.#cancelRequestIfObsolete, {
|
2594
|
+
capture: true,
|
2595
|
+
passive: true
|
2596
|
+
});
|
2597
|
+
this.eventTarget.removeEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true);
|
2598
|
+
this.started = false;
|
2599
|
+
}
|
2600
|
+
#enable=() => {
|
2601
|
+
this.eventTarget.addEventListener("mouseenter", this.#tryToPrefetchRequest, {
|
2602
|
+
capture: true,
|
2603
|
+
passive: true
|
2604
|
+
});
|
2605
|
+
this.eventTarget.addEventListener("mouseleave", this.#cancelRequestIfObsolete, {
|
2606
|
+
capture: true,
|
2607
|
+
passive: true
|
2608
|
+
});
|
2609
|
+
this.eventTarget.addEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true);
|
2610
|
+
this.started = true;
|
2611
|
+
};
|
2612
|
+
#tryToPrefetchRequest=event => {
|
2613
|
+
if (getMetaContent("turbo-prefetch") === "false") return;
|
2614
|
+
const target = event.target;
|
2615
|
+
const isLink = target.matches && target.matches("a[href]:not([target^=_]):not([download])");
|
2616
|
+
if (isLink && this.#isPrefetchable(target)) {
|
2617
|
+
const link = target;
|
2618
|
+
const location = getLocationForLink(link);
|
2619
|
+
if (this.delegate.canPrefetchRequestToLocation(link, location)) {
|
2620
|
+
this.#prefetchedLink = link;
|
2621
|
+
const fetchRequest = new FetchRequest(this, FetchMethod.get, location, new URLSearchParams, target);
|
2622
|
+
prefetchCache.setLater(location.toString(), fetchRequest, this.#cacheTtl);
|
2623
|
+
}
|
2624
|
+
}
|
2625
|
+
};
|
2626
|
+
#cancelRequestIfObsolete=event => {
|
2627
|
+
if (event.target === this.#prefetchedLink) this.#cancelPrefetchRequest();
|
2628
|
+
};
|
2629
|
+
#cancelPrefetchRequest=() => {
|
2630
|
+
prefetchCache.clear();
|
2631
|
+
this.#prefetchedLink = null;
|
2632
|
+
};
|
2633
|
+
#tryToUsePrefetchedRequest=event => {
|
2634
|
+
if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "get") {
|
2635
|
+
const cached = prefetchCache.get(event.detail.url.toString());
|
2636
|
+
if (cached) {
|
2637
|
+
event.detail.fetchRequest = cached;
|
2638
|
+
}
|
2639
|
+
prefetchCache.clear();
|
2640
|
+
}
|
2641
|
+
};
|
2642
|
+
prepareRequest(request) {
|
2643
|
+
const link = request.target;
|
2644
|
+
request.headers["X-Sec-Purpose"] = "prefetch";
|
2645
|
+
const turboFrame = link.closest("turbo-frame");
|
2646
|
+
const turboFrameTarget = link.getAttribute("data-turbo-frame") || turboFrame?.getAttribute("target") || turboFrame?.id;
|
2647
|
+
if (turboFrameTarget && turboFrameTarget !== "_top") {
|
2648
|
+
request.headers["Turbo-Frame"] = turboFrameTarget;
|
2649
|
+
}
|
2650
|
+
if (link.hasAttribute("data-turbo-stream")) {
|
2651
|
+
request.acceptResponseType(StreamMessage.contentType);
|
2652
|
+
}
|
2653
|
+
}
|
2654
|
+
requestSucceededWithResponse() {}
|
2655
|
+
requestStarted(fetchRequest) {}
|
2656
|
+
requestErrored(fetchRequest) {}
|
2657
|
+
requestFinished(fetchRequest) {}
|
2658
|
+
requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}
|
2659
|
+
requestFailedWithResponse(fetchRequest, fetchResponse) {}
|
2660
|
+
get #cacheTtl() {
|
2661
|
+
return Number(getMetaContent("turbo-prefetch-cache-time")) || cacheTtl;
|
2662
|
+
}
|
2663
|
+
#isPrefetchable(link) {
|
2664
|
+
const href = link.getAttribute("href");
|
2665
|
+
if (!href || href.startsWith("#") || link.getAttribute("data-turbo") === "false" || link.getAttribute("data-turbo-prefetch") === "false") {
|
2666
|
+
return false;
|
2667
|
+
}
|
2668
|
+
const event = dispatch("turbo:before-prefetch", {
|
2669
|
+
target: link,
|
2670
|
+
cancelable: true
|
2671
|
+
});
|
2672
|
+
if (event.defaultPrevented) {
|
2673
|
+
return false;
|
2674
|
+
}
|
2675
|
+
if (link.origin !== document.location.origin) {
|
2676
|
+
return false;
|
2677
|
+
}
|
2678
|
+
if (![ "http:", "https:" ].includes(link.protocol)) {
|
2679
|
+
return false;
|
2680
|
+
}
|
2681
|
+
if (link.pathname + link.search === document.location.pathname + document.location.search) {
|
2682
|
+
return false;
|
2683
|
+
}
|
2684
|
+
const turboMethod = link.getAttribute("data-turbo-method");
|
2685
|
+
if (turboMethod && turboMethod !== "get") {
|
2686
|
+
return false;
|
2687
|
+
}
|
2688
|
+
if (targetsIframe(link)) {
|
2689
|
+
return false;
|
2690
|
+
}
|
2691
|
+
const turboPrefetchParent = findClosestRecursively(link, "[data-turbo-prefetch]");
|
2692
|
+
if (turboPrefetchParent && turboPrefetchParent.getAttribute("data-turbo-prefetch") === "false") {
|
2693
|
+
return false;
|
2694
|
+
}
|
2695
|
+
return true;
|
2696
|
+
}
|
2697
|
+
}
|
2698
|
+
|
2699
|
+
const targetsIframe = link => !doesNotTargetIFrame(link);
|
2700
|
+
|
2346
2701
|
class Navigator {
|
2347
2702
|
constructor(delegate) {
|
2348
2703
|
this.delegate = delegate;
|
2349
2704
|
}
|
2350
2705
|
proposeVisit(location, options = {}) {
|
2351
2706
|
if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
|
2352
|
-
|
2353
|
-
this.delegate.visitProposedToLocation(location, options);
|
2354
|
-
} else {
|
2355
|
-
window.location.href = location.toString();
|
2356
|
-
}
|
2707
|
+
this.delegate.visitProposedToLocation(location, options);
|
2357
2708
|
}
|
2358
2709
|
}
|
2359
2710
|
startVisit(locatable, restorationIdentifier, options = {}) {
|
2360
2711
|
this.stop();
|
2361
|
-
this.currentVisit = new Visit(this, expandURL(locatable), restorationIdentifier,
|
2362
|
-
referrer: this.location
|
2363
|
-
|
2712
|
+
this.currentVisit = new Visit(this, expandURL(locatable), restorationIdentifier, {
|
2713
|
+
referrer: this.location,
|
2714
|
+
...options
|
2715
|
+
});
|
2364
2716
|
this.currentVisit.start();
|
2365
2717
|
}
|
2366
2718
|
submitForm(form, submitter) {
|
@@ -2384,6 +2736,9 @@ class Navigator {
|
|
2384
2736
|
get view() {
|
2385
2737
|
return this.delegate.view;
|
2386
2738
|
}
|
2739
|
+
get rootLocation() {
|
2740
|
+
return this.view.snapshot.rootLocation;
|
2741
|
+
}
|
2387
2742
|
get history() {
|
2388
2743
|
return this.delegate.history;
|
2389
2744
|
}
|
@@ -2401,7 +2756,7 @@ class Navigator {
|
|
2401
2756
|
this.view.clearSnapshotCache();
|
2402
2757
|
}
|
2403
2758
|
const {statusCode: statusCode, redirected: redirected} = fetchResponse;
|
2404
|
-
const action = this
|
2759
|
+
const action = this.#getActionForFormSubmission(formSubmission, fetchResponse);
|
2405
2760
|
const visitOptions = {
|
2406
2761
|
action: action,
|
2407
2762
|
shouldCacheSnapshot: shouldCacheSnapshot,
|
@@ -2424,7 +2779,9 @@ class Navigator {
|
|
2424
2779
|
} else {
|
2425
2780
|
await this.view.renderPage(snapshot, false, true, this.currentVisit);
|
2426
2781
|
}
|
2427
|
-
|
2782
|
+
if (!snapshot.shouldPreserveScrollPosition) {
|
2783
|
+
this.view.scrollToTop();
|
2784
|
+
}
|
2428
2785
|
this.view.clearSnapshotCache();
|
2429
2786
|
}
|
2430
2787
|
}
|
@@ -2457,35 +2814,27 @@ class Navigator {
|
|
2457
2814
|
get restorationIdentifier() {
|
2458
2815
|
return this.history.restorationIdentifier;
|
2459
2816
|
}
|
2460
|
-
getActionForFormSubmission(
|
2461
|
-
|
2817
|
+
#getActionForFormSubmission(formSubmission, fetchResponse) {
|
2818
|
+
const {submitter: submitter, formElement: formElement} = formSubmission;
|
2819
|
+
return getVisitAction(submitter, formElement) || this.#getDefaultAction(fetchResponse);
|
2820
|
+
}
|
2821
|
+
#getDefaultAction(fetchResponse) {
|
2822
|
+
const sameLocationRedirect = fetchResponse.redirected && fetchResponse.location.href === this.location?.href;
|
2823
|
+
return sameLocationRedirect ? "replace" : "advance";
|
2462
2824
|
}
|
2463
2825
|
}
|
2464
2826
|
|
2465
|
-
|
2466
|
-
|
2467
|
-
|
2468
|
-
|
2469
|
-
|
2470
|
-
|
2471
|
-
PageStage[PageStage["complete"] = 3] = "complete";
|
2472
|
-
})(PageStage || (PageStage = {}));
|
2827
|
+
const PageStage = {
|
2828
|
+
initial: 0,
|
2829
|
+
loading: 1,
|
2830
|
+
interactive: 2,
|
2831
|
+
complete: 3
|
2832
|
+
};
|
2473
2833
|
|
2474
2834
|
class PageObserver {
|
2835
|
+
stage=PageStage.initial;
|
2836
|
+
started=false;
|
2475
2837
|
constructor(delegate) {
|
2476
|
-
this.stage = PageStage.initial;
|
2477
|
-
this.started = false;
|
2478
|
-
this.interpretReadyState = () => {
|
2479
|
-
const {readyState: readyState} = this;
|
2480
|
-
if (readyState == "interactive") {
|
2481
|
-
this.pageIsInteractive();
|
2482
|
-
} else if (readyState == "complete") {
|
2483
|
-
this.pageIsComplete();
|
2484
|
-
}
|
2485
|
-
};
|
2486
|
-
this.pageWillUnload = () => {
|
2487
|
-
this.delegate.pageWillUnload();
|
2488
|
-
};
|
2489
2838
|
this.delegate = delegate;
|
2490
2839
|
}
|
2491
2840
|
start() {
|
@@ -2505,6 +2854,14 @@ class PageObserver {
|
|
2505
2854
|
this.started = false;
|
2506
2855
|
}
|
2507
2856
|
}
|
2857
|
+
interpretReadyState=() => {
|
2858
|
+
const {readyState: readyState} = this;
|
2859
|
+
if (readyState == "interactive") {
|
2860
|
+
this.pageIsInteractive();
|
2861
|
+
} else if (readyState == "complete") {
|
2862
|
+
this.pageIsComplete();
|
2863
|
+
}
|
2864
|
+
};
|
2508
2865
|
pageIsInteractive() {
|
2509
2866
|
if (this.stage == PageStage.loading) {
|
2510
2867
|
this.stage = PageStage.interactive;
|
@@ -2518,20 +2875,17 @@ class PageObserver {
|
|
2518
2875
|
this.delegate.pageLoaded();
|
2519
2876
|
}
|
2520
2877
|
}
|
2878
|
+
pageWillUnload=() => {
|
2879
|
+
this.delegate.pageWillUnload();
|
2880
|
+
};
|
2521
2881
|
get readyState() {
|
2522
2882
|
return document.readyState;
|
2523
2883
|
}
|
2524
2884
|
}
|
2525
2885
|
|
2526
2886
|
class ScrollObserver {
|
2887
|
+
started=false;
|
2527
2888
|
constructor(delegate) {
|
2528
|
-
this.started = false;
|
2529
|
-
this.onScroll = () => {
|
2530
|
-
this.updatePosition({
|
2531
|
-
x: window.pageXOffset,
|
2532
|
-
y: window.pageYOffset
|
2533
|
-
});
|
2534
|
-
};
|
2535
2889
|
this.delegate = delegate;
|
2536
2890
|
}
|
2537
2891
|
start() {
|
@@ -2547,6 +2901,12 @@ class ScrollObserver {
|
|
2547
2901
|
this.started = false;
|
2548
2902
|
}
|
2549
2903
|
}
|
2904
|
+
onScroll=() => {
|
2905
|
+
this.updatePosition({
|
2906
|
+
x: window.pageXOffset,
|
2907
|
+
y: window.pageYOffset
|
2908
|
+
});
|
2909
|
+
};
|
2550
2910
|
updatePosition(position) {
|
2551
2911
|
this.delegate.scrollPositionChanged(position);
|
2552
2912
|
}
|
@@ -2554,7 +2914,13 @@ class ScrollObserver {
|
|
2554
2914
|
|
2555
2915
|
class StreamMessageRenderer {
|
2556
2916
|
render({fragment: fragment}) {
|
2557
|
-
Bardo.preservingPermanentElements(this, getPermanentElementMapForFragment(fragment), (() =>
|
2917
|
+
Bardo.preservingPermanentElements(this, getPermanentElementMapForFragment(fragment), (() => {
|
2918
|
+
withAutofocusFromFragment(fragment, (() => {
|
2919
|
+
withPreservedFocus((() => {
|
2920
|
+
document.documentElement.appendChild(fragment);
|
2921
|
+
}));
|
2922
|
+
}));
|
2923
|
+
}));
|
2558
2924
|
}
|
2559
2925
|
enteringBardo(currentPermanentElement, newPermanentElement) {
|
2560
2926
|
newPermanentElement.replaceWith(currentPermanentElement.cloneNode(true));
|
@@ -2577,33 +2943,67 @@ function getPermanentElementMapForFragment(fragment) {
|
|
2577
2943
|
return permanentElementMap;
|
2578
2944
|
}
|
2579
2945
|
|
2946
|
+
async function withAutofocusFromFragment(fragment, callback) {
|
2947
|
+
const generatedID = `turbo-stream-autofocus-${uuid()}`;
|
2948
|
+
const turboStreams = fragment.querySelectorAll("turbo-stream");
|
2949
|
+
const elementWithAutofocus = firstAutofocusableElementInStreams(turboStreams);
|
2950
|
+
let willAutofocusId = null;
|
2951
|
+
if (elementWithAutofocus) {
|
2952
|
+
if (elementWithAutofocus.id) {
|
2953
|
+
willAutofocusId = elementWithAutofocus.id;
|
2954
|
+
} else {
|
2955
|
+
willAutofocusId = generatedID;
|
2956
|
+
}
|
2957
|
+
elementWithAutofocus.id = willAutofocusId;
|
2958
|
+
}
|
2959
|
+
callback();
|
2960
|
+
await nextRepaint();
|
2961
|
+
const hasNoActiveElement = document.activeElement == null || document.activeElement == document.body;
|
2962
|
+
if (hasNoActiveElement && willAutofocusId) {
|
2963
|
+
const elementToAutofocus = document.getElementById(willAutofocusId);
|
2964
|
+
if (elementIsFocusable(elementToAutofocus)) {
|
2965
|
+
elementToAutofocus.focus();
|
2966
|
+
}
|
2967
|
+
if (elementToAutofocus && elementToAutofocus.id == generatedID) {
|
2968
|
+
elementToAutofocus.removeAttribute("id");
|
2969
|
+
}
|
2970
|
+
}
|
2971
|
+
}
|
2972
|
+
|
2973
|
+
async function withPreservedFocus(callback) {
|
2974
|
+
const [activeElementBeforeRender, activeElementAfterRender] = await around(callback, (() => document.activeElement));
|
2975
|
+
const restoreFocusTo = activeElementBeforeRender && activeElementBeforeRender.id;
|
2976
|
+
if (restoreFocusTo) {
|
2977
|
+
const elementToFocus = document.getElementById(restoreFocusTo);
|
2978
|
+
if (elementIsFocusable(elementToFocus) && elementToFocus != activeElementAfterRender) {
|
2979
|
+
elementToFocus.focus();
|
2980
|
+
}
|
2981
|
+
}
|
2982
|
+
}
|
2983
|
+
|
2984
|
+
function firstAutofocusableElementInStreams(nodeListOfStreamElements) {
|
2985
|
+
for (const streamElement of nodeListOfStreamElements) {
|
2986
|
+
const elementWithAutofocus = queryAutofocusableElement(streamElement.templateElement.content);
|
2987
|
+
if (elementWithAutofocus) return elementWithAutofocus;
|
2988
|
+
}
|
2989
|
+
return null;
|
2990
|
+
}
|
2991
|
+
|
2580
2992
|
class StreamObserver {
|
2993
|
+
sources=new Set;
|
2994
|
+
#started=false;
|
2581
2995
|
constructor(delegate) {
|
2582
|
-
this.sources = new Set;
|
2583
|
-
this.started = false;
|
2584
|
-
this.inspectFetchResponse = event => {
|
2585
|
-
const response = fetchResponseFromEvent(event);
|
2586
|
-
if (response && fetchResponseIsStream(response)) {
|
2587
|
-
event.preventDefault();
|
2588
|
-
this.receiveMessageResponse(response);
|
2589
|
-
}
|
2590
|
-
};
|
2591
|
-
this.receiveMessageEvent = event => {
|
2592
|
-
if (this.started && typeof event.data == "string") {
|
2593
|
-
this.receiveMessageHTML(event.data);
|
2594
|
-
}
|
2595
|
-
};
|
2596
2996
|
this.delegate = delegate;
|
2597
2997
|
}
|
2598
2998
|
start() {
|
2599
|
-
if (!this
|
2600
|
-
this
|
2999
|
+
if (!this.#started) {
|
3000
|
+
this.#started = true;
|
2601
3001
|
addEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
|
2602
3002
|
}
|
2603
3003
|
}
|
2604
3004
|
stop() {
|
2605
|
-
if (this
|
2606
|
-
this
|
3005
|
+
if (this.#started) {
|
3006
|
+
this.#started = false;
|
2607
3007
|
removeEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
|
2608
3008
|
}
|
2609
3009
|
}
|
@@ -2622,6 +3022,18 @@ class StreamObserver {
|
|
2622
3022
|
streamSourceIsConnected(source) {
|
2623
3023
|
return this.sources.has(source);
|
2624
3024
|
}
|
3025
|
+
inspectFetchResponse=event => {
|
3026
|
+
const response = fetchResponseFromEvent(event);
|
3027
|
+
if (response && fetchResponseIsStream(response)) {
|
3028
|
+
event.preventDefault();
|
3029
|
+
this.receiveMessageResponse(response);
|
3030
|
+
}
|
3031
|
+
};
|
3032
|
+
receiveMessageEvent=event => {
|
3033
|
+
if (this.#started && typeof event.data == "string") {
|
3034
|
+
this.receiveMessageHTML(event.data);
|
3035
|
+
}
|
3036
|
+
};
|
2625
3037
|
async receiveMessageResponse(response) {
|
2626
3038
|
const html = await response.responseHTML;
|
2627
3039
|
if (html) {
|
@@ -2634,16 +3046,14 @@ class StreamObserver {
|
|
2634
3046
|
}
|
2635
3047
|
|
2636
3048
|
function fetchResponseFromEvent(event) {
|
2637
|
-
|
2638
|
-
const fetchResponse = (_a = event.detail) === null || _a === void 0 ? void 0 : _a.fetchResponse;
|
3049
|
+
const fetchResponse = event.detail?.fetchResponse;
|
2639
3050
|
if (fetchResponse instanceof FetchResponse) {
|
2640
3051
|
return fetchResponse;
|
2641
3052
|
}
|
2642
3053
|
}
|
2643
3054
|
|
2644
3055
|
function fetchResponseIsStream(response) {
|
2645
|
-
|
2646
|
-
const contentType = (_a = response.contentType) !== null && _a !== void 0 ? _a : "";
|
3056
|
+
const contentType = response.contentType ?? "";
|
2647
3057
|
return contentType.startsWith(StreamMessage.contentType);
|
2648
3058
|
}
|
2649
3059
|
|
@@ -2678,6 +3088,546 @@ class ErrorRenderer extends Renderer {
|
|
2678
3088
|
}
|
2679
3089
|
}
|
2680
3090
|
|
3091
|
+
var Idiomorph = function() {
|
3092
|
+
let EMPTY_SET = new Set;
|
3093
|
+
let defaults = {
|
3094
|
+
morphStyle: "outerHTML",
|
3095
|
+
callbacks: {
|
3096
|
+
beforeNodeAdded: noOp,
|
3097
|
+
afterNodeAdded: noOp,
|
3098
|
+
beforeNodeMorphed: noOp,
|
3099
|
+
afterNodeMorphed: noOp,
|
3100
|
+
beforeNodeRemoved: noOp,
|
3101
|
+
afterNodeRemoved: noOp,
|
3102
|
+
beforeAttributeUpdated: noOp
|
3103
|
+
},
|
3104
|
+
head: {
|
3105
|
+
style: "merge",
|
3106
|
+
shouldPreserve: function(elt) {
|
3107
|
+
return elt.getAttribute("im-preserve") === "true";
|
3108
|
+
},
|
3109
|
+
shouldReAppend: function(elt) {
|
3110
|
+
return elt.getAttribute("im-re-append") === "true";
|
3111
|
+
},
|
3112
|
+
shouldRemove: noOp,
|
3113
|
+
afterHeadMorphed: noOp
|
3114
|
+
}
|
3115
|
+
};
|
3116
|
+
function morph(oldNode, newContent, config = {}) {
|
3117
|
+
if (oldNode instanceof Document) {
|
3118
|
+
oldNode = oldNode.documentElement;
|
3119
|
+
}
|
3120
|
+
if (typeof newContent === "string") {
|
3121
|
+
newContent = parseContent(newContent);
|
3122
|
+
}
|
3123
|
+
let normalizedContent = normalizeContent(newContent);
|
3124
|
+
let ctx = createMorphContext(oldNode, normalizedContent, config);
|
3125
|
+
return morphNormalizedContent(oldNode, normalizedContent, ctx);
|
3126
|
+
}
|
3127
|
+
function morphNormalizedContent(oldNode, normalizedNewContent, ctx) {
|
3128
|
+
if (ctx.head.block) {
|
3129
|
+
let oldHead = oldNode.querySelector("head");
|
3130
|
+
let newHead = normalizedNewContent.querySelector("head");
|
3131
|
+
if (oldHead && newHead) {
|
3132
|
+
let promises = handleHeadElement(newHead, oldHead, ctx);
|
3133
|
+
Promise.all(promises).then((function() {
|
3134
|
+
morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, {
|
3135
|
+
head: {
|
3136
|
+
block: false,
|
3137
|
+
ignore: true
|
3138
|
+
}
|
3139
|
+
}));
|
3140
|
+
}));
|
3141
|
+
return;
|
3142
|
+
}
|
3143
|
+
}
|
3144
|
+
if (ctx.morphStyle === "innerHTML") {
|
3145
|
+
morphChildren(normalizedNewContent, oldNode, ctx);
|
3146
|
+
return oldNode.children;
|
3147
|
+
} else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) {
|
3148
|
+
let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx);
|
3149
|
+
let previousSibling = bestMatch?.previousSibling;
|
3150
|
+
let nextSibling = bestMatch?.nextSibling;
|
3151
|
+
let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx);
|
3152
|
+
if (bestMatch) {
|
3153
|
+
return insertSiblings(previousSibling, morphedNode, nextSibling);
|
3154
|
+
} else {
|
3155
|
+
return [];
|
3156
|
+
}
|
3157
|
+
} else {
|
3158
|
+
throw "Do not understand how to morph style " + ctx.morphStyle;
|
3159
|
+
}
|
3160
|
+
}
|
3161
|
+
function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
|
3162
|
+
return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement && possibleActiveElement !== document.body;
|
3163
|
+
}
|
3164
|
+
function morphOldNodeTo(oldNode, newContent, ctx) {
|
3165
|
+
if (ctx.ignoreActive && oldNode === document.activeElement) ; else if (newContent == null) {
|
3166
|
+
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
|
3167
|
+
oldNode.remove();
|
3168
|
+
ctx.callbacks.afterNodeRemoved(oldNode);
|
3169
|
+
return null;
|
3170
|
+
} else if (!isSoftMatch(oldNode, newContent)) {
|
3171
|
+
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
|
3172
|
+
if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode;
|
3173
|
+
oldNode.parentElement.replaceChild(newContent, oldNode);
|
3174
|
+
ctx.callbacks.afterNodeAdded(newContent);
|
3175
|
+
ctx.callbacks.afterNodeRemoved(oldNode);
|
3176
|
+
return newContent;
|
3177
|
+
} else {
|
3178
|
+
if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return oldNode;
|
3179
|
+
if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) ; else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") {
|
3180
|
+
handleHeadElement(newContent, oldNode, ctx);
|
3181
|
+
} else {
|
3182
|
+
syncNodeFrom(newContent, oldNode, ctx);
|
3183
|
+
if (!ignoreValueOfActiveElement(oldNode, ctx)) {
|
3184
|
+
morphChildren(newContent, oldNode, ctx);
|
3185
|
+
}
|
3186
|
+
}
|
3187
|
+
ctx.callbacks.afterNodeMorphed(oldNode, newContent);
|
3188
|
+
return oldNode;
|
3189
|
+
}
|
3190
|
+
}
|
3191
|
+
function morphChildren(newParent, oldParent, ctx) {
|
3192
|
+
let nextNewChild = newParent.firstChild;
|
3193
|
+
let insertionPoint = oldParent.firstChild;
|
3194
|
+
let newChild;
|
3195
|
+
while (nextNewChild) {
|
3196
|
+
newChild = nextNewChild;
|
3197
|
+
nextNewChild = newChild.nextSibling;
|
3198
|
+
if (insertionPoint == null) {
|
3199
|
+
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
3200
|
+
oldParent.appendChild(newChild);
|
3201
|
+
ctx.callbacks.afterNodeAdded(newChild);
|
3202
|
+
removeIdsFromConsideration(ctx, newChild);
|
3203
|
+
continue;
|
3204
|
+
}
|
3205
|
+
if (isIdSetMatch(newChild, insertionPoint, ctx)) {
|
3206
|
+
morphOldNodeTo(insertionPoint, newChild, ctx);
|
3207
|
+
insertionPoint = insertionPoint.nextSibling;
|
3208
|
+
removeIdsFromConsideration(ctx, newChild);
|
3209
|
+
continue;
|
3210
|
+
}
|
3211
|
+
let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx);
|
3212
|
+
if (idSetMatch) {
|
3213
|
+
insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx);
|
3214
|
+
morphOldNodeTo(idSetMatch, newChild, ctx);
|
3215
|
+
removeIdsFromConsideration(ctx, newChild);
|
3216
|
+
continue;
|
3217
|
+
}
|
3218
|
+
let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx);
|
3219
|
+
if (softMatch) {
|
3220
|
+
insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx);
|
3221
|
+
morphOldNodeTo(softMatch, newChild, ctx);
|
3222
|
+
removeIdsFromConsideration(ctx, newChild);
|
3223
|
+
continue;
|
3224
|
+
}
|
3225
|
+
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
3226
|
+
oldParent.insertBefore(newChild, insertionPoint);
|
3227
|
+
ctx.callbacks.afterNodeAdded(newChild);
|
3228
|
+
removeIdsFromConsideration(ctx, newChild);
|
3229
|
+
}
|
3230
|
+
while (insertionPoint !== null) {
|
3231
|
+
let tempNode = insertionPoint;
|
3232
|
+
insertionPoint = insertionPoint.nextSibling;
|
3233
|
+
removeNode(tempNode, ctx);
|
3234
|
+
}
|
3235
|
+
}
|
3236
|
+
function ignoreAttribute(attr, to, updateType, ctx) {
|
3237
|
+
if (attr === "value" && ctx.ignoreActiveValue && to === document.activeElement) {
|
3238
|
+
return true;
|
3239
|
+
}
|
3240
|
+
return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false;
|
3241
|
+
}
|
3242
|
+
function syncNodeFrom(from, to, ctx) {
|
3243
|
+
let type = from.nodeType;
|
3244
|
+
if (type === 1) {
|
3245
|
+
const fromAttributes = from.attributes;
|
3246
|
+
const toAttributes = to.attributes;
|
3247
|
+
for (const fromAttribute of fromAttributes) {
|
3248
|
+
if (ignoreAttribute(fromAttribute.name, to, "update", ctx)) {
|
3249
|
+
continue;
|
3250
|
+
}
|
3251
|
+
if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) {
|
3252
|
+
to.setAttribute(fromAttribute.name, fromAttribute.value);
|
3253
|
+
}
|
3254
|
+
}
|
3255
|
+
for (let i = toAttributes.length - 1; 0 <= i; i--) {
|
3256
|
+
const toAttribute = toAttributes[i];
|
3257
|
+
if (ignoreAttribute(toAttribute.name, to, "remove", ctx)) {
|
3258
|
+
continue;
|
3259
|
+
}
|
3260
|
+
if (!from.hasAttribute(toAttribute.name)) {
|
3261
|
+
to.removeAttribute(toAttribute.name);
|
3262
|
+
}
|
3263
|
+
}
|
3264
|
+
}
|
3265
|
+
if (type === 8 || type === 3) {
|
3266
|
+
if (to.nodeValue !== from.nodeValue) {
|
3267
|
+
to.nodeValue = from.nodeValue;
|
3268
|
+
}
|
3269
|
+
}
|
3270
|
+
if (!ignoreValueOfActiveElement(to, ctx)) {
|
3271
|
+
syncInputValue(from, to, ctx);
|
3272
|
+
}
|
3273
|
+
}
|
3274
|
+
function syncBooleanAttribute(from, to, attributeName, ctx) {
|
3275
|
+
if (from[attributeName] !== to[attributeName]) {
|
3276
|
+
let ignoreUpdate = ignoreAttribute(attributeName, to, "update", ctx);
|
3277
|
+
if (!ignoreUpdate) {
|
3278
|
+
to[attributeName] = from[attributeName];
|
3279
|
+
}
|
3280
|
+
if (from[attributeName]) {
|
3281
|
+
if (!ignoreUpdate) {
|
3282
|
+
to.setAttribute(attributeName, from[attributeName]);
|
3283
|
+
}
|
3284
|
+
} else {
|
3285
|
+
if (!ignoreAttribute(attributeName, to, "remove", ctx)) {
|
3286
|
+
to.removeAttribute(attributeName);
|
3287
|
+
}
|
3288
|
+
}
|
3289
|
+
}
|
3290
|
+
}
|
3291
|
+
function syncInputValue(from, to, ctx) {
|
3292
|
+
if (from instanceof HTMLInputElement && to instanceof HTMLInputElement && from.type !== "file") {
|
3293
|
+
let fromValue = from.value;
|
3294
|
+
let toValue = to.value;
|
3295
|
+
syncBooleanAttribute(from, to, "checked", ctx);
|
3296
|
+
syncBooleanAttribute(from, to, "disabled", ctx);
|
3297
|
+
if (!from.hasAttribute("value")) {
|
3298
|
+
if (!ignoreAttribute("value", to, "remove", ctx)) {
|
3299
|
+
to.value = "";
|
3300
|
+
to.removeAttribute("value");
|
3301
|
+
}
|
3302
|
+
} else if (fromValue !== toValue) {
|
3303
|
+
if (!ignoreAttribute("value", to, "update", ctx)) {
|
3304
|
+
to.setAttribute("value", fromValue);
|
3305
|
+
to.value = fromValue;
|
3306
|
+
}
|
3307
|
+
}
|
3308
|
+
} else if (from instanceof HTMLOptionElement) {
|
3309
|
+
syncBooleanAttribute(from, to, "selected", ctx);
|
3310
|
+
} else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) {
|
3311
|
+
let fromValue = from.value;
|
3312
|
+
let toValue = to.value;
|
3313
|
+
if (ignoreAttribute("value", to, "update", ctx)) {
|
3314
|
+
return;
|
3315
|
+
}
|
3316
|
+
if (fromValue !== toValue) {
|
3317
|
+
to.value = fromValue;
|
3318
|
+
}
|
3319
|
+
if (to.firstChild && to.firstChild.nodeValue !== fromValue) {
|
3320
|
+
to.firstChild.nodeValue = fromValue;
|
3321
|
+
}
|
3322
|
+
}
|
3323
|
+
}
|
3324
|
+
function handleHeadElement(newHeadTag, currentHead, ctx) {
|
3325
|
+
let added = [];
|
3326
|
+
let removed = [];
|
3327
|
+
let preserved = [];
|
3328
|
+
let nodesToAppend = [];
|
3329
|
+
let headMergeStyle = ctx.head.style;
|
3330
|
+
let srcToNewHeadNodes = new Map;
|
3331
|
+
for (const newHeadChild of newHeadTag.children) {
|
3332
|
+
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
|
3333
|
+
}
|
3334
|
+
for (const currentHeadElt of currentHead.children) {
|
3335
|
+
let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
|
3336
|
+
let isReAppended = ctx.head.shouldReAppend(currentHeadElt);
|
3337
|
+
let isPreserved = ctx.head.shouldPreserve(currentHeadElt);
|
3338
|
+
if (inNewContent || isPreserved) {
|
3339
|
+
if (isReAppended) {
|
3340
|
+
removed.push(currentHeadElt);
|
3341
|
+
} else {
|
3342
|
+
srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
|
3343
|
+
preserved.push(currentHeadElt);
|
3344
|
+
}
|
3345
|
+
} else {
|
3346
|
+
if (headMergeStyle === "append") {
|
3347
|
+
if (isReAppended) {
|
3348
|
+
removed.push(currentHeadElt);
|
3349
|
+
nodesToAppend.push(currentHeadElt);
|
3350
|
+
}
|
3351
|
+
} else {
|
3352
|
+
if (ctx.head.shouldRemove(currentHeadElt) !== false) {
|
3353
|
+
removed.push(currentHeadElt);
|
3354
|
+
}
|
3355
|
+
}
|
3356
|
+
}
|
3357
|
+
}
|
3358
|
+
nodesToAppend.push(...srcToNewHeadNodes.values());
|
3359
|
+
let promises = [];
|
3360
|
+
for (const newNode of nodesToAppend) {
|
3361
|
+
let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild;
|
3362
|
+
if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
|
3363
|
+
if (newElt.href || newElt.src) {
|
3364
|
+
let resolve = null;
|
3365
|
+
let promise = new Promise((function(_resolve) {
|
3366
|
+
resolve = _resolve;
|
3367
|
+
}));
|
3368
|
+
newElt.addEventListener("load", (function() {
|
3369
|
+
resolve();
|
3370
|
+
}));
|
3371
|
+
promises.push(promise);
|
3372
|
+
}
|
3373
|
+
currentHead.appendChild(newElt);
|
3374
|
+
ctx.callbacks.afterNodeAdded(newElt);
|
3375
|
+
added.push(newElt);
|
3376
|
+
}
|
3377
|
+
}
|
3378
|
+
for (const removedElement of removed) {
|
3379
|
+
if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
|
3380
|
+
currentHead.removeChild(removedElement);
|
3381
|
+
ctx.callbacks.afterNodeRemoved(removedElement);
|
3382
|
+
}
|
3383
|
+
}
|
3384
|
+
ctx.head.afterHeadMorphed(currentHead, {
|
3385
|
+
added: added,
|
3386
|
+
kept: preserved,
|
3387
|
+
removed: removed
|
3388
|
+
});
|
3389
|
+
return promises;
|
3390
|
+
}
|
3391
|
+
function noOp() {}
|
3392
|
+
function mergeDefaults(config) {
|
3393
|
+
let finalConfig = {};
|
3394
|
+
Object.assign(finalConfig, defaults);
|
3395
|
+
Object.assign(finalConfig, config);
|
3396
|
+
finalConfig.callbacks = {};
|
3397
|
+
Object.assign(finalConfig.callbacks, defaults.callbacks);
|
3398
|
+
Object.assign(finalConfig.callbacks, config.callbacks);
|
3399
|
+
finalConfig.head = {};
|
3400
|
+
Object.assign(finalConfig.head, defaults.head);
|
3401
|
+
Object.assign(finalConfig.head, config.head);
|
3402
|
+
return finalConfig;
|
3403
|
+
}
|
3404
|
+
function createMorphContext(oldNode, newContent, config) {
|
3405
|
+
config = mergeDefaults(config);
|
3406
|
+
return {
|
3407
|
+
target: oldNode,
|
3408
|
+
newContent: newContent,
|
3409
|
+
config: config,
|
3410
|
+
morphStyle: config.morphStyle,
|
3411
|
+
ignoreActive: config.ignoreActive,
|
3412
|
+
ignoreActiveValue: config.ignoreActiveValue,
|
3413
|
+
idMap: createIdMap(oldNode, newContent),
|
3414
|
+
deadIds: new Set,
|
3415
|
+
callbacks: config.callbacks,
|
3416
|
+
head: config.head
|
3417
|
+
};
|
3418
|
+
}
|
3419
|
+
function isIdSetMatch(node1, node2, ctx) {
|
3420
|
+
if (node1 == null || node2 == null) {
|
3421
|
+
return false;
|
3422
|
+
}
|
3423
|
+
if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) {
|
3424
|
+
if (node1.id !== "" && node1.id === node2.id) {
|
3425
|
+
return true;
|
3426
|
+
} else {
|
3427
|
+
return getIdIntersectionCount(ctx, node1, node2) > 0;
|
3428
|
+
}
|
3429
|
+
}
|
3430
|
+
return false;
|
3431
|
+
}
|
3432
|
+
function isSoftMatch(node1, node2) {
|
3433
|
+
if (node1 == null || node2 == null) {
|
3434
|
+
return false;
|
3435
|
+
}
|
3436
|
+
return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName;
|
3437
|
+
}
|
3438
|
+
function removeNodesBetween(startInclusive, endExclusive, ctx) {
|
3439
|
+
while (startInclusive !== endExclusive) {
|
3440
|
+
let tempNode = startInclusive;
|
3441
|
+
startInclusive = startInclusive.nextSibling;
|
3442
|
+
removeNode(tempNode, ctx);
|
3443
|
+
}
|
3444
|
+
removeIdsFromConsideration(ctx, endExclusive);
|
3445
|
+
return endExclusive.nextSibling;
|
3446
|
+
}
|
3447
|
+
function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
|
3448
|
+
let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent);
|
3449
|
+
let potentialMatch = null;
|
3450
|
+
if (newChildPotentialIdCount > 0) {
|
3451
|
+
let potentialMatch = insertionPoint;
|
3452
|
+
let otherMatchCount = 0;
|
3453
|
+
while (potentialMatch != null) {
|
3454
|
+
if (isIdSetMatch(newChild, potentialMatch, ctx)) {
|
3455
|
+
return potentialMatch;
|
3456
|
+
}
|
3457
|
+
otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent);
|
3458
|
+
if (otherMatchCount > newChildPotentialIdCount) {
|
3459
|
+
return null;
|
3460
|
+
}
|
3461
|
+
potentialMatch = potentialMatch.nextSibling;
|
3462
|
+
}
|
3463
|
+
}
|
3464
|
+
return potentialMatch;
|
3465
|
+
}
|
3466
|
+
function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
|
3467
|
+
let potentialSoftMatch = insertionPoint;
|
3468
|
+
let nextSibling = newChild.nextSibling;
|
3469
|
+
let siblingSoftMatchCount = 0;
|
3470
|
+
while (potentialSoftMatch != null) {
|
3471
|
+
if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) {
|
3472
|
+
return null;
|
3473
|
+
}
|
3474
|
+
if (isSoftMatch(newChild, potentialSoftMatch)) {
|
3475
|
+
return potentialSoftMatch;
|
3476
|
+
}
|
3477
|
+
if (isSoftMatch(nextSibling, potentialSoftMatch)) {
|
3478
|
+
siblingSoftMatchCount++;
|
3479
|
+
nextSibling = nextSibling.nextSibling;
|
3480
|
+
if (siblingSoftMatchCount >= 2) {
|
3481
|
+
return null;
|
3482
|
+
}
|
3483
|
+
}
|
3484
|
+
potentialSoftMatch = potentialSoftMatch.nextSibling;
|
3485
|
+
}
|
3486
|
+
return potentialSoftMatch;
|
3487
|
+
}
|
3488
|
+
function parseContent(newContent) {
|
3489
|
+
let parser = new DOMParser;
|
3490
|
+
let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, "");
|
3491
|
+
if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) {
|
3492
|
+
let content = parser.parseFromString(newContent, "text/html");
|
3493
|
+
if (contentWithSvgsRemoved.match(/<\/html>/)) {
|
3494
|
+
content.generatedByIdiomorph = true;
|
3495
|
+
return content;
|
3496
|
+
} else {
|
3497
|
+
let htmlElement = content.firstChild;
|
3498
|
+
if (htmlElement) {
|
3499
|
+
htmlElement.generatedByIdiomorph = true;
|
3500
|
+
return htmlElement;
|
3501
|
+
} else {
|
3502
|
+
return null;
|
3503
|
+
}
|
3504
|
+
}
|
3505
|
+
} else {
|
3506
|
+
let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html");
|
3507
|
+
let content = responseDoc.body.querySelector("template").content;
|
3508
|
+
content.generatedByIdiomorph = true;
|
3509
|
+
return content;
|
3510
|
+
}
|
3511
|
+
}
|
3512
|
+
function normalizeContent(newContent) {
|
3513
|
+
if (newContent == null) {
|
3514
|
+
const dummyParent = document.createElement("div");
|
3515
|
+
return dummyParent;
|
3516
|
+
} else if (newContent.generatedByIdiomorph) {
|
3517
|
+
return newContent;
|
3518
|
+
} else if (newContent instanceof Node) {
|
3519
|
+
const dummyParent = document.createElement("div");
|
3520
|
+
dummyParent.append(newContent);
|
3521
|
+
return dummyParent;
|
3522
|
+
} else {
|
3523
|
+
const dummyParent = document.createElement("div");
|
3524
|
+
for (const elt of [ ...newContent ]) {
|
3525
|
+
dummyParent.append(elt);
|
3526
|
+
}
|
3527
|
+
return dummyParent;
|
3528
|
+
}
|
3529
|
+
}
|
3530
|
+
function insertSiblings(previousSibling, morphedNode, nextSibling) {
|
3531
|
+
let stack = [];
|
3532
|
+
let added = [];
|
3533
|
+
while (previousSibling != null) {
|
3534
|
+
stack.push(previousSibling);
|
3535
|
+
previousSibling = previousSibling.previousSibling;
|
3536
|
+
}
|
3537
|
+
while (stack.length > 0) {
|
3538
|
+
let node = stack.pop();
|
3539
|
+
added.push(node);
|
3540
|
+
morphedNode.parentElement.insertBefore(node, morphedNode);
|
3541
|
+
}
|
3542
|
+
added.push(morphedNode);
|
3543
|
+
while (nextSibling != null) {
|
3544
|
+
stack.push(nextSibling);
|
3545
|
+
added.push(nextSibling);
|
3546
|
+
nextSibling = nextSibling.nextSibling;
|
3547
|
+
}
|
3548
|
+
while (stack.length > 0) {
|
3549
|
+
morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling);
|
3550
|
+
}
|
3551
|
+
return added;
|
3552
|
+
}
|
3553
|
+
function findBestNodeMatch(newContent, oldNode, ctx) {
|
3554
|
+
let currentElement;
|
3555
|
+
currentElement = newContent.firstChild;
|
3556
|
+
let bestElement = currentElement;
|
3557
|
+
let score = 0;
|
3558
|
+
while (currentElement) {
|
3559
|
+
let newScore = scoreElement(currentElement, oldNode, ctx);
|
3560
|
+
if (newScore > score) {
|
3561
|
+
bestElement = currentElement;
|
3562
|
+
score = newScore;
|
3563
|
+
}
|
3564
|
+
currentElement = currentElement.nextSibling;
|
3565
|
+
}
|
3566
|
+
return bestElement;
|
3567
|
+
}
|
3568
|
+
function scoreElement(node1, node2, ctx) {
|
3569
|
+
if (isSoftMatch(node1, node2)) {
|
3570
|
+
return .5 + getIdIntersectionCount(ctx, node1, node2);
|
3571
|
+
}
|
3572
|
+
return 0;
|
3573
|
+
}
|
3574
|
+
function removeNode(tempNode, ctx) {
|
3575
|
+
removeIdsFromConsideration(ctx, tempNode);
|
3576
|
+
if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return;
|
3577
|
+
tempNode.remove();
|
3578
|
+
ctx.callbacks.afterNodeRemoved(tempNode);
|
3579
|
+
}
|
3580
|
+
function isIdInConsideration(ctx, id) {
|
3581
|
+
return !ctx.deadIds.has(id);
|
3582
|
+
}
|
3583
|
+
function idIsWithinNode(ctx, id, targetNode) {
|
3584
|
+
let idSet = ctx.idMap.get(targetNode) || EMPTY_SET;
|
3585
|
+
return idSet.has(id);
|
3586
|
+
}
|
3587
|
+
function removeIdsFromConsideration(ctx, node) {
|
3588
|
+
let idSet = ctx.idMap.get(node) || EMPTY_SET;
|
3589
|
+
for (const id of idSet) {
|
3590
|
+
ctx.deadIds.add(id);
|
3591
|
+
}
|
3592
|
+
}
|
3593
|
+
function getIdIntersectionCount(ctx, node1, node2) {
|
3594
|
+
let sourceSet = ctx.idMap.get(node1) || EMPTY_SET;
|
3595
|
+
let matchCount = 0;
|
3596
|
+
for (const id of sourceSet) {
|
3597
|
+
if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) {
|
3598
|
+
++matchCount;
|
3599
|
+
}
|
3600
|
+
}
|
3601
|
+
return matchCount;
|
3602
|
+
}
|
3603
|
+
function populateIdMapForNode(node, idMap) {
|
3604
|
+
let nodeParent = node.parentElement;
|
3605
|
+
let idElements = node.querySelectorAll("[id]");
|
3606
|
+
for (const elt of idElements) {
|
3607
|
+
let current = elt;
|
3608
|
+
while (current !== nodeParent && current != null) {
|
3609
|
+
let idSet = idMap.get(current);
|
3610
|
+
if (idSet == null) {
|
3611
|
+
idSet = new Set;
|
3612
|
+
idMap.set(current, idSet);
|
3613
|
+
}
|
3614
|
+
idSet.add(elt.id);
|
3615
|
+
current = current.parentElement;
|
3616
|
+
}
|
3617
|
+
}
|
3618
|
+
}
|
3619
|
+
function createIdMap(oldContent, newContent) {
|
3620
|
+
let idMap = new Map;
|
3621
|
+
populateIdMapForNode(oldContent, idMap);
|
3622
|
+
populateIdMapForNode(newContent, idMap);
|
3623
|
+
return idMap;
|
3624
|
+
}
|
3625
|
+
return {
|
3626
|
+
morph: morph,
|
3627
|
+
defaults: defaults
|
3628
|
+
};
|
3629
|
+
}();
|
3630
|
+
|
2681
3631
|
class PageRenderer extends Renderer {
|
2682
3632
|
static renderElement(currentElement, newElement) {
|
2683
3633
|
if (document.body && newElement instanceof HTMLBodyElement) {
|
@@ -2702,6 +3652,7 @@ class PageRenderer extends Renderer {
|
|
2702
3652
|
}
|
2703
3653
|
}
|
2704
3654
|
async prepareToRender() {
|
3655
|
+
this.#setLanguage();
|
2705
3656
|
await this.mergeHead();
|
2706
3657
|
}
|
2707
3658
|
async render() {
|
@@ -2724,12 +3675,24 @@ class PageRenderer extends Renderer {
|
|
2724
3675
|
get newElement() {
|
2725
3676
|
return this.newSnapshot.element;
|
2726
3677
|
}
|
3678
|
+
#setLanguage() {
|
3679
|
+
const {documentElement: documentElement} = this.currentSnapshot;
|
3680
|
+
const {lang: lang} = this.newSnapshot;
|
3681
|
+
if (lang) {
|
3682
|
+
documentElement.setAttribute("lang", lang);
|
3683
|
+
} else {
|
3684
|
+
documentElement.removeAttribute("lang");
|
3685
|
+
}
|
3686
|
+
}
|
2727
3687
|
async mergeHead() {
|
2728
3688
|
const mergedHeadElements = this.mergeProvisionalElements();
|
2729
3689
|
const newStylesheetElements = this.copyNewHeadStylesheetElements();
|
2730
3690
|
this.copyNewHeadScriptElements();
|
2731
3691
|
await mergedHeadElements;
|
2732
3692
|
await newStylesheetElements;
|
3693
|
+
if (this.willRender) {
|
3694
|
+
this.removeUnusedDynamicStylesheetElements();
|
3695
|
+
}
|
2733
3696
|
}
|
2734
3697
|
async replaceBody() {
|
2735
3698
|
await this.preservingPermanentElements((async () => {
|
@@ -2753,6 +3716,11 @@ class PageRenderer extends Renderer {
|
|
2753
3716
|
document.head.appendChild(activateScriptElement(element));
|
2754
3717
|
}
|
2755
3718
|
}
|
3719
|
+
removeUnusedDynamicStylesheetElements() {
|
3720
|
+
for (const element of this.unusedDynamicStylesheetElements) {
|
3721
|
+
document.head.removeChild(element);
|
3722
|
+
}
|
3723
|
+
}
|
2756
3724
|
async mergeProvisionalElements() {
|
2757
3725
|
const newHeadElements = [ ...this.newHeadProvisionalElements ];
|
2758
3726
|
for (const element of this.currentHeadProvisionalElements) {
|
@@ -2805,6 +3773,12 @@ class PageRenderer extends Renderer {
|
|
2805
3773
|
async assignNewBody() {
|
2806
3774
|
await this.renderElement(this.currentElement, this.newElement);
|
2807
3775
|
}
|
3776
|
+
get unusedDynamicStylesheetElements() {
|
3777
|
+
return this.oldHeadStylesheetElements.filter((element => element.getAttribute("data-turbo-track") === "dynamic"));
|
3778
|
+
}
|
3779
|
+
get oldHeadStylesheetElements() {
|
3780
|
+
return this.currentHeadSnapshot.getStylesheetElementsNotInSnapshot(this.newHeadSnapshot);
|
3781
|
+
}
|
2808
3782
|
get newHeadStylesheetElements() {
|
2809
3783
|
return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot);
|
2810
3784
|
}
|
@@ -2822,10 +3796,113 @@ class PageRenderer extends Renderer {
|
|
2822
3796
|
}
|
2823
3797
|
}
|
2824
3798
|
|
3799
|
+
class MorphRenderer extends PageRenderer {
|
3800
|
+
async render() {
|
3801
|
+
if (this.willRender) await this.#morphBody();
|
3802
|
+
}
|
3803
|
+
get renderMethod() {
|
3804
|
+
return "morph";
|
3805
|
+
}
|
3806
|
+
async #morphBody() {
|
3807
|
+
this.#morphElements(this.currentElement, this.newElement);
|
3808
|
+
this.#reloadRemoteFrames();
|
3809
|
+
dispatch("turbo:morph", {
|
3810
|
+
detail: {
|
3811
|
+
currentElement: this.currentElement,
|
3812
|
+
newElement: this.newElement
|
3813
|
+
}
|
3814
|
+
});
|
3815
|
+
}
|
3816
|
+
#morphElements(currentElement, newElement, morphStyle = "outerHTML") {
|
3817
|
+
this.isMorphingTurboFrame = this.#isFrameReloadedWithMorph(currentElement);
|
3818
|
+
Idiomorph.morph(currentElement, newElement, {
|
3819
|
+
ignoreActiveValue: true,
|
3820
|
+
morphStyle: morphStyle,
|
3821
|
+
callbacks: {
|
3822
|
+
beforeNodeAdded: this.#shouldAddElement,
|
3823
|
+
beforeNodeMorphed: this.#shouldMorphElement,
|
3824
|
+
beforeAttributeUpdated: this.#shouldUpdateAttribute,
|
3825
|
+
beforeNodeRemoved: this.#shouldRemoveElement,
|
3826
|
+
afterNodeMorphed: this.#didMorphElement
|
3827
|
+
}
|
3828
|
+
});
|
3829
|
+
}
|
3830
|
+
#shouldAddElement=node => !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id));
|
3831
|
+
#shouldMorphElement=(oldNode, newNode) => {
|
3832
|
+
if (oldNode instanceof HTMLElement) {
|
3833
|
+
if (!oldNode.hasAttribute("data-turbo-permanent") && (this.isMorphingTurboFrame || !this.#isFrameReloadedWithMorph(oldNode))) {
|
3834
|
+
const event = dispatch("turbo:before-morph-element", {
|
3835
|
+
cancelable: true,
|
3836
|
+
target: oldNode,
|
3837
|
+
detail: {
|
3838
|
+
newElement: newNode
|
3839
|
+
}
|
3840
|
+
});
|
3841
|
+
return !event.defaultPrevented;
|
3842
|
+
} else {
|
3843
|
+
return false;
|
3844
|
+
}
|
3845
|
+
}
|
3846
|
+
};
|
3847
|
+
#shouldUpdateAttribute=(attributeName, target, mutationType) => {
|
3848
|
+
const event = dispatch("turbo:before-morph-attribute", {
|
3849
|
+
cancelable: true,
|
3850
|
+
target: target,
|
3851
|
+
detail: {
|
3852
|
+
attributeName: attributeName,
|
3853
|
+
mutationType: mutationType
|
3854
|
+
}
|
3855
|
+
});
|
3856
|
+
return !event.defaultPrevented;
|
3857
|
+
};
|
3858
|
+
#didMorphElement=(oldNode, newNode) => {
|
3859
|
+
if (newNode instanceof HTMLElement) {
|
3860
|
+
dispatch("turbo:morph-element", {
|
3861
|
+
target: oldNode,
|
3862
|
+
detail: {
|
3863
|
+
newElement: newNode
|
3864
|
+
}
|
3865
|
+
});
|
3866
|
+
}
|
3867
|
+
};
|
3868
|
+
#shouldRemoveElement=node => this.#shouldMorphElement(node);
|
3869
|
+
#reloadRemoteFrames() {
|
3870
|
+
this.#remoteFrames().forEach((frame => {
|
3871
|
+
if (this.#isFrameReloadedWithMorph(frame)) {
|
3872
|
+
this.#renderFrameWithMorph(frame);
|
3873
|
+
frame.reload();
|
3874
|
+
}
|
3875
|
+
}));
|
3876
|
+
}
|
3877
|
+
#renderFrameWithMorph(frame) {
|
3878
|
+
frame.addEventListener("turbo:before-frame-render", (event => {
|
3879
|
+
event.detail.render = this.#morphFrameUpdate;
|
3880
|
+
}), {
|
3881
|
+
once: true
|
3882
|
+
});
|
3883
|
+
}
|
3884
|
+
#morphFrameUpdate=(currentElement, newElement) => {
|
3885
|
+
dispatch("turbo:before-frame-morph", {
|
3886
|
+
target: currentElement,
|
3887
|
+
detail: {
|
3888
|
+
currentElement: currentElement,
|
3889
|
+
newElement: newElement
|
3890
|
+
}
|
3891
|
+
});
|
3892
|
+
this.#morphElements(currentElement, newElement.children, "innerHTML");
|
3893
|
+
};
|
3894
|
+
#isFrameReloadedWithMorph(element) {
|
3895
|
+
return element.src && element.refresh === "morph";
|
3896
|
+
}
|
3897
|
+
#remoteFrames() {
|
3898
|
+
return Array.from(document.querySelectorAll("turbo-frame[src]")).filter((frame => !frame.closest("[data-turbo-permanent]")));
|
3899
|
+
}
|
3900
|
+
}
|
3901
|
+
|
2825
3902
|
class SnapshotCache {
|
3903
|
+
keys=[];
|
3904
|
+
snapshots={};
|
2826
3905
|
constructor(size) {
|
2827
|
-
this.keys = [];
|
2828
|
-
this.snapshots = {};
|
2829
3906
|
this.size = size;
|
2830
3907
|
}
|
2831
3908
|
has(location) {
|
@@ -2867,23 +3944,25 @@ class SnapshotCache {
|
|
2867
3944
|
}
|
2868
3945
|
|
2869
3946
|
class PageView extends View {
|
2870
|
-
|
2871
|
-
|
2872
|
-
|
2873
|
-
|
2874
|
-
this.
|
3947
|
+
snapshotCache=new SnapshotCache(10);
|
3948
|
+
lastRenderedLocation=new URL(location.href);
|
3949
|
+
forceReloaded=false;
|
3950
|
+
shouldTransitionTo(newSnapshot) {
|
3951
|
+
return this.snapshot.prefersViewTransitions && newSnapshot.prefersViewTransitions;
|
2875
3952
|
}
|
2876
3953
|
renderPage(snapshot, isPreview = false, willRender = true, visit) {
|
2877
|
-
const
|
3954
|
+
const shouldMorphPage = this.isPageRefresh(visit) && this.snapshot.shouldMorphPage;
|
3955
|
+
const rendererClass = shouldMorphPage ? MorphRenderer : PageRenderer;
|
3956
|
+
const renderer = new rendererClass(this.snapshot, snapshot, PageRenderer.renderElement, isPreview, willRender);
|
2878
3957
|
if (!renderer.shouldRender) {
|
2879
3958
|
this.forceReloaded = true;
|
2880
3959
|
} else {
|
2881
|
-
visit
|
3960
|
+
visit?.changeHistory();
|
2882
3961
|
}
|
2883
3962
|
return this.render(renderer);
|
2884
3963
|
}
|
2885
3964
|
renderError(snapshot, visit) {
|
2886
|
-
visit
|
3965
|
+
visit?.changeHistory();
|
2887
3966
|
const renderer = new ErrorRenderer(this.snapshot, snapshot, ErrorRenderer.renderElement, false);
|
2888
3967
|
return this.render(renderer);
|
2889
3968
|
}
|
@@ -2903,31 +3982,38 @@ class PageView extends View {
|
|
2903
3982
|
getCachedSnapshotForLocation(location) {
|
2904
3983
|
return this.snapshotCache.get(location);
|
2905
3984
|
}
|
3985
|
+
isPageRefresh(visit) {
|
3986
|
+
return !visit || this.lastRenderedLocation.pathname === visit.location.pathname && visit.action === "replace";
|
3987
|
+
}
|
3988
|
+
shouldPreserveScrollPosition(visit) {
|
3989
|
+
return this.isPageRefresh(visit) && this.snapshot.shouldPreserveScrollPosition;
|
3990
|
+
}
|
2906
3991
|
get snapshot() {
|
2907
3992
|
return PageSnapshot.fromElement(this.element);
|
2908
3993
|
}
|
2909
3994
|
}
|
2910
3995
|
|
2911
3996
|
class Preloader {
|
2912
|
-
|
2913
|
-
|
3997
|
+
selector="a[data-turbo-preload]";
|
3998
|
+
constructor(delegate, snapshotCache) {
|
2914
3999
|
this.delegate = delegate;
|
2915
|
-
|
2916
|
-
get snapshotCache() {
|
2917
|
-
return this.delegate.navigator.view.snapshotCache;
|
4000
|
+
this.snapshotCache = snapshotCache;
|
2918
4001
|
}
|
2919
4002
|
start() {
|
2920
4003
|
if (document.readyState === "loading") {
|
2921
|
-
|
2922
|
-
this.preloadOnLoadLinksForView(document.body);
|
2923
|
-
}));
|
4004
|
+
document.addEventListener("DOMContentLoaded", this.#preloadAll);
|
2924
4005
|
} else {
|
2925
4006
|
this.preloadOnLoadLinksForView(document.body);
|
2926
4007
|
}
|
2927
4008
|
}
|
4009
|
+
stop() {
|
4010
|
+
document.removeEventListener("DOMContentLoaded", this.#preloadAll);
|
4011
|
+
}
|
2928
4012
|
preloadOnLoadLinksForView(element) {
|
2929
4013
|
for (const link of element.querySelectorAll(this.selector)) {
|
2930
|
-
this.
|
4014
|
+
if (this.delegate.shouldPreloadLink(link)) {
|
4015
|
+
this.preloadURL(link);
|
4016
|
+
}
|
2931
4017
|
}
|
2932
4018
|
}
|
2933
4019
|
async preloadURL(link) {
|
@@ -2935,46 +4021,83 @@ class Preloader {
|
|
2935
4021
|
if (this.snapshotCache.has(location)) {
|
2936
4022
|
return;
|
2937
4023
|
}
|
4024
|
+
const fetchRequest = new FetchRequest(this, FetchMethod.get, location, new URLSearchParams, link);
|
4025
|
+
await fetchRequest.perform();
|
4026
|
+
}
|
4027
|
+
prepareRequest(fetchRequest) {
|
4028
|
+
fetchRequest.headers["X-Sec-Purpose"] = "prefetch";
|
4029
|
+
}
|
4030
|
+
async requestSucceededWithResponse(fetchRequest, fetchResponse) {
|
2938
4031
|
try {
|
2939
|
-
const
|
2940
|
-
|
2941
|
-
|
2942
|
-
Accept: "text/html"
|
2943
|
-
}
|
2944
|
-
});
|
2945
|
-
const responseText = await response.text();
|
2946
|
-
const snapshot = PageSnapshot.fromHTMLString(responseText);
|
2947
|
-
this.snapshotCache.put(location, snapshot);
|
4032
|
+
const responseHTML = await fetchResponse.responseHTML;
|
4033
|
+
const snapshot = PageSnapshot.fromHTMLString(responseHTML);
|
4034
|
+
this.snapshotCache.put(fetchRequest.url, snapshot);
|
2948
4035
|
} catch (_) {}
|
2949
4036
|
}
|
4037
|
+
requestStarted(fetchRequest) {}
|
4038
|
+
requestErrored(fetchRequest) {}
|
4039
|
+
requestFinished(fetchRequest) {}
|
4040
|
+
requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}
|
4041
|
+
requestFailedWithResponse(fetchRequest, fetchResponse) {}
|
4042
|
+
#preloadAll=() => {
|
4043
|
+
this.preloadOnLoadLinksForView(document.body);
|
4044
|
+
};
|
4045
|
+
}
|
4046
|
+
|
4047
|
+
class Cache {
|
4048
|
+
constructor(session) {
|
4049
|
+
this.session = session;
|
4050
|
+
}
|
4051
|
+
clear() {
|
4052
|
+
this.session.clearCache();
|
4053
|
+
}
|
4054
|
+
resetCacheControl() {
|
4055
|
+
this.#setCacheControl("");
|
4056
|
+
}
|
4057
|
+
exemptPageFromCache() {
|
4058
|
+
this.#setCacheControl("no-cache");
|
4059
|
+
}
|
4060
|
+
exemptPageFromPreview() {
|
4061
|
+
this.#setCacheControl("no-preview");
|
4062
|
+
}
|
4063
|
+
#setCacheControl(value) {
|
4064
|
+
setMetaContent("turbo-cache-control", value);
|
4065
|
+
}
|
2950
4066
|
}
|
2951
4067
|
|
2952
4068
|
class Session {
|
2953
|
-
|
2954
|
-
|
2955
|
-
|
2956
|
-
|
2957
|
-
|
2958
|
-
|
2959
|
-
|
2960
|
-
|
2961
|
-
|
2962
|
-
|
2963
|
-
|
2964
|
-
|
2965
|
-
|
2966
|
-
|
2967
|
-
|
2968
|
-
|
2969
|
-
|
2970
|
-
|
2971
|
-
|
2972
|
-
|
4069
|
+
navigator=new Navigator(this);
|
4070
|
+
history=new History(this);
|
4071
|
+
view=new PageView(this, document.documentElement);
|
4072
|
+
adapter=new BrowserAdapter(this);
|
4073
|
+
pageObserver=new PageObserver(this);
|
4074
|
+
cacheObserver=new CacheObserver;
|
4075
|
+
linkPrefetchObserver=new LinkPrefetchObserver(this, document);
|
4076
|
+
linkClickObserver=new LinkClickObserver(this, window);
|
4077
|
+
formSubmitObserver=new FormSubmitObserver(this, document);
|
4078
|
+
scrollObserver=new ScrollObserver(this);
|
4079
|
+
streamObserver=new StreamObserver(this);
|
4080
|
+
formLinkClickObserver=new FormLinkClickObserver(this, document.documentElement);
|
4081
|
+
frameRedirector=new FrameRedirector(this, document.documentElement);
|
4082
|
+
streamMessageRenderer=new StreamMessageRenderer;
|
4083
|
+
cache=new Cache(this);
|
4084
|
+
drive=true;
|
4085
|
+
enabled=true;
|
4086
|
+
progressBarDelay=500;
|
4087
|
+
started=false;
|
4088
|
+
formMode="on";
|
4089
|
+
#pageRefreshDebouncePeriod=150;
|
4090
|
+
constructor(recentRequests) {
|
4091
|
+
this.recentRequests = recentRequests;
|
4092
|
+
this.preloader = new Preloader(this, this.view.snapshotCache);
|
4093
|
+
this.debouncedRefresh = this.refresh;
|
4094
|
+
this.pageRefreshDebouncePeriod = this.pageRefreshDebouncePeriod;
|
2973
4095
|
}
|
2974
4096
|
start() {
|
2975
4097
|
if (!this.started) {
|
2976
4098
|
this.pageObserver.start();
|
2977
4099
|
this.cacheObserver.start();
|
4100
|
+
this.linkPrefetchObserver.start();
|
2978
4101
|
this.formLinkClickObserver.start();
|
2979
4102
|
this.linkClickObserver.start();
|
2980
4103
|
this.formSubmitObserver.start();
|
@@ -2994,6 +4117,7 @@ class Session {
|
|
2994
4117
|
if (this.started) {
|
2995
4118
|
this.pageObserver.stop();
|
2996
4119
|
this.cacheObserver.stop();
|
4120
|
+
this.linkPrefetchObserver.stop();
|
2997
4121
|
this.formLinkClickObserver.stop();
|
2998
4122
|
this.linkClickObserver.stop();
|
2999
4123
|
this.formSubmitObserver.stop();
|
@@ -3001,6 +4125,7 @@ class Session {
|
|
3001
4125
|
this.streamObserver.stop();
|
3002
4126
|
this.frameRedirector.stop();
|
3003
4127
|
this.history.stop();
|
4128
|
+
this.preloader.stop();
|
3004
4129
|
this.started = false;
|
3005
4130
|
}
|
3006
4131
|
}
|
@@ -3010,12 +4135,22 @@ class Session {
|
|
3010
4135
|
visit(location, options = {}) {
|
3011
4136
|
const frameElement = options.frame ? document.getElementById(options.frame) : null;
|
3012
4137
|
if (frameElement instanceof FrameElement) {
|
4138
|
+
const action = options.action || getVisitAction(frameElement);
|
4139
|
+
frameElement.delegate.proposeVisitIfNavigatedWithAction(frameElement, action);
|
3013
4140
|
frameElement.src = location.toString();
|
3014
|
-
frameElement.loaded;
|
3015
4141
|
} else {
|
3016
4142
|
this.navigator.proposeVisit(expandURL(location), options);
|
3017
4143
|
}
|
3018
4144
|
}
|
4145
|
+
refresh(url, requestId) {
|
4146
|
+
const isRecentRequest = requestId && this.recentRequests.has(requestId);
|
4147
|
+
if (!isRecentRequest) {
|
4148
|
+
this.cache.exemptPageFromPreview();
|
4149
|
+
this.visit(url, {
|
4150
|
+
action: "replace"
|
4151
|
+
});
|
4152
|
+
}
|
4153
|
+
}
|
3019
4154
|
connectStreamSource(source) {
|
3020
4155
|
this.streamObserver.connectStreamSource(source);
|
3021
4156
|
}
|
@@ -3040,11 +4175,31 @@ class Session {
|
|
3040
4175
|
get restorationIdentifier() {
|
3041
4176
|
return this.history.restorationIdentifier;
|
3042
4177
|
}
|
3043
|
-
|
4178
|
+
get pageRefreshDebouncePeriod() {
|
4179
|
+
return this.#pageRefreshDebouncePeriod;
|
4180
|
+
}
|
4181
|
+
set pageRefreshDebouncePeriod(value) {
|
4182
|
+
this.refresh = debounce(this.debouncedRefresh.bind(this), value);
|
4183
|
+
this.#pageRefreshDebouncePeriod = value;
|
4184
|
+
}
|
4185
|
+
shouldPreloadLink(element) {
|
4186
|
+
const isUnsafe = element.hasAttribute("data-turbo-method");
|
4187
|
+
const isStream = element.hasAttribute("data-turbo-stream");
|
4188
|
+
const frameTarget = element.getAttribute("data-turbo-frame");
|
4189
|
+
const frame = frameTarget == "_top" ? null : document.getElementById(frameTarget) || findClosestRecursively(element, "turbo-frame:not([disabled])");
|
4190
|
+
if (isUnsafe || isStream || frame instanceof FrameElement) {
|
4191
|
+
return false;
|
4192
|
+
} else {
|
4193
|
+
const location = new URL(element.href);
|
4194
|
+
return this.elementIsNavigatable(element) && locationIsVisitable(location, this.snapshot.rootLocation);
|
4195
|
+
}
|
4196
|
+
}
|
4197
|
+
historyPoppedToLocationWithRestorationIdentifierAndDirection(location, restorationIdentifier, direction) {
|
3044
4198
|
if (this.enabled) {
|
3045
4199
|
this.navigator.startVisit(location, restorationIdentifier, {
|
3046
4200
|
action: "restore",
|
3047
|
-
historyChanged: true
|
4201
|
+
historyChanged: true,
|
4202
|
+
direction: direction
|
3048
4203
|
});
|
3049
4204
|
} else {
|
3050
4205
|
this.adapter.pageInvalidated({
|
@@ -3061,6 +4216,9 @@ class Session {
|
|
3061
4216
|
return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation);
|
3062
4217
|
}
|
3063
4218
|
submittedFormLinkToLocation() {}
|
4219
|
+
canPrefetchRequestToLocation(link, location) {
|
4220
|
+
return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation);
|
4221
|
+
}
|
3064
4222
|
willFollowLinkToLocation(link, location, event) {
|
3065
4223
|
return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation) && this.applicationAllowsFollowingLinkToLocation(link, location, event);
|
3066
4224
|
}
|
@@ -3082,6 +4240,7 @@ class Session {
|
|
3082
4240
|
visitStarted(visit) {
|
3083
4241
|
if (!visit.acceptsStreamResponse) {
|
3084
4242
|
markAsBusy(document.documentElement);
|
4243
|
+
this.view.markVisitDirection(visit.direction);
|
3085
4244
|
}
|
3086
4245
|
extendURLWithDeprecatedProperties(visit.location);
|
3087
4246
|
if (!visit.silent) {
|
@@ -3089,6 +4248,7 @@ class Session {
|
|
3089
4248
|
}
|
3090
4249
|
}
|
3091
4250
|
visitCompleted(visit) {
|
4251
|
+
this.view.unmarkVisitDirection();
|
3092
4252
|
clearBusyState(document.documentElement);
|
3093
4253
|
this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
|
3094
4254
|
}
|
@@ -3099,7 +4259,7 @@ class Session {
|
|
3099
4259
|
this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
|
3100
4260
|
}
|
3101
4261
|
willSubmitForm(form, submitter) {
|
3102
|
-
const action = getAction(form, submitter);
|
4262
|
+
const action = getAction$1(form, submitter);
|
3103
4263
|
return this.submissionIsNavigatable(form, submitter) && locationIsVisitable(expandURL(action), this.snapshot.rootLocation);
|
3104
4264
|
}
|
3105
4265
|
formSubmitted(form, submitter) {
|
@@ -3119,8 +4279,7 @@ class Session {
|
|
3119
4279
|
this.renderStreamMessage(message);
|
3120
4280
|
}
|
3121
4281
|
viewWillCacheSnapshot() {
|
3122
|
-
|
3123
|
-
if (!((_a = this.navigator.currentVisit) === null || _a === void 0 ? void 0 : _a.silent)) {
|
4282
|
+
if (!this.navigator.currentVisit?.silent) {
|
3124
4283
|
this.notifyApplicationBeforeCachingSnapshot();
|
3125
4284
|
}
|
3126
4285
|
}
|
@@ -3132,9 +4291,9 @@ class Session {
|
|
3132
4291
|
}
|
3133
4292
|
return !defaultPrevented;
|
3134
4293
|
}
|
3135
|
-
viewRenderedSnapshot(_snapshot, _isPreview) {
|
4294
|
+
viewRenderedSnapshot(_snapshot, _isPreview, renderMethod) {
|
3136
4295
|
this.view.lastRenderedLocation = this.history.location;
|
3137
|
-
this.notifyApplicationAfterRender();
|
4296
|
+
this.notifyApplicationAfterRender(renderMethod);
|
3138
4297
|
}
|
3139
4298
|
preloadOnLoadLinksForView(element) {
|
3140
4299
|
this.preloader.preloadOnLoadLinksForView(element);
|
@@ -3187,14 +4346,19 @@ class Session {
|
|
3187
4346
|
}
|
3188
4347
|
notifyApplicationBeforeRender(newBody, options) {
|
3189
4348
|
return dispatch("turbo:before-render", {
|
3190
|
-
detail:
|
3191
|
-
newBody: newBody
|
3192
|
-
|
4349
|
+
detail: {
|
4350
|
+
newBody: newBody,
|
4351
|
+
...options
|
4352
|
+
},
|
3193
4353
|
cancelable: true
|
3194
4354
|
});
|
3195
4355
|
}
|
3196
|
-
notifyApplicationAfterRender() {
|
3197
|
-
return dispatch("turbo:render"
|
4356
|
+
notifyApplicationAfterRender(renderMethod) {
|
4357
|
+
return dispatch("turbo:render", {
|
4358
|
+
detail: {
|
4359
|
+
renderMethod: renderMethod
|
4360
|
+
}
|
4361
|
+
});
|
3198
4362
|
}
|
3199
4363
|
notifyApplicationAfterPageLoad(timing = {}) {
|
3200
4364
|
return dispatch("turbo:load", {
|
@@ -3273,67 +4437,9 @@ const deprecatedLocationPropertyDescriptors = {
|
|
3273
4437
|
}
|
3274
4438
|
};
|
3275
4439
|
|
3276
|
-
|
3277
|
-
constructor(session) {
|
3278
|
-
this.session = session;
|
3279
|
-
}
|
3280
|
-
clear() {
|
3281
|
-
this.session.clearCache();
|
3282
|
-
}
|
3283
|
-
resetCacheControl() {
|
3284
|
-
this.setCacheControl("");
|
3285
|
-
}
|
3286
|
-
exemptPageFromCache() {
|
3287
|
-
this.setCacheControl("no-cache");
|
3288
|
-
}
|
3289
|
-
exemptPageFromPreview() {
|
3290
|
-
this.setCacheControl("no-preview");
|
3291
|
-
}
|
3292
|
-
setCacheControl(value) {
|
3293
|
-
setMetaContent("turbo-cache-control", value);
|
3294
|
-
}
|
3295
|
-
}
|
3296
|
-
|
3297
|
-
const StreamActions = {
|
3298
|
-
after() {
|
3299
|
-
this.targetElements.forEach((e => {
|
3300
|
-
var _a;
|
3301
|
-
return (_a = e.parentElement) === null || _a === void 0 ? void 0 : _a.insertBefore(this.templateContent, e.nextSibling);
|
3302
|
-
}));
|
3303
|
-
},
|
3304
|
-
append() {
|
3305
|
-
this.removeDuplicateTargetChildren();
|
3306
|
-
this.targetElements.forEach((e => e.append(this.templateContent)));
|
3307
|
-
},
|
3308
|
-
before() {
|
3309
|
-
this.targetElements.forEach((e => {
|
3310
|
-
var _a;
|
3311
|
-
return (_a = e.parentElement) === null || _a === void 0 ? void 0 : _a.insertBefore(this.templateContent, e);
|
3312
|
-
}));
|
3313
|
-
},
|
3314
|
-
prepend() {
|
3315
|
-
this.removeDuplicateTargetChildren();
|
3316
|
-
this.targetElements.forEach((e => e.prepend(this.templateContent)));
|
3317
|
-
},
|
3318
|
-
remove() {
|
3319
|
-
this.targetElements.forEach((e => e.remove()));
|
3320
|
-
},
|
3321
|
-
replace() {
|
3322
|
-
this.targetElements.forEach((e => e.replaceWith(this.templateContent)));
|
3323
|
-
},
|
3324
|
-
update() {
|
3325
|
-
this.targetElements.forEach((targetElement => {
|
3326
|
-
targetElement.innerHTML = "";
|
3327
|
-
targetElement.append(this.templateContent);
|
3328
|
-
}));
|
3329
|
-
}
|
3330
|
-
};
|
3331
|
-
|
3332
|
-
const session = new Session;
|
4440
|
+
const session = new Session(recentRequests);
|
3333
4441
|
|
3334
|
-
const cache =
|
3335
|
-
|
3336
|
-
const {navigator: navigator$1} = session;
|
4442
|
+
const {cache: cache, navigator: navigator$1} = session;
|
3337
4443
|
|
3338
4444
|
function start() {
|
3339
4445
|
session.start();
|
@@ -3384,6 +4490,7 @@ var Turbo = Object.freeze({
|
|
3384
4490
|
PageRenderer: PageRenderer,
|
3385
4491
|
PageSnapshot: PageSnapshot,
|
3386
4492
|
FrameRenderer: FrameRenderer,
|
4493
|
+
fetch: fetchWithTurboHeaders,
|
3387
4494
|
start: start,
|
3388
4495
|
registerAdapter: registerAdapter,
|
3389
4496
|
visit: visit,
|
@@ -3393,28 +4500,20 @@ var Turbo = Object.freeze({
|
|
3393
4500
|
clearCache: clearCache,
|
3394
4501
|
setProgressBarDelay: setProgressBarDelay,
|
3395
4502
|
setConfirmMethod: setConfirmMethod,
|
3396
|
-
setFormMode: setFormMode
|
3397
|
-
StreamActions: StreamActions
|
4503
|
+
setFormMode: setFormMode
|
3398
4504
|
});
|
3399
4505
|
|
3400
4506
|
class TurboFrameMissingError extends Error {}
|
3401
4507
|
|
3402
4508
|
class FrameController {
|
4509
|
+
fetchResponseLoaded=_fetchResponse => Promise.resolve();
|
4510
|
+
#currentFetchRequest=null;
|
4511
|
+
#resolveVisitPromise=() => {};
|
4512
|
+
#connected=false;
|
4513
|
+
#hasBeenLoaded=false;
|
4514
|
+
#ignoredAttributes=new Set;
|
4515
|
+
action=null;
|
3403
4516
|
constructor(element) {
|
3404
|
-
this.fetchResponseLoaded = _fetchResponse => {};
|
3405
|
-
this.currentFetchRequest = null;
|
3406
|
-
this.resolveVisitPromise = () => {};
|
3407
|
-
this.connected = false;
|
3408
|
-
this.hasBeenLoaded = false;
|
3409
|
-
this.ignoredAttributes = new Set;
|
3410
|
-
this.action = null;
|
3411
|
-
this.visitCachedSnapshot = ({element: element}) => {
|
3412
|
-
const frame = element.querySelector("#" + this.element.id);
|
3413
|
-
if (frame && this.previousFrameElement) {
|
3414
|
-
frame.replaceChildren(...this.previousFrameElement.children);
|
3415
|
-
}
|
3416
|
-
delete this.previousFrameElement;
|
3417
|
-
};
|
3418
4517
|
this.element = element;
|
3419
4518
|
this.view = new FrameView(this, this.element);
|
3420
4519
|
this.appearanceObserver = new AppearanceObserver(this, this.element);
|
@@ -3424,12 +4523,12 @@ class FrameController {
|
|
3424
4523
|
this.formSubmitObserver = new FormSubmitObserver(this, this.element);
|
3425
4524
|
}
|
3426
4525
|
connect() {
|
3427
|
-
if (!this
|
3428
|
-
this
|
4526
|
+
if (!this.#connected) {
|
4527
|
+
this.#connected = true;
|
3429
4528
|
if (this.loadingStyle == FrameLoadingStyle.lazy) {
|
3430
4529
|
this.appearanceObserver.start();
|
3431
4530
|
} else {
|
3432
|
-
this
|
4531
|
+
this.#loadSourceURL();
|
3433
4532
|
}
|
3434
4533
|
this.formLinkClickObserver.start();
|
3435
4534
|
this.linkInterceptor.start();
|
@@ -3437,8 +4536,8 @@ class FrameController {
|
|
3437
4536
|
}
|
3438
4537
|
}
|
3439
4538
|
disconnect() {
|
3440
|
-
if (this
|
3441
|
-
this
|
4539
|
+
if (this.#connected) {
|
4540
|
+
this.#connected = false;
|
3442
4541
|
this.appearanceObserver.stop();
|
3443
4542
|
this.formLinkClickObserver.stop();
|
3444
4543
|
this.linkInterceptor.stop();
|
@@ -3447,21 +4546,21 @@ class FrameController {
|
|
3447
4546
|
}
|
3448
4547
|
disabledChanged() {
|
3449
4548
|
if (this.loadingStyle == FrameLoadingStyle.eager) {
|
3450
|
-
this
|
4549
|
+
this.#loadSourceURL();
|
3451
4550
|
}
|
3452
4551
|
}
|
3453
4552
|
sourceURLChanged() {
|
3454
|
-
if (this
|
4553
|
+
if (this.#isIgnoringChangesTo("src")) return;
|
3455
4554
|
if (this.element.isConnected) {
|
3456
4555
|
this.complete = false;
|
3457
4556
|
}
|
3458
|
-
if (this.loadingStyle == FrameLoadingStyle.eager || this
|
3459
|
-
this
|
4557
|
+
if (this.loadingStyle == FrameLoadingStyle.eager || this.#hasBeenLoaded) {
|
4558
|
+
this.#loadSourceURL();
|
3460
4559
|
}
|
3461
4560
|
}
|
3462
4561
|
sourceURLReloaded() {
|
3463
4562
|
const {src: src} = this.element;
|
3464
|
-
this
|
4563
|
+
this.#ignoringChangesToAttribute("complete", (() => {
|
3465
4564
|
this.element.removeAttribute("complete");
|
3466
4565
|
}));
|
3467
4566
|
this.element.src = null;
|
@@ -3469,23 +4568,23 @@ class FrameController {
|
|
3469
4568
|
return this.element.loaded;
|
3470
4569
|
}
|
3471
4570
|
completeChanged() {
|
3472
|
-
if (this
|
3473
|
-
this
|
4571
|
+
if (this.#isIgnoringChangesTo("complete")) return;
|
4572
|
+
this.#loadSourceURL();
|
3474
4573
|
}
|
3475
4574
|
loadingStyleChanged() {
|
3476
4575
|
if (this.loadingStyle == FrameLoadingStyle.lazy) {
|
3477
4576
|
this.appearanceObserver.start();
|
3478
4577
|
} else {
|
3479
4578
|
this.appearanceObserver.stop();
|
3480
|
-
this
|
4579
|
+
this.#loadSourceURL();
|
3481
4580
|
}
|
3482
4581
|
}
|
3483
|
-
async loadSourceURL() {
|
4582
|
+
async #loadSourceURL() {
|
3484
4583
|
if (this.enabled && this.isActive && !this.complete && this.sourceURL) {
|
3485
|
-
this.element.loaded = this
|
4584
|
+
this.element.loaded = this.#visit(expandURL(this.sourceURL));
|
3486
4585
|
this.appearanceObserver.stop();
|
3487
4586
|
await this.element.loaded;
|
3488
|
-
this
|
4587
|
+
this.#hasBeenLoaded = true;
|
3489
4588
|
}
|
3490
4589
|
}
|
3491
4590
|
async loadResponse(fetchResponse) {
|
@@ -3498,34 +4597,34 @@ class FrameController {
|
|
3498
4597
|
const document = parseHTMLDocument(html);
|
3499
4598
|
const pageSnapshot = PageSnapshot.fromDocument(document);
|
3500
4599
|
if (pageSnapshot.isVisitable) {
|
3501
|
-
await this
|
4600
|
+
await this.#loadFrameResponse(fetchResponse, document);
|
3502
4601
|
} else {
|
3503
|
-
await this
|
4602
|
+
await this.#handleUnvisitableFrameResponse(fetchResponse);
|
3504
4603
|
}
|
3505
4604
|
}
|
3506
4605
|
} finally {
|
3507
|
-
this.fetchResponseLoaded = () =>
|
4606
|
+
this.fetchResponseLoaded = () => Promise.resolve();
|
3508
4607
|
}
|
3509
4608
|
}
|
3510
4609
|
elementAppearedInViewport(element) {
|
3511
|
-
this.proposeVisitIfNavigatedWithAction(element, element);
|
3512
|
-
this
|
4610
|
+
this.proposeVisitIfNavigatedWithAction(element, getVisitAction(element));
|
4611
|
+
this.#loadSourceURL();
|
3513
4612
|
}
|
3514
4613
|
willSubmitFormLinkToLocation(link) {
|
3515
|
-
return this
|
4614
|
+
return this.#shouldInterceptNavigation(link);
|
3516
4615
|
}
|
3517
4616
|
submittedFormLinkToLocation(link, _location, form) {
|
3518
|
-
const frame = this
|
4617
|
+
const frame = this.#findFrameElement(link);
|
3519
4618
|
if (frame) form.setAttribute("data-turbo-frame", frame.id);
|
3520
4619
|
}
|
3521
4620
|
shouldInterceptLinkClick(element, _location, _event) {
|
3522
|
-
return this
|
4621
|
+
return this.#shouldInterceptNavigation(element);
|
3523
4622
|
}
|
3524
4623
|
linkClickIntercepted(element, location) {
|
3525
|
-
this
|
4624
|
+
this.#navigateFrame(element, location);
|
3526
4625
|
}
|
3527
4626
|
willSubmitForm(element, submitter) {
|
3528
|
-
return element.closest("turbo-frame") == this.element && this
|
4627
|
+
return element.closest("turbo-frame") == this.element && this.#shouldInterceptNavigation(element, submitter);
|
3529
4628
|
}
|
3530
4629
|
formSubmitted(element, submitter) {
|
3531
4630
|
if (this.formSubmission) {
|
@@ -3537,9 +4636,8 @@ class FrameController {
|
|
3537
4636
|
this.formSubmission.start();
|
3538
4637
|
}
|
3539
4638
|
prepareRequest(request) {
|
3540
|
-
var _a;
|
3541
4639
|
request.headers["Turbo-Frame"] = this.id;
|
3542
|
-
if (
|
4640
|
+
if (this.currentNavigationElement?.hasAttribute("data-turbo-stream")) {
|
3543
4641
|
request.acceptResponseType(StreamMessage.contentType);
|
3544
4642
|
}
|
3545
4643
|
}
|
@@ -3547,29 +4645,29 @@ class FrameController {
|
|
3547
4645
|
markAsBusy(this.element);
|
3548
4646
|
}
|
3549
4647
|
requestPreventedHandlingResponse(_request, _response) {
|
3550
|
-
this
|
4648
|
+
this.#resolveVisitPromise();
|
3551
4649
|
}
|
3552
4650
|
async requestSucceededWithResponse(request, response) {
|
3553
4651
|
await this.loadResponse(response);
|
3554
|
-
this
|
4652
|
+
this.#resolveVisitPromise();
|
3555
4653
|
}
|
3556
4654
|
async requestFailedWithResponse(request, response) {
|
3557
4655
|
await this.loadResponse(response);
|
3558
|
-
this
|
4656
|
+
this.#resolveVisitPromise();
|
3559
4657
|
}
|
3560
4658
|
requestErrored(request, error) {
|
3561
4659
|
console.error(error);
|
3562
|
-
this
|
4660
|
+
this.#resolveVisitPromise();
|
3563
4661
|
}
|
3564
4662
|
requestFinished(_request) {
|
3565
4663
|
clearBusyState(this.element);
|
3566
4664
|
}
|
3567
4665
|
formSubmissionStarted({formElement: formElement}) {
|
3568
|
-
markAsBusy(formElement, this
|
4666
|
+
markAsBusy(formElement, this.#findFrameElement(formElement));
|
3569
4667
|
}
|
3570
4668
|
formSubmissionSucceededWithResponse(formSubmission, response) {
|
3571
|
-
const frame = this
|
3572
|
-
frame.delegate.proposeVisitIfNavigatedWithAction(frame, formSubmission.
|
4669
|
+
const frame = this.#findFrameElement(formSubmission.formElement, formSubmission.submitter);
|
4670
|
+
frame.delegate.proposeVisitIfNavigatedWithAction(frame, getVisitAction(formSubmission.submitter, formSubmission.formElement, frame));
|
3573
4671
|
frame.delegate.loadResponse(response);
|
3574
4672
|
if (!formSubmission.isSafe) {
|
3575
4673
|
session.clearCache();
|
@@ -3583,14 +4681,15 @@ class FrameController {
|
|
3583
4681
|
console.error(error);
|
3584
4682
|
}
|
3585
4683
|
formSubmissionFinished({formElement: formElement}) {
|
3586
|
-
clearBusyState(formElement, this
|
4684
|
+
clearBusyState(formElement, this.#findFrameElement(formElement));
|
3587
4685
|
}
|
3588
4686
|
allowsImmediateRender({element: newFrame}, options) {
|
3589
4687
|
const event = dispatch("turbo:before-frame-render", {
|
3590
4688
|
target: this.element,
|
3591
|
-
detail:
|
3592
|
-
newFrame: newFrame
|
3593
|
-
|
4689
|
+
detail: {
|
4690
|
+
newFrame: newFrame,
|
4691
|
+
...options
|
4692
|
+
},
|
3594
4693
|
cancelable: true
|
3595
4694
|
});
|
3596
4695
|
const {defaultPrevented: defaultPrevented, detail: {render: render}} = event;
|
@@ -3599,7 +4698,7 @@ class FrameController {
|
|
3599
4698
|
}
|
3600
4699
|
return !defaultPrevented;
|
3601
4700
|
}
|
3602
|
-
viewRenderedSnapshot(_snapshot, _isPreview) {}
|
4701
|
+
viewRenderedSnapshot(_snapshot, _isPreview, _renderMethod) {}
|
3603
4702
|
preloadOnLoadLinksForView(element) {
|
3604
4703
|
session.preloadOnLoadLinksForView(element);
|
3605
4704
|
}
|
@@ -3607,7 +4706,14 @@ class FrameController {
|
|
3607
4706
|
willRenderFrame(currentElement, _newElement) {
|
3608
4707
|
this.previousFrameElement = currentElement.cloneNode(true);
|
3609
4708
|
}
|
3610
|
-
|
4709
|
+
visitCachedSnapshot=({element: element}) => {
|
4710
|
+
const frame = element.querySelector("#" + this.element.id);
|
4711
|
+
if (frame && this.previousFrameElement) {
|
4712
|
+
frame.replaceChildren(...this.previousFrameElement.children);
|
4713
|
+
}
|
4714
|
+
delete this.previousFrameElement;
|
4715
|
+
};
|
4716
|
+
async #loadFrameResponse(fetchResponse, document) {
|
3611
4717
|
const newFrameElement = await this.extractForeignFrameElement(document.body);
|
3612
4718
|
if (newFrameElement) {
|
3613
4719
|
const snapshot = new Snapshot(newFrameElement);
|
@@ -3618,41 +4724,40 @@ class FrameController {
|
|
3618
4724
|
this.complete = true;
|
3619
4725
|
session.frameRendered(fetchResponse, this.element);
|
3620
4726
|
session.frameLoaded(this.element);
|
3621
|
-
this.fetchResponseLoaded(fetchResponse);
|
3622
|
-
} else if (this
|
3623
|
-
this
|
4727
|
+
await this.fetchResponseLoaded(fetchResponse);
|
4728
|
+
} else if (this.#willHandleFrameMissingFromResponse(fetchResponse)) {
|
4729
|
+
this.#handleFrameMissingFromResponse(fetchResponse);
|
3624
4730
|
}
|
3625
4731
|
}
|
3626
|
-
async visit(url) {
|
3627
|
-
var _a;
|
4732
|
+
async #visit(url) {
|
3628
4733
|
const request = new FetchRequest(this, FetchMethod.get, url, new URLSearchParams, this.element);
|
3629
|
-
|
3630
|
-
this
|
4734
|
+
this.#currentFetchRequest?.cancel();
|
4735
|
+
this.#currentFetchRequest = request;
|
3631
4736
|
return new Promise((resolve => {
|
3632
|
-
this
|
3633
|
-
this
|
3634
|
-
this
|
4737
|
+
this.#resolveVisitPromise = () => {
|
4738
|
+
this.#resolveVisitPromise = () => {};
|
4739
|
+
this.#currentFetchRequest = null;
|
3635
4740
|
resolve();
|
3636
4741
|
};
|
3637
4742
|
request.perform();
|
3638
4743
|
}));
|
3639
4744
|
}
|
3640
|
-
navigateFrame(element, url, submitter) {
|
3641
|
-
const frame = this
|
3642
|
-
frame.delegate.proposeVisitIfNavigatedWithAction(frame, element,
|
3643
|
-
this
|
4745
|
+
#navigateFrame(element, url, submitter) {
|
4746
|
+
const frame = this.#findFrameElement(element, submitter);
|
4747
|
+
frame.delegate.proposeVisitIfNavigatedWithAction(frame, getVisitAction(submitter, element, frame));
|
4748
|
+
this.#withCurrentNavigationElement(element, (() => {
|
3644
4749
|
frame.src = url;
|
3645
4750
|
}));
|
3646
4751
|
}
|
3647
|
-
proposeVisitIfNavigatedWithAction(frame,
|
3648
|
-
this.action =
|
4752
|
+
proposeVisitIfNavigatedWithAction(frame, action = null) {
|
4753
|
+
this.action = action;
|
3649
4754
|
if (this.action) {
|
3650
4755
|
const pageSnapshot = PageSnapshot.fromElement(frame).clone();
|
3651
4756
|
const {visitCachedSnapshot: visitCachedSnapshot} = frame.delegate;
|
3652
|
-
frame.delegate.fetchResponseLoaded = fetchResponse => {
|
4757
|
+
frame.delegate.fetchResponseLoaded = async fetchResponse => {
|
3653
4758
|
if (frame.src) {
|
3654
4759
|
const {statusCode: statusCode, redirected: redirected} = fetchResponse;
|
3655
|
-
const responseHTML =
|
4760
|
+
const responseHTML = await fetchResponse.responseHTML;
|
3656
4761
|
const response = {
|
3657
4762
|
statusCode: statusCode,
|
3658
4763
|
redirected: redirected,
|
@@ -3678,16 +4783,16 @@ class FrameController {
|
|
3678
4783
|
session.history.update(method, expandURL(this.element.src || ""), this.restorationIdentifier);
|
3679
4784
|
}
|
3680
4785
|
}
|
3681
|
-
async handleUnvisitableFrameResponse(fetchResponse) {
|
4786
|
+
async #handleUnvisitableFrameResponse(fetchResponse) {
|
3682
4787
|
console.warn(`The response (${fetchResponse.statusCode}) from <turbo-frame id="${this.element.id}"> is performing a full page visit due to turbo-visit-control.`);
|
3683
|
-
await this
|
4788
|
+
await this.#visitResponse(fetchResponse.response);
|
3684
4789
|
}
|
3685
|
-
willHandleFrameMissingFromResponse(fetchResponse) {
|
4790
|
+
#willHandleFrameMissingFromResponse(fetchResponse) {
|
3686
4791
|
this.element.setAttribute("complete", "");
|
3687
4792
|
const response = fetchResponse.response;
|
3688
|
-
const visit = async (url, options
|
4793
|
+
const visit = async (url, options) => {
|
3689
4794
|
if (url instanceof Response) {
|
3690
|
-
this
|
4795
|
+
this.#visitResponse(url);
|
3691
4796
|
} else {
|
3692
4797
|
session.visit(url, options);
|
3693
4798
|
}
|
@@ -3702,15 +4807,15 @@ class FrameController {
|
|
3702
4807
|
});
|
3703
4808
|
return !event.defaultPrevented;
|
3704
4809
|
}
|
3705
|
-
handleFrameMissingFromResponse(fetchResponse) {
|
4810
|
+
#handleFrameMissingFromResponse(fetchResponse) {
|
3706
4811
|
this.view.missing();
|
3707
|
-
this
|
4812
|
+
this.#throwFrameMissingError(fetchResponse);
|
3708
4813
|
}
|
3709
|
-
throwFrameMissingError(fetchResponse) {
|
4814
|
+
#throwFrameMissingError(fetchResponse) {
|
3710
4815
|
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.`;
|
3711
4816
|
throw new TurboFrameMissingError(message);
|
3712
4817
|
}
|
3713
|
-
async visitResponse(response) {
|
4818
|
+
async #visitResponse(response) {
|
3714
4819
|
const wrapped = new FetchResponse(response);
|
3715
4820
|
const responseHTML = await wrapped.responseHTML;
|
3716
4821
|
const {location: location, redirected: redirected, statusCode: statusCode} = wrapped;
|
@@ -3722,10 +4827,9 @@ class FrameController {
|
|
3722
4827
|
}
|
3723
4828
|
});
|
3724
4829
|
}
|
3725
|
-
findFrameElement(element, submitter) {
|
3726
|
-
var _a;
|
4830
|
+
#findFrameElement(element, submitter) {
|
3727
4831
|
const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
|
3728
|
-
return
|
4832
|
+
return getFrameElementById(id) ?? this.element;
|
3729
4833
|
}
|
3730
4834
|
async extractForeignFrameElement(container) {
|
3731
4835
|
let element;
|
@@ -3746,13 +4850,13 @@ class FrameController {
|
|
3746
4850
|
}
|
3747
4851
|
return null;
|
3748
4852
|
}
|
3749
|
-
formActionIsVisitable(form, submitter) {
|
3750
|
-
const action = getAction(form, submitter);
|
4853
|
+
#formActionIsVisitable(form, submitter) {
|
4854
|
+
const action = getAction$1(form, submitter);
|
3751
4855
|
return locationIsVisitable(expandURL(action), this.rootLocation);
|
3752
4856
|
}
|
3753
|
-
shouldInterceptNavigation(element, submitter) {
|
4857
|
+
#shouldInterceptNavigation(element, submitter) {
|
3754
4858
|
const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
|
3755
|
-
if (element instanceof HTMLFormElement && !this
|
4859
|
+
if (element instanceof HTMLFormElement && !this.#formActionIsVisitable(element, submitter)) {
|
3756
4860
|
return false;
|
3757
4861
|
}
|
3758
4862
|
if (!this.enabled || id == "_top") {
|
@@ -3784,21 +4888,21 @@ class FrameController {
|
|
3784
4888
|
}
|
3785
4889
|
}
|
3786
4890
|
set sourceURL(sourceURL) {
|
3787
|
-
this
|
3788
|
-
this.element.src = sourceURL
|
4891
|
+
this.#ignoringChangesToAttribute("src", (() => {
|
4892
|
+
this.element.src = sourceURL ?? null;
|
3789
4893
|
}));
|
3790
4894
|
}
|
3791
4895
|
get loadingStyle() {
|
3792
4896
|
return this.element.loading;
|
3793
4897
|
}
|
3794
4898
|
get isLoading() {
|
3795
|
-
return this.formSubmission !== undefined || this
|
4899
|
+
return this.formSubmission !== undefined || this.#resolveVisitPromise() !== undefined;
|
3796
4900
|
}
|
3797
4901
|
get complete() {
|
3798
4902
|
return this.element.hasAttribute("complete");
|
3799
4903
|
}
|
3800
4904
|
set complete(value) {
|
3801
|
-
this
|
4905
|
+
this.#ignoringChangesToAttribute("complete", (() => {
|
3802
4906
|
if (value) {
|
3803
4907
|
this.element.setAttribute("complete", "");
|
3804
4908
|
} else {
|
@@ -3807,23 +4911,22 @@ class FrameController {
|
|
3807
4911
|
}));
|
3808
4912
|
}
|
3809
4913
|
get isActive() {
|
3810
|
-
return this.element.isActive && this
|
4914
|
+
return this.element.isActive && this.#connected;
|
3811
4915
|
}
|
3812
4916
|
get rootLocation() {
|
3813
|
-
var _a;
|
3814
4917
|
const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
|
3815
|
-
const root =
|
4918
|
+
const root = meta?.content ?? "/";
|
3816
4919
|
return expandURL(root);
|
3817
4920
|
}
|
3818
|
-
isIgnoringChangesTo(attributeName) {
|
3819
|
-
return this
|
4921
|
+
#isIgnoringChangesTo(attributeName) {
|
4922
|
+
return this.#ignoredAttributes.has(attributeName);
|
3820
4923
|
}
|
3821
|
-
ignoringChangesToAttribute(attributeName, callback) {
|
3822
|
-
this
|
4924
|
+
#ignoringChangesToAttribute(attributeName, callback) {
|
4925
|
+
this.#ignoredAttributes.add(attributeName);
|
3823
4926
|
callback();
|
3824
|
-
this
|
4927
|
+
this.#ignoredAttributes.delete(attributeName);
|
3825
4928
|
}
|
3826
|
-
withCurrentNavigationElement(element, callback) {
|
4929
|
+
#withCurrentNavigationElement(element, callback) {
|
3827
4930
|
this.currentNavigationElement = element;
|
3828
4931
|
callback();
|
3829
4932
|
delete this.currentNavigationElement;
|
@@ -3856,6 +4959,38 @@ function activateElement(element, currentURL) {
|
|
3856
4959
|
}
|
3857
4960
|
}
|
3858
4961
|
|
4962
|
+
const StreamActions = {
|
4963
|
+
after() {
|
4964
|
+
this.targetElements.forEach((e => e.parentElement?.insertBefore(this.templateContent, e.nextSibling)));
|
4965
|
+
},
|
4966
|
+
append() {
|
4967
|
+
this.removeDuplicateTargetChildren();
|
4968
|
+
this.targetElements.forEach((e => e.append(this.templateContent)));
|
4969
|
+
},
|
4970
|
+
before() {
|
4971
|
+
this.targetElements.forEach((e => e.parentElement?.insertBefore(this.templateContent, e)));
|
4972
|
+
},
|
4973
|
+
prepend() {
|
4974
|
+
this.removeDuplicateTargetChildren();
|
4975
|
+
this.targetElements.forEach((e => e.prepend(this.templateContent)));
|
4976
|
+
},
|
4977
|
+
remove() {
|
4978
|
+
this.targetElements.forEach((e => e.remove()));
|
4979
|
+
},
|
4980
|
+
replace() {
|
4981
|
+
this.targetElements.forEach((e => e.replaceWith(this.templateContent)));
|
4982
|
+
},
|
4983
|
+
update() {
|
4984
|
+
this.targetElements.forEach((targetElement => {
|
4985
|
+
targetElement.innerHTML = "";
|
4986
|
+
targetElement.append(this.templateContent);
|
4987
|
+
}));
|
4988
|
+
},
|
4989
|
+
refresh() {
|
4990
|
+
session.refresh(this.baseURI, this.requestId);
|
4991
|
+
}
|
4992
|
+
};
|
4993
|
+
|
3859
4994
|
class StreamElement extends HTMLElement {
|
3860
4995
|
static async renderElement(newElement) {
|
3861
4996
|
await newElement.performAction();
|
@@ -3870,11 +5005,10 @@ class StreamElement extends HTMLElement {
|
|
3870
5005
|
}
|
3871
5006
|
}
|
3872
5007
|
async render() {
|
3873
|
-
|
3874
|
-
return (_a = this.renderPromise) !== null && _a !== void 0 ? _a : this.renderPromise = (async () => {
|
5008
|
+
return this.renderPromise ??= (async () => {
|
3875
5009
|
const event = this.beforeRenderEvent;
|
3876
5010
|
if (this.dispatchEvent(event)) {
|
3877
|
-
await
|
5011
|
+
await nextRepaint();
|
3878
5012
|
await event.detail.render(this);
|
3879
5013
|
}
|
3880
5014
|
})();
|
@@ -3882,15 +5016,14 @@ class StreamElement extends HTMLElement {
|
|
3882
5016
|
disconnect() {
|
3883
5017
|
try {
|
3884
5018
|
this.remove();
|
3885
|
-
} catch
|
5019
|
+
} catch {}
|
3886
5020
|
}
|
3887
5021
|
removeDuplicateTargetChildren() {
|
3888
5022
|
this.duplicateChildren.forEach((c => c.remove()));
|
3889
5023
|
}
|
3890
5024
|
get duplicateChildren() {
|
3891
|
-
var _a;
|
3892
5025
|
const existingChildren = this.targetElements.flatMap((e => [ ...e.children ])).filter((c => !!c.id));
|
3893
|
-
const newChildrenIds = [ ...
|
5026
|
+
const newChildrenIds = [ ...this.templateContent?.children || [] ].filter((c => !!c.id)).map((c => c.id));
|
3894
5027
|
return existingChildren.filter((c => newChildrenIds.includes(c.id)));
|
3895
5028
|
}
|
3896
5029
|
get performAction() {
|
@@ -3899,9 +5032,9 @@ class StreamElement extends HTMLElement {
|
|
3899
5032
|
if (actionFunction) {
|
3900
5033
|
return actionFunction;
|
3901
5034
|
}
|
3902
|
-
this
|
5035
|
+
this.#raise("unknown action");
|
3903
5036
|
}
|
3904
|
-
this
|
5037
|
+
this.#raise("action attribute is missing");
|
3905
5038
|
}
|
3906
5039
|
get targetElements() {
|
3907
5040
|
if (this.target) {
|
@@ -3909,7 +5042,7 @@ class StreamElement extends HTMLElement {
|
|
3909
5042
|
} else if (this.targets) {
|
3910
5043
|
return this.targetElementsByQuery;
|
3911
5044
|
} else {
|
3912
|
-
this
|
5045
|
+
this.#raise("target or targets attribute is missing");
|
3913
5046
|
}
|
3914
5047
|
}
|
3915
5048
|
get templateContent() {
|
@@ -3923,7 +5056,7 @@ class StreamElement extends HTMLElement {
|
|
3923
5056
|
} else if (this.firstElementChild instanceof HTMLTemplateElement) {
|
3924
5057
|
return this.firstElementChild;
|
3925
5058
|
}
|
3926
|
-
this
|
5059
|
+
this.#raise("first child element must be a <template> element");
|
3927
5060
|
}
|
3928
5061
|
get action() {
|
3929
5062
|
return this.getAttribute("action");
|
@@ -3934,12 +5067,14 @@ class StreamElement extends HTMLElement {
|
|
3934
5067
|
get targets() {
|
3935
5068
|
return this.getAttribute("targets");
|
3936
5069
|
}
|
3937
|
-
|
5070
|
+
get requestId() {
|
5071
|
+
return this.getAttribute("request-id");
|
5072
|
+
}
|
5073
|
+
#raise(message) {
|
3938
5074
|
throw new Error(`${this.description}: ${message}`);
|
3939
5075
|
}
|
3940
5076
|
get description() {
|
3941
|
-
|
3942
|
-
return (_b = ((_a = this.outerHTML.match(/<[^>]+>/)) !== null && _a !== void 0 ? _a : [])[0]) !== null && _b !== void 0 ? _b : "<turbo-stream>";
|
5077
|
+
return (this.outerHTML.match(/<[^>]+>/) ?? [])[0] ?? "<turbo-stream>";
|
3943
5078
|
}
|
3944
5079
|
get beforeRenderEvent() {
|
3945
5080
|
return new CustomEvent("turbo:before-stream-render", {
|
@@ -3952,8 +5087,7 @@ class StreamElement extends HTMLElement {
|
|
3952
5087
|
});
|
3953
5088
|
}
|
3954
5089
|
get targetElementsById() {
|
3955
|
-
|
3956
|
-
const element = (_a = this.ownerDocument) === null || _a === void 0 ? void 0 : _a.getElementById(this.target);
|
5090
|
+
const element = this.ownerDocument?.getElementById(this.target);
|
3957
5091
|
if (element !== null) {
|
3958
5092
|
return [ element ];
|
3959
5093
|
} else {
|
@@ -3961,8 +5095,7 @@ class StreamElement extends HTMLElement {
|
|
3961
5095
|
}
|
3962
5096
|
}
|
3963
5097
|
get targetElementsByQuery() {
|
3964
|
-
|
3965
|
-
const elements = (_a = this.ownerDocument) === null || _a === void 0 ? void 0 : _a.querySelectorAll(this.targets);
|
5098
|
+
const elements = this.ownerDocument?.querySelectorAll(this.targets);
|
3966
5099
|
if (elements.length !== 0) {
|
3967
5100
|
return Array.prototype.slice.call(elements);
|
3968
5101
|
} else {
|
@@ -3972,16 +5105,14 @@ class StreamElement extends HTMLElement {
|
|
3972
5105
|
}
|
3973
5106
|
|
3974
5107
|
class StreamSourceElement extends HTMLElement {
|
3975
|
-
|
3976
|
-
super(...arguments);
|
3977
|
-
this.streamSource = null;
|
3978
|
-
}
|
5108
|
+
streamSource=null;
|
3979
5109
|
connectedCallback() {
|
3980
5110
|
this.streamSource = this.src.match(/^ws{1,2}:/) ? new WebSocket(this.src) : new EventSource(this.src);
|
3981
5111
|
connectStreamSource(this.streamSource);
|
3982
5112
|
}
|
3983
5113
|
disconnectedCallback() {
|
3984
5114
|
if (this.streamSource) {
|
5115
|
+
this.streamSource.close();
|
3985
5116
|
disconnectStreamSource(this.streamSource);
|
3986
5117
|
}
|
3987
5118
|
}
|
@@ -4026,16 +5157,21 @@ if (customElements.get("turbo-stream-source") === undefined) {
|
|
4026
5157
|
}
|
4027
5158
|
})();
|
4028
5159
|
|
4029
|
-
window.Turbo =
|
5160
|
+
window.Turbo = {
|
5161
|
+
...Turbo,
|
5162
|
+
StreamActions: StreamActions
|
5163
|
+
};
|
4030
5164
|
|
4031
5165
|
start();
|
4032
5166
|
|
4033
|
-
var
|
5167
|
+
var Turbo$1 = Object.freeze({
|
4034
5168
|
__proto__: null,
|
5169
|
+
FetchEnctype: FetchEnctype,
|
5170
|
+
FetchMethod: FetchMethod,
|
5171
|
+
FetchRequest: FetchRequest,
|
5172
|
+
FetchResponse: FetchResponse,
|
4035
5173
|
FrameElement: FrameElement,
|
4036
|
-
|
4037
|
-
return FrameLoadingStyle;
|
4038
|
-
},
|
5174
|
+
FrameLoadingStyle: FrameLoadingStyle,
|
4039
5175
|
FrameRenderer: FrameRenderer,
|
4040
5176
|
PageRenderer: PageRenderer,
|
4041
5177
|
PageSnapshot: PageSnapshot,
|
@@ -4046,6 +5182,10 @@ var turbo_es2017Esm = Object.freeze({
|
|
4046
5182
|
clearCache: clearCache,
|
4047
5183
|
connectStreamSource: connectStreamSource,
|
4048
5184
|
disconnectStreamSource: disconnectStreamSource,
|
5185
|
+
fetch: fetchWithTurboHeaders,
|
5186
|
+
fetchEnctypeFromString: fetchEnctypeFromString,
|
5187
|
+
fetchMethodFromString: fetchMethodFromString,
|
5188
|
+
isSafe: isSafe,
|
4049
5189
|
navigator: navigator$1,
|
4050
5190
|
registerAdapter: registerAdapter,
|
4051
5191
|
renderStreamMessage: renderStreamMessage,
|
@@ -4060,14 +5200,14 @@ var turbo_es2017Esm = Object.freeze({
|
|
4060
5200
|
let consumer;
|
4061
5201
|
|
4062
5202
|
async function getConsumer() {
|
4063
|
-
return consumer || setConsumer(createConsumer().then(setConsumer));
|
5203
|
+
return consumer || setConsumer(createConsumer$1().then(setConsumer));
|
4064
5204
|
}
|
4065
5205
|
|
4066
5206
|
function setConsumer(newConsumer) {
|
4067
5207
|
return consumer = newConsumer;
|
4068
5208
|
}
|
4069
5209
|
|
4070
|
-
async function createConsumer() {
|
5210
|
+
async function createConsumer$1() {
|
4071
5211
|
const {createConsumer: createConsumer} = await Promise.resolve().then((function() {
|
4072
5212
|
return index;
|
4073
5213
|
}));
|
@@ -4083,7 +5223,7 @@ var cable = Object.freeze({
|
|
4083
5223
|
__proto__: null,
|
4084
5224
|
getConsumer: getConsumer,
|
4085
5225
|
setConsumer: setConsumer,
|
4086
|
-
createConsumer: createConsumer,
|
5226
|
+
createConsumer: createConsumer$1,
|
4087
5227
|
subscribeTo: subscribeTo
|
4088
5228
|
});
|
4089
5229
|
|
@@ -4193,6 +5333,8 @@ function isBodyInit(body) {
|
|
4193
5333
|
return body instanceof FormData || body instanceof URLSearchParams;
|
4194
5334
|
}
|
4195
5335
|
|
5336
|
+
window.Turbo = Turbo$1;
|
5337
|
+
|
4196
5338
|
addEventListener("turbo:before-fetch-request", encodeMethodIntoRequestBody);
|
4197
5339
|
|
4198
5340
|
var adapters = {
|
@@ -4309,6 +5451,8 @@ ConnectionMonitor.staleThreshold = 6;
|
|
4309
5451
|
|
4310
5452
|
ConnectionMonitor.reconnectionBackoffRate = .15;
|
4311
5453
|
|
5454
|
+
var ConnectionMonitor$1 = ConnectionMonitor;
|
5455
|
+
|
4312
5456
|
var INTERNAL = {
|
4313
5457
|
message_types: {
|
4314
5458
|
welcome: "welcome",
|
@@ -4320,7 +5464,8 @@ var INTERNAL = {
|
|
4320
5464
|
disconnect_reasons: {
|
4321
5465
|
unauthorized: "unauthorized",
|
4322
5466
|
invalid_request: "invalid_request",
|
4323
|
-
server_restart: "server_restart"
|
5467
|
+
server_restart: "server_restart",
|
5468
|
+
remote: "remote"
|
4324
5469
|
},
|
4325
5470
|
default_mount_path: "/cable",
|
4326
5471
|
protocols: [ "actioncable-v1-json", "actioncable-unsupported" ]
|
@@ -4337,7 +5482,7 @@ class Connection {
|
|
4337
5482
|
this.open = this.open.bind(this);
|
4338
5483
|
this.consumer = consumer;
|
4339
5484
|
this.subscriptions = this.consumer.subscriptions;
|
4340
|
-
this.monitor = new ConnectionMonitor(this);
|
5485
|
+
this.monitor = new ConnectionMonitor$1(this);
|
4341
5486
|
this.disconnected = true;
|
4342
5487
|
}
|
4343
5488
|
send(data) {
|
@@ -4353,11 +5498,12 @@ class Connection {
|
|
4353
5498
|
logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`);
|
4354
5499
|
return false;
|
4355
5500
|
} else {
|
4356
|
-
|
5501
|
+
const socketProtocols = [ ...protocols, ...this.consumer.subprotocols || [] ];
|
5502
|
+
logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${socketProtocols}`);
|
4357
5503
|
if (this.webSocket) {
|
4358
5504
|
this.uninstallEventHandlers();
|
4359
5505
|
}
|
4360
|
-
this.webSocket = new adapters.WebSocket(this.consumer.url,
|
5506
|
+
this.webSocket = new adapters.WebSocket(this.consumer.url, socketProtocols);
|
4361
5507
|
this.installEventHandlers();
|
4362
5508
|
this.monitor.start();
|
4363
5509
|
return true;
|
@@ -4369,7 +5515,7 @@ class Connection {
|
|
4369
5515
|
if (!allowReconnect) {
|
4370
5516
|
this.monitor.stop();
|
4371
5517
|
}
|
4372
|
-
if (this.
|
5518
|
+
if (this.isOpen()) {
|
4373
5519
|
return this.webSocket.close();
|
4374
5520
|
}
|
4375
5521
|
}
|
@@ -4399,6 +5545,9 @@ class Connection {
|
|
4399
5545
|
isActive() {
|
4400
5546
|
return this.isState("open", "connecting");
|
4401
5547
|
}
|
5548
|
+
triedToReconnect() {
|
5549
|
+
return this.monitor.reconnectAttempts > 0;
|
5550
|
+
}
|
4402
5551
|
isProtocolSupported() {
|
4403
5552
|
return indexOf.call(supportedProtocols, this.getProtocol()) >= 0;
|
4404
5553
|
}
|
@@ -4438,6 +5587,9 @@ Connection.prototype.events = {
|
|
4438
5587
|
const {identifier: identifier, message: message, reason: reason, reconnect: reconnect, type: type} = JSON.parse(event.data);
|
4439
5588
|
switch (type) {
|
4440
5589
|
case message_types.welcome:
|
5590
|
+
if (this.triedToReconnect()) {
|
5591
|
+
this.reconnectAttempted = true;
|
5592
|
+
}
|
4441
5593
|
this.monitor.recordConnect();
|
4442
5594
|
return this.subscriptions.reload();
|
4443
5595
|
|
@@ -4452,7 +5604,16 @@ Connection.prototype.events = {
|
|
4452
5604
|
|
4453
5605
|
case message_types.confirmation:
|
4454
5606
|
this.subscriptions.confirmSubscription(identifier);
|
4455
|
-
|
5607
|
+
if (this.reconnectAttempted) {
|
5608
|
+
this.reconnectAttempted = false;
|
5609
|
+
return this.subscriptions.notify(identifier, "connected", {
|
5610
|
+
reconnected: true
|
5611
|
+
});
|
5612
|
+
} else {
|
5613
|
+
return this.subscriptions.notify(identifier, "connected", {
|
5614
|
+
reconnected: false
|
5615
|
+
});
|
5616
|
+
}
|
4456
5617
|
|
4457
5618
|
case message_types.rejection:
|
4458
5619
|
return this.subscriptions.reject(identifier);
|
@@ -4487,6 +5648,8 @@ Connection.prototype.events = {
|
|
4487
5648
|
}
|
4488
5649
|
};
|
4489
5650
|
|
5651
|
+
var Connection$1 = Connection;
|
5652
|
+
|
4490
5653
|
const extend = function(object, properties) {
|
4491
5654
|
if (properties != null) {
|
4492
5655
|
for (let key in properties) {
|
@@ -4556,10 +5719,12 @@ class SubscriptionGuarantor {
|
|
4556
5719
|
}
|
4557
5720
|
}
|
4558
5721
|
|
5722
|
+
var SubscriptionGuarantor$1 = SubscriptionGuarantor;
|
5723
|
+
|
4559
5724
|
class Subscriptions {
|
4560
5725
|
constructor(consumer) {
|
4561
5726
|
this.consumer = consumer;
|
4562
|
-
this.guarantor = new SubscriptionGuarantor(this);
|
5727
|
+
this.guarantor = new SubscriptionGuarantor$1(this);
|
4563
5728
|
this.subscriptions = [];
|
4564
5729
|
}
|
4565
5730
|
create(channelName, mixin) {
|
@@ -4636,7 +5801,8 @@ class Consumer {
|
|
4636
5801
|
constructor(url) {
|
4637
5802
|
this._url = url;
|
4638
5803
|
this.subscriptions = new Subscriptions(this);
|
4639
|
-
this.connection = new Connection(this);
|
5804
|
+
this.connection = new Connection$1(this);
|
5805
|
+
this.subprotocols = [];
|
4640
5806
|
}
|
4641
5807
|
get url() {
|
4642
5808
|
return createWebSocketURL(this._url);
|
@@ -4657,6 +5823,9 @@ class Consumer {
|
|
4657
5823
|
return this.connection.open();
|
4658
5824
|
}
|
4659
5825
|
}
|
5826
|
+
addSubProtocol(subprotocol) {
|
5827
|
+
this.subprotocols = [ ...this.subprotocols, subprotocol ];
|
5828
|
+
}
|
4660
5829
|
}
|
4661
5830
|
|
4662
5831
|
function createWebSocketURL(url) {
|
@@ -4674,7 +5843,7 @@ function createWebSocketURL(url) {
|
|
4674
5843
|
}
|
4675
5844
|
}
|
4676
5845
|
|
4677
|
-
function createConsumer
|
5846
|
+
function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) {
|
4678
5847
|
return new Consumer(url);
|
4679
5848
|
}
|
4680
5849
|
|
@@ -4687,18 +5856,18 @@ function getConfig(name) {
|
|
4687
5856
|
|
4688
5857
|
var index = Object.freeze({
|
4689
5858
|
__proto__: null,
|
4690
|
-
Connection: Connection,
|
4691
|
-
ConnectionMonitor: ConnectionMonitor,
|
5859
|
+
Connection: Connection$1,
|
5860
|
+
ConnectionMonitor: ConnectionMonitor$1,
|
4692
5861
|
Consumer: Consumer,
|
4693
5862
|
INTERNAL: INTERNAL,
|
4694
5863
|
Subscription: Subscription,
|
4695
5864
|
Subscriptions: Subscriptions,
|
4696
|
-
SubscriptionGuarantor: SubscriptionGuarantor,
|
5865
|
+
SubscriptionGuarantor: SubscriptionGuarantor$1,
|
4697
5866
|
adapters: adapters,
|
4698
5867
|
createWebSocketURL: createWebSocketURL,
|
4699
5868
|
logger: logger,
|
4700
|
-
createConsumer: createConsumer
|
5869
|
+
createConsumer: createConsumer,
|
4701
5870
|
getConfig: getConfig
|
4702
5871
|
});
|
4703
5872
|
|
4704
|
-
export {
|
5873
|
+
export { Turbo$1 as Turbo, cable };
|