@hotwired/turbo 8.0.19 → 8.0.20
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 +144 -67
- package/dist/turbo.es2017-umd.js +144 -67
- 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.19
|
|
3
3
|
Copyright © 2025 37signals LLC
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
@@ -2139,6 +2139,7 @@ var Idiomorph = (function () {
|
|
|
2139
2139
|
* @property {ConfigInternal['callbacks']} callbacks
|
|
2140
2140
|
* @property {ConfigInternal['head']} head
|
|
2141
2141
|
* @property {HTMLDivElement} pantry
|
|
2142
|
+
* @property {Element[]} activeElementAndParents
|
|
2142
2143
|
*/
|
|
2143
2144
|
|
|
2144
2145
|
//=============================================================================
|
|
@@ -2214,14 +2215,6 @@ var Idiomorph = (function () {
|
|
|
2214
2215
|
*/
|
|
2215
2216
|
function morphOuterHTML(ctx, oldNode, newNode) {
|
|
2216
2217
|
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
2218
|
morphChildren(
|
|
2226
2219
|
ctx,
|
|
2227
2220
|
oldParent,
|
|
@@ -2230,10 +2223,8 @@ var Idiomorph = (function () {
|
|
|
2230
2223
|
oldNode, // start point for iteration
|
|
2231
2224
|
oldNode.nextSibling, // end point for iteration
|
|
2232
2225
|
);
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
childNodes = Array.from(oldParent.childNodes);
|
|
2236
|
-
return childNodes.slice(index, childNodes.length - rightMargin);
|
|
2226
|
+
// this is safe even with siblings, because normalizeParent returns a SlicedParentNode if needed.
|
|
2227
|
+
return Array.from(oldParent.childNodes);
|
|
2237
2228
|
}
|
|
2238
2229
|
|
|
2239
2230
|
/**
|
|
@@ -2262,8 +2253,11 @@ var Idiomorph = (function () {
|
|
|
2262
2253
|
|
|
2263
2254
|
const results = fn();
|
|
2264
2255
|
|
|
2265
|
-
if (
|
|
2266
|
-
|
|
2256
|
+
if (
|
|
2257
|
+
activeElementId &&
|
|
2258
|
+
activeElementId !== document.activeElement?.getAttribute("id")
|
|
2259
|
+
) {
|
|
2260
|
+
activeElement = ctx.target.querySelector(`[id="${activeElementId}"]`);
|
|
2267
2261
|
activeElement?.focus();
|
|
2268
2262
|
}
|
|
2269
2263
|
if (activeElement && !activeElement.selectionEnd && selectionEnd) {
|
|
@@ -2341,17 +2335,23 @@ var Idiomorph = (function () {
|
|
|
2341
2335
|
}
|
|
2342
2336
|
|
|
2343
2337
|
// if the matching node is elsewhere in the original content
|
|
2344
|
-
if (newChild instanceof Element
|
|
2345
|
-
//
|
|
2346
|
-
const
|
|
2347
|
-
|
|
2348
|
-
newChild.id,
|
|
2349
|
-
insertionPoint,
|
|
2350
|
-
ctx,
|
|
2338
|
+
if (newChild instanceof Element) {
|
|
2339
|
+
// we can pretend the id is non-null because the next `.has` line will reject it if not
|
|
2340
|
+
const newChildId = /** @type {String} */ (
|
|
2341
|
+
newChild.getAttribute("id")
|
|
2351
2342
|
);
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2343
|
+
if (ctx.persistentIds.has(newChildId)) {
|
|
2344
|
+
// move it and all its children here and morph
|
|
2345
|
+
const movedChild = moveBeforeById(
|
|
2346
|
+
oldParent,
|
|
2347
|
+
newChildId,
|
|
2348
|
+
insertionPoint,
|
|
2349
|
+
ctx,
|
|
2350
|
+
);
|
|
2351
|
+
morphNode(movedChild, newChild, ctx);
|
|
2352
|
+
insertionPoint = movedChild.nextSibling;
|
|
2353
|
+
continue;
|
|
2354
|
+
}
|
|
2355
2355
|
}
|
|
2356
2356
|
|
|
2357
2357
|
// last resort: insert the new node from scratch
|
|
@@ -2461,7 +2461,8 @@ var Idiomorph = (function () {
|
|
|
2461
2461
|
|
|
2462
2462
|
// if the current node contains active element, stop looking for better future matches,
|
|
2463
2463
|
// because if one is found, this node will be moved to the pantry, reparenting it and thus losing focus
|
|
2464
|
-
|
|
2464
|
+
// @ts-ignore pretend cursor is Element rather than Node, we're just testing for array inclusion
|
|
2465
|
+
if (ctx.activeElementAndParents.includes(cursor)) break;
|
|
2465
2466
|
|
|
2466
2467
|
cursor = cursor.nextSibling;
|
|
2467
2468
|
}
|
|
@@ -2511,7 +2512,9 @@ var Idiomorph = (function () {
|
|
|
2511
2512
|
// If oldElt has an `id` with possible state and it doesn't match newElt.id then avoid morphing.
|
|
2512
2513
|
// We'll still match an anonymous node with an IDed newElt, though, because if it got this far,
|
|
2513
2514
|
// its not persistent, and new nodes can't have any hidden state.
|
|
2514
|
-
|
|
2515
|
+
// 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
|
|
2516
|
+
(!oldElt.getAttribute?.("id") ||
|
|
2517
|
+
oldElt.getAttribute?.("id") === newElt.getAttribute?.("id"))
|
|
2515
2518
|
);
|
|
2516
2519
|
}
|
|
2517
2520
|
|
|
@@ -2575,8 +2578,11 @@ var Idiomorph = (function () {
|
|
|
2575
2578
|
const target =
|
|
2576
2579
|
/** @type {Element} - will always be found */
|
|
2577
2580
|
(
|
|
2578
|
-
ctx.target.
|
|
2579
|
-
|
|
2581
|
+
// ctx.target.id unsafe because of form input shadowing
|
|
2582
|
+
// ctx.target could be a document fragment which doesn't have `getAttribute`
|
|
2583
|
+
(ctx.target.getAttribute?.("id") === id && ctx.target) ||
|
|
2584
|
+
ctx.target.querySelector(`[id="${id}"]`) ||
|
|
2585
|
+
ctx.pantry.querySelector(`[id="${id}"]`)
|
|
2580
2586
|
);
|
|
2581
2587
|
removeElementFromAncestorsIdMaps(target, ctx);
|
|
2582
2588
|
moveBefore(parentNode, target, after);
|
|
@@ -2592,7 +2598,8 @@ var Idiomorph = (function () {
|
|
|
2592
2598
|
* @param {MorphContext} ctx
|
|
2593
2599
|
*/
|
|
2594
2600
|
function removeElementFromAncestorsIdMaps(element, ctx) {
|
|
2595
|
-
|
|
2601
|
+
// we know id is non-null String, because this function is only called on elements with ids
|
|
2602
|
+
const id = /** @type {String} */ (element.getAttribute("id"));
|
|
2596
2603
|
/** @ts-ignore - safe to loop in this way **/
|
|
2597
2604
|
while ((element = element.parentNode)) {
|
|
2598
2605
|
let idSet = ctx.idMap.get(element);
|
|
@@ -3030,6 +3037,7 @@ var Idiomorph = (function () {
|
|
|
3030
3037
|
idMap: idMap,
|
|
3031
3038
|
persistentIds: persistentIds,
|
|
3032
3039
|
pantry: createPantry(),
|
|
3040
|
+
activeElementAndParents: createActiveElementAndParents(oldNode),
|
|
3033
3041
|
callbacks: mergedConfig.callbacks,
|
|
3034
3042
|
head: mergedConfig.head,
|
|
3035
3043
|
};
|
|
@@ -3070,6 +3078,24 @@ var Idiomorph = (function () {
|
|
|
3070
3078
|
return pantry;
|
|
3071
3079
|
}
|
|
3072
3080
|
|
|
3081
|
+
/**
|
|
3082
|
+
* @param {Element} oldNode
|
|
3083
|
+
* @returns {Element[]}
|
|
3084
|
+
*/
|
|
3085
|
+
function createActiveElementAndParents(oldNode) {
|
|
3086
|
+
/** @type {Element[]} */
|
|
3087
|
+
let activeElementAndParents = [];
|
|
3088
|
+
let elt = document.activeElement;
|
|
3089
|
+
if (elt?.tagName !== "BODY" && oldNode.contains(elt)) {
|
|
3090
|
+
while (elt) {
|
|
3091
|
+
activeElementAndParents.push(elt);
|
|
3092
|
+
if (elt === oldNode) break;
|
|
3093
|
+
elt = elt.parentElement;
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
return activeElementAndParents;
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3073
3099
|
/**
|
|
3074
3100
|
* Returns all elements with an ID contained within the root element and its descendants
|
|
3075
3101
|
*
|
|
@@ -3078,7 +3104,8 @@ var Idiomorph = (function () {
|
|
|
3078
3104
|
*/
|
|
3079
3105
|
function findIdElements(root) {
|
|
3080
3106
|
let elements = Array.from(root.querySelectorAll("[id]"));
|
|
3081
|
-
|
|
3107
|
+
// root could be a document fragment which doesn't have `getAttribute`
|
|
3108
|
+
if (root.getAttribute?.("id")) {
|
|
3082
3109
|
elements.push(root);
|
|
3083
3110
|
}
|
|
3084
3111
|
return elements;
|
|
@@ -3097,7 +3124,9 @@ var Idiomorph = (function () {
|
|
|
3097
3124
|
*/
|
|
3098
3125
|
function populateIdMapWithTree(idMap, persistentIds, root, elements) {
|
|
3099
3126
|
for (const elt of elements) {
|
|
3100
|
-
|
|
3127
|
+
// we can pretend id is non-null String, because the .has line will reject it immediately if not
|
|
3128
|
+
const id = /** @type {String} */ (elt.getAttribute("id"));
|
|
3129
|
+
if (persistentIds.has(id)) {
|
|
3101
3130
|
/** @type {Element|null} */
|
|
3102
3131
|
let current = elt;
|
|
3103
3132
|
// walk up the parent hierarchy of that element, adding the id
|
|
@@ -3109,7 +3138,7 @@ var Idiomorph = (function () {
|
|
|
3109
3138
|
idSet = new Set();
|
|
3110
3139
|
idMap.set(current, idSet);
|
|
3111
3140
|
}
|
|
3112
|
-
idSet.add(
|
|
3141
|
+
idSet.add(id);
|
|
3113
3142
|
|
|
3114
3143
|
if (current === root) break;
|
|
3115
3144
|
current = current.parentElement;
|
|
@@ -3223,8 +3252,9 @@ var Idiomorph = (function () {
|
|
|
3223
3252
|
if (newContent.parentNode) {
|
|
3224
3253
|
// we can't use the parent directly because newContent may have siblings
|
|
3225
3254
|
// that we don't want in the morph, and reparenting might be expensive (TODO is it?),
|
|
3226
|
-
// so we create a
|
|
3227
|
-
|
|
3255
|
+
// so instead we create a fake parent node that only sees a slice of its children.
|
|
3256
|
+
/** @type {Element} */
|
|
3257
|
+
return /** @type {any} */ (new SlicedParentNode(newContent));
|
|
3228
3258
|
} else {
|
|
3229
3259
|
// a single node is added as a child to a dummy parent
|
|
3230
3260
|
const dummyParent = document.createElement("div");
|
|
@@ -3243,33 +3273,78 @@ var Idiomorph = (function () {
|
|
|
3243
3273
|
}
|
|
3244
3274
|
|
|
3245
3275
|
/**
|
|
3246
|
-
*
|
|
3276
|
+
* A fake duck-typed parent element to wrap a single node, without actually reparenting it.
|
|
3277
|
+
* This is useful because the node may have siblings that we don't want in the morph, and it may also be moved
|
|
3278
|
+
* or replaced with one or more elements during the morph. This class effectively allows us a window into
|
|
3279
|
+
* a slice of a node's children.
|
|
3247
3280
|
* "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
3281
|
*/
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
}
|
|
3272
|
-
|
|
3282
|
+
class SlicedParentNode {
|
|
3283
|
+
/** @param {Node} node */
|
|
3284
|
+
constructor(node) {
|
|
3285
|
+
this.originalNode = node;
|
|
3286
|
+
this.realParentNode = /** @type {Element} */ (node.parentNode);
|
|
3287
|
+
this.previousSibling = node.previousSibling;
|
|
3288
|
+
this.nextSibling = node.nextSibling;
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3291
|
+
/** @returns {Node[]} */
|
|
3292
|
+
get childNodes() {
|
|
3293
|
+
// return slice of realParent's current childNodes, based on previousSibling and nextSibling
|
|
3294
|
+
const nodes = [];
|
|
3295
|
+
let cursor = this.previousSibling
|
|
3296
|
+
? this.previousSibling.nextSibling
|
|
3297
|
+
: this.realParentNode.firstChild;
|
|
3298
|
+
while (cursor && cursor != this.nextSibling) {
|
|
3299
|
+
nodes.push(cursor);
|
|
3300
|
+
cursor = cursor.nextSibling;
|
|
3301
|
+
}
|
|
3302
|
+
return nodes;
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3305
|
+
/**
|
|
3306
|
+
* @param {string} selector
|
|
3307
|
+
* @returns {Element[]}
|
|
3308
|
+
*/
|
|
3309
|
+
querySelectorAll(selector) {
|
|
3310
|
+
return this.childNodes.reduce((results, node) => {
|
|
3311
|
+
if (node instanceof Element) {
|
|
3312
|
+
if (node.matches(selector)) results.push(node);
|
|
3313
|
+
const nodeList = node.querySelectorAll(selector);
|
|
3314
|
+
for (let i = 0; i < nodeList.length; i++) {
|
|
3315
|
+
results.push(nodeList[i]);
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
return results;
|
|
3319
|
+
}, /** @type {Element[]} */ ([]));
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3322
|
+
/**
|
|
3323
|
+
* @param {Node} node
|
|
3324
|
+
* @param {Node} referenceNode
|
|
3325
|
+
* @returns {Node}
|
|
3326
|
+
*/
|
|
3327
|
+
insertBefore(node, referenceNode) {
|
|
3328
|
+
return this.realParentNode.insertBefore(node, referenceNode);
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
/**
|
|
3332
|
+
* @param {Node} node
|
|
3333
|
+
* @param {Node} referenceNode
|
|
3334
|
+
* @returns {Node}
|
|
3335
|
+
*/
|
|
3336
|
+
moveBefore(node, referenceNode) {
|
|
3337
|
+
// @ts-ignore - use new moveBefore feature
|
|
3338
|
+
return this.realParentNode.moveBefore(node, referenceNode);
|
|
3339
|
+
}
|
|
3340
|
+
|
|
3341
|
+
/**
|
|
3342
|
+
* for later use with populateIdMapWithTree to halt upwards iteration
|
|
3343
|
+
* @returns {Node}
|
|
3344
|
+
*/
|
|
3345
|
+
get __idiomorphRoot() {
|
|
3346
|
+
return this.originalNode;
|
|
3347
|
+
}
|
|
3273
3348
|
}
|
|
3274
3349
|
|
|
3275
3350
|
/**
|
|
@@ -3364,16 +3439,18 @@ function morphChildren(currentElement, newElement, options = {}) {
|
|
|
3364
3439
|
|
|
3365
3440
|
function shouldRefreshFrameWithMorphing(currentFrame, newFrame) {
|
|
3366
3441
|
return currentFrame instanceof FrameElement &&
|
|
3367
|
-
|
|
3368
|
-
// elements don't get initialized until they're attached to the DOM, so
|
|
3369
|
-
// test its Element#nodeName instead
|
|
3370
|
-
newFrame instanceof Element && newFrame.nodeName === "TURBO-FRAME" &&
|
|
3371
|
-
currentFrame.shouldReloadWithMorph &&
|
|
3372
|
-
currentFrame.id === newFrame.id &&
|
|
3373
|
-
(!newFrame.getAttribute("src") || urlsAreEqual(currentFrame.src, newFrame.getAttribute("src"))) &&
|
|
3442
|
+
currentFrame.shouldReloadWithMorph && (!newFrame || areFramesCompatibleForRefreshing(currentFrame, newFrame)) &&
|
|
3374
3443
|
!currentFrame.closest("[data-turbo-permanent]")
|
|
3375
3444
|
}
|
|
3376
3445
|
|
|
3446
|
+
function areFramesCompatibleForRefreshing(currentFrame, newFrame) {
|
|
3447
|
+
// newFrame cannot yet be an instance of FrameElement because custom
|
|
3448
|
+
// elements don't get initialized until they're attached to the DOM, so
|
|
3449
|
+
// test its Element#nodeName instead
|
|
3450
|
+
return newFrame instanceof Element && newFrame.nodeName === "TURBO-FRAME" && currentFrame.id === newFrame.id &&
|
|
3451
|
+
(!newFrame.getAttribute("src") || urlsAreEqual(currentFrame.src, newFrame.getAttribute("src")))
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3377
3454
|
function closestFrameReloadableWithMorphing(node) {
|
|
3378
3455
|
return node.parentElement.closest("turbo-frame[src][refresh=morph]")
|
|
3379
3456
|
}
|
package/dist/turbo.es2017-umd.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
Turbo 8.0.
|
|
2
|
+
Turbo 8.0.19
|
|
3
3
|
Copyright © 2025 37signals LLC
|
|
4
4
|
*/
|
|
5
5
|
(function (global, factory) {
|
|
@@ -2145,6 +2145,7 @@ Copyright © 2025 37signals LLC
|
|
|
2145
2145
|
* @property {ConfigInternal['callbacks']} callbacks
|
|
2146
2146
|
* @property {ConfigInternal['head']} head
|
|
2147
2147
|
* @property {HTMLDivElement} pantry
|
|
2148
|
+
* @property {Element[]} activeElementAndParents
|
|
2148
2149
|
*/
|
|
2149
2150
|
|
|
2150
2151
|
//=============================================================================
|
|
@@ -2220,14 +2221,6 @@ Copyright © 2025 37signals LLC
|
|
|
2220
2221
|
*/
|
|
2221
2222
|
function morphOuterHTML(ctx, oldNode, newNode) {
|
|
2222
2223
|
const oldParent = normalizeParent(oldNode);
|
|
2223
|
-
|
|
2224
|
-
// basis for calulating which nodes were morphed
|
|
2225
|
-
// since there may be unmorphed sibling nodes
|
|
2226
|
-
let childNodes = Array.from(oldParent.childNodes);
|
|
2227
|
-
const index = childNodes.indexOf(oldNode);
|
|
2228
|
-
// how many elements are to the right of the oldNode
|
|
2229
|
-
const rightMargin = childNodes.length - (index + 1);
|
|
2230
|
-
|
|
2231
2224
|
morphChildren(
|
|
2232
2225
|
ctx,
|
|
2233
2226
|
oldParent,
|
|
@@ -2236,10 +2229,8 @@ Copyright © 2025 37signals LLC
|
|
|
2236
2229
|
oldNode, // start point for iteration
|
|
2237
2230
|
oldNode.nextSibling, // end point for iteration
|
|
2238
2231
|
);
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
childNodes = Array.from(oldParent.childNodes);
|
|
2242
|
-
return childNodes.slice(index, childNodes.length - rightMargin);
|
|
2232
|
+
// this is safe even with siblings, because normalizeParent returns a SlicedParentNode if needed.
|
|
2233
|
+
return Array.from(oldParent.childNodes);
|
|
2243
2234
|
}
|
|
2244
2235
|
|
|
2245
2236
|
/**
|
|
@@ -2268,8 +2259,11 @@ Copyright © 2025 37signals LLC
|
|
|
2268
2259
|
|
|
2269
2260
|
const results = fn();
|
|
2270
2261
|
|
|
2271
|
-
if (
|
|
2272
|
-
|
|
2262
|
+
if (
|
|
2263
|
+
activeElementId &&
|
|
2264
|
+
activeElementId !== document.activeElement?.getAttribute("id")
|
|
2265
|
+
) {
|
|
2266
|
+
activeElement = ctx.target.querySelector(`[id="${activeElementId}"]`);
|
|
2273
2267
|
activeElement?.focus();
|
|
2274
2268
|
}
|
|
2275
2269
|
if (activeElement && !activeElement.selectionEnd && selectionEnd) {
|
|
@@ -2347,17 +2341,23 @@ Copyright © 2025 37signals LLC
|
|
|
2347
2341
|
}
|
|
2348
2342
|
|
|
2349
2343
|
// if the matching node is elsewhere in the original content
|
|
2350
|
-
if (newChild instanceof Element
|
|
2351
|
-
//
|
|
2352
|
-
const
|
|
2353
|
-
|
|
2354
|
-
newChild.id,
|
|
2355
|
-
insertionPoint,
|
|
2356
|
-
ctx,
|
|
2344
|
+
if (newChild instanceof Element) {
|
|
2345
|
+
// we can pretend the id is non-null because the next `.has` line will reject it if not
|
|
2346
|
+
const newChildId = /** @type {String} */ (
|
|
2347
|
+
newChild.getAttribute("id")
|
|
2357
2348
|
);
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2349
|
+
if (ctx.persistentIds.has(newChildId)) {
|
|
2350
|
+
// move it and all its children here and morph
|
|
2351
|
+
const movedChild = moveBeforeById(
|
|
2352
|
+
oldParent,
|
|
2353
|
+
newChildId,
|
|
2354
|
+
insertionPoint,
|
|
2355
|
+
ctx,
|
|
2356
|
+
);
|
|
2357
|
+
morphNode(movedChild, newChild, ctx);
|
|
2358
|
+
insertionPoint = movedChild.nextSibling;
|
|
2359
|
+
continue;
|
|
2360
|
+
}
|
|
2361
2361
|
}
|
|
2362
2362
|
|
|
2363
2363
|
// last resort: insert the new node from scratch
|
|
@@ -2467,7 +2467,8 @@ Copyright © 2025 37signals LLC
|
|
|
2467
2467
|
|
|
2468
2468
|
// if the current node contains active element, stop looking for better future matches,
|
|
2469
2469
|
// because if one is found, this node will be moved to the pantry, reparenting it and thus losing focus
|
|
2470
|
-
|
|
2470
|
+
// @ts-ignore pretend cursor is Element rather than Node, we're just testing for array inclusion
|
|
2471
|
+
if (ctx.activeElementAndParents.includes(cursor)) break;
|
|
2471
2472
|
|
|
2472
2473
|
cursor = cursor.nextSibling;
|
|
2473
2474
|
}
|
|
@@ -2517,7 +2518,9 @@ Copyright © 2025 37signals LLC
|
|
|
2517
2518
|
// If oldElt has an `id` with possible state and it doesn't match newElt.id then avoid morphing.
|
|
2518
2519
|
// We'll still match an anonymous node with an IDed newElt, though, because if it got this far,
|
|
2519
2520
|
// its not persistent, and new nodes can't have any hidden state.
|
|
2520
|
-
|
|
2521
|
+
// 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
|
|
2522
|
+
(!oldElt.getAttribute?.("id") ||
|
|
2523
|
+
oldElt.getAttribute?.("id") === newElt.getAttribute?.("id"))
|
|
2521
2524
|
);
|
|
2522
2525
|
}
|
|
2523
2526
|
|
|
@@ -2581,8 +2584,11 @@ Copyright © 2025 37signals LLC
|
|
|
2581
2584
|
const target =
|
|
2582
2585
|
/** @type {Element} - will always be found */
|
|
2583
2586
|
(
|
|
2584
|
-
ctx.target.
|
|
2585
|
-
|
|
2587
|
+
// ctx.target.id unsafe because of form input shadowing
|
|
2588
|
+
// ctx.target could be a document fragment which doesn't have `getAttribute`
|
|
2589
|
+
(ctx.target.getAttribute?.("id") === id && ctx.target) ||
|
|
2590
|
+
ctx.target.querySelector(`[id="${id}"]`) ||
|
|
2591
|
+
ctx.pantry.querySelector(`[id="${id}"]`)
|
|
2586
2592
|
);
|
|
2587
2593
|
removeElementFromAncestorsIdMaps(target, ctx);
|
|
2588
2594
|
moveBefore(parentNode, target, after);
|
|
@@ -2598,7 +2604,8 @@ Copyright © 2025 37signals LLC
|
|
|
2598
2604
|
* @param {MorphContext} ctx
|
|
2599
2605
|
*/
|
|
2600
2606
|
function removeElementFromAncestorsIdMaps(element, ctx) {
|
|
2601
|
-
|
|
2607
|
+
// we know id is non-null String, because this function is only called on elements with ids
|
|
2608
|
+
const id = /** @type {String} */ (element.getAttribute("id"));
|
|
2602
2609
|
/** @ts-ignore - safe to loop in this way **/
|
|
2603
2610
|
while ((element = element.parentNode)) {
|
|
2604
2611
|
let idSet = ctx.idMap.get(element);
|
|
@@ -3036,6 +3043,7 @@ Copyright © 2025 37signals LLC
|
|
|
3036
3043
|
idMap: idMap,
|
|
3037
3044
|
persistentIds: persistentIds,
|
|
3038
3045
|
pantry: createPantry(),
|
|
3046
|
+
activeElementAndParents: createActiveElementAndParents(oldNode),
|
|
3039
3047
|
callbacks: mergedConfig.callbacks,
|
|
3040
3048
|
head: mergedConfig.head,
|
|
3041
3049
|
};
|
|
@@ -3076,6 +3084,24 @@ Copyright © 2025 37signals LLC
|
|
|
3076
3084
|
return pantry;
|
|
3077
3085
|
}
|
|
3078
3086
|
|
|
3087
|
+
/**
|
|
3088
|
+
* @param {Element} oldNode
|
|
3089
|
+
* @returns {Element[]}
|
|
3090
|
+
*/
|
|
3091
|
+
function createActiveElementAndParents(oldNode) {
|
|
3092
|
+
/** @type {Element[]} */
|
|
3093
|
+
let activeElementAndParents = [];
|
|
3094
|
+
let elt = document.activeElement;
|
|
3095
|
+
if (elt?.tagName !== "BODY" && oldNode.contains(elt)) {
|
|
3096
|
+
while (elt) {
|
|
3097
|
+
activeElementAndParents.push(elt);
|
|
3098
|
+
if (elt === oldNode) break;
|
|
3099
|
+
elt = elt.parentElement;
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
return activeElementAndParents;
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3079
3105
|
/**
|
|
3080
3106
|
* Returns all elements with an ID contained within the root element and its descendants
|
|
3081
3107
|
*
|
|
@@ -3084,7 +3110,8 @@ Copyright © 2025 37signals LLC
|
|
|
3084
3110
|
*/
|
|
3085
3111
|
function findIdElements(root) {
|
|
3086
3112
|
let elements = Array.from(root.querySelectorAll("[id]"));
|
|
3087
|
-
|
|
3113
|
+
// root could be a document fragment which doesn't have `getAttribute`
|
|
3114
|
+
if (root.getAttribute?.("id")) {
|
|
3088
3115
|
elements.push(root);
|
|
3089
3116
|
}
|
|
3090
3117
|
return elements;
|
|
@@ -3103,7 +3130,9 @@ Copyright © 2025 37signals LLC
|
|
|
3103
3130
|
*/
|
|
3104
3131
|
function populateIdMapWithTree(idMap, persistentIds, root, elements) {
|
|
3105
3132
|
for (const elt of elements) {
|
|
3106
|
-
|
|
3133
|
+
// we can pretend id is non-null String, because the .has line will reject it immediately if not
|
|
3134
|
+
const id = /** @type {String} */ (elt.getAttribute("id"));
|
|
3135
|
+
if (persistentIds.has(id)) {
|
|
3107
3136
|
/** @type {Element|null} */
|
|
3108
3137
|
let current = elt;
|
|
3109
3138
|
// walk up the parent hierarchy of that element, adding the id
|
|
@@ -3115,7 +3144,7 @@ Copyright © 2025 37signals LLC
|
|
|
3115
3144
|
idSet = new Set();
|
|
3116
3145
|
idMap.set(current, idSet);
|
|
3117
3146
|
}
|
|
3118
|
-
idSet.add(
|
|
3147
|
+
idSet.add(id);
|
|
3119
3148
|
|
|
3120
3149
|
if (current === root) break;
|
|
3121
3150
|
current = current.parentElement;
|
|
@@ -3229,8 +3258,9 @@ Copyright © 2025 37signals LLC
|
|
|
3229
3258
|
if (newContent.parentNode) {
|
|
3230
3259
|
// we can't use the parent directly because newContent may have siblings
|
|
3231
3260
|
// that we don't want in the morph, and reparenting might be expensive (TODO is it?),
|
|
3232
|
-
// so we create a
|
|
3233
|
-
|
|
3261
|
+
// so instead we create a fake parent node that only sees a slice of its children.
|
|
3262
|
+
/** @type {Element} */
|
|
3263
|
+
return /** @type {any} */ (new SlicedParentNode(newContent));
|
|
3234
3264
|
} else {
|
|
3235
3265
|
// a single node is added as a child to a dummy parent
|
|
3236
3266
|
const dummyParent = document.createElement("div");
|
|
@@ -3249,33 +3279,78 @@ Copyright © 2025 37signals LLC
|
|
|
3249
3279
|
}
|
|
3250
3280
|
|
|
3251
3281
|
/**
|
|
3252
|
-
*
|
|
3282
|
+
* A fake duck-typed parent element to wrap a single node, without actually reparenting it.
|
|
3283
|
+
* This is useful because the node may have siblings that we don't want in the morph, and it may also be moved
|
|
3284
|
+
* or replaced with one or more elements during the morph. This class effectively allows us a window into
|
|
3285
|
+
* a slice of a node's children.
|
|
3253
3286
|
* "If it walks like a duck, and quacks like a duck, then it must be a duck!" -- James Whitcomb Riley (1849–1916)
|
|
3254
|
-
*
|
|
3255
|
-
* @param {Node} newContent
|
|
3256
|
-
* @returns {Element}
|
|
3257
3287
|
*/
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
}
|
|
3278
|
-
|
|
3288
|
+
class SlicedParentNode {
|
|
3289
|
+
/** @param {Node} node */
|
|
3290
|
+
constructor(node) {
|
|
3291
|
+
this.originalNode = node;
|
|
3292
|
+
this.realParentNode = /** @type {Element} */ (node.parentNode);
|
|
3293
|
+
this.previousSibling = node.previousSibling;
|
|
3294
|
+
this.nextSibling = node.nextSibling;
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
/** @returns {Node[]} */
|
|
3298
|
+
get childNodes() {
|
|
3299
|
+
// return slice of realParent's current childNodes, based on previousSibling and nextSibling
|
|
3300
|
+
const nodes = [];
|
|
3301
|
+
let cursor = this.previousSibling
|
|
3302
|
+
? this.previousSibling.nextSibling
|
|
3303
|
+
: this.realParentNode.firstChild;
|
|
3304
|
+
while (cursor && cursor != this.nextSibling) {
|
|
3305
|
+
nodes.push(cursor);
|
|
3306
|
+
cursor = cursor.nextSibling;
|
|
3307
|
+
}
|
|
3308
|
+
return nodes;
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
/**
|
|
3312
|
+
* @param {string} selector
|
|
3313
|
+
* @returns {Element[]}
|
|
3314
|
+
*/
|
|
3315
|
+
querySelectorAll(selector) {
|
|
3316
|
+
return this.childNodes.reduce((results, node) => {
|
|
3317
|
+
if (node instanceof Element) {
|
|
3318
|
+
if (node.matches(selector)) results.push(node);
|
|
3319
|
+
const nodeList = node.querySelectorAll(selector);
|
|
3320
|
+
for (let i = 0; i < nodeList.length; i++) {
|
|
3321
|
+
results.push(nodeList[i]);
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
return results;
|
|
3325
|
+
}, /** @type {Element[]} */ ([]));
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
/**
|
|
3329
|
+
* @param {Node} node
|
|
3330
|
+
* @param {Node} referenceNode
|
|
3331
|
+
* @returns {Node}
|
|
3332
|
+
*/
|
|
3333
|
+
insertBefore(node, referenceNode) {
|
|
3334
|
+
return this.realParentNode.insertBefore(node, referenceNode);
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
/**
|
|
3338
|
+
* @param {Node} node
|
|
3339
|
+
* @param {Node} referenceNode
|
|
3340
|
+
* @returns {Node}
|
|
3341
|
+
*/
|
|
3342
|
+
moveBefore(node, referenceNode) {
|
|
3343
|
+
// @ts-ignore - use new moveBefore feature
|
|
3344
|
+
return this.realParentNode.moveBefore(node, referenceNode);
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
/**
|
|
3348
|
+
* for later use with populateIdMapWithTree to halt upwards iteration
|
|
3349
|
+
* @returns {Node}
|
|
3350
|
+
*/
|
|
3351
|
+
get __idiomorphRoot() {
|
|
3352
|
+
return this.originalNode;
|
|
3353
|
+
}
|
|
3279
3354
|
}
|
|
3280
3355
|
|
|
3281
3356
|
/**
|
|
@@ -3370,16 +3445,18 @@ Copyright © 2025 37signals LLC
|
|
|
3370
3445
|
|
|
3371
3446
|
function shouldRefreshFrameWithMorphing(currentFrame, newFrame) {
|
|
3372
3447
|
return currentFrame instanceof FrameElement &&
|
|
3373
|
-
|
|
3374
|
-
// elements don't get initialized until they're attached to the DOM, so
|
|
3375
|
-
// test its Element#nodeName instead
|
|
3376
|
-
newFrame instanceof Element && newFrame.nodeName === "TURBO-FRAME" &&
|
|
3377
|
-
currentFrame.shouldReloadWithMorph &&
|
|
3378
|
-
currentFrame.id === newFrame.id &&
|
|
3379
|
-
(!newFrame.getAttribute("src") || urlsAreEqual(currentFrame.src, newFrame.getAttribute("src"))) &&
|
|
3448
|
+
currentFrame.shouldReloadWithMorph && (!newFrame || areFramesCompatibleForRefreshing(currentFrame, newFrame)) &&
|
|
3380
3449
|
!currentFrame.closest("[data-turbo-permanent]")
|
|
3381
3450
|
}
|
|
3382
3451
|
|
|
3452
|
+
function areFramesCompatibleForRefreshing(currentFrame, newFrame) {
|
|
3453
|
+
// newFrame cannot yet be an instance of FrameElement because custom
|
|
3454
|
+
// elements don't get initialized until they're attached to the DOM, so
|
|
3455
|
+
// test its Element#nodeName instead
|
|
3456
|
+
return newFrame instanceof Element && newFrame.nodeName === "TURBO-FRAME" && currentFrame.id === newFrame.id &&
|
|
3457
|
+
(!newFrame.getAttribute("src") || urlsAreEqual(currentFrame.src, newFrame.getAttribute("src")))
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3383
3460
|
function closestFrameReloadableWithMorphing(node) {
|
|
3384
3461
|
return node.parentElement.closest("turbo-frame[src][refresh=morph]")
|
|
3385
3462
|
}
|
package/package.json
CHANGED