@hotwired/turbo 8.0.0-beta.2 → 8.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/turbo.es2017-esm.js +1063 -655
- package/dist/turbo.es2017-umd.js +1063 -655
- package/package.json +2 -2
package/dist/turbo.es2017-umd.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
Turbo 8.0.0-beta.
|
|
3
|
-
Copyright ©
|
|
2
|
+
Turbo 8.0.0-beta.3
|
|
3
|
+
Copyright © 2024 37signals LLC
|
|
4
4
|
*/
|
|
5
5
|
(function (global, factory) {
|
|
6
6
|
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
@@ -639,6 +639,34 @@ Copyright © 2023 37signals LLC
|
|
|
639
639
|
return [before, after]
|
|
640
640
|
}
|
|
641
641
|
|
|
642
|
+
function doesNotTargetIFrame(anchor) {
|
|
643
|
+
if (anchor.hasAttribute("target")) {
|
|
644
|
+
for (const element of document.getElementsByName(anchor.target)) {
|
|
645
|
+
if (element instanceof HTMLIFrameElement) return false
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return true
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function findLinkFromClickTarget(target) {
|
|
653
|
+
return findClosestRecursively(target, "a[href]:not([target^=_]):not([download])")
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function getLocationForLink(link) {
|
|
657
|
+
return expandURL(link.getAttribute("href") || "")
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function debounce(fn, delay) {
|
|
661
|
+
let timeoutId = null;
|
|
662
|
+
|
|
663
|
+
return (...args) => {
|
|
664
|
+
const callback = () => fn.apply(this, args);
|
|
665
|
+
clearTimeout(timeoutId);
|
|
666
|
+
timeoutId = setTimeout(callback, delay);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
642
670
|
class LimitedSet extends Set {
|
|
643
671
|
constructor(maxSize) {
|
|
644
672
|
super();
|
|
@@ -789,10 +817,17 @@ Copyright © 2023 37signals LLC
|
|
|
789
817
|
async perform() {
|
|
790
818
|
const { fetchOptions } = this;
|
|
791
819
|
this.delegate.prepareRequest(this);
|
|
792
|
-
await this.#allowRequestToBeIntercepted(fetchOptions);
|
|
820
|
+
const event = await this.#allowRequestToBeIntercepted(fetchOptions);
|
|
793
821
|
try {
|
|
794
822
|
this.delegate.requestStarted(this);
|
|
795
|
-
|
|
823
|
+
|
|
824
|
+
if (event.detail.fetchRequest) {
|
|
825
|
+
this.response = event.detail.fetchRequest.response;
|
|
826
|
+
} else {
|
|
827
|
+
this.response = fetchWithTurboHeaders(this.url.href, fetchOptions);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const response = await this.response;
|
|
796
831
|
return await this.receive(response)
|
|
797
832
|
} catch (error) {
|
|
798
833
|
if (error.name !== "AbortError") {
|
|
@@ -854,6 +889,8 @@ Copyright © 2023 37signals LLC
|
|
|
854
889
|
});
|
|
855
890
|
this.url = event.detail.url;
|
|
856
891
|
if (event.defaultPrevented) await requestInterception;
|
|
892
|
+
|
|
893
|
+
return event
|
|
857
894
|
}
|
|
858
895
|
|
|
859
896
|
#willDelegateErrorHandling(error) {
|
|
@@ -964,6 +1001,41 @@ Copyright © 2023 37signals LLC
|
|
|
964
1001
|
return fragment
|
|
965
1002
|
}
|
|
966
1003
|
|
|
1004
|
+
const PREFETCH_DELAY = 100;
|
|
1005
|
+
|
|
1006
|
+
class PrefetchCache {
|
|
1007
|
+
#prefetchTimeout = null
|
|
1008
|
+
#prefetched = null
|
|
1009
|
+
|
|
1010
|
+
get(url) {
|
|
1011
|
+
if (this.#prefetched && this.#prefetched.url === url && this.#prefetched.expire > Date.now()) {
|
|
1012
|
+
return this.#prefetched.request
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
setLater(url, request, ttl) {
|
|
1017
|
+
this.clear();
|
|
1018
|
+
|
|
1019
|
+
this.#prefetchTimeout = setTimeout(() => {
|
|
1020
|
+
request.perform();
|
|
1021
|
+
this.set(url, request, ttl);
|
|
1022
|
+
this.#prefetchTimeout = null;
|
|
1023
|
+
}, PREFETCH_DELAY);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
set(url, request, ttl) {
|
|
1027
|
+
this.#prefetched = { url, request, expire: new Date(new Date().getTime() + ttl) };
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
clear() {
|
|
1031
|
+
if (this.#prefetchTimeout) clearTimeout(this.#prefetchTimeout);
|
|
1032
|
+
this.#prefetched = null;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const cacheTtl = 10 * 1000;
|
|
1037
|
+
const prefetchCache = new PrefetchCache();
|
|
1038
|
+
|
|
967
1039
|
const FormSubmissionState = {
|
|
968
1040
|
initialized: "initialized",
|
|
969
1041
|
requesting: "requesting",
|
|
@@ -1081,13 +1153,20 @@ Copyright © 2023 37signals LLC
|
|
|
1081
1153
|
}
|
|
1082
1154
|
|
|
1083
1155
|
requestPreventedHandlingResponse(request, response) {
|
|
1156
|
+
prefetchCache.clear();
|
|
1157
|
+
|
|
1084
1158
|
this.result = { success: response.succeeded, fetchResponse: response };
|
|
1085
1159
|
}
|
|
1086
1160
|
|
|
1087
1161
|
requestSucceededWithResponse(request, response) {
|
|
1088
1162
|
if (response.clientError || response.serverError) {
|
|
1089
1163
|
this.delegate.formSubmissionFailedWithResponse(this, response);
|
|
1090
|
-
|
|
1164
|
+
return
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
prefetchCache.clear();
|
|
1168
|
+
|
|
1169
|
+
if (this.requestMustRedirect(request) && responseSucceededWithoutRedirect(response)) {
|
|
1091
1170
|
const error = new Error("Form responses must redirect to another location");
|
|
1092
1171
|
this.delegate.formSubmissionErrored(this, error);
|
|
1093
1172
|
} else {
|
|
@@ -1405,7 +1484,7 @@ Copyright © 2023 37signals LLC
|
|
|
1405
1484
|
|
|
1406
1485
|
const renderInterception = new Promise((resolve) => (this.#resolveInterceptionPromise = resolve));
|
|
1407
1486
|
const options = { resume: this.#resolveInterceptionPromise, render: this.renderer.renderElement };
|
|
1408
|
-
const immediateRender = this.delegate.allowsImmediateRender(snapshot,
|
|
1487
|
+
const immediateRender = this.delegate.allowsImmediateRender(snapshot, options);
|
|
1409
1488
|
if (!immediateRender) await renderInterception;
|
|
1410
1489
|
|
|
1411
1490
|
await this.renderSnapshot(renderer);
|
|
@@ -1543,9 +1622,9 @@ Copyright © 2023 37signals LLC
|
|
|
1543
1622
|
clickBubbled = (event) => {
|
|
1544
1623
|
if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {
|
|
1545
1624
|
const target = (event.composedPath && event.composedPath()[0]) || event.target;
|
|
1546
|
-
const link =
|
|
1625
|
+
const link = findLinkFromClickTarget(target);
|
|
1547
1626
|
if (link && doesNotTargetIFrame(link)) {
|
|
1548
|
-
const location =
|
|
1627
|
+
const location = getLocationForLink(link);
|
|
1549
1628
|
if (this.delegate.willFollowLinkToLocation(link, location, event)) {
|
|
1550
1629
|
event.preventDefault();
|
|
1551
1630
|
this.delegate.followedLinkToLocation(link, location);
|
|
@@ -1565,26 +1644,6 @@ Copyright © 2023 37signals LLC
|
|
|
1565
1644
|
event.shiftKey
|
|
1566
1645
|
)
|
|
1567
1646
|
}
|
|
1568
|
-
|
|
1569
|
-
findLinkFromClickTarget(target) {
|
|
1570
|
-
return findClosestRecursively(target, "a[href]:not([target^=_]):not([download])")
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
getLocationForLink(link) {
|
|
1574
|
-
return expandURL(link.getAttribute("href") || "")
|
|
1575
|
-
}
|
|
1576
|
-
}
|
|
1577
|
-
|
|
1578
|
-
function doesNotTargetIFrame(anchor) {
|
|
1579
|
-
if (anchor.hasAttribute("target")) {
|
|
1580
|
-
for (const element of document.getElementsByName(anchor.target)) {
|
|
1581
|
-
if (element instanceof HTMLIFrameElement) return false
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
return true
|
|
1585
|
-
} else {
|
|
1586
|
-
return true
|
|
1587
|
-
}
|
|
1588
1647
|
}
|
|
1589
1648
|
|
|
1590
1649
|
class FormLinkClickObserver {
|
|
@@ -1601,6 +1660,16 @@ Copyright © 2023 37signals LLC
|
|
|
1601
1660
|
this.linkInterceptor.stop();
|
|
1602
1661
|
}
|
|
1603
1662
|
|
|
1663
|
+
// Link hover observer delegate
|
|
1664
|
+
|
|
1665
|
+
canPrefetchRequestToLocation(link, location) {
|
|
1666
|
+
return false
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
prefetchAndCacheRequestToLocation(link, location) {
|
|
1670
|
+
return
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1604
1673
|
// Link click observer delegate
|
|
1605
1674
|
|
|
1606
1675
|
willFollowLinkToLocation(link, location, originalEvent) {
|
|
@@ -1874,6 +1943,8 @@ Copyright © 2023 37signals LLC
|
|
|
1874
1943
|
}
|
|
1875
1944
|
}
|
|
1876
1945
|
|
|
1946
|
+
const ProgressBarID = "turbo-progress-bar";
|
|
1947
|
+
|
|
1877
1948
|
class ProgressBar {
|
|
1878
1949
|
static animationDuration = 300 /*ms*/
|
|
1879
1950
|
|
|
@@ -1978,6 +2049,8 @@ Copyright © 2023 37signals LLC
|
|
|
1978
2049
|
|
|
1979
2050
|
createStylesheetElement() {
|
|
1980
2051
|
const element = document.createElement("style");
|
|
2052
|
+
element.id = ProgressBarID;
|
|
2053
|
+
element.setAttribute("data-turbo-permanent", "");
|
|
1981
2054
|
element.type = "text/css";
|
|
1982
2055
|
element.textContent = ProgressBar.defaultCSS;
|
|
1983
2056
|
if (this.cspNonce) {
|
|
@@ -3014,6 +3087,176 @@ Copyright © 2023 37signals LLC
|
|
|
3014
3087
|
}
|
|
3015
3088
|
}
|
|
3016
3089
|
|
|
3090
|
+
class LinkPrefetchObserver {
|
|
3091
|
+
started = false
|
|
3092
|
+
hoverTriggerEvent = "mouseenter"
|
|
3093
|
+
touchTriggerEvent = "touchstart"
|
|
3094
|
+
|
|
3095
|
+
constructor(delegate, eventTarget) {
|
|
3096
|
+
this.delegate = delegate;
|
|
3097
|
+
this.eventTarget = eventTarget;
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
start() {
|
|
3101
|
+
if (this.started) return
|
|
3102
|
+
|
|
3103
|
+
if (this.eventTarget.readyState === "loading") {
|
|
3104
|
+
this.eventTarget.addEventListener("DOMContentLoaded", this.#enable, { once: true });
|
|
3105
|
+
} else {
|
|
3106
|
+
this.#enable();
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
stop() {
|
|
3111
|
+
if (!this.started) return
|
|
3112
|
+
|
|
3113
|
+
this.eventTarget.removeEventListener(this.hoverTriggerEvent, this.#tryToPrefetchRequest, {
|
|
3114
|
+
capture: true,
|
|
3115
|
+
passive: true
|
|
3116
|
+
});
|
|
3117
|
+
this.eventTarget.removeEventListener(this.touchTriggerEvent, this.#tryToPrefetchRequest, {
|
|
3118
|
+
capture: true,
|
|
3119
|
+
passive: true
|
|
3120
|
+
});
|
|
3121
|
+
this.eventTarget.removeEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true);
|
|
3122
|
+
this.started = false;
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
#enable = () => {
|
|
3126
|
+
this.eventTarget.addEventListener(this.hoverTriggerEvent, this.#tryToPrefetchRequest, {
|
|
3127
|
+
capture: true,
|
|
3128
|
+
passive: true
|
|
3129
|
+
});
|
|
3130
|
+
this.eventTarget.addEventListener(this.touchTriggerEvent, this.#tryToPrefetchRequest, {
|
|
3131
|
+
capture: true,
|
|
3132
|
+
passive: true
|
|
3133
|
+
});
|
|
3134
|
+
this.eventTarget.addEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true);
|
|
3135
|
+
this.started = true;
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
#tryToPrefetchRequest = (event) => {
|
|
3139
|
+
if (getMetaContent("turbo-prefetch") !== "true") return
|
|
3140
|
+
|
|
3141
|
+
const target = event.target;
|
|
3142
|
+
const isLink = target.matches && target.matches("a[href]:not([target^=_]):not([download])");
|
|
3143
|
+
|
|
3144
|
+
if (isLink && this.#isPrefetchable(target)) {
|
|
3145
|
+
const link = target;
|
|
3146
|
+
const location = getLocationForLink(link);
|
|
3147
|
+
|
|
3148
|
+
if (this.delegate.canPrefetchRequestToLocation(link, location)) {
|
|
3149
|
+
const fetchRequest = new FetchRequest(
|
|
3150
|
+
this,
|
|
3151
|
+
FetchMethod.get,
|
|
3152
|
+
location,
|
|
3153
|
+
new URLSearchParams(),
|
|
3154
|
+
target
|
|
3155
|
+
);
|
|
3156
|
+
|
|
3157
|
+
prefetchCache.setLater(location.toString(), fetchRequest, this.#cacheTtl);
|
|
3158
|
+
|
|
3159
|
+
link.addEventListener("mouseleave", () => prefetchCache.clear(), { once: true });
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
#tryToUsePrefetchedRequest = (event) => {
|
|
3165
|
+
if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "get") {
|
|
3166
|
+
const cached = prefetchCache.get(event.detail.url.toString());
|
|
3167
|
+
|
|
3168
|
+
if (cached) {
|
|
3169
|
+
// User clicked link, use cache response
|
|
3170
|
+
event.detail.fetchRequest = cached;
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
prefetchCache.clear();
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
prepareRequest(request) {
|
|
3178
|
+
const link = request.target;
|
|
3179
|
+
|
|
3180
|
+
request.headers["Sec-Purpose"] = "prefetch";
|
|
3181
|
+
|
|
3182
|
+
if (link.dataset.turboFrame && link.dataset.turboFrame !== "_top") {
|
|
3183
|
+
request.headers["Turbo-Frame"] = link.dataset.turboFrame;
|
|
3184
|
+
} else if (link.dataset.turboFrame !== "_top") {
|
|
3185
|
+
const turboFrame = link.closest("turbo-frame");
|
|
3186
|
+
|
|
3187
|
+
if (turboFrame) {
|
|
3188
|
+
request.headers["Turbo-Frame"] = turboFrame.id;
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
if (link.hasAttribute("data-turbo-stream")) {
|
|
3193
|
+
request.acceptResponseType("text/vnd.turbo-stream.html");
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3197
|
+
// Fetch request interface
|
|
3198
|
+
|
|
3199
|
+
requestSucceededWithResponse() {}
|
|
3200
|
+
|
|
3201
|
+
requestStarted(fetchRequest) {}
|
|
3202
|
+
|
|
3203
|
+
requestErrored(fetchRequest) {}
|
|
3204
|
+
|
|
3205
|
+
requestFinished(fetchRequest) {}
|
|
3206
|
+
|
|
3207
|
+
requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}
|
|
3208
|
+
|
|
3209
|
+
requestFailedWithResponse(fetchRequest, fetchResponse) {}
|
|
3210
|
+
|
|
3211
|
+
get #cacheTtl() {
|
|
3212
|
+
return Number(getMetaContent("turbo-prefetch-cache-time")) || cacheTtl
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
#isPrefetchable(link) {
|
|
3216
|
+
const href = link.getAttribute("href");
|
|
3217
|
+
|
|
3218
|
+
if (!href || href === "#" || link.dataset.turbo === "false" || link.dataset.turboPrefetch === "false") {
|
|
3219
|
+
return false
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3222
|
+
if (link.origin !== document.location.origin) {
|
|
3223
|
+
return false
|
|
3224
|
+
}
|
|
3225
|
+
|
|
3226
|
+
if (!["http:", "https:"].includes(link.protocol)) {
|
|
3227
|
+
return false
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3230
|
+
if (link.pathname + link.search === document.location.pathname + document.location.search) {
|
|
3231
|
+
return false
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
if (link.dataset.turboMethod && link.dataset.turboMethod !== "get") {
|
|
3235
|
+
return false
|
|
3236
|
+
}
|
|
3237
|
+
|
|
3238
|
+
if (targetsIframe(link)) {
|
|
3239
|
+
return false
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3242
|
+
if (link.pathname + link.search === document.location.pathname + document.location.search) {
|
|
3243
|
+
return false
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
const turboPrefetchParent = findClosestRecursively(link, "[data-turbo-prefetch]");
|
|
3247
|
+
|
|
3248
|
+
if (turboPrefetchParent && turboPrefetchParent.dataset.turboPrefetch === "false") {
|
|
3249
|
+
return false
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
return true
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3256
|
+
const targetsIframe = (link) => {
|
|
3257
|
+
return !doesNotTargetIFrame(link)
|
|
3258
|
+
};
|
|
3259
|
+
|
|
3017
3260
|
class Navigator {
|
|
3018
3261
|
constructor(delegate) {
|
|
3019
3262
|
this.delegate = delegate;
|
|
@@ -3484,722 +3727,838 @@ Copyright © 2023 37signals LLC
|
|
|
3484
3727
|
}
|
|
3485
3728
|
}
|
|
3486
3729
|
|
|
3487
|
-
|
|
3730
|
+
// base IIFE to define idiomorph
|
|
3731
|
+
var Idiomorph = (function () {
|
|
3488
3732
|
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3733
|
+
//=============================================================================
|
|
3734
|
+
// AND NOW IT BEGINS...
|
|
3735
|
+
//=============================================================================
|
|
3736
|
+
let EMPTY_SET = new Set();
|
|
3493
3737
|
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3738
|
+
// default configuration values, updatable by users now
|
|
3739
|
+
let defaults = {
|
|
3740
|
+
morphStyle: "outerHTML",
|
|
3741
|
+
callbacks : {
|
|
3742
|
+
beforeNodeAdded: noOp,
|
|
3743
|
+
afterNodeAdded: noOp,
|
|
3744
|
+
beforeNodeMorphed: noOp,
|
|
3745
|
+
afterNodeMorphed: noOp,
|
|
3746
|
+
beforeNodeRemoved: noOp,
|
|
3747
|
+
afterNodeRemoved: noOp,
|
|
3748
|
+
beforeAttributeUpdated: noOp,
|
|
3497
3749
|
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
let oldHead = oldNode.querySelector('head');
|
|
3512
|
-
let newHead = normalizedNewContent.querySelector('head');
|
|
3513
|
-
if (oldHead && newHead) {
|
|
3514
|
-
let promises = handleHeadElement(newHead, oldHead, ctx);
|
|
3515
|
-
// when head promises resolve, call morph again, ignoring the head tag
|
|
3516
|
-
Promise.all(promises).then(function () {
|
|
3517
|
-
morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, {
|
|
3518
|
-
head: {
|
|
3519
|
-
block: false,
|
|
3520
|
-
ignore: true
|
|
3521
|
-
}
|
|
3522
|
-
}));
|
|
3523
|
-
});
|
|
3524
|
-
return;
|
|
3525
|
-
}
|
|
3526
|
-
}
|
|
3750
|
+
},
|
|
3751
|
+
head: {
|
|
3752
|
+
style: 'merge',
|
|
3753
|
+
shouldPreserve: function (elt) {
|
|
3754
|
+
return elt.getAttribute("im-preserve") === "true";
|
|
3755
|
+
},
|
|
3756
|
+
shouldReAppend: function (elt) {
|
|
3757
|
+
return elt.getAttribute("im-re-append") === "true";
|
|
3758
|
+
},
|
|
3759
|
+
shouldRemove: noOp,
|
|
3760
|
+
afterHeadMorphed: noOp,
|
|
3761
|
+
}
|
|
3762
|
+
};
|
|
3527
3763
|
|
|
3528
|
-
|
|
3764
|
+
//=============================================================================
|
|
3765
|
+
// Core Morphing Algorithm - morph, morphNormalizedContent, morphOldNodeTo, morphChildren
|
|
3766
|
+
//=============================================================================
|
|
3767
|
+
function morph(oldNode, newContent, config = {}) {
|
|
3529
3768
|
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3769
|
+
if (oldNode instanceof Document) {
|
|
3770
|
+
oldNode = oldNode.documentElement;
|
|
3771
|
+
}
|
|
3533
3772
|
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx);
|
|
3773
|
+
if (typeof newContent === 'string') {
|
|
3774
|
+
newContent = parseContent(newContent);
|
|
3775
|
+
}
|
|
3538
3776
|
|
|
3539
|
-
|
|
3540
|
-
let previousSibling = bestMatch?.previousSibling;
|
|
3541
|
-
let nextSibling = bestMatch?.nextSibling;
|
|
3777
|
+
let normalizedContent = normalizeContent(newContent);
|
|
3542
3778
|
|
|
3543
|
-
|
|
3544
|
-
let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx);
|
|
3779
|
+
let ctx = createMorphContext(oldNode, normalizedContent, config);
|
|
3545
3780
|
|
|
3546
|
-
|
|
3547
|
-
// if there was a best match, merge the siblings in too and return the
|
|
3548
|
-
// whole bunch
|
|
3549
|
-
return insertSiblings(previousSibling, morphedNode, nextSibling);
|
|
3550
|
-
} else {
|
|
3551
|
-
// otherwise nothing was added to the DOM
|
|
3552
|
-
return []
|
|
3781
|
+
return morphNormalizedContent(oldNode, normalizedContent, ctx);
|
|
3553
3782
|
}
|
|
3554
|
-
} else {
|
|
3555
|
-
throw "Do not understand how to morph style " + ctx.morphStyle;
|
|
3556
|
-
}
|
|
3557
|
-
}
|
|
3558
|
-
|
|
3559
3783
|
|
|
3784
|
+
function morphNormalizedContent(oldNode, normalizedNewContent, ctx) {
|
|
3785
|
+
if (ctx.head.block) {
|
|
3786
|
+
let oldHead = oldNode.querySelector('head');
|
|
3787
|
+
let newHead = normalizedNewContent.querySelector('head');
|
|
3788
|
+
if (oldHead && newHead) {
|
|
3789
|
+
let promises = handleHeadElement(newHead, oldHead, ctx);
|
|
3790
|
+
// when head promises resolve, call morph again, ignoring the head tag
|
|
3791
|
+
Promise.all(promises).then(function () {
|
|
3792
|
+
morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, {
|
|
3793
|
+
head: {
|
|
3794
|
+
block: false,
|
|
3795
|
+
ignore: true
|
|
3796
|
+
}
|
|
3797
|
+
}));
|
|
3798
|
+
});
|
|
3799
|
+
return;
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3560
3802
|
|
|
3561
|
-
|
|
3562
|
-
* @param oldNode root node to merge content into
|
|
3563
|
-
* @param newContent new content to merge
|
|
3564
|
-
* @param ctx the merge context
|
|
3565
|
-
* @returns {Element} the element that ended up in the DOM
|
|
3566
|
-
*/
|
|
3567
|
-
function morphOldNodeTo(oldNode, newContent, ctx) {
|
|
3568
|
-
if (ctx.ignoreActive && oldNode === document.activeElement) ; else if (newContent == null) {
|
|
3569
|
-
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return;
|
|
3570
|
-
|
|
3571
|
-
oldNode.remove();
|
|
3572
|
-
ctx.callbacks.afterNodeRemoved(oldNode);
|
|
3573
|
-
return null;
|
|
3574
|
-
} else if (!isSoftMatch(oldNode, newContent)) {
|
|
3575
|
-
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return;
|
|
3576
|
-
if (ctx.callbacks.beforeNodeAdded(newContent) === false) return;
|
|
3577
|
-
|
|
3578
|
-
oldNode.parentElement.replaceChild(newContent, oldNode);
|
|
3579
|
-
ctx.callbacks.afterNodeAdded(newContent);
|
|
3580
|
-
ctx.callbacks.afterNodeRemoved(oldNode);
|
|
3581
|
-
return newContent;
|
|
3582
|
-
} else {
|
|
3583
|
-
if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return;
|
|
3803
|
+
if (ctx.morphStyle === "innerHTML") {
|
|
3584
3804
|
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
syncNodeFrom(newContent, oldNode);
|
|
3589
|
-
morphChildren(newContent, oldNode, ctx);
|
|
3590
|
-
}
|
|
3591
|
-
ctx.callbacks.afterNodeMorphed(oldNode, newContent);
|
|
3592
|
-
return oldNode;
|
|
3593
|
-
}
|
|
3594
|
-
}
|
|
3805
|
+
// innerHTML, so we are only updating the children
|
|
3806
|
+
morphChildren(normalizedNewContent, oldNode, ctx);
|
|
3807
|
+
return oldNode.children;
|
|
3595
3808
|
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
*
|
|
3601
|
-
* Basic algorithm is, for each node in the new content:
|
|
3602
|
-
*
|
|
3603
|
-
* - if we have reached the end of the old parent, append the new content
|
|
3604
|
-
* - if the new content has an id set match with the current insertion point, morph
|
|
3605
|
-
* - search for an id set match
|
|
3606
|
-
* - if id set match found, morph
|
|
3607
|
-
* - otherwise search for a "soft" match
|
|
3608
|
-
* - if a soft match is found, morph
|
|
3609
|
-
* - otherwise, prepend the new node before the current insertion point
|
|
3610
|
-
*
|
|
3611
|
-
* The two search algorithms terminate if competing node matches appear to outweigh what can be achieved
|
|
3612
|
-
* with the current node. See findIdSetMatch() and findSoftMatch() for details.
|
|
3613
|
-
*
|
|
3614
|
-
* @param {Element} newParent the parent element of the new content
|
|
3615
|
-
* @param {Element } oldParent the old content that we are merging the new content into
|
|
3616
|
-
* @param ctx the merge context
|
|
3617
|
-
*/
|
|
3618
|
-
function morphChildren(newParent, oldParent, ctx) {
|
|
3809
|
+
} else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) {
|
|
3810
|
+
// otherwise find the best element match in the new content, morph that, and merge its siblings
|
|
3811
|
+
// into either side of the best match
|
|
3812
|
+
let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx);
|
|
3619
3813
|
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3814
|
+
// stash the siblings that will need to be inserted on either side of the best match
|
|
3815
|
+
let previousSibling = bestMatch?.previousSibling;
|
|
3816
|
+
let nextSibling = bestMatch?.nextSibling;
|
|
3623
3817
|
|
|
3624
|
-
|
|
3625
|
-
|
|
3818
|
+
// morph it
|
|
3819
|
+
let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx);
|
|
3626
3820
|
|
|
3627
|
-
|
|
3628
|
-
|
|
3821
|
+
if (bestMatch) {
|
|
3822
|
+
// if there was a best match, merge the siblings in too and return the
|
|
3823
|
+
// whole bunch
|
|
3824
|
+
return insertSiblings(previousSibling, morphedNode, nextSibling);
|
|
3825
|
+
} else {
|
|
3826
|
+
// otherwise nothing was added to the DOM
|
|
3827
|
+
return []
|
|
3828
|
+
}
|
|
3829
|
+
} else {
|
|
3830
|
+
throw "Do not understand how to morph style " + ctx.morphStyle;
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3629
3833
|
|
|
3630
|
-
// if we are at the end of the exiting parent's children, just append
|
|
3631
|
-
if (insertionPoint == null) {
|
|
3632
|
-
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
|
3633
3834
|
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3835
|
+
/**
|
|
3836
|
+
* @param possibleActiveElement
|
|
3837
|
+
* @param ctx
|
|
3838
|
+
* @returns {boolean}
|
|
3839
|
+
*/
|
|
3840
|
+
function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
|
|
3841
|
+
return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement;
|
|
3638
3842
|
}
|
|
3639
3843
|
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3844
|
+
/**
|
|
3845
|
+
* @param oldNode root node to merge content into
|
|
3846
|
+
* @param newContent new content to merge
|
|
3847
|
+
* @param ctx the merge context
|
|
3848
|
+
* @returns {Element} the element that ended up in the DOM
|
|
3849
|
+
*/
|
|
3850
|
+
function morphOldNodeTo(oldNode, newContent, ctx) {
|
|
3851
|
+
if (ctx.ignoreActive && oldNode === document.activeElement) ; else if (newContent == null) {
|
|
3852
|
+
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
|
|
3853
|
+
|
|
3854
|
+
oldNode.remove();
|
|
3855
|
+
ctx.callbacks.afterNodeRemoved(oldNode);
|
|
3856
|
+
return null;
|
|
3857
|
+
} else if (!isSoftMatch(oldNode, newContent)) {
|
|
3858
|
+
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
|
|
3859
|
+
if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode;
|
|
3860
|
+
|
|
3861
|
+
oldNode.parentElement.replaceChild(newContent, oldNode);
|
|
3862
|
+
ctx.callbacks.afterNodeAdded(newContent);
|
|
3863
|
+
ctx.callbacks.afterNodeRemoved(oldNode);
|
|
3864
|
+
return newContent;
|
|
3865
|
+
} else {
|
|
3866
|
+
if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return oldNode;
|
|
3867
|
+
|
|
3868
|
+
if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) ; else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") {
|
|
3869
|
+
handleHeadElement(newContent, oldNode, ctx);
|
|
3870
|
+
} else {
|
|
3871
|
+
syncNodeFrom(newContent, oldNode, ctx);
|
|
3872
|
+
if (!ignoreValueOfActiveElement(oldNode, ctx)) {
|
|
3873
|
+
morphChildren(newContent, oldNode, ctx);
|
|
3874
|
+
}
|
|
3875
|
+
}
|
|
3876
|
+
ctx.callbacks.afterNodeMorphed(oldNode, newContent);
|
|
3877
|
+
return oldNode;
|
|
3878
|
+
}
|
|
3646
3879
|
}
|
|
3647
3880
|
|
|
3648
|
-
|
|
3649
|
-
|
|
3881
|
+
/**
|
|
3882
|
+
* This is the core algorithm for matching up children. The idea is to use id sets to try to match up
|
|
3883
|
+
* nodes as faithfully as possible. We greedily match, which allows us to keep the algorithm fast, but
|
|
3884
|
+
* by using id sets, we are able to better match up with content deeper in the DOM.
|
|
3885
|
+
*
|
|
3886
|
+
* Basic algorithm is, for each node in the new content:
|
|
3887
|
+
*
|
|
3888
|
+
* - if we have reached the end of the old parent, append the new content
|
|
3889
|
+
* - if the new content has an id set match with the current insertion point, morph
|
|
3890
|
+
* - search for an id set match
|
|
3891
|
+
* - if id set match found, morph
|
|
3892
|
+
* - otherwise search for a "soft" match
|
|
3893
|
+
* - if a soft match is found, morph
|
|
3894
|
+
* - otherwise, prepend the new node before the current insertion point
|
|
3895
|
+
*
|
|
3896
|
+
* The two search algorithms terminate if competing node matches appear to outweigh what can be achieved
|
|
3897
|
+
* with the current node. See findIdSetMatch() and findSoftMatch() for details.
|
|
3898
|
+
*
|
|
3899
|
+
* @param {Element} newParent the parent element of the new content
|
|
3900
|
+
* @param {Element } oldParent the old content that we are merging the new content into
|
|
3901
|
+
* @param ctx the merge context
|
|
3902
|
+
*/
|
|
3903
|
+
function morphChildren(newParent, oldParent, ctx) {
|
|
3904
|
+
|
|
3905
|
+
let nextNewChild = newParent.firstChild;
|
|
3906
|
+
let insertionPoint = oldParent.firstChild;
|
|
3907
|
+
let newChild;
|
|
3908
|
+
|
|
3909
|
+
// run through all the new content
|
|
3910
|
+
while (nextNewChild) {
|
|
3911
|
+
|
|
3912
|
+
newChild = nextNewChild;
|
|
3913
|
+
nextNewChild = newChild.nextSibling;
|
|
3914
|
+
|
|
3915
|
+
// if we are at the end of the exiting parent's children, just append
|
|
3916
|
+
if (insertionPoint == null) {
|
|
3917
|
+
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
|
3918
|
+
|
|
3919
|
+
oldParent.appendChild(newChild);
|
|
3920
|
+
ctx.callbacks.afterNodeAdded(newChild);
|
|
3921
|
+
removeIdsFromConsideration(ctx, newChild);
|
|
3922
|
+
continue;
|
|
3923
|
+
}
|
|
3650
3924
|
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3925
|
+
// if the current node has an id set match then morph
|
|
3926
|
+
if (isIdSetMatch(newChild, insertionPoint, ctx)) {
|
|
3927
|
+
morphOldNodeTo(insertionPoint, newChild, ctx);
|
|
3928
|
+
insertionPoint = insertionPoint.nextSibling;
|
|
3929
|
+
removeIdsFromConsideration(ctx, newChild);
|
|
3930
|
+
continue;
|
|
3931
|
+
}
|
|
3658
3932
|
|
|
3659
|
-
|
|
3660
|
-
|
|
3933
|
+
// otherwise search forward in the existing old children for an id set match
|
|
3934
|
+
let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx);
|
|
3661
3935
|
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3936
|
+
// if we found a potential match, remove the nodes until that point and morph
|
|
3937
|
+
if (idSetMatch) {
|
|
3938
|
+
insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx);
|
|
3939
|
+
morphOldNodeTo(idSetMatch, newChild, ctx);
|
|
3940
|
+
removeIdsFromConsideration(ctx, newChild);
|
|
3941
|
+
continue;
|
|
3942
|
+
}
|
|
3669
3943
|
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
|
3944
|
+
// no id set match found, so scan forward for a soft match for the current node
|
|
3945
|
+
let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx);
|
|
3673
3946
|
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3947
|
+
// if we found a soft match for the current node, morph
|
|
3948
|
+
if (softMatch) {
|
|
3949
|
+
insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx);
|
|
3950
|
+
morphOldNodeTo(softMatch, newChild, ctx);
|
|
3951
|
+
removeIdsFromConsideration(ctx, newChild);
|
|
3952
|
+
continue;
|
|
3953
|
+
}
|
|
3678
3954
|
|
|
3679
|
-
|
|
3680
|
-
|
|
3955
|
+
// abandon all hope of morphing, just insert the new child before the insertion point
|
|
3956
|
+
// and move on
|
|
3957
|
+
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
|
3681
3958
|
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
}
|
|
3959
|
+
oldParent.insertBefore(newChild, insertionPoint);
|
|
3960
|
+
ctx.callbacks.afterNodeAdded(newChild);
|
|
3961
|
+
removeIdsFromConsideration(ctx, newChild);
|
|
3962
|
+
}
|
|
3687
3963
|
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
//=============================================================================
|
|
3964
|
+
// remove any remaining old nodes that didn't match up with new content
|
|
3965
|
+
while (insertionPoint !== null) {
|
|
3691
3966
|
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
*
|
|
3696
|
-
* @param {Element} from the element to copy attributes & state from
|
|
3697
|
-
* @param {Element} to the element to copy attributes & state to
|
|
3698
|
-
*/
|
|
3699
|
-
function syncNodeFrom(from, to) {
|
|
3700
|
-
let type = from.nodeType;
|
|
3701
|
-
|
|
3702
|
-
// if is an element type, sync the attributes from the
|
|
3703
|
-
// new node into the new node
|
|
3704
|
-
if (type === 1 /* element type */) {
|
|
3705
|
-
const fromAttributes = from.attributes;
|
|
3706
|
-
const toAttributes = to.attributes;
|
|
3707
|
-
for (const fromAttribute of fromAttributes) {
|
|
3708
|
-
if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) {
|
|
3709
|
-
to.setAttribute(fromAttribute.name, fromAttribute.value);
|
|
3967
|
+
let tempNode = insertionPoint;
|
|
3968
|
+
insertionPoint = insertionPoint.nextSibling;
|
|
3969
|
+
removeNode(tempNode, ctx);
|
|
3710
3970
|
}
|
|
3711
3971
|
}
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3972
|
+
|
|
3973
|
+
//=============================================================================
|
|
3974
|
+
// Attribute Syncing Code
|
|
3975
|
+
//=============================================================================
|
|
3976
|
+
|
|
3977
|
+
/**
|
|
3978
|
+
* @param attr {String} the attribute to be mutated
|
|
3979
|
+
* @param to {Element} the element that is going to be updated
|
|
3980
|
+
* @param updateType {("update"|"remove")}
|
|
3981
|
+
* @param ctx the merge context
|
|
3982
|
+
* @returns {boolean} true if the attribute should be ignored, false otherwise
|
|
3983
|
+
*/
|
|
3984
|
+
function ignoreAttribute(attr, to, updateType, ctx) {
|
|
3985
|
+
if(attr === 'value' && ctx.ignoreActiveValue && to === document.activeElement){
|
|
3986
|
+
return true;
|
|
3715
3987
|
}
|
|
3988
|
+
return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false;
|
|
3716
3989
|
}
|
|
3717
|
-
}
|
|
3718
3990
|
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3991
|
+
/**
|
|
3992
|
+
* syncs a given node with another node, copying over all attributes and
|
|
3993
|
+
* inner element state from the 'from' node to the 'to' node
|
|
3994
|
+
*
|
|
3995
|
+
* @param {Element} from the element to copy attributes & state from
|
|
3996
|
+
* @param {Element} to the element to copy attributes & state to
|
|
3997
|
+
* @param ctx the merge context
|
|
3998
|
+
*/
|
|
3999
|
+
function syncNodeFrom(from, to, ctx) {
|
|
4000
|
+
let type = from.nodeType;
|
|
4001
|
+
|
|
4002
|
+
// if is an element type, sync the attributes from the
|
|
4003
|
+
// new node into the new node
|
|
4004
|
+
if (type === 1 /* element type */) {
|
|
4005
|
+
const fromAttributes = from.attributes;
|
|
4006
|
+
const toAttributes = to.attributes;
|
|
4007
|
+
for (const fromAttribute of fromAttributes) {
|
|
4008
|
+
if (ignoreAttribute(fromAttribute.name, to, 'update', ctx)) {
|
|
4009
|
+
continue;
|
|
4010
|
+
}
|
|
4011
|
+
if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) {
|
|
4012
|
+
to.setAttribute(fromAttribute.name, fromAttribute.value);
|
|
4013
|
+
}
|
|
4014
|
+
}
|
|
4015
|
+
// iterate backwards to avoid skipping over items when a delete occurs
|
|
4016
|
+
for (let i = toAttributes.length - 1; 0 <= i; i--) {
|
|
4017
|
+
const toAttribute = toAttributes[i];
|
|
4018
|
+
if (ignoreAttribute(toAttribute.name, to, 'remove', ctx)) {
|
|
4019
|
+
continue;
|
|
4020
|
+
}
|
|
4021
|
+
if (!from.hasAttribute(toAttribute.name)) {
|
|
4022
|
+
to.removeAttribute(toAttribute.name);
|
|
4023
|
+
}
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
3725
4026
|
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
if (from instanceof HTMLInputElement &&
|
|
3733
|
-
to instanceof HTMLInputElement &&
|
|
3734
|
-
from.type !== 'file') {
|
|
3735
|
-
|
|
3736
|
-
to.value = from.value || '';
|
|
3737
|
-
syncAttribute(from, to, 'value');
|
|
3738
|
-
|
|
3739
|
-
// sync boolean attributes
|
|
3740
|
-
syncAttribute(from, to, 'checked');
|
|
3741
|
-
syncAttribute(from, to, 'disabled');
|
|
3742
|
-
} else if (from instanceof HTMLOptionElement) {
|
|
3743
|
-
syncAttribute(from, to, 'selected');
|
|
3744
|
-
} else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) {
|
|
3745
|
-
let fromValue = from.value;
|
|
3746
|
-
let toValue = to.value;
|
|
3747
|
-
if (fromValue !== toValue) {
|
|
3748
|
-
to.value = fromValue;
|
|
3749
|
-
}
|
|
3750
|
-
if (to.firstChild && to.firstChild.nodeValue !== fromValue) {
|
|
3751
|
-
to.firstChild.nodeValue = fromValue;
|
|
3752
|
-
}
|
|
3753
|
-
}
|
|
3754
|
-
}
|
|
4027
|
+
// sync text nodes
|
|
4028
|
+
if (type === 8 /* comment */ || type === 3 /* text */) {
|
|
4029
|
+
if (to.nodeValue !== from.nodeValue) {
|
|
4030
|
+
to.nodeValue = from.nodeValue;
|
|
4031
|
+
}
|
|
4032
|
+
}
|
|
3755
4033
|
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
} else {
|
|
3761
|
-
to.removeAttribute(attributeName);
|
|
4034
|
+
if (!ignoreValueOfActiveElement(to, ctx)) {
|
|
4035
|
+
// sync input values
|
|
4036
|
+
syncInputValue(from, to, ctx);
|
|
4037
|
+
}
|
|
3762
4038
|
}
|
|
3763
|
-
}
|
|
3764
|
-
}
|
|
3765
4039
|
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
4040
|
+
/**
|
|
4041
|
+
* @param from {Element} element to sync the value from
|
|
4042
|
+
* @param to {Element} element to sync the value to
|
|
4043
|
+
* @param attributeName {String} the attribute name
|
|
4044
|
+
* @param ctx the merge context
|
|
4045
|
+
*/
|
|
4046
|
+
function syncBooleanAttribute(from, to, attributeName, ctx) {
|
|
4047
|
+
if (from[attributeName] !== to[attributeName]) {
|
|
4048
|
+
let ignoreUpdate = ignoreAttribute(attributeName, to, 'update', ctx);
|
|
4049
|
+
if (!ignoreUpdate) {
|
|
4050
|
+
to[attributeName] = from[attributeName];
|
|
4051
|
+
}
|
|
4052
|
+
if (from[attributeName]) {
|
|
4053
|
+
if (!ignoreUpdate) {
|
|
4054
|
+
to.setAttribute(attributeName, from[attributeName]);
|
|
4055
|
+
}
|
|
4056
|
+
} else {
|
|
4057
|
+
if (!ignoreAttribute(attributeName, to, 'remove', ctx)) {
|
|
4058
|
+
to.removeAttribute(attributeName);
|
|
4059
|
+
}
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
3770
4063
|
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
4064
|
+
/**
|
|
4065
|
+
* NB: many bothans died to bring us information:
|
|
4066
|
+
*
|
|
4067
|
+
* https://github.com/patrick-steele-idem/morphdom/blob/master/src/specialElHandlers.js
|
|
4068
|
+
* https://github.com/choojs/nanomorph/blob/master/lib/morph.jsL113
|
|
4069
|
+
*
|
|
4070
|
+
* @param from {Element} the element to sync the input value from
|
|
4071
|
+
* @param to {Element} the element to sync the input value to
|
|
4072
|
+
* @param ctx the merge context
|
|
4073
|
+
*/
|
|
4074
|
+
function syncInputValue(from, to, ctx) {
|
|
4075
|
+
if (from instanceof HTMLInputElement &&
|
|
4076
|
+
to instanceof HTMLInputElement &&
|
|
4077
|
+
from.type !== 'file') {
|
|
4078
|
+
|
|
4079
|
+
let fromValue = from.value;
|
|
4080
|
+
let toValue = to.value;
|
|
4081
|
+
|
|
4082
|
+
// sync boolean attributes
|
|
4083
|
+
syncBooleanAttribute(from, to, 'checked', ctx);
|
|
4084
|
+
syncBooleanAttribute(from, to, 'disabled', ctx);
|
|
4085
|
+
|
|
4086
|
+
if (!from.hasAttribute('value')) {
|
|
4087
|
+
if (!ignoreAttribute('value', to, 'remove', ctx)) {
|
|
4088
|
+
to.value = '';
|
|
4089
|
+
to.removeAttribute('value');
|
|
4090
|
+
}
|
|
4091
|
+
} else if (fromValue !== toValue) {
|
|
4092
|
+
if (!ignoreAttribute('value', to, 'update', ctx)) {
|
|
4093
|
+
to.setAttribute('value', fromValue);
|
|
4094
|
+
to.value = fromValue;
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
4097
|
+
} else if (from instanceof HTMLOptionElement) {
|
|
4098
|
+
syncBooleanAttribute(from, to, 'selected', ctx);
|
|
4099
|
+
} else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) {
|
|
4100
|
+
let fromValue = from.value;
|
|
4101
|
+
let toValue = to.value;
|
|
4102
|
+
if (ignoreAttribute('value', to, 'update', ctx)) {
|
|
4103
|
+
return;
|
|
4104
|
+
}
|
|
4105
|
+
if (fromValue !== toValue) {
|
|
4106
|
+
to.value = fromValue;
|
|
4107
|
+
}
|
|
4108
|
+
if (to.firstChild && to.firstChild.nodeValue !== fromValue) {
|
|
4109
|
+
to.firstChild.nodeValue = fromValue;
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
3775
4113
|
|
|
3776
|
-
|
|
4114
|
+
//=============================================================================
|
|
4115
|
+
// the HEAD tag can be handled specially, either w/ a 'merge' or 'append' style
|
|
4116
|
+
//=============================================================================
|
|
4117
|
+
function handleHeadElement(newHeadTag, currentHead, ctx) {
|
|
3777
4118
|
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
}
|
|
4119
|
+
let added = [];
|
|
4120
|
+
let removed = [];
|
|
4121
|
+
let preserved = [];
|
|
4122
|
+
let nodesToAppend = [];
|
|
3783
4123
|
|
|
3784
|
-
|
|
3785
|
-
for (const currentHeadElt of currentHead.children) {
|
|
4124
|
+
let headMergeStyle = ctx.head.style;
|
|
3786
4125
|
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
if (inNewContent || isPreserved) {
|
|
3792
|
-
if (isReAppended) {
|
|
3793
|
-
// remove the current version and let the new version replace it and re-execute
|
|
3794
|
-
removed.push(currentHeadElt);
|
|
3795
|
-
} else {
|
|
3796
|
-
// this element already exists and should not be re-appended, so remove it from
|
|
3797
|
-
// the new content map, preserving it in the DOM
|
|
3798
|
-
srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
|
|
3799
|
-
preserved.push(currentHeadElt);
|
|
4126
|
+
// put all new head elements into a Map, by their outerHTML
|
|
4127
|
+
let srcToNewHeadNodes = new Map();
|
|
4128
|
+
for (const newHeadChild of newHeadTag.children) {
|
|
4129
|
+
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
|
|
3800
4130
|
}
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
4131
|
+
|
|
4132
|
+
// for each elt in the current head
|
|
4133
|
+
for (const currentHeadElt of currentHead.children) {
|
|
4134
|
+
|
|
4135
|
+
// If the current head element is in the map
|
|
4136
|
+
let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
|
|
4137
|
+
let isReAppended = ctx.head.shouldReAppend(currentHeadElt);
|
|
4138
|
+
let isPreserved = ctx.head.shouldPreserve(currentHeadElt);
|
|
4139
|
+
if (inNewContent || isPreserved) {
|
|
4140
|
+
if (isReAppended) {
|
|
4141
|
+
// remove the current version and let the new version replace it and re-execute
|
|
4142
|
+
removed.push(currentHeadElt);
|
|
4143
|
+
} else {
|
|
4144
|
+
// this element already exists and should not be re-appended, so remove it from
|
|
4145
|
+
// the new content map, preserving it in the DOM
|
|
4146
|
+
srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
|
|
4147
|
+
preserved.push(currentHeadElt);
|
|
4148
|
+
}
|
|
4149
|
+
} else {
|
|
4150
|
+
if (headMergeStyle === "append") {
|
|
4151
|
+
// we are appending and this existing element is not new content
|
|
4152
|
+
// so if and only if it is marked for re-append do we do anything
|
|
4153
|
+
if (isReAppended) {
|
|
4154
|
+
removed.push(currentHeadElt);
|
|
4155
|
+
nodesToAppend.push(currentHeadElt);
|
|
4156
|
+
}
|
|
4157
|
+
} else {
|
|
4158
|
+
// if this is a merge, we remove this content since it is not in the new head
|
|
4159
|
+
if (ctx.head.shouldRemove(currentHeadElt) !== false) {
|
|
4160
|
+
removed.push(currentHeadElt);
|
|
4161
|
+
}
|
|
4162
|
+
}
|
|
3808
4163
|
}
|
|
3809
|
-
}
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
4164
|
+
}
|
|
4165
|
+
|
|
4166
|
+
// Push the remaining new head elements in the Map into the
|
|
4167
|
+
// nodes to append to the head tag
|
|
4168
|
+
nodesToAppend.push(...srcToNewHeadNodes.values());
|
|
4169
|
+
|
|
4170
|
+
let promises = [];
|
|
4171
|
+
for (const newNode of nodesToAppend) {
|
|
4172
|
+
let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild;
|
|
4173
|
+
if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
|
|
4174
|
+
if (newElt.href || newElt.src) {
|
|
4175
|
+
let resolve = null;
|
|
4176
|
+
let promise = new Promise(function (_resolve) {
|
|
4177
|
+
resolve = _resolve;
|
|
4178
|
+
});
|
|
4179
|
+
newElt.addEventListener('load', function () {
|
|
4180
|
+
resolve();
|
|
4181
|
+
});
|
|
4182
|
+
promises.push(promise);
|
|
4183
|
+
}
|
|
4184
|
+
currentHead.appendChild(newElt);
|
|
4185
|
+
ctx.callbacks.afterNodeAdded(newElt);
|
|
4186
|
+
added.push(newElt);
|
|
3813
4187
|
}
|
|
3814
4188
|
}
|
|
3815
|
-
}
|
|
3816
|
-
}
|
|
3817
4189
|
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
|
|
3826
|
-
if (newElt.href || newElt.src) {
|
|
3827
|
-
let resolve = null;
|
|
3828
|
-
let promise = new Promise(function (_resolve) {
|
|
3829
|
-
resolve = _resolve;
|
|
3830
|
-
});
|
|
3831
|
-
newElt.addEventListener('load',function() {
|
|
3832
|
-
resolve();
|
|
3833
|
-
});
|
|
3834
|
-
promises.push(promise);
|
|
4190
|
+
// remove all removed elements, after we have appended the new elements to avoid
|
|
4191
|
+
// additional network requests for things like style sheets
|
|
4192
|
+
for (const removedElement of removed) {
|
|
4193
|
+
if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
|
|
4194
|
+
currentHead.removeChild(removedElement);
|
|
4195
|
+
ctx.callbacks.afterNodeRemoved(removedElement);
|
|
4196
|
+
}
|
|
3835
4197
|
}
|
|
3836
|
-
|
|
3837
|
-
ctx.
|
|
3838
|
-
|
|
4198
|
+
|
|
4199
|
+
ctx.head.afterHeadMorphed(currentHead, {added: added, kept: preserved, removed: removed});
|
|
4200
|
+
return promises;
|
|
3839
4201
|
}
|
|
3840
|
-
}
|
|
3841
4202
|
|
|
3842
|
-
|
|
3843
|
-
// additional network requests for things like style sheets
|
|
3844
|
-
for (const removedElement of removed) {
|
|
3845
|
-
if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
|
|
3846
|
-
currentHead.removeChild(removedElement);
|
|
3847
|
-
ctx.callbacks.afterNodeRemoved(removedElement);
|
|
4203
|
+
function noOp() {
|
|
3848
4204
|
}
|
|
3849
|
-
}
|
|
3850
4205
|
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
4206
|
+
/*
|
|
4207
|
+
Deep merges the config object and the Idiomoroph.defaults object to
|
|
4208
|
+
produce a final configuration object
|
|
4209
|
+
*/
|
|
4210
|
+
function mergeDefaults(config) {
|
|
4211
|
+
let finalConfig = {};
|
|
4212
|
+
// copy top level stuff into final config
|
|
4213
|
+
Object.assign(finalConfig, defaults);
|
|
4214
|
+
Object.assign(finalConfig, config);
|
|
4215
|
+
|
|
4216
|
+
// copy callbacks into final config (do this to deep merge the callbacks)
|
|
4217
|
+
finalConfig.callbacks = {};
|
|
4218
|
+
Object.assign(finalConfig.callbacks, defaults.callbacks);
|
|
4219
|
+
Object.assign(finalConfig.callbacks, config.callbacks);
|
|
4220
|
+
|
|
4221
|
+
// copy head config into final config (do this to deep merge the head)
|
|
4222
|
+
finalConfig.head = {};
|
|
4223
|
+
Object.assign(finalConfig.head, defaults.head);
|
|
4224
|
+
Object.assign(finalConfig.head, config.head);
|
|
4225
|
+
return finalConfig;
|
|
4226
|
+
}
|
|
3854
4227
|
|
|
3855
|
-
|
|
4228
|
+
function createMorphContext(oldNode, newContent, config) {
|
|
4229
|
+
config = mergeDefaults(config);
|
|
4230
|
+
return {
|
|
4231
|
+
target: oldNode,
|
|
4232
|
+
newContent: newContent,
|
|
4233
|
+
config: config,
|
|
4234
|
+
morphStyle: config.morphStyle,
|
|
4235
|
+
ignoreActive: config.ignoreActive,
|
|
4236
|
+
ignoreActiveValue: config.ignoreActiveValue,
|
|
4237
|
+
idMap: createIdMap(oldNode, newContent),
|
|
4238
|
+
deadIds: new Set(),
|
|
4239
|
+
callbacks: config.callbacks,
|
|
4240
|
+
head: config.head
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
3856
4243
|
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
afterNodeMorphed : noOp,
|
|
3871
|
-
beforeNodeRemoved: noOp,
|
|
3872
|
-
afterNodeRemoved : noOp,
|
|
3873
|
-
|
|
3874
|
-
}, config.callbacks),
|
|
3875
|
-
head: Object.assign({
|
|
3876
|
-
style: 'merge',
|
|
3877
|
-
shouldPreserve : function(elt) {
|
|
3878
|
-
return elt.getAttribute("im-preserve") === "true";
|
|
3879
|
-
},
|
|
3880
|
-
shouldReAppend : function(elt) {
|
|
3881
|
-
return elt.getAttribute("im-re-append") === "true";
|
|
3882
|
-
},
|
|
3883
|
-
shouldRemove : noOp,
|
|
3884
|
-
afterHeadMorphed : noOp,
|
|
3885
|
-
}, config.head),
|
|
3886
|
-
}
|
|
3887
|
-
}
|
|
4244
|
+
function isIdSetMatch(node1, node2, ctx) {
|
|
4245
|
+
if (node1 == null || node2 == null) {
|
|
4246
|
+
return false;
|
|
4247
|
+
}
|
|
4248
|
+
if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) {
|
|
4249
|
+
if (node1.id !== "" && node1.id === node2.id) {
|
|
4250
|
+
return true;
|
|
4251
|
+
} else {
|
|
4252
|
+
return getIdIntersectionCount(ctx, node1, node2) > 0;
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4255
|
+
return false;
|
|
4256
|
+
}
|
|
3888
4257
|
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
if (node1.id !== "" && node1.id === node2.id) {
|
|
3895
|
-
return true;
|
|
3896
|
-
} else {
|
|
3897
|
-
return getIdIntersectionCount(ctx, node1, node2) > 0;
|
|
4258
|
+
function isSoftMatch(node1, node2) {
|
|
4259
|
+
if (node1 == null || node2 == null) {
|
|
4260
|
+
return false;
|
|
4261
|
+
}
|
|
4262
|
+
return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName
|
|
3898
4263
|
}
|
|
3899
|
-
}
|
|
3900
|
-
return false;
|
|
3901
|
-
}
|
|
3902
4264
|
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
4265
|
+
function removeNodesBetween(startInclusive, endExclusive, ctx) {
|
|
4266
|
+
while (startInclusive !== endExclusive) {
|
|
4267
|
+
let tempNode = startInclusive;
|
|
4268
|
+
startInclusive = startInclusive.nextSibling;
|
|
4269
|
+
removeNode(tempNode, ctx);
|
|
4270
|
+
}
|
|
4271
|
+
removeIdsFromConsideration(ctx, endExclusive);
|
|
4272
|
+
return endExclusive.nextSibling;
|
|
4273
|
+
}
|
|
3909
4274
|
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
4275
|
+
//=============================================================================
|
|
4276
|
+
// Scans forward from the insertionPoint in the old parent looking for a potential id match
|
|
4277
|
+
// for the newChild. We stop if we find a potential id match for the new child OR
|
|
4278
|
+
// if the number of potential id matches we are discarding is greater than the
|
|
4279
|
+
// potential id matches for the new child
|
|
4280
|
+
//=============================================================================
|
|
4281
|
+
function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
|
|
4282
|
+
|
|
4283
|
+
// max id matches we are willing to discard in our search
|
|
4284
|
+
let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent);
|
|
4285
|
+
|
|
4286
|
+
let potentialMatch = null;
|
|
4287
|
+
|
|
4288
|
+
// only search forward if there is a possibility of an id match
|
|
4289
|
+
if (newChildPotentialIdCount > 0) {
|
|
4290
|
+
let potentialMatch = insertionPoint;
|
|
4291
|
+
// if there is a possibility of an id match, scan forward
|
|
4292
|
+
// keep track of the potential id match count we are discarding (the
|
|
4293
|
+
// newChildPotentialIdCount must be greater than this to make it likely
|
|
4294
|
+
// worth it)
|
|
4295
|
+
let otherMatchCount = 0;
|
|
4296
|
+
while (potentialMatch != null) {
|
|
4297
|
+
|
|
4298
|
+
// If we have an id match, return the current potential match
|
|
4299
|
+
if (isIdSetMatch(newChild, potentialMatch, ctx)) {
|
|
4300
|
+
return potentialMatch;
|
|
4301
|
+
}
|
|
3919
4302
|
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
4303
|
+
// computer the other potential matches of this new content
|
|
4304
|
+
otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent);
|
|
4305
|
+
if (otherMatchCount > newChildPotentialIdCount) {
|
|
4306
|
+
// if we have more potential id matches in _other_ content, we
|
|
4307
|
+
// do not have a good candidate for an id match, so return null
|
|
4308
|
+
return null;
|
|
4309
|
+
}
|
|
3927
4310
|
|
|
3928
|
-
|
|
3929
|
-
|
|
4311
|
+
// advanced to the next old content child
|
|
4312
|
+
potentialMatch = potentialMatch.nextSibling;
|
|
4313
|
+
}
|
|
4314
|
+
}
|
|
4315
|
+
return potentialMatch;
|
|
4316
|
+
}
|
|
3930
4317
|
|
|
3931
|
-
|
|
4318
|
+
//=============================================================================
|
|
4319
|
+
// Scans forward from the insertionPoint in the old parent looking for a potential soft match
|
|
4320
|
+
// for the newChild. We stop if we find a potential soft match for the new child OR
|
|
4321
|
+
// if we find a potential id match in the old parents children OR if we find two
|
|
4322
|
+
// potential soft matches for the next two pieces of new content
|
|
4323
|
+
//=============================================================================
|
|
4324
|
+
function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
|
|
3932
4325
|
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
// if there is a possibility of an id match, scan forward
|
|
3937
|
-
// keep track of the potential id match count we are discarding (the
|
|
3938
|
-
// newChildPotentialIdCount must be greater than this to make it likely
|
|
3939
|
-
// worth it)
|
|
3940
|
-
let otherMatchCount = 0;
|
|
3941
|
-
while (potentialMatch != null) {
|
|
4326
|
+
let potentialSoftMatch = insertionPoint;
|
|
4327
|
+
let nextSibling = newChild.nextSibling;
|
|
4328
|
+
let siblingSoftMatchCount = 0;
|
|
3942
4329
|
|
|
3943
|
-
|
|
3944
|
-
if (isIdSetMatch(newChild, potentialMatch, ctx)) {
|
|
3945
|
-
return potentialMatch;
|
|
3946
|
-
}
|
|
4330
|
+
while (potentialSoftMatch != null) {
|
|
3947
4331
|
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
return null;
|
|
3954
|
-
}
|
|
3955
|
-
|
|
3956
|
-
// advanced to the next old content child
|
|
3957
|
-
potentialMatch = potentialMatch.nextSibling;
|
|
3958
|
-
}
|
|
3959
|
-
}
|
|
3960
|
-
return potentialMatch;
|
|
3961
|
-
}
|
|
4332
|
+
if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) {
|
|
4333
|
+
// the current potential soft match has a potential id set match with the remaining new
|
|
4334
|
+
// content so bail out of looking
|
|
4335
|
+
return null;
|
|
4336
|
+
}
|
|
3962
4337
|
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
// potential soft matches for the next two pieces of new content
|
|
3968
|
-
//=============================================================================
|
|
3969
|
-
function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
|
|
4338
|
+
// if we have a soft match with the current node, return it
|
|
4339
|
+
if (isSoftMatch(newChild, potentialSoftMatch)) {
|
|
4340
|
+
return potentialSoftMatch;
|
|
4341
|
+
}
|
|
3970
4342
|
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
4343
|
+
if (isSoftMatch(nextSibling, potentialSoftMatch)) {
|
|
4344
|
+
// the next new node has a soft match with this node, so
|
|
4345
|
+
// increment the count of future soft matches
|
|
4346
|
+
siblingSoftMatchCount++;
|
|
4347
|
+
nextSibling = nextSibling.nextSibling;
|
|
3974
4348
|
|
|
3975
|
-
|
|
4349
|
+
// If there are two future soft matches, bail to allow the siblings to soft match
|
|
4350
|
+
// so that we don't consume future soft matches for the sake of the current node
|
|
4351
|
+
if (siblingSoftMatchCount >= 2) {
|
|
4352
|
+
return null;
|
|
4353
|
+
}
|
|
4354
|
+
}
|
|
3976
4355
|
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
return null;
|
|
3981
|
-
}
|
|
4356
|
+
// advanced to the next old content child
|
|
4357
|
+
potentialSoftMatch = potentialSoftMatch.nextSibling;
|
|
4358
|
+
}
|
|
3982
4359
|
|
|
3983
|
-
// if we have a soft match with the current node, return it
|
|
3984
|
-
if (isSoftMatch(newChild, potentialSoftMatch)) {
|
|
3985
4360
|
return potentialSoftMatch;
|
|
3986
4361
|
}
|
|
3987
4362
|
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
//
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
return
|
|
4363
|
+
function parseContent(newContent) {
|
|
4364
|
+
let parser = new DOMParser();
|
|
4365
|
+
|
|
4366
|
+
// remove svgs to avoid false-positive matches on head, etc.
|
|
4367
|
+
let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
|
|
4368
|
+
|
|
4369
|
+
// if the newContent contains a html, head or body tag, we can simply parse it w/o wrapping
|
|
4370
|
+
if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) {
|
|
4371
|
+
let content = parser.parseFromString(newContent, "text/html");
|
|
4372
|
+
// if it is a full HTML document, return the document itself as the parent container
|
|
4373
|
+
if (contentWithSvgsRemoved.match(/<\/html>/)) {
|
|
4374
|
+
content.generatedByIdiomorph = true;
|
|
4375
|
+
return content;
|
|
4376
|
+
} else {
|
|
4377
|
+
// otherwise return the html element as the parent container
|
|
4378
|
+
let htmlElement = content.firstChild;
|
|
4379
|
+
if (htmlElement) {
|
|
4380
|
+
htmlElement.generatedByIdiomorph = true;
|
|
4381
|
+
return htmlElement;
|
|
4382
|
+
} else {
|
|
4383
|
+
return null;
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
4386
|
+
} else {
|
|
4387
|
+
// if it is partial HTML, wrap it in a template tag to provide a parent element and also to help
|
|
4388
|
+
// deal with touchy tags like tr, tbody, etc.
|
|
4389
|
+
let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html");
|
|
4390
|
+
let content = responseDoc.body.querySelector('template').content;
|
|
4391
|
+
content.generatedByIdiomorph = true;
|
|
4392
|
+
return content
|
|
3998
4393
|
}
|
|
3999
4394
|
}
|
|
4000
4395
|
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
// if the newContent contains a html, head or body tag, we can simply parse it w/o wrapping
|
|
4015
|
-
if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) {
|
|
4016
|
-
let content = parser.parseFromString(newContent, "text/html");
|
|
4017
|
-
// if it is a full HTML document, return the document itself as the parent container
|
|
4018
|
-
if (contentWithSvgsRemoved.match(/<\/html>/)) {
|
|
4019
|
-
content.generatedByIdiomorph = true;
|
|
4020
|
-
return content;
|
|
4021
|
-
} else {
|
|
4022
|
-
// otherwise return the html element as the parent container
|
|
4023
|
-
let htmlElement = content.firstChild;
|
|
4024
|
-
if (htmlElement) {
|
|
4025
|
-
htmlElement.generatedByIdiomorph = true;
|
|
4026
|
-
return htmlElement;
|
|
4396
|
+
function normalizeContent(newContent) {
|
|
4397
|
+
if (newContent == null) {
|
|
4398
|
+
// noinspection UnnecessaryLocalVariableJS
|
|
4399
|
+
const dummyParent = document.createElement('div');
|
|
4400
|
+
return dummyParent;
|
|
4401
|
+
} else if (newContent.generatedByIdiomorph) {
|
|
4402
|
+
// the template tag created by idiomorph parsing can serve as a dummy parent
|
|
4403
|
+
return newContent;
|
|
4404
|
+
} else if (newContent instanceof Node) {
|
|
4405
|
+
// a single node is added as a child to a dummy parent
|
|
4406
|
+
const dummyParent = document.createElement('div');
|
|
4407
|
+
dummyParent.append(newContent);
|
|
4408
|
+
return dummyParent;
|
|
4027
4409
|
} else {
|
|
4028
|
-
|
|
4410
|
+
// all nodes in the array or HTMLElement collection are consolidated under
|
|
4411
|
+
// a single dummy parent element
|
|
4412
|
+
const dummyParent = document.createElement('div');
|
|
4413
|
+
for (const elt of [...newContent]) {
|
|
4414
|
+
dummyParent.append(elt);
|
|
4415
|
+
}
|
|
4416
|
+
return dummyParent;
|
|
4029
4417
|
}
|
|
4030
4418
|
}
|
|
4031
|
-
} else {
|
|
4032
|
-
// if it is partial HTML, wrap it in a template tag to provide a parent element and also to help
|
|
4033
|
-
// deal with touchy tags like tr, tbody, etc.
|
|
4034
|
-
let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html");
|
|
4035
|
-
let content = responseDoc.body.querySelector('template').content;
|
|
4036
|
-
content.generatedByIdiomorph = true;
|
|
4037
|
-
return content
|
|
4038
|
-
}
|
|
4039
|
-
}
|
|
4040
|
-
|
|
4041
|
-
function normalizeContent(newContent) {
|
|
4042
|
-
if (newContent == null) {
|
|
4043
|
-
// noinspection UnnecessaryLocalVariableJS
|
|
4044
|
-
const dummyParent = document.createElement('div');
|
|
4045
|
-
return dummyParent;
|
|
4046
|
-
} else if (newContent.generatedByIdiomorph) {
|
|
4047
|
-
// the template tag created by idiomorph parsing can serve as a dummy parent
|
|
4048
|
-
return newContent;
|
|
4049
|
-
} else if (newContent instanceof Node) {
|
|
4050
|
-
// a single node is added as a child to a dummy parent
|
|
4051
|
-
const dummyParent = document.createElement('div');
|
|
4052
|
-
dummyParent.append(newContent);
|
|
4053
|
-
return dummyParent;
|
|
4054
|
-
} else {
|
|
4055
|
-
// all nodes in the array or HTMLElement collection are consolidated under
|
|
4056
|
-
// a single dummy parent element
|
|
4057
|
-
const dummyParent = document.createElement('div');
|
|
4058
|
-
for (const elt of [...newContent]) {
|
|
4059
|
-
dummyParent.append(elt);
|
|
4060
|
-
}
|
|
4061
|
-
return dummyParent;
|
|
4062
|
-
}
|
|
4063
|
-
}
|
|
4064
4419
|
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4420
|
+
function insertSiblings(previousSibling, morphedNode, nextSibling) {
|
|
4421
|
+
let stack = [];
|
|
4422
|
+
let added = [];
|
|
4423
|
+
while (previousSibling != null) {
|
|
4424
|
+
stack.push(previousSibling);
|
|
4425
|
+
previousSibling = previousSibling.previousSibling;
|
|
4426
|
+
}
|
|
4427
|
+
while (stack.length > 0) {
|
|
4428
|
+
let node = stack.pop();
|
|
4429
|
+
added.push(node); // push added preceding siblings on in order and insert
|
|
4430
|
+
morphedNode.parentElement.insertBefore(node, morphedNode);
|
|
4431
|
+
}
|
|
4432
|
+
added.push(morphedNode);
|
|
4433
|
+
while (nextSibling != null) {
|
|
4434
|
+
stack.push(nextSibling);
|
|
4435
|
+
added.push(nextSibling); // here we are going in order, so push on as we scan, rather than add
|
|
4436
|
+
nextSibling = nextSibling.nextSibling;
|
|
4437
|
+
}
|
|
4438
|
+
while (stack.length > 0) {
|
|
4439
|
+
morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling);
|
|
4440
|
+
}
|
|
4441
|
+
return added;
|
|
4442
|
+
}
|
|
4088
4443
|
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4444
|
+
function findBestNodeMatch(newContent, oldNode, ctx) {
|
|
4445
|
+
let currentElement;
|
|
4446
|
+
currentElement = newContent.firstChild;
|
|
4447
|
+
let bestElement = currentElement;
|
|
4448
|
+
let score = 0;
|
|
4449
|
+
while (currentElement) {
|
|
4450
|
+
let newScore = scoreElement(currentElement, oldNode, ctx);
|
|
4451
|
+
if (newScore > score) {
|
|
4452
|
+
bestElement = currentElement;
|
|
4453
|
+
score = newScore;
|
|
4454
|
+
}
|
|
4455
|
+
currentElement = currentElement.nextSibling;
|
|
4456
|
+
}
|
|
4457
|
+
return bestElement;
|
|
4099
4458
|
}
|
|
4100
|
-
currentElement = currentElement.nextSibling;
|
|
4101
|
-
}
|
|
4102
|
-
return bestElement;
|
|
4103
|
-
}
|
|
4104
4459
|
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4460
|
+
function scoreElement(node1, node2, ctx) {
|
|
4461
|
+
if (isSoftMatch(node1, node2)) {
|
|
4462
|
+
return .5 + getIdIntersectionCount(ctx, node1, node2);
|
|
4463
|
+
}
|
|
4464
|
+
return 0;
|
|
4465
|
+
}
|
|
4111
4466
|
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4467
|
+
function removeNode(tempNode, ctx) {
|
|
4468
|
+
removeIdsFromConsideration(ctx, tempNode);
|
|
4469
|
+
if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return;
|
|
4115
4470
|
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4471
|
+
tempNode.remove();
|
|
4472
|
+
ctx.callbacks.afterNodeRemoved(tempNode);
|
|
4473
|
+
}
|
|
4119
4474
|
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4475
|
+
//=============================================================================
|
|
4476
|
+
// ID Set Functions
|
|
4477
|
+
//=============================================================================
|
|
4123
4478
|
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4479
|
+
function isIdInConsideration(ctx, id) {
|
|
4480
|
+
return !ctx.deadIds.has(id);
|
|
4481
|
+
}
|
|
4127
4482
|
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4483
|
+
function idIsWithinNode(ctx, id, targetNode) {
|
|
4484
|
+
let idSet = ctx.idMap.get(targetNode) || EMPTY_SET;
|
|
4485
|
+
return idSet.has(id);
|
|
4486
|
+
}
|
|
4132
4487
|
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4488
|
+
function removeIdsFromConsideration(ctx, node) {
|
|
4489
|
+
let idSet = ctx.idMap.get(node) || EMPTY_SET;
|
|
4490
|
+
for (const id of idSet) {
|
|
4491
|
+
ctx.deadIds.add(id);
|
|
4492
|
+
}
|
|
4493
|
+
}
|
|
4139
4494
|
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4495
|
+
function getIdIntersectionCount(ctx, node1, node2) {
|
|
4496
|
+
let sourceSet = ctx.idMap.get(node1) || EMPTY_SET;
|
|
4497
|
+
let matchCount = 0;
|
|
4498
|
+
for (const id of sourceSet) {
|
|
4499
|
+
// a potential match is an id in the source and potentialIdsSet, but
|
|
4500
|
+
// that has not already been merged into the DOM
|
|
4501
|
+
if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) {
|
|
4502
|
+
++matchCount;
|
|
4503
|
+
}
|
|
4504
|
+
}
|
|
4505
|
+
return matchCount;
|
|
4148
4506
|
}
|
|
4149
|
-
}
|
|
4150
|
-
return matchCount;
|
|
4151
|
-
}
|
|
4152
4507
|
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4508
|
+
/**
|
|
4509
|
+
* A bottom up algorithm that finds all elements with ids inside of the node
|
|
4510
|
+
* argument and populates id sets for those nodes and all their parents, generating
|
|
4511
|
+
* a set of ids contained within all nodes for the entire hierarchy in the DOM
|
|
4512
|
+
*
|
|
4513
|
+
* @param node {Element}
|
|
4514
|
+
* @param {Map<Node, Set<String>>} idMap
|
|
4515
|
+
*/
|
|
4516
|
+
function populateIdMapForNode(node, idMap) {
|
|
4517
|
+
let nodeParent = node.parentElement;
|
|
4518
|
+
// find all elements with an id property
|
|
4519
|
+
let idElements = node.querySelectorAll('[id]');
|
|
4520
|
+
for (const elt of idElements) {
|
|
4521
|
+
let current = elt;
|
|
4522
|
+
// walk up the parent hierarchy of that element, adding the id
|
|
4523
|
+
// of element to the parent's id set
|
|
4524
|
+
while (current !== nodeParent && current != null) {
|
|
4525
|
+
let idSet = idMap.get(current);
|
|
4526
|
+
// if the id set doesn't exist, create it and insert it in the map
|
|
4527
|
+
if (idSet == null) {
|
|
4528
|
+
idSet = new Set();
|
|
4529
|
+
idMap.set(current, idSet);
|
|
4530
|
+
}
|
|
4531
|
+
idSet.add(elt.id);
|
|
4532
|
+
current = current.parentElement;
|
|
4533
|
+
}
|
|
4175
4534
|
}
|
|
4176
|
-
idSet.add(elt.id);
|
|
4177
|
-
current = current.parentElement;
|
|
4178
4535
|
}
|
|
4179
|
-
}
|
|
4180
|
-
}
|
|
4181
4536
|
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4537
|
+
/**
|
|
4538
|
+
* This function computes a map of nodes to all ids contained within that node (inclusive of the
|
|
4539
|
+
* node). This map can be used to ask if two nodes have intersecting sets of ids, which allows
|
|
4540
|
+
* for a looser definition of "matching" than tradition id matching, and allows child nodes
|
|
4541
|
+
* to contribute to a parent nodes matching.
|
|
4542
|
+
*
|
|
4543
|
+
* @param {Element} oldContent the old content that will be morphed
|
|
4544
|
+
* @param {Element} newContent the new content to morph to
|
|
4545
|
+
* @returns {Map<Node, Set<String>>} a map of nodes to id sets for the
|
|
4546
|
+
*/
|
|
4547
|
+
function createIdMap(oldContent, newContent) {
|
|
4548
|
+
let idMap = new Map();
|
|
4549
|
+
populateIdMapForNode(oldContent, idMap);
|
|
4550
|
+
populateIdMapForNode(newContent, idMap);
|
|
4551
|
+
return idMap;
|
|
4552
|
+
}
|
|
4198
4553
|
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4554
|
+
//=============================================================================
|
|
4555
|
+
// This is what ends up becoming the Idiomorph global object
|
|
4556
|
+
//=============================================================================
|
|
4557
|
+
return {
|
|
4558
|
+
morph,
|
|
4559
|
+
defaults
|
|
4560
|
+
}
|
|
4561
|
+
})();
|
|
4203
4562
|
|
|
4204
4563
|
class MorphRenderer extends Renderer {
|
|
4205
4564
|
async render() {
|
|
@@ -4227,7 +4586,7 @@ Copyright © 2023 37signals LLC
|
|
|
4227
4586
|
#morphElements(currentElement, newElement, morphStyle = "outerHTML") {
|
|
4228
4587
|
this.isMorphingTurboFrame = this.#isFrameReloadedWithMorph(currentElement);
|
|
4229
4588
|
|
|
4230
|
-
|
|
4589
|
+
Idiomorph.morph(currentElement, newElement, {
|
|
4231
4590
|
morphStyle: morphStyle,
|
|
4232
4591
|
callbacks: {
|
|
4233
4592
|
beforeNodeAdded: this.#shouldAddElement,
|
|
@@ -4359,8 +4718,13 @@ Copyright © 2023 37signals LLC
|
|
|
4359
4718
|
const mergedHeadElements = this.mergeProvisionalElements();
|
|
4360
4719
|
const newStylesheetElements = this.copyNewHeadStylesheetElements();
|
|
4361
4720
|
this.copyNewHeadScriptElements();
|
|
4721
|
+
|
|
4362
4722
|
await mergedHeadElements;
|
|
4363
4723
|
await newStylesheetElements;
|
|
4724
|
+
|
|
4725
|
+
if (this.willRender) {
|
|
4726
|
+
this.removeUnusedHeadStylesheetElements();
|
|
4727
|
+
}
|
|
4364
4728
|
}
|
|
4365
4729
|
|
|
4366
4730
|
async replaceBody() {
|
|
@@ -4392,6 +4756,12 @@ Copyright © 2023 37signals LLC
|
|
|
4392
4756
|
}
|
|
4393
4757
|
}
|
|
4394
4758
|
|
|
4759
|
+
removeUnusedHeadStylesheetElements() {
|
|
4760
|
+
for (const element of this.unusedHeadStylesheetElements) {
|
|
4761
|
+
document.head.removeChild(element);
|
|
4762
|
+
}
|
|
4763
|
+
}
|
|
4764
|
+
|
|
4395
4765
|
async mergeProvisionalElements() {
|
|
4396
4766
|
const newHeadElements = [...this.newHeadProvisionalElements];
|
|
4397
4767
|
|
|
@@ -4457,6 +4827,20 @@ Copyright © 2023 37signals LLC
|
|
|
4457
4827
|
await this.renderElement(this.currentElement, this.newElement);
|
|
4458
4828
|
}
|
|
4459
4829
|
|
|
4830
|
+
get unusedHeadStylesheetElements() {
|
|
4831
|
+
return this.oldHeadStylesheetElements.filter((element) => {
|
|
4832
|
+
return !(element.hasAttribute("data-turbo-permanent") ||
|
|
4833
|
+
// Trix dynamically adds styles to the head that we want to keep around which have a
|
|
4834
|
+
// `data-tag-name` attribute. Long term we should moves those styles to Trix's CSS file
|
|
4835
|
+
// but for now we'll just skip removing them
|
|
4836
|
+
element.hasAttribute("data-tag-name"))
|
|
4837
|
+
})
|
|
4838
|
+
}
|
|
4839
|
+
|
|
4840
|
+
get oldHeadStylesheetElements() {
|
|
4841
|
+
return this.currentHeadSnapshot.getStylesheetElementsNotInSnapshot(this.newHeadSnapshot)
|
|
4842
|
+
}
|
|
4843
|
+
|
|
4460
4844
|
get newHeadStylesheetElements() {
|
|
4461
4845
|
return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot)
|
|
4462
4846
|
}
|
|
@@ -4700,6 +5084,7 @@ Copyright © 2023 37signals LLC
|
|
|
4700
5084
|
|
|
4701
5085
|
pageObserver = new PageObserver(this)
|
|
4702
5086
|
cacheObserver = new CacheObserver()
|
|
5087
|
+
linkPrefetchObserver = new LinkPrefetchObserver(this, document)
|
|
4703
5088
|
linkClickObserver = new LinkClickObserver(this, window)
|
|
4704
5089
|
formSubmitObserver = new FormSubmitObserver(this, document)
|
|
4705
5090
|
scrollObserver = new ScrollObserver(this)
|
|
@@ -4714,16 +5099,20 @@ Copyright © 2023 37signals LLC
|
|
|
4714
5099
|
progressBarDelay = 500
|
|
4715
5100
|
started = false
|
|
4716
5101
|
formMode = "on"
|
|
5102
|
+
#pageRefreshDebouncePeriod = 150
|
|
4717
5103
|
|
|
4718
5104
|
constructor(recentRequests) {
|
|
4719
5105
|
this.recentRequests = recentRequests;
|
|
4720
5106
|
this.preloader = new Preloader(this, this.view.snapshotCache);
|
|
5107
|
+
this.debouncedRefresh = this.refresh;
|
|
5108
|
+
this.pageRefreshDebouncePeriod = this.pageRefreshDebouncePeriod;
|
|
4721
5109
|
}
|
|
4722
5110
|
|
|
4723
5111
|
start() {
|
|
4724
5112
|
if (!this.started) {
|
|
4725
5113
|
this.pageObserver.start();
|
|
4726
5114
|
this.cacheObserver.start();
|
|
5115
|
+
this.linkPrefetchObserver.start();
|
|
4727
5116
|
this.formLinkClickObserver.start();
|
|
4728
5117
|
this.linkClickObserver.start();
|
|
4729
5118
|
this.formSubmitObserver.start();
|
|
@@ -4745,6 +5134,7 @@ Copyright © 2023 37signals LLC
|
|
|
4745
5134
|
if (this.started) {
|
|
4746
5135
|
this.pageObserver.stop();
|
|
4747
5136
|
this.cacheObserver.stop();
|
|
5137
|
+
this.linkPrefetchObserver.stop();
|
|
4748
5138
|
this.formLinkClickObserver.stop();
|
|
4749
5139
|
this.linkClickObserver.stop();
|
|
4750
5140
|
this.formSubmitObserver.stop();
|
|
@@ -4812,6 +5202,15 @@ Copyright © 2023 37signals LLC
|
|
|
4812
5202
|
return this.history.restorationIdentifier
|
|
4813
5203
|
}
|
|
4814
5204
|
|
|
5205
|
+
get pageRefreshDebouncePeriod() {
|
|
5206
|
+
return this.#pageRefreshDebouncePeriod
|
|
5207
|
+
}
|
|
5208
|
+
|
|
5209
|
+
set pageRefreshDebouncePeriod(value) {
|
|
5210
|
+
this.refresh = debounce(this.debouncedRefresh.bind(this), value);
|
|
5211
|
+
this.#pageRefreshDebouncePeriod = value;
|
|
5212
|
+
}
|
|
5213
|
+
|
|
4815
5214
|
// Preloader delegate
|
|
4816
5215
|
|
|
4817
5216
|
shouldPreloadLink(element) {
|
|
@@ -4861,6 +5260,15 @@ Copyright © 2023 37signals LLC
|
|
|
4861
5260
|
|
|
4862
5261
|
submittedFormLinkToLocation() {}
|
|
4863
5262
|
|
|
5263
|
+
// Link hover observer delegate
|
|
5264
|
+
|
|
5265
|
+
canPrefetchRequestToLocation(link, location) {
|
|
5266
|
+
return (
|
|
5267
|
+
this.elementIsNavigatable(link) &&
|
|
5268
|
+
locationIsVisitable(location, this.snapshot.rootLocation)
|
|
5269
|
+
)
|
|
5270
|
+
}
|
|
5271
|
+
|
|
4864
5272
|
// Link click observer delegate
|
|
4865
5273
|
|
|
4866
5274
|
willFollowLinkToLocation(link, location, event) {
|
|
@@ -4960,8 +5368,8 @@ Copyright © 2023 37signals LLC
|
|
|
4960
5368
|
}
|
|
4961
5369
|
}
|
|
4962
5370
|
|
|
4963
|
-
allowsImmediateRender({ element },
|
|
4964
|
-
const event = this.notifyApplicationBeforeRender(element,
|
|
5371
|
+
allowsImmediateRender({ element }, options) {
|
|
5372
|
+
const event = this.notifyApplicationBeforeRender(element, options);
|
|
4965
5373
|
const {
|
|
4966
5374
|
defaultPrevented,
|
|
4967
5375
|
detail: { render }
|
|
@@ -4974,9 +5382,9 @@ Copyright © 2023 37signals LLC
|
|
|
4974
5382
|
return !defaultPrevented
|
|
4975
5383
|
}
|
|
4976
5384
|
|
|
4977
|
-
viewRenderedSnapshot(_snapshot,
|
|
5385
|
+
viewRenderedSnapshot(_snapshot, _isPreview, renderMethod) {
|
|
4978
5386
|
this.view.lastRenderedLocation = this.history.location;
|
|
4979
|
-
this.notifyApplicationAfterRender(
|
|
5387
|
+
this.notifyApplicationAfterRender(renderMethod);
|
|
4980
5388
|
}
|
|
4981
5389
|
|
|
4982
5390
|
preloadOnLoadLinksForView(element) {
|
|
@@ -5032,15 +5440,15 @@ Copyright © 2023 37signals LLC
|
|
|
5032
5440
|
return dispatch("turbo:before-cache")
|
|
5033
5441
|
}
|
|
5034
5442
|
|
|
5035
|
-
notifyApplicationBeforeRender(newBody,
|
|
5443
|
+
notifyApplicationBeforeRender(newBody, options) {
|
|
5036
5444
|
return dispatch("turbo:before-render", {
|
|
5037
|
-
detail: { newBody,
|
|
5445
|
+
detail: { newBody, ...options },
|
|
5038
5446
|
cancelable: true
|
|
5039
5447
|
})
|
|
5040
5448
|
}
|
|
5041
5449
|
|
|
5042
|
-
notifyApplicationAfterRender(
|
|
5043
|
-
return dispatch("turbo:render", { detail: {
|
|
5450
|
+
notifyApplicationAfterRender(renderMethod) {
|
|
5451
|
+
return dispatch("turbo:render", { detail: { renderMethod } })
|
|
5044
5452
|
}
|
|
5045
5453
|
|
|
5046
5454
|
notifyApplicationAfterPageLoad(timing = {}) {
|
|
@@ -5500,7 +5908,7 @@ Copyright © 2023 37signals LLC
|
|
|
5500
5908
|
|
|
5501
5909
|
// View delegate
|
|
5502
5910
|
|
|
5503
|
-
allowsImmediateRender({ element: newFrame },
|
|
5911
|
+
allowsImmediateRender({ element: newFrame }, options) {
|
|
5504
5912
|
const event = dispatch("turbo:before-frame-render", {
|
|
5505
5913
|
target: this.element,
|
|
5506
5914
|
detail: { newFrame, ...options },
|