turbo-rails 1.5.0 → 2.0.7
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 +67 -16
- data/app/assets/javascripts/turbo.js +1974 -785
- 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 +33 -7
- data/app/channels/turbo/streams_channel.rb +15 -15
- data/app/controllers/concerns/turbo/request_id_tracking.rb +12 -0
- data/app/controllers/turbo/frames/frame_request.rb +2 -2
- data/app/controllers/turbo/native/navigation.rb +6 -3
- data/app/helpers/turbo/drive_helper.rb +72 -14
- data/app/helpers/turbo/frames_helper.rb +8 -8
- data/app/helpers/turbo/streams/action_helper.rb +12 -4
- data/app/helpers/turbo/streams_helper.rb +5 -0
- 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 +184 -35
- 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 +3 -4
- data/lib/install/turbo_with_importmap.rb +1 -1
- data/lib/tasks/turbo_tasks.rake +0 -22
- data/lib/turbo/broadcastable/test_helper.rb +5 -5
- data/lib/turbo/engine.rb +34 -8
- data/lib/turbo/test_assertions/integration_test_assertions.rb +2 -2
- data/lib/turbo/test_assertions.rb +2 -2
- data/lib/turbo/version.rb +1 -1
- data/lib/turbo-rails.rb +10 -0
- metadata +9 -5
- data/lib/install/turbo_needs_redis.rb +0 -20
@@ -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.5
|
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
|
-
return [ "disabled", "
|
75
|
+
return [ "disabled", "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() {
|
@@ -100,11 +90,9 @@ class FrameElement extends HTMLElement {
|
|
100
90
|
attributeChangedCallback(name) {
|
101
91
|
if (name == "loading") {
|
102
92
|
this.delegate.loadingStyleChanged();
|
103
|
-
} else if (name == "complete") {
|
104
|
-
this.delegate.completeChanged();
|
105
93
|
} else if (name == "src") {
|
106
94
|
this.delegate.sourceURLChanged();
|
107
|
-
} else {
|
95
|
+
} else if (name == "disabled") {
|
108
96
|
this.delegate.disabledChanged();
|
109
97
|
}
|
110
98
|
}
|
@@ -118,6 +106,16 @@ class FrameElement extends HTMLElement {
|
|
118
106
|
this.removeAttribute("src");
|
119
107
|
}
|
120
108
|
}
|
109
|
+
get refresh() {
|
110
|
+
return this.getAttribute("refresh");
|
111
|
+
}
|
112
|
+
set refresh(value) {
|
113
|
+
if (value) {
|
114
|
+
this.setAttribute("refresh", value);
|
115
|
+
} else {
|
116
|
+
this.removeAttribute("refresh");
|
117
|
+
}
|
118
|
+
}
|
121
119
|
get loading() {
|
122
120
|
return frameLoadingStyleFromString(this.getAttribute("loading") || "");
|
123
121
|
}
|
@@ -155,8 +153,7 @@ class FrameElement extends HTMLElement {
|
|
155
153
|
return this.ownerDocument === document && !this.isPreview;
|
156
154
|
}
|
157
155
|
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");
|
156
|
+
return this.ownerDocument?.documentElement?.hasAttribute("data-turbo-preview");
|
160
157
|
}
|
161
158
|
}
|
162
159
|
|
@@ -183,8 +180,8 @@ function getAnchor(url) {
|
|
183
180
|
}
|
184
181
|
}
|
185
182
|
|
186
|
-
function getAction(form, submitter) {
|
187
|
-
const action =
|
183
|
+
function getAction$1(form, submitter) {
|
184
|
+
const action = submitter?.getAttribute("formaction") || form.getAttribute("action") || form.action;
|
188
185
|
return expandURL(action);
|
189
186
|
}
|
190
187
|
|
@@ -323,6 +320,14 @@ function dispatch(eventName, {target: target, cancelable: cancelable, detail: de
|
|
323
320
|
return event;
|
324
321
|
}
|
325
322
|
|
323
|
+
function nextRepaint() {
|
324
|
+
if (document.visibilityState === "hidden") {
|
325
|
+
return nextEventLoopTick();
|
326
|
+
} else {
|
327
|
+
return nextAnimationFrame();
|
328
|
+
}
|
329
|
+
}
|
330
|
+
|
326
331
|
function nextAnimationFrame() {
|
327
332
|
return new Promise((resolve => requestAnimationFrame((() => resolve()))));
|
328
333
|
}
|
@@ -370,7 +375,7 @@ function uuid() {
|
|
370
375
|
}
|
371
376
|
|
372
377
|
function getAttribute(attributeName, ...elements) {
|
373
|
-
for (const value of elements.map((element => element
|
378
|
+
for (const value of elements.map((element => element?.getAttribute(attributeName)))) {
|
374
379
|
if (typeof value == "string") return value;
|
375
380
|
}
|
376
381
|
return null;
|
@@ -456,21 +461,87 @@ function setMetaContent(name, content) {
|
|
456
461
|
}
|
457
462
|
|
458
463
|
function findClosestRecursively(element, selector) {
|
459
|
-
var _a;
|
460
464
|
if (element instanceof Element) {
|
461
|
-
return element.closest(selector) || findClosestRecursively(element.assignedSlot ||
|
465
|
+
return element.closest(selector) || findClosestRecursively(element.assignedSlot || element.getRootNode()?.host, selector);
|
466
|
+
}
|
467
|
+
}
|
468
|
+
|
469
|
+
function elementIsFocusable(element) {
|
470
|
+
const inertDisabledOrHidden = "[inert], :disabled, [hidden], details:not([open]), dialog:not([open])";
|
471
|
+
return !!element && element.closest(inertDisabledOrHidden) == null && typeof element.focus == "function";
|
472
|
+
}
|
473
|
+
|
474
|
+
function queryAutofocusableElement(elementOrDocumentFragment) {
|
475
|
+
return Array.from(elementOrDocumentFragment.querySelectorAll("[autofocus]")).find(elementIsFocusable);
|
476
|
+
}
|
477
|
+
|
478
|
+
async function around(callback, reader) {
|
479
|
+
const before = reader();
|
480
|
+
callback();
|
481
|
+
await nextAnimationFrame();
|
482
|
+
const after = reader();
|
483
|
+
return [ before, after ];
|
484
|
+
}
|
485
|
+
|
486
|
+
function doesNotTargetIFrame(name) {
|
487
|
+
if (name === "_blank") {
|
488
|
+
return false;
|
489
|
+
} else if (name) {
|
490
|
+
for (const element of document.getElementsByName(name)) {
|
491
|
+
if (element instanceof HTMLIFrameElement) return false;
|
492
|
+
}
|
493
|
+
return true;
|
494
|
+
} else {
|
495
|
+
return true;
|
496
|
+
}
|
497
|
+
}
|
498
|
+
|
499
|
+
function findLinkFromClickTarget(target) {
|
500
|
+
return findClosestRecursively(target, "a[href]:not([target^=_]):not([download])");
|
501
|
+
}
|
502
|
+
|
503
|
+
function getLocationForLink(link) {
|
504
|
+
return expandURL(link.getAttribute("href") || "");
|
505
|
+
}
|
506
|
+
|
507
|
+
function debounce(fn, delay) {
|
508
|
+
let timeoutId = null;
|
509
|
+
return (...args) => {
|
510
|
+
const callback = () => fn.apply(this, args);
|
511
|
+
clearTimeout(timeoutId);
|
512
|
+
timeoutId = setTimeout(callback, delay);
|
513
|
+
};
|
514
|
+
}
|
515
|
+
|
516
|
+
class LimitedSet extends Set {
|
517
|
+
constructor(maxSize) {
|
518
|
+
super();
|
519
|
+
this.maxSize = maxSize;
|
520
|
+
}
|
521
|
+
add(value) {
|
522
|
+
if (this.size >= this.maxSize) {
|
523
|
+
const iterator = this.values();
|
524
|
+
const oldestValue = iterator.next().value;
|
525
|
+
this.delete(oldestValue);
|
526
|
+
}
|
527
|
+
super.add(value);
|
462
528
|
}
|
463
529
|
}
|
464
530
|
|
465
|
-
|
531
|
+
const recentRequests = new LimitedSet(20);
|
466
532
|
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
533
|
+
const nativeFetch = window.fetch;
|
534
|
+
|
535
|
+
function fetchWithTurboHeaders(url, options = {}) {
|
536
|
+
const modifiedHeaders = new Headers(options.headers || {});
|
537
|
+
const requestUID = uuid();
|
538
|
+
recentRequests.add(requestUID);
|
539
|
+
modifiedHeaders.append("X-Turbo-Request-Id", requestUID);
|
540
|
+
return nativeFetch(url, {
|
541
|
+
...options,
|
542
|
+
headers: modifiedHeaders
|
543
|
+
});
|
544
|
+
}
|
474
545
|
|
475
546
|
function fetchMethodFromString(method) {
|
476
547
|
switch (method.toLowerCase()) {
|
@@ -491,16 +562,81 @@ function fetchMethodFromString(method) {
|
|
491
562
|
}
|
492
563
|
}
|
493
564
|
|
565
|
+
const FetchMethod = {
|
566
|
+
get: "get",
|
567
|
+
post: "post",
|
568
|
+
put: "put",
|
569
|
+
patch: "patch",
|
570
|
+
delete: "delete"
|
571
|
+
};
|
572
|
+
|
573
|
+
function fetchEnctypeFromString(encoding) {
|
574
|
+
switch (encoding.toLowerCase()) {
|
575
|
+
case FetchEnctype.multipart:
|
576
|
+
return FetchEnctype.multipart;
|
577
|
+
|
578
|
+
case FetchEnctype.plain:
|
579
|
+
return FetchEnctype.plain;
|
580
|
+
|
581
|
+
default:
|
582
|
+
return FetchEnctype.urlEncoded;
|
583
|
+
}
|
584
|
+
}
|
585
|
+
|
586
|
+
const FetchEnctype = {
|
587
|
+
urlEncoded: "application/x-www-form-urlencoded",
|
588
|
+
multipart: "multipart/form-data",
|
589
|
+
plain: "text/plain"
|
590
|
+
};
|
591
|
+
|
494
592
|
class FetchRequest {
|
495
|
-
|
496
|
-
|
497
|
-
|
593
|
+
abortController=new AbortController;
|
594
|
+
#resolveRequestPromise=_value => {};
|
595
|
+
constructor(delegate, method, location, requestBody = new URLSearchParams, target = null, enctype = FetchEnctype.urlEncoded) {
|
596
|
+
const [url, body] = buildResourceAndBody(expandURL(location), method, requestBody, enctype);
|
498
597
|
this.delegate = delegate;
|
499
|
-
this.
|
500
|
-
this.headers = this.defaultHeaders;
|
501
|
-
this.body = body;
|
502
|
-
this.url = location;
|
598
|
+
this.url = url;
|
503
599
|
this.target = target;
|
600
|
+
this.fetchOptions = {
|
601
|
+
credentials: "same-origin",
|
602
|
+
redirect: "follow",
|
603
|
+
method: method.toUpperCase(),
|
604
|
+
headers: {
|
605
|
+
...this.defaultHeaders
|
606
|
+
},
|
607
|
+
body: body,
|
608
|
+
signal: this.abortSignal,
|
609
|
+
referrer: this.delegate.referrer?.href
|
610
|
+
};
|
611
|
+
this.enctype = enctype;
|
612
|
+
}
|
613
|
+
get method() {
|
614
|
+
return this.fetchOptions.method;
|
615
|
+
}
|
616
|
+
set method(value) {
|
617
|
+
const fetchBody = this.isSafe ? this.url.searchParams : this.fetchOptions.body || new FormData;
|
618
|
+
const fetchMethod = fetchMethodFromString(value) || FetchMethod.get;
|
619
|
+
this.url.search = "";
|
620
|
+
const [url, body] = buildResourceAndBody(this.url, fetchMethod, fetchBody, this.enctype);
|
621
|
+
this.url = url;
|
622
|
+
this.fetchOptions.body = body;
|
623
|
+
this.fetchOptions.method = fetchMethod.toUpperCase();
|
624
|
+
}
|
625
|
+
get headers() {
|
626
|
+
return this.fetchOptions.headers;
|
627
|
+
}
|
628
|
+
set headers(value) {
|
629
|
+
this.fetchOptions.headers = value;
|
630
|
+
}
|
631
|
+
get body() {
|
632
|
+
if (this.isSafe) {
|
633
|
+
return this.url.searchParams;
|
634
|
+
} else {
|
635
|
+
return this.fetchOptions.body;
|
636
|
+
}
|
637
|
+
}
|
638
|
+
set body(value) {
|
639
|
+
this.fetchOptions.body = value;
|
504
640
|
}
|
505
641
|
get location() {
|
506
642
|
return this.url;
|
@@ -517,14 +653,19 @@ class FetchRequest {
|
|
517
653
|
async perform() {
|
518
654
|
const {fetchOptions: fetchOptions} = this;
|
519
655
|
this.delegate.prepareRequest(this);
|
520
|
-
await this
|
656
|
+
const event = await this.#allowRequestToBeIntercepted(fetchOptions);
|
521
657
|
try {
|
522
658
|
this.delegate.requestStarted(this);
|
523
|
-
|
659
|
+
if (event.detail.fetchRequest) {
|
660
|
+
this.response = event.detail.fetchRequest.response;
|
661
|
+
} else {
|
662
|
+
this.response = fetchWithTurboHeaders(this.url.href, fetchOptions);
|
663
|
+
}
|
664
|
+
const response = await this.response;
|
524
665
|
return await this.receive(response);
|
525
666
|
} catch (error) {
|
526
667
|
if (error.name !== "AbortError") {
|
527
|
-
if (this
|
668
|
+
if (this.#willDelegateErrorHandling(error)) {
|
528
669
|
this.delegate.requestErrored(this, error);
|
529
670
|
}
|
530
671
|
throw error;
|
@@ -551,25 +692,13 @@ class FetchRequest {
|
|
551
692
|
}
|
552
693
|
return fetchResponse;
|
553
694
|
}
|
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
695
|
get defaultHeaders() {
|
567
696
|
return {
|
568
697
|
Accept: "text/html, application/xhtml+xml"
|
569
698
|
};
|
570
699
|
}
|
571
700
|
get isSafe() {
|
572
|
-
return this.method
|
701
|
+
return isSafe(this.method);
|
573
702
|
}
|
574
703
|
get abortSignal() {
|
575
704
|
return this.abortController.signal;
|
@@ -577,20 +706,22 @@ class FetchRequest {
|
|
577
706
|
acceptResponseType(mimeType) {
|
578
707
|
this.headers["Accept"] = [ mimeType, this.headers["Accept"] ].join(", ");
|
579
708
|
}
|
580
|
-
async allowRequestToBeIntercepted(fetchOptions) {
|
581
|
-
const requestInterception = new Promise((resolve => this
|
709
|
+
async #allowRequestToBeIntercepted(fetchOptions) {
|
710
|
+
const requestInterception = new Promise((resolve => this.#resolveRequestPromise = resolve));
|
582
711
|
const event = dispatch("turbo:before-fetch-request", {
|
583
712
|
cancelable: true,
|
584
713
|
detail: {
|
585
714
|
fetchOptions: fetchOptions,
|
586
715
|
url: this.url,
|
587
|
-
resume: this
|
716
|
+
resume: this.#resolveRequestPromise
|
588
717
|
},
|
589
718
|
target: this.target
|
590
719
|
});
|
720
|
+
this.url = event.detail.url;
|
591
721
|
if (event.defaultPrevented) await requestInterception;
|
722
|
+
return event;
|
592
723
|
}
|
593
|
-
willDelegateErrorHandling(error) {
|
724
|
+
#willDelegateErrorHandling(error) {
|
594
725
|
const event = dispatch("turbo:fetch-request-error", {
|
595
726
|
target: this.target,
|
596
727
|
cancelable: true,
|
@@ -603,15 +734,38 @@ class FetchRequest {
|
|
603
734
|
}
|
604
735
|
}
|
605
736
|
|
737
|
+
function isSafe(fetchMethod) {
|
738
|
+
return fetchMethodFromString(fetchMethod) == FetchMethod.get;
|
739
|
+
}
|
740
|
+
|
741
|
+
function buildResourceAndBody(resource, method, requestBody, enctype) {
|
742
|
+
const searchParams = Array.from(requestBody).length > 0 ? new URLSearchParams(entriesExcludingFiles(requestBody)) : resource.searchParams;
|
743
|
+
if (isSafe(method)) {
|
744
|
+
return [ mergeIntoURLSearchParams(resource, searchParams), null ];
|
745
|
+
} else if (enctype == FetchEnctype.urlEncoded) {
|
746
|
+
return [ resource, searchParams ];
|
747
|
+
} else {
|
748
|
+
return [ resource, requestBody ];
|
749
|
+
}
|
750
|
+
}
|
751
|
+
|
752
|
+
function entriesExcludingFiles(requestBody) {
|
753
|
+
const entries = [];
|
754
|
+
for (const [name, value] of requestBody) {
|
755
|
+
if (value instanceof File) continue; else entries.push([ name, value ]);
|
756
|
+
}
|
757
|
+
return entries;
|
758
|
+
}
|
759
|
+
|
760
|
+
function mergeIntoURLSearchParams(url, requestBody) {
|
761
|
+
const searchParams = new URLSearchParams(entriesExcludingFiles(requestBody));
|
762
|
+
url.search = searchParams.toString();
|
763
|
+
return url;
|
764
|
+
}
|
765
|
+
|
606
766
|
class AppearanceObserver {
|
767
|
+
started=false;
|
607
768
|
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
769
|
this.delegate = delegate;
|
616
770
|
this.element = element;
|
617
771
|
this.intersectionObserver = new IntersectionObserver(this.intersect);
|
@@ -628,9 +782,16 @@ class AppearanceObserver {
|
|
628
782
|
this.intersectionObserver.unobserve(this.element);
|
629
783
|
}
|
630
784
|
}
|
785
|
+
intersect=entries => {
|
786
|
+
const lastEntry = entries.slice(-1)[0];
|
787
|
+
if (lastEntry?.isIntersecting) {
|
788
|
+
this.delegate.elementAppearedInViewport(this.element);
|
789
|
+
}
|
790
|
+
};
|
631
791
|
}
|
632
792
|
|
633
793
|
class StreamMessage {
|
794
|
+
static contentType="text/vnd.turbo-stream.html";
|
634
795
|
static wrap(message) {
|
635
796
|
if (typeof message == "string") {
|
636
797
|
return new this(createDocumentFragment(message));
|
@@ -643,8 +804,6 @@ class StreamMessage {
|
|
643
804
|
}
|
644
805
|
}
|
645
806
|
|
646
|
-
StreamMessage.contentType = "text/vnd.turbo-stream.html";
|
647
|
-
|
648
807
|
function importStreamElements(fragment) {
|
649
808
|
for (const element of fragment.querySelectorAll("turbo-stream")) {
|
650
809
|
const streamElement = document.importNode(element, true);
|
@@ -656,85 +815,89 @@ function importStreamElements(fragment) {
|
|
656
815
|
return fragment;
|
657
816
|
}
|
658
817
|
|
659
|
-
|
818
|
+
const PREFETCH_DELAY = 100;
|
660
819
|
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
}
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
(
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
}
|
820
|
+
class PrefetchCache {
|
821
|
+
#prefetchTimeout=null;
|
822
|
+
#prefetched=null;
|
823
|
+
get(url) {
|
824
|
+
if (this.#prefetched && this.#prefetched.url === url && this.#prefetched.expire > Date.now()) {
|
825
|
+
return this.#prefetched.request;
|
826
|
+
}
|
827
|
+
}
|
828
|
+
setLater(url, request, ttl) {
|
829
|
+
this.clear();
|
830
|
+
this.#prefetchTimeout = setTimeout((() => {
|
831
|
+
request.perform();
|
832
|
+
this.set(url, request, ttl);
|
833
|
+
this.#prefetchTimeout = null;
|
834
|
+
}), PREFETCH_DELAY);
|
835
|
+
}
|
836
|
+
set(url, request, ttl) {
|
837
|
+
this.#prefetched = {
|
838
|
+
url: url,
|
839
|
+
request: request,
|
840
|
+
expire: new Date((new Date).getTime() + ttl)
|
841
|
+
};
|
842
|
+
}
|
843
|
+
clear() {
|
844
|
+
if (this.#prefetchTimeout) clearTimeout(this.#prefetchTimeout);
|
845
|
+
this.#prefetched = null;
|
846
|
+
}
|
847
|
+
}
|
677
848
|
|
678
|
-
|
679
|
-
switch (encoding.toLowerCase()) {
|
680
|
-
case FormEnctype.multipart:
|
681
|
-
return FormEnctype.multipart;
|
849
|
+
const cacheTtl = 10 * 1e3;
|
682
850
|
|
683
|
-
|
684
|
-
return FormEnctype.plain;
|
851
|
+
const prefetchCache = new PrefetchCache;
|
685
852
|
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
853
|
+
const FormSubmissionState = {
|
854
|
+
initialized: "initialized",
|
855
|
+
requesting: "requesting",
|
856
|
+
waiting: "waiting",
|
857
|
+
receiving: "receiving",
|
858
|
+
stopping: "stopping",
|
859
|
+
stopped: "stopped"
|
860
|
+
};
|
690
861
|
|
691
862
|
class FormSubmission {
|
863
|
+
state=FormSubmissionState.initialized;
|
692
864
|
static confirmMethod(message, _element, _submitter) {
|
693
865
|
return Promise.resolve(confirm(message));
|
694
866
|
}
|
695
867
|
constructor(delegate, formElement, submitter, mustRedirect = false) {
|
696
|
-
|
868
|
+
const method = getMethod(formElement, submitter);
|
869
|
+
const action = getAction(getFormAction(formElement, submitter), method);
|
870
|
+
const body = buildFormData(formElement, submitter);
|
871
|
+
const enctype = getEnctype(formElement, submitter);
|
697
872
|
this.delegate = delegate;
|
698
873
|
this.formElement = formElement;
|
699
874
|
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);
|
875
|
+
this.fetchRequest = new FetchRequest(this, method, action, body, formElement, enctype);
|
706
876
|
this.mustRedirect = mustRedirect;
|
707
877
|
}
|
708
878
|
get method() {
|
709
|
-
|
710
|
-
|
711
|
-
|
879
|
+
return this.fetchRequest.method;
|
880
|
+
}
|
881
|
+
set method(value) {
|
882
|
+
this.fetchRequest.method = value;
|
712
883
|
}
|
713
884
|
get action() {
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
} else {
|
719
|
-
return this.formElement.getAttribute("action") || formElementAction || "";
|
720
|
-
}
|
885
|
+
return this.fetchRequest.url.toString();
|
886
|
+
}
|
887
|
+
set action(value) {
|
888
|
+
this.fetchRequest.url = expandURL(value);
|
721
889
|
}
|
722
890
|
get body() {
|
723
|
-
|
724
|
-
return new URLSearchParams(this.stringFormData);
|
725
|
-
} else {
|
726
|
-
return this.formData;
|
727
|
-
}
|
891
|
+
return this.fetchRequest.body;
|
728
892
|
}
|
729
893
|
get enctype() {
|
730
|
-
|
731
|
-
return formEnctypeFromString(((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formenctype")) || this.formElement.enctype);
|
894
|
+
return this.fetchRequest.enctype;
|
732
895
|
}
|
733
896
|
get isSafe() {
|
734
897
|
return this.fetchRequest.isSafe;
|
735
898
|
}
|
736
|
-
get
|
737
|
-
return
|
899
|
+
get location() {
|
900
|
+
return this.fetchRequest.url;
|
738
901
|
}
|
739
902
|
async start() {
|
740
903
|
const {initialized: initialized, requesting: requesting} = FormSubmissionState;
|
@@ -770,10 +933,10 @@ class FormSubmission {
|
|
770
933
|
}
|
771
934
|
}
|
772
935
|
requestStarted(_request) {
|
773
|
-
var _a;
|
774
936
|
this.state = FormSubmissionState.waiting;
|
775
|
-
|
937
|
+
this.submitter?.setAttribute("disabled", "");
|
776
938
|
this.setSubmitsWith();
|
939
|
+
markAsBusy(this.formElement);
|
777
940
|
dispatch("turbo:submit-start", {
|
778
941
|
target: this.formElement,
|
779
942
|
detail: {
|
@@ -783,6 +946,7 @@ class FormSubmission {
|
|
783
946
|
this.delegate.formSubmissionStarted(this);
|
784
947
|
}
|
785
948
|
requestPreventedHandlingResponse(request, response) {
|
949
|
+
prefetchCache.clear();
|
786
950
|
this.result = {
|
787
951
|
success: response.succeeded,
|
788
952
|
fetchResponse: response
|
@@ -791,7 +955,10 @@ class FormSubmission {
|
|
791
955
|
requestSucceededWithResponse(request, response) {
|
792
956
|
if (response.clientError || response.serverError) {
|
793
957
|
this.delegate.formSubmissionFailedWithResponse(this, response);
|
794
|
-
|
958
|
+
return;
|
959
|
+
}
|
960
|
+
prefetchCache.clear();
|
961
|
+
if (this.requestMustRedirect(request) && responseSucceededWithoutRedirect(response)) {
|
795
962
|
const error = new Error("Form responses must redirect to another location");
|
796
963
|
this.delegate.formSubmissionErrored(this, error);
|
797
964
|
} else {
|
@@ -818,15 +985,16 @@ class FormSubmission {
|
|
818
985
|
this.delegate.formSubmissionErrored(this, error);
|
819
986
|
}
|
820
987
|
requestFinished(_request) {
|
821
|
-
var _a;
|
822
988
|
this.state = FormSubmissionState.stopped;
|
823
|
-
|
989
|
+
this.submitter?.removeAttribute("disabled");
|
824
990
|
this.resetSubmitterText();
|
991
|
+
clearBusyState(this.formElement);
|
825
992
|
dispatch("turbo:submit-end", {
|
826
993
|
target: this.formElement,
|
827
|
-
detail:
|
828
|
-
formSubmission: this
|
829
|
-
|
994
|
+
detail: {
|
995
|
+
formSubmission: this,
|
996
|
+
...this.result
|
997
|
+
}
|
830
998
|
});
|
831
999
|
this.delegate.formSubmissionFinished(this);
|
832
1000
|
}
|
@@ -857,15 +1025,14 @@ class FormSubmission {
|
|
857
1025
|
return !request.isSafe || hasAttribute("data-turbo-stream", this.submitter, this.formElement);
|
858
1026
|
}
|
859
1027
|
get submitsWith() {
|
860
|
-
|
861
|
-
return (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("data-turbo-submits-with");
|
1028
|
+
return this.submitter?.getAttribute("data-turbo-submits-with");
|
862
1029
|
}
|
863
1030
|
}
|
864
1031
|
|
865
1032
|
function buildFormData(formElement, submitter) {
|
866
1033
|
const formData = new FormData(formElement);
|
867
|
-
const name = submitter
|
868
|
-
const value = submitter
|
1034
|
+
const name = submitter?.getAttribute("name");
|
1035
|
+
const value = submitter?.getAttribute("value");
|
869
1036
|
if (name) {
|
870
1037
|
formData.append(name, value || "");
|
871
1038
|
}
|
@@ -887,14 +1054,30 @@ function responseSucceededWithoutRedirect(response) {
|
|
887
1054
|
return response.statusCode == 200 && !response.redirected;
|
888
1055
|
}
|
889
1056
|
|
890
|
-
function
|
891
|
-
const
|
892
|
-
|
893
|
-
|
894
|
-
|
1057
|
+
function getFormAction(formElement, submitter) {
|
1058
|
+
const formElementAction = typeof formElement.action === "string" ? formElement.action : null;
|
1059
|
+
if (submitter?.hasAttribute("formaction")) {
|
1060
|
+
return submitter.getAttribute("formaction") || "";
|
1061
|
+
} else {
|
1062
|
+
return formElement.getAttribute("action") || formElementAction || "";
|
895
1063
|
}
|
896
|
-
|
897
|
-
|
1064
|
+
}
|
1065
|
+
|
1066
|
+
function getAction(formAction, fetchMethod) {
|
1067
|
+
const action = expandURL(formAction);
|
1068
|
+
if (isSafe(fetchMethod)) {
|
1069
|
+
action.search = "";
|
1070
|
+
}
|
1071
|
+
return action;
|
1072
|
+
}
|
1073
|
+
|
1074
|
+
function getMethod(formElement, submitter) {
|
1075
|
+
const method = submitter?.getAttribute("formmethod") || formElement.getAttribute("method") || "";
|
1076
|
+
return fetchMethodFromString(method.toLowerCase()) || FetchMethod.get;
|
1077
|
+
}
|
1078
|
+
|
1079
|
+
function getEnctype(formElement, submitter) {
|
1080
|
+
return fetchEnctypeFromString(submitter?.getAttribute("formenctype") || formElement.enctype);
|
898
1081
|
}
|
899
1082
|
|
900
1083
|
class Snapshot {
|
@@ -917,11 +1100,7 @@ class Snapshot {
|
|
917
1100
|
return this.element.isConnected;
|
918
1101
|
}
|
919
1102
|
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;
|
1103
|
+
return queryAutofocusableElement(this.element);
|
925
1104
|
}
|
926
1105
|
get permanentElements() {
|
927
1106
|
return queryPermanentElementsAll(this.element);
|
@@ -951,23 +1130,8 @@ function queryPermanentElementsAll(node) {
|
|
951
1130
|
}
|
952
1131
|
|
953
1132
|
class FormSubmitObserver {
|
1133
|
+
started=false;
|
954
1134
|
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
1135
|
this.delegate = delegate;
|
972
1136
|
this.eventTarget = eventTarget;
|
973
1137
|
}
|
@@ -983,29 +1147,37 @@ class FormSubmitObserver {
|
|
983
1147
|
this.started = false;
|
984
1148
|
}
|
985
1149
|
}
|
1150
|
+
submitCaptured=() => {
|
1151
|
+
this.eventTarget.removeEventListener("submit", this.submitBubbled, false);
|
1152
|
+
this.eventTarget.addEventListener("submit", this.submitBubbled, false);
|
1153
|
+
};
|
1154
|
+
submitBubbled=event => {
|
1155
|
+
if (!event.defaultPrevented) {
|
1156
|
+
const form = event.target instanceof HTMLFormElement ? event.target : undefined;
|
1157
|
+
const submitter = event.submitter || undefined;
|
1158
|
+
if (form && submissionDoesNotDismissDialog(form, submitter) && submissionDoesNotTargetIFrame(form, submitter) && this.delegate.willSubmitForm(form, submitter)) {
|
1159
|
+
event.preventDefault();
|
1160
|
+
event.stopImmediatePropagation();
|
1161
|
+
this.delegate.formSubmitted(form, submitter);
|
1162
|
+
}
|
1163
|
+
}
|
1164
|
+
};
|
986
1165
|
}
|
987
1166
|
|
988
1167
|
function submissionDoesNotDismissDialog(form, submitter) {
|
989
|
-
const method =
|
1168
|
+
const method = submitter?.getAttribute("formmethod") || form.getAttribute("method");
|
990
1169
|
return method != "dialog";
|
991
1170
|
}
|
992
1171
|
|
993
1172
|
function submissionDoesNotTargetIFrame(form, submitter) {
|
994
|
-
|
995
|
-
|
996
|
-
for (const element of document.getElementsByName(target)) {
|
997
|
-
if (element instanceof HTMLIFrameElement) return false;
|
998
|
-
}
|
999
|
-
return true;
|
1000
|
-
} else {
|
1001
|
-
return true;
|
1002
|
-
}
|
1173
|
+
const target = submitter?.getAttribute("formtarget") || form.getAttribute("target");
|
1174
|
+
return doesNotTargetIFrame(target);
|
1003
1175
|
}
|
1004
1176
|
|
1005
1177
|
class View {
|
1178
|
+
#resolveRenderPromise=_value => {};
|
1179
|
+
#resolveInterceptionPromise=_value => {};
|
1006
1180
|
constructor(delegate, element) {
|
1007
|
-
this.resolveRenderPromise = _value => {};
|
1008
|
-
this.resolveInterceptionPromise = _value => {};
|
1009
1181
|
this.delegate = delegate;
|
1010
1182
|
this.element = element;
|
1011
1183
|
}
|
@@ -1051,29 +1223,31 @@ class View {
|
|
1051
1223
|
return window;
|
1052
1224
|
}
|
1053
1225
|
async render(renderer) {
|
1054
|
-
const {isPreview: isPreview, shouldRender: shouldRender, newSnapshot: snapshot} = renderer;
|
1226
|
+
const {isPreview: isPreview, shouldRender: shouldRender, willRender: willRender, newSnapshot: snapshot} = renderer;
|
1227
|
+
const shouldInvalidate = willRender;
|
1055
1228
|
if (shouldRender) {
|
1056
1229
|
try {
|
1057
|
-
this.renderPromise = new Promise((resolve => this
|
1230
|
+
this.renderPromise = new Promise((resolve => this.#resolveRenderPromise = resolve));
|
1058
1231
|
this.renderer = renderer;
|
1059
1232
|
await this.prepareToRenderSnapshot(renderer);
|
1060
|
-
const renderInterception = new Promise((resolve => this
|
1233
|
+
const renderInterception = new Promise((resolve => this.#resolveInterceptionPromise = resolve));
|
1061
1234
|
const options = {
|
1062
|
-
resume: this
|
1063
|
-
render: this.renderer.renderElement
|
1235
|
+
resume: this.#resolveInterceptionPromise,
|
1236
|
+
render: this.renderer.renderElement,
|
1237
|
+
renderMethod: this.renderer.renderMethod
|
1064
1238
|
};
|
1065
1239
|
const immediateRender = this.delegate.allowsImmediateRender(snapshot, options);
|
1066
1240
|
if (!immediateRender) await renderInterception;
|
1067
1241
|
await this.renderSnapshot(renderer);
|
1068
|
-
this.delegate.viewRenderedSnapshot(snapshot, isPreview);
|
1242
|
+
this.delegate.viewRenderedSnapshot(snapshot, isPreview, this.renderer.renderMethod);
|
1069
1243
|
this.delegate.preloadOnLoadLinksForView(this.element);
|
1070
1244
|
this.finishRenderingSnapshot(renderer);
|
1071
1245
|
} finally {
|
1072
1246
|
delete this.renderer;
|
1073
|
-
this
|
1247
|
+
this.#resolveRenderPromise(undefined);
|
1074
1248
|
delete this.renderPromise;
|
1075
1249
|
}
|
1076
|
-
} else {
|
1250
|
+
} else if (shouldInvalidate) {
|
1077
1251
|
this.invalidate(renderer.reloadReason);
|
1078
1252
|
}
|
1079
1253
|
}
|
@@ -1091,6 +1265,12 @@ class View {
|
|
1091
1265
|
this.element.removeAttribute("data-turbo-preview");
|
1092
1266
|
}
|
1093
1267
|
}
|
1268
|
+
markVisitDirection(direction) {
|
1269
|
+
this.element.setAttribute("data-turbo-visit-direction", direction);
|
1270
|
+
}
|
1271
|
+
unmarkVisitDirection() {
|
1272
|
+
this.element.removeAttribute("data-turbo-visit-direction");
|
1273
|
+
}
|
1094
1274
|
async renderSnapshot(renderer) {
|
1095
1275
|
await renderer.render();
|
1096
1276
|
}
|
@@ -1110,26 +1290,6 @@ class FrameView extends View {
|
|
1110
1290
|
|
1111
1291
|
class LinkInterceptor {
|
1112
1292
|
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
1293
|
this.delegate = delegate;
|
1134
1294
|
this.element = element;
|
1135
1295
|
}
|
@@ -1143,32 +1303,36 @@ class LinkInterceptor {
|
|
1143
1303
|
document.removeEventListener("turbo:click", this.linkClicked);
|
1144
1304
|
document.removeEventListener("turbo:before-visit", this.willVisit);
|
1145
1305
|
}
|
1146
|
-
|
1147
|
-
|
1148
|
-
|
1306
|
+
clickBubbled=event => {
|
1307
|
+
if (this.clickEventIsSignificant(event)) {
|
1308
|
+
this.clickEvent = event;
|
1309
|
+
} else {
|
1310
|
+
delete this.clickEvent;
|
1311
|
+
}
|
1312
|
+
};
|
1313
|
+
linkClicked=event => {
|
1314
|
+
if (this.clickEvent && this.clickEventIsSignificant(event)) {
|
1315
|
+
if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url, event.detail.originalEvent)) {
|
1316
|
+
this.clickEvent.preventDefault();
|
1317
|
+
event.preventDefault();
|
1318
|
+
this.delegate.linkClickIntercepted(event.target, event.detail.url, event.detail.originalEvent);
|
1319
|
+
}
|
1320
|
+
}
|
1321
|
+
delete this.clickEvent;
|
1322
|
+
};
|
1323
|
+
willVisit=_event => {
|
1324
|
+
delete this.clickEvent;
|
1325
|
+
};
|
1326
|
+
clickEventIsSignificant(event) {
|
1327
|
+
const target = event.composed ? event.target?.parentElement : event.target;
|
1328
|
+
const element = findLinkFromClickTarget(target) || target;
|
1329
|
+
return element instanceof Element && element.closest("turbo-frame, html") == this.element;
|
1149
1330
|
}
|
1150
1331
|
}
|
1151
1332
|
|
1152
1333
|
class LinkClickObserver {
|
1334
|
+
started=false;
|
1153
1335
|
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
1336
|
this.delegate = delegate;
|
1173
1337
|
this.eventTarget = eventTarget;
|
1174
1338
|
}
|
@@ -1184,26 +1348,26 @@ class LinkClickObserver {
|
|
1184
1348
|
this.started = false;
|
1185
1349
|
}
|
1186
1350
|
}
|
1351
|
+
clickCaptured=() => {
|
1352
|
+
this.eventTarget.removeEventListener("click", this.clickBubbled, false);
|
1353
|
+
this.eventTarget.addEventListener("click", this.clickBubbled, false);
|
1354
|
+
};
|
1355
|
+
clickBubbled=event => {
|
1356
|
+
if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {
|
1357
|
+
const target = event.composedPath && event.composedPath()[0] || event.target;
|
1358
|
+
const link = findLinkFromClickTarget(target);
|
1359
|
+
if (link && doesNotTargetIFrame(link.target)) {
|
1360
|
+
const location = getLocationForLink(link);
|
1361
|
+
if (this.delegate.willFollowLinkToLocation(link, location, event)) {
|
1362
|
+
event.preventDefault();
|
1363
|
+
this.delegate.followedLinkToLocation(link, location);
|
1364
|
+
}
|
1365
|
+
}
|
1366
|
+
}
|
1367
|
+
};
|
1187
1368
|
clickEventIsSignificant(event) {
|
1188
1369
|
return !(event.target && event.target.isContentEditable || event.defaultPrevented || event.which > 1 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey);
|
1189
1370
|
}
|
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
1371
|
}
|
1208
1372
|
|
1209
1373
|
class FormLinkClickObserver {
|
@@ -1217,8 +1381,14 @@ class FormLinkClickObserver {
|
|
1217
1381
|
stop() {
|
1218
1382
|
this.linkInterceptor.stop();
|
1219
1383
|
}
|
1384
|
+
canPrefetchRequestToLocation(link, location) {
|
1385
|
+
return false;
|
1386
|
+
}
|
1387
|
+
prefetchAndCacheRequestToLocation(link, location) {
|
1388
|
+
return;
|
1389
|
+
}
|
1220
1390
|
willFollowLinkToLocation(link, location, originalEvent) {
|
1221
|
-
return this.delegate.willSubmitFormLinkToLocation(link, location, originalEvent) && link.hasAttribute("data-turbo-method");
|
1391
|
+
return this.delegate.willSubmitFormLinkToLocation(link, location, originalEvent) && (link.hasAttribute("data-turbo-method") || link.hasAttribute("data-turbo-stream"));
|
1222
1392
|
}
|
1223
1393
|
followedLinkToLocation(link, location) {
|
1224
1394
|
const form = document.createElement("form");
|
@@ -1291,7 +1461,7 @@ class Bardo {
|
|
1291
1461
|
}
|
1292
1462
|
replacePlaceholderWithPermanentElement(permanentElement) {
|
1293
1463
|
const placeholder = this.getPlaceholderById(permanentElement.id);
|
1294
|
-
placeholder
|
1464
|
+
placeholder?.replaceWith(permanentElement);
|
1295
1465
|
}
|
1296
1466
|
getPlaceholderById(id) {
|
1297
1467
|
return this.placeholders.find((element => element.content == id));
|
@@ -1309,8 +1479,8 @@ function createPlaceholderForPermanentElement(permanentElement) {
|
|
1309
1479
|
}
|
1310
1480
|
|
1311
1481
|
class Renderer {
|
1482
|
+
#activeElement=null;
|
1312
1483
|
constructor(currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) {
|
1313
|
-
this.activeElement = null;
|
1314
1484
|
this.currentSnapshot = currentSnapshot;
|
1315
1485
|
this.newSnapshot = newSnapshot;
|
1316
1486
|
this.isPreview = isPreview;
|
@@ -1324,12 +1494,16 @@ class Renderer {
|
|
1324
1494
|
get shouldRender() {
|
1325
1495
|
return true;
|
1326
1496
|
}
|
1497
|
+
get shouldAutofocus() {
|
1498
|
+
return true;
|
1499
|
+
}
|
1327
1500
|
get reloadReason() {
|
1328
1501
|
return;
|
1329
1502
|
}
|
1330
1503
|
prepareToRender() {
|
1331
1504
|
return;
|
1332
1505
|
}
|
1506
|
+
render() {}
|
1333
1507
|
finishRendering() {
|
1334
1508
|
if (this.resolvingFunctions) {
|
1335
1509
|
this.resolvingFunctions.resolve();
|
@@ -1340,21 +1514,23 @@ class Renderer {
|
|
1340
1514
|
await Bardo.preservingPermanentElements(this, this.permanentElementMap, callback);
|
1341
1515
|
}
|
1342
1516
|
focusFirstAutofocusableElement() {
|
1343
|
-
|
1344
|
-
|
1345
|
-
element
|
1517
|
+
if (this.shouldAutofocus) {
|
1518
|
+
const element = this.connectedSnapshot.firstAutofocusableElement;
|
1519
|
+
if (element) {
|
1520
|
+
element.focus();
|
1521
|
+
}
|
1346
1522
|
}
|
1347
1523
|
}
|
1348
1524
|
enteringBardo(currentPermanentElement) {
|
1349
|
-
if (this
|
1525
|
+
if (this.#activeElement) return;
|
1350
1526
|
if (currentPermanentElement.contains(this.currentSnapshot.activeElement)) {
|
1351
|
-
this
|
1527
|
+
this.#activeElement = this.currentSnapshot.activeElement;
|
1352
1528
|
}
|
1353
1529
|
}
|
1354
1530
|
leavingBardo(currentPermanentElement) {
|
1355
|
-
if (currentPermanentElement.contains(this
|
1356
|
-
this
|
1357
|
-
this
|
1531
|
+
if (currentPermanentElement.contains(this.#activeElement) && this.#activeElement instanceof HTMLElement) {
|
1532
|
+
this.#activeElement.focus();
|
1533
|
+
this.#activeElement = null;
|
1358
1534
|
}
|
1359
1535
|
}
|
1360
1536
|
get connectedSnapshot() {
|
@@ -1369,20 +1545,18 @@ class Renderer {
|
|
1369
1545
|
get permanentElementMap() {
|
1370
1546
|
return this.currentSnapshot.getPermanentElementMapForSnapshot(this.newSnapshot);
|
1371
1547
|
}
|
1372
|
-
|
1373
|
-
|
1374
|
-
|
1375
|
-
return element && typeof element.focus == "function";
|
1548
|
+
get renderMethod() {
|
1549
|
+
return "replace";
|
1550
|
+
}
|
1376
1551
|
}
|
1377
1552
|
|
1378
1553
|
class FrameRenderer extends Renderer {
|
1379
1554
|
static renderElement(currentElement, newElement) {
|
1380
|
-
var _a;
|
1381
1555
|
const destinationRange = document.createRange();
|
1382
1556
|
destinationRange.selectNodeContents(currentElement);
|
1383
1557
|
destinationRange.deleteContents();
|
1384
1558
|
const frameElement = newElement;
|
1385
|
-
const sourceRange =
|
1559
|
+
const sourceRange = frameElement.ownerDocument?.createRange();
|
1386
1560
|
if (sourceRange) {
|
1387
1561
|
sourceRange.selectNodeContents(frameElement);
|
1388
1562
|
currentElement.appendChild(sourceRange.extractContents());
|
@@ -1396,14 +1570,14 @@ class FrameRenderer extends Renderer {
|
|
1396
1570
|
return true;
|
1397
1571
|
}
|
1398
1572
|
async render() {
|
1399
|
-
await
|
1573
|
+
await nextRepaint();
|
1400
1574
|
this.preservingPermanentElements((() => {
|
1401
1575
|
this.loadFrameElement();
|
1402
1576
|
}));
|
1403
1577
|
this.scrollFrameIntoView();
|
1404
|
-
await
|
1578
|
+
await nextRepaint();
|
1405
1579
|
this.focusFirstAutofocusableElement();
|
1406
|
-
await
|
1580
|
+
await nextRepaint();
|
1407
1581
|
this.activateScriptElements();
|
1408
1582
|
}
|
1409
1583
|
loadFrameElement() {
|
@@ -1453,6 +1627,7 @@ function readScrollBehavior(value, defaultValue) {
|
|
1453
1627
|
}
|
1454
1628
|
|
1455
1629
|
class ProgressBar {
|
1630
|
+
static animationDuration=300;
|
1456
1631
|
static get defaultCSS() {
|
1457
1632
|
return unindent`
|
1458
1633
|
.turbo-progress-bar {
|
@@ -1470,13 +1645,10 @@ class ProgressBar {
|
|
1470
1645
|
}
|
1471
1646
|
`;
|
1472
1647
|
}
|
1648
|
+
hiding=false;
|
1649
|
+
value=0;
|
1650
|
+
visible=false;
|
1473
1651
|
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
1652
|
this.stylesheetElement = this.createStylesheetElement();
|
1481
1653
|
this.progressElement = this.createProgressElement();
|
1482
1654
|
this.installStylesheetElement();
|
@@ -1531,6 +1703,9 @@ class ProgressBar {
|
|
1531
1703
|
window.clearInterval(this.trickleInterval);
|
1532
1704
|
delete this.trickleInterval;
|
1533
1705
|
}
|
1706
|
+
trickle=() => {
|
1707
|
+
this.setValue(this.value + Math.random() / 100);
|
1708
|
+
};
|
1534
1709
|
refresh() {
|
1535
1710
|
requestAnimationFrame((() => {
|
1536
1711
|
this.progressElement.style.width = `${10 + this.value * 90}%`;
|
@@ -1555,25 +1730,22 @@ class ProgressBar {
|
|
1555
1730
|
}
|
1556
1731
|
}
|
1557
1732
|
|
1558
|
-
ProgressBar.animationDuration = 300;
|
1559
|
-
|
1560
1733
|
class HeadSnapshot extends Snapshot {
|
1561
|
-
|
1562
|
-
|
1563
|
-
|
1564
|
-
|
1565
|
-
|
1566
|
-
|
1567
|
-
|
1568
|
-
|
1569
|
-
|
1570
|
-
|
1571
|
-
|
1572
|
-
|
1573
|
-
|
1574
|
-
|
1575
|
-
|
1576
|
-
}
|
1734
|
+
detailsByOuterHTML=this.children.filter((element => !elementIsNoscript(element))).map((element => elementWithoutNonce(element))).reduce(((result, element) => {
|
1735
|
+
const {outerHTML: outerHTML} = element;
|
1736
|
+
const details = outerHTML in result ? result[outerHTML] : {
|
1737
|
+
type: elementType(element),
|
1738
|
+
tracked: elementIsTracked(element),
|
1739
|
+
elements: []
|
1740
|
+
};
|
1741
|
+
return {
|
1742
|
+
...result,
|
1743
|
+
[outerHTML]: {
|
1744
|
+
...details,
|
1745
|
+
elements: [ ...details.elements, element ]
|
1746
|
+
}
|
1747
|
+
};
|
1748
|
+
}), {});
|
1577
1749
|
get trackedElementSignature() {
|
1578
1750
|
return Object.keys(this.detailsByOuterHTML).filter((outerHTML => this.detailsByOuterHTML[outerHTML].tracked)).join("");
|
1579
1751
|
}
|
@@ -1606,7 +1778,7 @@ class HeadSnapshot extends Snapshot {
|
|
1606
1778
|
return Object.keys(this.detailsByOuterHTML).reduce(((result, outerHTML) => {
|
1607
1779
|
const {elements: [element]} = this.detailsByOuterHTML[outerHTML];
|
1608
1780
|
return elementIsMetaElementWithName(element, name) ? element : result;
|
1609
|
-
}), undefined);
|
1781
|
+
}), undefined | undefined);
|
1610
1782
|
}
|
1611
1783
|
}
|
1612
1784
|
|
@@ -1656,11 +1828,12 @@ class PageSnapshot extends Snapshot {
|
|
1656
1828
|
static fromElement(element) {
|
1657
1829
|
return this.fromDocument(element.ownerDocument);
|
1658
1830
|
}
|
1659
|
-
static fromDocument({
|
1660
|
-
return new this(body, new HeadSnapshot(head));
|
1831
|
+
static fromDocument({documentElement: documentElement, body: body, head: head}) {
|
1832
|
+
return new this(documentElement, body, new HeadSnapshot(head));
|
1661
1833
|
}
|
1662
|
-
constructor(
|
1663
|
-
super(
|
1834
|
+
constructor(documentElement, body, headSnapshot) {
|
1835
|
+
super(body);
|
1836
|
+
this.documentElement = documentElement;
|
1664
1837
|
this.headSnapshot = headSnapshot;
|
1665
1838
|
}
|
1666
1839
|
clone() {
|
@@ -1675,14 +1848,16 @@ class PageSnapshot extends Snapshot {
|
|
1675
1848
|
for (const clonedPasswordInput of clonedElement.querySelectorAll('input[type="password"]')) {
|
1676
1849
|
clonedPasswordInput.value = "";
|
1677
1850
|
}
|
1678
|
-
return new PageSnapshot(clonedElement, this.headSnapshot);
|
1851
|
+
return new PageSnapshot(this.documentElement, clonedElement, this.headSnapshot);
|
1852
|
+
}
|
1853
|
+
get lang() {
|
1854
|
+
return this.documentElement.getAttribute("lang");
|
1679
1855
|
}
|
1680
1856
|
get headElement() {
|
1681
1857
|
return this.headSnapshot.element;
|
1682
1858
|
}
|
1683
1859
|
get rootLocation() {
|
1684
|
-
|
1685
|
-
const root = (_a = this.getSetting("root")) !== null && _a !== void 0 ? _a : "/";
|
1860
|
+
const root = this.getSetting("root") ?? "/";
|
1686
1861
|
return expandURL(root);
|
1687
1862
|
}
|
1688
1863
|
get cacheControlValue() {
|
@@ -1697,29 +1872,38 @@ class PageSnapshot extends Snapshot {
|
|
1697
1872
|
get isVisitable() {
|
1698
1873
|
return this.getSetting("visit-control") != "reload";
|
1699
1874
|
}
|
1875
|
+
get prefersViewTransitions() {
|
1876
|
+
return this.headSnapshot.getMetaValue("view-transition") === "same-origin";
|
1877
|
+
}
|
1878
|
+
get shouldMorphPage() {
|
1879
|
+
return this.getSetting("refresh-method") === "morph";
|
1880
|
+
}
|
1881
|
+
get shouldPreserveScrollPosition() {
|
1882
|
+
return this.getSetting("refresh-scroll") === "preserve";
|
1883
|
+
}
|
1700
1884
|
getSetting(name) {
|
1701
1885
|
return this.headSnapshot.getMetaValue(`turbo-${name}`);
|
1702
1886
|
}
|
1703
1887
|
}
|
1704
1888
|
|
1705
|
-
|
1706
|
-
|
1707
|
-
(
|
1708
|
-
|
1709
|
-
|
1710
|
-
|
1711
|
-
|
1712
|
-
|
1713
|
-
|
1714
|
-
|
1715
|
-
|
1716
|
-
|
1717
|
-
|
1718
|
-
|
1719
|
-
|
1720
|
-
|
1721
|
-
|
1722
|
-
}
|
1889
|
+
class ViewTransitioner {
|
1890
|
+
#viewTransitionStarted=false;
|
1891
|
+
#lastOperation=Promise.resolve();
|
1892
|
+
renderChange(useViewTransition, render) {
|
1893
|
+
if (useViewTransition && this.viewTransitionsAvailable && !this.#viewTransitionStarted) {
|
1894
|
+
this.#viewTransitionStarted = true;
|
1895
|
+
this.#lastOperation = this.#lastOperation.then((async () => {
|
1896
|
+
await document.startViewTransition(render).finished;
|
1897
|
+
}));
|
1898
|
+
} else {
|
1899
|
+
this.#lastOperation = this.#lastOperation.then(render);
|
1900
|
+
}
|
1901
|
+
return this.#lastOperation;
|
1902
|
+
}
|
1903
|
+
get viewTransitionsAvailable() {
|
1904
|
+
return document.startViewTransition;
|
1905
|
+
}
|
1906
|
+
}
|
1723
1907
|
|
1724
1908
|
const defaultOptions = {
|
1725
1909
|
action: "advance",
|
@@ -1731,29 +1915,52 @@ const defaultOptions = {
|
|
1731
1915
|
acceptsStreamResponse: false
|
1732
1916
|
};
|
1733
1917
|
|
1734
|
-
|
1918
|
+
const TimingMetric = {
|
1919
|
+
visitStart: "visitStart",
|
1920
|
+
requestStart: "requestStart",
|
1921
|
+
requestEnd: "requestEnd",
|
1922
|
+
visitEnd: "visitEnd"
|
1923
|
+
};
|
1924
|
+
|
1925
|
+
const VisitState = {
|
1926
|
+
initialized: "initialized",
|
1927
|
+
started: "started",
|
1928
|
+
canceled: "canceled",
|
1929
|
+
failed: "failed",
|
1930
|
+
completed: "completed"
|
1931
|
+
};
|
1932
|
+
|
1933
|
+
const SystemStatusCode = {
|
1934
|
+
networkFailure: 0,
|
1935
|
+
timeoutFailure: -1,
|
1936
|
+
contentTypeMismatch: -2
|
1937
|
+
};
|
1735
1938
|
|
1736
|
-
|
1737
|
-
|
1738
|
-
|
1739
|
-
|
1740
|
-
}
|
1939
|
+
const Direction = {
|
1940
|
+
advance: "forward",
|
1941
|
+
restore: "back",
|
1942
|
+
replace: "none"
|
1943
|
+
};
|
1741
1944
|
|
1742
1945
|
class Visit {
|
1946
|
+
identifier=uuid();
|
1947
|
+
timingMetrics={};
|
1948
|
+
followedRedirect=false;
|
1949
|
+
historyChanged=false;
|
1950
|
+
scrolled=false;
|
1951
|
+
shouldCacheSnapshot=true;
|
1952
|
+
acceptsStreamResponse=false;
|
1953
|
+
snapshotCached=false;
|
1954
|
+
state=VisitState.initialized;
|
1955
|
+
viewTransitioner=new ViewTransitioner;
|
1743
1956
|
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
1957
|
this.delegate = delegate;
|
1754
1958
|
this.location = location;
|
1755
1959
|
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} =
|
1960
|
+
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} = {
|
1961
|
+
...defaultOptions,
|
1962
|
+
...options
|
1963
|
+
};
|
1757
1964
|
this.action = action;
|
1758
1965
|
this.historyChanged = historyChanged;
|
1759
1966
|
this.referrer = referrer;
|
@@ -1761,12 +1968,14 @@ class Visit {
|
|
1761
1968
|
this.snapshotHTML = snapshotHTML;
|
1762
1969
|
this.response = response;
|
1763
1970
|
this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
|
1971
|
+
this.isPageRefresh = this.view.isPageRefresh(this);
|
1764
1972
|
this.visitCachedSnapshot = visitCachedSnapshot;
|
1765
1973
|
this.willRender = willRender;
|
1766
1974
|
this.updateHistory = updateHistory;
|
1767
1975
|
this.scrolled = !willRender;
|
1768
1976
|
this.shouldCacheSnapshot = shouldCacheSnapshot;
|
1769
1977
|
this.acceptsStreamResponse = acceptsStreamResponse;
|
1978
|
+
this.direction = direction || Direction[action];
|
1770
1979
|
}
|
1771
1980
|
get adapter() {
|
1772
1981
|
return this.delegate.adapter;
|
@@ -1803,10 +2012,10 @@ class Visit {
|
|
1803
2012
|
complete() {
|
1804
2013
|
if (this.state == VisitState.started) {
|
1805
2014
|
this.recordTimingMetric(TimingMetric.visitEnd);
|
2015
|
+
this.adapter.visitCompleted(this);
|
1806
2016
|
this.state = VisitState.completed;
|
1807
2017
|
this.followRedirect();
|
1808
2018
|
if (!this.followedRedirect) {
|
1809
|
-
this.adapter.visitCompleted(this);
|
1810
2019
|
this.delegate.visitCompleted(this);
|
1811
2020
|
}
|
1812
2021
|
}
|
@@ -1815,12 +2024,12 @@ class Visit {
|
|
1815
2024
|
if (this.state == VisitState.started) {
|
1816
2025
|
this.state = VisitState.failed;
|
1817
2026
|
this.adapter.visitFailed(this);
|
2027
|
+
this.delegate.visitCompleted(this);
|
1818
2028
|
}
|
1819
2029
|
}
|
1820
2030
|
changeHistory() {
|
1821
|
-
var _a;
|
1822
2031
|
if (!this.historyChanged && this.updateHistory) {
|
1823
|
-
const actionForHistory = this.location.href ===
|
2032
|
+
const actionForHistory = this.location.href === this.referrer?.href ? "replace" : this.action;
|
1824
2033
|
const method = getHistoryMethodForAction(actionForHistory);
|
1825
2034
|
this.history.update(method, this.location, this.restorationIdentifier);
|
1826
2035
|
this.historyChanged = true;
|
@@ -1867,8 +2076,8 @@ class Visit {
|
|
1867
2076
|
if (this.shouldCacheSnapshot) this.cacheSnapshot();
|
1868
2077
|
if (this.view.renderPromise) await this.view.renderPromise;
|
1869
2078
|
if (isSuccessful(statusCode) && responseHTML != null) {
|
1870
|
-
|
1871
|
-
this.
|
2079
|
+
const snapshot = PageSnapshot.fromHTMLString(responseHTML);
|
2080
|
+
await this.renderPageSnapshot(snapshot, false);
|
1872
2081
|
this.adapter.visitRendered(this);
|
1873
2082
|
this.complete();
|
1874
2083
|
} else {
|
@@ -1901,12 +2110,11 @@ class Visit {
|
|
1901
2110
|
const isPreview = this.shouldIssueRequest();
|
1902
2111
|
this.render((async () => {
|
1903
2112
|
this.cacheSnapshot();
|
1904
|
-
if (this.isSamePage) {
|
2113
|
+
if (this.isSamePage || this.isPageRefresh) {
|
1905
2114
|
this.adapter.visitRendered(this);
|
1906
2115
|
} else {
|
1907
2116
|
if (this.view.renderPromise) await this.view.renderPromise;
|
1908
|
-
await this.
|
1909
|
-
this.performScroll();
|
2117
|
+
await this.renderPageSnapshot(snapshot, isPreview);
|
1910
2118
|
this.adapter.visitRendered(this);
|
1911
2119
|
if (!isPreview) {
|
1912
2120
|
this.complete();
|
@@ -1916,8 +2124,7 @@ class Visit {
|
|
1916
2124
|
}
|
1917
2125
|
}
|
1918
2126
|
followRedirect() {
|
1919
|
-
|
1920
|
-
if (this.redirectedToLocation && !this.followedRedirect && ((_a = this.response) === null || _a === void 0 ? void 0 : _a.redirected)) {
|
2127
|
+
if (this.redirectedToLocation && !this.followedRedirect && this.response?.redirected) {
|
1921
2128
|
this.adapter.visitProposedToLocation(this.redirectedToLocation, {
|
1922
2129
|
action: "replace",
|
1923
2130
|
response: this.response,
|
@@ -1989,7 +2196,7 @@ class Visit {
|
|
1989
2196
|
this.finishRequest();
|
1990
2197
|
}
|
1991
2198
|
performScroll() {
|
1992
|
-
if (!this.scrolled && !this.view.forceReloaded) {
|
2199
|
+
if (!this.scrolled && !this.view.forceReloaded && !this.view.shouldPreserveScrollPosition(this)) {
|
1993
2200
|
if (this.action == "restore") {
|
1994
2201
|
this.scrollToRestoredPosition() || this.scrollToAnchor() || this.view.scrollToTop();
|
1995
2202
|
} else {
|
@@ -2019,8 +2226,10 @@ class Visit {
|
|
2019
2226
|
this.timingMetrics[metric] = (new Date).getTime();
|
2020
2227
|
}
|
2021
2228
|
getTimingMetrics() {
|
2022
|
-
return
|
2023
|
-
|
2229
|
+
return {
|
2230
|
+
...this.timingMetrics
|
2231
|
+
};
|
2232
|
+
}
|
2024
2233
|
getHistoryMethodForAction(action) {
|
2025
2234
|
switch (action) {
|
2026
2235
|
case "replace":
|
@@ -2051,12 +2260,16 @@ class Visit {
|
|
2051
2260
|
}
|
2052
2261
|
async render(callback) {
|
2053
2262
|
this.cancelRender();
|
2054
|
-
await
|
2055
|
-
this.frame = requestAnimationFrame((() => resolve()));
|
2056
|
-
}));
|
2263
|
+
this.frame = await nextRepaint();
|
2057
2264
|
await callback();
|
2058
2265
|
delete this.frame;
|
2059
2266
|
}
|
2267
|
+
async renderPageSnapshot(snapshot, isPreview) {
|
2268
|
+
await this.viewTransitioner.renderChange(this.view.shouldTransitionTo(snapshot), (async () => {
|
2269
|
+
await this.view.renderPage(snapshot, isPreview, this.willRender, this);
|
2270
|
+
this.performScroll();
|
2271
|
+
}));
|
2272
|
+
}
|
2060
2273
|
cancelRender() {
|
2061
2274
|
if (this.frame) {
|
2062
2275
|
cancelAnimationFrame(this.frame);
|
@@ -2070,15 +2283,16 @@ function isSuccessful(statusCode) {
|
|
2070
2283
|
}
|
2071
2284
|
|
2072
2285
|
class BrowserAdapter {
|
2286
|
+
progressBar=new ProgressBar;
|
2073
2287
|
constructor(session) {
|
2074
|
-
this.progressBar = new ProgressBar;
|
2075
|
-
this.showProgressBar = () => {
|
2076
|
-
this.progressBar.show();
|
2077
|
-
};
|
2078
2288
|
this.session = session;
|
2079
2289
|
}
|
2080
2290
|
visitProposedToLocation(location, options) {
|
2081
|
-
|
2291
|
+
if (locationIsVisitable(location, this.navigator.rootLocation)) {
|
2292
|
+
this.navigator.startVisit(location, options?.restorationIdentifier || uuid(), options);
|
2293
|
+
} else {
|
2294
|
+
window.location.href = location.toString();
|
2295
|
+
}
|
2082
2296
|
}
|
2083
2297
|
visitStarted(visit) {
|
2084
2298
|
this.location = visit.location;
|
@@ -2113,15 +2327,18 @@ class BrowserAdapter {
|
|
2113
2327
|
return visit.loadResponse();
|
2114
2328
|
}
|
2115
2329
|
}
|
2116
|
-
visitRequestFinished(_visit) {
|
2330
|
+
visitRequestFinished(_visit) {}
|
2331
|
+
visitCompleted(_visit) {
|
2117
2332
|
this.progressBar.setValue(1);
|
2118
2333
|
this.hideVisitProgressBar();
|
2119
2334
|
}
|
2120
|
-
visitCompleted(_visit) {}
|
2121
2335
|
pageInvalidated(reason) {
|
2122
2336
|
this.reload(reason);
|
2123
2337
|
}
|
2124
|
-
visitFailed(_visit) {
|
2338
|
+
visitFailed(_visit) {
|
2339
|
+
this.progressBar.setValue(1);
|
2340
|
+
this.hideVisitProgressBar();
|
2341
|
+
}
|
2125
2342
|
visitRendered(_visit) {}
|
2126
2343
|
formSubmissionStarted(_formSubmission) {
|
2127
2344
|
this.progressBar.setValue(0);
|
@@ -2153,12 +2370,14 @@ class BrowserAdapter {
|
|
2153
2370
|
delete this.formProgressBarTimeout;
|
2154
2371
|
}
|
2155
2372
|
}
|
2373
|
+
showProgressBar=() => {
|
2374
|
+
this.progressBar.show();
|
2375
|
+
};
|
2156
2376
|
reload(reason) {
|
2157
|
-
var _a;
|
2158
2377
|
dispatch("turbo:reload", {
|
2159
2378
|
detail: reason
|
2160
2379
|
});
|
2161
|
-
window.location.href =
|
2380
|
+
window.location.href = this.location?.toString() || window.location.href;
|
2162
2381
|
}
|
2163
2382
|
get navigator() {
|
2164
2383
|
return this.session.navigator;
|
@@ -2166,16 +2385,9 @@ class BrowserAdapter {
|
|
2166
2385
|
}
|
2167
2386
|
|
2168
2387
|
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
|
-
}
|
2388
|
+
selector="[data-turbo-temporary]";
|
2389
|
+
deprecatedSelector="[data-turbo-cache=false]";
|
2390
|
+
started=false;
|
2179
2391
|
start() {
|
2180
2392
|
if (!this.started) {
|
2181
2393
|
this.started = true;
|
@@ -2188,6 +2400,11 @@ class CacheObserver {
|
|
2188
2400
|
removeEventListener("turbo:before-cache", this.removeTemporaryElements, false);
|
2189
2401
|
}
|
2190
2402
|
}
|
2403
|
+
removeTemporaryElements=_event => {
|
2404
|
+
for (const element of this.temporaryElements) {
|
2405
|
+
element.remove();
|
2406
|
+
}
|
2407
|
+
};
|
2191
2408
|
get temporaryElements() {
|
2192
2409
|
return [ ...document.querySelectorAll(this.selector), ...this.temporaryElementsWithDeprecation ];
|
2193
2410
|
}
|
@@ -2216,41 +2433,40 @@ class FrameRedirector {
|
|
2216
2433
|
this.formSubmitObserver.stop();
|
2217
2434
|
}
|
2218
2435
|
shouldInterceptLinkClick(element, _location, _event) {
|
2219
|
-
return this
|
2436
|
+
return this.#shouldRedirect(element);
|
2220
2437
|
}
|
2221
2438
|
linkClickIntercepted(element, url, event) {
|
2222
|
-
const frame = this
|
2439
|
+
const frame = this.#findFrameElement(element);
|
2223
2440
|
if (frame) {
|
2224
2441
|
frame.delegate.linkClickIntercepted(element, url, event);
|
2225
2442
|
}
|
2226
2443
|
}
|
2227
2444
|
willSubmitForm(element, submitter) {
|
2228
|
-
return element.closest("turbo-frame") == null && this
|
2445
|
+
return element.closest("turbo-frame") == null && this.#shouldSubmit(element, submitter) && this.#shouldRedirect(element, submitter);
|
2229
2446
|
}
|
2230
2447
|
formSubmitted(element, submitter) {
|
2231
|
-
const frame = this
|
2448
|
+
const frame = this.#findFrameElement(element, submitter);
|
2232
2449
|
if (frame) {
|
2233
2450
|
frame.delegate.formSubmitted(element, submitter);
|
2234
2451
|
}
|
2235
2452
|
}
|
2236
|
-
shouldSubmit(form, submitter) {
|
2237
|
-
|
2238
|
-
const action = getAction(form, submitter);
|
2453
|
+
#shouldSubmit(form, submitter) {
|
2454
|
+
const action = getAction$1(form, submitter);
|
2239
2455
|
const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
|
2240
|
-
const rootLocation = expandURL(
|
2241
|
-
return this
|
2456
|
+
const rootLocation = expandURL(meta?.content ?? "/");
|
2457
|
+
return this.#shouldRedirect(form, submitter) && locationIsVisitable(action, rootLocation);
|
2242
2458
|
}
|
2243
|
-
shouldRedirect(element, submitter) {
|
2459
|
+
#shouldRedirect(element, submitter) {
|
2244
2460
|
const isNavigatable = element instanceof HTMLFormElement ? this.session.submissionIsNavigatable(element, submitter) : this.session.elementIsNavigatable(element);
|
2245
2461
|
if (isNavigatable) {
|
2246
|
-
const frame = this
|
2462
|
+
const frame = this.#findFrameElement(element, submitter);
|
2247
2463
|
return frame ? frame != element.closest("turbo-frame") : false;
|
2248
2464
|
} else {
|
2249
2465
|
return false;
|
2250
2466
|
}
|
2251
2467
|
}
|
2252
|
-
findFrameElement(element, submitter) {
|
2253
|
-
const id =
|
2468
|
+
#findFrameElement(element, submitter) {
|
2469
|
+
const id = submitter?.getAttribute("data-turbo-frame") || element.getAttribute("data-turbo-frame");
|
2254
2470
|
if (id && id != "_top") {
|
2255
2471
|
const frame = this.element.querySelector(`#${id}:not([disabled])`);
|
2256
2472
|
if (frame instanceof FrameElement) {
|
@@ -2261,32 +2477,20 @@ class FrameRedirector {
|
|
2261
2477
|
}
|
2262
2478
|
|
2263
2479
|
class History {
|
2480
|
+
location;
|
2481
|
+
restorationIdentifier=uuid();
|
2482
|
+
restorationData={};
|
2483
|
+
started=false;
|
2484
|
+
pageLoaded=false;
|
2485
|
+
currentIndex=0;
|
2264
2486
|
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
2487
|
this.delegate = delegate;
|
2285
2488
|
}
|
2286
2489
|
start() {
|
2287
2490
|
if (!this.started) {
|
2288
2491
|
addEventListener("popstate", this.onPopState, false);
|
2289
2492
|
addEventListener("load", this.onPageLoad, false);
|
2493
|
+
this.currentIndex = history.state?.turbo?.restorationIndex || 0;
|
2290
2494
|
this.started = true;
|
2291
2495
|
this.replace(new URL(window.location.href));
|
2292
2496
|
}
|
@@ -2305,9 +2509,11 @@ class History {
|
|
2305
2509
|
this.update(history.replaceState, location, restorationIdentifier);
|
2306
2510
|
}
|
2307
2511
|
update(method, location, restorationIdentifier = uuid()) {
|
2512
|
+
if (method === history.pushState) ++this.currentIndex;
|
2308
2513
|
const state = {
|
2309
2514
|
turbo: {
|
2310
|
-
restorationIdentifier: restorationIdentifier
|
2515
|
+
restorationIdentifier: restorationIdentifier,
|
2516
|
+
restorationIndex: this.currentIndex
|
2311
2517
|
}
|
2312
2518
|
};
|
2313
2519
|
method.call(history, state, "", location.href);
|
@@ -2320,12 +2526,14 @@ class History {
|
|
2320
2526
|
updateRestorationData(additionalData) {
|
2321
2527
|
const {restorationIdentifier: restorationIdentifier} = this;
|
2322
2528
|
const restorationData = this.restorationData[restorationIdentifier];
|
2323
|
-
this.restorationData[restorationIdentifier] =
|
2529
|
+
this.restorationData[restorationIdentifier] = {
|
2530
|
+
...restorationData,
|
2531
|
+
...additionalData
|
2532
|
+
};
|
2324
2533
|
}
|
2325
2534
|
assumeControlOfScrollRestoration() {
|
2326
|
-
var _a;
|
2327
2535
|
if (!this.previousScrollRestoration) {
|
2328
|
-
this.previousScrollRestoration =
|
2536
|
+
this.previousScrollRestoration = history.scrollRestoration ?? "auto";
|
2329
2537
|
history.scrollRestoration = "manual";
|
2330
2538
|
}
|
2331
2539
|
}
|
@@ -2335,6 +2543,23 @@ class History {
|
|
2335
2543
|
delete this.previousScrollRestoration;
|
2336
2544
|
}
|
2337
2545
|
}
|
2546
|
+
onPopState=event => {
|
2547
|
+
if (this.shouldHandlePopState()) {
|
2548
|
+
const {turbo: turbo} = event.state || {};
|
2549
|
+
if (turbo) {
|
2550
|
+
this.location = new URL(window.location.href);
|
2551
|
+
const {restorationIdentifier: restorationIdentifier, restorationIndex: restorationIndex} = turbo;
|
2552
|
+
this.restorationIdentifier = restorationIdentifier;
|
2553
|
+
const direction = restorationIndex > this.currentIndex ? "forward" : "back";
|
2554
|
+
this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(this.location, restorationIdentifier, direction);
|
2555
|
+
this.currentIndex = restorationIndex;
|
2556
|
+
}
|
2557
|
+
}
|
2558
|
+
};
|
2559
|
+
onPageLoad=async _event => {
|
2560
|
+
await nextMicrotask();
|
2561
|
+
this.pageLoaded = true;
|
2562
|
+
};
|
2338
2563
|
shouldHandlePopState() {
|
2339
2564
|
return this.pageIsLoaded();
|
2340
2565
|
}
|
@@ -2343,24 +2568,154 @@ class History {
|
|
2343
2568
|
}
|
2344
2569
|
}
|
2345
2570
|
|
2571
|
+
class LinkPrefetchObserver {
|
2572
|
+
started=false;
|
2573
|
+
#prefetchedLink=null;
|
2574
|
+
constructor(delegate, eventTarget) {
|
2575
|
+
this.delegate = delegate;
|
2576
|
+
this.eventTarget = eventTarget;
|
2577
|
+
}
|
2578
|
+
start() {
|
2579
|
+
if (this.started) return;
|
2580
|
+
if (this.eventTarget.readyState === "loading") {
|
2581
|
+
this.eventTarget.addEventListener("DOMContentLoaded", this.#enable, {
|
2582
|
+
once: true
|
2583
|
+
});
|
2584
|
+
} else {
|
2585
|
+
this.#enable();
|
2586
|
+
}
|
2587
|
+
}
|
2588
|
+
stop() {
|
2589
|
+
if (!this.started) return;
|
2590
|
+
this.eventTarget.removeEventListener("mouseenter", this.#tryToPrefetchRequest, {
|
2591
|
+
capture: true,
|
2592
|
+
passive: true
|
2593
|
+
});
|
2594
|
+
this.eventTarget.removeEventListener("mouseleave", this.#cancelRequestIfObsolete, {
|
2595
|
+
capture: true,
|
2596
|
+
passive: true
|
2597
|
+
});
|
2598
|
+
this.eventTarget.removeEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true);
|
2599
|
+
this.started = false;
|
2600
|
+
}
|
2601
|
+
#enable=() => {
|
2602
|
+
this.eventTarget.addEventListener("mouseenter", this.#tryToPrefetchRequest, {
|
2603
|
+
capture: true,
|
2604
|
+
passive: true
|
2605
|
+
});
|
2606
|
+
this.eventTarget.addEventListener("mouseleave", this.#cancelRequestIfObsolete, {
|
2607
|
+
capture: true,
|
2608
|
+
passive: true
|
2609
|
+
});
|
2610
|
+
this.eventTarget.addEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true);
|
2611
|
+
this.started = true;
|
2612
|
+
};
|
2613
|
+
#tryToPrefetchRequest=event => {
|
2614
|
+
if (getMetaContent("turbo-prefetch") === "false") return;
|
2615
|
+
const target = event.target;
|
2616
|
+
const isLink = target.matches && target.matches("a[href]:not([target^=_]):not([download])");
|
2617
|
+
if (isLink && this.#isPrefetchable(target)) {
|
2618
|
+
const link = target;
|
2619
|
+
const location = getLocationForLink(link);
|
2620
|
+
if (this.delegate.canPrefetchRequestToLocation(link, location)) {
|
2621
|
+
this.#prefetchedLink = link;
|
2622
|
+
const fetchRequest = new FetchRequest(this, FetchMethod.get, location, new URLSearchParams, target);
|
2623
|
+
prefetchCache.setLater(location.toString(), fetchRequest, this.#cacheTtl);
|
2624
|
+
}
|
2625
|
+
}
|
2626
|
+
};
|
2627
|
+
#cancelRequestIfObsolete=event => {
|
2628
|
+
if (event.target === this.#prefetchedLink) this.#cancelPrefetchRequest();
|
2629
|
+
};
|
2630
|
+
#cancelPrefetchRequest=() => {
|
2631
|
+
prefetchCache.clear();
|
2632
|
+
this.#prefetchedLink = null;
|
2633
|
+
};
|
2634
|
+
#tryToUsePrefetchedRequest=event => {
|
2635
|
+
if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "GET") {
|
2636
|
+
const cached = prefetchCache.get(event.detail.url.toString());
|
2637
|
+
if (cached) {
|
2638
|
+
event.detail.fetchRequest = cached;
|
2639
|
+
}
|
2640
|
+
prefetchCache.clear();
|
2641
|
+
}
|
2642
|
+
};
|
2643
|
+
prepareRequest(request) {
|
2644
|
+
const link = request.target;
|
2645
|
+
request.headers["X-Sec-Purpose"] = "prefetch";
|
2646
|
+
const turboFrame = link.closest("turbo-frame");
|
2647
|
+
const turboFrameTarget = link.getAttribute("data-turbo-frame") || turboFrame?.getAttribute("target") || turboFrame?.id;
|
2648
|
+
if (turboFrameTarget && turboFrameTarget !== "_top") {
|
2649
|
+
request.headers["Turbo-Frame"] = turboFrameTarget;
|
2650
|
+
}
|
2651
|
+
}
|
2652
|
+
requestSucceededWithResponse() {}
|
2653
|
+
requestStarted(fetchRequest) {}
|
2654
|
+
requestErrored(fetchRequest) {}
|
2655
|
+
requestFinished(fetchRequest) {}
|
2656
|
+
requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}
|
2657
|
+
requestFailedWithResponse(fetchRequest, fetchResponse) {}
|
2658
|
+
get #cacheTtl() {
|
2659
|
+
return Number(getMetaContent("turbo-prefetch-cache-time")) || cacheTtl;
|
2660
|
+
}
|
2661
|
+
#isPrefetchable(link) {
|
2662
|
+
const href = link.getAttribute("href");
|
2663
|
+
if (!href) return false;
|
2664
|
+
if (unfetchableLink(link)) return false;
|
2665
|
+
if (linkToTheSamePage(link)) return false;
|
2666
|
+
if (linkOptsOut(link)) return false;
|
2667
|
+
if (nonSafeLink(link)) return false;
|
2668
|
+
if (eventPrevented(link)) return false;
|
2669
|
+
return true;
|
2670
|
+
}
|
2671
|
+
}
|
2672
|
+
|
2673
|
+
const unfetchableLink = link => link.origin !== document.location.origin || ![ "http:", "https:" ].includes(link.protocol) || link.hasAttribute("target");
|
2674
|
+
|
2675
|
+
const linkToTheSamePage = link => link.pathname + link.search === document.location.pathname + document.location.search || link.href.startsWith("#");
|
2676
|
+
|
2677
|
+
const linkOptsOut = link => {
|
2678
|
+
if (link.getAttribute("data-turbo-prefetch") === "false") return true;
|
2679
|
+
if (link.getAttribute("data-turbo") === "false") return true;
|
2680
|
+
const turboPrefetchParent = findClosestRecursively(link, "[data-turbo-prefetch]");
|
2681
|
+
if (turboPrefetchParent && turboPrefetchParent.getAttribute("data-turbo-prefetch") === "false") return true;
|
2682
|
+
return false;
|
2683
|
+
};
|
2684
|
+
|
2685
|
+
const nonSafeLink = link => {
|
2686
|
+
const turboMethod = link.getAttribute("data-turbo-method");
|
2687
|
+
if (turboMethod && turboMethod.toLowerCase() !== "get") return true;
|
2688
|
+
if (isUJS(link)) return true;
|
2689
|
+
if (link.hasAttribute("data-turbo-confirm")) return true;
|
2690
|
+
if (link.hasAttribute("data-turbo-stream")) return true;
|
2691
|
+
return false;
|
2692
|
+
};
|
2693
|
+
|
2694
|
+
const isUJS = link => link.hasAttribute("data-remote") || link.hasAttribute("data-behavior") || link.hasAttribute("data-confirm") || link.hasAttribute("data-method");
|
2695
|
+
|
2696
|
+
const eventPrevented = link => {
|
2697
|
+
const event = dispatch("turbo:before-prefetch", {
|
2698
|
+
target: link,
|
2699
|
+
cancelable: true
|
2700
|
+
});
|
2701
|
+
return event.defaultPrevented;
|
2702
|
+
};
|
2703
|
+
|
2346
2704
|
class Navigator {
|
2347
2705
|
constructor(delegate) {
|
2348
2706
|
this.delegate = delegate;
|
2349
2707
|
}
|
2350
2708
|
proposeVisit(location, options = {}) {
|
2351
2709
|
if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
|
2352
|
-
|
2353
|
-
this.delegate.visitProposedToLocation(location, options);
|
2354
|
-
} else {
|
2355
|
-
window.location.href = location.toString();
|
2356
|
-
}
|
2710
|
+
this.delegate.visitProposedToLocation(location, options);
|
2357
2711
|
}
|
2358
2712
|
}
|
2359
2713
|
startVisit(locatable, restorationIdentifier, options = {}) {
|
2360
2714
|
this.stop();
|
2361
|
-
this.currentVisit = new Visit(this, expandURL(locatable), restorationIdentifier,
|
2362
|
-
referrer: this.location
|
2363
|
-
|
2715
|
+
this.currentVisit = new Visit(this, expandURL(locatable), restorationIdentifier, {
|
2716
|
+
referrer: this.location,
|
2717
|
+
...options
|
2718
|
+
});
|
2364
2719
|
this.currentVisit.start();
|
2365
2720
|
}
|
2366
2721
|
submitForm(form, submitter) {
|
@@ -2384,6 +2739,9 @@ class Navigator {
|
|
2384
2739
|
get view() {
|
2385
2740
|
return this.delegate.view;
|
2386
2741
|
}
|
2742
|
+
get rootLocation() {
|
2743
|
+
return this.view.snapshot.rootLocation;
|
2744
|
+
}
|
2387
2745
|
get history() {
|
2388
2746
|
return this.delegate.history;
|
2389
2747
|
}
|
@@ -2401,7 +2759,7 @@ class Navigator {
|
|
2401
2759
|
this.view.clearSnapshotCache();
|
2402
2760
|
}
|
2403
2761
|
const {statusCode: statusCode, redirected: redirected} = fetchResponse;
|
2404
|
-
const action = this
|
2762
|
+
const action = this.#getActionForFormSubmission(formSubmission, fetchResponse);
|
2405
2763
|
const visitOptions = {
|
2406
2764
|
action: action,
|
2407
2765
|
shouldCacheSnapshot: shouldCacheSnapshot,
|
@@ -2424,7 +2782,9 @@ class Navigator {
|
|
2424
2782
|
} else {
|
2425
2783
|
await this.view.renderPage(snapshot, false, true, this.currentVisit);
|
2426
2784
|
}
|
2427
|
-
|
2785
|
+
if (!snapshot.shouldPreserveScrollPosition) {
|
2786
|
+
this.view.scrollToTop();
|
2787
|
+
}
|
2428
2788
|
this.view.clearSnapshotCache();
|
2429
2789
|
}
|
2430
2790
|
}
|
@@ -2441,6 +2801,7 @@ class Navigator {
|
|
2441
2801
|
}
|
2442
2802
|
visitCompleted(visit) {
|
2443
2803
|
this.delegate.visitCompleted(visit);
|
2804
|
+
delete this.currentVisit;
|
2444
2805
|
}
|
2445
2806
|
locationWithActionIsSamePage(location, action) {
|
2446
2807
|
const anchor = getAnchor(location);
|
@@ -2457,35 +2818,27 @@ class Navigator {
|
|
2457
2818
|
get restorationIdentifier() {
|
2458
2819
|
return this.history.restorationIdentifier;
|
2459
2820
|
}
|
2460
|
-
getActionForFormSubmission(
|
2461
|
-
|
2821
|
+
#getActionForFormSubmission(formSubmission, fetchResponse) {
|
2822
|
+
const {submitter: submitter, formElement: formElement} = formSubmission;
|
2823
|
+
return getVisitAction(submitter, formElement) || this.#getDefaultAction(fetchResponse);
|
2824
|
+
}
|
2825
|
+
#getDefaultAction(fetchResponse) {
|
2826
|
+
const sameLocationRedirect = fetchResponse.redirected && fetchResponse.location.href === this.location?.href;
|
2827
|
+
return sameLocationRedirect ? "replace" : "advance";
|
2462
2828
|
}
|
2463
2829
|
}
|
2464
2830
|
|
2465
|
-
|
2466
|
-
|
2467
|
-
|
2468
|
-
|
2469
|
-
|
2470
|
-
|
2471
|
-
PageStage[PageStage["complete"] = 3] = "complete";
|
2472
|
-
})(PageStage || (PageStage = {}));
|
2831
|
+
const PageStage = {
|
2832
|
+
initial: 0,
|
2833
|
+
loading: 1,
|
2834
|
+
interactive: 2,
|
2835
|
+
complete: 3
|
2836
|
+
};
|
2473
2837
|
|
2474
2838
|
class PageObserver {
|
2839
|
+
stage=PageStage.initial;
|
2840
|
+
started=false;
|
2475
2841
|
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
2842
|
this.delegate = delegate;
|
2490
2843
|
}
|
2491
2844
|
start() {
|
@@ -2505,6 +2858,14 @@ class PageObserver {
|
|
2505
2858
|
this.started = false;
|
2506
2859
|
}
|
2507
2860
|
}
|
2861
|
+
interpretReadyState=() => {
|
2862
|
+
const {readyState: readyState} = this;
|
2863
|
+
if (readyState == "interactive") {
|
2864
|
+
this.pageIsInteractive();
|
2865
|
+
} else if (readyState == "complete") {
|
2866
|
+
this.pageIsComplete();
|
2867
|
+
}
|
2868
|
+
};
|
2508
2869
|
pageIsInteractive() {
|
2509
2870
|
if (this.stage == PageStage.loading) {
|
2510
2871
|
this.stage = PageStage.interactive;
|
@@ -2518,20 +2879,17 @@ class PageObserver {
|
|
2518
2879
|
this.delegate.pageLoaded();
|
2519
2880
|
}
|
2520
2881
|
}
|
2882
|
+
pageWillUnload=() => {
|
2883
|
+
this.delegate.pageWillUnload();
|
2884
|
+
};
|
2521
2885
|
get readyState() {
|
2522
2886
|
return document.readyState;
|
2523
2887
|
}
|
2524
2888
|
}
|
2525
2889
|
|
2526
2890
|
class ScrollObserver {
|
2891
|
+
started=false;
|
2527
2892
|
constructor(delegate) {
|
2528
|
-
this.started = false;
|
2529
|
-
this.onScroll = () => {
|
2530
|
-
this.updatePosition({
|
2531
|
-
x: window.pageXOffset,
|
2532
|
-
y: window.pageYOffset
|
2533
|
-
});
|
2534
|
-
};
|
2535
2893
|
this.delegate = delegate;
|
2536
2894
|
}
|
2537
2895
|
start() {
|
@@ -2547,6 +2905,12 @@ class ScrollObserver {
|
|
2547
2905
|
this.started = false;
|
2548
2906
|
}
|
2549
2907
|
}
|
2908
|
+
onScroll=() => {
|
2909
|
+
this.updatePosition({
|
2910
|
+
x: window.pageXOffset,
|
2911
|
+
y: window.pageYOffset
|
2912
|
+
});
|
2913
|
+
};
|
2550
2914
|
updatePosition(position) {
|
2551
2915
|
this.delegate.scrollPositionChanged(position);
|
2552
2916
|
}
|
@@ -2554,7 +2918,13 @@ class ScrollObserver {
|
|
2554
2918
|
|
2555
2919
|
class StreamMessageRenderer {
|
2556
2920
|
render({fragment: fragment}) {
|
2557
|
-
Bardo.preservingPermanentElements(this, getPermanentElementMapForFragment(fragment), (() =>
|
2921
|
+
Bardo.preservingPermanentElements(this, getPermanentElementMapForFragment(fragment), (() => {
|
2922
|
+
withAutofocusFromFragment(fragment, (() => {
|
2923
|
+
withPreservedFocus((() => {
|
2924
|
+
document.documentElement.appendChild(fragment);
|
2925
|
+
}));
|
2926
|
+
}));
|
2927
|
+
}));
|
2558
2928
|
}
|
2559
2929
|
enteringBardo(currentPermanentElement, newPermanentElement) {
|
2560
2930
|
newPermanentElement.replaceWith(currentPermanentElement.cloneNode(true));
|
@@ -2577,33 +2947,67 @@ function getPermanentElementMapForFragment(fragment) {
|
|
2577
2947
|
return permanentElementMap;
|
2578
2948
|
}
|
2579
2949
|
|
2950
|
+
async function withAutofocusFromFragment(fragment, callback) {
|
2951
|
+
const generatedID = `turbo-stream-autofocus-${uuid()}`;
|
2952
|
+
const turboStreams = fragment.querySelectorAll("turbo-stream");
|
2953
|
+
const elementWithAutofocus = firstAutofocusableElementInStreams(turboStreams);
|
2954
|
+
let willAutofocusId = null;
|
2955
|
+
if (elementWithAutofocus) {
|
2956
|
+
if (elementWithAutofocus.id) {
|
2957
|
+
willAutofocusId = elementWithAutofocus.id;
|
2958
|
+
} else {
|
2959
|
+
willAutofocusId = generatedID;
|
2960
|
+
}
|
2961
|
+
elementWithAutofocus.id = willAutofocusId;
|
2962
|
+
}
|
2963
|
+
callback();
|
2964
|
+
await nextRepaint();
|
2965
|
+
const hasNoActiveElement = document.activeElement == null || document.activeElement == document.body;
|
2966
|
+
if (hasNoActiveElement && willAutofocusId) {
|
2967
|
+
const elementToAutofocus = document.getElementById(willAutofocusId);
|
2968
|
+
if (elementIsFocusable(elementToAutofocus)) {
|
2969
|
+
elementToAutofocus.focus();
|
2970
|
+
}
|
2971
|
+
if (elementToAutofocus && elementToAutofocus.id == generatedID) {
|
2972
|
+
elementToAutofocus.removeAttribute("id");
|
2973
|
+
}
|
2974
|
+
}
|
2975
|
+
}
|
2976
|
+
|
2977
|
+
async function withPreservedFocus(callback) {
|
2978
|
+
const [activeElementBeforeRender, activeElementAfterRender] = await around(callback, (() => document.activeElement));
|
2979
|
+
const restoreFocusTo = activeElementBeforeRender && activeElementBeforeRender.id;
|
2980
|
+
if (restoreFocusTo) {
|
2981
|
+
const elementToFocus = document.getElementById(restoreFocusTo);
|
2982
|
+
if (elementIsFocusable(elementToFocus) && elementToFocus != activeElementAfterRender) {
|
2983
|
+
elementToFocus.focus();
|
2984
|
+
}
|
2985
|
+
}
|
2986
|
+
}
|
2987
|
+
|
2988
|
+
function firstAutofocusableElementInStreams(nodeListOfStreamElements) {
|
2989
|
+
for (const streamElement of nodeListOfStreamElements) {
|
2990
|
+
const elementWithAutofocus = queryAutofocusableElement(streamElement.templateElement.content);
|
2991
|
+
if (elementWithAutofocus) return elementWithAutofocus;
|
2992
|
+
}
|
2993
|
+
return null;
|
2994
|
+
}
|
2995
|
+
|
2580
2996
|
class StreamObserver {
|
2997
|
+
sources=new Set;
|
2998
|
+
#started=false;
|
2581
2999
|
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
3000
|
this.delegate = delegate;
|
2597
3001
|
}
|
2598
3002
|
start() {
|
2599
|
-
if (!this
|
2600
|
-
this
|
3003
|
+
if (!this.#started) {
|
3004
|
+
this.#started = true;
|
2601
3005
|
addEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
|
2602
3006
|
}
|
2603
3007
|
}
|
2604
3008
|
stop() {
|
2605
|
-
if (this
|
2606
|
-
this
|
3009
|
+
if (this.#started) {
|
3010
|
+
this.#started = false;
|
2607
3011
|
removeEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
|
2608
3012
|
}
|
2609
3013
|
}
|
@@ -2622,6 +3026,18 @@ class StreamObserver {
|
|
2622
3026
|
streamSourceIsConnected(source) {
|
2623
3027
|
return this.sources.has(source);
|
2624
3028
|
}
|
3029
|
+
inspectFetchResponse=event => {
|
3030
|
+
const response = fetchResponseFromEvent(event);
|
3031
|
+
if (response && fetchResponseIsStream(response)) {
|
3032
|
+
event.preventDefault();
|
3033
|
+
this.receiveMessageResponse(response);
|
3034
|
+
}
|
3035
|
+
};
|
3036
|
+
receiveMessageEvent=event => {
|
3037
|
+
if (this.#started && typeof event.data == "string") {
|
3038
|
+
this.receiveMessageHTML(event.data);
|
3039
|
+
}
|
3040
|
+
};
|
2625
3041
|
async receiveMessageResponse(response) {
|
2626
3042
|
const html = await response.responseHTML;
|
2627
3043
|
if (html) {
|
@@ -2633,48 +3049,660 @@ class StreamObserver {
|
|
2633
3049
|
}
|
2634
3050
|
}
|
2635
3051
|
|
2636
|
-
function fetchResponseFromEvent(event) {
|
2637
|
-
|
2638
|
-
|
2639
|
-
|
2640
|
-
|
2641
|
-
|
3052
|
+
function fetchResponseFromEvent(event) {
|
3053
|
+
const fetchResponse = event.detail?.fetchResponse;
|
3054
|
+
if (fetchResponse instanceof FetchResponse) {
|
3055
|
+
return fetchResponse;
|
3056
|
+
}
|
3057
|
+
}
|
3058
|
+
|
3059
|
+
function fetchResponseIsStream(response) {
|
3060
|
+
const contentType = response.contentType ?? "";
|
3061
|
+
return contentType.startsWith(StreamMessage.contentType);
|
3062
|
+
}
|
3063
|
+
|
3064
|
+
class ErrorRenderer extends Renderer {
|
3065
|
+
static renderElement(currentElement, newElement) {
|
3066
|
+
const {documentElement: documentElement, body: body} = document;
|
3067
|
+
documentElement.replaceChild(newElement, body);
|
3068
|
+
}
|
3069
|
+
async render() {
|
3070
|
+
this.replaceHeadAndBody();
|
3071
|
+
this.activateScriptElements();
|
3072
|
+
}
|
3073
|
+
replaceHeadAndBody() {
|
3074
|
+
const {documentElement: documentElement, head: head} = document;
|
3075
|
+
documentElement.replaceChild(this.newHead, head);
|
3076
|
+
this.renderElement(this.currentElement, this.newElement);
|
3077
|
+
}
|
3078
|
+
activateScriptElements() {
|
3079
|
+
for (const replaceableElement of this.scriptElements) {
|
3080
|
+
const parentNode = replaceableElement.parentNode;
|
3081
|
+
if (parentNode) {
|
3082
|
+
const element = activateScriptElement(replaceableElement);
|
3083
|
+
parentNode.replaceChild(element, replaceableElement);
|
3084
|
+
}
|
3085
|
+
}
|
3086
|
+
}
|
3087
|
+
get newHead() {
|
3088
|
+
return this.newSnapshot.headSnapshot.element;
|
3089
|
+
}
|
3090
|
+
get scriptElements() {
|
3091
|
+
return document.documentElement.querySelectorAll("script");
|
3092
|
+
}
|
3093
|
+
}
|
3094
|
+
|
3095
|
+
var Idiomorph = function() {
|
3096
|
+
let EMPTY_SET = new Set;
|
3097
|
+
let defaults = {
|
3098
|
+
morphStyle: "outerHTML",
|
3099
|
+
callbacks: {
|
3100
|
+
beforeNodeAdded: noOp,
|
3101
|
+
afterNodeAdded: noOp,
|
3102
|
+
beforeNodeMorphed: noOp,
|
3103
|
+
afterNodeMorphed: noOp,
|
3104
|
+
beforeNodeRemoved: noOp,
|
3105
|
+
afterNodeRemoved: noOp,
|
3106
|
+
beforeAttributeUpdated: noOp
|
3107
|
+
},
|
3108
|
+
head: {
|
3109
|
+
style: "merge",
|
3110
|
+
shouldPreserve: function(elt) {
|
3111
|
+
return elt.getAttribute("im-preserve") === "true";
|
3112
|
+
},
|
3113
|
+
shouldReAppend: function(elt) {
|
3114
|
+
return elt.getAttribute("im-re-append") === "true";
|
3115
|
+
},
|
3116
|
+
shouldRemove: noOp,
|
3117
|
+
afterHeadMorphed: noOp
|
3118
|
+
}
|
3119
|
+
};
|
3120
|
+
function morph(oldNode, newContent, config = {}) {
|
3121
|
+
if (oldNode instanceof Document) {
|
3122
|
+
oldNode = oldNode.documentElement;
|
3123
|
+
}
|
3124
|
+
if (typeof newContent === "string") {
|
3125
|
+
newContent = parseContent(newContent);
|
3126
|
+
}
|
3127
|
+
let normalizedContent = normalizeContent(newContent);
|
3128
|
+
let ctx = createMorphContext(oldNode, normalizedContent, config);
|
3129
|
+
return morphNormalizedContent(oldNode, normalizedContent, ctx);
|
3130
|
+
}
|
3131
|
+
function morphNormalizedContent(oldNode, normalizedNewContent, ctx) {
|
3132
|
+
if (ctx.head.block) {
|
3133
|
+
let oldHead = oldNode.querySelector("head");
|
3134
|
+
let newHead = normalizedNewContent.querySelector("head");
|
3135
|
+
if (oldHead && newHead) {
|
3136
|
+
let promises = handleHeadElement(newHead, oldHead, ctx);
|
3137
|
+
Promise.all(promises).then((function() {
|
3138
|
+
morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, {
|
3139
|
+
head: {
|
3140
|
+
block: false,
|
3141
|
+
ignore: true
|
3142
|
+
}
|
3143
|
+
}));
|
3144
|
+
}));
|
3145
|
+
return;
|
3146
|
+
}
|
3147
|
+
}
|
3148
|
+
if (ctx.morphStyle === "innerHTML") {
|
3149
|
+
morphChildren(normalizedNewContent, oldNode, ctx);
|
3150
|
+
return oldNode.children;
|
3151
|
+
} else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) {
|
3152
|
+
let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx);
|
3153
|
+
let previousSibling = bestMatch?.previousSibling;
|
3154
|
+
let nextSibling = bestMatch?.nextSibling;
|
3155
|
+
let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx);
|
3156
|
+
if (bestMatch) {
|
3157
|
+
return insertSiblings(previousSibling, morphedNode, nextSibling);
|
3158
|
+
} else {
|
3159
|
+
return [];
|
3160
|
+
}
|
3161
|
+
} else {
|
3162
|
+
throw "Do not understand how to morph style " + ctx.morphStyle;
|
3163
|
+
}
|
3164
|
+
}
|
3165
|
+
function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
|
3166
|
+
return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement && possibleActiveElement !== document.body;
|
3167
|
+
}
|
3168
|
+
function morphOldNodeTo(oldNode, newContent, ctx) {
|
3169
|
+
if (ctx.ignoreActive && oldNode === document.activeElement) ; else if (newContent == null) {
|
3170
|
+
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
|
3171
|
+
oldNode.remove();
|
3172
|
+
ctx.callbacks.afterNodeRemoved(oldNode);
|
3173
|
+
return null;
|
3174
|
+
} else if (!isSoftMatch(oldNode, newContent)) {
|
3175
|
+
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
|
3176
|
+
if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode;
|
3177
|
+
oldNode.parentElement.replaceChild(newContent, oldNode);
|
3178
|
+
ctx.callbacks.afterNodeAdded(newContent);
|
3179
|
+
ctx.callbacks.afterNodeRemoved(oldNode);
|
3180
|
+
return newContent;
|
3181
|
+
} else {
|
3182
|
+
if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return oldNode;
|
3183
|
+
if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) ; else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") {
|
3184
|
+
handleHeadElement(newContent, oldNode, ctx);
|
3185
|
+
} else {
|
3186
|
+
syncNodeFrom(newContent, oldNode, ctx);
|
3187
|
+
if (!ignoreValueOfActiveElement(oldNode, ctx)) {
|
3188
|
+
morphChildren(newContent, oldNode, ctx);
|
3189
|
+
}
|
3190
|
+
}
|
3191
|
+
ctx.callbacks.afterNodeMorphed(oldNode, newContent);
|
3192
|
+
return oldNode;
|
3193
|
+
}
|
3194
|
+
}
|
3195
|
+
function morphChildren(newParent, oldParent, ctx) {
|
3196
|
+
let nextNewChild = newParent.firstChild;
|
3197
|
+
let insertionPoint = oldParent.firstChild;
|
3198
|
+
let newChild;
|
3199
|
+
while (nextNewChild) {
|
3200
|
+
newChild = nextNewChild;
|
3201
|
+
nextNewChild = newChild.nextSibling;
|
3202
|
+
if (insertionPoint == null) {
|
3203
|
+
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
3204
|
+
oldParent.appendChild(newChild);
|
3205
|
+
ctx.callbacks.afterNodeAdded(newChild);
|
3206
|
+
removeIdsFromConsideration(ctx, newChild);
|
3207
|
+
continue;
|
3208
|
+
}
|
3209
|
+
if (isIdSetMatch(newChild, insertionPoint, ctx)) {
|
3210
|
+
morphOldNodeTo(insertionPoint, newChild, ctx);
|
3211
|
+
insertionPoint = insertionPoint.nextSibling;
|
3212
|
+
removeIdsFromConsideration(ctx, newChild);
|
3213
|
+
continue;
|
3214
|
+
}
|
3215
|
+
let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx);
|
3216
|
+
if (idSetMatch) {
|
3217
|
+
insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx);
|
3218
|
+
morphOldNodeTo(idSetMatch, newChild, ctx);
|
3219
|
+
removeIdsFromConsideration(ctx, newChild);
|
3220
|
+
continue;
|
3221
|
+
}
|
3222
|
+
let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx);
|
3223
|
+
if (softMatch) {
|
3224
|
+
insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx);
|
3225
|
+
morphOldNodeTo(softMatch, newChild, ctx);
|
3226
|
+
removeIdsFromConsideration(ctx, newChild);
|
3227
|
+
continue;
|
3228
|
+
}
|
3229
|
+
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
3230
|
+
oldParent.insertBefore(newChild, insertionPoint);
|
3231
|
+
ctx.callbacks.afterNodeAdded(newChild);
|
3232
|
+
removeIdsFromConsideration(ctx, newChild);
|
3233
|
+
}
|
3234
|
+
while (insertionPoint !== null) {
|
3235
|
+
let tempNode = insertionPoint;
|
3236
|
+
insertionPoint = insertionPoint.nextSibling;
|
3237
|
+
removeNode(tempNode, ctx);
|
3238
|
+
}
|
3239
|
+
}
|
3240
|
+
function ignoreAttribute(attr, to, updateType, ctx) {
|
3241
|
+
if (attr === "value" && ctx.ignoreActiveValue && to === document.activeElement) {
|
3242
|
+
return true;
|
3243
|
+
}
|
3244
|
+
return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false;
|
3245
|
+
}
|
3246
|
+
function syncNodeFrom(from, to, ctx) {
|
3247
|
+
let type = from.nodeType;
|
3248
|
+
if (type === 1) {
|
3249
|
+
const fromAttributes = from.attributes;
|
3250
|
+
const toAttributes = to.attributes;
|
3251
|
+
for (const fromAttribute of fromAttributes) {
|
3252
|
+
if (ignoreAttribute(fromAttribute.name, to, "update", ctx)) {
|
3253
|
+
continue;
|
3254
|
+
}
|
3255
|
+
if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) {
|
3256
|
+
to.setAttribute(fromAttribute.name, fromAttribute.value);
|
3257
|
+
}
|
3258
|
+
}
|
3259
|
+
for (let i = toAttributes.length - 1; 0 <= i; i--) {
|
3260
|
+
const toAttribute = toAttributes[i];
|
3261
|
+
if (ignoreAttribute(toAttribute.name, to, "remove", ctx)) {
|
3262
|
+
continue;
|
3263
|
+
}
|
3264
|
+
if (!from.hasAttribute(toAttribute.name)) {
|
3265
|
+
to.removeAttribute(toAttribute.name);
|
3266
|
+
}
|
3267
|
+
}
|
3268
|
+
}
|
3269
|
+
if (type === 8 || type === 3) {
|
3270
|
+
if (to.nodeValue !== from.nodeValue) {
|
3271
|
+
to.nodeValue = from.nodeValue;
|
3272
|
+
}
|
3273
|
+
}
|
3274
|
+
if (!ignoreValueOfActiveElement(to, ctx)) {
|
3275
|
+
syncInputValue(from, to, ctx);
|
3276
|
+
}
|
3277
|
+
}
|
3278
|
+
function syncBooleanAttribute(from, to, attributeName, ctx) {
|
3279
|
+
if (from[attributeName] !== to[attributeName]) {
|
3280
|
+
let ignoreUpdate = ignoreAttribute(attributeName, to, "update", ctx);
|
3281
|
+
if (!ignoreUpdate) {
|
3282
|
+
to[attributeName] = from[attributeName];
|
3283
|
+
}
|
3284
|
+
if (from[attributeName]) {
|
3285
|
+
if (!ignoreUpdate) {
|
3286
|
+
to.setAttribute(attributeName, from[attributeName]);
|
3287
|
+
}
|
3288
|
+
} else {
|
3289
|
+
if (!ignoreAttribute(attributeName, to, "remove", ctx)) {
|
3290
|
+
to.removeAttribute(attributeName);
|
3291
|
+
}
|
3292
|
+
}
|
3293
|
+
}
|
3294
|
+
}
|
3295
|
+
function syncInputValue(from, to, ctx) {
|
3296
|
+
if (from instanceof HTMLInputElement && to instanceof HTMLInputElement && from.type !== "file") {
|
3297
|
+
let fromValue = from.value;
|
3298
|
+
let toValue = to.value;
|
3299
|
+
syncBooleanAttribute(from, to, "checked", ctx);
|
3300
|
+
syncBooleanAttribute(from, to, "disabled", ctx);
|
3301
|
+
if (!from.hasAttribute("value")) {
|
3302
|
+
if (!ignoreAttribute("value", to, "remove", ctx)) {
|
3303
|
+
to.value = "";
|
3304
|
+
to.removeAttribute("value");
|
3305
|
+
}
|
3306
|
+
} else if (fromValue !== toValue) {
|
3307
|
+
if (!ignoreAttribute("value", to, "update", ctx)) {
|
3308
|
+
to.setAttribute("value", fromValue);
|
3309
|
+
to.value = fromValue;
|
3310
|
+
}
|
3311
|
+
}
|
3312
|
+
} else if (from instanceof HTMLOptionElement) {
|
3313
|
+
syncBooleanAttribute(from, to, "selected", ctx);
|
3314
|
+
} else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) {
|
3315
|
+
let fromValue = from.value;
|
3316
|
+
let toValue = to.value;
|
3317
|
+
if (ignoreAttribute("value", to, "update", ctx)) {
|
3318
|
+
return;
|
3319
|
+
}
|
3320
|
+
if (fromValue !== toValue) {
|
3321
|
+
to.value = fromValue;
|
3322
|
+
}
|
3323
|
+
if (to.firstChild && to.firstChild.nodeValue !== fromValue) {
|
3324
|
+
to.firstChild.nodeValue = fromValue;
|
3325
|
+
}
|
3326
|
+
}
|
3327
|
+
}
|
3328
|
+
function handleHeadElement(newHeadTag, currentHead, ctx) {
|
3329
|
+
let added = [];
|
3330
|
+
let removed = [];
|
3331
|
+
let preserved = [];
|
3332
|
+
let nodesToAppend = [];
|
3333
|
+
let headMergeStyle = ctx.head.style;
|
3334
|
+
let srcToNewHeadNodes = new Map;
|
3335
|
+
for (const newHeadChild of newHeadTag.children) {
|
3336
|
+
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
|
3337
|
+
}
|
3338
|
+
for (const currentHeadElt of currentHead.children) {
|
3339
|
+
let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
|
3340
|
+
let isReAppended = ctx.head.shouldReAppend(currentHeadElt);
|
3341
|
+
let isPreserved = ctx.head.shouldPreserve(currentHeadElt);
|
3342
|
+
if (inNewContent || isPreserved) {
|
3343
|
+
if (isReAppended) {
|
3344
|
+
removed.push(currentHeadElt);
|
3345
|
+
} else {
|
3346
|
+
srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
|
3347
|
+
preserved.push(currentHeadElt);
|
3348
|
+
}
|
3349
|
+
} else {
|
3350
|
+
if (headMergeStyle === "append") {
|
3351
|
+
if (isReAppended) {
|
3352
|
+
removed.push(currentHeadElt);
|
3353
|
+
nodesToAppend.push(currentHeadElt);
|
3354
|
+
}
|
3355
|
+
} else {
|
3356
|
+
if (ctx.head.shouldRemove(currentHeadElt) !== false) {
|
3357
|
+
removed.push(currentHeadElt);
|
3358
|
+
}
|
3359
|
+
}
|
3360
|
+
}
|
3361
|
+
}
|
3362
|
+
nodesToAppend.push(...srcToNewHeadNodes.values());
|
3363
|
+
let promises = [];
|
3364
|
+
for (const newNode of nodesToAppend) {
|
3365
|
+
let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild;
|
3366
|
+
if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
|
3367
|
+
if (newElt.href || newElt.src) {
|
3368
|
+
let resolve = null;
|
3369
|
+
let promise = new Promise((function(_resolve) {
|
3370
|
+
resolve = _resolve;
|
3371
|
+
}));
|
3372
|
+
newElt.addEventListener("load", (function() {
|
3373
|
+
resolve();
|
3374
|
+
}));
|
3375
|
+
promises.push(promise);
|
3376
|
+
}
|
3377
|
+
currentHead.appendChild(newElt);
|
3378
|
+
ctx.callbacks.afterNodeAdded(newElt);
|
3379
|
+
added.push(newElt);
|
3380
|
+
}
|
3381
|
+
}
|
3382
|
+
for (const removedElement of removed) {
|
3383
|
+
if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
|
3384
|
+
currentHead.removeChild(removedElement);
|
3385
|
+
ctx.callbacks.afterNodeRemoved(removedElement);
|
3386
|
+
}
|
3387
|
+
}
|
3388
|
+
ctx.head.afterHeadMorphed(currentHead, {
|
3389
|
+
added: added,
|
3390
|
+
kept: preserved,
|
3391
|
+
removed: removed
|
3392
|
+
});
|
3393
|
+
return promises;
|
3394
|
+
}
|
3395
|
+
function noOp() {}
|
3396
|
+
function mergeDefaults(config) {
|
3397
|
+
let finalConfig = {};
|
3398
|
+
Object.assign(finalConfig, defaults);
|
3399
|
+
Object.assign(finalConfig, config);
|
3400
|
+
finalConfig.callbacks = {};
|
3401
|
+
Object.assign(finalConfig.callbacks, defaults.callbacks);
|
3402
|
+
Object.assign(finalConfig.callbacks, config.callbacks);
|
3403
|
+
finalConfig.head = {};
|
3404
|
+
Object.assign(finalConfig.head, defaults.head);
|
3405
|
+
Object.assign(finalConfig.head, config.head);
|
3406
|
+
return finalConfig;
|
3407
|
+
}
|
3408
|
+
function createMorphContext(oldNode, newContent, config) {
|
3409
|
+
config = mergeDefaults(config);
|
3410
|
+
return {
|
3411
|
+
target: oldNode,
|
3412
|
+
newContent: newContent,
|
3413
|
+
config: config,
|
3414
|
+
morphStyle: config.morphStyle,
|
3415
|
+
ignoreActive: config.ignoreActive,
|
3416
|
+
ignoreActiveValue: config.ignoreActiveValue,
|
3417
|
+
idMap: createIdMap(oldNode, newContent),
|
3418
|
+
deadIds: new Set,
|
3419
|
+
callbacks: config.callbacks,
|
3420
|
+
head: config.head
|
3421
|
+
};
|
3422
|
+
}
|
3423
|
+
function isIdSetMatch(node1, node2, ctx) {
|
3424
|
+
if (node1 == null || node2 == null) {
|
3425
|
+
return false;
|
3426
|
+
}
|
3427
|
+
if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) {
|
3428
|
+
if (node1.id !== "" && node1.id === node2.id) {
|
3429
|
+
return true;
|
3430
|
+
} else {
|
3431
|
+
return getIdIntersectionCount(ctx, node1, node2) > 0;
|
3432
|
+
}
|
3433
|
+
}
|
3434
|
+
return false;
|
3435
|
+
}
|
3436
|
+
function isSoftMatch(node1, node2) {
|
3437
|
+
if (node1 == null || node2 == null) {
|
3438
|
+
return false;
|
3439
|
+
}
|
3440
|
+
return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName;
|
3441
|
+
}
|
3442
|
+
function removeNodesBetween(startInclusive, endExclusive, ctx) {
|
3443
|
+
while (startInclusive !== endExclusive) {
|
3444
|
+
let tempNode = startInclusive;
|
3445
|
+
startInclusive = startInclusive.nextSibling;
|
3446
|
+
removeNode(tempNode, ctx);
|
3447
|
+
}
|
3448
|
+
removeIdsFromConsideration(ctx, endExclusive);
|
3449
|
+
return endExclusive.nextSibling;
|
3450
|
+
}
|
3451
|
+
function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
|
3452
|
+
let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent);
|
3453
|
+
let potentialMatch = null;
|
3454
|
+
if (newChildPotentialIdCount > 0) {
|
3455
|
+
let potentialMatch = insertionPoint;
|
3456
|
+
let otherMatchCount = 0;
|
3457
|
+
while (potentialMatch != null) {
|
3458
|
+
if (isIdSetMatch(newChild, potentialMatch, ctx)) {
|
3459
|
+
return potentialMatch;
|
3460
|
+
}
|
3461
|
+
otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent);
|
3462
|
+
if (otherMatchCount > newChildPotentialIdCount) {
|
3463
|
+
return null;
|
3464
|
+
}
|
3465
|
+
potentialMatch = potentialMatch.nextSibling;
|
3466
|
+
}
|
3467
|
+
}
|
3468
|
+
return potentialMatch;
|
3469
|
+
}
|
3470
|
+
function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
|
3471
|
+
let potentialSoftMatch = insertionPoint;
|
3472
|
+
let nextSibling = newChild.nextSibling;
|
3473
|
+
let siblingSoftMatchCount = 0;
|
3474
|
+
while (potentialSoftMatch != null) {
|
3475
|
+
if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) {
|
3476
|
+
return null;
|
3477
|
+
}
|
3478
|
+
if (isSoftMatch(newChild, potentialSoftMatch)) {
|
3479
|
+
return potentialSoftMatch;
|
3480
|
+
}
|
3481
|
+
if (isSoftMatch(nextSibling, potentialSoftMatch)) {
|
3482
|
+
siblingSoftMatchCount++;
|
3483
|
+
nextSibling = nextSibling.nextSibling;
|
3484
|
+
if (siblingSoftMatchCount >= 2) {
|
3485
|
+
return null;
|
3486
|
+
}
|
3487
|
+
}
|
3488
|
+
potentialSoftMatch = potentialSoftMatch.nextSibling;
|
3489
|
+
}
|
3490
|
+
return potentialSoftMatch;
|
3491
|
+
}
|
3492
|
+
function parseContent(newContent) {
|
3493
|
+
let parser = new DOMParser;
|
3494
|
+
let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, "");
|
3495
|
+
if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) {
|
3496
|
+
let content = parser.parseFromString(newContent, "text/html");
|
3497
|
+
if (contentWithSvgsRemoved.match(/<\/html>/)) {
|
3498
|
+
content.generatedByIdiomorph = true;
|
3499
|
+
return content;
|
3500
|
+
} else {
|
3501
|
+
let htmlElement = content.firstChild;
|
3502
|
+
if (htmlElement) {
|
3503
|
+
htmlElement.generatedByIdiomorph = true;
|
3504
|
+
return htmlElement;
|
3505
|
+
} else {
|
3506
|
+
return null;
|
3507
|
+
}
|
3508
|
+
}
|
3509
|
+
} else {
|
3510
|
+
let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html");
|
3511
|
+
let content = responseDoc.body.querySelector("template").content;
|
3512
|
+
content.generatedByIdiomorph = true;
|
3513
|
+
return content;
|
3514
|
+
}
|
3515
|
+
}
|
3516
|
+
function normalizeContent(newContent) {
|
3517
|
+
if (newContent == null) {
|
3518
|
+
const dummyParent = document.createElement("div");
|
3519
|
+
return dummyParent;
|
3520
|
+
} else if (newContent.generatedByIdiomorph) {
|
3521
|
+
return newContent;
|
3522
|
+
} else if (newContent instanceof Node) {
|
3523
|
+
const dummyParent = document.createElement("div");
|
3524
|
+
dummyParent.append(newContent);
|
3525
|
+
return dummyParent;
|
3526
|
+
} else {
|
3527
|
+
const dummyParent = document.createElement("div");
|
3528
|
+
for (const elt of [ ...newContent ]) {
|
3529
|
+
dummyParent.append(elt);
|
3530
|
+
}
|
3531
|
+
return dummyParent;
|
3532
|
+
}
|
3533
|
+
}
|
3534
|
+
function insertSiblings(previousSibling, morphedNode, nextSibling) {
|
3535
|
+
let stack = [];
|
3536
|
+
let added = [];
|
3537
|
+
while (previousSibling != null) {
|
3538
|
+
stack.push(previousSibling);
|
3539
|
+
previousSibling = previousSibling.previousSibling;
|
3540
|
+
}
|
3541
|
+
while (stack.length > 0) {
|
3542
|
+
let node = stack.pop();
|
3543
|
+
added.push(node);
|
3544
|
+
morphedNode.parentElement.insertBefore(node, morphedNode);
|
3545
|
+
}
|
3546
|
+
added.push(morphedNode);
|
3547
|
+
while (nextSibling != null) {
|
3548
|
+
stack.push(nextSibling);
|
3549
|
+
added.push(nextSibling);
|
3550
|
+
nextSibling = nextSibling.nextSibling;
|
3551
|
+
}
|
3552
|
+
while (stack.length > 0) {
|
3553
|
+
morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling);
|
3554
|
+
}
|
3555
|
+
return added;
|
3556
|
+
}
|
3557
|
+
function findBestNodeMatch(newContent, oldNode, ctx) {
|
3558
|
+
let currentElement;
|
3559
|
+
currentElement = newContent.firstChild;
|
3560
|
+
let bestElement = currentElement;
|
3561
|
+
let score = 0;
|
3562
|
+
while (currentElement) {
|
3563
|
+
let newScore = scoreElement(currentElement, oldNode, ctx);
|
3564
|
+
if (newScore > score) {
|
3565
|
+
bestElement = currentElement;
|
3566
|
+
score = newScore;
|
3567
|
+
}
|
3568
|
+
currentElement = currentElement.nextSibling;
|
3569
|
+
}
|
3570
|
+
return bestElement;
|
3571
|
+
}
|
3572
|
+
function scoreElement(node1, node2, ctx) {
|
3573
|
+
if (isSoftMatch(node1, node2)) {
|
3574
|
+
return .5 + getIdIntersectionCount(ctx, node1, node2);
|
3575
|
+
}
|
3576
|
+
return 0;
|
3577
|
+
}
|
3578
|
+
function removeNode(tempNode, ctx) {
|
3579
|
+
removeIdsFromConsideration(ctx, tempNode);
|
3580
|
+
if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return;
|
3581
|
+
tempNode.remove();
|
3582
|
+
ctx.callbacks.afterNodeRemoved(tempNode);
|
3583
|
+
}
|
3584
|
+
function isIdInConsideration(ctx, id) {
|
3585
|
+
return !ctx.deadIds.has(id);
|
3586
|
+
}
|
3587
|
+
function idIsWithinNode(ctx, id, targetNode) {
|
3588
|
+
let idSet = ctx.idMap.get(targetNode) || EMPTY_SET;
|
3589
|
+
return idSet.has(id);
|
3590
|
+
}
|
3591
|
+
function removeIdsFromConsideration(ctx, node) {
|
3592
|
+
let idSet = ctx.idMap.get(node) || EMPTY_SET;
|
3593
|
+
for (const id of idSet) {
|
3594
|
+
ctx.deadIds.add(id);
|
3595
|
+
}
|
3596
|
+
}
|
3597
|
+
function getIdIntersectionCount(ctx, node1, node2) {
|
3598
|
+
let sourceSet = ctx.idMap.get(node1) || EMPTY_SET;
|
3599
|
+
let matchCount = 0;
|
3600
|
+
for (const id of sourceSet) {
|
3601
|
+
if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) {
|
3602
|
+
++matchCount;
|
3603
|
+
}
|
3604
|
+
}
|
3605
|
+
return matchCount;
|
3606
|
+
}
|
3607
|
+
function populateIdMapForNode(node, idMap) {
|
3608
|
+
let nodeParent = node.parentElement;
|
3609
|
+
let idElements = node.querySelectorAll("[id]");
|
3610
|
+
for (const elt of idElements) {
|
3611
|
+
let current = elt;
|
3612
|
+
while (current !== nodeParent && current != null) {
|
3613
|
+
let idSet = idMap.get(current);
|
3614
|
+
if (idSet == null) {
|
3615
|
+
idSet = new Set;
|
3616
|
+
idMap.set(current, idSet);
|
3617
|
+
}
|
3618
|
+
idSet.add(elt.id);
|
3619
|
+
current = current.parentElement;
|
3620
|
+
}
|
3621
|
+
}
|
3622
|
+
}
|
3623
|
+
function createIdMap(oldContent, newContent) {
|
3624
|
+
let idMap = new Map;
|
3625
|
+
populateIdMapForNode(oldContent, idMap);
|
3626
|
+
populateIdMapForNode(newContent, idMap);
|
3627
|
+
return idMap;
|
3628
|
+
}
|
3629
|
+
return {
|
3630
|
+
morph: morph,
|
3631
|
+
defaults: defaults
|
3632
|
+
};
|
3633
|
+
}();
|
3634
|
+
|
3635
|
+
function morphElements(currentElement, newElement, {callbacks: callbacks, ...options} = {}) {
|
3636
|
+
Idiomorph.morph(currentElement, newElement, {
|
3637
|
+
...options,
|
3638
|
+
callbacks: new DefaultIdiomorphCallbacks(callbacks)
|
3639
|
+
});
|
3640
|
+
}
|
3641
|
+
|
3642
|
+
function morphChildren(currentElement, newElement) {
|
3643
|
+
morphElements(currentElement, newElement.children, {
|
3644
|
+
morphStyle: "innerHTML"
|
3645
|
+
});
|
2642
3646
|
}
|
2643
3647
|
|
2644
|
-
|
2645
|
-
|
2646
|
-
|
2647
|
-
|
3648
|
+
class DefaultIdiomorphCallbacks {
|
3649
|
+
#beforeNodeMorphed;
|
3650
|
+
constructor({beforeNodeMorphed: beforeNodeMorphed} = {}) {
|
3651
|
+
this.#beforeNodeMorphed = beforeNodeMorphed || (() => true);
|
3652
|
+
}
|
3653
|
+
beforeNodeAdded=node => !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id));
|
3654
|
+
beforeNodeMorphed=(currentElement, newElement) => {
|
3655
|
+
if (currentElement instanceof Element) {
|
3656
|
+
if (!currentElement.hasAttribute("data-turbo-permanent") && this.#beforeNodeMorphed(currentElement, newElement)) {
|
3657
|
+
const event = dispatch("turbo:before-morph-element", {
|
3658
|
+
cancelable: true,
|
3659
|
+
target: currentElement,
|
3660
|
+
detail: {
|
3661
|
+
currentElement: currentElement,
|
3662
|
+
newElement: newElement
|
3663
|
+
}
|
3664
|
+
});
|
3665
|
+
return !event.defaultPrevented;
|
3666
|
+
} else {
|
3667
|
+
return false;
|
3668
|
+
}
|
3669
|
+
}
|
3670
|
+
};
|
3671
|
+
beforeAttributeUpdated=(attributeName, target, mutationType) => {
|
3672
|
+
const event = dispatch("turbo:before-morph-attribute", {
|
3673
|
+
cancelable: true,
|
3674
|
+
target: target,
|
3675
|
+
detail: {
|
3676
|
+
attributeName: attributeName,
|
3677
|
+
mutationType: mutationType
|
3678
|
+
}
|
3679
|
+
});
|
3680
|
+
return !event.defaultPrevented;
|
3681
|
+
};
|
3682
|
+
beforeNodeRemoved=node => this.beforeNodeMorphed(node);
|
3683
|
+
afterNodeMorphed=(currentElement, newElement) => {
|
3684
|
+
if (currentElement instanceof Element) {
|
3685
|
+
dispatch("turbo:morph-element", {
|
3686
|
+
target: currentElement,
|
3687
|
+
detail: {
|
3688
|
+
currentElement: currentElement,
|
3689
|
+
newElement: newElement
|
3690
|
+
}
|
3691
|
+
});
|
3692
|
+
}
|
3693
|
+
};
|
2648
3694
|
}
|
2649
3695
|
|
2650
|
-
class
|
3696
|
+
class MorphingFrameRenderer extends FrameRenderer {
|
2651
3697
|
static renderElement(currentElement, newElement) {
|
2652
|
-
|
2653
|
-
|
2654
|
-
|
2655
|
-
|
2656
|
-
|
2657
|
-
this.activateScriptElements();
|
2658
|
-
}
|
2659
|
-
replaceHeadAndBody() {
|
2660
|
-
const {documentElement: documentElement, head: head} = document;
|
2661
|
-
documentElement.replaceChild(this.newHead, head);
|
2662
|
-
this.renderElement(this.currentElement, this.newElement);
|
2663
|
-
}
|
2664
|
-
activateScriptElements() {
|
2665
|
-
for (const replaceableElement of this.scriptElements) {
|
2666
|
-
const parentNode = replaceableElement.parentNode;
|
2667
|
-
if (parentNode) {
|
2668
|
-
const element = activateScriptElement(replaceableElement);
|
2669
|
-
parentNode.replaceChild(element, replaceableElement);
|
3698
|
+
dispatch("turbo:before-frame-morph", {
|
3699
|
+
target: currentElement,
|
3700
|
+
detail: {
|
3701
|
+
currentElement: currentElement,
|
3702
|
+
newElement: newElement
|
2670
3703
|
}
|
2671
|
-
}
|
2672
|
-
|
2673
|
-
get newHead() {
|
2674
|
-
return this.newSnapshot.headSnapshot.element;
|
2675
|
-
}
|
2676
|
-
get scriptElements() {
|
2677
|
-
return document.documentElement.querySelectorAll("script");
|
3704
|
+
});
|
3705
|
+
morphChildren(currentElement, newElement);
|
2678
3706
|
}
|
2679
3707
|
}
|
2680
3708
|
|
@@ -2702,6 +3730,7 @@ class PageRenderer extends Renderer {
|
|
2702
3730
|
}
|
2703
3731
|
}
|
2704
3732
|
async prepareToRender() {
|
3733
|
+
this.#setLanguage();
|
2705
3734
|
await this.mergeHead();
|
2706
3735
|
}
|
2707
3736
|
async render() {
|
@@ -2724,12 +3753,24 @@ class PageRenderer extends Renderer {
|
|
2724
3753
|
get newElement() {
|
2725
3754
|
return this.newSnapshot.element;
|
2726
3755
|
}
|
3756
|
+
#setLanguage() {
|
3757
|
+
const {documentElement: documentElement} = this.currentSnapshot;
|
3758
|
+
const {lang: lang} = this.newSnapshot;
|
3759
|
+
if (lang) {
|
3760
|
+
documentElement.setAttribute("lang", lang);
|
3761
|
+
} else {
|
3762
|
+
documentElement.removeAttribute("lang");
|
3763
|
+
}
|
3764
|
+
}
|
2727
3765
|
async mergeHead() {
|
2728
3766
|
const mergedHeadElements = this.mergeProvisionalElements();
|
2729
3767
|
const newStylesheetElements = this.copyNewHeadStylesheetElements();
|
2730
3768
|
this.copyNewHeadScriptElements();
|
2731
3769
|
await mergedHeadElements;
|
2732
3770
|
await newStylesheetElements;
|
3771
|
+
if (this.willRender) {
|
3772
|
+
this.removeUnusedDynamicStylesheetElements();
|
3773
|
+
}
|
2733
3774
|
}
|
2734
3775
|
async replaceBody() {
|
2735
3776
|
await this.preservingPermanentElements((async () => {
|
@@ -2753,6 +3794,11 @@ class PageRenderer extends Renderer {
|
|
2753
3794
|
document.head.appendChild(activateScriptElement(element));
|
2754
3795
|
}
|
2755
3796
|
}
|
3797
|
+
removeUnusedDynamicStylesheetElements() {
|
3798
|
+
for (const element of this.unusedDynamicStylesheetElements) {
|
3799
|
+
document.head.removeChild(element);
|
3800
|
+
}
|
3801
|
+
}
|
2756
3802
|
async mergeProvisionalElements() {
|
2757
3803
|
const newHeadElements = [ ...this.newHeadProvisionalElements ];
|
2758
3804
|
for (const element of this.currentHeadProvisionalElements) {
|
@@ -2805,6 +3851,12 @@ class PageRenderer extends Renderer {
|
|
2805
3851
|
async assignNewBody() {
|
2806
3852
|
await this.renderElement(this.currentElement, this.newElement);
|
2807
3853
|
}
|
3854
|
+
get unusedDynamicStylesheetElements() {
|
3855
|
+
return this.oldHeadStylesheetElements.filter((element => element.getAttribute("data-turbo-track") === "dynamic"));
|
3856
|
+
}
|
3857
|
+
get oldHeadStylesheetElements() {
|
3858
|
+
return this.currentHeadSnapshot.getStylesheetElementsNotInSnapshot(this.newHeadSnapshot);
|
3859
|
+
}
|
2808
3860
|
get newHeadStylesheetElements() {
|
2809
3861
|
return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot);
|
2810
3862
|
}
|
@@ -2822,10 +3874,51 @@ class PageRenderer extends Renderer {
|
|
2822
3874
|
}
|
2823
3875
|
}
|
2824
3876
|
|
3877
|
+
class MorphingPageRenderer extends PageRenderer {
|
3878
|
+
static renderElement(currentElement, newElement) {
|
3879
|
+
morphElements(currentElement, newElement, {
|
3880
|
+
callbacks: {
|
3881
|
+
beforeNodeMorphed: element => !canRefreshFrame(element)
|
3882
|
+
}
|
3883
|
+
});
|
3884
|
+
for (const frame of currentElement.querySelectorAll("turbo-frame")) {
|
3885
|
+
if (canRefreshFrame(frame)) refreshFrame(frame);
|
3886
|
+
}
|
3887
|
+
dispatch("turbo:morph", {
|
3888
|
+
detail: {
|
3889
|
+
currentElement: currentElement,
|
3890
|
+
newElement: newElement
|
3891
|
+
}
|
3892
|
+
});
|
3893
|
+
}
|
3894
|
+
async preservingPermanentElements(callback) {
|
3895
|
+
return await callback();
|
3896
|
+
}
|
3897
|
+
get renderMethod() {
|
3898
|
+
return "morph";
|
3899
|
+
}
|
3900
|
+
get shouldAutofocus() {
|
3901
|
+
return false;
|
3902
|
+
}
|
3903
|
+
}
|
3904
|
+
|
3905
|
+
function canRefreshFrame(frame) {
|
3906
|
+
return frame instanceof FrameElement && frame.src && frame.refresh === "morph" && !frame.closest("[data-turbo-permanent]");
|
3907
|
+
}
|
3908
|
+
|
3909
|
+
function refreshFrame(frame) {
|
3910
|
+
frame.addEventListener("turbo:before-frame-render", (({detail: detail}) => {
|
3911
|
+
detail.render = MorphingFrameRenderer.renderElement;
|
3912
|
+
}), {
|
3913
|
+
once: true
|
3914
|
+
});
|
3915
|
+
frame.reload();
|
3916
|
+
}
|
3917
|
+
|
2825
3918
|
class SnapshotCache {
|
3919
|
+
keys=[];
|
3920
|
+
snapshots={};
|
2826
3921
|
constructor(size) {
|
2827
|
-
this.keys = [];
|
2828
|
-
this.snapshots = {};
|
2829
3922
|
this.size = size;
|
2830
3923
|
}
|
2831
3924
|
has(location) {
|
@@ -2867,23 +3960,25 @@ class SnapshotCache {
|
|
2867
3960
|
}
|
2868
3961
|
|
2869
3962
|
class PageView extends View {
|
2870
|
-
|
2871
|
-
|
2872
|
-
|
2873
|
-
|
2874
|
-
this.
|
3963
|
+
snapshotCache=new SnapshotCache(10);
|
3964
|
+
lastRenderedLocation=new URL(location.href);
|
3965
|
+
forceReloaded=false;
|
3966
|
+
shouldTransitionTo(newSnapshot) {
|
3967
|
+
return this.snapshot.prefersViewTransitions && newSnapshot.prefersViewTransitions;
|
2875
3968
|
}
|
2876
3969
|
renderPage(snapshot, isPreview = false, willRender = true, visit) {
|
2877
|
-
const
|
3970
|
+
const shouldMorphPage = this.isPageRefresh(visit) && this.snapshot.shouldMorphPage;
|
3971
|
+
const rendererClass = shouldMorphPage ? MorphingPageRenderer : PageRenderer;
|
3972
|
+
const renderer = new rendererClass(this.snapshot, snapshot, rendererClass.renderElement, isPreview, willRender);
|
2878
3973
|
if (!renderer.shouldRender) {
|
2879
3974
|
this.forceReloaded = true;
|
2880
3975
|
} else {
|
2881
|
-
visit
|
3976
|
+
visit?.changeHistory();
|
2882
3977
|
}
|
2883
3978
|
return this.render(renderer);
|
2884
3979
|
}
|
2885
3980
|
renderError(snapshot, visit) {
|
2886
|
-
visit
|
3981
|
+
visit?.changeHistory();
|
2887
3982
|
const renderer = new ErrorRenderer(this.snapshot, snapshot, ErrorRenderer.renderElement, false);
|
2888
3983
|
return this.render(renderer);
|
2889
3984
|
}
|
@@ -2903,31 +3998,38 @@ class PageView extends View {
|
|
2903
3998
|
getCachedSnapshotForLocation(location) {
|
2904
3999
|
return this.snapshotCache.get(location);
|
2905
4000
|
}
|
4001
|
+
isPageRefresh(visit) {
|
4002
|
+
return !visit || this.lastRenderedLocation.pathname === visit.location.pathname && visit.action === "replace";
|
4003
|
+
}
|
4004
|
+
shouldPreserveScrollPosition(visit) {
|
4005
|
+
return this.isPageRefresh(visit) && this.snapshot.shouldPreserveScrollPosition;
|
4006
|
+
}
|
2906
4007
|
get snapshot() {
|
2907
4008
|
return PageSnapshot.fromElement(this.element);
|
2908
4009
|
}
|
2909
4010
|
}
|
2910
4011
|
|
2911
4012
|
class Preloader {
|
2912
|
-
|
2913
|
-
|
4013
|
+
selector="a[data-turbo-preload]";
|
4014
|
+
constructor(delegate, snapshotCache) {
|
2914
4015
|
this.delegate = delegate;
|
2915
|
-
|
2916
|
-
get snapshotCache() {
|
2917
|
-
return this.delegate.navigator.view.snapshotCache;
|
4016
|
+
this.snapshotCache = snapshotCache;
|
2918
4017
|
}
|
2919
4018
|
start() {
|
2920
4019
|
if (document.readyState === "loading") {
|
2921
|
-
|
2922
|
-
this.preloadOnLoadLinksForView(document.body);
|
2923
|
-
}));
|
4020
|
+
document.addEventListener("DOMContentLoaded", this.#preloadAll);
|
2924
4021
|
} else {
|
2925
4022
|
this.preloadOnLoadLinksForView(document.body);
|
2926
4023
|
}
|
2927
4024
|
}
|
4025
|
+
stop() {
|
4026
|
+
document.removeEventListener("DOMContentLoaded", this.#preloadAll);
|
4027
|
+
}
|
2928
4028
|
preloadOnLoadLinksForView(element) {
|
2929
4029
|
for (const link of element.querySelectorAll(this.selector)) {
|
2930
|
-
this.
|
4030
|
+
if (this.delegate.shouldPreloadLink(link)) {
|
4031
|
+
this.preloadURL(link);
|
4032
|
+
}
|
2931
4033
|
}
|
2932
4034
|
}
|
2933
4035
|
async preloadURL(link) {
|
@@ -2935,46 +4037,83 @@ class Preloader {
|
|
2935
4037
|
if (this.snapshotCache.has(location)) {
|
2936
4038
|
return;
|
2937
4039
|
}
|
4040
|
+
const fetchRequest = new FetchRequest(this, FetchMethod.get, location, new URLSearchParams, link);
|
4041
|
+
await fetchRequest.perform();
|
4042
|
+
}
|
4043
|
+
prepareRequest(fetchRequest) {
|
4044
|
+
fetchRequest.headers["X-Sec-Purpose"] = "prefetch";
|
4045
|
+
}
|
4046
|
+
async requestSucceededWithResponse(fetchRequest, fetchResponse) {
|
2938
4047
|
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);
|
4048
|
+
const responseHTML = await fetchResponse.responseHTML;
|
4049
|
+
const snapshot = PageSnapshot.fromHTMLString(responseHTML);
|
4050
|
+
this.snapshotCache.put(fetchRequest.url, snapshot);
|
2948
4051
|
} catch (_) {}
|
2949
4052
|
}
|
4053
|
+
requestStarted(fetchRequest) {}
|
4054
|
+
requestErrored(fetchRequest) {}
|
4055
|
+
requestFinished(fetchRequest) {}
|
4056
|
+
requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}
|
4057
|
+
requestFailedWithResponse(fetchRequest, fetchResponse) {}
|
4058
|
+
#preloadAll=() => {
|
4059
|
+
this.preloadOnLoadLinksForView(document.body);
|
4060
|
+
};
|
4061
|
+
}
|
4062
|
+
|
4063
|
+
class Cache {
|
4064
|
+
constructor(session) {
|
4065
|
+
this.session = session;
|
4066
|
+
}
|
4067
|
+
clear() {
|
4068
|
+
this.session.clearCache();
|
4069
|
+
}
|
4070
|
+
resetCacheControl() {
|
4071
|
+
this.#setCacheControl("");
|
4072
|
+
}
|
4073
|
+
exemptPageFromCache() {
|
4074
|
+
this.#setCacheControl("no-cache");
|
4075
|
+
}
|
4076
|
+
exemptPageFromPreview() {
|
4077
|
+
this.#setCacheControl("no-preview");
|
4078
|
+
}
|
4079
|
+
#setCacheControl(value) {
|
4080
|
+
setMetaContent("turbo-cache-control", value);
|
4081
|
+
}
|
2950
4082
|
}
|
2951
4083
|
|
2952
4084
|
class Session {
|
2953
|
-
|
2954
|
-
|
2955
|
-
|
2956
|
-
|
2957
|
-
|
2958
|
-
|
2959
|
-
|
2960
|
-
|
2961
|
-
|
2962
|
-
|
2963
|
-
|
2964
|
-
|
2965
|
-
|
2966
|
-
|
2967
|
-
|
2968
|
-
|
2969
|
-
|
2970
|
-
|
2971
|
-
|
2972
|
-
|
4085
|
+
navigator=new Navigator(this);
|
4086
|
+
history=new History(this);
|
4087
|
+
view=new PageView(this, document.documentElement);
|
4088
|
+
adapter=new BrowserAdapter(this);
|
4089
|
+
pageObserver=new PageObserver(this);
|
4090
|
+
cacheObserver=new CacheObserver;
|
4091
|
+
linkPrefetchObserver=new LinkPrefetchObserver(this, document);
|
4092
|
+
linkClickObserver=new LinkClickObserver(this, window);
|
4093
|
+
formSubmitObserver=new FormSubmitObserver(this, document);
|
4094
|
+
scrollObserver=new ScrollObserver(this);
|
4095
|
+
streamObserver=new StreamObserver(this);
|
4096
|
+
formLinkClickObserver=new FormLinkClickObserver(this, document.documentElement);
|
4097
|
+
frameRedirector=new FrameRedirector(this, document.documentElement);
|
4098
|
+
streamMessageRenderer=new StreamMessageRenderer;
|
4099
|
+
cache=new Cache(this);
|
4100
|
+
drive=true;
|
4101
|
+
enabled=true;
|
4102
|
+
progressBarDelay=500;
|
4103
|
+
started=false;
|
4104
|
+
formMode="on";
|
4105
|
+
#pageRefreshDebouncePeriod=150;
|
4106
|
+
constructor(recentRequests) {
|
4107
|
+
this.recentRequests = recentRequests;
|
4108
|
+
this.preloader = new Preloader(this, this.view.snapshotCache);
|
4109
|
+
this.debouncedRefresh = this.refresh;
|
4110
|
+
this.pageRefreshDebouncePeriod = this.pageRefreshDebouncePeriod;
|
2973
4111
|
}
|
2974
4112
|
start() {
|
2975
4113
|
if (!this.started) {
|
2976
4114
|
this.pageObserver.start();
|
2977
4115
|
this.cacheObserver.start();
|
4116
|
+
this.linkPrefetchObserver.start();
|
2978
4117
|
this.formLinkClickObserver.start();
|
2979
4118
|
this.linkClickObserver.start();
|
2980
4119
|
this.formSubmitObserver.start();
|
@@ -2994,6 +4133,7 @@ class Session {
|
|
2994
4133
|
if (this.started) {
|
2995
4134
|
this.pageObserver.stop();
|
2996
4135
|
this.cacheObserver.stop();
|
4136
|
+
this.linkPrefetchObserver.stop();
|
2997
4137
|
this.formLinkClickObserver.stop();
|
2998
4138
|
this.linkClickObserver.stop();
|
2999
4139
|
this.formSubmitObserver.stop();
|
@@ -3001,6 +4141,7 @@ class Session {
|
|
3001
4141
|
this.streamObserver.stop();
|
3002
4142
|
this.frameRedirector.stop();
|
3003
4143
|
this.history.stop();
|
4144
|
+
this.preloader.stop();
|
3004
4145
|
this.started = false;
|
3005
4146
|
}
|
3006
4147
|
}
|
@@ -3010,12 +4151,22 @@ class Session {
|
|
3010
4151
|
visit(location, options = {}) {
|
3011
4152
|
const frameElement = options.frame ? document.getElementById(options.frame) : null;
|
3012
4153
|
if (frameElement instanceof FrameElement) {
|
4154
|
+
const action = options.action || getVisitAction(frameElement);
|
4155
|
+
frameElement.delegate.proposeVisitIfNavigatedWithAction(frameElement, action);
|
3013
4156
|
frameElement.src = location.toString();
|
3014
|
-
frameElement.loaded;
|
3015
4157
|
} else {
|
3016
4158
|
this.navigator.proposeVisit(expandURL(location), options);
|
3017
4159
|
}
|
3018
4160
|
}
|
4161
|
+
refresh(url, requestId) {
|
4162
|
+
const isRecentRequest = requestId && this.recentRequests.has(requestId);
|
4163
|
+
if (!isRecentRequest && !this.navigator.currentVisit) {
|
4164
|
+
this.visit(url, {
|
4165
|
+
action: "replace",
|
4166
|
+
shouldCacheSnapshot: false
|
4167
|
+
});
|
4168
|
+
}
|
4169
|
+
}
|
3019
4170
|
connectStreamSource(source) {
|
3020
4171
|
this.streamObserver.connectStreamSource(source);
|
3021
4172
|
}
|
@@ -3040,11 +4191,31 @@ class Session {
|
|
3040
4191
|
get restorationIdentifier() {
|
3041
4192
|
return this.history.restorationIdentifier;
|
3042
4193
|
}
|
3043
|
-
|
4194
|
+
get pageRefreshDebouncePeriod() {
|
4195
|
+
return this.#pageRefreshDebouncePeriod;
|
4196
|
+
}
|
4197
|
+
set pageRefreshDebouncePeriod(value) {
|
4198
|
+
this.refresh = debounce(this.debouncedRefresh.bind(this), value);
|
4199
|
+
this.#pageRefreshDebouncePeriod = value;
|
4200
|
+
}
|
4201
|
+
shouldPreloadLink(element) {
|
4202
|
+
const isUnsafe = element.hasAttribute("data-turbo-method");
|
4203
|
+
const isStream = element.hasAttribute("data-turbo-stream");
|
4204
|
+
const frameTarget = element.getAttribute("data-turbo-frame");
|
4205
|
+
const frame = frameTarget == "_top" ? null : document.getElementById(frameTarget) || findClosestRecursively(element, "turbo-frame:not([disabled])");
|
4206
|
+
if (isUnsafe || isStream || frame instanceof FrameElement) {
|
4207
|
+
return false;
|
4208
|
+
} else {
|
4209
|
+
const location = new URL(element.href);
|
4210
|
+
return this.elementIsNavigatable(element) && locationIsVisitable(location, this.snapshot.rootLocation);
|
4211
|
+
}
|
4212
|
+
}
|
4213
|
+
historyPoppedToLocationWithRestorationIdentifierAndDirection(location, restorationIdentifier, direction) {
|
3044
4214
|
if (this.enabled) {
|
3045
4215
|
this.navigator.startVisit(location, restorationIdentifier, {
|
3046
4216
|
action: "restore",
|
3047
|
-
historyChanged: true
|
4217
|
+
historyChanged: true,
|
4218
|
+
direction: direction
|
3048
4219
|
});
|
3049
4220
|
} else {
|
3050
4221
|
this.adapter.pageInvalidated({
|
@@ -3061,6 +4232,9 @@ class Session {
|
|
3061
4232
|
return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation);
|
3062
4233
|
}
|
3063
4234
|
submittedFormLinkToLocation() {}
|
4235
|
+
canPrefetchRequestToLocation(link, location) {
|
4236
|
+
return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation);
|
4237
|
+
}
|
3064
4238
|
willFollowLinkToLocation(link, location, event) {
|
3065
4239
|
return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation) && this.applicationAllowsFollowingLinkToLocation(link, location, event);
|
3066
4240
|
}
|
@@ -3082,6 +4256,7 @@ class Session {
|
|
3082
4256
|
visitStarted(visit) {
|
3083
4257
|
if (!visit.acceptsStreamResponse) {
|
3084
4258
|
markAsBusy(document.documentElement);
|
4259
|
+
this.view.markVisitDirection(visit.direction);
|
3085
4260
|
}
|
3086
4261
|
extendURLWithDeprecatedProperties(visit.location);
|
3087
4262
|
if (!visit.silent) {
|
@@ -3089,6 +4264,7 @@ class Session {
|
|
3089
4264
|
}
|
3090
4265
|
}
|
3091
4266
|
visitCompleted(visit) {
|
4267
|
+
this.view.unmarkVisitDirection();
|
3092
4268
|
clearBusyState(document.documentElement);
|
3093
4269
|
this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
|
3094
4270
|
}
|
@@ -3099,7 +4275,7 @@ class Session {
|
|
3099
4275
|
this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
|
3100
4276
|
}
|
3101
4277
|
willSubmitForm(form, submitter) {
|
3102
|
-
const action = getAction(form, submitter);
|
4278
|
+
const action = getAction$1(form, submitter);
|
3103
4279
|
return this.submissionIsNavigatable(form, submitter) && locationIsVisitable(expandURL(action), this.snapshot.rootLocation);
|
3104
4280
|
}
|
3105
4281
|
formSubmitted(form, submitter) {
|
@@ -3119,8 +4295,7 @@ class Session {
|
|
3119
4295
|
this.renderStreamMessage(message);
|
3120
4296
|
}
|
3121
4297
|
viewWillCacheSnapshot() {
|
3122
|
-
|
3123
|
-
if (!((_a = this.navigator.currentVisit) === null || _a === void 0 ? void 0 : _a.silent)) {
|
4298
|
+
if (!this.navigator.currentVisit?.silent) {
|
3124
4299
|
this.notifyApplicationBeforeCachingSnapshot();
|
3125
4300
|
}
|
3126
4301
|
}
|
@@ -3132,9 +4307,9 @@ class Session {
|
|
3132
4307
|
}
|
3133
4308
|
return !defaultPrevented;
|
3134
4309
|
}
|
3135
|
-
viewRenderedSnapshot(_snapshot, _isPreview) {
|
4310
|
+
viewRenderedSnapshot(_snapshot, _isPreview, renderMethod) {
|
3136
4311
|
this.view.lastRenderedLocation = this.history.location;
|
3137
|
-
this.notifyApplicationAfterRender();
|
4312
|
+
this.notifyApplicationAfterRender(renderMethod);
|
3138
4313
|
}
|
3139
4314
|
preloadOnLoadLinksForView(element) {
|
3140
4315
|
this.preloader.preloadOnLoadLinksForView(element);
|
@@ -3187,14 +4362,19 @@ class Session {
|
|
3187
4362
|
}
|
3188
4363
|
notifyApplicationBeforeRender(newBody, options) {
|
3189
4364
|
return dispatch("turbo:before-render", {
|
3190
|
-
detail:
|
3191
|
-
newBody: newBody
|
3192
|
-
|
4365
|
+
detail: {
|
4366
|
+
newBody: newBody,
|
4367
|
+
...options
|
4368
|
+
},
|
3193
4369
|
cancelable: true
|
3194
4370
|
});
|
3195
4371
|
}
|
3196
|
-
notifyApplicationAfterRender() {
|
3197
|
-
return dispatch("turbo:render"
|
4372
|
+
notifyApplicationAfterRender(renderMethod) {
|
4373
|
+
return dispatch("turbo:render", {
|
4374
|
+
detail: {
|
4375
|
+
renderMethod: renderMethod
|
4376
|
+
}
|
4377
|
+
});
|
3198
4378
|
}
|
3199
4379
|
notifyApplicationAfterPageLoad(timing = {}) {
|
3200
4380
|
return dispatch("turbo:load", {
|
@@ -3273,67 +4453,9 @@ const deprecatedLocationPropertyDescriptors = {
|
|
3273
4453
|
}
|
3274
4454
|
};
|
3275
4455
|
|
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;
|
3333
|
-
|
3334
|
-
const cache = new Cache(session);
|
4456
|
+
const session = new Session(recentRequests);
|
3335
4457
|
|
3336
|
-
const {navigator: navigator$1} = session;
|
4458
|
+
const {cache: cache, navigator: navigator$1} = session;
|
3337
4459
|
|
3338
4460
|
function start() {
|
3339
4461
|
session.start();
|
@@ -3384,6 +4506,7 @@ var Turbo = Object.freeze({
|
|
3384
4506
|
PageRenderer: PageRenderer,
|
3385
4507
|
PageSnapshot: PageSnapshot,
|
3386
4508
|
FrameRenderer: FrameRenderer,
|
4509
|
+
fetch: fetchWithTurboHeaders,
|
3387
4510
|
start: start,
|
3388
4511
|
registerAdapter: registerAdapter,
|
3389
4512
|
visit: visit,
|
@@ -3393,28 +4516,20 @@ var Turbo = Object.freeze({
|
|
3393
4516
|
clearCache: clearCache,
|
3394
4517
|
setProgressBarDelay: setProgressBarDelay,
|
3395
4518
|
setConfirmMethod: setConfirmMethod,
|
3396
|
-
setFormMode: setFormMode
|
3397
|
-
StreamActions: StreamActions
|
4519
|
+
setFormMode: setFormMode
|
3398
4520
|
});
|
3399
4521
|
|
3400
4522
|
class TurboFrameMissingError extends Error {}
|
3401
4523
|
|
3402
4524
|
class FrameController {
|
4525
|
+
fetchResponseLoaded=_fetchResponse => Promise.resolve();
|
4526
|
+
#currentFetchRequest=null;
|
4527
|
+
#resolveVisitPromise=() => {};
|
4528
|
+
#connected=false;
|
4529
|
+
#hasBeenLoaded=false;
|
4530
|
+
#ignoredAttributes=new Set;
|
4531
|
+
action=null;
|
3403
4532
|
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
4533
|
this.element = element;
|
3419
4534
|
this.view = new FrameView(this, this.element);
|
3420
4535
|
this.appearanceObserver = new AppearanceObserver(this, this.element);
|
@@ -3424,12 +4539,12 @@ class FrameController {
|
|
3424
4539
|
this.formSubmitObserver = new FormSubmitObserver(this, this.element);
|
3425
4540
|
}
|
3426
4541
|
connect() {
|
3427
|
-
if (!this
|
3428
|
-
this
|
4542
|
+
if (!this.#connected) {
|
4543
|
+
this.#connected = true;
|
3429
4544
|
if (this.loadingStyle == FrameLoadingStyle.lazy) {
|
3430
4545
|
this.appearanceObserver.start();
|
3431
4546
|
} else {
|
3432
|
-
this
|
4547
|
+
this.#loadSourceURL();
|
3433
4548
|
}
|
3434
4549
|
this.formLinkClickObserver.start();
|
3435
4550
|
this.linkInterceptor.start();
|
@@ -3437,8 +4552,8 @@ class FrameController {
|
|
3437
4552
|
}
|
3438
4553
|
}
|
3439
4554
|
disconnect() {
|
3440
|
-
if (this
|
3441
|
-
this
|
4555
|
+
if (this.#connected) {
|
4556
|
+
this.#connected = false;
|
3442
4557
|
this.appearanceObserver.stop();
|
3443
4558
|
this.formLinkClickObserver.stop();
|
3444
4559
|
this.linkInterceptor.stop();
|
@@ -3447,45 +4562,39 @@ class FrameController {
|
|
3447
4562
|
}
|
3448
4563
|
disabledChanged() {
|
3449
4564
|
if (this.loadingStyle == FrameLoadingStyle.eager) {
|
3450
|
-
this
|
4565
|
+
this.#loadSourceURL();
|
3451
4566
|
}
|
3452
4567
|
}
|
3453
4568
|
sourceURLChanged() {
|
3454
|
-
if (this
|
4569
|
+
if (this.#isIgnoringChangesTo("src")) return;
|
3455
4570
|
if (this.element.isConnected) {
|
3456
4571
|
this.complete = false;
|
3457
4572
|
}
|
3458
|
-
if (this.loadingStyle == FrameLoadingStyle.eager || this
|
3459
|
-
this
|
4573
|
+
if (this.loadingStyle == FrameLoadingStyle.eager || this.#hasBeenLoaded) {
|
4574
|
+
this.#loadSourceURL();
|
3460
4575
|
}
|
3461
4576
|
}
|
3462
4577
|
sourceURLReloaded() {
|
3463
4578
|
const {src: src} = this.element;
|
3464
|
-
this.
|
3465
|
-
this.element.removeAttribute("complete");
|
3466
|
-
}));
|
4579
|
+
this.element.removeAttribute("complete");
|
3467
4580
|
this.element.src = null;
|
3468
4581
|
this.element.src = src;
|
3469
4582
|
return this.element.loaded;
|
3470
4583
|
}
|
3471
|
-
completeChanged() {
|
3472
|
-
if (this.isIgnoringChangesTo("complete")) return;
|
3473
|
-
this.loadSourceURL();
|
3474
|
-
}
|
3475
4584
|
loadingStyleChanged() {
|
3476
4585
|
if (this.loadingStyle == FrameLoadingStyle.lazy) {
|
3477
4586
|
this.appearanceObserver.start();
|
3478
4587
|
} else {
|
3479
4588
|
this.appearanceObserver.stop();
|
3480
|
-
this
|
4589
|
+
this.#loadSourceURL();
|
3481
4590
|
}
|
3482
4591
|
}
|
3483
|
-
async loadSourceURL() {
|
4592
|
+
async #loadSourceURL() {
|
3484
4593
|
if (this.enabled && this.isActive && !this.complete && this.sourceURL) {
|
3485
|
-
this.element.loaded = this
|
4594
|
+
this.element.loaded = this.#visit(expandURL(this.sourceURL));
|
3486
4595
|
this.appearanceObserver.stop();
|
3487
4596
|
await this.element.loaded;
|
3488
|
-
this
|
4597
|
+
this.#hasBeenLoaded = true;
|
3489
4598
|
}
|
3490
4599
|
}
|
3491
4600
|
async loadResponse(fetchResponse) {
|
@@ -3498,34 +4607,34 @@ class FrameController {
|
|
3498
4607
|
const document = parseHTMLDocument(html);
|
3499
4608
|
const pageSnapshot = PageSnapshot.fromDocument(document);
|
3500
4609
|
if (pageSnapshot.isVisitable) {
|
3501
|
-
await this
|
4610
|
+
await this.#loadFrameResponse(fetchResponse, document);
|
3502
4611
|
} else {
|
3503
|
-
await this
|
4612
|
+
await this.#handleUnvisitableFrameResponse(fetchResponse);
|
3504
4613
|
}
|
3505
4614
|
}
|
3506
4615
|
} finally {
|
3507
|
-
this.fetchResponseLoaded = () =>
|
4616
|
+
this.fetchResponseLoaded = () => Promise.resolve();
|
3508
4617
|
}
|
3509
4618
|
}
|
3510
4619
|
elementAppearedInViewport(element) {
|
3511
|
-
this.proposeVisitIfNavigatedWithAction(element, element);
|
3512
|
-
this
|
4620
|
+
this.proposeVisitIfNavigatedWithAction(element, getVisitAction(element));
|
4621
|
+
this.#loadSourceURL();
|
3513
4622
|
}
|
3514
4623
|
willSubmitFormLinkToLocation(link) {
|
3515
|
-
return this
|
4624
|
+
return this.#shouldInterceptNavigation(link);
|
3516
4625
|
}
|
3517
4626
|
submittedFormLinkToLocation(link, _location, form) {
|
3518
|
-
const frame = this
|
4627
|
+
const frame = this.#findFrameElement(link);
|
3519
4628
|
if (frame) form.setAttribute("data-turbo-frame", frame.id);
|
3520
4629
|
}
|
3521
4630
|
shouldInterceptLinkClick(element, _location, _event) {
|
3522
|
-
return this
|
4631
|
+
return this.#shouldInterceptNavigation(element);
|
3523
4632
|
}
|
3524
4633
|
linkClickIntercepted(element, location) {
|
3525
|
-
this
|
4634
|
+
this.#navigateFrame(element, location);
|
3526
4635
|
}
|
3527
4636
|
willSubmitForm(element, submitter) {
|
3528
|
-
return element.closest("turbo-frame") == this.element && this
|
4637
|
+
return element.closest("turbo-frame") == this.element && this.#shouldInterceptNavigation(element, submitter);
|
3529
4638
|
}
|
3530
4639
|
formSubmitted(element, submitter) {
|
3531
4640
|
if (this.formSubmission) {
|
@@ -3537,9 +4646,8 @@ class FrameController {
|
|
3537
4646
|
this.formSubmission.start();
|
3538
4647
|
}
|
3539
4648
|
prepareRequest(request) {
|
3540
|
-
var _a;
|
3541
4649
|
request.headers["Turbo-Frame"] = this.id;
|
3542
|
-
if (
|
4650
|
+
if (this.currentNavigationElement?.hasAttribute("data-turbo-stream")) {
|
3543
4651
|
request.acceptResponseType(StreamMessage.contentType);
|
3544
4652
|
}
|
3545
4653
|
}
|
@@ -3547,29 +4655,29 @@ class FrameController {
|
|
3547
4655
|
markAsBusy(this.element);
|
3548
4656
|
}
|
3549
4657
|
requestPreventedHandlingResponse(_request, _response) {
|
3550
|
-
this
|
4658
|
+
this.#resolveVisitPromise();
|
3551
4659
|
}
|
3552
4660
|
async requestSucceededWithResponse(request, response) {
|
3553
4661
|
await this.loadResponse(response);
|
3554
|
-
this
|
4662
|
+
this.#resolveVisitPromise();
|
3555
4663
|
}
|
3556
4664
|
async requestFailedWithResponse(request, response) {
|
3557
4665
|
await this.loadResponse(response);
|
3558
|
-
this
|
4666
|
+
this.#resolveVisitPromise();
|
3559
4667
|
}
|
3560
4668
|
requestErrored(request, error) {
|
3561
4669
|
console.error(error);
|
3562
|
-
this
|
4670
|
+
this.#resolveVisitPromise();
|
3563
4671
|
}
|
3564
4672
|
requestFinished(_request) {
|
3565
4673
|
clearBusyState(this.element);
|
3566
4674
|
}
|
3567
4675
|
formSubmissionStarted({formElement: formElement}) {
|
3568
|
-
markAsBusy(formElement, this
|
4676
|
+
markAsBusy(formElement, this.#findFrameElement(formElement));
|
3569
4677
|
}
|
3570
4678
|
formSubmissionSucceededWithResponse(formSubmission, response) {
|
3571
|
-
const frame = this
|
3572
|
-
frame.delegate.proposeVisitIfNavigatedWithAction(frame, formSubmission.
|
4679
|
+
const frame = this.#findFrameElement(formSubmission.formElement, formSubmission.submitter);
|
4680
|
+
frame.delegate.proposeVisitIfNavigatedWithAction(frame, getVisitAction(formSubmission.submitter, formSubmission.formElement, frame));
|
3573
4681
|
frame.delegate.loadResponse(response);
|
3574
4682
|
if (!formSubmission.isSafe) {
|
3575
4683
|
session.clearCache();
|
@@ -3583,14 +4691,15 @@ class FrameController {
|
|
3583
4691
|
console.error(error);
|
3584
4692
|
}
|
3585
4693
|
formSubmissionFinished({formElement: formElement}) {
|
3586
|
-
clearBusyState(formElement, this
|
4694
|
+
clearBusyState(formElement, this.#findFrameElement(formElement));
|
3587
4695
|
}
|
3588
4696
|
allowsImmediateRender({element: newFrame}, options) {
|
3589
4697
|
const event = dispatch("turbo:before-frame-render", {
|
3590
4698
|
target: this.element,
|
3591
|
-
detail:
|
3592
|
-
newFrame: newFrame
|
3593
|
-
|
4699
|
+
detail: {
|
4700
|
+
newFrame: newFrame,
|
4701
|
+
...options
|
4702
|
+
},
|
3594
4703
|
cancelable: true
|
3595
4704
|
});
|
3596
4705
|
const {defaultPrevented: defaultPrevented, detail: {render: render}} = event;
|
@@ -3599,7 +4708,7 @@ class FrameController {
|
|
3599
4708
|
}
|
3600
4709
|
return !defaultPrevented;
|
3601
4710
|
}
|
3602
|
-
viewRenderedSnapshot(_snapshot, _isPreview) {}
|
4711
|
+
viewRenderedSnapshot(_snapshot, _isPreview, _renderMethod) {}
|
3603
4712
|
preloadOnLoadLinksForView(element) {
|
3604
4713
|
session.preloadOnLoadLinksForView(element);
|
3605
4714
|
}
|
@@ -3607,7 +4716,14 @@ class FrameController {
|
|
3607
4716
|
willRenderFrame(currentElement, _newElement) {
|
3608
4717
|
this.previousFrameElement = currentElement.cloneNode(true);
|
3609
4718
|
}
|
3610
|
-
|
4719
|
+
visitCachedSnapshot=({element: element}) => {
|
4720
|
+
const frame = element.querySelector("#" + this.element.id);
|
4721
|
+
if (frame && this.previousFrameElement) {
|
4722
|
+
frame.replaceChildren(...this.previousFrameElement.children);
|
4723
|
+
}
|
4724
|
+
delete this.previousFrameElement;
|
4725
|
+
};
|
4726
|
+
async #loadFrameResponse(fetchResponse, document) {
|
3611
4727
|
const newFrameElement = await this.extractForeignFrameElement(document.body);
|
3612
4728
|
if (newFrameElement) {
|
3613
4729
|
const snapshot = new Snapshot(newFrameElement);
|
@@ -3618,41 +4734,40 @@ class FrameController {
|
|
3618
4734
|
this.complete = true;
|
3619
4735
|
session.frameRendered(fetchResponse, this.element);
|
3620
4736
|
session.frameLoaded(this.element);
|
3621
|
-
this.fetchResponseLoaded(fetchResponse);
|
3622
|
-
} else if (this
|
3623
|
-
this
|
4737
|
+
await this.fetchResponseLoaded(fetchResponse);
|
4738
|
+
} else if (this.#willHandleFrameMissingFromResponse(fetchResponse)) {
|
4739
|
+
this.#handleFrameMissingFromResponse(fetchResponse);
|
3624
4740
|
}
|
3625
4741
|
}
|
3626
|
-
async visit(url) {
|
3627
|
-
var _a;
|
4742
|
+
async #visit(url) {
|
3628
4743
|
const request = new FetchRequest(this, FetchMethod.get, url, new URLSearchParams, this.element);
|
3629
|
-
|
3630
|
-
this
|
4744
|
+
this.#currentFetchRequest?.cancel();
|
4745
|
+
this.#currentFetchRequest = request;
|
3631
4746
|
return new Promise((resolve => {
|
3632
|
-
this
|
3633
|
-
this
|
3634
|
-
this
|
4747
|
+
this.#resolveVisitPromise = () => {
|
4748
|
+
this.#resolveVisitPromise = () => {};
|
4749
|
+
this.#currentFetchRequest = null;
|
3635
4750
|
resolve();
|
3636
4751
|
};
|
3637
4752
|
request.perform();
|
3638
4753
|
}));
|
3639
4754
|
}
|
3640
|
-
navigateFrame(element, url, submitter) {
|
3641
|
-
const frame = this
|
3642
|
-
frame.delegate.proposeVisitIfNavigatedWithAction(frame, element,
|
3643
|
-
this
|
4755
|
+
#navigateFrame(element, url, submitter) {
|
4756
|
+
const frame = this.#findFrameElement(element, submitter);
|
4757
|
+
frame.delegate.proposeVisitIfNavigatedWithAction(frame, getVisitAction(submitter, element, frame));
|
4758
|
+
this.#withCurrentNavigationElement(element, (() => {
|
3644
4759
|
frame.src = url;
|
3645
4760
|
}));
|
3646
4761
|
}
|
3647
|
-
proposeVisitIfNavigatedWithAction(frame,
|
3648
|
-
this.action =
|
4762
|
+
proposeVisitIfNavigatedWithAction(frame, action = null) {
|
4763
|
+
this.action = action;
|
3649
4764
|
if (this.action) {
|
3650
4765
|
const pageSnapshot = PageSnapshot.fromElement(frame).clone();
|
3651
4766
|
const {visitCachedSnapshot: visitCachedSnapshot} = frame.delegate;
|
3652
|
-
frame.delegate.fetchResponseLoaded = fetchResponse => {
|
4767
|
+
frame.delegate.fetchResponseLoaded = async fetchResponse => {
|
3653
4768
|
if (frame.src) {
|
3654
4769
|
const {statusCode: statusCode, redirected: redirected} = fetchResponse;
|
3655
|
-
const responseHTML =
|
4770
|
+
const responseHTML = await fetchResponse.responseHTML;
|
3656
4771
|
const response = {
|
3657
4772
|
statusCode: statusCode,
|
3658
4773
|
redirected: redirected,
|
@@ -3678,16 +4793,16 @@ class FrameController {
|
|
3678
4793
|
session.history.update(method, expandURL(this.element.src || ""), this.restorationIdentifier);
|
3679
4794
|
}
|
3680
4795
|
}
|
3681
|
-
async handleUnvisitableFrameResponse(fetchResponse) {
|
4796
|
+
async #handleUnvisitableFrameResponse(fetchResponse) {
|
3682
4797
|
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
|
4798
|
+
await this.#visitResponse(fetchResponse.response);
|
3684
4799
|
}
|
3685
|
-
willHandleFrameMissingFromResponse(fetchResponse) {
|
4800
|
+
#willHandleFrameMissingFromResponse(fetchResponse) {
|
3686
4801
|
this.element.setAttribute("complete", "");
|
3687
4802
|
const response = fetchResponse.response;
|
3688
|
-
const visit = async (url, options
|
4803
|
+
const visit = async (url, options) => {
|
3689
4804
|
if (url instanceof Response) {
|
3690
|
-
this
|
4805
|
+
this.#visitResponse(url);
|
3691
4806
|
} else {
|
3692
4807
|
session.visit(url, options);
|
3693
4808
|
}
|
@@ -3702,15 +4817,15 @@ class FrameController {
|
|
3702
4817
|
});
|
3703
4818
|
return !event.defaultPrevented;
|
3704
4819
|
}
|
3705
|
-
handleFrameMissingFromResponse(fetchResponse) {
|
4820
|
+
#handleFrameMissingFromResponse(fetchResponse) {
|
3706
4821
|
this.view.missing();
|
3707
|
-
this
|
4822
|
+
this.#throwFrameMissingError(fetchResponse);
|
3708
4823
|
}
|
3709
|
-
throwFrameMissingError(fetchResponse) {
|
4824
|
+
#throwFrameMissingError(fetchResponse) {
|
3710
4825
|
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
4826
|
throw new TurboFrameMissingError(message);
|
3712
4827
|
}
|
3713
|
-
async visitResponse(response) {
|
4828
|
+
async #visitResponse(response) {
|
3714
4829
|
const wrapped = new FetchResponse(response);
|
3715
4830
|
const responseHTML = await wrapped.responseHTML;
|
3716
4831
|
const {location: location, redirected: redirected, statusCode: statusCode} = wrapped;
|
@@ -3722,10 +4837,9 @@ class FrameController {
|
|
3722
4837
|
}
|
3723
4838
|
});
|
3724
4839
|
}
|
3725
|
-
findFrameElement(element, submitter) {
|
3726
|
-
var _a;
|
4840
|
+
#findFrameElement(element, submitter) {
|
3727
4841
|
const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
|
3728
|
-
return
|
4842
|
+
return getFrameElementById(id) ?? this.element;
|
3729
4843
|
}
|
3730
4844
|
async extractForeignFrameElement(container) {
|
3731
4845
|
let element;
|
@@ -3746,13 +4860,13 @@ class FrameController {
|
|
3746
4860
|
}
|
3747
4861
|
return null;
|
3748
4862
|
}
|
3749
|
-
formActionIsVisitable(form, submitter) {
|
3750
|
-
const action = getAction(form, submitter);
|
4863
|
+
#formActionIsVisitable(form, submitter) {
|
4864
|
+
const action = getAction$1(form, submitter);
|
3751
4865
|
return locationIsVisitable(expandURL(action), this.rootLocation);
|
3752
4866
|
}
|
3753
|
-
shouldInterceptNavigation(element, submitter) {
|
4867
|
+
#shouldInterceptNavigation(element, submitter) {
|
3754
4868
|
const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
|
3755
|
-
if (element instanceof HTMLFormElement && !this
|
4869
|
+
if (element instanceof HTMLFormElement && !this.#formActionIsVisitable(element, submitter)) {
|
3756
4870
|
return false;
|
3757
4871
|
}
|
3758
4872
|
if (!this.enabled || id == "_top") {
|
@@ -3784,46 +4898,43 @@ class FrameController {
|
|
3784
4898
|
}
|
3785
4899
|
}
|
3786
4900
|
set sourceURL(sourceURL) {
|
3787
|
-
this
|
3788
|
-
this.element.src = sourceURL
|
4901
|
+
this.#ignoringChangesToAttribute("src", (() => {
|
4902
|
+
this.element.src = sourceURL ?? null;
|
3789
4903
|
}));
|
3790
4904
|
}
|
3791
4905
|
get loadingStyle() {
|
3792
4906
|
return this.element.loading;
|
3793
4907
|
}
|
3794
4908
|
get isLoading() {
|
3795
|
-
return this.formSubmission !== undefined || this
|
4909
|
+
return this.formSubmission !== undefined || this.#resolveVisitPromise() !== undefined;
|
3796
4910
|
}
|
3797
4911
|
get complete() {
|
3798
4912
|
return this.element.hasAttribute("complete");
|
3799
4913
|
}
|
3800
4914
|
set complete(value) {
|
3801
|
-
|
3802
|
-
|
3803
|
-
|
3804
|
-
|
3805
|
-
|
3806
|
-
}
|
3807
|
-
}));
|
4915
|
+
if (value) {
|
4916
|
+
this.element.setAttribute("complete", "");
|
4917
|
+
} else {
|
4918
|
+
this.element.removeAttribute("complete");
|
4919
|
+
}
|
3808
4920
|
}
|
3809
4921
|
get isActive() {
|
3810
|
-
return this.element.isActive && this
|
4922
|
+
return this.element.isActive && this.#connected;
|
3811
4923
|
}
|
3812
4924
|
get rootLocation() {
|
3813
|
-
var _a;
|
3814
4925
|
const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
|
3815
|
-
const root =
|
4926
|
+
const root = meta?.content ?? "/";
|
3816
4927
|
return expandURL(root);
|
3817
4928
|
}
|
3818
|
-
isIgnoringChangesTo(attributeName) {
|
3819
|
-
return this
|
4929
|
+
#isIgnoringChangesTo(attributeName) {
|
4930
|
+
return this.#ignoredAttributes.has(attributeName);
|
3820
4931
|
}
|
3821
|
-
ignoringChangesToAttribute(attributeName, callback) {
|
3822
|
-
this
|
4932
|
+
#ignoringChangesToAttribute(attributeName, callback) {
|
4933
|
+
this.#ignoredAttributes.add(attributeName);
|
3823
4934
|
callback();
|
3824
|
-
this
|
4935
|
+
this.#ignoredAttributes.delete(attributeName);
|
3825
4936
|
}
|
3826
|
-
withCurrentNavigationElement(element, callback) {
|
4937
|
+
#withCurrentNavigationElement(element, callback) {
|
3827
4938
|
this.currentNavigationElement = element;
|
3828
4939
|
callback();
|
3829
4940
|
delete this.currentNavigationElement;
|
@@ -3856,6 +4967,50 @@ function activateElement(element, currentURL) {
|
|
3856
4967
|
}
|
3857
4968
|
}
|
3858
4969
|
|
4970
|
+
const StreamActions = {
|
4971
|
+
after() {
|
4972
|
+
this.targetElements.forEach((e => e.parentElement?.insertBefore(this.templateContent, e.nextSibling)));
|
4973
|
+
},
|
4974
|
+
append() {
|
4975
|
+
this.removeDuplicateTargetChildren();
|
4976
|
+
this.targetElements.forEach((e => e.append(this.templateContent)));
|
4977
|
+
},
|
4978
|
+
before() {
|
4979
|
+
this.targetElements.forEach((e => e.parentElement?.insertBefore(this.templateContent, e)));
|
4980
|
+
},
|
4981
|
+
prepend() {
|
4982
|
+
this.removeDuplicateTargetChildren();
|
4983
|
+
this.targetElements.forEach((e => e.prepend(this.templateContent)));
|
4984
|
+
},
|
4985
|
+
remove() {
|
4986
|
+
this.targetElements.forEach((e => e.remove()));
|
4987
|
+
},
|
4988
|
+
replace() {
|
4989
|
+
const method = this.getAttribute("method");
|
4990
|
+
this.targetElements.forEach((targetElement => {
|
4991
|
+
if (method === "morph") {
|
4992
|
+
morphElements(targetElement, this.templateContent);
|
4993
|
+
} else {
|
4994
|
+
targetElement.replaceWith(this.templateContent);
|
4995
|
+
}
|
4996
|
+
}));
|
4997
|
+
},
|
4998
|
+
update() {
|
4999
|
+
const method = this.getAttribute("method");
|
5000
|
+
this.targetElements.forEach((targetElement => {
|
5001
|
+
if (method === "morph") {
|
5002
|
+
morphChildren(targetElement, this.templateContent);
|
5003
|
+
} else {
|
5004
|
+
targetElement.innerHTML = "";
|
5005
|
+
targetElement.append(this.templateContent);
|
5006
|
+
}
|
5007
|
+
}));
|
5008
|
+
},
|
5009
|
+
refresh() {
|
5010
|
+
session.refresh(this.baseURI, this.requestId);
|
5011
|
+
}
|
5012
|
+
};
|
5013
|
+
|
3859
5014
|
class StreamElement extends HTMLElement {
|
3860
5015
|
static async renderElement(newElement) {
|
3861
5016
|
await newElement.performAction();
|
@@ -3870,11 +5025,10 @@ class StreamElement extends HTMLElement {
|
|
3870
5025
|
}
|
3871
5026
|
}
|
3872
5027
|
async render() {
|
3873
|
-
|
3874
|
-
return (_a = this.renderPromise) !== null && _a !== void 0 ? _a : this.renderPromise = (async () => {
|
5028
|
+
return this.renderPromise ??= (async () => {
|
3875
5029
|
const event = this.beforeRenderEvent;
|
3876
5030
|
if (this.dispatchEvent(event)) {
|
3877
|
-
await
|
5031
|
+
await nextRepaint();
|
3878
5032
|
await event.detail.render(this);
|
3879
5033
|
}
|
3880
5034
|
})();
|
@@ -3882,15 +5036,14 @@ class StreamElement extends HTMLElement {
|
|
3882
5036
|
disconnect() {
|
3883
5037
|
try {
|
3884
5038
|
this.remove();
|
3885
|
-
} catch
|
5039
|
+
} catch {}
|
3886
5040
|
}
|
3887
5041
|
removeDuplicateTargetChildren() {
|
3888
5042
|
this.duplicateChildren.forEach((c => c.remove()));
|
3889
5043
|
}
|
3890
5044
|
get duplicateChildren() {
|
3891
|
-
var _a;
|
3892
5045
|
const existingChildren = this.targetElements.flatMap((e => [ ...e.children ])).filter((c => !!c.id));
|
3893
|
-
const newChildrenIds = [ ...
|
5046
|
+
const newChildrenIds = [ ...this.templateContent?.children || [] ].filter((c => !!c.id)).map((c => c.id));
|
3894
5047
|
return existingChildren.filter((c => newChildrenIds.includes(c.id)));
|
3895
5048
|
}
|
3896
5049
|
get performAction() {
|
@@ -3899,9 +5052,9 @@ class StreamElement extends HTMLElement {
|
|
3899
5052
|
if (actionFunction) {
|
3900
5053
|
return actionFunction;
|
3901
5054
|
}
|
3902
|
-
this
|
5055
|
+
this.#raise("unknown action");
|
3903
5056
|
}
|
3904
|
-
this
|
5057
|
+
this.#raise("action attribute is missing");
|
3905
5058
|
}
|
3906
5059
|
get targetElements() {
|
3907
5060
|
if (this.target) {
|
@@ -3909,7 +5062,7 @@ class StreamElement extends HTMLElement {
|
|
3909
5062
|
} else if (this.targets) {
|
3910
5063
|
return this.targetElementsByQuery;
|
3911
5064
|
} else {
|
3912
|
-
this
|
5065
|
+
this.#raise("target or targets attribute is missing");
|
3913
5066
|
}
|
3914
5067
|
}
|
3915
5068
|
get templateContent() {
|
@@ -3923,7 +5076,7 @@ class StreamElement extends HTMLElement {
|
|
3923
5076
|
} else if (this.firstElementChild instanceof HTMLTemplateElement) {
|
3924
5077
|
return this.firstElementChild;
|
3925
5078
|
}
|
3926
|
-
this
|
5079
|
+
this.#raise("first child element must be a <template> element");
|
3927
5080
|
}
|
3928
5081
|
get action() {
|
3929
5082
|
return this.getAttribute("action");
|
@@ -3934,12 +5087,14 @@ class StreamElement extends HTMLElement {
|
|
3934
5087
|
get targets() {
|
3935
5088
|
return this.getAttribute("targets");
|
3936
5089
|
}
|
3937
|
-
|
5090
|
+
get requestId() {
|
5091
|
+
return this.getAttribute("request-id");
|
5092
|
+
}
|
5093
|
+
#raise(message) {
|
3938
5094
|
throw new Error(`${this.description}: ${message}`);
|
3939
5095
|
}
|
3940
5096
|
get description() {
|
3941
|
-
|
3942
|
-
return (_b = ((_a = this.outerHTML.match(/<[^>]+>/)) !== null && _a !== void 0 ? _a : [])[0]) !== null && _b !== void 0 ? _b : "<turbo-stream>";
|
5097
|
+
return (this.outerHTML.match(/<[^>]+>/) ?? [])[0] ?? "<turbo-stream>";
|
3943
5098
|
}
|
3944
5099
|
get beforeRenderEvent() {
|
3945
5100
|
return new CustomEvent("turbo:before-stream-render", {
|
@@ -3952,8 +5107,7 @@ class StreamElement extends HTMLElement {
|
|
3952
5107
|
});
|
3953
5108
|
}
|
3954
5109
|
get targetElementsById() {
|
3955
|
-
|
3956
|
-
const element = (_a = this.ownerDocument) === null || _a === void 0 ? void 0 : _a.getElementById(this.target);
|
5110
|
+
const element = this.ownerDocument?.getElementById(this.target);
|
3957
5111
|
if (element !== null) {
|
3958
5112
|
return [ element ];
|
3959
5113
|
} else {
|
@@ -3961,8 +5115,7 @@ class StreamElement extends HTMLElement {
|
|
3961
5115
|
}
|
3962
5116
|
}
|
3963
5117
|
get targetElementsByQuery() {
|
3964
|
-
|
3965
|
-
const elements = (_a = this.ownerDocument) === null || _a === void 0 ? void 0 : _a.querySelectorAll(this.targets);
|
5118
|
+
const elements = this.ownerDocument?.querySelectorAll(this.targets);
|
3966
5119
|
if (elements.length !== 0) {
|
3967
5120
|
return Array.prototype.slice.call(elements);
|
3968
5121
|
} else {
|
@@ -3972,16 +5125,14 @@ class StreamElement extends HTMLElement {
|
|
3972
5125
|
}
|
3973
5126
|
|
3974
5127
|
class StreamSourceElement extends HTMLElement {
|
3975
|
-
|
3976
|
-
super(...arguments);
|
3977
|
-
this.streamSource = null;
|
3978
|
-
}
|
5128
|
+
streamSource=null;
|
3979
5129
|
connectedCallback() {
|
3980
5130
|
this.streamSource = this.src.match(/^ws{1,2}:/) ? new WebSocket(this.src) : new EventSource(this.src);
|
3981
5131
|
connectStreamSource(this.streamSource);
|
3982
5132
|
}
|
3983
5133
|
disconnectedCallback() {
|
3984
5134
|
if (this.streamSource) {
|
5135
|
+
this.streamSource.close();
|
3985
5136
|
disconnectStreamSource(this.streamSource);
|
3986
5137
|
}
|
3987
5138
|
}
|
@@ -4026,16 +5177,21 @@ if (customElements.get("turbo-stream-source") === undefined) {
|
|
4026
5177
|
}
|
4027
5178
|
})();
|
4028
5179
|
|
4029
|
-
window.Turbo =
|
5180
|
+
window.Turbo = {
|
5181
|
+
...Turbo,
|
5182
|
+
StreamActions: StreamActions
|
5183
|
+
};
|
4030
5184
|
|
4031
5185
|
start();
|
4032
5186
|
|
4033
|
-
var
|
5187
|
+
var Turbo$1 = Object.freeze({
|
4034
5188
|
__proto__: null,
|
5189
|
+
FetchEnctype: FetchEnctype,
|
5190
|
+
FetchMethod: FetchMethod,
|
5191
|
+
FetchRequest: FetchRequest,
|
5192
|
+
FetchResponse: FetchResponse,
|
4035
5193
|
FrameElement: FrameElement,
|
4036
|
-
|
4037
|
-
return FrameLoadingStyle;
|
4038
|
-
},
|
5194
|
+
FrameLoadingStyle: FrameLoadingStyle,
|
4039
5195
|
FrameRenderer: FrameRenderer,
|
4040
5196
|
PageRenderer: PageRenderer,
|
4041
5197
|
PageSnapshot: PageSnapshot,
|
@@ -4046,6 +5202,10 @@ var turbo_es2017Esm = Object.freeze({
|
|
4046
5202
|
clearCache: clearCache,
|
4047
5203
|
connectStreamSource: connectStreamSource,
|
4048
5204
|
disconnectStreamSource: disconnectStreamSource,
|
5205
|
+
fetch: fetchWithTurboHeaders,
|
5206
|
+
fetchEnctypeFromString: fetchEnctypeFromString,
|
5207
|
+
fetchMethodFromString: fetchMethodFromString,
|
5208
|
+
isSafe: isSafe,
|
4049
5209
|
navigator: navigator$1,
|
4050
5210
|
registerAdapter: registerAdapter,
|
4051
5211
|
renderStreamMessage: renderStreamMessage,
|
@@ -4060,14 +5220,14 @@ var turbo_es2017Esm = Object.freeze({
|
|
4060
5220
|
let consumer;
|
4061
5221
|
|
4062
5222
|
async function getConsumer() {
|
4063
|
-
return consumer || setConsumer(createConsumer().then(setConsumer));
|
5223
|
+
return consumer || setConsumer(createConsumer$1().then(setConsumer));
|
4064
5224
|
}
|
4065
5225
|
|
4066
5226
|
function setConsumer(newConsumer) {
|
4067
5227
|
return consumer = newConsumer;
|
4068
5228
|
}
|
4069
5229
|
|
4070
|
-
async function createConsumer() {
|
5230
|
+
async function createConsumer$1() {
|
4071
5231
|
const {createConsumer: createConsumer} = await Promise.resolve().then((function() {
|
4072
5232
|
return index;
|
4073
5233
|
}));
|
@@ -4083,7 +5243,7 @@ var cable = Object.freeze({
|
|
4083
5243
|
__proto__: null,
|
4084
5244
|
getConsumer: getConsumer,
|
4085
5245
|
setConsumer: setConsumer,
|
4086
|
-
createConsumer: createConsumer,
|
5246
|
+
createConsumer: createConsumer$1,
|
4087
5247
|
subscribeTo: subscribeTo
|
4088
5248
|
});
|
4089
5249
|
|
@@ -4193,6 +5353,8 @@ function isBodyInit(body) {
|
|
4193
5353
|
return body instanceof FormData || body instanceof URLSearchParams;
|
4194
5354
|
}
|
4195
5355
|
|
5356
|
+
window.Turbo = Turbo$1;
|
5357
|
+
|
4196
5358
|
addEventListener("turbo:before-fetch-request", encodeMethodIntoRequestBody);
|
4197
5359
|
|
4198
5360
|
var adapters = {
|
@@ -4309,6 +5471,8 @@ ConnectionMonitor.staleThreshold = 6;
|
|
4309
5471
|
|
4310
5472
|
ConnectionMonitor.reconnectionBackoffRate = .15;
|
4311
5473
|
|
5474
|
+
var ConnectionMonitor$1 = ConnectionMonitor;
|
5475
|
+
|
4312
5476
|
var INTERNAL = {
|
4313
5477
|
message_types: {
|
4314
5478
|
welcome: "welcome",
|
@@ -4320,7 +5484,8 @@ var INTERNAL = {
|
|
4320
5484
|
disconnect_reasons: {
|
4321
5485
|
unauthorized: "unauthorized",
|
4322
5486
|
invalid_request: "invalid_request",
|
4323
|
-
server_restart: "server_restart"
|
5487
|
+
server_restart: "server_restart",
|
5488
|
+
remote: "remote"
|
4324
5489
|
},
|
4325
5490
|
default_mount_path: "/cable",
|
4326
5491
|
protocols: [ "actioncable-v1-json", "actioncable-unsupported" ]
|
@@ -4337,7 +5502,7 @@ class Connection {
|
|
4337
5502
|
this.open = this.open.bind(this);
|
4338
5503
|
this.consumer = consumer;
|
4339
5504
|
this.subscriptions = this.consumer.subscriptions;
|
4340
|
-
this.monitor = new ConnectionMonitor(this);
|
5505
|
+
this.monitor = new ConnectionMonitor$1(this);
|
4341
5506
|
this.disconnected = true;
|
4342
5507
|
}
|
4343
5508
|
send(data) {
|
@@ -4353,11 +5518,12 @@ class Connection {
|
|
4353
5518
|
logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`);
|
4354
5519
|
return false;
|
4355
5520
|
} else {
|
4356
|
-
|
5521
|
+
const socketProtocols = [ ...protocols, ...this.consumer.subprotocols || [] ];
|
5522
|
+
logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${socketProtocols}`);
|
4357
5523
|
if (this.webSocket) {
|
4358
5524
|
this.uninstallEventHandlers();
|
4359
5525
|
}
|
4360
|
-
this.webSocket = new adapters.WebSocket(this.consumer.url,
|
5526
|
+
this.webSocket = new adapters.WebSocket(this.consumer.url, socketProtocols);
|
4361
5527
|
this.installEventHandlers();
|
4362
5528
|
this.monitor.start();
|
4363
5529
|
return true;
|
@@ -4369,7 +5535,7 @@ class Connection {
|
|
4369
5535
|
if (!allowReconnect) {
|
4370
5536
|
this.monitor.stop();
|
4371
5537
|
}
|
4372
|
-
if (this.
|
5538
|
+
if (this.isOpen()) {
|
4373
5539
|
return this.webSocket.close();
|
4374
5540
|
}
|
4375
5541
|
}
|
@@ -4399,6 +5565,9 @@ class Connection {
|
|
4399
5565
|
isActive() {
|
4400
5566
|
return this.isState("open", "connecting");
|
4401
5567
|
}
|
5568
|
+
triedToReconnect() {
|
5569
|
+
return this.monitor.reconnectAttempts > 0;
|
5570
|
+
}
|
4402
5571
|
isProtocolSupported() {
|
4403
5572
|
return indexOf.call(supportedProtocols, this.getProtocol()) >= 0;
|
4404
5573
|
}
|
@@ -4438,6 +5607,9 @@ Connection.prototype.events = {
|
|
4438
5607
|
const {identifier: identifier, message: message, reason: reason, reconnect: reconnect, type: type} = JSON.parse(event.data);
|
4439
5608
|
switch (type) {
|
4440
5609
|
case message_types.welcome:
|
5610
|
+
if (this.triedToReconnect()) {
|
5611
|
+
this.reconnectAttempted = true;
|
5612
|
+
}
|
4441
5613
|
this.monitor.recordConnect();
|
4442
5614
|
return this.subscriptions.reload();
|
4443
5615
|
|
@@ -4452,7 +5624,16 @@ Connection.prototype.events = {
|
|
4452
5624
|
|
4453
5625
|
case message_types.confirmation:
|
4454
5626
|
this.subscriptions.confirmSubscription(identifier);
|
4455
|
-
|
5627
|
+
if (this.reconnectAttempted) {
|
5628
|
+
this.reconnectAttempted = false;
|
5629
|
+
return this.subscriptions.notify(identifier, "connected", {
|
5630
|
+
reconnected: true
|
5631
|
+
});
|
5632
|
+
} else {
|
5633
|
+
return this.subscriptions.notify(identifier, "connected", {
|
5634
|
+
reconnected: false
|
5635
|
+
});
|
5636
|
+
}
|
4456
5637
|
|
4457
5638
|
case message_types.rejection:
|
4458
5639
|
return this.subscriptions.reject(identifier);
|
@@ -4487,6 +5668,8 @@ Connection.prototype.events = {
|
|
4487
5668
|
}
|
4488
5669
|
};
|
4489
5670
|
|
5671
|
+
var Connection$1 = Connection;
|
5672
|
+
|
4490
5673
|
const extend = function(object, properties) {
|
4491
5674
|
if (properties != null) {
|
4492
5675
|
for (let key in properties) {
|
@@ -4556,10 +5739,12 @@ class SubscriptionGuarantor {
|
|
4556
5739
|
}
|
4557
5740
|
}
|
4558
5741
|
|
5742
|
+
var SubscriptionGuarantor$1 = SubscriptionGuarantor;
|
5743
|
+
|
4559
5744
|
class Subscriptions {
|
4560
5745
|
constructor(consumer) {
|
4561
5746
|
this.consumer = consumer;
|
4562
|
-
this.guarantor = new SubscriptionGuarantor(this);
|
5747
|
+
this.guarantor = new SubscriptionGuarantor$1(this);
|
4563
5748
|
this.subscriptions = [];
|
4564
5749
|
}
|
4565
5750
|
create(channelName, mixin) {
|
@@ -4636,7 +5821,8 @@ class Consumer {
|
|
4636
5821
|
constructor(url) {
|
4637
5822
|
this._url = url;
|
4638
5823
|
this.subscriptions = new Subscriptions(this);
|
4639
|
-
this.connection = new Connection(this);
|
5824
|
+
this.connection = new Connection$1(this);
|
5825
|
+
this.subprotocols = [];
|
4640
5826
|
}
|
4641
5827
|
get url() {
|
4642
5828
|
return createWebSocketURL(this._url);
|
@@ -4657,6 +5843,9 @@ class Consumer {
|
|
4657
5843
|
return this.connection.open();
|
4658
5844
|
}
|
4659
5845
|
}
|
5846
|
+
addSubProtocol(subprotocol) {
|
5847
|
+
this.subprotocols = [ ...this.subprotocols, subprotocol ];
|
5848
|
+
}
|
4660
5849
|
}
|
4661
5850
|
|
4662
5851
|
function createWebSocketURL(url) {
|
@@ -4674,7 +5863,7 @@ function createWebSocketURL(url) {
|
|
4674
5863
|
}
|
4675
5864
|
}
|
4676
5865
|
|
4677
|
-
function createConsumer
|
5866
|
+
function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) {
|
4678
5867
|
return new Consumer(url);
|
4679
5868
|
}
|
4680
5869
|
|
@@ -4687,18 +5876,18 @@ function getConfig(name) {
|
|
4687
5876
|
|
4688
5877
|
var index = Object.freeze({
|
4689
5878
|
__proto__: null,
|
4690
|
-
Connection: Connection,
|
4691
|
-
ConnectionMonitor: ConnectionMonitor,
|
5879
|
+
Connection: Connection$1,
|
5880
|
+
ConnectionMonitor: ConnectionMonitor$1,
|
4692
5881
|
Consumer: Consumer,
|
4693
5882
|
INTERNAL: INTERNAL,
|
4694
5883
|
Subscription: Subscription,
|
4695
5884
|
Subscriptions: Subscriptions,
|
4696
|
-
SubscriptionGuarantor: SubscriptionGuarantor,
|
5885
|
+
SubscriptionGuarantor: SubscriptionGuarantor$1,
|
4697
5886
|
adapters: adapters,
|
4698
5887
|
createWebSocketURL: createWebSocketURL,
|
4699
5888
|
logger: logger,
|
4700
|
-
createConsumer: createConsumer
|
5889
|
+
createConsumer: createConsumer,
|
4701
5890
|
getConfig: getConfig
|
4702
5891
|
});
|
4703
5892
|
|
4704
|
-
export {
|
5893
|
+
export { Turbo$1 as Turbo, cable };
|