@hotwired/turbo 8.0.19 → 8.0.21
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.
- package/dist/turbo.es2017-esm.js +385 -392
- package/dist/turbo.es2017-umd.js +385 -393
- package/package.json +2 -3
package/dist/turbo.es2017-esm.js
CHANGED
|
@@ -1,104 +1,7 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
Turbo 8.0.
|
|
3
|
-
Copyright ©
|
|
2
|
+
Turbo 8.0.21
|
|
3
|
+
Copyright © 2026 37signals LLC
|
|
4
4
|
*/
|
|
5
|
-
/**
|
|
6
|
-
* The MIT License (MIT)
|
|
7
|
-
*
|
|
8
|
-
* Copyright (c) 2019 Javan Makhmali
|
|
9
|
-
*
|
|
10
|
-
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
-
* of this software and associated documentation files (the "Software"), to deal
|
|
12
|
-
* in the Software without restriction, including without limitation the rights
|
|
13
|
-
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
-
* copies of the Software, and to permit persons to whom the Software is
|
|
15
|
-
* furnished to do so, subject to the following conditions:
|
|
16
|
-
*
|
|
17
|
-
* The above copyright notice and this permission notice shall be included in
|
|
18
|
-
* all copies or substantial portions of the Software.
|
|
19
|
-
*
|
|
20
|
-
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
-
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
-
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
-
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
-
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
-
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
26
|
-
* THE SOFTWARE.
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
(function (prototype) {
|
|
30
|
-
if (typeof prototype.requestSubmit == "function") return
|
|
31
|
-
|
|
32
|
-
prototype.requestSubmit = function (submitter) {
|
|
33
|
-
if (submitter) {
|
|
34
|
-
validateSubmitter(submitter, this);
|
|
35
|
-
submitter.click();
|
|
36
|
-
} else {
|
|
37
|
-
submitter = document.createElement("input");
|
|
38
|
-
submitter.type = "submit";
|
|
39
|
-
submitter.hidden = true;
|
|
40
|
-
this.appendChild(submitter);
|
|
41
|
-
submitter.click();
|
|
42
|
-
this.removeChild(submitter);
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
function validateSubmitter(submitter, form) {
|
|
47
|
-
submitter instanceof HTMLElement || raise(TypeError, "parameter 1 is not of type 'HTMLElement'");
|
|
48
|
-
submitter.type == "submit" || raise(TypeError, "The specified element is not a submit button");
|
|
49
|
-
submitter.form == form ||
|
|
50
|
-
raise(DOMException, "The specified element is not owned by this form element", "NotFoundError");
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function raise(errorConstructor, message, name) {
|
|
54
|
-
throw new errorConstructor("Failed to execute 'requestSubmit' on 'HTMLFormElement': " + message + ".", name)
|
|
55
|
-
}
|
|
56
|
-
})(HTMLFormElement.prototype);
|
|
57
|
-
|
|
58
|
-
const submittersByForm = new WeakMap();
|
|
59
|
-
|
|
60
|
-
function findSubmitterFromClickTarget(target) {
|
|
61
|
-
const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
|
|
62
|
-
const candidate = element ? element.closest("input, button") : null;
|
|
63
|
-
return candidate?.type == "submit" ? candidate : null
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function clickCaptured(event) {
|
|
67
|
-
const submitter = findSubmitterFromClickTarget(event.target);
|
|
68
|
-
|
|
69
|
-
if (submitter && submitter.form) {
|
|
70
|
-
submittersByForm.set(submitter.form, submitter);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
(function () {
|
|
75
|
-
if ("submitter" in Event.prototype) return
|
|
76
|
-
|
|
77
|
-
let prototype = window.Event.prototype;
|
|
78
|
-
// Certain versions of Safari 15 have a bug where they won't
|
|
79
|
-
// populate the submitter. This hurts TurboDrive's enable/disable detection.
|
|
80
|
-
// See https://bugs.webkit.org/show_bug.cgi?id=229660
|
|
81
|
-
if ("SubmitEvent" in window) {
|
|
82
|
-
const prototypeOfSubmitEvent = window.SubmitEvent.prototype;
|
|
83
|
-
|
|
84
|
-
if (/Apple Computer/.test(navigator.vendor) && !("submitter" in prototypeOfSubmitEvent)) {
|
|
85
|
-
prototype = prototypeOfSubmitEvent;
|
|
86
|
-
} else {
|
|
87
|
-
return // polyfill not needed
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
addEventListener("click", clickCaptured, true);
|
|
92
|
-
|
|
93
|
-
Object.defineProperty(prototype, "submitter", {
|
|
94
|
-
get() {
|
|
95
|
-
if (this.type == "submit" && this.target instanceof HTMLFormElement) {
|
|
96
|
-
return submittersByForm.get(this.target)
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
})();
|
|
101
|
-
|
|
102
5
|
const FrameLoadingStyle = {
|
|
103
6
|
eager: "eager",
|
|
104
7
|
lazy: "lazy"
|
|
@@ -374,10 +277,6 @@ function nextEventLoopTick() {
|
|
|
374
277
|
return new Promise((resolve) => setTimeout(() => resolve(), 0))
|
|
375
278
|
}
|
|
376
279
|
|
|
377
|
-
function nextMicrotask() {
|
|
378
|
-
return Promise.resolve()
|
|
379
|
-
}
|
|
380
|
-
|
|
381
280
|
function parseHTMLDocument(html = "") {
|
|
382
281
|
return new DOMParser().parseFromString(html, "text/html")
|
|
383
282
|
}
|
|
@@ -406,7 +305,7 @@ function uuid() {
|
|
|
406
305
|
} else if (i == 19) {
|
|
407
306
|
return (Math.floor(Math.random() * 4) + 8).toString(16)
|
|
408
307
|
} else {
|
|
409
|
-
return Math.floor(Math.random() *
|
|
308
|
+
return Math.floor(Math.random() * 16).toString(16)
|
|
410
309
|
}
|
|
411
310
|
})
|
|
412
311
|
.join("")
|
|
@@ -558,14 +457,13 @@ function findLinkFromClickTarget(target) {
|
|
|
558
457
|
const link = findClosestRecursively(target, "a[href], a[xlink\\:href]");
|
|
559
458
|
|
|
560
459
|
if (!link) return null
|
|
460
|
+
if (link.href.startsWith("#")) return null
|
|
561
461
|
if (link.hasAttribute("download")) return null
|
|
562
|
-
if (link.hasAttribute("target") && link.target !== "_self") return null
|
|
563
462
|
|
|
564
|
-
|
|
565
|
-
|
|
463
|
+
const linkTarget = link.getAttribute("target");
|
|
464
|
+
if (linkTarget && linkTarget !== "_self") return null
|
|
566
465
|
|
|
567
|
-
|
|
568
|
-
return expandURL(link.getAttribute("href") || "")
|
|
466
|
+
return link
|
|
569
467
|
}
|
|
570
468
|
|
|
571
469
|
function debounce(fn, delay) {
|
|
@@ -656,6 +554,10 @@ function locationIsVisitable(location, rootLocation) {
|
|
|
656
554
|
return isPrefixedBy(location, rootLocation) && !config.drive.unvisitableExtensions.has(getExtension(location))
|
|
657
555
|
}
|
|
658
556
|
|
|
557
|
+
function getLocationForLink(link) {
|
|
558
|
+
return expandURL(link.getAttribute("href") || "")
|
|
559
|
+
}
|
|
560
|
+
|
|
659
561
|
function getRequestURL(url) {
|
|
660
562
|
const anchor = getAnchor(url);
|
|
661
563
|
return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href
|
|
@@ -1071,35 +973,113 @@ function importStreamElements(fragment) {
|
|
|
1071
973
|
return fragment
|
|
1072
974
|
}
|
|
1073
975
|
|
|
1074
|
-
const
|
|
976
|
+
const identity = key => key;
|
|
1075
977
|
|
|
1076
|
-
class
|
|
1077
|
-
|
|
1078
|
-
|
|
978
|
+
class LRUCache {
|
|
979
|
+
keys = []
|
|
980
|
+
entries = {}
|
|
981
|
+
#toCacheKey
|
|
982
|
+
|
|
983
|
+
constructor(size, toCacheKey = identity) {
|
|
984
|
+
this.size = size;
|
|
985
|
+
this.#toCacheKey = toCacheKey;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
has(key) {
|
|
989
|
+
return this.#toCacheKey(key) in this.entries
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
get(key) {
|
|
993
|
+
if (this.has(key)) {
|
|
994
|
+
const entry = this.read(key);
|
|
995
|
+
this.touch(key);
|
|
996
|
+
return entry
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
put(key, entry) {
|
|
1001
|
+
this.write(key, entry);
|
|
1002
|
+
this.touch(key);
|
|
1003
|
+
return entry
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
clear() {
|
|
1007
|
+
for (const key of Object.keys(this.entries)) {
|
|
1008
|
+
this.evict(key);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Private
|
|
1013
|
+
|
|
1014
|
+
read(key) {
|
|
1015
|
+
return this.entries[this.#toCacheKey(key)]
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
write(key, entry) {
|
|
1019
|
+
this.entries[this.#toCacheKey(key)] = entry;
|
|
1020
|
+
}
|
|
1079
1021
|
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1022
|
+
touch(key) {
|
|
1023
|
+
key = this.#toCacheKey(key);
|
|
1024
|
+
const index = this.keys.indexOf(key);
|
|
1025
|
+
if (index > -1) this.keys.splice(index, 1);
|
|
1026
|
+
this.keys.unshift(key);
|
|
1027
|
+
this.trim();
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
trim() {
|
|
1031
|
+
for (const key of this.keys.splice(this.size)) {
|
|
1032
|
+
this.evict(key);
|
|
1083
1033
|
}
|
|
1084
1034
|
}
|
|
1085
1035
|
|
|
1086
|
-
|
|
1087
|
-
this.
|
|
1036
|
+
evict(key) {
|
|
1037
|
+
delete this.entries[key];
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
const PREFETCH_DELAY = 100;
|
|
1088
1042
|
|
|
1043
|
+
class PrefetchCache extends LRUCache {
|
|
1044
|
+
#prefetchTimeout = null
|
|
1045
|
+
#maxAges = {}
|
|
1046
|
+
|
|
1047
|
+
constructor(size = 1, prefetchDelay = PREFETCH_DELAY) {
|
|
1048
|
+
super(size, toCacheKey);
|
|
1049
|
+
this.prefetchDelay = prefetchDelay;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
putLater(url, request, ttl) {
|
|
1089
1053
|
this.#prefetchTimeout = setTimeout(() => {
|
|
1090
1054
|
request.perform();
|
|
1091
|
-
this.
|
|
1055
|
+
this.put(url, request, ttl);
|
|
1092
1056
|
this.#prefetchTimeout = null;
|
|
1093
|
-
},
|
|
1057
|
+
}, this.prefetchDelay);
|
|
1094
1058
|
}
|
|
1095
1059
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1060
|
+
put(url, request, ttl = cacheTtl) {
|
|
1061
|
+
super.put(url, request);
|
|
1062
|
+
this.#maxAges[toCacheKey(url)] = new Date(new Date().getTime() + ttl);
|
|
1098
1063
|
}
|
|
1099
1064
|
|
|
1100
1065
|
clear() {
|
|
1066
|
+
super.clear();
|
|
1101
1067
|
if (this.#prefetchTimeout) clearTimeout(this.#prefetchTimeout);
|
|
1102
|
-
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
evict(key) {
|
|
1071
|
+
super.evict(key);
|
|
1072
|
+
delete this.#maxAges[key];
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
has(key) {
|
|
1076
|
+
if (super.has(key)) {
|
|
1077
|
+
const maxAge = this.#maxAges[toCacheKey(key)];
|
|
1078
|
+
|
|
1079
|
+
return maxAge && maxAge > Date.now()
|
|
1080
|
+
} else {
|
|
1081
|
+
return false
|
|
1082
|
+
}
|
|
1103
1083
|
}
|
|
1104
1084
|
}
|
|
1105
1085
|
|
|
@@ -2139,6 +2119,7 @@ var Idiomorph = (function () {
|
|
|
2139
2119
|
* @property {ConfigInternal['callbacks']} callbacks
|
|
2140
2120
|
* @property {ConfigInternal['head']} head
|
|
2141
2121
|
* @property {HTMLDivElement} pantry
|
|
2122
|
+
* @property {Element[]} activeElementAndParents
|
|
2142
2123
|
*/
|
|
2143
2124
|
|
|
2144
2125
|
//=============================================================================
|
|
@@ -2214,14 +2195,6 @@ var Idiomorph = (function () {
|
|
|
2214
2195
|
*/
|
|
2215
2196
|
function morphOuterHTML(ctx, oldNode, newNode) {
|
|
2216
2197
|
const oldParent = normalizeParent(oldNode);
|
|
2217
|
-
|
|
2218
|
-
// basis for calulating which nodes were morphed
|
|
2219
|
-
// since there may be unmorphed sibling nodes
|
|
2220
|
-
let childNodes = Array.from(oldParent.childNodes);
|
|
2221
|
-
const index = childNodes.indexOf(oldNode);
|
|
2222
|
-
// how many elements are to the right of the oldNode
|
|
2223
|
-
const rightMargin = childNodes.length - (index + 1);
|
|
2224
|
-
|
|
2225
2198
|
morphChildren(
|
|
2226
2199
|
ctx,
|
|
2227
2200
|
oldParent,
|
|
@@ -2230,10 +2203,8 @@ var Idiomorph = (function () {
|
|
|
2230
2203
|
oldNode, // start point for iteration
|
|
2231
2204
|
oldNode.nextSibling, // end point for iteration
|
|
2232
2205
|
);
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
childNodes = Array.from(oldParent.childNodes);
|
|
2236
|
-
return childNodes.slice(index, childNodes.length - rightMargin);
|
|
2206
|
+
// this is safe even with siblings, because normalizeParent returns a SlicedParentNode if needed.
|
|
2207
|
+
return Array.from(oldParent.childNodes);
|
|
2237
2208
|
}
|
|
2238
2209
|
|
|
2239
2210
|
/**
|
|
@@ -2262,8 +2233,11 @@ var Idiomorph = (function () {
|
|
|
2262
2233
|
|
|
2263
2234
|
const results = fn();
|
|
2264
2235
|
|
|
2265
|
-
if (
|
|
2266
|
-
|
|
2236
|
+
if (
|
|
2237
|
+
activeElementId &&
|
|
2238
|
+
activeElementId !== document.activeElement?.getAttribute("id")
|
|
2239
|
+
) {
|
|
2240
|
+
activeElement = ctx.target.querySelector(`[id="${activeElementId}"]`);
|
|
2267
2241
|
activeElement?.focus();
|
|
2268
2242
|
}
|
|
2269
2243
|
if (activeElement && !activeElement.selectionEnd && selectionEnd) {
|
|
@@ -2341,17 +2315,23 @@ var Idiomorph = (function () {
|
|
|
2341
2315
|
}
|
|
2342
2316
|
|
|
2343
2317
|
// if the matching node is elsewhere in the original content
|
|
2344
|
-
if (newChild instanceof Element
|
|
2345
|
-
//
|
|
2346
|
-
const
|
|
2347
|
-
|
|
2348
|
-
newChild.id,
|
|
2349
|
-
insertionPoint,
|
|
2350
|
-
ctx,
|
|
2318
|
+
if (newChild instanceof Element) {
|
|
2319
|
+
// we can pretend the id is non-null because the next `.has` line will reject it if not
|
|
2320
|
+
const newChildId = /** @type {String} */ (
|
|
2321
|
+
newChild.getAttribute("id")
|
|
2351
2322
|
);
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2323
|
+
if (ctx.persistentIds.has(newChildId)) {
|
|
2324
|
+
// move it and all its children here and morph
|
|
2325
|
+
const movedChild = moveBeforeById(
|
|
2326
|
+
oldParent,
|
|
2327
|
+
newChildId,
|
|
2328
|
+
insertionPoint,
|
|
2329
|
+
ctx,
|
|
2330
|
+
);
|
|
2331
|
+
morphNode(movedChild, newChild, ctx);
|
|
2332
|
+
insertionPoint = movedChild.nextSibling;
|
|
2333
|
+
continue;
|
|
2334
|
+
}
|
|
2355
2335
|
}
|
|
2356
2336
|
|
|
2357
2337
|
// last resort: insert the new node from scratch
|
|
@@ -2461,7 +2441,8 @@ var Idiomorph = (function () {
|
|
|
2461
2441
|
|
|
2462
2442
|
// if the current node contains active element, stop looking for better future matches,
|
|
2463
2443
|
// because if one is found, this node will be moved to the pantry, reparenting it and thus losing focus
|
|
2464
|
-
|
|
2444
|
+
// @ts-ignore pretend cursor is Element rather than Node, we're just testing for array inclusion
|
|
2445
|
+
if (ctx.activeElementAndParents.includes(cursor)) break;
|
|
2465
2446
|
|
|
2466
2447
|
cursor = cursor.nextSibling;
|
|
2467
2448
|
}
|
|
@@ -2511,7 +2492,9 @@ var Idiomorph = (function () {
|
|
|
2511
2492
|
// If oldElt has an `id` with possible state and it doesn't match newElt.id then avoid morphing.
|
|
2512
2493
|
// We'll still match an anonymous node with an IDed newElt, though, because if it got this far,
|
|
2513
2494
|
// its not persistent, and new nodes can't have any hidden state.
|
|
2514
|
-
|
|
2495
|
+
// We can't use .id because of form input shadowing, and we can't count on .getAttribute's presence because it could be a document-fragment
|
|
2496
|
+
(!oldElt.getAttribute?.("id") ||
|
|
2497
|
+
oldElt.getAttribute?.("id") === newElt.getAttribute?.("id"))
|
|
2515
2498
|
);
|
|
2516
2499
|
}
|
|
2517
2500
|
|
|
@@ -2575,8 +2558,11 @@ var Idiomorph = (function () {
|
|
|
2575
2558
|
const target =
|
|
2576
2559
|
/** @type {Element} - will always be found */
|
|
2577
2560
|
(
|
|
2578
|
-
ctx.target.
|
|
2579
|
-
|
|
2561
|
+
// ctx.target.id unsafe because of form input shadowing
|
|
2562
|
+
// ctx.target could be a document fragment which doesn't have `getAttribute`
|
|
2563
|
+
(ctx.target.getAttribute?.("id") === id && ctx.target) ||
|
|
2564
|
+
ctx.target.querySelector(`[id="${id}"]`) ||
|
|
2565
|
+
ctx.pantry.querySelector(`[id="${id}"]`)
|
|
2580
2566
|
);
|
|
2581
2567
|
removeElementFromAncestorsIdMaps(target, ctx);
|
|
2582
2568
|
moveBefore(parentNode, target, after);
|
|
@@ -2592,7 +2578,8 @@ var Idiomorph = (function () {
|
|
|
2592
2578
|
* @param {MorphContext} ctx
|
|
2593
2579
|
*/
|
|
2594
2580
|
function removeElementFromAncestorsIdMaps(element, ctx) {
|
|
2595
|
-
|
|
2581
|
+
// we know id is non-null String, because this function is only called on elements with ids
|
|
2582
|
+
const id = /** @type {String} */ (element.getAttribute("id"));
|
|
2596
2583
|
/** @ts-ignore - safe to loop in this way **/
|
|
2597
2584
|
while ((element = element.parentNode)) {
|
|
2598
2585
|
let idSet = ctx.idMap.get(element);
|
|
@@ -3030,6 +3017,7 @@ var Idiomorph = (function () {
|
|
|
3030
3017
|
idMap: idMap,
|
|
3031
3018
|
persistentIds: persistentIds,
|
|
3032
3019
|
pantry: createPantry(),
|
|
3020
|
+
activeElementAndParents: createActiveElementAndParents(oldNode),
|
|
3033
3021
|
callbacks: mergedConfig.callbacks,
|
|
3034
3022
|
head: mergedConfig.head,
|
|
3035
3023
|
};
|
|
@@ -3070,6 +3058,24 @@ var Idiomorph = (function () {
|
|
|
3070
3058
|
return pantry;
|
|
3071
3059
|
}
|
|
3072
3060
|
|
|
3061
|
+
/**
|
|
3062
|
+
* @param {Element} oldNode
|
|
3063
|
+
* @returns {Element[]}
|
|
3064
|
+
*/
|
|
3065
|
+
function createActiveElementAndParents(oldNode) {
|
|
3066
|
+
/** @type {Element[]} */
|
|
3067
|
+
let activeElementAndParents = [];
|
|
3068
|
+
let elt = document.activeElement;
|
|
3069
|
+
if (elt?.tagName !== "BODY" && oldNode.contains(elt)) {
|
|
3070
|
+
while (elt) {
|
|
3071
|
+
activeElementAndParents.push(elt);
|
|
3072
|
+
if (elt === oldNode) break;
|
|
3073
|
+
elt = elt.parentElement;
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
return activeElementAndParents;
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3073
3079
|
/**
|
|
3074
3080
|
* Returns all elements with an ID contained within the root element and its descendants
|
|
3075
3081
|
*
|
|
@@ -3078,7 +3084,8 @@ var Idiomorph = (function () {
|
|
|
3078
3084
|
*/
|
|
3079
3085
|
function findIdElements(root) {
|
|
3080
3086
|
let elements = Array.from(root.querySelectorAll("[id]"));
|
|
3081
|
-
|
|
3087
|
+
// root could be a document fragment which doesn't have `getAttribute`
|
|
3088
|
+
if (root.getAttribute?.("id")) {
|
|
3082
3089
|
elements.push(root);
|
|
3083
3090
|
}
|
|
3084
3091
|
return elements;
|
|
@@ -3097,7 +3104,9 @@ var Idiomorph = (function () {
|
|
|
3097
3104
|
*/
|
|
3098
3105
|
function populateIdMapWithTree(idMap, persistentIds, root, elements) {
|
|
3099
3106
|
for (const elt of elements) {
|
|
3100
|
-
|
|
3107
|
+
// we can pretend id is non-null String, because the .has line will reject it immediately if not
|
|
3108
|
+
const id = /** @type {String} */ (elt.getAttribute("id"));
|
|
3109
|
+
if (persistentIds.has(id)) {
|
|
3101
3110
|
/** @type {Element|null} */
|
|
3102
3111
|
let current = elt;
|
|
3103
3112
|
// walk up the parent hierarchy of that element, adding the id
|
|
@@ -3109,7 +3118,7 @@ var Idiomorph = (function () {
|
|
|
3109
3118
|
idSet = new Set();
|
|
3110
3119
|
idMap.set(current, idSet);
|
|
3111
3120
|
}
|
|
3112
|
-
idSet.add(
|
|
3121
|
+
idSet.add(id);
|
|
3113
3122
|
|
|
3114
3123
|
if (current === root) break;
|
|
3115
3124
|
current = current.parentElement;
|
|
@@ -3223,8 +3232,9 @@ var Idiomorph = (function () {
|
|
|
3223
3232
|
if (newContent.parentNode) {
|
|
3224
3233
|
// we can't use the parent directly because newContent may have siblings
|
|
3225
3234
|
// that we don't want in the morph, and reparenting might be expensive (TODO is it?),
|
|
3226
|
-
// so we create a
|
|
3227
|
-
|
|
3235
|
+
// so instead we create a fake parent node that only sees a slice of its children.
|
|
3236
|
+
/** @type {Element} */
|
|
3237
|
+
return /** @type {any} */ (new SlicedParentNode(newContent));
|
|
3228
3238
|
} else {
|
|
3229
3239
|
// a single node is added as a child to a dummy parent
|
|
3230
3240
|
const dummyParent = document.createElement("div");
|
|
@@ -3243,33 +3253,78 @@ var Idiomorph = (function () {
|
|
|
3243
3253
|
}
|
|
3244
3254
|
|
|
3245
3255
|
/**
|
|
3246
|
-
*
|
|
3256
|
+
* A fake duck-typed parent element to wrap a single node, without actually reparenting it.
|
|
3257
|
+
* This is useful because the node may have siblings that we don't want in the morph, and it may also be moved
|
|
3258
|
+
* or replaced with one or more elements during the morph. This class effectively allows us a window into
|
|
3259
|
+
* a slice of a node's children.
|
|
3247
3260
|
* "If it walks like a duck, and quacks like a duck, then it must be a duck!" -- James Whitcomb Riley (1849–1916)
|
|
3248
|
-
*
|
|
3249
|
-
* @param {Node} newContent
|
|
3250
|
-
* @returns {Element}
|
|
3251
3261
|
*/
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
}
|
|
3272
|
-
|
|
3262
|
+
class SlicedParentNode {
|
|
3263
|
+
/** @param {Node} node */
|
|
3264
|
+
constructor(node) {
|
|
3265
|
+
this.originalNode = node;
|
|
3266
|
+
this.realParentNode = /** @type {Element} */ (node.parentNode);
|
|
3267
|
+
this.previousSibling = node.previousSibling;
|
|
3268
|
+
this.nextSibling = node.nextSibling;
|
|
3269
|
+
}
|
|
3270
|
+
|
|
3271
|
+
/** @returns {Node[]} */
|
|
3272
|
+
get childNodes() {
|
|
3273
|
+
// return slice of realParent's current childNodes, based on previousSibling and nextSibling
|
|
3274
|
+
const nodes = [];
|
|
3275
|
+
let cursor = this.previousSibling
|
|
3276
|
+
? this.previousSibling.nextSibling
|
|
3277
|
+
: this.realParentNode.firstChild;
|
|
3278
|
+
while (cursor && cursor != this.nextSibling) {
|
|
3279
|
+
nodes.push(cursor);
|
|
3280
|
+
cursor = cursor.nextSibling;
|
|
3281
|
+
}
|
|
3282
|
+
return nodes;
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
/**
|
|
3286
|
+
* @param {string} selector
|
|
3287
|
+
* @returns {Element[]}
|
|
3288
|
+
*/
|
|
3289
|
+
querySelectorAll(selector) {
|
|
3290
|
+
return this.childNodes.reduce((results, node) => {
|
|
3291
|
+
if (node instanceof Element) {
|
|
3292
|
+
if (node.matches(selector)) results.push(node);
|
|
3293
|
+
const nodeList = node.querySelectorAll(selector);
|
|
3294
|
+
for (let i = 0; i < nodeList.length; i++) {
|
|
3295
|
+
results.push(nodeList[i]);
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
return results;
|
|
3299
|
+
}, /** @type {Element[]} */ ([]));
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
/**
|
|
3303
|
+
* @param {Node} node
|
|
3304
|
+
* @param {Node} referenceNode
|
|
3305
|
+
* @returns {Node}
|
|
3306
|
+
*/
|
|
3307
|
+
insertBefore(node, referenceNode) {
|
|
3308
|
+
return this.realParentNode.insertBefore(node, referenceNode);
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
/**
|
|
3312
|
+
* @param {Node} node
|
|
3313
|
+
* @param {Node} referenceNode
|
|
3314
|
+
* @returns {Node}
|
|
3315
|
+
*/
|
|
3316
|
+
moveBefore(node, referenceNode) {
|
|
3317
|
+
// @ts-ignore - use new moveBefore feature
|
|
3318
|
+
return this.realParentNode.moveBefore(node, referenceNode);
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3321
|
+
/**
|
|
3322
|
+
* for later use with populateIdMapWithTree to halt upwards iteration
|
|
3323
|
+
* @returns {Node}
|
|
3324
|
+
*/
|
|
3325
|
+
get __idiomorphRoot() {
|
|
3326
|
+
return this.originalNode;
|
|
3327
|
+
}
|
|
3273
3328
|
}
|
|
3274
3329
|
|
|
3275
3330
|
/**
|
|
@@ -3364,16 +3419,18 @@ function morphChildren(currentElement, newElement, options = {}) {
|
|
|
3364
3419
|
|
|
3365
3420
|
function shouldRefreshFrameWithMorphing(currentFrame, newFrame) {
|
|
3366
3421
|
return currentFrame instanceof FrameElement &&
|
|
3367
|
-
|
|
3368
|
-
// elements don't get initialized until they're attached to the DOM, so
|
|
3369
|
-
// test its Element#nodeName instead
|
|
3370
|
-
newFrame instanceof Element && newFrame.nodeName === "TURBO-FRAME" &&
|
|
3371
|
-
currentFrame.shouldReloadWithMorph &&
|
|
3372
|
-
currentFrame.id === newFrame.id &&
|
|
3373
|
-
(!newFrame.getAttribute("src") || urlsAreEqual(currentFrame.src, newFrame.getAttribute("src"))) &&
|
|
3422
|
+
currentFrame.shouldReloadWithMorph && (!newFrame || areFramesCompatibleForRefreshing(currentFrame, newFrame)) &&
|
|
3374
3423
|
!currentFrame.closest("[data-turbo-permanent]")
|
|
3375
3424
|
}
|
|
3376
3425
|
|
|
3426
|
+
function areFramesCompatibleForRefreshing(currentFrame, newFrame) {
|
|
3427
|
+
// newFrame cannot yet be an instance of FrameElement because custom
|
|
3428
|
+
// elements don't get initialized until they're attached to the DOM, so
|
|
3429
|
+
// test its Element#nodeName instead
|
|
3430
|
+
return newFrame instanceof Element && newFrame.nodeName === "TURBO-FRAME" && currentFrame.id === newFrame.id &&
|
|
3431
|
+
(!newFrame.getAttribute("src") || urlsAreEqual(currentFrame.src, newFrame.getAttribute("src")))
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3377
3434
|
function closestFrameReloadableWithMorphing(node) {
|
|
3378
3435
|
return node.parentElement.closest("turbo-frame[src][refresh=morph]")
|
|
3379
3436
|
}
|
|
@@ -3725,6 +3782,10 @@ class PageSnapshot extends Snapshot {
|
|
|
3725
3782
|
clonedPasswordInput.value = "";
|
|
3726
3783
|
}
|
|
3727
3784
|
|
|
3785
|
+
for (const clonedNoscriptElement of clonedElement.querySelectorAll("noscript")) {
|
|
3786
|
+
clonedNoscriptElement.remove();
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3728
3789
|
return new PageSnapshot(this.documentElement, clonedElement, this.headSnapshot)
|
|
3729
3790
|
}
|
|
3730
3791
|
|
|
@@ -3732,6 +3793,10 @@ class PageSnapshot extends Snapshot {
|
|
|
3732
3793
|
return this.documentElement.getAttribute("lang")
|
|
3733
3794
|
}
|
|
3734
3795
|
|
|
3796
|
+
get dir() {
|
|
3797
|
+
return this.documentElement.getAttribute("dir")
|
|
3798
|
+
}
|
|
3799
|
+
|
|
3735
3800
|
get headElement() {
|
|
3736
3801
|
return this.headSnapshot.element
|
|
3737
3802
|
}
|
|
@@ -3762,12 +3827,12 @@ class PageSnapshot extends Snapshot {
|
|
|
3762
3827
|
return viewTransitionEnabled && !window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
3763
3828
|
}
|
|
3764
3829
|
|
|
3765
|
-
get
|
|
3766
|
-
return this.getSetting("refresh-method")
|
|
3830
|
+
get refreshMethod() {
|
|
3831
|
+
return this.getSetting("refresh-method")
|
|
3767
3832
|
}
|
|
3768
3833
|
|
|
3769
|
-
get
|
|
3770
|
-
return this.getSetting("refresh-scroll")
|
|
3834
|
+
get refreshScroll() {
|
|
3835
|
+
return this.getSetting("refresh-scroll")
|
|
3771
3836
|
}
|
|
3772
3837
|
|
|
3773
3838
|
// Private
|
|
@@ -3806,7 +3871,8 @@ const defaultOptions = {
|
|
|
3806
3871
|
willRender: true,
|
|
3807
3872
|
updateHistory: true,
|
|
3808
3873
|
shouldCacheSnapshot: true,
|
|
3809
|
-
acceptsStreamResponse: false
|
|
3874
|
+
acceptsStreamResponse: false,
|
|
3875
|
+
refresh: {}
|
|
3810
3876
|
};
|
|
3811
3877
|
|
|
3812
3878
|
const TimingMetric = {
|
|
@@ -3866,7 +3932,8 @@ class Visit {
|
|
|
3866
3932
|
updateHistory,
|
|
3867
3933
|
shouldCacheSnapshot,
|
|
3868
3934
|
acceptsStreamResponse,
|
|
3869
|
-
direction
|
|
3935
|
+
direction,
|
|
3936
|
+
refresh
|
|
3870
3937
|
} = {
|
|
3871
3938
|
...defaultOptions,
|
|
3872
3939
|
...options
|
|
@@ -3877,7 +3944,6 @@ class Visit {
|
|
|
3877
3944
|
this.snapshot = snapshot;
|
|
3878
3945
|
this.snapshotHTML = snapshotHTML;
|
|
3879
3946
|
this.response = response;
|
|
3880
|
-
this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
|
|
3881
3947
|
this.isPageRefresh = this.view.isPageRefresh(this);
|
|
3882
3948
|
this.visitCachedSnapshot = visitCachedSnapshot;
|
|
3883
3949
|
this.willRender = willRender;
|
|
@@ -3886,6 +3952,7 @@ class Visit {
|
|
|
3886
3952
|
this.shouldCacheSnapshot = shouldCacheSnapshot;
|
|
3887
3953
|
this.acceptsStreamResponse = acceptsStreamResponse;
|
|
3888
3954
|
this.direction = direction || Direction[action];
|
|
3955
|
+
this.refresh = refresh;
|
|
3889
3956
|
}
|
|
3890
3957
|
|
|
3891
3958
|
get adapter() {
|
|
@@ -3904,10 +3971,6 @@ class Visit {
|
|
|
3904
3971
|
return this.history.getRestorationDataForIdentifier(this.restorationIdentifier)
|
|
3905
3972
|
}
|
|
3906
3973
|
|
|
3907
|
-
get silent() {
|
|
3908
|
-
return this.isSamePage
|
|
3909
|
-
}
|
|
3910
|
-
|
|
3911
3974
|
start() {
|
|
3912
3975
|
if (this.state == VisitState.initialized) {
|
|
3913
3976
|
this.recordTimingMetric(TimingMetric.visitStart);
|
|
@@ -4044,7 +4107,7 @@ class Visit {
|
|
|
4044
4107
|
const isPreview = this.shouldIssueRequest();
|
|
4045
4108
|
this.render(async () => {
|
|
4046
4109
|
this.cacheSnapshot();
|
|
4047
|
-
if (this.
|
|
4110
|
+
if (this.isPageRefresh) {
|
|
4048
4111
|
this.adapter.visitRendered(this);
|
|
4049
4112
|
} else {
|
|
4050
4113
|
if (this.view.renderPromise) await this.view.renderPromise;
|
|
@@ -4072,17 +4135,6 @@ class Visit {
|
|
|
4072
4135
|
}
|
|
4073
4136
|
}
|
|
4074
4137
|
|
|
4075
|
-
goToSamePageAnchor() {
|
|
4076
|
-
if (this.isSamePage) {
|
|
4077
|
-
this.render(async () => {
|
|
4078
|
-
this.cacheSnapshot();
|
|
4079
|
-
this.performScroll();
|
|
4080
|
-
this.changeHistory();
|
|
4081
|
-
this.adapter.visitRendered(this);
|
|
4082
|
-
});
|
|
4083
|
-
}
|
|
4084
|
-
}
|
|
4085
|
-
|
|
4086
4138
|
// Fetch request delegate
|
|
4087
4139
|
|
|
4088
4140
|
prepareRequest(request) {
|
|
@@ -4144,9 +4196,6 @@ class Visit {
|
|
|
4144
4196
|
} else {
|
|
4145
4197
|
this.scrollToAnchor() || this.view.scrollToTop();
|
|
4146
4198
|
}
|
|
4147
|
-
if (this.isSamePage) {
|
|
4148
|
-
this.delegate.visitScrolledToSamePageLocation(this.view.lastRenderedLocation, this.location);
|
|
4149
|
-
}
|
|
4150
4199
|
|
|
4151
4200
|
this.scrolled = true;
|
|
4152
4201
|
}
|
|
@@ -4185,9 +4234,7 @@ class Visit {
|
|
|
4185
4234
|
}
|
|
4186
4235
|
|
|
4187
4236
|
shouldIssueRequest() {
|
|
4188
|
-
if (this.
|
|
4189
|
-
return false
|
|
4190
|
-
} else if (this.action == "restore") {
|
|
4237
|
+
if (this.action == "restore") {
|
|
4191
4238
|
return !this.hasCachedSnapshot()
|
|
4192
4239
|
} else {
|
|
4193
4240
|
return this.willRender
|
|
@@ -4251,7 +4298,6 @@ class BrowserAdapter {
|
|
|
4251
4298
|
|
|
4252
4299
|
visit.loadCachedSnapshot();
|
|
4253
4300
|
visit.issueRequest();
|
|
4254
|
-
visit.goToSamePageAnchor();
|
|
4255
4301
|
}
|
|
4256
4302
|
|
|
4257
4303
|
visitRequestStarted(visit) {
|
|
@@ -4368,7 +4414,6 @@ class BrowserAdapter {
|
|
|
4368
4414
|
|
|
4369
4415
|
class CacheObserver {
|
|
4370
4416
|
selector = "[data-turbo-temporary]"
|
|
4371
|
-
deprecatedSelector = "[data-turbo-cache=false]"
|
|
4372
4417
|
|
|
4373
4418
|
started = false
|
|
4374
4419
|
|
|
@@ -4393,19 +4438,7 @@ class CacheObserver {
|
|
|
4393
4438
|
}
|
|
4394
4439
|
|
|
4395
4440
|
get temporaryElements() {
|
|
4396
|
-
return [...document.querySelectorAll(this.selector)
|
|
4397
|
-
}
|
|
4398
|
-
|
|
4399
|
-
get temporaryElementsWithDeprecation() {
|
|
4400
|
-
const elements = document.querySelectorAll(this.deprecatedSelector);
|
|
4401
|
-
|
|
4402
|
-
if (elements.length) {
|
|
4403
|
-
console.warn(
|
|
4404
|
-
`The ${this.deprecatedSelector} selector is deprecated and will be removed in a future version. Use ${this.selector} instead.`
|
|
4405
|
-
);
|
|
4406
|
-
}
|
|
4407
|
-
|
|
4408
|
-
return [...elements]
|
|
4441
|
+
return [...document.querySelectorAll(this.selector)]
|
|
4409
4442
|
}
|
|
4410
4443
|
}
|
|
4411
4444
|
|
|
@@ -4495,7 +4528,6 @@ class History {
|
|
|
4495
4528
|
restorationIdentifier = uuid()
|
|
4496
4529
|
restorationData = {}
|
|
4497
4530
|
started = false
|
|
4498
|
-
pageLoaded = false
|
|
4499
4531
|
currentIndex = 0
|
|
4500
4532
|
|
|
4501
4533
|
constructor(delegate) {
|
|
@@ -4505,7 +4537,6 @@ class History {
|
|
|
4505
4537
|
start() {
|
|
4506
4538
|
if (!this.started) {
|
|
4507
4539
|
addEventListener("popstate", this.onPopState, false);
|
|
4508
|
-
addEventListener("load", this.onPageLoad, false);
|
|
4509
4540
|
this.currentIndex = history.state?.turbo?.restorationIndex || 0;
|
|
4510
4541
|
this.started = true;
|
|
4511
4542
|
this.replace(new URL(window.location.href));
|
|
@@ -4515,7 +4546,6 @@ class History {
|
|
|
4515
4546
|
stop() {
|
|
4516
4547
|
if (this.started) {
|
|
4517
4548
|
removeEventListener("popstate", this.onPopState, false);
|
|
4518
|
-
removeEventListener("load", this.onPageLoad, false);
|
|
4519
4549
|
this.started = false;
|
|
4520
4550
|
}
|
|
4521
4551
|
}
|
|
@@ -4571,34 +4601,20 @@ class History {
|
|
|
4571
4601
|
// Event handlers
|
|
4572
4602
|
|
|
4573
4603
|
onPopState = (event) => {
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4604
|
+
const { turbo } = event.state || {};
|
|
4605
|
+
this.location = new URL(window.location.href);
|
|
4606
|
+
|
|
4607
|
+
if (turbo) {
|
|
4608
|
+
const { restorationIdentifier, restorationIndex } = turbo;
|
|
4609
|
+
this.restorationIdentifier = restorationIdentifier;
|
|
4610
|
+
const direction = restorationIndex > this.currentIndex ? "forward" : "back";
|
|
4611
|
+
this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(this.location, restorationIdentifier, direction);
|
|
4612
|
+
this.currentIndex = restorationIndex;
|
|
4613
|
+
} else {
|
|
4614
|
+
this.currentIndex++;
|
|
4615
|
+
this.delegate.historyPoppedWithEmptyState(this.location);
|
|
4584
4616
|
}
|
|
4585
4617
|
}
|
|
4586
|
-
|
|
4587
|
-
onPageLoad = async (_event) => {
|
|
4588
|
-
await nextMicrotask();
|
|
4589
|
-
this.pageLoaded = true;
|
|
4590
|
-
}
|
|
4591
|
-
|
|
4592
|
-
// Private
|
|
4593
|
-
|
|
4594
|
-
shouldHandlePopState() {
|
|
4595
|
-
// Safari dispatches a popstate event after window's load event, ignore it
|
|
4596
|
-
return this.pageIsLoaded()
|
|
4597
|
-
}
|
|
4598
|
-
|
|
4599
|
-
pageIsLoaded() {
|
|
4600
|
-
return this.pageLoaded || document.readyState == "complete"
|
|
4601
|
-
}
|
|
4602
4618
|
}
|
|
4603
4619
|
|
|
4604
4620
|
class LinkPrefetchObserver {
|
|
@@ -4673,7 +4689,7 @@ class LinkPrefetchObserver {
|
|
|
4673
4689
|
|
|
4674
4690
|
fetchRequest.fetchOptions.priority = "low";
|
|
4675
4691
|
|
|
4676
|
-
prefetchCache.
|
|
4692
|
+
prefetchCache.putLater(location, fetchRequest, this.#cacheTtl);
|
|
4677
4693
|
}
|
|
4678
4694
|
}
|
|
4679
4695
|
}
|
|
@@ -4689,7 +4705,7 @@ class LinkPrefetchObserver {
|
|
|
4689
4705
|
|
|
4690
4706
|
#tryToUsePrefetchedRequest = (event) => {
|
|
4691
4707
|
if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "GET") {
|
|
4692
|
-
const cached = prefetchCache.get(event.detail.url
|
|
4708
|
+
const cached = prefetchCache.get(event.detail.url);
|
|
4693
4709
|
|
|
4694
4710
|
if (cached) {
|
|
4695
4711
|
// User clicked link, use cache response
|
|
@@ -4879,7 +4895,7 @@ class Navigator {
|
|
|
4879
4895
|
} else {
|
|
4880
4896
|
await this.view.renderPage(snapshot, false, true, this.currentVisit);
|
|
4881
4897
|
}
|
|
4882
|
-
if(
|
|
4898
|
+
if (snapshot.refreshScroll !== "preserve") {
|
|
4883
4899
|
this.view.scrollToTop();
|
|
4884
4900
|
}
|
|
4885
4901
|
this.view.clearSnapshotCache();
|
|
@@ -4919,20 +4935,10 @@ class Navigator {
|
|
|
4919
4935
|
delete this.currentVisit;
|
|
4920
4936
|
}
|
|
4921
4937
|
|
|
4938
|
+
// Same-page links are no longer handled with a Visit.
|
|
4939
|
+
// This method is still needed for Turbo Native adapters.
|
|
4922
4940
|
locationWithActionIsSamePage(location, action) {
|
|
4923
|
-
|
|
4924
|
-
const currentAnchor = getAnchor(this.view.lastRenderedLocation);
|
|
4925
|
-
const isRestorationToTop = action === "restore" && typeof anchor === "undefined";
|
|
4926
|
-
|
|
4927
|
-
return (
|
|
4928
|
-
action !== "replace" &&
|
|
4929
|
-
getRequestURL(location) === getRequestURL(this.view.lastRenderedLocation) &&
|
|
4930
|
-
(isRestorationToTop || (anchor != null && anchor !== currentAnchor))
|
|
4931
|
-
)
|
|
4932
|
-
}
|
|
4933
|
-
|
|
4934
|
-
visitScrolledToSamePageLocation(oldURL, newURL) {
|
|
4935
|
-
this.delegate.visitScrolledToSamePageLocation(oldURL, newURL);
|
|
4941
|
+
return false
|
|
4936
4942
|
}
|
|
4937
4943
|
|
|
4938
4944
|
// Visits
|
|
@@ -5325,13 +5331,18 @@ class PageRenderer extends Renderer {
|
|
|
5325
5331
|
|
|
5326
5332
|
#setLanguage() {
|
|
5327
5333
|
const { documentElement } = this.currentSnapshot;
|
|
5328
|
-
const { lang } = this.newSnapshot;
|
|
5334
|
+
const { dir, lang } = this.newSnapshot;
|
|
5329
5335
|
|
|
5330
5336
|
if (lang) {
|
|
5331
5337
|
documentElement.setAttribute("lang", lang);
|
|
5332
5338
|
} else {
|
|
5333
5339
|
documentElement.removeAttribute("lang");
|
|
5334
5340
|
}
|
|
5341
|
+
if (dir) {
|
|
5342
|
+
documentElement.setAttribute("dir", dir);
|
|
5343
|
+
} else {
|
|
5344
|
+
documentElement.removeAttribute("dir");
|
|
5345
|
+
}
|
|
5335
5346
|
}
|
|
5336
5347
|
|
|
5337
5348
|
async mergeHead() {
|
|
@@ -5433,9 +5444,16 @@ class PageRenderer extends Renderer {
|
|
|
5433
5444
|
|
|
5434
5445
|
activateNewBody() {
|
|
5435
5446
|
document.adoptNode(this.newElement);
|
|
5447
|
+
this.removeNoscriptElements();
|
|
5436
5448
|
this.activateNewBodyScriptElements();
|
|
5437
5449
|
}
|
|
5438
5450
|
|
|
5451
|
+
removeNoscriptElements() {
|
|
5452
|
+
for (const noscriptElement of this.newElement.querySelectorAll("noscript")) {
|
|
5453
|
+
noscriptElement.remove();
|
|
5454
|
+
}
|
|
5455
|
+
}
|
|
5456
|
+
|
|
5439
5457
|
activateNewBodyScriptElements() {
|
|
5440
5458
|
for (const inertScriptElement of this.newBodyScriptElements) {
|
|
5441
5459
|
const activatedScriptElement = activateScriptElement(inertScriptElement);
|
|
@@ -5511,58 +5529,13 @@ class MorphingPageRenderer extends PageRenderer {
|
|
|
5511
5529
|
}
|
|
5512
5530
|
}
|
|
5513
5531
|
|
|
5514
|
-
class SnapshotCache {
|
|
5515
|
-
keys = []
|
|
5516
|
-
snapshots = {}
|
|
5517
|
-
|
|
5532
|
+
class SnapshotCache extends LRUCache {
|
|
5518
5533
|
constructor(size) {
|
|
5519
|
-
|
|
5534
|
+
super(size, toCacheKey);
|
|
5520
5535
|
}
|
|
5521
5536
|
|
|
5522
|
-
|
|
5523
|
-
return
|
|
5524
|
-
}
|
|
5525
|
-
|
|
5526
|
-
get(location) {
|
|
5527
|
-
if (this.has(location)) {
|
|
5528
|
-
const snapshot = this.read(location);
|
|
5529
|
-
this.touch(location);
|
|
5530
|
-
return snapshot
|
|
5531
|
-
}
|
|
5532
|
-
}
|
|
5533
|
-
|
|
5534
|
-
put(location, snapshot) {
|
|
5535
|
-
this.write(location, snapshot);
|
|
5536
|
-
this.touch(location);
|
|
5537
|
-
return snapshot
|
|
5538
|
-
}
|
|
5539
|
-
|
|
5540
|
-
clear() {
|
|
5541
|
-
this.snapshots = {};
|
|
5542
|
-
}
|
|
5543
|
-
|
|
5544
|
-
// Private
|
|
5545
|
-
|
|
5546
|
-
read(location) {
|
|
5547
|
-
return this.snapshots[toCacheKey(location)]
|
|
5548
|
-
}
|
|
5549
|
-
|
|
5550
|
-
write(location, snapshot) {
|
|
5551
|
-
this.snapshots[toCacheKey(location)] = snapshot;
|
|
5552
|
-
}
|
|
5553
|
-
|
|
5554
|
-
touch(location) {
|
|
5555
|
-
const key = toCacheKey(location);
|
|
5556
|
-
const index = this.keys.indexOf(key);
|
|
5557
|
-
if (index > -1) this.keys.splice(index, 1);
|
|
5558
|
-
this.keys.unshift(key);
|
|
5559
|
-
this.trim();
|
|
5560
|
-
}
|
|
5561
|
-
|
|
5562
|
-
trim() {
|
|
5563
|
-
for (const key of this.keys.splice(this.size)) {
|
|
5564
|
-
delete this.snapshots[key];
|
|
5565
|
-
}
|
|
5537
|
+
get snapshots() {
|
|
5538
|
+
return this.entries
|
|
5566
5539
|
}
|
|
5567
5540
|
}
|
|
5568
5541
|
|
|
@@ -5576,7 +5549,7 @@ class PageView extends View {
|
|
|
5576
5549
|
}
|
|
5577
5550
|
|
|
5578
5551
|
renderPage(snapshot, isPreview = false, willRender = true, visit) {
|
|
5579
|
-
const shouldMorphPage = this.isPageRefresh(visit) && this.snapshot.
|
|
5552
|
+
const shouldMorphPage = this.isPageRefresh(visit) && (visit?.refresh?.method || this.snapshot.refreshMethod) === "morph";
|
|
5580
5553
|
const rendererClass = shouldMorphPage ? MorphingPageRenderer : PageRenderer;
|
|
5581
5554
|
|
|
5582
5555
|
const renderer = new rendererClass(this.snapshot, snapshot, isPreview, willRender);
|
|
@@ -5620,7 +5593,7 @@ class PageView extends View {
|
|
|
5620
5593
|
}
|
|
5621
5594
|
|
|
5622
5595
|
shouldPreserveScrollPosition(visit) {
|
|
5623
|
-
return this.isPageRefresh(visit) && this.snapshot.
|
|
5596
|
+
return this.isPageRefresh(visit) && (visit?.refresh?.scroll || this.snapshot.refreshScroll) === "preserve"
|
|
5624
5597
|
}
|
|
5625
5598
|
|
|
5626
5599
|
get snapshot() {
|
|
@@ -5810,11 +5783,14 @@ class Session {
|
|
|
5810
5783
|
}
|
|
5811
5784
|
}
|
|
5812
5785
|
|
|
5813
|
-
refresh(url,
|
|
5786
|
+
refresh(url, options = {}) {
|
|
5787
|
+
options = typeof options === "string" ? { requestId: options } : options;
|
|
5788
|
+
|
|
5789
|
+
const { method, requestId, scroll } = options;
|
|
5814
5790
|
const isRecentRequest = requestId && this.recentRequests.has(requestId);
|
|
5815
5791
|
const isCurrentUrl = url === document.baseURI;
|
|
5816
5792
|
if (!isRecentRequest && !this.navigator.currentVisit && isCurrentUrl) {
|
|
5817
|
-
this.visit(url, { action: "replace", shouldCacheSnapshot: false });
|
|
5793
|
+
this.visit(url, { action: "replace", shouldCacheSnapshot: false, refresh: { method, scroll } });
|
|
5818
5794
|
}
|
|
5819
5795
|
}
|
|
5820
5796
|
|
|
@@ -5918,6 +5894,12 @@ class Session {
|
|
|
5918
5894
|
}
|
|
5919
5895
|
}
|
|
5920
5896
|
|
|
5897
|
+
historyPoppedWithEmptyState(location) {
|
|
5898
|
+
this.history.replace(location);
|
|
5899
|
+
this.view.lastRenderedLocation = location;
|
|
5900
|
+
this.view.cacheSnapshot();
|
|
5901
|
+
}
|
|
5902
|
+
|
|
5921
5903
|
// Scroll observer delegate
|
|
5922
5904
|
|
|
5923
5905
|
scrollPositionChanged(position) {
|
|
@@ -5962,7 +5944,7 @@ class Session {
|
|
|
5962
5944
|
// Navigator delegate
|
|
5963
5945
|
|
|
5964
5946
|
allowsVisitingLocationWithAction(location, action) {
|
|
5965
|
-
return this.
|
|
5947
|
+
return this.applicationAllowsVisitingLocation(location)
|
|
5966
5948
|
}
|
|
5967
5949
|
|
|
5968
5950
|
visitProposedToLocation(location, options) {
|
|
@@ -5978,9 +5960,7 @@ class Session {
|
|
|
5978
5960
|
this.view.markVisitDirection(visit.direction);
|
|
5979
5961
|
}
|
|
5980
5962
|
extendURLWithDeprecatedProperties(visit.location);
|
|
5981
|
-
|
|
5982
|
-
this.notifyApplicationAfterVisitingLocation(visit.location, visit.action);
|
|
5983
|
-
}
|
|
5963
|
+
this.notifyApplicationAfterVisitingLocation(visit.location, visit.action);
|
|
5984
5964
|
}
|
|
5985
5965
|
|
|
5986
5966
|
visitCompleted(visit) {
|
|
@@ -5989,14 +5969,6 @@ class Session {
|
|
|
5989
5969
|
this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
|
|
5990
5970
|
}
|
|
5991
5971
|
|
|
5992
|
-
locationWithActionIsSamePage(location, action) {
|
|
5993
|
-
return this.navigator.locationWithActionIsSamePage(location, action)
|
|
5994
|
-
}
|
|
5995
|
-
|
|
5996
|
-
visitScrolledToSamePageLocation(oldURL, newURL) {
|
|
5997
|
-
this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
|
|
5998
|
-
}
|
|
5999
|
-
|
|
6000
5972
|
// Form submit observer delegate
|
|
6001
5973
|
|
|
6002
5974
|
willSubmitForm(form, submitter) {
|
|
@@ -6036,9 +6008,7 @@ class Session {
|
|
|
6036
6008
|
// Page view delegate
|
|
6037
6009
|
|
|
6038
6010
|
viewWillCacheSnapshot() {
|
|
6039
|
-
|
|
6040
|
-
this.notifyApplicationBeforeCachingSnapshot();
|
|
6041
|
-
}
|
|
6011
|
+
this.notifyApplicationBeforeCachingSnapshot();
|
|
6042
6012
|
}
|
|
6043
6013
|
|
|
6044
6014
|
allowsImmediateRender({ element }, options) {
|
|
@@ -6130,15 +6100,6 @@ class Session {
|
|
|
6130
6100
|
})
|
|
6131
6101
|
}
|
|
6132
6102
|
|
|
6133
|
-
notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL) {
|
|
6134
|
-
dispatchEvent(
|
|
6135
|
-
new HashChangeEvent("hashchange", {
|
|
6136
|
-
oldURL: oldURL.toString(),
|
|
6137
|
-
newURL: newURL.toString()
|
|
6138
|
-
})
|
|
6139
|
-
);
|
|
6140
|
-
}
|
|
6141
|
-
|
|
6142
6103
|
notifyApplicationAfterFrameLoad(frame) {
|
|
6143
6104
|
return dispatch("turbo:frame-load", { target: frame })
|
|
6144
6105
|
}
|
|
@@ -6224,7 +6185,7 @@ const deprecatedLocationPropertyDescriptors = {
|
|
|
6224
6185
|
};
|
|
6225
6186
|
|
|
6226
6187
|
const session = new Session(recentRequests);
|
|
6227
|
-
const { cache, navigator
|
|
6188
|
+
const { cache, navigator } = session;
|
|
6228
6189
|
|
|
6229
6190
|
/**
|
|
6230
6191
|
* Starts the main session.
|
|
@@ -6290,19 +6251,6 @@ function renderStreamMessage(message) {
|
|
|
6290
6251
|
session.renderStreamMessage(message);
|
|
6291
6252
|
}
|
|
6292
6253
|
|
|
6293
|
-
/**
|
|
6294
|
-
* Removes all entries from the Turbo Drive page cache.
|
|
6295
|
-
* Call this when state has changed on the server that may affect cached pages.
|
|
6296
|
-
*
|
|
6297
|
-
* @deprecated since version 7.2.0 in favor of `Turbo.cache.clear()`
|
|
6298
|
-
*/
|
|
6299
|
-
function clearCache() {
|
|
6300
|
-
console.warn(
|
|
6301
|
-
"Please replace `Turbo.clearCache()` with `Turbo.cache.clear()`. The top-level function is deprecated and will be removed in a future version of Turbo.`"
|
|
6302
|
-
);
|
|
6303
|
-
session.clearCache();
|
|
6304
|
-
}
|
|
6305
|
-
|
|
6306
6254
|
/**
|
|
6307
6255
|
* Sets the delay after which the progress bar will appear during navigation.
|
|
6308
6256
|
*
|
|
@@ -6362,7 +6310,7 @@ function morphTurboFrameElements(currentFrame, newFrame) {
|
|
|
6362
6310
|
|
|
6363
6311
|
var Turbo = /*#__PURE__*/Object.freeze({
|
|
6364
6312
|
__proto__: null,
|
|
6365
|
-
navigator: navigator
|
|
6313
|
+
navigator: navigator,
|
|
6366
6314
|
session: session,
|
|
6367
6315
|
cache: cache,
|
|
6368
6316
|
PageRenderer: PageRenderer,
|
|
@@ -6376,7 +6324,6 @@ var Turbo = /*#__PURE__*/Object.freeze({
|
|
|
6376
6324
|
connectStreamSource: connectStreamSource,
|
|
6377
6325
|
disconnectStreamSource: disconnectStreamSource,
|
|
6378
6326
|
renderStreamMessage: renderStreamMessage,
|
|
6379
|
-
clearCache: clearCache,
|
|
6380
6327
|
setProgressBarDelay: setProgressBarDelay,
|
|
6381
6328
|
setConfirmMethod: setConfirmMethod,
|
|
6382
6329
|
setFormMode: setFormMode,
|
|
@@ -6431,11 +6378,17 @@ class FrameController {
|
|
|
6431
6378
|
this.formLinkClickObserver.stop();
|
|
6432
6379
|
this.linkInterceptor.stop();
|
|
6433
6380
|
this.formSubmitObserver.stop();
|
|
6381
|
+
|
|
6382
|
+
if (!this.element.hasAttribute("recurse")) {
|
|
6383
|
+
this.#currentFetchRequest?.cancel();
|
|
6384
|
+
}
|
|
6434
6385
|
}
|
|
6435
6386
|
}
|
|
6436
6387
|
|
|
6437
6388
|
disabledChanged() {
|
|
6438
|
-
if (this.
|
|
6389
|
+
if (this.disabled) {
|
|
6390
|
+
this.#currentFetchRequest?.cancel();
|
|
6391
|
+
} else if (this.loadingStyle == FrameLoadingStyle.eager) {
|
|
6439
6392
|
this.#loadSourceURL();
|
|
6440
6393
|
}
|
|
6441
6394
|
}
|
|
@@ -6443,6 +6396,10 @@ class FrameController {
|
|
|
6443
6396
|
sourceURLChanged() {
|
|
6444
6397
|
if (this.#isIgnoringChangesTo("src")) return
|
|
6445
6398
|
|
|
6399
|
+
if (!this.sourceURL) {
|
|
6400
|
+
this.#currentFetchRequest?.cancel();
|
|
6401
|
+
}
|
|
6402
|
+
|
|
6446
6403
|
if (this.element.isConnected) {
|
|
6447
6404
|
this.complete = false;
|
|
6448
6405
|
}
|
|
@@ -6544,15 +6501,18 @@ class FrameController {
|
|
|
6544
6501
|
}
|
|
6545
6502
|
|
|
6546
6503
|
this.formSubmission = new FormSubmission(this, element, submitter);
|
|
6504
|
+
|
|
6547
6505
|
const { fetchRequest } = this.formSubmission;
|
|
6548
|
-
this
|
|
6506
|
+
const frame = this.#findFrameElement(element, submitter);
|
|
6507
|
+
|
|
6508
|
+
this.prepareRequest(fetchRequest, frame);
|
|
6549
6509
|
this.formSubmission.start();
|
|
6550
6510
|
}
|
|
6551
6511
|
|
|
6552
6512
|
// Fetch request delegate
|
|
6553
6513
|
|
|
6554
|
-
prepareRequest(request) {
|
|
6555
|
-
request.headers["Turbo-Frame"] =
|
|
6514
|
+
prepareRequest(request, frame = this) {
|
|
6515
|
+
request.headers["Turbo-Frame"] = frame.id;
|
|
6556
6516
|
|
|
6557
6517
|
if (this.currentNavigationElement?.hasAttribute("data-turbo-stream")) {
|
|
6558
6518
|
request.acceptResponseType(StreamMessage.contentType);
|
|
@@ -6794,7 +6754,9 @@ class FrameController {
|
|
|
6794
6754
|
|
|
6795
6755
|
#findFrameElement(element, submitter) {
|
|
6796
6756
|
const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
|
|
6797
|
-
|
|
6757
|
+
const target = this.#getFrameElementById(id);
|
|
6758
|
+
|
|
6759
|
+
return target instanceof FrameElement ? target : this.element
|
|
6798
6760
|
}
|
|
6799
6761
|
|
|
6800
6762
|
async extractForeignFrameElement(container) {
|
|
@@ -6838,9 +6800,11 @@ class FrameController {
|
|
|
6838
6800
|
}
|
|
6839
6801
|
|
|
6840
6802
|
if (id) {
|
|
6841
|
-
const frameElement = getFrameElementById(id);
|
|
6803
|
+
const frameElement = this.#getFrameElementById(id);
|
|
6842
6804
|
if (frameElement) {
|
|
6843
6805
|
return !frameElement.disabled
|
|
6806
|
+
} else if (id == "_parent") {
|
|
6807
|
+
return false
|
|
6844
6808
|
}
|
|
6845
6809
|
}
|
|
6846
6810
|
|
|
@@ -6861,8 +6825,12 @@ class FrameController {
|
|
|
6861
6825
|
return this.element.id
|
|
6862
6826
|
}
|
|
6863
6827
|
|
|
6828
|
+
get disabled() {
|
|
6829
|
+
return this.element.disabled
|
|
6830
|
+
}
|
|
6831
|
+
|
|
6864
6832
|
get enabled() {
|
|
6865
|
-
return !this.
|
|
6833
|
+
return !this.disabled
|
|
6866
6834
|
}
|
|
6867
6835
|
|
|
6868
6836
|
get sourceURL() {
|
|
@@ -6922,13 +6890,15 @@ class FrameController {
|
|
|
6922
6890
|
callback();
|
|
6923
6891
|
delete this.currentNavigationElement;
|
|
6924
6892
|
}
|
|
6925
|
-
}
|
|
6926
6893
|
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
|
|
6931
|
-
|
|
6894
|
+
#getFrameElementById(id) {
|
|
6895
|
+
if (id != null) {
|
|
6896
|
+
const element = id === "_parent" ?
|
|
6897
|
+
this.element.parentElement.closest("turbo-frame") :
|
|
6898
|
+
document.getElementById(id);
|
|
6899
|
+
if (element instanceof FrameElement) {
|
|
6900
|
+
return element
|
|
6901
|
+
}
|
|
6932
6902
|
}
|
|
6933
6903
|
}
|
|
6934
6904
|
}
|
|
@@ -6953,6 +6923,7 @@ function activateElement(element, currentURL) {
|
|
|
6953
6923
|
|
|
6954
6924
|
const StreamActions = {
|
|
6955
6925
|
after() {
|
|
6926
|
+
this.removeDuplicateTargetSiblings();
|
|
6956
6927
|
this.targetElements.forEach((e) => e.parentElement?.insertBefore(this.templateContent, e.nextSibling));
|
|
6957
6928
|
},
|
|
6958
6929
|
|
|
@@ -6962,6 +6933,7 @@ const StreamActions = {
|
|
|
6962
6933
|
},
|
|
6963
6934
|
|
|
6964
6935
|
before() {
|
|
6936
|
+
this.removeDuplicateTargetSiblings();
|
|
6965
6937
|
this.targetElements.forEach((e) => e.parentElement?.insertBefore(this.templateContent, e));
|
|
6966
6938
|
},
|
|
6967
6939
|
|
|
@@ -7000,7 +6972,11 @@ const StreamActions = {
|
|
|
7000
6972
|
},
|
|
7001
6973
|
|
|
7002
6974
|
refresh() {
|
|
7003
|
-
|
|
6975
|
+
const method = this.getAttribute("method");
|
|
6976
|
+
const requestId = this.requestId;
|
|
6977
|
+
const scroll = this.getAttribute("scroll");
|
|
6978
|
+
|
|
6979
|
+
session.refresh(this.baseURI, { method, requestId, scroll });
|
|
7004
6980
|
}
|
|
7005
6981
|
};
|
|
7006
6982
|
|
|
@@ -7078,6 +7054,23 @@ class StreamElement extends HTMLElement {
|
|
|
7078
7054
|
return existingChildren.filter((c) => newChildrenIds.includes(c.getAttribute("id")))
|
|
7079
7055
|
}
|
|
7080
7056
|
|
|
7057
|
+
/**
|
|
7058
|
+
* Removes duplicate siblings (by ID)
|
|
7059
|
+
*/
|
|
7060
|
+
removeDuplicateTargetSiblings() {
|
|
7061
|
+
this.duplicateSiblings.forEach((c) => c.remove());
|
|
7062
|
+
}
|
|
7063
|
+
|
|
7064
|
+
/**
|
|
7065
|
+
* Gets the list of duplicate siblings (i.e. those with the same ID)
|
|
7066
|
+
*/
|
|
7067
|
+
get duplicateSiblings() {
|
|
7068
|
+
const existingChildren = this.targetElements.flatMap((e) => [...e.parentElement.children]).filter((c) => !!c.id);
|
|
7069
|
+
const newChildrenIds = [...(this.templateContent?.children || [])].filter((c) => !!c.id).map((c) => c.id);
|
|
7070
|
+
|
|
7071
|
+
return existingChildren.filter((c) => newChildrenIds.includes(c.id))
|
|
7072
|
+
}
|
|
7073
|
+
|
|
7081
7074
|
/**
|
|
7082
7075
|
* Gets the action function to be performed.
|
|
7083
7076
|
*/
|
|
@@ -7229,11 +7222,11 @@ if (customElements.get("turbo-stream-source") === undefined) {
|
|
|
7229
7222
|
}
|
|
7230
7223
|
|
|
7231
7224
|
(() => {
|
|
7232
|
-
|
|
7233
|
-
if (!
|
|
7234
|
-
if (
|
|
7225
|
+
const scriptElement = document.currentScript;
|
|
7226
|
+
if (!scriptElement) return
|
|
7227
|
+
if (scriptElement.hasAttribute("data-turbo-suppress-warning")) return
|
|
7235
7228
|
|
|
7236
|
-
element =
|
|
7229
|
+
let element = scriptElement.parentElement;
|
|
7237
7230
|
while (element) {
|
|
7238
7231
|
if (element == document.body) {
|
|
7239
7232
|
return console.warn(
|
|
@@ -7247,7 +7240,7 @@ if (customElements.get("turbo-stream-source") === undefined) {
|
|
|
7247
7240
|
——
|
|
7248
7241
|
Suppress this warning by adding a "data-turbo-suppress-warning" attribute to: %s
|
|
7249
7242
|
`,
|
|
7250
|
-
|
|
7243
|
+
scriptElement.outerHTML
|
|
7251
7244
|
)
|
|
7252
7245
|
}
|
|
7253
7246
|
|
|
@@ -7258,4 +7251,4 @@ if (customElements.get("turbo-stream-source") === undefined) {
|
|
|
7258
7251
|
window.Turbo = { ...Turbo, StreamActions };
|
|
7259
7252
|
start();
|
|
7260
7253
|
|
|
7261
|
-
export { FetchEnctype, FetchMethod, FetchRequest, FetchResponse, FrameElement, FrameLoadingStyle, FrameRenderer, PageRenderer, PageSnapshot, StreamActions, StreamElement, StreamSourceElement, cache,
|
|
7254
|
+
export { FetchEnctype, FetchMethod, FetchRequest, FetchResponse, FrameElement, FrameLoadingStyle, FrameRenderer, PageRenderer, PageSnapshot, StreamActions, StreamElement, StreamSourceElement, cache, config, connectStreamSource, disconnectStreamSource, fetchWithTurboHeaders as fetch, fetchEnctypeFromString, fetchMethodFromString, isSafe, morphBodyElements, morphChildren, morphElements, morphTurboFrameElements, navigator, registerAdapter, renderStreamMessage, session, setConfirmMethod, setFormMode, setProgressBarDelay, start, visit };
|