@hotwired/turbo 8.0.21 → 8.0.22

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/README.md CHANGED
@@ -15,4 +15,4 @@ Read more on [turbo.hotwired.dev](https://turbo.hotwired.dev).
15
15
 
16
16
  Please read [CONTRIBUTING.md](./CONTRIBUTING.md).
17
17
 
18
- © 2024 37signals LLC.
18
+ © 2026 37signals LLC.
@@ -2119,7 +2119,6 @@ var Idiomorph = (function () {
2119
2119
  * @property {ConfigInternal['callbacks']} callbacks
2120
2120
  * @property {ConfigInternal['head']} head
2121
2121
  * @property {HTMLDivElement} pantry
2122
- * @property {Element[]} activeElementAndParents
2123
2122
  */
2124
2123
 
2125
2124
  //=============================================================================
@@ -2195,6 +2194,14 @@ var Idiomorph = (function () {
2195
2194
  */
2196
2195
  function morphOuterHTML(ctx, oldNode, newNode) {
2197
2196
  const oldParent = normalizeParent(oldNode);
2197
+
2198
+ // basis for calulating which nodes were morphed
2199
+ // since there may be unmorphed sibling nodes
2200
+ let childNodes = Array.from(oldParent.childNodes);
2201
+ const index = childNodes.indexOf(oldNode);
2202
+ // how many elements are to the right of the oldNode
2203
+ const rightMargin = childNodes.length - (index + 1);
2204
+
2198
2205
  morphChildren(
2199
2206
  ctx,
2200
2207
  oldParent,
@@ -2203,8 +2210,10 @@ var Idiomorph = (function () {
2203
2210
  oldNode, // start point for iteration
2204
2211
  oldNode.nextSibling, // end point for iteration
2205
2212
  );
2206
- // this is safe even with siblings, because normalizeParent returns a SlicedParentNode if needed.
2207
- return Array.from(oldParent.childNodes);
2213
+
2214
+ // return just the morphed nodes
2215
+ childNodes = Array.from(oldParent.childNodes);
2216
+ return childNodes.slice(index, childNodes.length - rightMargin);
2208
2217
  }
2209
2218
 
2210
2219
  /**
@@ -2233,11 +2242,8 @@ var Idiomorph = (function () {
2233
2242
 
2234
2243
  const results = fn();
2235
2244
 
2236
- if (
2237
- activeElementId &&
2238
- activeElementId !== document.activeElement?.getAttribute("id")
2239
- ) {
2240
- activeElement = ctx.target.querySelector(`[id="${activeElementId}"]`);
2245
+ if (activeElementId && activeElementId !== document.activeElement?.id) {
2246
+ activeElement = ctx.target.querySelector(`#${activeElementId}`);
2241
2247
  activeElement?.focus();
2242
2248
  }
2243
2249
  if (activeElement && !activeElement.selectionEnd && selectionEnd) {
@@ -2315,23 +2321,17 @@ var Idiomorph = (function () {
2315
2321
  }
2316
2322
 
2317
2323
  // if the matching node is elsewhere in the original content
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")
2324
+ if (newChild instanceof Element && ctx.persistentIds.has(newChild.id)) {
2325
+ // move it and all its children here and morph
2326
+ const movedChild = moveBeforeById(
2327
+ oldParent,
2328
+ newChild.id,
2329
+ insertionPoint,
2330
+ ctx,
2322
2331
  );
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
- }
2332
+ morphNode(movedChild, newChild, ctx);
2333
+ insertionPoint = movedChild.nextSibling;
2334
+ continue;
2335
2335
  }
2336
2336
 
2337
2337
  // last resort: insert the new node from scratch
@@ -2441,8 +2441,7 @@ var Idiomorph = (function () {
2441
2441
 
2442
2442
  // if the current node contains active element, stop looking for better future matches,
2443
2443
  // because if one is found, this node will be moved to the pantry, reparenting it and thus losing focus
2444
- // @ts-ignore pretend cursor is Element rather than Node, we're just testing for array inclusion
2445
- if (ctx.activeElementAndParents.includes(cursor)) break;
2444
+ if (cursor.contains(document.activeElement)) break;
2446
2445
 
2447
2446
  cursor = cursor.nextSibling;
2448
2447
  }
@@ -2492,9 +2491,7 @@ var Idiomorph = (function () {
2492
2491
  // If oldElt has an `id` with possible state and it doesn't match newElt.id then avoid morphing.
2493
2492
  // We'll still match an anonymous node with an IDed newElt, though, because if it got this far,
2494
2493
  // its not persistent, and new nodes can't have any hidden state.
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"))
2494
+ (!oldElt.id || oldElt.id === newElt.id)
2498
2495
  );
2499
2496
  }
2500
2497
 
@@ -2558,11 +2555,8 @@ var Idiomorph = (function () {
2558
2555
  const target =
2559
2556
  /** @type {Element} - will always be found */
2560
2557
  (
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}"]`)
2558
+ ctx.target.querySelector(`#${id}`) ||
2559
+ ctx.pantry.querySelector(`#${id}`)
2566
2560
  );
2567
2561
  removeElementFromAncestorsIdMaps(target, ctx);
2568
2562
  moveBefore(parentNode, target, after);
@@ -2578,8 +2572,7 @@ var Idiomorph = (function () {
2578
2572
  * @param {MorphContext} ctx
2579
2573
  */
2580
2574
  function removeElementFromAncestorsIdMaps(element, ctx) {
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"));
2575
+ const id = element.id;
2583
2576
  /** @ts-ignore - safe to loop in this way **/
2584
2577
  while ((element = element.parentNode)) {
2585
2578
  let idSet = ctx.idMap.get(element);
@@ -3017,7 +3010,6 @@ var Idiomorph = (function () {
3017
3010
  idMap: idMap,
3018
3011
  persistentIds: persistentIds,
3019
3012
  pantry: createPantry(),
3020
- activeElementAndParents: createActiveElementAndParents(oldNode),
3021
3013
  callbacks: mergedConfig.callbacks,
3022
3014
  head: mergedConfig.head,
3023
3015
  };
@@ -3058,24 +3050,6 @@ var Idiomorph = (function () {
3058
3050
  return pantry;
3059
3051
  }
3060
3052
 
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
-
3079
3053
  /**
3080
3054
  * Returns all elements with an ID contained within the root element and its descendants
3081
3055
  *
@@ -3084,8 +3058,7 @@ var Idiomorph = (function () {
3084
3058
  */
3085
3059
  function findIdElements(root) {
3086
3060
  let elements = Array.from(root.querySelectorAll("[id]"));
3087
- // root could be a document fragment which doesn't have `getAttribute`
3088
- if (root.getAttribute?.("id")) {
3061
+ if (root.id) {
3089
3062
  elements.push(root);
3090
3063
  }
3091
3064
  return elements;
@@ -3104,9 +3077,7 @@ var Idiomorph = (function () {
3104
3077
  */
3105
3078
  function populateIdMapWithTree(idMap, persistentIds, root, elements) {
3106
3079
  for (const elt of elements) {
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)) {
3080
+ if (persistentIds.has(elt.id)) {
3110
3081
  /** @type {Element|null} */
3111
3082
  let current = elt;
3112
3083
  // walk up the parent hierarchy of that element, adding the id
@@ -3118,7 +3089,7 @@ var Idiomorph = (function () {
3118
3089
  idSet = new Set();
3119
3090
  idMap.set(current, idSet);
3120
3091
  }
3121
- idSet.add(id);
3092
+ idSet.add(elt.id);
3122
3093
 
3123
3094
  if (current === root) break;
3124
3095
  current = current.parentElement;
@@ -3232,9 +3203,8 @@ var Idiomorph = (function () {
3232
3203
  if (newContent.parentNode) {
3233
3204
  // we can't use the parent directly because newContent may have siblings
3234
3205
  // that we don't want in the morph, and reparenting might be expensive (TODO is it?),
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));
3206
+ // so we create a duck-typed parent node instead.
3207
+ return createDuckTypedParent(newContent);
3238
3208
  } else {
3239
3209
  // a single node is added as a child to a dummy parent
3240
3210
  const dummyParent = document.createElement("div");
@@ -3253,78 +3223,33 @@ var Idiomorph = (function () {
3253
3223
  }
3254
3224
 
3255
3225
  /**
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.
3226
+ * Creates a fake duck-typed parent element to wrap a single node, without actually reparenting it.
3260
3227
  * "If it walks like a duck, and quacks like a duck, then it must be a duck!" -- James Whitcomb Riley (1849–1916)
3228
+ *
3229
+ * @param {Node} newContent
3230
+ * @returns {Element}
3261
3231
  */
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
- }
3232
+ function createDuckTypedParent(newContent) {
3233
+ return /** @type {Element} */ (
3234
+ /** @type {unknown} */ ({
3235
+ childNodes: [newContent],
3236
+ /** @ts-ignore - cover your eyes for a minute, tsc */
3237
+ querySelectorAll: (s) => {
3238
+ /** @ts-ignore */
3239
+ const elements = newContent.querySelectorAll(s);
3240
+ /** @ts-ignore */
3241
+ return newContent.matches(s) ? [newContent, ...elements] : elements;
3242
+ },
3243
+ /** @ts-ignore */
3244
+ insertBefore: (n, r) => newContent.parentNode.insertBefore(n, r),
3245
+ /** @ts-ignore */
3246
+ moveBefore: (n, r) => newContent.parentNode.moveBefore(n, r),
3247
+ // for later use with populateIdMapWithTree to halt upwards iteration
3248
+ get __idiomorphRoot() {
3249
+ return newContent;
3250
+ },
3251
+ })
3252
+ );
3328
3253
  }
3329
3254
 
3330
3255
  /**
@@ -6185,7 +6110,9 @@ const deprecatedLocationPropertyDescriptors = {
6185
6110
  };
6186
6111
 
6187
6112
  const session = new Session(recentRequests);
6188
- const { cache, navigator } = session;
6113
+
6114
+ // Rename `navigator` to avoid shadowing `window.navigator`
6115
+ const { cache, navigator: sessionNavigator } = session;
6189
6116
 
6190
6117
  /**
6191
6118
  * Starts the main session.
@@ -6310,14 +6237,14 @@ function morphTurboFrameElements(currentFrame, newFrame) {
6310
6237
 
6311
6238
  var Turbo = /*#__PURE__*/Object.freeze({
6312
6239
  __proto__: null,
6313
- navigator: navigator,
6314
- session: session,
6315
- cache: cache,
6316
6240
  PageRenderer: PageRenderer,
6317
6241
  PageSnapshot: PageSnapshot,
6318
6242
  FrameRenderer: FrameRenderer,
6319
6243
  fetch: fetchWithTurboHeaders,
6320
6244
  config: config,
6245
+ session: session,
6246
+ cache: cache,
6247
+ navigator: sessionNavigator,
6321
6248
  start: start,
6322
6249
  registerAdapter: registerAdapter,
6323
6250
  visit: visit,
@@ -7251,4 +7178,4 @@ if (customElements.get("turbo-stream-source") === undefined) {
7251
7178
  window.Turbo = { ...Turbo, StreamActions };
7252
7179
  start();
7253
7180
 
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 };
7181
+ 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, sessionNavigator as navigator, registerAdapter, renderStreamMessage, session, setConfirmMethod, setFormMode, setProgressBarDelay, start, visit };
@@ -2125,7 +2125,6 @@ Copyright © 2026 37signals LLC
2125
2125
  * @property {ConfigInternal['callbacks']} callbacks
2126
2126
  * @property {ConfigInternal['head']} head
2127
2127
  * @property {HTMLDivElement} pantry
2128
- * @property {Element[]} activeElementAndParents
2129
2128
  */
2130
2129
 
2131
2130
  //=============================================================================
@@ -2201,6 +2200,14 @@ Copyright © 2026 37signals LLC
2201
2200
  */
2202
2201
  function morphOuterHTML(ctx, oldNode, newNode) {
2203
2202
  const oldParent = normalizeParent(oldNode);
2203
+
2204
+ // basis for calulating which nodes were morphed
2205
+ // since there may be unmorphed sibling nodes
2206
+ let childNodes = Array.from(oldParent.childNodes);
2207
+ const index = childNodes.indexOf(oldNode);
2208
+ // how many elements are to the right of the oldNode
2209
+ const rightMargin = childNodes.length - (index + 1);
2210
+
2204
2211
  morphChildren(
2205
2212
  ctx,
2206
2213
  oldParent,
@@ -2209,8 +2216,10 @@ Copyright © 2026 37signals LLC
2209
2216
  oldNode, // start point for iteration
2210
2217
  oldNode.nextSibling, // end point for iteration
2211
2218
  );
2212
- // this is safe even with siblings, because normalizeParent returns a SlicedParentNode if needed.
2213
- return Array.from(oldParent.childNodes);
2219
+
2220
+ // return just the morphed nodes
2221
+ childNodes = Array.from(oldParent.childNodes);
2222
+ return childNodes.slice(index, childNodes.length - rightMargin);
2214
2223
  }
2215
2224
 
2216
2225
  /**
@@ -2239,11 +2248,8 @@ Copyright © 2026 37signals LLC
2239
2248
 
2240
2249
  const results = fn();
2241
2250
 
2242
- if (
2243
- activeElementId &&
2244
- activeElementId !== document.activeElement?.getAttribute("id")
2245
- ) {
2246
- activeElement = ctx.target.querySelector(`[id="${activeElementId}"]`);
2251
+ if (activeElementId && activeElementId !== document.activeElement?.id) {
2252
+ activeElement = ctx.target.querySelector(`#${activeElementId}`);
2247
2253
  activeElement?.focus();
2248
2254
  }
2249
2255
  if (activeElement && !activeElement.selectionEnd && selectionEnd) {
@@ -2321,23 +2327,17 @@ Copyright © 2026 37signals LLC
2321
2327
  }
2322
2328
 
2323
2329
  // if the matching node is elsewhere in the original content
2324
- if (newChild instanceof Element) {
2325
- // we can pretend the id is non-null because the next `.has` line will reject it if not
2326
- const newChildId = /** @type {String} */ (
2327
- newChild.getAttribute("id")
2330
+ if (newChild instanceof Element && ctx.persistentIds.has(newChild.id)) {
2331
+ // move it and all its children here and morph
2332
+ const movedChild = moveBeforeById(
2333
+ oldParent,
2334
+ newChild.id,
2335
+ insertionPoint,
2336
+ ctx,
2328
2337
  );
2329
- if (ctx.persistentIds.has(newChildId)) {
2330
- // move it and all its children here and morph
2331
- const movedChild = moveBeforeById(
2332
- oldParent,
2333
- newChildId,
2334
- insertionPoint,
2335
- ctx,
2336
- );
2337
- morphNode(movedChild, newChild, ctx);
2338
- insertionPoint = movedChild.nextSibling;
2339
- continue;
2340
- }
2338
+ morphNode(movedChild, newChild, ctx);
2339
+ insertionPoint = movedChild.nextSibling;
2340
+ continue;
2341
2341
  }
2342
2342
 
2343
2343
  // last resort: insert the new node from scratch
@@ -2447,8 +2447,7 @@ Copyright © 2026 37signals LLC
2447
2447
 
2448
2448
  // if the current node contains active element, stop looking for better future matches,
2449
2449
  // because if one is found, this node will be moved to the pantry, reparenting it and thus losing focus
2450
- // @ts-ignore pretend cursor is Element rather than Node, we're just testing for array inclusion
2451
- if (ctx.activeElementAndParents.includes(cursor)) break;
2450
+ if (cursor.contains(document.activeElement)) break;
2452
2451
 
2453
2452
  cursor = cursor.nextSibling;
2454
2453
  }
@@ -2498,9 +2497,7 @@ Copyright © 2026 37signals LLC
2498
2497
  // If oldElt has an `id` with possible state and it doesn't match newElt.id then avoid morphing.
2499
2498
  // We'll still match an anonymous node with an IDed newElt, though, because if it got this far,
2500
2499
  // its not persistent, and new nodes can't have any hidden state.
2501
- // 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
2502
- (!oldElt.getAttribute?.("id") ||
2503
- oldElt.getAttribute?.("id") === newElt.getAttribute?.("id"))
2500
+ (!oldElt.id || oldElt.id === newElt.id)
2504
2501
  );
2505
2502
  }
2506
2503
 
@@ -2564,11 +2561,8 @@ Copyright © 2026 37signals LLC
2564
2561
  const target =
2565
2562
  /** @type {Element} - will always be found */
2566
2563
  (
2567
- // ctx.target.id unsafe because of form input shadowing
2568
- // ctx.target could be a document fragment which doesn't have `getAttribute`
2569
- (ctx.target.getAttribute?.("id") === id && ctx.target) ||
2570
- ctx.target.querySelector(`[id="${id}"]`) ||
2571
- ctx.pantry.querySelector(`[id="${id}"]`)
2564
+ ctx.target.querySelector(`#${id}`) ||
2565
+ ctx.pantry.querySelector(`#${id}`)
2572
2566
  );
2573
2567
  removeElementFromAncestorsIdMaps(target, ctx);
2574
2568
  moveBefore(parentNode, target, after);
@@ -2584,8 +2578,7 @@ Copyright © 2026 37signals LLC
2584
2578
  * @param {MorphContext} ctx
2585
2579
  */
2586
2580
  function removeElementFromAncestorsIdMaps(element, ctx) {
2587
- // we know id is non-null String, because this function is only called on elements with ids
2588
- const id = /** @type {String} */ (element.getAttribute("id"));
2581
+ const id = element.id;
2589
2582
  /** @ts-ignore - safe to loop in this way **/
2590
2583
  while ((element = element.parentNode)) {
2591
2584
  let idSet = ctx.idMap.get(element);
@@ -3023,7 +3016,6 @@ Copyright © 2026 37signals LLC
3023
3016
  idMap: idMap,
3024
3017
  persistentIds: persistentIds,
3025
3018
  pantry: createPantry(),
3026
- activeElementAndParents: createActiveElementAndParents(oldNode),
3027
3019
  callbacks: mergedConfig.callbacks,
3028
3020
  head: mergedConfig.head,
3029
3021
  };
@@ -3064,24 +3056,6 @@ Copyright © 2026 37signals LLC
3064
3056
  return pantry;
3065
3057
  }
3066
3058
 
3067
- /**
3068
- * @param {Element} oldNode
3069
- * @returns {Element[]}
3070
- */
3071
- function createActiveElementAndParents(oldNode) {
3072
- /** @type {Element[]} */
3073
- let activeElementAndParents = [];
3074
- let elt = document.activeElement;
3075
- if (elt?.tagName !== "BODY" && oldNode.contains(elt)) {
3076
- while (elt) {
3077
- activeElementAndParents.push(elt);
3078
- if (elt === oldNode) break;
3079
- elt = elt.parentElement;
3080
- }
3081
- }
3082
- return activeElementAndParents;
3083
- }
3084
-
3085
3059
  /**
3086
3060
  * Returns all elements with an ID contained within the root element and its descendants
3087
3061
  *
@@ -3090,8 +3064,7 @@ Copyright © 2026 37signals LLC
3090
3064
  */
3091
3065
  function findIdElements(root) {
3092
3066
  let elements = Array.from(root.querySelectorAll("[id]"));
3093
- // root could be a document fragment which doesn't have `getAttribute`
3094
- if (root.getAttribute?.("id")) {
3067
+ if (root.id) {
3095
3068
  elements.push(root);
3096
3069
  }
3097
3070
  return elements;
@@ -3110,9 +3083,7 @@ Copyright © 2026 37signals LLC
3110
3083
  */
3111
3084
  function populateIdMapWithTree(idMap, persistentIds, root, elements) {
3112
3085
  for (const elt of elements) {
3113
- // we can pretend id is non-null String, because the .has line will reject it immediately if not
3114
- const id = /** @type {String} */ (elt.getAttribute("id"));
3115
- if (persistentIds.has(id)) {
3086
+ if (persistentIds.has(elt.id)) {
3116
3087
  /** @type {Element|null} */
3117
3088
  let current = elt;
3118
3089
  // walk up the parent hierarchy of that element, adding the id
@@ -3124,7 +3095,7 @@ Copyright © 2026 37signals LLC
3124
3095
  idSet = new Set();
3125
3096
  idMap.set(current, idSet);
3126
3097
  }
3127
- idSet.add(id);
3098
+ idSet.add(elt.id);
3128
3099
 
3129
3100
  if (current === root) break;
3130
3101
  current = current.parentElement;
@@ -3238,9 +3209,8 @@ Copyright © 2026 37signals LLC
3238
3209
  if (newContent.parentNode) {
3239
3210
  // we can't use the parent directly because newContent may have siblings
3240
3211
  // that we don't want in the morph, and reparenting might be expensive (TODO is it?),
3241
- // so instead we create a fake parent node that only sees a slice of its children.
3242
- /** @type {Element} */
3243
- return /** @type {any} */ (new SlicedParentNode(newContent));
3212
+ // so we create a duck-typed parent node instead.
3213
+ return createDuckTypedParent(newContent);
3244
3214
  } else {
3245
3215
  // a single node is added as a child to a dummy parent
3246
3216
  const dummyParent = document.createElement("div");
@@ -3259,78 +3229,33 @@ Copyright © 2026 37signals LLC
3259
3229
  }
3260
3230
 
3261
3231
  /**
3262
- * A fake duck-typed parent element to wrap a single node, without actually reparenting it.
3263
- * This is useful because the node may have siblings that we don't want in the morph, and it may also be moved
3264
- * or replaced with one or more elements during the morph. This class effectively allows us a window into
3265
- * a slice of a node's children.
3232
+ * Creates a fake duck-typed parent element to wrap a single node, without actually reparenting it.
3266
3233
  * "If it walks like a duck, and quacks like a duck, then it must be a duck!" -- James Whitcomb Riley (1849–1916)
3234
+ *
3235
+ * @param {Node} newContent
3236
+ * @returns {Element}
3267
3237
  */
3268
- class SlicedParentNode {
3269
- /** @param {Node} node */
3270
- constructor(node) {
3271
- this.originalNode = node;
3272
- this.realParentNode = /** @type {Element} */ (node.parentNode);
3273
- this.previousSibling = node.previousSibling;
3274
- this.nextSibling = node.nextSibling;
3275
- }
3276
-
3277
- /** @returns {Node[]} */
3278
- get childNodes() {
3279
- // return slice of realParent's current childNodes, based on previousSibling and nextSibling
3280
- const nodes = [];
3281
- let cursor = this.previousSibling
3282
- ? this.previousSibling.nextSibling
3283
- : this.realParentNode.firstChild;
3284
- while (cursor && cursor != this.nextSibling) {
3285
- nodes.push(cursor);
3286
- cursor = cursor.nextSibling;
3287
- }
3288
- return nodes;
3289
- }
3290
-
3291
- /**
3292
- * @param {string} selector
3293
- * @returns {Element[]}
3294
- */
3295
- querySelectorAll(selector) {
3296
- return this.childNodes.reduce((results, node) => {
3297
- if (node instanceof Element) {
3298
- if (node.matches(selector)) results.push(node);
3299
- const nodeList = node.querySelectorAll(selector);
3300
- for (let i = 0; i < nodeList.length; i++) {
3301
- results.push(nodeList[i]);
3302
- }
3303
- }
3304
- return results;
3305
- }, /** @type {Element[]} */ ([]));
3306
- }
3307
-
3308
- /**
3309
- * @param {Node} node
3310
- * @param {Node} referenceNode
3311
- * @returns {Node}
3312
- */
3313
- insertBefore(node, referenceNode) {
3314
- return this.realParentNode.insertBefore(node, referenceNode);
3315
- }
3316
-
3317
- /**
3318
- * @param {Node} node
3319
- * @param {Node} referenceNode
3320
- * @returns {Node}
3321
- */
3322
- moveBefore(node, referenceNode) {
3323
- // @ts-ignore - use new moveBefore feature
3324
- return this.realParentNode.moveBefore(node, referenceNode);
3325
- }
3326
-
3327
- /**
3328
- * for later use with populateIdMapWithTree to halt upwards iteration
3329
- * @returns {Node}
3330
- */
3331
- get __idiomorphRoot() {
3332
- return this.originalNode;
3333
- }
3238
+ function createDuckTypedParent(newContent) {
3239
+ return /** @type {Element} */ (
3240
+ /** @type {unknown} */ ({
3241
+ childNodes: [newContent],
3242
+ /** @ts-ignore - cover your eyes for a minute, tsc */
3243
+ querySelectorAll: (s) => {
3244
+ /** @ts-ignore */
3245
+ const elements = newContent.querySelectorAll(s);
3246
+ /** @ts-ignore */
3247
+ return newContent.matches(s) ? [newContent, ...elements] : elements;
3248
+ },
3249
+ /** @ts-ignore */
3250
+ insertBefore: (n, r) => newContent.parentNode.insertBefore(n, r),
3251
+ /** @ts-ignore */
3252
+ moveBefore: (n, r) => newContent.parentNode.moveBefore(n, r),
3253
+ // for later use with populateIdMapWithTree to halt upwards iteration
3254
+ get __idiomorphRoot() {
3255
+ return newContent;
3256
+ },
3257
+ })
3258
+ );
3334
3259
  }
3335
3260
 
3336
3261
  /**
@@ -6191,7 +6116,9 @@ Copyright © 2026 37signals LLC
6191
6116
  };
6192
6117
 
6193
6118
  const session = new Session(recentRequests);
6194
- const { cache, navigator } = session;
6119
+
6120
+ // Rename `navigator` to avoid shadowing `window.navigator`
6121
+ const { cache, navigator: sessionNavigator } = session;
6195
6122
 
6196
6123
  /**
6197
6124
  * Starts the main session.
@@ -6316,14 +6243,14 @@ Copyright © 2026 37signals LLC
6316
6243
 
6317
6244
  var Turbo = /*#__PURE__*/Object.freeze({
6318
6245
  __proto__: null,
6319
- navigator: navigator,
6320
- session: session,
6321
- cache: cache,
6322
6246
  PageRenderer: PageRenderer,
6323
6247
  PageSnapshot: PageSnapshot,
6324
6248
  FrameRenderer: FrameRenderer,
6325
6249
  fetch: fetchWithTurboHeaders,
6326
6250
  config: config,
6251
+ session: session,
6252
+ cache: cache,
6253
+ navigator: sessionNavigator,
6327
6254
  start: start,
6328
6255
  registerAdapter: registerAdapter,
6329
6256
  visit: visit,
@@ -7281,7 +7208,7 @@ Copyright © 2026 37signals LLC
7281
7208
  exports.morphChildren = morphChildren;
7282
7209
  exports.morphElements = morphElements;
7283
7210
  exports.morphTurboFrameElements = morphTurboFrameElements;
7284
- exports.navigator = navigator;
7211
+ exports.navigator = sessionNavigator;
7285
7212
  exports.registerAdapter = registerAdapter;
7286
7213
  exports.renderStreamMessage = renderStreamMessage;
7287
7214
  exports.session = session;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotwired/turbo",
3
- "version": "8.0.21",
3
+ "version": "8.0.22",
4
4
  "description": "The speed of a single-page web application without having to write any JavaScript",
5
5
  "module": "dist/turbo.es2017-esm.js",
6
6
  "main": "dist/turbo.es2017-umd.js",