@hotwired/turbo 8.0.22 → 8.0.23
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 +135 -60
- package/dist/turbo.es2017-umd.js +135 -60
- package/package.json +1 -1
package/dist/turbo.es2017-esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
Turbo 8.0.
|
|
2
|
+
Turbo 8.0.23
|
|
3
3
|
Copyright © 2026 37signals LLC
|
|
4
4
|
*/
|
|
5
5
|
const FrameLoadingStyle = {
|
|
@@ -2119,6 +2119,7 @@ var Idiomorph = (function () {
|
|
|
2119
2119
|
* @property {ConfigInternal['callbacks']} callbacks
|
|
2120
2120
|
* @property {ConfigInternal['head']} head
|
|
2121
2121
|
* @property {HTMLDivElement} pantry
|
|
2122
|
+
* @property {Element[]} activeElementAndParents
|
|
2122
2123
|
*/
|
|
2123
2124
|
|
|
2124
2125
|
//=============================================================================
|
|
@@ -2194,14 +2195,6 @@ var Idiomorph = (function () {
|
|
|
2194
2195
|
*/
|
|
2195
2196
|
function morphOuterHTML(ctx, oldNode, newNode) {
|
|
2196
2197
|
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
|
-
|
|
2205
2198
|
morphChildren(
|
|
2206
2199
|
ctx,
|
|
2207
2200
|
oldParent,
|
|
@@ -2210,10 +2203,8 @@ var Idiomorph = (function () {
|
|
|
2210
2203
|
oldNode, // start point for iteration
|
|
2211
2204
|
oldNode.nextSibling, // end point for iteration
|
|
2212
2205
|
);
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
childNodes = Array.from(oldParent.childNodes);
|
|
2216
|
-
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);
|
|
2217
2208
|
}
|
|
2218
2209
|
|
|
2219
2210
|
/**
|
|
@@ -2242,8 +2233,11 @@ var Idiomorph = (function () {
|
|
|
2242
2233
|
|
|
2243
2234
|
const results = fn();
|
|
2244
2235
|
|
|
2245
|
-
if (
|
|
2246
|
-
|
|
2236
|
+
if (
|
|
2237
|
+
activeElementId &&
|
|
2238
|
+
activeElementId !== document.activeElement?.getAttribute("id")
|
|
2239
|
+
) {
|
|
2240
|
+
activeElement = ctx.target.querySelector(`[id="${activeElementId}"]`);
|
|
2247
2241
|
activeElement?.focus();
|
|
2248
2242
|
}
|
|
2249
2243
|
if (activeElement && !activeElement.selectionEnd && selectionEnd) {
|
|
@@ -2321,17 +2315,23 @@ var Idiomorph = (function () {
|
|
|
2321
2315
|
}
|
|
2322
2316
|
|
|
2323
2317
|
// if the matching node is elsewhere in the original content
|
|
2324
|
-
if (newChild instanceof Element
|
|
2325
|
-
//
|
|
2326
|
-
const
|
|
2327
|
-
|
|
2328
|
-
newChild.id,
|
|
2329
|
-
insertionPoint,
|
|
2330
|
-
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")
|
|
2331
2322
|
);
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
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
|
+
}
|
|
2335
2335
|
}
|
|
2336
2336
|
|
|
2337
2337
|
// last resort: insert the new node from scratch
|
|
@@ -2441,7 +2441,8 @@ 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
|
-
|
|
2444
|
+
// @ts-ignore pretend cursor is Element rather than Node, we're just testing for array inclusion
|
|
2445
|
+
if (ctx.activeElementAndParents.includes(cursor)) break;
|
|
2445
2446
|
|
|
2446
2447
|
cursor = cursor.nextSibling;
|
|
2447
2448
|
}
|
|
@@ -2491,7 +2492,9 @@ var Idiomorph = (function () {
|
|
|
2491
2492
|
// If oldElt has an `id` with possible state and it doesn't match newElt.id then avoid morphing.
|
|
2492
2493
|
// We'll still match an anonymous node with an IDed newElt, though, because if it got this far,
|
|
2493
2494
|
// its not persistent, and new nodes can't have any hidden state.
|
|
2494
|
-
|
|
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"))
|
|
2495
2498
|
);
|
|
2496
2499
|
}
|
|
2497
2500
|
|
|
@@ -2555,8 +2558,11 @@ var Idiomorph = (function () {
|
|
|
2555
2558
|
const target =
|
|
2556
2559
|
/** @type {Element} - will always be found */
|
|
2557
2560
|
(
|
|
2558
|
-
ctx.target.
|
|
2559
|
-
|
|
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}"]`)
|
|
2560
2566
|
);
|
|
2561
2567
|
removeElementFromAncestorsIdMaps(target, ctx);
|
|
2562
2568
|
moveBefore(parentNode, target, after);
|
|
@@ -2572,7 +2578,8 @@ var Idiomorph = (function () {
|
|
|
2572
2578
|
* @param {MorphContext} ctx
|
|
2573
2579
|
*/
|
|
2574
2580
|
function removeElementFromAncestorsIdMaps(element, ctx) {
|
|
2575
|
-
|
|
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"));
|
|
2576
2583
|
/** @ts-ignore - safe to loop in this way **/
|
|
2577
2584
|
while ((element = element.parentNode)) {
|
|
2578
2585
|
let idSet = ctx.idMap.get(element);
|
|
@@ -3010,6 +3017,7 @@ var Idiomorph = (function () {
|
|
|
3010
3017
|
idMap: idMap,
|
|
3011
3018
|
persistentIds: persistentIds,
|
|
3012
3019
|
pantry: createPantry(),
|
|
3020
|
+
activeElementAndParents: createActiveElementAndParents(oldNode),
|
|
3013
3021
|
callbacks: mergedConfig.callbacks,
|
|
3014
3022
|
head: mergedConfig.head,
|
|
3015
3023
|
};
|
|
@@ -3050,6 +3058,24 @@ var Idiomorph = (function () {
|
|
|
3050
3058
|
return pantry;
|
|
3051
3059
|
}
|
|
3052
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
|
+
|
|
3053
3079
|
/**
|
|
3054
3080
|
* Returns all elements with an ID contained within the root element and its descendants
|
|
3055
3081
|
*
|
|
@@ -3058,7 +3084,8 @@ var Idiomorph = (function () {
|
|
|
3058
3084
|
*/
|
|
3059
3085
|
function findIdElements(root) {
|
|
3060
3086
|
let elements = Array.from(root.querySelectorAll("[id]"));
|
|
3061
|
-
|
|
3087
|
+
// root could be a document fragment which doesn't have `getAttribute`
|
|
3088
|
+
if (root.getAttribute?.("id")) {
|
|
3062
3089
|
elements.push(root);
|
|
3063
3090
|
}
|
|
3064
3091
|
return elements;
|
|
@@ -3077,7 +3104,9 @@ var Idiomorph = (function () {
|
|
|
3077
3104
|
*/
|
|
3078
3105
|
function populateIdMapWithTree(idMap, persistentIds, root, elements) {
|
|
3079
3106
|
for (const elt of elements) {
|
|
3080
|
-
|
|
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)) {
|
|
3081
3110
|
/** @type {Element|null} */
|
|
3082
3111
|
let current = elt;
|
|
3083
3112
|
// walk up the parent hierarchy of that element, adding the id
|
|
@@ -3089,7 +3118,7 @@ var Idiomorph = (function () {
|
|
|
3089
3118
|
idSet = new Set();
|
|
3090
3119
|
idMap.set(current, idSet);
|
|
3091
3120
|
}
|
|
3092
|
-
idSet.add(
|
|
3121
|
+
idSet.add(id);
|
|
3093
3122
|
|
|
3094
3123
|
if (current === root) break;
|
|
3095
3124
|
current = current.parentElement;
|
|
@@ -3203,8 +3232,9 @@ var Idiomorph = (function () {
|
|
|
3203
3232
|
if (newContent.parentNode) {
|
|
3204
3233
|
// we can't use the parent directly because newContent may have siblings
|
|
3205
3234
|
// that we don't want in the morph, and reparenting might be expensive (TODO is it?),
|
|
3206
|
-
// so we create a
|
|
3207
|
-
|
|
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));
|
|
3208
3238
|
} else {
|
|
3209
3239
|
// a single node is added as a child to a dummy parent
|
|
3210
3240
|
const dummyParent = document.createElement("div");
|
|
@@ -3223,33 +3253,78 @@ var Idiomorph = (function () {
|
|
|
3223
3253
|
}
|
|
3224
3254
|
|
|
3225
3255
|
/**
|
|
3226
|
-
*
|
|
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.
|
|
3227
3260
|
* "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}
|
|
3231
3261
|
*/
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
}
|
|
3252
|
-
|
|
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
|
+
}
|
|
3253
3328
|
}
|
|
3254
3329
|
|
|
3255
3330
|
/**
|
package/dist/turbo.es2017-umd.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
Turbo 8.0.
|
|
2
|
+
Turbo 8.0.23
|
|
3
3
|
Copyright © 2026 37signals LLC
|
|
4
4
|
*/
|
|
5
5
|
(function (global, factory) {
|
|
@@ -2125,6 +2125,7 @@ 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
|
|
2128
2129
|
*/
|
|
2129
2130
|
|
|
2130
2131
|
//=============================================================================
|
|
@@ -2200,14 +2201,6 @@ Copyright © 2026 37signals LLC
|
|
|
2200
2201
|
*/
|
|
2201
2202
|
function morphOuterHTML(ctx, oldNode, newNode) {
|
|
2202
2203
|
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
|
-
|
|
2211
2204
|
morphChildren(
|
|
2212
2205
|
ctx,
|
|
2213
2206
|
oldParent,
|
|
@@ -2216,10 +2209,8 @@ Copyright © 2026 37signals LLC
|
|
|
2216
2209
|
oldNode, // start point for iteration
|
|
2217
2210
|
oldNode.nextSibling, // end point for iteration
|
|
2218
2211
|
);
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
childNodes = Array.from(oldParent.childNodes);
|
|
2222
|
-
return childNodes.slice(index, childNodes.length - rightMargin);
|
|
2212
|
+
// this is safe even with siblings, because normalizeParent returns a SlicedParentNode if needed.
|
|
2213
|
+
return Array.from(oldParent.childNodes);
|
|
2223
2214
|
}
|
|
2224
2215
|
|
|
2225
2216
|
/**
|
|
@@ -2248,8 +2239,11 @@ Copyright © 2026 37signals LLC
|
|
|
2248
2239
|
|
|
2249
2240
|
const results = fn();
|
|
2250
2241
|
|
|
2251
|
-
if (
|
|
2252
|
-
|
|
2242
|
+
if (
|
|
2243
|
+
activeElementId &&
|
|
2244
|
+
activeElementId !== document.activeElement?.getAttribute("id")
|
|
2245
|
+
) {
|
|
2246
|
+
activeElement = ctx.target.querySelector(`[id="${activeElementId}"]`);
|
|
2253
2247
|
activeElement?.focus();
|
|
2254
2248
|
}
|
|
2255
2249
|
if (activeElement && !activeElement.selectionEnd && selectionEnd) {
|
|
@@ -2327,17 +2321,23 @@ Copyright © 2026 37signals LLC
|
|
|
2327
2321
|
}
|
|
2328
2322
|
|
|
2329
2323
|
// if the matching node is elsewhere in the original content
|
|
2330
|
-
if (newChild instanceof Element
|
|
2331
|
-
//
|
|
2332
|
-
const
|
|
2333
|
-
|
|
2334
|
-
newChild.id,
|
|
2335
|
-
insertionPoint,
|
|
2336
|
-
ctx,
|
|
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")
|
|
2337
2328
|
);
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
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
|
+
}
|
|
2341
2341
|
}
|
|
2342
2342
|
|
|
2343
2343
|
// last resort: insert the new node from scratch
|
|
@@ -2447,7 +2447,8 @@ 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
|
-
|
|
2450
|
+
// @ts-ignore pretend cursor is Element rather than Node, we're just testing for array inclusion
|
|
2451
|
+
if (ctx.activeElementAndParents.includes(cursor)) break;
|
|
2451
2452
|
|
|
2452
2453
|
cursor = cursor.nextSibling;
|
|
2453
2454
|
}
|
|
@@ -2497,7 +2498,9 @@ Copyright © 2026 37signals LLC
|
|
|
2497
2498
|
// If oldElt has an `id` with possible state and it doesn't match newElt.id then avoid morphing.
|
|
2498
2499
|
// We'll still match an anonymous node with an IDed newElt, though, because if it got this far,
|
|
2499
2500
|
// its not persistent, and new nodes can't have any hidden state.
|
|
2500
|
-
|
|
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"))
|
|
2501
2504
|
);
|
|
2502
2505
|
}
|
|
2503
2506
|
|
|
@@ -2561,8 +2564,11 @@ Copyright © 2026 37signals LLC
|
|
|
2561
2564
|
const target =
|
|
2562
2565
|
/** @type {Element} - will always be found */
|
|
2563
2566
|
(
|
|
2564
|
-
ctx.target.
|
|
2565
|
-
|
|
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}"]`)
|
|
2566
2572
|
);
|
|
2567
2573
|
removeElementFromAncestorsIdMaps(target, ctx);
|
|
2568
2574
|
moveBefore(parentNode, target, after);
|
|
@@ -2578,7 +2584,8 @@ Copyright © 2026 37signals LLC
|
|
|
2578
2584
|
* @param {MorphContext} ctx
|
|
2579
2585
|
*/
|
|
2580
2586
|
function removeElementFromAncestorsIdMaps(element, ctx) {
|
|
2581
|
-
|
|
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"));
|
|
2582
2589
|
/** @ts-ignore - safe to loop in this way **/
|
|
2583
2590
|
while ((element = element.parentNode)) {
|
|
2584
2591
|
let idSet = ctx.idMap.get(element);
|
|
@@ -3016,6 +3023,7 @@ Copyright © 2026 37signals LLC
|
|
|
3016
3023
|
idMap: idMap,
|
|
3017
3024
|
persistentIds: persistentIds,
|
|
3018
3025
|
pantry: createPantry(),
|
|
3026
|
+
activeElementAndParents: createActiveElementAndParents(oldNode),
|
|
3019
3027
|
callbacks: mergedConfig.callbacks,
|
|
3020
3028
|
head: mergedConfig.head,
|
|
3021
3029
|
};
|
|
@@ -3056,6 +3064,24 @@ Copyright © 2026 37signals LLC
|
|
|
3056
3064
|
return pantry;
|
|
3057
3065
|
}
|
|
3058
3066
|
|
|
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
|
+
|
|
3059
3085
|
/**
|
|
3060
3086
|
* Returns all elements with an ID contained within the root element and its descendants
|
|
3061
3087
|
*
|
|
@@ -3064,7 +3090,8 @@ Copyright © 2026 37signals LLC
|
|
|
3064
3090
|
*/
|
|
3065
3091
|
function findIdElements(root) {
|
|
3066
3092
|
let elements = Array.from(root.querySelectorAll("[id]"));
|
|
3067
|
-
|
|
3093
|
+
// root could be a document fragment which doesn't have `getAttribute`
|
|
3094
|
+
if (root.getAttribute?.("id")) {
|
|
3068
3095
|
elements.push(root);
|
|
3069
3096
|
}
|
|
3070
3097
|
return elements;
|
|
@@ -3083,7 +3110,9 @@ Copyright © 2026 37signals LLC
|
|
|
3083
3110
|
*/
|
|
3084
3111
|
function populateIdMapWithTree(idMap, persistentIds, root, elements) {
|
|
3085
3112
|
for (const elt of elements) {
|
|
3086
|
-
|
|
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)) {
|
|
3087
3116
|
/** @type {Element|null} */
|
|
3088
3117
|
let current = elt;
|
|
3089
3118
|
// walk up the parent hierarchy of that element, adding the id
|
|
@@ -3095,7 +3124,7 @@ Copyright © 2026 37signals LLC
|
|
|
3095
3124
|
idSet = new Set();
|
|
3096
3125
|
idMap.set(current, idSet);
|
|
3097
3126
|
}
|
|
3098
|
-
idSet.add(
|
|
3127
|
+
idSet.add(id);
|
|
3099
3128
|
|
|
3100
3129
|
if (current === root) break;
|
|
3101
3130
|
current = current.parentElement;
|
|
@@ -3209,8 +3238,9 @@ Copyright © 2026 37signals LLC
|
|
|
3209
3238
|
if (newContent.parentNode) {
|
|
3210
3239
|
// we can't use the parent directly because newContent may have siblings
|
|
3211
3240
|
// that we don't want in the morph, and reparenting might be expensive (TODO is it?),
|
|
3212
|
-
// so we create a
|
|
3213
|
-
|
|
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));
|
|
3214
3244
|
} else {
|
|
3215
3245
|
// a single node is added as a child to a dummy parent
|
|
3216
3246
|
const dummyParent = document.createElement("div");
|
|
@@ -3229,33 +3259,78 @@ Copyright © 2026 37signals LLC
|
|
|
3229
3259
|
}
|
|
3230
3260
|
|
|
3231
3261
|
/**
|
|
3232
|
-
*
|
|
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.
|
|
3233
3266
|
* "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}
|
|
3237
3267
|
*/
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
}
|
|
3258
|
-
|
|
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
|
+
}
|
|
3259
3334
|
}
|
|
3260
3335
|
|
|
3261
3336
|
/**
|
package/package.json
CHANGED