@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.
@@ -1,104 +1,7 @@
1
1
  /*!
2
- Turbo 8.0.18
3
- Copyright © 2025 37signals LLC
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() * 15).toString(16)
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
- return link
565
- }
463
+ const linkTarget = link.getAttribute("target");
464
+ if (linkTarget && linkTarget !== "_self") return null
566
465
 
567
- function getLocationForLink(link) {
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 PREFETCH_DELAY = 100;
976
+ const identity = key => key;
1075
977
 
1076
- class PrefetchCache {
1077
- #prefetchTimeout = null
1078
- #prefetched = null
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
- get(url) {
1081
- if (this.#prefetched && this.#prefetched.url === url && this.#prefetched.expire > Date.now()) {
1082
- return this.#prefetched.request
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
- setLater(url, request, ttl) {
1087
- this.clear();
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.set(url, request, ttl);
1055
+ this.put(url, request, ttl);
1092
1056
  this.#prefetchTimeout = null;
1093
- }, PREFETCH_DELAY);
1057
+ }, this.prefetchDelay);
1094
1058
  }
1095
1059
 
1096
- set(url, request, ttl) {
1097
- this.#prefetched = { url, request, expire: new Date(new Date().getTime() + ttl) };
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
- this.#prefetched = null;
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
- // return just the morphed nodes
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 (activeElementId && activeElementId !== document.activeElement?.id) {
2266
- activeElement = ctx.target.querySelector(`#${activeElementId}`);
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 && ctx.persistentIds.has(newChild.id)) {
2345
- // move it and all its children here and morph
2346
- const movedChild = moveBeforeById(
2347
- oldParent,
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
- morphNode(movedChild, newChild, ctx);
2353
- insertionPoint = movedChild.nextSibling;
2354
- continue;
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
- if (cursor.contains(document.activeElement)) break;
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
- (!oldElt.id || oldElt.id === newElt.id)
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.querySelector(`#${id}`) ||
2579
- ctx.pantry.querySelector(`#${id}`)
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
- const id = element.id;
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
- if (root.id) {
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
- if (persistentIds.has(elt.id)) {
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(elt.id);
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 duck-typed parent node instead.
3227
- return createDuckTypedParent(newContent);
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
- * Creates a fake duck-typed parent element to wrap a single node, without actually reparenting it.
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
- function createDuckTypedParent(newContent) {
3253
- return /** @type {Element} */ (
3254
- /** @type {unknown} */ ({
3255
- childNodes: [newContent],
3256
- /** @ts-ignore - cover your eyes for a minute, tsc */
3257
- querySelectorAll: (s) => {
3258
- /** @ts-ignore */
3259
- const elements = newContent.querySelectorAll(s);
3260
- /** @ts-ignore */
3261
- return newContent.matches(s) ? [newContent, ...elements] : elements;
3262
- },
3263
- /** @ts-ignore */
3264
- insertBefore: (n, r) => newContent.parentNode.insertBefore(n, r),
3265
- /** @ts-ignore */
3266
- moveBefore: (n, r) => newContent.parentNode.moveBefore(n, r),
3267
- // for later use with populateIdMapWithTree to halt upwards iteration
3268
- get __idiomorphRoot() {
3269
- return newContent;
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
- // newFrame cannot yet be an instance of FrameElement because custom
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 shouldMorphPage() {
3766
- return this.getSetting("refresh-method") === "morph"
3830
+ get refreshMethod() {
3831
+ return this.getSetting("refresh-method")
3767
3832
  }
3768
3833
 
3769
- get shouldPreserveScrollPosition() {
3770
- return this.getSetting("refresh-scroll") === "preserve"
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.isSamePage || this.isPageRefresh) {
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.isSamePage) {
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), ...this.temporaryElementsWithDeprecation]
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
- if (this.shouldHandlePopState()) {
4575
- const { turbo } = event.state || {};
4576
- if (turbo) {
4577
- this.location = new URL(window.location.href);
4578
- const { restorationIdentifier, restorationIndex } = turbo;
4579
- this.restorationIdentifier = restorationIdentifier;
4580
- const direction = restorationIndex > this.currentIndex ? "forward" : "back";
4581
- this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(this.location, restorationIdentifier, direction);
4582
- this.currentIndex = restorationIndex;
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.setLater(location.toString(), fetchRequest, this.#cacheTtl);
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.toString());
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(!snapshot.shouldPreserveScrollPosition) {
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
- const anchor = getAnchor(location);
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
- this.size = size;
5534
+ super(size, toCacheKey);
5520
5535
  }
5521
5536
 
5522
- has(location) {
5523
- return toCacheKey(location) in this.snapshots
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.shouldMorphPage;
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.shouldPreserveScrollPosition
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, requestId) {
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.locationWithActionIsSamePage(location, action) || this.applicationAllowsVisitingLocation(location)
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
- if (!visit.silent) {
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
- if (!this.navigator.currentVisit?.silent) {
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: navigator$1 } = session;
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$1,
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.loadingStyle == FrameLoadingStyle.eager) {
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.prepareRequest(fetchRequest);
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"] = this.id;
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
- return getFrameElementById(id) ?? this.element
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.element.disabled
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
- function getFrameElementById(id) {
6928
- if (id != null) {
6929
- const element = document.getElementById(id);
6930
- if (element instanceof FrameElement) {
6931
- return element
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
- session.refresh(this.baseURI, this.requestId);
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
- let element = document.currentScript;
7233
- if (!element) return
7234
- if (element.hasAttribute("data-turbo-suppress-warning")) return
7225
+ const scriptElement = document.currentScript;
7226
+ if (!scriptElement) return
7227
+ if (scriptElement.hasAttribute("data-turbo-suppress-warning")) return
7235
7228
 
7236
- element = element.parentElement;
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
- element.outerHTML
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, clearCache, config, connectStreamSource, disconnectStreamSource, fetchWithTurboHeaders as fetch, fetchEnctypeFromString, fetchMethodFromString, isSafe, morphBodyElements, morphChildren, morphElements, morphTurboFrameElements, navigator$1 as navigator, registerAdapter, renderStreamMessage, session, setConfirmMethod, setFormMode, setProgressBarDelay, start, visit };
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 };