@hotwired/turbo 8.0.12 → 8.0.14
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/CHANGELOG.md +3 -0
- package/dist/turbo.es2017-esm.js +1367 -802
- package/dist/turbo.es2017-umd.js +1370 -801
- package/package.json +5 -5
package/dist/turbo.es2017-esm.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
Turbo 8.0.
|
|
3
|
-
Copyright ©
|
|
2
|
+
Turbo 8.0.13
|
|
3
|
+
Copyright © 2025 37signals LLC
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
6
|
* The MIT License (MIT)
|
|
@@ -555,7 +555,13 @@ function doesNotTargetIFrame(name) {
|
|
|
555
555
|
}
|
|
556
556
|
|
|
557
557
|
function findLinkFromClickTarget(target) {
|
|
558
|
-
|
|
558
|
+
const link = findClosestRecursively(target, "a[href], a[xlink\\:href]");
|
|
559
|
+
|
|
560
|
+
if (!link) return null
|
|
561
|
+
if (link.hasAttribute("download")) return null
|
|
562
|
+
if (link.hasAttribute("target") && link.target !== "_self") return null
|
|
563
|
+
|
|
564
|
+
return link
|
|
559
565
|
}
|
|
560
566
|
|
|
561
567
|
function getLocationForLink(link) {
|
|
@@ -642,8 +648,8 @@ function getExtension(url) {
|
|
|
642
648
|
}
|
|
643
649
|
|
|
644
650
|
function isPrefixedBy(baseURL, url) {
|
|
645
|
-
const prefix =
|
|
646
|
-
return baseURL.href ===
|
|
651
|
+
const prefix = addTrailingSlash(url.origin + url.pathname);
|
|
652
|
+
return addTrailingSlash(baseURL.href) === prefix || baseURL.href.startsWith(prefix)
|
|
647
653
|
}
|
|
648
654
|
|
|
649
655
|
function locationIsVisitable(location, rootLocation) {
|
|
@@ -671,10 +677,6 @@ function getLastPathComponent(url) {
|
|
|
671
677
|
return getPathComponents(url).slice(-1)[0]
|
|
672
678
|
}
|
|
673
679
|
|
|
674
|
-
function getPrefix(url) {
|
|
675
|
-
return addTrailingSlash(url.origin + url.pathname)
|
|
676
|
-
}
|
|
677
|
-
|
|
678
680
|
function addTrailingSlash(value) {
|
|
679
681
|
return value.endsWith("/") ? value : value + "/"
|
|
680
682
|
}
|
|
@@ -755,15 +757,13 @@ class LimitedSet extends Set {
|
|
|
755
757
|
|
|
756
758
|
const recentRequests = new LimitedSet(20);
|
|
757
759
|
|
|
758
|
-
const nativeFetch = window.fetch;
|
|
759
|
-
|
|
760
760
|
function fetchWithTurboHeaders(url, options = {}) {
|
|
761
761
|
const modifiedHeaders = new Headers(options.headers || {});
|
|
762
762
|
const requestUID = uuid();
|
|
763
763
|
recentRequests.add(requestUID);
|
|
764
764
|
modifiedHeaders.append("X-Turbo-Request-Id", requestUID);
|
|
765
765
|
|
|
766
|
-
return
|
|
766
|
+
return window.fetch(url, {
|
|
767
767
|
...options,
|
|
768
768
|
headers: modifiedHeaders
|
|
769
769
|
})
|
|
@@ -1499,8 +1499,8 @@ class View {
|
|
|
1499
1499
|
scrollToAnchor(anchor) {
|
|
1500
1500
|
const element = this.snapshot.getElementForAnchor(anchor);
|
|
1501
1501
|
if (element) {
|
|
1502
|
-
this.scrollToElement(element);
|
|
1503
1502
|
this.focusElement(element);
|
|
1503
|
+
this.scrollToElement(element);
|
|
1504
1504
|
} else {
|
|
1505
1505
|
this.scrollToPosition({ x: 0, y: 0 });
|
|
1506
1506
|
}
|
|
@@ -2026,839 +2026,1320 @@ function readScrollBehavior(value, defaultValue) {
|
|
|
2026
2026
|
}
|
|
2027
2027
|
}
|
|
2028
2028
|
|
|
2029
|
+
/**
|
|
2030
|
+
* @typedef {object} ConfigHead
|
|
2031
|
+
*
|
|
2032
|
+
* @property {'merge' | 'append' | 'morph' | 'none'} [style]
|
|
2033
|
+
* @property {boolean} [block]
|
|
2034
|
+
* @property {boolean} [ignore]
|
|
2035
|
+
* @property {function(Element): boolean} [shouldPreserve]
|
|
2036
|
+
* @property {function(Element): boolean} [shouldReAppend]
|
|
2037
|
+
* @property {function(Element): boolean} [shouldRemove]
|
|
2038
|
+
* @property {function(Element, {added: Node[], kept: Element[], removed: Element[]}): void} [afterHeadMorphed]
|
|
2039
|
+
*/
|
|
2040
|
+
|
|
2041
|
+
/**
|
|
2042
|
+
* @typedef {object} ConfigCallbacks
|
|
2043
|
+
*
|
|
2044
|
+
* @property {function(Node): boolean} [beforeNodeAdded]
|
|
2045
|
+
* @property {function(Node): void} [afterNodeAdded]
|
|
2046
|
+
* @property {function(Element, Node): boolean} [beforeNodeMorphed]
|
|
2047
|
+
* @property {function(Element, Node): void} [afterNodeMorphed]
|
|
2048
|
+
* @property {function(Element): boolean} [beforeNodeRemoved]
|
|
2049
|
+
* @property {function(Element): void} [afterNodeRemoved]
|
|
2050
|
+
* @property {function(string, Element, "update" | "remove"): boolean} [beforeAttributeUpdated]
|
|
2051
|
+
*/
|
|
2052
|
+
|
|
2053
|
+
/**
|
|
2054
|
+
* @typedef {object} Config
|
|
2055
|
+
*
|
|
2056
|
+
* @property {'outerHTML' | 'innerHTML'} [morphStyle]
|
|
2057
|
+
* @property {boolean} [ignoreActive]
|
|
2058
|
+
* @property {boolean} [ignoreActiveValue]
|
|
2059
|
+
* @property {boolean} [restoreFocus]
|
|
2060
|
+
* @property {ConfigCallbacks} [callbacks]
|
|
2061
|
+
* @property {ConfigHead} [head]
|
|
2062
|
+
*/
|
|
2063
|
+
|
|
2064
|
+
/**
|
|
2065
|
+
* @typedef {function} NoOp
|
|
2066
|
+
*
|
|
2067
|
+
* @returns {void}
|
|
2068
|
+
*/
|
|
2069
|
+
|
|
2070
|
+
/**
|
|
2071
|
+
* @typedef {object} ConfigHeadInternal
|
|
2072
|
+
*
|
|
2073
|
+
* @property {'merge' | 'append' | 'morph' | 'none'} style
|
|
2074
|
+
* @property {boolean} [block]
|
|
2075
|
+
* @property {boolean} [ignore]
|
|
2076
|
+
* @property {(function(Element): boolean) | NoOp} shouldPreserve
|
|
2077
|
+
* @property {(function(Element): boolean) | NoOp} shouldReAppend
|
|
2078
|
+
* @property {(function(Element): boolean) | NoOp} shouldRemove
|
|
2079
|
+
* @property {(function(Element, {added: Node[], kept: Element[], removed: Element[]}): void) | NoOp} afterHeadMorphed
|
|
2080
|
+
*/
|
|
2081
|
+
|
|
2082
|
+
/**
|
|
2083
|
+
* @typedef {object} ConfigCallbacksInternal
|
|
2084
|
+
*
|
|
2085
|
+
* @property {(function(Node): boolean) | NoOp} beforeNodeAdded
|
|
2086
|
+
* @property {(function(Node): void) | NoOp} afterNodeAdded
|
|
2087
|
+
* @property {(function(Node, Node): boolean) | NoOp} beforeNodeMorphed
|
|
2088
|
+
* @property {(function(Node, Node): void) | NoOp} afterNodeMorphed
|
|
2089
|
+
* @property {(function(Node): boolean) | NoOp} beforeNodeRemoved
|
|
2090
|
+
* @property {(function(Node): void) | NoOp} afterNodeRemoved
|
|
2091
|
+
* @property {(function(string, Element, "update" | "remove"): boolean) | NoOp} beforeAttributeUpdated
|
|
2092
|
+
*/
|
|
2093
|
+
|
|
2094
|
+
/**
|
|
2095
|
+
* @typedef {object} ConfigInternal
|
|
2096
|
+
*
|
|
2097
|
+
* @property {'outerHTML' | 'innerHTML'} morphStyle
|
|
2098
|
+
* @property {boolean} [ignoreActive]
|
|
2099
|
+
* @property {boolean} [ignoreActiveValue]
|
|
2100
|
+
* @property {boolean} [restoreFocus]
|
|
2101
|
+
* @property {ConfigCallbacksInternal} callbacks
|
|
2102
|
+
* @property {ConfigHeadInternal} head
|
|
2103
|
+
*/
|
|
2104
|
+
|
|
2105
|
+
/**
|
|
2106
|
+
* @typedef {Object} IdSets
|
|
2107
|
+
* @property {Set<string>} persistentIds
|
|
2108
|
+
* @property {Map<Node, Set<string>>} idMap
|
|
2109
|
+
*/
|
|
2110
|
+
|
|
2111
|
+
/**
|
|
2112
|
+
* @typedef {Function} Morph
|
|
2113
|
+
*
|
|
2114
|
+
* @param {Element | Document} oldNode
|
|
2115
|
+
* @param {Element | Node | HTMLCollection | Node[] | string | null} newContent
|
|
2116
|
+
* @param {Config} [config]
|
|
2117
|
+
* @returns {undefined | Node[]}
|
|
2118
|
+
*/
|
|
2119
|
+
|
|
2029
2120
|
// base IIFE to define idiomorph
|
|
2121
|
+
/**
|
|
2122
|
+
*
|
|
2123
|
+
* @type {{defaults: ConfigInternal, morph: Morph}}
|
|
2124
|
+
*/
|
|
2030
2125
|
var Idiomorph = (function () {
|
|
2031
2126
|
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
},
|
|
2050
|
-
head: {
|
|
2051
|
-
style: 'merge',
|
|
2052
|
-
shouldPreserve: function (elt) {
|
|
2053
|
-
return elt.getAttribute("im-preserve") === "true";
|
|
2054
|
-
},
|
|
2055
|
-
shouldReAppend: function (elt) {
|
|
2056
|
-
return elt.getAttribute("im-re-append") === "true";
|
|
2057
|
-
},
|
|
2058
|
-
shouldRemove: noOp,
|
|
2059
|
-
afterHeadMorphed: noOp,
|
|
2060
|
-
}
|
|
2061
|
-
};
|
|
2127
|
+
/**
|
|
2128
|
+
* @typedef {object} MorphContext
|
|
2129
|
+
*
|
|
2130
|
+
* @property {Element} target
|
|
2131
|
+
* @property {Element} newContent
|
|
2132
|
+
* @property {ConfigInternal} config
|
|
2133
|
+
* @property {ConfigInternal['morphStyle']} morphStyle
|
|
2134
|
+
* @property {ConfigInternal['ignoreActive']} ignoreActive
|
|
2135
|
+
* @property {ConfigInternal['ignoreActiveValue']} ignoreActiveValue
|
|
2136
|
+
* @property {ConfigInternal['restoreFocus']} restoreFocus
|
|
2137
|
+
* @property {Map<Node, Set<string>>} idMap
|
|
2138
|
+
* @property {Set<string>} persistentIds
|
|
2139
|
+
* @property {ConfigInternal['callbacks']} callbacks
|
|
2140
|
+
* @property {ConfigInternal['head']} head
|
|
2141
|
+
* @property {HTMLDivElement} pantry
|
|
2142
|
+
*/
|
|
2062
2143
|
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
function morph(oldNode, newContent, config = {}) {
|
|
2144
|
+
//=============================================================================
|
|
2145
|
+
// AND NOW IT BEGINS...
|
|
2146
|
+
//=============================================================================
|
|
2067
2147
|
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2148
|
+
const noOp = () => {};
|
|
2149
|
+
/**
|
|
2150
|
+
* Default configuration values, updatable by users now
|
|
2151
|
+
* @type {ConfigInternal}
|
|
2152
|
+
*/
|
|
2153
|
+
const defaults = {
|
|
2154
|
+
morphStyle: "outerHTML",
|
|
2155
|
+
callbacks: {
|
|
2156
|
+
beforeNodeAdded: noOp,
|
|
2157
|
+
afterNodeAdded: noOp,
|
|
2158
|
+
beforeNodeMorphed: noOp,
|
|
2159
|
+
afterNodeMorphed: noOp,
|
|
2160
|
+
beforeNodeRemoved: noOp,
|
|
2161
|
+
afterNodeRemoved: noOp,
|
|
2162
|
+
beforeAttributeUpdated: noOp,
|
|
2163
|
+
},
|
|
2164
|
+
head: {
|
|
2165
|
+
style: "merge",
|
|
2166
|
+
shouldPreserve: (elt) => elt.getAttribute("im-preserve") === "true",
|
|
2167
|
+
shouldReAppend: (elt) => elt.getAttribute("im-re-append") === "true",
|
|
2168
|
+
shouldRemove: noOp,
|
|
2169
|
+
afterHeadMorphed: noOp,
|
|
2170
|
+
},
|
|
2171
|
+
restoreFocus: true,
|
|
2172
|
+
};
|
|
2071
2173
|
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2174
|
+
/**
|
|
2175
|
+
* Core idiomorph function for morphing one DOM tree to another
|
|
2176
|
+
*
|
|
2177
|
+
* @param {Element | Document} oldNode
|
|
2178
|
+
* @param {Element | Node | HTMLCollection | Node[] | string | null} newContent
|
|
2179
|
+
* @param {Config} [config]
|
|
2180
|
+
* @returns {Promise<Node[]> | Node[]}
|
|
2181
|
+
*/
|
|
2182
|
+
function morph(oldNode, newContent, config = {}) {
|
|
2183
|
+
oldNode = normalizeElement(oldNode);
|
|
2184
|
+
const newNode = normalizeParent(newContent);
|
|
2185
|
+
const ctx = createMorphContext(oldNode, newNode, config);
|
|
2186
|
+
|
|
2187
|
+
const morphedNodes = saveAndRestoreFocus(ctx, () => {
|
|
2188
|
+
return withHeadBlocking(
|
|
2189
|
+
ctx,
|
|
2190
|
+
oldNode,
|
|
2191
|
+
newNode,
|
|
2192
|
+
/** @param {MorphContext} ctx */ (ctx) => {
|
|
2193
|
+
if (ctx.morphStyle === "innerHTML") {
|
|
2194
|
+
morphChildren(ctx, oldNode, newNode);
|
|
2195
|
+
return Array.from(oldNode.childNodes);
|
|
2196
|
+
} else {
|
|
2197
|
+
return morphOuterHTML(ctx, oldNode, newNode);
|
|
2198
|
+
}
|
|
2199
|
+
},
|
|
2200
|
+
);
|
|
2201
|
+
});
|
|
2075
2202
|
|
|
2076
|
-
|
|
2203
|
+
ctx.pantry.remove();
|
|
2204
|
+
return morphedNodes;
|
|
2205
|
+
}
|
|
2077
2206
|
|
|
2078
|
-
|
|
2207
|
+
/**
|
|
2208
|
+
* Morph just the outerHTML of the oldNode to the newContent
|
|
2209
|
+
* We have to be careful because the oldNode could have siblings which need to be untouched
|
|
2210
|
+
* @param {MorphContext} ctx
|
|
2211
|
+
* @param {Element} oldNode
|
|
2212
|
+
* @param {Element} newNode
|
|
2213
|
+
* @returns {Node[]}
|
|
2214
|
+
*/
|
|
2215
|
+
function morphOuterHTML(ctx, oldNode, newNode) {
|
|
2216
|
+
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
|
+
morphChildren(
|
|
2226
|
+
ctx,
|
|
2227
|
+
oldParent,
|
|
2228
|
+
newNode,
|
|
2229
|
+
// these two optional params are the secret sauce
|
|
2230
|
+
oldNode, // start point for iteration
|
|
2231
|
+
oldNode.nextSibling, // end point for iteration
|
|
2232
|
+
);
|
|
2079
2233
|
|
|
2080
|
-
|
|
2081
|
-
|
|
2234
|
+
// return just the morphed nodes
|
|
2235
|
+
childNodes = Array.from(oldParent.childNodes);
|
|
2236
|
+
return childNodes.slice(index, childNodes.length - rightMargin);
|
|
2237
|
+
}
|
|
2082
2238
|
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
ignore: true
|
|
2095
|
-
}
|
|
2096
|
-
}));
|
|
2097
|
-
});
|
|
2098
|
-
return;
|
|
2099
|
-
}
|
|
2100
|
-
}
|
|
2239
|
+
/**
|
|
2240
|
+
* @param {MorphContext} ctx
|
|
2241
|
+
* @param {Function} fn
|
|
2242
|
+
* @returns {Promise<Node[]> | Node[]}
|
|
2243
|
+
*/
|
|
2244
|
+
function saveAndRestoreFocus(ctx, fn) {
|
|
2245
|
+
if (!ctx.config.restoreFocus) return fn();
|
|
2246
|
+
let activeElement =
|
|
2247
|
+
/** @type {HTMLInputElement|HTMLTextAreaElement|null} */ (
|
|
2248
|
+
document.activeElement
|
|
2249
|
+
);
|
|
2101
2250
|
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2251
|
+
// don't bother if the active element is not an input or textarea
|
|
2252
|
+
if (
|
|
2253
|
+
!(
|
|
2254
|
+
activeElement instanceof HTMLInputElement ||
|
|
2255
|
+
activeElement instanceof HTMLTextAreaElement
|
|
2256
|
+
)
|
|
2257
|
+
) {
|
|
2258
|
+
return fn();
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
const { id: activeElementId, selectionStart, selectionEnd } = activeElement;
|
|
2262
|
+
|
|
2263
|
+
const results = fn();
|
|
2264
|
+
|
|
2265
|
+
if (activeElementId && activeElementId !== document.activeElement?.id) {
|
|
2266
|
+
activeElement = ctx.target.querySelector(`#${activeElementId}`);
|
|
2267
|
+
activeElement?.focus();
|
|
2268
|
+
}
|
|
2269
|
+
if (activeElement && !activeElement.selectionEnd && selectionEnd) {
|
|
2270
|
+
activeElement.setSelectionRange(selectionStart, selectionEnd);
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
return results;
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
const morphChildren = (function () {
|
|
2277
|
+
/**
|
|
2278
|
+
* This is the core algorithm for matching up children. The idea is to use id sets to try to match up
|
|
2279
|
+
* nodes as faithfully as possible. We greedily match, which allows us to keep the algorithm fast, but
|
|
2280
|
+
* by using id sets, we are able to better match up with content deeper in the DOM.
|
|
2281
|
+
*
|
|
2282
|
+
* Basic algorithm:
|
|
2283
|
+
* - for each node in the new content:
|
|
2284
|
+
* - search self and siblings for an id set match, falling back to a soft match
|
|
2285
|
+
* - if match found
|
|
2286
|
+
* - remove any nodes up to the match:
|
|
2287
|
+
* - pantry persistent nodes
|
|
2288
|
+
* - delete the rest
|
|
2289
|
+
* - morph the match
|
|
2290
|
+
* - elsif no match found, and node is persistent
|
|
2291
|
+
* - find its match by querying the old root (future) and pantry (past)
|
|
2292
|
+
* - move it and its children here
|
|
2293
|
+
* - morph it
|
|
2294
|
+
* - else
|
|
2295
|
+
* - create a new node from scratch as a last result
|
|
2296
|
+
*
|
|
2297
|
+
* @param {MorphContext} ctx the merge context
|
|
2298
|
+
* @param {Element} oldParent the old content that we are merging the new content into
|
|
2299
|
+
* @param {Element} newParent the parent element of the new content
|
|
2300
|
+
* @param {Node|null} [insertionPoint] the point in the DOM we start morphing at (defaults to first child)
|
|
2301
|
+
* @param {Node|null} [endPoint] the point in the DOM we stop morphing at (defaults to after last child)
|
|
2302
|
+
*/
|
|
2303
|
+
function morphChildren(
|
|
2304
|
+
ctx,
|
|
2305
|
+
oldParent,
|
|
2306
|
+
newParent,
|
|
2307
|
+
insertionPoint = null,
|
|
2308
|
+
endPoint = null,
|
|
2309
|
+
) {
|
|
2310
|
+
// normalize
|
|
2311
|
+
if (
|
|
2312
|
+
oldParent instanceof HTMLTemplateElement &&
|
|
2313
|
+
newParent instanceof HTMLTemplateElement
|
|
2314
|
+
) {
|
|
2315
|
+
// @ts-ignore we can pretend the DocumentFragment is an Element
|
|
2316
|
+
oldParent = oldParent.content;
|
|
2317
|
+
// @ts-ignore ditto
|
|
2318
|
+
newParent = newParent.content;
|
|
2319
|
+
}
|
|
2320
|
+
insertionPoint ||= oldParent.firstChild;
|
|
2321
|
+
|
|
2322
|
+
// run through all the new content
|
|
2323
|
+
for (const newChild of newParent.childNodes) {
|
|
2324
|
+
// once we reach the end of the old parent content skip to the end and insert the rest
|
|
2325
|
+
if (insertionPoint && insertionPoint != endPoint) {
|
|
2326
|
+
const bestMatch = findBestMatch(
|
|
2327
|
+
ctx,
|
|
2328
|
+
newChild,
|
|
2329
|
+
insertionPoint,
|
|
2330
|
+
endPoint,
|
|
2331
|
+
);
|
|
2332
|
+
if (bestMatch) {
|
|
2333
|
+
// if the node to morph is not at the insertion point then remove/move up to it
|
|
2334
|
+
if (bestMatch !== insertionPoint) {
|
|
2335
|
+
removeNodesBetween(ctx, insertionPoint, bestMatch);
|
|
2130
2336
|
}
|
|
2337
|
+
morphNode(bestMatch, newChild, ctx);
|
|
2338
|
+
insertionPoint = bestMatch.nextSibling;
|
|
2339
|
+
continue;
|
|
2340
|
+
}
|
|
2131
2341
|
}
|
|
2132
2342
|
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2343
|
+
// 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,
|
|
2351
|
+
);
|
|
2352
|
+
morphNode(movedChild, newChild, ctx);
|
|
2353
|
+
insertionPoint = movedChild.nextSibling;
|
|
2354
|
+
continue;
|
|
2141
2355
|
}
|
|
2142
2356
|
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
oldNode.remove();
|
|
2154
|
-
ctx.callbacks.afterNodeRemoved(oldNode);
|
|
2155
|
-
return null;
|
|
2156
|
-
} else if (!isSoftMatch(oldNode, newContent)) {
|
|
2157
|
-
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
|
|
2158
|
-
if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode;
|
|
2159
|
-
|
|
2160
|
-
oldNode.parentElement.replaceChild(newContent, oldNode);
|
|
2161
|
-
ctx.callbacks.afterNodeAdded(newContent);
|
|
2162
|
-
ctx.callbacks.afterNodeRemoved(oldNode);
|
|
2163
|
-
return newContent;
|
|
2164
|
-
} else {
|
|
2165
|
-
if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return oldNode;
|
|
2166
|
-
|
|
2167
|
-
if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) ; else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") {
|
|
2168
|
-
handleHeadElement(newContent, oldNode, ctx);
|
|
2169
|
-
} else {
|
|
2170
|
-
syncNodeFrom(newContent, oldNode, ctx);
|
|
2171
|
-
if (!ignoreValueOfActiveElement(oldNode, ctx)) {
|
|
2172
|
-
morphChildren(newContent, oldNode, ctx);
|
|
2173
|
-
}
|
|
2174
|
-
}
|
|
2175
|
-
ctx.callbacks.afterNodeMorphed(oldNode, newContent);
|
|
2176
|
-
return oldNode;
|
|
2177
|
-
}
|
|
2357
|
+
// last resort: insert the new node from scratch
|
|
2358
|
+
const insertedNode = createNode(
|
|
2359
|
+
oldParent,
|
|
2360
|
+
newChild,
|
|
2361
|
+
insertionPoint,
|
|
2362
|
+
ctx,
|
|
2363
|
+
);
|
|
2364
|
+
// could be null if beforeNodeAdded prevented insertion
|
|
2365
|
+
if (insertedNode) {
|
|
2366
|
+
insertionPoint = insertedNode.nextSibling;
|
|
2178
2367
|
}
|
|
2368
|
+
}
|
|
2179
2369
|
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
* - if we have reached the end of the old parent, append the new content
|
|
2188
|
-
* - if the new content has an id set match with the current insertion point, morph
|
|
2189
|
-
* - search for an id set match
|
|
2190
|
-
* - if id set match found, morph
|
|
2191
|
-
* - otherwise search for a "soft" match
|
|
2192
|
-
* - if a soft match is found, morph
|
|
2193
|
-
* - otherwise, prepend the new node before the current insertion point
|
|
2194
|
-
*
|
|
2195
|
-
* The two search algorithms terminate if competing node matches appear to outweigh what can be achieved
|
|
2196
|
-
* with the current node. See findIdSetMatch() and findSoftMatch() for details.
|
|
2197
|
-
*
|
|
2198
|
-
* @param {Element} newParent the parent element of the new content
|
|
2199
|
-
* @param {Element } oldParent the old content that we are merging the new content into
|
|
2200
|
-
* @param ctx the merge context
|
|
2201
|
-
*/
|
|
2202
|
-
function morphChildren(newParent, oldParent, ctx) {
|
|
2203
|
-
|
|
2204
|
-
let nextNewChild = newParent.firstChild;
|
|
2205
|
-
let insertionPoint = oldParent.firstChild;
|
|
2206
|
-
let newChild;
|
|
2207
|
-
|
|
2208
|
-
// run through all the new content
|
|
2209
|
-
while (nextNewChild) {
|
|
2210
|
-
|
|
2211
|
-
newChild = nextNewChild;
|
|
2212
|
-
nextNewChild = newChild.nextSibling;
|
|
2213
|
-
|
|
2214
|
-
// if we are at the end of the exiting parent's children, just append
|
|
2215
|
-
if (insertionPoint == null) {
|
|
2216
|
-
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
|
2217
|
-
|
|
2218
|
-
oldParent.appendChild(newChild);
|
|
2219
|
-
ctx.callbacks.afterNodeAdded(newChild);
|
|
2220
|
-
removeIdsFromConsideration(ctx, newChild);
|
|
2221
|
-
continue;
|
|
2222
|
-
}
|
|
2223
|
-
|
|
2224
|
-
// if the current node has an id set match then morph
|
|
2225
|
-
if (isIdSetMatch(newChild, insertionPoint, ctx)) {
|
|
2226
|
-
morphOldNodeTo(insertionPoint, newChild, ctx);
|
|
2227
|
-
insertionPoint = insertionPoint.nextSibling;
|
|
2228
|
-
removeIdsFromConsideration(ctx, newChild);
|
|
2229
|
-
continue;
|
|
2230
|
-
}
|
|
2231
|
-
|
|
2232
|
-
// otherwise search forward in the existing old children for an id set match
|
|
2233
|
-
let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx);
|
|
2234
|
-
|
|
2235
|
-
// if we found a potential match, remove the nodes until that point and morph
|
|
2236
|
-
if (idSetMatch) {
|
|
2237
|
-
insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx);
|
|
2238
|
-
morphOldNodeTo(idSetMatch, newChild, ctx);
|
|
2239
|
-
removeIdsFromConsideration(ctx, newChild);
|
|
2240
|
-
continue;
|
|
2241
|
-
}
|
|
2242
|
-
|
|
2243
|
-
// no id set match found, so scan forward for a soft match for the current node
|
|
2244
|
-
let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx);
|
|
2245
|
-
|
|
2246
|
-
// if we found a soft match for the current node, morph
|
|
2247
|
-
if (softMatch) {
|
|
2248
|
-
insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx);
|
|
2249
|
-
morphOldNodeTo(softMatch, newChild, ctx);
|
|
2250
|
-
removeIdsFromConsideration(ctx, newChild);
|
|
2251
|
-
continue;
|
|
2252
|
-
}
|
|
2253
|
-
|
|
2254
|
-
// abandon all hope of morphing, just insert the new child before the insertion point
|
|
2255
|
-
// and move on
|
|
2256
|
-
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
|
2257
|
-
|
|
2258
|
-
oldParent.insertBefore(newChild, insertionPoint);
|
|
2259
|
-
ctx.callbacks.afterNodeAdded(newChild);
|
|
2260
|
-
removeIdsFromConsideration(ctx, newChild);
|
|
2261
|
-
}
|
|
2370
|
+
// remove any remaining old nodes that didn't match up with new content
|
|
2371
|
+
while (insertionPoint && insertionPoint != endPoint) {
|
|
2372
|
+
const tempNode = insertionPoint;
|
|
2373
|
+
insertionPoint = insertionPoint.nextSibling;
|
|
2374
|
+
removeNode(ctx, tempNode);
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2262
2377
|
|
|
2263
|
-
|
|
2264
|
-
|
|
2378
|
+
/**
|
|
2379
|
+
* This performs the action of inserting a new node while handling situations where the node contains
|
|
2380
|
+
* elements with persistent ids and possible state info we can still preserve by moving in and then morphing
|
|
2381
|
+
*
|
|
2382
|
+
* @param {Element} oldParent
|
|
2383
|
+
* @param {Node} newChild
|
|
2384
|
+
* @param {Node|null} insertionPoint
|
|
2385
|
+
* @param {MorphContext} ctx
|
|
2386
|
+
* @returns {Node|null}
|
|
2387
|
+
*/
|
|
2388
|
+
function createNode(oldParent, newChild, insertionPoint, ctx) {
|
|
2389
|
+
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return null;
|
|
2390
|
+
if (ctx.idMap.has(newChild)) {
|
|
2391
|
+
// node has children with ids with possible state so create a dummy elt of same type and apply full morph algorithm
|
|
2392
|
+
const newEmptyChild = document.createElement(
|
|
2393
|
+
/** @type {Element} */ (newChild).tagName,
|
|
2394
|
+
);
|
|
2395
|
+
oldParent.insertBefore(newEmptyChild, insertionPoint);
|
|
2396
|
+
morphNode(newEmptyChild, newChild, ctx);
|
|
2397
|
+
ctx.callbacks.afterNodeAdded(newEmptyChild);
|
|
2398
|
+
return newEmptyChild;
|
|
2399
|
+
} else {
|
|
2400
|
+
// optimisation: no id state to preserve so we can just insert a clone of the newChild and its descendants
|
|
2401
|
+
const newClonedChild = document.importNode(newChild, true); // importNode to not mutate newParent
|
|
2402
|
+
oldParent.insertBefore(newClonedChild, insertionPoint);
|
|
2403
|
+
ctx.callbacks.afterNodeAdded(newClonedChild);
|
|
2404
|
+
return newClonedChild;
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2265
2407
|
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2408
|
+
//=============================================================================
|
|
2409
|
+
// Matching Functions
|
|
2410
|
+
//=============================================================================
|
|
2411
|
+
const findBestMatch = (function () {
|
|
2412
|
+
/**
|
|
2413
|
+
* Scans forward from the startPoint to the endPoint looking for a match
|
|
2414
|
+
* for the node. It looks for an id set match first, then a soft match.
|
|
2415
|
+
* We abort softmatching if we find two future soft matches, to reduce churn.
|
|
2416
|
+
* @param {Node} node
|
|
2417
|
+
* @param {MorphContext} ctx
|
|
2418
|
+
* @param {Node | null} startPoint
|
|
2419
|
+
* @param {Node | null} endPoint
|
|
2420
|
+
* @returns {Node | null}
|
|
2421
|
+
*/
|
|
2422
|
+
function findBestMatch(ctx, node, startPoint, endPoint) {
|
|
2423
|
+
let softMatch = null;
|
|
2424
|
+
let nextSibling = node.nextSibling;
|
|
2425
|
+
let siblingSoftMatchCount = 0;
|
|
2426
|
+
|
|
2427
|
+
let cursor = startPoint;
|
|
2428
|
+
while (cursor && cursor != endPoint) {
|
|
2429
|
+
// soft matching is a prerequisite for id set matching
|
|
2430
|
+
if (isSoftMatch(cursor, node)) {
|
|
2431
|
+
if (isIdSetMatch(ctx, cursor, node)) {
|
|
2432
|
+
return cursor; // found an id set match, we're done!
|
|
2269
2433
|
}
|
|
2270
|
-
}
|
|
2271
2434
|
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
* @param updateType {("update"|"remove")}
|
|
2280
|
-
* @param ctx the merge context
|
|
2281
|
-
* @returns {boolean} true if the attribute should be ignored, false otherwise
|
|
2282
|
-
*/
|
|
2283
|
-
function ignoreAttribute(attr, to, updateType, ctx) {
|
|
2284
|
-
if(attr === 'value' && ctx.ignoreActiveValue && to === document.activeElement){
|
|
2285
|
-
return true;
|
|
2435
|
+
// we haven't yet saved a soft match fallback
|
|
2436
|
+
if (softMatch === null) {
|
|
2437
|
+
// the current soft match will hard match something else in the future, leave it
|
|
2438
|
+
if (!ctx.idMap.has(cursor)) {
|
|
2439
|
+
// save this as the fallback if we get through the loop without finding a hard match
|
|
2440
|
+
softMatch = cursor;
|
|
2441
|
+
}
|
|
2286
2442
|
}
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
if (type === 1 /* element type */) {
|
|
2304
|
-
const fromAttributes = from.attributes;
|
|
2305
|
-
const toAttributes = to.attributes;
|
|
2306
|
-
for (const fromAttribute of fromAttributes) {
|
|
2307
|
-
if (ignoreAttribute(fromAttribute.name, to, 'update', ctx)) {
|
|
2308
|
-
continue;
|
|
2309
|
-
}
|
|
2310
|
-
if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) {
|
|
2311
|
-
to.setAttribute(fromAttribute.name, fromAttribute.value);
|
|
2312
|
-
}
|
|
2313
|
-
}
|
|
2314
|
-
// iterate backwards to avoid skipping over items when a delete occurs
|
|
2315
|
-
for (let i = toAttributes.length - 1; 0 <= i; i--) {
|
|
2316
|
-
const toAttribute = toAttributes[i];
|
|
2317
|
-
if (ignoreAttribute(toAttribute.name, to, 'remove', ctx)) {
|
|
2318
|
-
continue;
|
|
2319
|
-
}
|
|
2320
|
-
if (!from.hasAttribute(toAttribute.name)) {
|
|
2321
|
-
to.removeAttribute(toAttribute.name);
|
|
2322
|
-
}
|
|
2323
|
-
}
|
|
2443
|
+
}
|
|
2444
|
+
if (
|
|
2445
|
+
softMatch === null &&
|
|
2446
|
+
nextSibling &&
|
|
2447
|
+
isSoftMatch(cursor, nextSibling)
|
|
2448
|
+
) {
|
|
2449
|
+
// The next new node has a soft match with this node, so
|
|
2450
|
+
// increment the count of future soft matches
|
|
2451
|
+
siblingSoftMatchCount++;
|
|
2452
|
+
nextSibling = nextSibling.nextSibling;
|
|
2453
|
+
|
|
2454
|
+
// If there are two future soft matches, block soft matching for this node to allow
|
|
2455
|
+
// future siblings to soft match. This is to reduce churn in the DOM when an element
|
|
2456
|
+
// is prepended.
|
|
2457
|
+
if (siblingSoftMatchCount >= 2) {
|
|
2458
|
+
softMatch = undefined;
|
|
2324
2459
|
}
|
|
2460
|
+
}
|
|
2325
2461
|
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
to.nodeValue = from.nodeValue;
|
|
2330
|
-
}
|
|
2331
|
-
}
|
|
2462
|
+
// if the current node contains active element, stop looking for better future matches,
|
|
2463
|
+
// 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;
|
|
2332
2465
|
|
|
2333
|
-
|
|
2334
|
-
// sync input values
|
|
2335
|
-
syncInputValue(from, to, ctx);
|
|
2336
|
-
}
|
|
2466
|
+
cursor = cursor.nextSibling;
|
|
2337
2467
|
}
|
|
2338
2468
|
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
* @param to {Element} element to sync the value to
|
|
2342
|
-
* @param attributeName {String} the attribute name
|
|
2343
|
-
* @param ctx the merge context
|
|
2344
|
-
*/
|
|
2345
|
-
function syncBooleanAttribute(from, to, attributeName, ctx) {
|
|
2346
|
-
if (from[attributeName] !== to[attributeName]) {
|
|
2347
|
-
let ignoreUpdate = ignoreAttribute(attributeName, to, 'update', ctx);
|
|
2348
|
-
if (!ignoreUpdate) {
|
|
2349
|
-
to[attributeName] = from[attributeName];
|
|
2350
|
-
}
|
|
2351
|
-
if (from[attributeName]) {
|
|
2352
|
-
if (!ignoreUpdate) {
|
|
2353
|
-
to.setAttribute(attributeName, from[attributeName]);
|
|
2354
|
-
}
|
|
2355
|
-
} else {
|
|
2356
|
-
if (!ignoreAttribute(attributeName, to, 'remove', ctx)) {
|
|
2357
|
-
to.removeAttribute(attributeName);
|
|
2358
|
-
}
|
|
2359
|
-
}
|
|
2360
|
-
}
|
|
2361
|
-
}
|
|
2469
|
+
return softMatch || null;
|
|
2470
|
+
}
|
|
2362
2471
|
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
if (!from.hasAttribute('value')) {
|
|
2386
|
-
if (!ignoreAttribute('value', to, 'remove', ctx)) {
|
|
2387
|
-
to.value = '';
|
|
2388
|
-
to.removeAttribute('value');
|
|
2389
|
-
}
|
|
2390
|
-
} else if (fromValue !== toValue) {
|
|
2391
|
-
if (!ignoreAttribute('value', to, 'update', ctx)) {
|
|
2392
|
-
to.setAttribute('value', fromValue);
|
|
2393
|
-
to.value = fromValue;
|
|
2394
|
-
}
|
|
2395
|
-
}
|
|
2396
|
-
} else if (from instanceof HTMLOptionElement) {
|
|
2397
|
-
syncBooleanAttribute(from, to, 'selected', ctx);
|
|
2398
|
-
} else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) {
|
|
2399
|
-
let fromValue = from.value;
|
|
2400
|
-
let toValue = to.value;
|
|
2401
|
-
if (ignoreAttribute('value', to, 'update', ctx)) {
|
|
2402
|
-
return;
|
|
2403
|
-
}
|
|
2404
|
-
if (fromValue !== toValue) {
|
|
2405
|
-
to.value = fromValue;
|
|
2406
|
-
}
|
|
2407
|
-
if (to.firstChild && to.firstChild.nodeValue !== fromValue) {
|
|
2408
|
-
to.firstChild.nodeValue = fromValue;
|
|
2409
|
-
}
|
|
2410
|
-
}
|
|
2472
|
+
/**
|
|
2473
|
+
*
|
|
2474
|
+
* @param {MorphContext} ctx
|
|
2475
|
+
* @param {Node} oldNode
|
|
2476
|
+
* @param {Node} newNode
|
|
2477
|
+
* @returns {boolean}
|
|
2478
|
+
*/
|
|
2479
|
+
function isIdSetMatch(ctx, oldNode, newNode) {
|
|
2480
|
+
let oldSet = ctx.idMap.get(oldNode);
|
|
2481
|
+
let newSet = ctx.idMap.get(newNode);
|
|
2482
|
+
|
|
2483
|
+
if (!newSet || !oldSet) return false;
|
|
2484
|
+
|
|
2485
|
+
for (const id of oldSet) {
|
|
2486
|
+
// a potential match is an id in the new and old nodes that
|
|
2487
|
+
// has not already been merged into the DOM
|
|
2488
|
+
// But the newNode content we call this on has not been
|
|
2489
|
+
// merged yet and we don't allow duplicate IDs so it is simple
|
|
2490
|
+
if (newSet.has(id)) {
|
|
2491
|
+
return true;
|
|
2492
|
+
}
|
|
2411
2493
|
}
|
|
2494
|
+
return false;
|
|
2495
|
+
}
|
|
2412
2496
|
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2497
|
+
/**
|
|
2498
|
+
*
|
|
2499
|
+
* @param {Node} oldNode
|
|
2500
|
+
* @param {Node} newNode
|
|
2501
|
+
* @returns {boolean}
|
|
2502
|
+
*/
|
|
2503
|
+
function isSoftMatch(oldNode, newNode) {
|
|
2504
|
+
// ok to cast: if one is not element, `id` and `tagName` will be undefined and we'll just compare that.
|
|
2505
|
+
const oldElt = /** @type {Element} */ (oldNode);
|
|
2506
|
+
const newElt = /** @type {Element} */ (newNode);
|
|
2507
|
+
|
|
2508
|
+
return (
|
|
2509
|
+
oldElt.nodeType === newElt.nodeType &&
|
|
2510
|
+
oldElt.tagName === newElt.tagName &&
|
|
2511
|
+
// If oldElt has an `id` with possible state and it doesn't match newElt.id then avoid morphing.
|
|
2512
|
+
// We'll still match an anonymous node with an IDed newElt, though, because if it got this far,
|
|
2513
|
+
// its not persistent, and new nodes can't have any hidden state.
|
|
2514
|
+
(!oldElt.id || oldElt.id === newElt.id)
|
|
2515
|
+
);
|
|
2516
|
+
}
|
|
2417
2517
|
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
let preserved = [];
|
|
2421
|
-
let nodesToAppend = [];
|
|
2518
|
+
return findBestMatch;
|
|
2519
|
+
})();
|
|
2422
2520
|
|
|
2423
|
-
|
|
2521
|
+
//=============================================================================
|
|
2522
|
+
// DOM Manipulation Functions
|
|
2523
|
+
//=============================================================================
|
|
2524
|
+
|
|
2525
|
+
/**
|
|
2526
|
+
* Gets rid of an unwanted DOM node; strategy depends on nature of its reuse:
|
|
2527
|
+
* - Persistent nodes will be moved to the pantry for later reuse
|
|
2528
|
+
* - Other nodes will have their hooks called, and then are removed
|
|
2529
|
+
* @param {MorphContext} ctx
|
|
2530
|
+
* @param {Node} node
|
|
2531
|
+
*/
|
|
2532
|
+
function removeNode(ctx, node) {
|
|
2533
|
+
// are we going to id set match this later?
|
|
2534
|
+
if (ctx.idMap.has(node)) {
|
|
2535
|
+
// skip callbacks and move to pantry
|
|
2536
|
+
moveBefore(ctx.pantry, node, null);
|
|
2537
|
+
} else {
|
|
2538
|
+
// remove for realsies
|
|
2539
|
+
if (ctx.callbacks.beforeNodeRemoved(node) === false) return;
|
|
2540
|
+
node.parentNode?.removeChild(node);
|
|
2541
|
+
ctx.callbacks.afterNodeRemoved(node);
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2424
2544
|
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2545
|
+
/**
|
|
2546
|
+
* Remove nodes between the start and end nodes
|
|
2547
|
+
* @param {MorphContext} ctx
|
|
2548
|
+
* @param {Node} startInclusive
|
|
2549
|
+
* @param {Node} endExclusive
|
|
2550
|
+
* @returns {Node|null}
|
|
2551
|
+
*/
|
|
2552
|
+
function removeNodesBetween(ctx, startInclusive, endExclusive) {
|
|
2553
|
+
/** @type {Node | null} */
|
|
2554
|
+
let cursor = startInclusive;
|
|
2555
|
+
// remove nodes until the endExclusive node
|
|
2556
|
+
while (cursor && cursor !== endExclusive) {
|
|
2557
|
+
let tempNode = /** @type {Node} */ (cursor);
|
|
2558
|
+
cursor = cursor.nextSibling;
|
|
2559
|
+
removeNode(ctx, tempNode);
|
|
2560
|
+
}
|
|
2561
|
+
return cursor;
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
/**
|
|
2565
|
+
* Search for an element by id within the document and pantry, and move it using moveBefore.
|
|
2566
|
+
*
|
|
2567
|
+
* @param {Element} parentNode - The parent node to which the element will be moved.
|
|
2568
|
+
* @param {string} id - The ID of the element to be moved.
|
|
2569
|
+
* @param {Node | null} after - The reference node to insert the element before.
|
|
2570
|
+
* If `null`, the element is appended as the last child.
|
|
2571
|
+
* @param {MorphContext} ctx
|
|
2572
|
+
* @returns {Element} The found element
|
|
2573
|
+
*/
|
|
2574
|
+
function moveBeforeById(parentNode, id, after, ctx) {
|
|
2575
|
+
const target =
|
|
2576
|
+
/** @type {Element} - will always be found */
|
|
2577
|
+
(
|
|
2578
|
+
ctx.target.querySelector(`#${id}`) ||
|
|
2579
|
+
ctx.pantry.querySelector(`#${id}`)
|
|
2580
|
+
);
|
|
2581
|
+
removeElementFromAncestorsIdMaps(target, ctx);
|
|
2582
|
+
moveBefore(parentNode, target, after);
|
|
2583
|
+
return target;
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
/**
|
|
2587
|
+
* Removes an element from its ancestors' id maps. This is needed when an element is moved from the
|
|
2588
|
+
* "future" via `moveBeforeId`. Otherwise, its erstwhile ancestors could be mistakenly moved to the
|
|
2589
|
+
* pantry rather than being deleted, preventing their removal hooks from being called.
|
|
2590
|
+
*
|
|
2591
|
+
* @param {Element} element - element to remove from its ancestors' id maps
|
|
2592
|
+
* @param {MorphContext} ctx
|
|
2593
|
+
*/
|
|
2594
|
+
function removeElementFromAncestorsIdMaps(element, ctx) {
|
|
2595
|
+
const id = element.id;
|
|
2596
|
+
/** @ts-ignore - safe to loop in this way **/
|
|
2597
|
+
while ((element = element.parentNode)) {
|
|
2598
|
+
let idSet = ctx.idMap.get(element);
|
|
2599
|
+
if (idSet) {
|
|
2600
|
+
idSet.delete(id);
|
|
2601
|
+
if (!idSet.size) {
|
|
2602
|
+
ctx.idMap.delete(element);
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2430
2607
|
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
}
|
|
2456
|
-
} else {
|
|
2457
|
-
// if this is a merge, we remove this content since it is not in the new head
|
|
2458
|
-
if (ctx.head.shouldRemove(currentHeadElt) !== false) {
|
|
2459
|
-
removed.push(currentHeadElt);
|
|
2460
|
-
}
|
|
2461
|
-
}
|
|
2462
|
-
}
|
|
2463
|
-
}
|
|
2608
|
+
/**
|
|
2609
|
+
* Moves an element before another element within the same parent.
|
|
2610
|
+
* Uses the proposed `moveBefore` API if available (and working), otherwise falls back to `insertBefore`.
|
|
2611
|
+
* This is essentialy a forward-compat wrapper.
|
|
2612
|
+
*
|
|
2613
|
+
* @param {Element} parentNode - The parent node containing the after element.
|
|
2614
|
+
* @param {Node} element - The element to be moved.
|
|
2615
|
+
* @param {Node | null} after - The reference node to insert `element` before.
|
|
2616
|
+
* If `null`, `element` is appended as the last child.
|
|
2617
|
+
*/
|
|
2618
|
+
function moveBefore(parentNode, element, after) {
|
|
2619
|
+
// @ts-ignore - use proposed moveBefore feature
|
|
2620
|
+
if (parentNode.moveBefore) {
|
|
2621
|
+
try {
|
|
2622
|
+
// @ts-ignore - use proposed moveBefore feature
|
|
2623
|
+
parentNode.moveBefore(element, after);
|
|
2624
|
+
} catch (e) {
|
|
2625
|
+
// fall back to insertBefore as some browsers may fail on moveBefore when trying to move Dom disconnected nodes to pantry
|
|
2626
|
+
parentNode.insertBefore(element, after);
|
|
2627
|
+
}
|
|
2628
|
+
} else {
|
|
2629
|
+
parentNode.insertBefore(element, after);
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2464
2632
|
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
currentHead.appendChild(newElt);
|
|
2484
|
-
ctx.callbacks.afterNodeAdded(newElt);
|
|
2485
|
-
added.push(newElt);
|
|
2486
|
-
}
|
|
2487
|
-
}
|
|
2633
|
+
return morphChildren;
|
|
2634
|
+
})();
|
|
2635
|
+
|
|
2636
|
+
//=============================================================================
|
|
2637
|
+
// Single Node Morphing Code
|
|
2638
|
+
//=============================================================================
|
|
2639
|
+
const morphNode = (function () {
|
|
2640
|
+
/**
|
|
2641
|
+
* @param {Node} oldNode root node to merge content into
|
|
2642
|
+
* @param {Node} newContent new content to merge
|
|
2643
|
+
* @param {MorphContext} ctx the merge context
|
|
2644
|
+
* @returns {Node | null} the element that ended up in the DOM
|
|
2645
|
+
*/
|
|
2646
|
+
function morphNode(oldNode, newContent, ctx) {
|
|
2647
|
+
if (ctx.ignoreActive && oldNode === document.activeElement) {
|
|
2648
|
+
// don't morph focused element
|
|
2649
|
+
return null;
|
|
2650
|
+
}
|
|
2488
2651
|
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
|
|
2493
|
-
currentHead.removeChild(removedElement);
|
|
2494
|
-
ctx.callbacks.afterNodeRemoved(removedElement);
|
|
2495
|
-
}
|
|
2496
|
-
}
|
|
2652
|
+
if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) {
|
|
2653
|
+
return oldNode;
|
|
2654
|
+
}
|
|
2497
2655
|
|
|
2498
|
-
|
|
2499
|
-
|
|
2656
|
+
if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) ; else if (
|
|
2657
|
+
oldNode instanceof HTMLHeadElement &&
|
|
2658
|
+
ctx.head.style !== "morph"
|
|
2659
|
+
) {
|
|
2660
|
+
// ok to cast: if newContent wasn't also a <head>, it would've got caught in the `!isSoftMatch` branch above
|
|
2661
|
+
handleHeadElement(
|
|
2662
|
+
oldNode,
|
|
2663
|
+
/** @type {HTMLHeadElement} */ (newContent),
|
|
2664
|
+
ctx,
|
|
2665
|
+
);
|
|
2666
|
+
} else {
|
|
2667
|
+
morphAttributes(oldNode, newContent, ctx);
|
|
2668
|
+
if (!ignoreValueOfActiveElement(oldNode, ctx)) {
|
|
2669
|
+
// @ts-ignore newContent can be a node here because .firstChild will be null
|
|
2670
|
+
morphChildren(ctx, oldNode, newContent);
|
|
2500
2671
|
}
|
|
2501
|
-
|
|
2502
|
-
|
|
2672
|
+
}
|
|
2673
|
+
ctx.callbacks.afterNodeMorphed(oldNode, newContent);
|
|
2674
|
+
return oldNode;
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
/**
|
|
2678
|
+
* syncs the oldNode to the newNode, copying over all attributes and
|
|
2679
|
+
* inner element state from the newNode to the oldNode
|
|
2680
|
+
*
|
|
2681
|
+
* @param {Node} oldNode the node to copy attributes & state to
|
|
2682
|
+
* @param {Node} newNode the node to copy attributes & state from
|
|
2683
|
+
* @param {MorphContext} ctx the merge context
|
|
2684
|
+
*/
|
|
2685
|
+
function morphAttributes(oldNode, newNode, ctx) {
|
|
2686
|
+
let type = newNode.nodeType;
|
|
2687
|
+
|
|
2688
|
+
// if is an element type, sync the attributes from the
|
|
2689
|
+
// new node into the new node
|
|
2690
|
+
if (type === 1 /* element type */) {
|
|
2691
|
+
const oldElt = /** @type {Element} */ (oldNode);
|
|
2692
|
+
const newElt = /** @type {Element} */ (newNode);
|
|
2693
|
+
|
|
2694
|
+
const oldAttributes = oldElt.attributes;
|
|
2695
|
+
const newAttributes = newElt.attributes;
|
|
2696
|
+
for (const newAttribute of newAttributes) {
|
|
2697
|
+
if (ignoreAttribute(newAttribute.name, oldElt, "update", ctx)) {
|
|
2698
|
+
continue;
|
|
2699
|
+
}
|
|
2700
|
+
if (oldElt.getAttribute(newAttribute.name) !== newAttribute.value) {
|
|
2701
|
+
oldElt.setAttribute(newAttribute.name, newAttribute.value);
|
|
2702
|
+
}
|
|
2503
2703
|
}
|
|
2704
|
+
// iterate backwards to avoid skipping over items when a delete occurs
|
|
2705
|
+
for (let i = oldAttributes.length - 1; 0 <= i; i--) {
|
|
2706
|
+
const oldAttribute = oldAttributes[i];
|
|
2504
2707
|
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
*/
|
|
2509
|
-
function mergeDefaults(config) {
|
|
2510
|
-
let finalConfig = {};
|
|
2511
|
-
// copy top level stuff into final config
|
|
2512
|
-
Object.assign(finalConfig, defaults);
|
|
2513
|
-
Object.assign(finalConfig, config);
|
|
2514
|
-
|
|
2515
|
-
// copy callbacks into final config (do this to deep merge the callbacks)
|
|
2516
|
-
finalConfig.callbacks = {};
|
|
2517
|
-
Object.assign(finalConfig.callbacks, defaults.callbacks);
|
|
2518
|
-
Object.assign(finalConfig.callbacks, config.callbacks);
|
|
2519
|
-
|
|
2520
|
-
// copy head config into final config (do this to deep merge the head)
|
|
2521
|
-
finalConfig.head = {};
|
|
2522
|
-
Object.assign(finalConfig.head, defaults.head);
|
|
2523
|
-
Object.assign(finalConfig.head, config.head);
|
|
2524
|
-
return finalConfig;
|
|
2525
|
-
}
|
|
2708
|
+
// toAttributes is a live NamedNodeMap, so iteration+mutation is unsafe
|
|
2709
|
+
// e.g. custom element attribute callbacks can remove other attributes
|
|
2710
|
+
if (!oldAttribute) continue;
|
|
2526
2711
|
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
target: oldNode,
|
|
2531
|
-
newContent: newContent,
|
|
2532
|
-
config: config,
|
|
2533
|
-
morphStyle: config.morphStyle,
|
|
2534
|
-
ignoreActive: config.ignoreActive,
|
|
2535
|
-
ignoreActiveValue: config.ignoreActiveValue,
|
|
2536
|
-
idMap: createIdMap(oldNode, newContent),
|
|
2537
|
-
deadIds: new Set(),
|
|
2538
|
-
callbacks: config.callbacks,
|
|
2539
|
-
head: config.head
|
|
2712
|
+
if (!newElt.hasAttribute(oldAttribute.name)) {
|
|
2713
|
+
if (ignoreAttribute(oldAttribute.name, oldElt, "remove", ctx)) {
|
|
2714
|
+
continue;
|
|
2540
2715
|
}
|
|
2716
|
+
oldElt.removeAttribute(oldAttribute.name);
|
|
2717
|
+
}
|
|
2541
2718
|
}
|
|
2542
2719
|
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
return false;
|
|
2546
|
-
}
|
|
2547
|
-
if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) {
|
|
2548
|
-
if (node1.id !== "" && node1.id === node2.id) {
|
|
2549
|
-
return true;
|
|
2550
|
-
} else {
|
|
2551
|
-
return getIdIntersectionCount(ctx, node1, node2) > 0;
|
|
2552
|
-
}
|
|
2553
|
-
}
|
|
2554
|
-
return false;
|
|
2720
|
+
if (!ignoreValueOfActiveElement(oldElt, ctx)) {
|
|
2721
|
+
syncInputValue(oldElt, newElt, ctx);
|
|
2555
2722
|
}
|
|
2723
|
+
}
|
|
2556
2724
|
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName
|
|
2725
|
+
// sync text nodes
|
|
2726
|
+
if (type === 8 /* comment */ || type === 3 /* text */) {
|
|
2727
|
+
if (oldNode.nodeValue !== newNode.nodeValue) {
|
|
2728
|
+
oldNode.nodeValue = newNode.nodeValue;
|
|
2562
2729
|
}
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2563
2732
|
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2733
|
+
/**
|
|
2734
|
+
* NB: many bothans died to bring us information:
|
|
2735
|
+
*
|
|
2736
|
+
* https://github.com/patrick-steele-idem/morphdom/blob/master/src/specialElHandlers.js
|
|
2737
|
+
* https://github.com/choojs/nanomorph/blob/master/lib/morph.jsL113
|
|
2738
|
+
*
|
|
2739
|
+
* @param {Element} oldElement the element to sync the input value to
|
|
2740
|
+
* @param {Element} newElement the element to sync the input value from
|
|
2741
|
+
* @param {MorphContext} ctx the merge context
|
|
2742
|
+
*/
|
|
2743
|
+
function syncInputValue(oldElement, newElement, ctx) {
|
|
2744
|
+
if (
|
|
2745
|
+
oldElement instanceof HTMLInputElement &&
|
|
2746
|
+
newElement instanceof HTMLInputElement &&
|
|
2747
|
+
newElement.type !== "file"
|
|
2748
|
+
) {
|
|
2749
|
+
let newValue = newElement.value;
|
|
2750
|
+
let oldValue = oldElement.value;
|
|
2751
|
+
|
|
2752
|
+
// sync boolean attributes
|
|
2753
|
+
syncBooleanAttribute(oldElement, newElement, "checked", ctx);
|
|
2754
|
+
syncBooleanAttribute(oldElement, newElement, "disabled", ctx);
|
|
2755
|
+
|
|
2756
|
+
if (!newElement.hasAttribute("value")) {
|
|
2757
|
+
if (!ignoreAttribute("value", oldElement, "remove", ctx)) {
|
|
2758
|
+
oldElement.value = "";
|
|
2759
|
+
oldElement.removeAttribute("value");
|
|
2760
|
+
}
|
|
2761
|
+
} else if (oldValue !== newValue) {
|
|
2762
|
+
if (!ignoreAttribute("value", oldElement, "update", ctx)) {
|
|
2763
|
+
oldElement.setAttribute("value", newValue);
|
|
2764
|
+
oldElement.value = newValue;
|
|
2765
|
+
}
|
|
2572
2766
|
}
|
|
2767
|
+
// TODO: QUESTION(1cg): this used to only check `newElement` unlike the other branches -- why?
|
|
2768
|
+
// did I break something?
|
|
2769
|
+
} else if (
|
|
2770
|
+
oldElement instanceof HTMLOptionElement &&
|
|
2771
|
+
newElement instanceof HTMLOptionElement
|
|
2772
|
+
) {
|
|
2773
|
+
syncBooleanAttribute(oldElement, newElement, "selected", ctx);
|
|
2774
|
+
} else if (
|
|
2775
|
+
oldElement instanceof HTMLTextAreaElement &&
|
|
2776
|
+
newElement instanceof HTMLTextAreaElement
|
|
2777
|
+
) {
|
|
2778
|
+
let newValue = newElement.value;
|
|
2779
|
+
let oldValue = oldElement.value;
|
|
2780
|
+
if (ignoreAttribute("value", oldElement, "update", ctx)) {
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2783
|
+
if (newValue !== oldValue) {
|
|
2784
|
+
oldElement.value = newValue;
|
|
2785
|
+
}
|
|
2786
|
+
if (
|
|
2787
|
+
oldElement.firstChild &&
|
|
2788
|
+
oldElement.firstChild.nodeValue !== newValue
|
|
2789
|
+
) {
|
|
2790
|
+
oldElement.firstChild.nodeValue = newValue;
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2573
2794
|
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
// If we have an id match, return the current potential match
|
|
2598
|
-
if (isIdSetMatch(newChild, potentialMatch, ctx)) {
|
|
2599
|
-
return potentialMatch;
|
|
2600
|
-
}
|
|
2601
|
-
|
|
2602
|
-
// computer the other potential matches of this new content
|
|
2603
|
-
otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent);
|
|
2604
|
-
if (otherMatchCount > newChildPotentialIdCount) {
|
|
2605
|
-
// if we have more potential id matches in _other_ content, we
|
|
2606
|
-
// do not have a good candidate for an id match, so return null
|
|
2607
|
-
return null;
|
|
2608
|
-
}
|
|
2609
|
-
|
|
2610
|
-
// advanced to the next old content child
|
|
2611
|
-
potentialMatch = potentialMatch.nextSibling;
|
|
2612
|
-
}
|
|
2613
|
-
}
|
|
2614
|
-
return potentialMatch;
|
|
2795
|
+
/**
|
|
2796
|
+
* @param {Element} oldElement element to write the value to
|
|
2797
|
+
* @param {Element} newElement element to read the value from
|
|
2798
|
+
* @param {string} attributeName the attribute name
|
|
2799
|
+
* @param {MorphContext} ctx the merge context
|
|
2800
|
+
*/
|
|
2801
|
+
function syncBooleanAttribute(oldElement, newElement, attributeName, ctx) {
|
|
2802
|
+
// @ts-ignore this function is only used on boolean attrs that are reflected as dom properties
|
|
2803
|
+
const newLiveValue = newElement[attributeName],
|
|
2804
|
+
// @ts-ignore ditto
|
|
2805
|
+
oldLiveValue = oldElement[attributeName];
|
|
2806
|
+
if (newLiveValue !== oldLiveValue) {
|
|
2807
|
+
const ignoreUpdate = ignoreAttribute(
|
|
2808
|
+
attributeName,
|
|
2809
|
+
oldElement,
|
|
2810
|
+
"update",
|
|
2811
|
+
ctx,
|
|
2812
|
+
);
|
|
2813
|
+
if (!ignoreUpdate) {
|
|
2814
|
+
// update attribute's associated DOM property
|
|
2815
|
+
// @ts-ignore this function is only used on boolean attrs that are reflected as dom properties
|
|
2816
|
+
oldElement[attributeName] = newElement[attributeName];
|
|
2615
2817
|
}
|
|
2818
|
+
if (newLiveValue) {
|
|
2819
|
+
if (!ignoreUpdate) {
|
|
2820
|
+
// https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML
|
|
2821
|
+
// this is the correct way to set a boolean attribute to "true"
|
|
2822
|
+
oldElement.setAttribute(attributeName, "");
|
|
2823
|
+
}
|
|
2824
|
+
} else {
|
|
2825
|
+
if (!ignoreAttribute(attributeName, oldElement, "remove", ctx)) {
|
|
2826
|
+
oldElement.removeAttribute(attributeName);
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2616
2831
|
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
// if we have a soft match with the current node, return it
|
|
2638
|
-
if (isSoftMatch(newChild, potentialSoftMatch)) {
|
|
2639
|
-
return potentialSoftMatch;
|
|
2640
|
-
}
|
|
2641
|
-
|
|
2642
|
-
if (isSoftMatch(nextSibling, potentialSoftMatch)) {
|
|
2643
|
-
// the next new node has a soft match with this node, so
|
|
2644
|
-
// increment the count of future soft matches
|
|
2645
|
-
siblingSoftMatchCount++;
|
|
2646
|
-
nextSibling = nextSibling.nextSibling;
|
|
2647
|
-
|
|
2648
|
-
// If there are two future soft matches, bail to allow the siblings to soft match
|
|
2649
|
-
// so that we don't consume future soft matches for the sake of the current node
|
|
2650
|
-
if (siblingSoftMatchCount >= 2) {
|
|
2651
|
-
return null;
|
|
2652
|
-
}
|
|
2653
|
-
}
|
|
2654
|
-
|
|
2655
|
-
// advanced to the next old content child
|
|
2656
|
-
potentialSoftMatch = potentialSoftMatch.nextSibling;
|
|
2657
|
-
}
|
|
2832
|
+
/**
|
|
2833
|
+
* @param {string} attr the attribute to be mutated
|
|
2834
|
+
* @param {Element} element the element that is going to be updated
|
|
2835
|
+
* @param {"update" | "remove"} updateType
|
|
2836
|
+
* @param {MorphContext} ctx the merge context
|
|
2837
|
+
* @returns {boolean} true if the attribute should be ignored, false otherwise
|
|
2838
|
+
*/
|
|
2839
|
+
function ignoreAttribute(attr, element, updateType, ctx) {
|
|
2840
|
+
if (
|
|
2841
|
+
attr === "value" &&
|
|
2842
|
+
ctx.ignoreActiveValue &&
|
|
2843
|
+
element === document.activeElement
|
|
2844
|
+
) {
|
|
2845
|
+
return true;
|
|
2846
|
+
}
|
|
2847
|
+
return (
|
|
2848
|
+
ctx.callbacks.beforeAttributeUpdated(attr, element, updateType) ===
|
|
2849
|
+
false
|
|
2850
|
+
);
|
|
2851
|
+
}
|
|
2658
2852
|
|
|
2659
|
-
|
|
2660
|
-
|
|
2853
|
+
/**
|
|
2854
|
+
* @param {Node} possibleActiveElement
|
|
2855
|
+
* @param {MorphContext} ctx
|
|
2856
|
+
* @returns {boolean}
|
|
2857
|
+
*/
|
|
2858
|
+
function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
|
|
2859
|
+
return (
|
|
2860
|
+
!!ctx.ignoreActiveValue &&
|
|
2861
|
+
possibleActiveElement === document.activeElement &&
|
|
2862
|
+
possibleActiveElement !== document.body
|
|
2863
|
+
);
|
|
2864
|
+
}
|
|
2661
2865
|
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
// remove svgs to avoid false-positive matches on head, etc.
|
|
2666
|
-
let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
|
|
2667
|
-
|
|
2668
|
-
// if the newContent contains a html, head or body tag, we can simply parse it w/o wrapping
|
|
2669
|
-
if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) {
|
|
2670
|
-
let content = parser.parseFromString(newContent, "text/html");
|
|
2671
|
-
// if it is a full HTML document, return the document itself as the parent container
|
|
2672
|
-
if (contentWithSvgsRemoved.match(/<\/html>/)) {
|
|
2673
|
-
content.generatedByIdiomorph = true;
|
|
2674
|
-
return content;
|
|
2675
|
-
} else {
|
|
2676
|
-
// otherwise return the html element as the parent container
|
|
2677
|
-
let htmlElement = content.firstChild;
|
|
2678
|
-
if (htmlElement) {
|
|
2679
|
-
htmlElement.generatedByIdiomorph = true;
|
|
2680
|
-
return htmlElement;
|
|
2681
|
-
} else {
|
|
2682
|
-
return null;
|
|
2683
|
-
}
|
|
2684
|
-
}
|
|
2685
|
-
} else {
|
|
2686
|
-
// if it is partial HTML, wrap it in a template tag to provide a parent element and also to help
|
|
2687
|
-
// deal with touchy tags like tr, tbody, etc.
|
|
2688
|
-
let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html");
|
|
2689
|
-
let content = responseDoc.body.querySelector('template').content;
|
|
2690
|
-
content.generatedByIdiomorph = true;
|
|
2691
|
-
return content
|
|
2692
|
-
}
|
|
2693
|
-
}
|
|
2866
|
+
return morphNode;
|
|
2867
|
+
})();
|
|
2694
2868
|
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2869
|
+
//=============================================================================
|
|
2870
|
+
// Head Management Functions
|
|
2871
|
+
//=============================================================================
|
|
2872
|
+
/**
|
|
2873
|
+
* @param {MorphContext} ctx
|
|
2874
|
+
* @param {Element} oldNode
|
|
2875
|
+
* @param {Element} newNode
|
|
2876
|
+
* @param {function} callback
|
|
2877
|
+
* @returns {Node[] | Promise<Node[]>}
|
|
2878
|
+
*/
|
|
2879
|
+
function withHeadBlocking(ctx, oldNode, newNode, callback) {
|
|
2880
|
+
if (ctx.head.block) {
|
|
2881
|
+
const oldHead = oldNode.querySelector("head");
|
|
2882
|
+
const newHead = newNode.querySelector("head");
|
|
2883
|
+
if (oldHead && newHead) {
|
|
2884
|
+
const promises = handleHeadElement(oldHead, newHead, ctx);
|
|
2885
|
+
// when head promises resolve, proceed ignoring the head tag
|
|
2886
|
+
return Promise.all(promises).then(() => {
|
|
2887
|
+
const newCtx = Object.assign(ctx, {
|
|
2888
|
+
head: {
|
|
2889
|
+
block: false,
|
|
2890
|
+
ignore: true,
|
|
2891
|
+
},
|
|
2892
|
+
});
|
|
2893
|
+
return callback(newCtx);
|
|
2894
|
+
});
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
// just proceed if we not head blocking
|
|
2898
|
+
return callback(ctx);
|
|
2899
|
+
}
|
|
2718
2900
|
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2901
|
+
/**
|
|
2902
|
+
* The HEAD tag can be handled specially, either w/ a 'merge' or 'append' style
|
|
2903
|
+
*
|
|
2904
|
+
* @param {Element} oldHead
|
|
2905
|
+
* @param {Element} newHead
|
|
2906
|
+
* @param {MorphContext} ctx
|
|
2907
|
+
* @returns {Promise<void>[]}
|
|
2908
|
+
*/
|
|
2909
|
+
function handleHeadElement(oldHead, newHead, ctx) {
|
|
2910
|
+
let added = [];
|
|
2911
|
+
let removed = [];
|
|
2912
|
+
let preserved = [];
|
|
2913
|
+
let nodesToAppend = [];
|
|
2914
|
+
|
|
2915
|
+
// put all new head elements into a Map, by their outerHTML
|
|
2916
|
+
let srcToNewHeadNodes = new Map();
|
|
2917
|
+
for (const newHeadChild of newHead.children) {
|
|
2918
|
+
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
// for each elt in the current head
|
|
2922
|
+
for (const currentHeadElt of oldHead.children) {
|
|
2923
|
+
// If the current head element is in the map
|
|
2924
|
+
let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
|
|
2925
|
+
let isReAppended = ctx.head.shouldReAppend(currentHeadElt);
|
|
2926
|
+
let isPreserved = ctx.head.shouldPreserve(currentHeadElt);
|
|
2927
|
+
if (inNewContent || isPreserved) {
|
|
2928
|
+
if (isReAppended) {
|
|
2929
|
+
// remove the current version and let the new version replace it and re-execute
|
|
2930
|
+
removed.push(currentHeadElt);
|
|
2931
|
+
} else {
|
|
2932
|
+
// this element already exists and should not be re-appended, so remove it from
|
|
2933
|
+
// the new content map, preserving it in the DOM
|
|
2934
|
+
srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
|
|
2935
|
+
preserved.push(currentHeadElt);
|
|
2741
2936
|
}
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
}
|
|
2756
|
-
return bestElement;
|
|
2937
|
+
} else {
|
|
2938
|
+
if (ctx.head.style === "append") {
|
|
2939
|
+
// we are appending and this existing element is not new content
|
|
2940
|
+
// so if and only if it is marked for re-append do we do anything
|
|
2941
|
+
if (isReAppended) {
|
|
2942
|
+
removed.push(currentHeadElt);
|
|
2943
|
+
nodesToAppend.push(currentHeadElt);
|
|
2944
|
+
}
|
|
2945
|
+
} else {
|
|
2946
|
+
// if this is a merge, we remove this content since it is not in the new head
|
|
2947
|
+
if (ctx.head.shouldRemove(currentHeadElt) !== false) {
|
|
2948
|
+
removed.push(currentHeadElt);
|
|
2949
|
+
}
|
|
2757
2950
|
}
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2758
2953
|
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2954
|
+
// Push the remaining new head elements in the Map into the
|
|
2955
|
+
// nodes to append to the head tag
|
|
2956
|
+
nodesToAppend.push(...srcToNewHeadNodes.values());
|
|
2957
|
+
|
|
2958
|
+
let promises = [];
|
|
2959
|
+
for (const newNode of nodesToAppend) {
|
|
2960
|
+
// TODO: This could theoretically be null, based on type
|
|
2961
|
+
let newElt = /** @type {ChildNode} */ (
|
|
2962
|
+
document.createRange().createContextualFragment(newNode.outerHTML)
|
|
2963
|
+
.firstChild
|
|
2964
|
+
);
|
|
2965
|
+
if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
|
|
2966
|
+
if (
|
|
2967
|
+
("href" in newElt && newElt.href) ||
|
|
2968
|
+
("src" in newElt && newElt.src)
|
|
2969
|
+
) {
|
|
2970
|
+
/** @type {(result?: any) => void} */ let resolve;
|
|
2971
|
+
let promise = new Promise(function (_resolve) {
|
|
2972
|
+
resolve = _resolve;
|
|
2973
|
+
});
|
|
2974
|
+
newElt.addEventListener("load", function () {
|
|
2975
|
+
resolve();
|
|
2976
|
+
});
|
|
2977
|
+
promises.push(promise);
|
|
2764
2978
|
}
|
|
2979
|
+
oldHead.appendChild(newElt);
|
|
2980
|
+
ctx.callbacks.afterNodeAdded(newElt);
|
|
2981
|
+
added.push(newElt);
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
// remove all removed elements, after we have appended the new elements to avoid
|
|
2986
|
+
// additional network requests for things like style sheets
|
|
2987
|
+
for (const removedElement of removed) {
|
|
2988
|
+
if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
|
|
2989
|
+
oldHead.removeChild(removedElement);
|
|
2990
|
+
ctx.callbacks.afterNodeRemoved(removedElement);
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2765
2993
|
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2994
|
+
ctx.head.afterHeadMorphed(oldHead, {
|
|
2995
|
+
added: added,
|
|
2996
|
+
kept: preserved,
|
|
2997
|
+
removed: removed,
|
|
2998
|
+
});
|
|
2999
|
+
return promises;
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
//=============================================================================
|
|
3003
|
+
// Create Morph Context Functions
|
|
3004
|
+
//=============================================================================
|
|
3005
|
+
const createMorphContext = (function () {
|
|
3006
|
+
/**
|
|
3007
|
+
*
|
|
3008
|
+
* @param {Element} oldNode
|
|
3009
|
+
* @param {Element} newContent
|
|
3010
|
+
* @param {Config} config
|
|
3011
|
+
* @returns {MorphContext}
|
|
3012
|
+
*/
|
|
3013
|
+
function createMorphContext(oldNode, newContent, config) {
|
|
3014
|
+
const { persistentIds, idMap } = createIdMaps(oldNode, newContent);
|
|
3015
|
+
|
|
3016
|
+
const mergedConfig = mergeDefaults(config);
|
|
3017
|
+
const morphStyle = mergedConfig.morphStyle || "outerHTML";
|
|
3018
|
+
if (!["innerHTML", "outerHTML"].includes(morphStyle)) {
|
|
3019
|
+
throw `Do not understand how to morph style ${morphStyle}`;
|
|
3020
|
+
}
|
|
2769
3021
|
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
3022
|
+
return {
|
|
3023
|
+
target: oldNode,
|
|
3024
|
+
newContent: newContent,
|
|
3025
|
+
config: mergedConfig,
|
|
3026
|
+
morphStyle: morphStyle,
|
|
3027
|
+
ignoreActive: mergedConfig.ignoreActive,
|
|
3028
|
+
ignoreActiveValue: mergedConfig.ignoreActiveValue,
|
|
3029
|
+
restoreFocus: mergedConfig.restoreFocus,
|
|
3030
|
+
idMap: idMap,
|
|
3031
|
+
persistentIds: persistentIds,
|
|
3032
|
+
pantry: createPantry(),
|
|
3033
|
+
callbacks: mergedConfig.callbacks,
|
|
3034
|
+
head: mergedConfig.head,
|
|
3035
|
+
};
|
|
3036
|
+
}
|
|
2773
3037
|
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
3038
|
+
/**
|
|
3039
|
+
* Deep merges the config object and the Idiomorph.defaults object to
|
|
3040
|
+
* produce a final configuration object
|
|
3041
|
+
* @param {Config} config
|
|
3042
|
+
* @returns {ConfigInternal}
|
|
3043
|
+
*/
|
|
3044
|
+
function mergeDefaults(config) {
|
|
3045
|
+
let finalConfig = Object.assign({}, defaults);
|
|
2777
3046
|
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
}
|
|
3047
|
+
// copy top level stuff into final config
|
|
3048
|
+
Object.assign(finalConfig, config);
|
|
2781
3049
|
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
3050
|
+
// copy callbacks into final config (do this to deep merge the callbacks)
|
|
3051
|
+
finalConfig.callbacks = Object.assign(
|
|
3052
|
+
{},
|
|
3053
|
+
defaults.callbacks,
|
|
3054
|
+
config.callbacks,
|
|
3055
|
+
);
|
|
2786
3056
|
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
3057
|
+
// copy head config into final config (do this to deep merge the head)
|
|
3058
|
+
finalConfig.head = Object.assign({}, defaults.head, config.head);
|
|
3059
|
+
|
|
3060
|
+
return finalConfig;
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
/**
|
|
3064
|
+
* @returns {HTMLDivElement}
|
|
3065
|
+
*/
|
|
3066
|
+
function createPantry() {
|
|
3067
|
+
const pantry = document.createElement("div");
|
|
3068
|
+
pantry.hidden = true;
|
|
3069
|
+
document.body.insertAdjacentElement("afterend", pantry);
|
|
3070
|
+
return pantry;
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
/**
|
|
3074
|
+
* Returns all elements with an ID contained within the root element and its descendants
|
|
3075
|
+
*
|
|
3076
|
+
* @param {Element} root
|
|
3077
|
+
* @returns {Element[]}
|
|
3078
|
+
*/
|
|
3079
|
+
function findIdElements(root) {
|
|
3080
|
+
let elements = Array.from(root.querySelectorAll("[id]"));
|
|
3081
|
+
if (root.id) {
|
|
3082
|
+
elements.push(root);
|
|
3083
|
+
}
|
|
3084
|
+
return elements;
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
/**
|
|
3088
|
+
* A bottom-up algorithm that populates a map of Element -> IdSet.
|
|
3089
|
+
* The idSet for a given element is the set of all IDs contained within its subtree.
|
|
3090
|
+
* As an optimzation, we filter these IDs through the given list of persistent IDs,
|
|
3091
|
+
* because we don't need to bother considering IDed elements that won't be in the new content.
|
|
3092
|
+
*
|
|
3093
|
+
* @param {Map<Node, Set<string>>} idMap
|
|
3094
|
+
* @param {Set<string>} persistentIds
|
|
3095
|
+
* @param {Element} root
|
|
3096
|
+
* @param {Element[]} elements
|
|
3097
|
+
*/
|
|
3098
|
+
function populateIdMapWithTree(idMap, persistentIds, root, elements) {
|
|
3099
|
+
for (const elt of elements) {
|
|
3100
|
+
if (persistentIds.has(elt.id)) {
|
|
3101
|
+
/** @type {Element|null} */
|
|
3102
|
+
let current = elt;
|
|
3103
|
+
// walk up the parent hierarchy of that element, adding the id
|
|
3104
|
+
// of element to the parent's id set
|
|
3105
|
+
while (current) {
|
|
3106
|
+
let idSet = idMap.get(current);
|
|
3107
|
+
// if the id set doesn't exist, create it and insert it in the map
|
|
3108
|
+
if (idSet == null) {
|
|
3109
|
+
idSet = new Set();
|
|
3110
|
+
idMap.set(current, idSet);
|
|
2791
3111
|
}
|
|
3112
|
+
idSet.add(elt.id);
|
|
3113
|
+
|
|
3114
|
+
if (current === root) break;
|
|
3115
|
+
current = current.parentElement;
|
|
3116
|
+
}
|
|
2792
3117
|
}
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
2793
3120
|
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
3121
|
+
/**
|
|
3122
|
+
* This function computes a map of nodes to all ids contained within that node (inclusive of the
|
|
3123
|
+
* node). This map can be used to ask if two nodes have intersecting sets of ids, which allows
|
|
3124
|
+
* for a looser definition of "matching" than tradition id matching, and allows child nodes
|
|
3125
|
+
* to contribute to a parent nodes matching.
|
|
3126
|
+
*
|
|
3127
|
+
* @param {Element} oldContent the old content that will be morphed
|
|
3128
|
+
* @param {Element} newContent the new content to morph to
|
|
3129
|
+
* @returns {IdSets}
|
|
3130
|
+
*/
|
|
3131
|
+
function createIdMaps(oldContent, newContent) {
|
|
3132
|
+
const oldIdElements = findIdElements(oldContent);
|
|
3133
|
+
const newIdElements = findIdElements(newContent);
|
|
3134
|
+
|
|
3135
|
+
const persistentIds = createPersistentIds(oldIdElements, newIdElements);
|
|
3136
|
+
|
|
3137
|
+
/** @type {Map<Node, Set<string>>} */
|
|
3138
|
+
let idMap = new Map();
|
|
3139
|
+
populateIdMapWithTree(idMap, persistentIds, oldContent, oldIdElements);
|
|
3140
|
+
|
|
3141
|
+
/** @ts-ignore - if newContent is a duck-typed parent, pass its single child node as the root to halt upwards iteration */
|
|
3142
|
+
const newRoot = newContent.__idiomorphRoot || newContent;
|
|
3143
|
+
populateIdMapWithTree(idMap, persistentIds, newRoot, newIdElements);
|
|
3144
|
+
|
|
3145
|
+
return { persistentIds, idMap };
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
/**
|
|
3149
|
+
* This function computes the set of ids that persist between the two contents excluding duplicates
|
|
3150
|
+
*
|
|
3151
|
+
* @param {Element[]} oldIdElements
|
|
3152
|
+
* @param {Element[]} newIdElements
|
|
3153
|
+
* @returns {Set<string>}
|
|
3154
|
+
*/
|
|
3155
|
+
function createPersistentIds(oldIdElements, newIdElements) {
|
|
3156
|
+
let duplicateIds = new Set();
|
|
3157
|
+
|
|
3158
|
+
/** @type {Map<string, string>} */
|
|
3159
|
+
let oldIdTagNameMap = new Map();
|
|
3160
|
+
for (const { id, tagName } of oldIdElements) {
|
|
3161
|
+
if (oldIdTagNameMap.has(id)) {
|
|
3162
|
+
duplicateIds.add(id);
|
|
3163
|
+
} else {
|
|
3164
|
+
oldIdTagNameMap.set(id, tagName);
|
|
2805
3165
|
}
|
|
3166
|
+
}
|
|
2806
3167
|
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
* @param {Map<Node, Set<String>>} idMap
|
|
2814
|
-
*/
|
|
2815
|
-
function populateIdMapForNode(node, idMap) {
|
|
2816
|
-
let nodeParent = node.parentElement;
|
|
2817
|
-
// find all elements with an id property
|
|
2818
|
-
let idElements = node.querySelectorAll('[id]');
|
|
2819
|
-
for (const elt of idElements) {
|
|
2820
|
-
let current = elt;
|
|
2821
|
-
// walk up the parent hierarchy of that element, adding the id
|
|
2822
|
-
// of element to the parent's id set
|
|
2823
|
-
while (current !== nodeParent && current != null) {
|
|
2824
|
-
let idSet = idMap.get(current);
|
|
2825
|
-
// if the id set doesn't exist, create it and insert it in the map
|
|
2826
|
-
if (idSet == null) {
|
|
2827
|
-
idSet = new Set();
|
|
2828
|
-
idMap.set(current, idSet);
|
|
2829
|
-
}
|
|
2830
|
-
idSet.add(elt.id);
|
|
2831
|
-
current = current.parentElement;
|
|
2832
|
-
}
|
|
2833
|
-
}
|
|
3168
|
+
let persistentIds = new Set();
|
|
3169
|
+
for (const { id, tagName } of newIdElements) {
|
|
3170
|
+
if (persistentIds.has(id)) {
|
|
3171
|
+
duplicateIds.add(id);
|
|
3172
|
+
} else if (oldIdTagNameMap.get(id) === tagName) {
|
|
3173
|
+
persistentIds.add(id);
|
|
2834
3174
|
}
|
|
3175
|
+
// skip if tag types mismatch because its not possible to morph one tag into another
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
for (const id of duplicateIds) {
|
|
3179
|
+
persistentIds.delete(id);
|
|
3180
|
+
}
|
|
3181
|
+
return persistentIds;
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
return createMorphContext;
|
|
3185
|
+
})();
|
|
3186
|
+
|
|
3187
|
+
//=============================================================================
|
|
3188
|
+
// HTML Normalization Functions
|
|
3189
|
+
//=============================================================================
|
|
3190
|
+
const { normalizeElement, normalizeParent } = (function () {
|
|
3191
|
+
/** @type {WeakSet<Node>} */
|
|
3192
|
+
const generatedByIdiomorph = new WeakSet();
|
|
3193
|
+
|
|
3194
|
+
/**
|
|
3195
|
+
*
|
|
3196
|
+
* @param {Element | Document} content
|
|
3197
|
+
* @returns {Element}
|
|
3198
|
+
*/
|
|
3199
|
+
function normalizeElement(content) {
|
|
3200
|
+
if (content instanceof Document) {
|
|
3201
|
+
return content.documentElement;
|
|
3202
|
+
} else {
|
|
3203
|
+
return content;
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
2835
3206
|
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
3207
|
+
/**
|
|
3208
|
+
*
|
|
3209
|
+
* @param {null | string | Node | HTMLCollection | Node[] | Document & {generatedByIdiomorph:boolean}} newContent
|
|
3210
|
+
* @returns {Element}
|
|
3211
|
+
*/
|
|
3212
|
+
function normalizeParent(newContent) {
|
|
3213
|
+
if (newContent == null) {
|
|
3214
|
+
return document.createElement("div"); // dummy parent element
|
|
3215
|
+
} else if (typeof newContent === "string") {
|
|
3216
|
+
return normalizeParent(parseContent(newContent));
|
|
3217
|
+
} else if (
|
|
3218
|
+
generatedByIdiomorph.has(/** @type {Element} */ (newContent))
|
|
3219
|
+
) {
|
|
3220
|
+
// the template tag created by idiomorph parsing can serve as a dummy parent
|
|
3221
|
+
return /** @type {Element} */ (newContent);
|
|
3222
|
+
} else if (newContent instanceof Node) {
|
|
3223
|
+
if (newContent.parentNode) {
|
|
3224
|
+
// we can't use the parent directly because newContent may have siblings
|
|
3225
|
+
// 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);
|
|
3228
|
+
} else {
|
|
3229
|
+
// a single node is added as a child to a dummy parent
|
|
3230
|
+
const dummyParent = document.createElement("div");
|
|
3231
|
+
dummyParent.append(newContent);
|
|
3232
|
+
return dummyParent;
|
|
3233
|
+
}
|
|
3234
|
+
} else {
|
|
3235
|
+
// all nodes in the array or HTMLElement collection are consolidated under
|
|
3236
|
+
// a single dummy parent element
|
|
3237
|
+
const dummyParent = document.createElement("div");
|
|
3238
|
+
for (const elt of [...newContent]) {
|
|
3239
|
+
dummyParent.append(elt);
|
|
2851
3240
|
}
|
|
3241
|
+
return dummyParent;
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
/**
|
|
3246
|
+
* Creates a fake duck-typed parent element to wrap a single node, without actually reparenting it.
|
|
3247
|
+
* "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
|
+
*/
|
|
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
|
+
);
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
/**
|
|
3276
|
+
*
|
|
3277
|
+
* @param {string} newContent
|
|
3278
|
+
* @returns {Node | null | DocumentFragment}
|
|
3279
|
+
*/
|
|
3280
|
+
function parseContent(newContent) {
|
|
3281
|
+
let parser = new DOMParser();
|
|
2852
3282
|
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
3283
|
+
// remove svgs to avoid false-positive matches on head, etc.
|
|
3284
|
+
let contentWithSvgsRemoved = newContent.replace(
|
|
3285
|
+
/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim,
|
|
3286
|
+
"",
|
|
3287
|
+
);
|
|
3288
|
+
|
|
3289
|
+
// if the newContent contains a html, head or body tag, we can simply parse it w/o wrapping
|
|
3290
|
+
if (
|
|
3291
|
+
contentWithSvgsRemoved.match(/<\/html>/) ||
|
|
3292
|
+
contentWithSvgsRemoved.match(/<\/head>/) ||
|
|
3293
|
+
contentWithSvgsRemoved.match(/<\/body>/)
|
|
3294
|
+
) {
|
|
3295
|
+
let content = parser.parseFromString(newContent, "text/html");
|
|
3296
|
+
// if it is a full HTML document, return the document itself as the parent container
|
|
3297
|
+
if (contentWithSvgsRemoved.match(/<\/html>/)) {
|
|
3298
|
+
generatedByIdiomorph.add(content);
|
|
3299
|
+
return content;
|
|
3300
|
+
} else {
|
|
3301
|
+
// otherwise return the html element as the parent container
|
|
3302
|
+
let htmlElement = content.firstChild;
|
|
3303
|
+
if (htmlElement) {
|
|
3304
|
+
generatedByIdiomorph.add(htmlElement);
|
|
3305
|
+
}
|
|
3306
|
+
return htmlElement;
|
|
2859
3307
|
}
|
|
2860
|
-
|
|
3308
|
+
} else {
|
|
3309
|
+
// if it is partial HTML, wrap it in a template tag to provide a parent element and also to help
|
|
3310
|
+
// deal with touchy tags like tr, tbody, etc.
|
|
3311
|
+
let responseDoc = parser.parseFromString(
|
|
3312
|
+
"<body><template>" + newContent + "</template></body>",
|
|
3313
|
+
"text/html",
|
|
3314
|
+
);
|
|
3315
|
+
let content = /** @type {HTMLTemplateElement} */ (
|
|
3316
|
+
responseDoc.body.querySelector("template")
|
|
3317
|
+
).content;
|
|
3318
|
+
generatedByIdiomorph.add(content);
|
|
3319
|
+
return content;
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3323
|
+
return { normalizeElement, normalizeParent };
|
|
3324
|
+
})();
|
|
3325
|
+
|
|
3326
|
+
//=============================================================================
|
|
3327
|
+
// This is what ends up becoming the Idiomorph global object
|
|
3328
|
+
//=============================================================================
|
|
3329
|
+
return {
|
|
3330
|
+
morph,
|
|
3331
|
+
defaults,
|
|
3332
|
+
};
|
|
3333
|
+
})();
|
|
2861
3334
|
|
|
3335
|
+
/**
|
|
3336
|
+
* Morph the state of the currentElement based on the attributes and contents of
|
|
3337
|
+
* the newElement. Morphing may dispatch turbo:before-morph-element,
|
|
3338
|
+
* turbo:before-morph-attribute, and turbo:morph-element events.
|
|
3339
|
+
*
|
|
3340
|
+
* @param currentElement Element destination of morphing changes
|
|
3341
|
+
* @param newElement Element source of morphing changes
|
|
3342
|
+
*/
|
|
2862
3343
|
function morphElements(currentElement, newElement, { callbacks, ...options } = {}) {
|
|
2863
3344
|
Idiomorph.morph(currentElement, newElement, {
|
|
2864
3345
|
...options,
|
|
@@ -2866,12 +3347,37 @@ function morphElements(currentElement, newElement, { callbacks, ...options } = {
|
|
|
2866
3347
|
});
|
|
2867
3348
|
}
|
|
2868
3349
|
|
|
2869
|
-
|
|
2870
|
-
|
|
3350
|
+
/**
|
|
3351
|
+
* Morph the child elements of the currentElement based on the child elements of
|
|
3352
|
+
* the newElement. Morphing children may dispatch turbo:before-morph-element,
|
|
3353
|
+
* turbo:before-morph-attribute, and turbo:morph-element events.
|
|
3354
|
+
*
|
|
3355
|
+
* @param currentElement Element destination of morphing children changes
|
|
3356
|
+
* @param newElement Element source of morphing children changes
|
|
3357
|
+
*/
|
|
3358
|
+
function morphChildren(currentElement, newElement, options = {}) {
|
|
3359
|
+
morphElements(currentElement, newElement.childNodes, {
|
|
3360
|
+
...options,
|
|
2871
3361
|
morphStyle: "innerHTML"
|
|
2872
3362
|
});
|
|
2873
3363
|
}
|
|
2874
3364
|
|
|
3365
|
+
function shouldRefreshFrameWithMorphing(currentFrame, newFrame) {
|
|
3366
|
+
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"))) &&
|
|
3374
|
+
!currentFrame.closest("[data-turbo-permanent]")
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
function closestFrameReloadableWithMorphing(node) {
|
|
3378
|
+
return node.parentElement.closest("turbo-frame[src][refresh=morph]")
|
|
3379
|
+
}
|
|
3380
|
+
|
|
2875
3381
|
class DefaultIdiomorphCallbacks {
|
|
2876
3382
|
#beforeNodeMorphed
|
|
2877
3383
|
|
|
@@ -2930,7 +3436,20 @@ class MorphingFrameRenderer extends FrameRenderer {
|
|
|
2930
3436
|
detail: { currentElement, newElement }
|
|
2931
3437
|
});
|
|
2932
3438
|
|
|
2933
|
-
morphChildren(currentElement, newElement
|
|
3439
|
+
morphChildren(currentElement, newElement, {
|
|
3440
|
+
callbacks: {
|
|
3441
|
+
beforeNodeMorphed: (node, newNode) => {
|
|
3442
|
+
if (
|
|
3443
|
+
shouldRefreshFrameWithMorphing(node, newNode) &&
|
|
3444
|
+
closestFrameReloadableWithMorphing(node) === currentElement
|
|
3445
|
+
) {
|
|
3446
|
+
node.reload();
|
|
3447
|
+
return false
|
|
3448
|
+
}
|
|
3449
|
+
return true
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
});
|
|
2934
3453
|
}
|
|
2935
3454
|
|
|
2936
3455
|
async preservingPermanentElements(callback) {
|
|
@@ -3239,7 +3758,8 @@ class PageSnapshot extends Snapshot {
|
|
|
3239
3758
|
}
|
|
3240
3759
|
|
|
3241
3760
|
get prefersViewTransitions() {
|
|
3242
|
-
|
|
3761
|
+
const viewTransitionEnabled = this.getSetting("view-transition") === "true" || this.headSnapshot.getMetaValue("view-transition") === "same-origin";
|
|
3762
|
+
return viewTransitionEnabled && !window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
3243
3763
|
}
|
|
3244
3764
|
|
|
3245
3765
|
get shouldMorphPage() {
|
|
@@ -3660,16 +4180,6 @@ class Visit {
|
|
|
3660
4180
|
|
|
3661
4181
|
// Private
|
|
3662
4182
|
|
|
3663
|
-
getHistoryMethodForAction(action) {
|
|
3664
|
-
switch (action) {
|
|
3665
|
-
case "replace":
|
|
3666
|
-
return history.replaceState
|
|
3667
|
-
case "advance":
|
|
3668
|
-
case "restore":
|
|
3669
|
-
return history.pushState
|
|
3670
|
-
}
|
|
3671
|
-
}
|
|
3672
|
-
|
|
3673
4183
|
hasPreloadedResponse() {
|
|
3674
4184
|
return typeof this.response == "object"
|
|
3675
4185
|
}
|
|
@@ -3737,6 +4247,8 @@ class BrowserAdapter {
|
|
|
3737
4247
|
|
|
3738
4248
|
visitStarted(visit) {
|
|
3739
4249
|
this.location = visit.location;
|
|
4250
|
+
this.redirectedToLocation = null;
|
|
4251
|
+
|
|
3740
4252
|
visit.loadCachedSnapshot();
|
|
3741
4253
|
visit.issueRequest();
|
|
3742
4254
|
visit.goToSamePageAnchor();
|
|
@@ -3753,6 +4265,10 @@ class BrowserAdapter {
|
|
|
3753
4265
|
|
|
3754
4266
|
visitRequestCompleted(visit) {
|
|
3755
4267
|
visit.loadResponse();
|
|
4268
|
+
|
|
4269
|
+
if (visit.response.redirected) {
|
|
4270
|
+
this.redirectedToLocation = visit.redirectedToLocation;
|
|
4271
|
+
}
|
|
3756
4272
|
}
|
|
3757
4273
|
|
|
3758
4274
|
visitRequestFailedWithStatusCode(visit, statusCode) {
|
|
@@ -3789,6 +4305,12 @@ class BrowserAdapter {
|
|
|
3789
4305
|
|
|
3790
4306
|
visitRendered(_visit) {}
|
|
3791
4307
|
|
|
4308
|
+
// Link prefetching
|
|
4309
|
+
|
|
4310
|
+
linkPrefetchingIsEnabledForLocation(location) {
|
|
4311
|
+
return true
|
|
4312
|
+
}
|
|
4313
|
+
|
|
3792
4314
|
// Form Submission Delegate
|
|
3793
4315
|
|
|
3794
4316
|
formSubmissionStarted(_formSubmission) {
|
|
@@ -3836,7 +4358,7 @@ class BrowserAdapter {
|
|
|
3836
4358
|
reload(reason) {
|
|
3837
4359
|
dispatch("turbo:reload", { detail: reason });
|
|
3838
4360
|
|
|
3839
|
-
window.location.href = this.location?.toString() || window.location.href;
|
|
4361
|
+
window.location.href = (this.redirectedToLocation || this.location)?.toString() || window.location.href;
|
|
3840
4362
|
}
|
|
3841
4363
|
|
|
3842
4364
|
get navigator() {
|
|
@@ -4149,6 +4671,8 @@ class LinkPrefetchObserver {
|
|
|
4149
4671
|
target
|
|
4150
4672
|
);
|
|
4151
4673
|
|
|
4674
|
+
fetchRequest.fetchOptions.priority = "low";
|
|
4675
|
+
|
|
4152
4676
|
prefetchCache.setLater(location.toString(), fetchRequest, this.#cacheTtl);
|
|
4153
4677
|
}
|
|
4154
4678
|
}
|
|
@@ -4373,6 +4897,17 @@ class Navigator {
|
|
|
4373
4897
|
}
|
|
4374
4898
|
}
|
|
4375
4899
|
|
|
4900
|
+
// Link prefetching
|
|
4901
|
+
|
|
4902
|
+
linkPrefetchingIsEnabledForLocation(location) {
|
|
4903
|
+
// Not all adapters implement linkPrefetchingIsEnabledForLocation
|
|
4904
|
+
if (typeof this.adapter.linkPrefetchingIsEnabledForLocation === "function") {
|
|
4905
|
+
return this.adapter.linkPrefetchingIsEnabledForLocation(location)
|
|
4906
|
+
}
|
|
4907
|
+
|
|
4908
|
+
return true
|
|
4909
|
+
}
|
|
4910
|
+
|
|
4376
4911
|
// Visit delegate
|
|
4377
4912
|
|
|
4378
4913
|
visitStarted(visit) {
|
|
@@ -4947,14 +5482,19 @@ class MorphingPageRenderer extends PageRenderer {
|
|
|
4947
5482
|
static renderElement(currentElement, newElement) {
|
|
4948
5483
|
morphElements(currentElement, newElement, {
|
|
4949
5484
|
callbacks: {
|
|
4950
|
-
beforeNodeMorphed:
|
|
5485
|
+
beforeNodeMorphed: (node, newNode) => {
|
|
5486
|
+
if (
|
|
5487
|
+
shouldRefreshFrameWithMorphing(node, newNode) &&
|
|
5488
|
+
!closestFrameReloadableWithMorphing(node)
|
|
5489
|
+
) {
|
|
5490
|
+
node.reload();
|
|
5491
|
+
return false
|
|
5492
|
+
}
|
|
5493
|
+
return true
|
|
5494
|
+
}
|
|
4951
5495
|
}
|
|
4952
5496
|
});
|
|
4953
5497
|
|
|
4954
|
-
for (const frame of currentElement.querySelectorAll("turbo-frame")) {
|
|
4955
|
-
if (canRefreshFrame(frame)) frame.reload();
|
|
4956
|
-
}
|
|
4957
|
-
|
|
4958
5498
|
dispatch("turbo:morph", { detail: { currentElement, newElement } });
|
|
4959
5499
|
}
|
|
4960
5500
|
|
|
@@ -4971,13 +5511,6 @@ class MorphingPageRenderer extends PageRenderer {
|
|
|
4971
5511
|
}
|
|
4972
5512
|
}
|
|
4973
5513
|
|
|
4974
|
-
function canRefreshFrame(frame) {
|
|
4975
|
-
return frame instanceof FrameElement &&
|
|
4976
|
-
frame.src &&
|
|
4977
|
-
frame.refresh === "morph" &&
|
|
4978
|
-
!frame.closest("[data-turbo-permanent]")
|
|
4979
|
-
}
|
|
4980
|
-
|
|
4981
5514
|
class SnapshotCache {
|
|
4982
5515
|
keys = []
|
|
4983
5516
|
snapshots = {}
|
|
@@ -5279,7 +5812,8 @@ class Session {
|
|
|
5279
5812
|
|
|
5280
5813
|
refresh(url, requestId) {
|
|
5281
5814
|
const isRecentRequest = requestId && this.recentRequests.has(requestId);
|
|
5282
|
-
|
|
5815
|
+
const isCurrentUrl = url === document.baseURI;
|
|
5816
|
+
if (!isRecentRequest && !this.navigator.currentVisit && isCurrentUrl) {
|
|
5283
5817
|
this.visit(url, { action: "replace", shouldCacheSnapshot: false });
|
|
5284
5818
|
}
|
|
5285
5819
|
}
|
|
@@ -5403,7 +5937,8 @@ class Session {
|
|
|
5403
5937
|
canPrefetchRequestToLocation(link, location) {
|
|
5404
5938
|
return (
|
|
5405
5939
|
this.elementIsNavigatable(link) &&
|
|
5406
|
-
|
|
5940
|
+
locationIsVisitable(location, this.snapshot.rootLocation) &&
|
|
5941
|
+
this.navigator.linkPrefetchingIsEnabledForLocation(location)
|
|
5407
5942
|
)
|
|
5408
5943
|
}
|
|
5409
5944
|
|
|
@@ -5799,6 +6334,32 @@ function setFormMode(mode) {
|
|
|
5799
6334
|
config.forms.mode = mode;
|
|
5800
6335
|
}
|
|
5801
6336
|
|
|
6337
|
+
/**
|
|
6338
|
+
* Morph the state of the currentBody based on the attributes and contents of
|
|
6339
|
+
* the newBody. Morphing body elements may dispatch turbo:morph,
|
|
6340
|
+
* turbo:before-morph-element, turbo:before-morph-attribute, and
|
|
6341
|
+
* turbo:morph-element events.
|
|
6342
|
+
*
|
|
6343
|
+
* @param currentBody HTMLBodyElement destination of morphing changes
|
|
6344
|
+
* @param newBody HTMLBodyElement source of morphing changes
|
|
6345
|
+
*/
|
|
6346
|
+
function morphBodyElements(currentBody, newBody) {
|
|
6347
|
+
MorphingPageRenderer.renderElement(currentBody, newBody);
|
|
6348
|
+
}
|
|
6349
|
+
|
|
6350
|
+
/**
|
|
6351
|
+
* Morph the child elements of the currentFrame based on the child elements of
|
|
6352
|
+
* the newFrame. Morphing turbo-frame elements may dispatch turbo:before-frame-morph,
|
|
6353
|
+
* turbo:before-morph-element, turbo:before-morph-attribute, and
|
|
6354
|
+
* turbo:morph-element events.
|
|
6355
|
+
*
|
|
6356
|
+
* @param currentFrame FrameElement destination of morphing children changes
|
|
6357
|
+
* @param newFrame FrameElement source of morphing children changes
|
|
6358
|
+
*/
|
|
6359
|
+
function morphTurboFrameElements(currentFrame, newFrame) {
|
|
6360
|
+
MorphingFrameRenderer.renderElement(currentFrame, newFrame);
|
|
6361
|
+
}
|
|
6362
|
+
|
|
5802
6363
|
var Turbo = /*#__PURE__*/Object.freeze({
|
|
5803
6364
|
__proto__: null,
|
|
5804
6365
|
navigator: navigator$1,
|
|
@@ -5818,7 +6379,11 @@ var Turbo = /*#__PURE__*/Object.freeze({
|
|
|
5818
6379
|
clearCache: clearCache,
|
|
5819
6380
|
setProgressBarDelay: setProgressBarDelay,
|
|
5820
6381
|
setConfirmMethod: setConfirmMethod,
|
|
5821
|
-
setFormMode: setFormMode
|
|
6382
|
+
setFormMode: setFormMode,
|
|
6383
|
+
morphBodyElements: morphBodyElements,
|
|
6384
|
+
morphTurboFrameElements: morphTurboFrameElements,
|
|
6385
|
+
morphChildren: morphChildren,
|
|
6386
|
+
morphElements: morphElements
|
|
5822
6387
|
});
|
|
5823
6388
|
|
|
5824
6389
|
class TurboFrameMissingError extends Error {}
|
|
@@ -6507,10 +7072,10 @@ class StreamElement extends HTMLElement {
|
|
|
6507
7072
|
* Gets the list of duplicate children (i.e. those with the same ID)
|
|
6508
7073
|
*/
|
|
6509
7074
|
get duplicateChildren() {
|
|
6510
|
-
const existingChildren = this.targetElements.flatMap((e) => [...e.children]).filter((c) => !!c.id);
|
|
6511
|
-
const newChildrenIds = [...(this.templateContent?.children || [])].filter((c) => !!c.id).map((c) => c.id);
|
|
7075
|
+
const existingChildren = this.targetElements.flatMap((e) => [...e.children]).filter((c) => !!c.getAttribute("id"));
|
|
7076
|
+
const newChildrenIds = [...(this.templateContent?.children || [])].filter((c) => !!c.getAttribute("id")).map((c) => c.getAttribute("id"));
|
|
6512
7077
|
|
|
6513
|
-
return existingChildren.filter((c) => newChildrenIds.includes(c.id))
|
|
7078
|
+
return existingChildren.filter((c) => newChildrenIds.includes(c.getAttribute("id")))
|
|
6514
7079
|
}
|
|
6515
7080
|
|
|
6516
7081
|
/**
|
|
@@ -6693,4 +7258,4 @@ if (customElements.get("turbo-stream-source") === undefined) {
|
|
|
6693
7258
|
window.Turbo = { ...Turbo, StreamActions };
|
|
6694
7259
|
start();
|
|
6695
7260
|
|
|
6696
|
-
export { FetchEnctype, FetchMethod, FetchRequest, FetchResponse, FrameElement, FrameLoadingStyle, FrameRenderer, PageRenderer, PageSnapshot, StreamActions, StreamElement, StreamSourceElement, cache, clearCache, config, connectStreamSource, disconnectStreamSource, fetchWithTurboHeaders as fetch, fetchEnctypeFromString, fetchMethodFromString, isSafe, navigator$1 as navigator, registerAdapter, renderStreamMessage, session, setConfirmMethod, setFormMode, setProgressBarDelay, start, visit };
|
|
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 };
|