@hotwired/turbo 8.0.0-beta.2 → 8.0.0-beta.4
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 +1052 -655
- package/dist/turbo.es2017-umd.js +1052 -655
- package/package.json +2 -2
package/dist/turbo.es2017-esm.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
Turbo 8.0.0-beta.
|
|
3
|
-
Copyright ©
|
|
2
|
+
Turbo 8.0.0-beta.4
|
|
3
|
+
Copyright © 2024 37signals LLC
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
6
|
* The MIT License (MIT)
|
|
@@ -633,6 +633,34 @@ async function around(callback, reader) {
|
|
|
633
633
|
return [before, after]
|
|
634
634
|
}
|
|
635
635
|
|
|
636
|
+
function doesNotTargetIFrame(anchor) {
|
|
637
|
+
if (anchor.hasAttribute("target")) {
|
|
638
|
+
for (const element of document.getElementsByName(anchor.target)) {
|
|
639
|
+
if (element instanceof HTMLIFrameElement) return false
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return true
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function findLinkFromClickTarget(target) {
|
|
647
|
+
return findClosestRecursively(target, "a[href]:not([target^=_]):not([download])")
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function getLocationForLink(link) {
|
|
651
|
+
return expandURL(link.getAttribute("href") || "")
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function debounce(fn, delay) {
|
|
655
|
+
let timeoutId = null;
|
|
656
|
+
|
|
657
|
+
return (...args) => {
|
|
658
|
+
const callback = () => fn.apply(this, args);
|
|
659
|
+
clearTimeout(timeoutId);
|
|
660
|
+
timeoutId = setTimeout(callback, delay);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
636
664
|
class LimitedSet extends Set {
|
|
637
665
|
constructor(maxSize) {
|
|
638
666
|
super();
|
|
@@ -783,10 +811,17 @@ class FetchRequest {
|
|
|
783
811
|
async perform() {
|
|
784
812
|
const { fetchOptions } = this;
|
|
785
813
|
this.delegate.prepareRequest(this);
|
|
786
|
-
await this.#allowRequestToBeIntercepted(fetchOptions);
|
|
814
|
+
const event = await this.#allowRequestToBeIntercepted(fetchOptions);
|
|
787
815
|
try {
|
|
788
816
|
this.delegate.requestStarted(this);
|
|
789
|
-
|
|
817
|
+
|
|
818
|
+
if (event.detail.fetchRequest) {
|
|
819
|
+
this.response = event.detail.fetchRequest.response;
|
|
820
|
+
} else {
|
|
821
|
+
this.response = fetchWithTurboHeaders(this.url.href, fetchOptions);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const response = await this.response;
|
|
790
825
|
return await this.receive(response)
|
|
791
826
|
} catch (error) {
|
|
792
827
|
if (error.name !== "AbortError") {
|
|
@@ -848,6 +883,8 @@ class FetchRequest {
|
|
|
848
883
|
});
|
|
849
884
|
this.url = event.detail.url;
|
|
850
885
|
if (event.defaultPrevented) await requestInterception;
|
|
886
|
+
|
|
887
|
+
return event
|
|
851
888
|
}
|
|
852
889
|
|
|
853
890
|
#willDelegateErrorHandling(error) {
|
|
@@ -958,6 +995,41 @@ function importStreamElements(fragment) {
|
|
|
958
995
|
return fragment
|
|
959
996
|
}
|
|
960
997
|
|
|
998
|
+
const PREFETCH_DELAY = 100;
|
|
999
|
+
|
|
1000
|
+
class PrefetchCache {
|
|
1001
|
+
#prefetchTimeout = null
|
|
1002
|
+
#prefetched = null
|
|
1003
|
+
|
|
1004
|
+
get(url) {
|
|
1005
|
+
if (this.#prefetched && this.#prefetched.url === url && this.#prefetched.expire > Date.now()) {
|
|
1006
|
+
return this.#prefetched.request
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
setLater(url, request, ttl) {
|
|
1011
|
+
this.clear();
|
|
1012
|
+
|
|
1013
|
+
this.#prefetchTimeout = setTimeout(() => {
|
|
1014
|
+
request.perform();
|
|
1015
|
+
this.set(url, request, ttl);
|
|
1016
|
+
this.#prefetchTimeout = null;
|
|
1017
|
+
}, PREFETCH_DELAY);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
set(url, request, ttl) {
|
|
1021
|
+
this.#prefetched = { url, request, expire: new Date(new Date().getTime() + ttl) };
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
clear() {
|
|
1025
|
+
if (this.#prefetchTimeout) clearTimeout(this.#prefetchTimeout);
|
|
1026
|
+
this.#prefetched = null;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
const cacheTtl = 10 * 1000;
|
|
1031
|
+
const prefetchCache = new PrefetchCache();
|
|
1032
|
+
|
|
961
1033
|
const FormSubmissionState = {
|
|
962
1034
|
initialized: "initialized",
|
|
963
1035
|
requesting: "requesting",
|
|
@@ -1075,13 +1147,20 @@ class FormSubmission {
|
|
|
1075
1147
|
}
|
|
1076
1148
|
|
|
1077
1149
|
requestPreventedHandlingResponse(request, response) {
|
|
1150
|
+
prefetchCache.clear();
|
|
1151
|
+
|
|
1078
1152
|
this.result = { success: response.succeeded, fetchResponse: response };
|
|
1079
1153
|
}
|
|
1080
1154
|
|
|
1081
1155
|
requestSucceededWithResponse(request, response) {
|
|
1082
1156
|
if (response.clientError || response.serverError) {
|
|
1083
1157
|
this.delegate.formSubmissionFailedWithResponse(this, response);
|
|
1084
|
-
|
|
1158
|
+
return
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
prefetchCache.clear();
|
|
1162
|
+
|
|
1163
|
+
if (this.requestMustRedirect(request) && responseSucceededWithoutRedirect(response)) {
|
|
1085
1164
|
const error = new Error("Form responses must redirect to another location");
|
|
1086
1165
|
this.delegate.formSubmissionErrored(this, error);
|
|
1087
1166
|
} else {
|
|
@@ -1399,7 +1478,7 @@ class View {
|
|
|
1399
1478
|
|
|
1400
1479
|
const renderInterception = new Promise((resolve) => (this.#resolveInterceptionPromise = resolve));
|
|
1401
1480
|
const options = { resume: this.#resolveInterceptionPromise, render: this.renderer.renderElement };
|
|
1402
|
-
const immediateRender = this.delegate.allowsImmediateRender(snapshot,
|
|
1481
|
+
const immediateRender = this.delegate.allowsImmediateRender(snapshot, options);
|
|
1403
1482
|
if (!immediateRender) await renderInterception;
|
|
1404
1483
|
|
|
1405
1484
|
await this.renderSnapshot(renderer);
|
|
@@ -1537,9 +1616,9 @@ class LinkClickObserver {
|
|
|
1537
1616
|
clickBubbled = (event) => {
|
|
1538
1617
|
if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {
|
|
1539
1618
|
const target = (event.composedPath && event.composedPath()[0]) || event.target;
|
|
1540
|
-
const link =
|
|
1619
|
+
const link = findLinkFromClickTarget(target);
|
|
1541
1620
|
if (link && doesNotTargetIFrame(link)) {
|
|
1542
|
-
const location =
|
|
1621
|
+
const location = getLocationForLink(link);
|
|
1543
1622
|
if (this.delegate.willFollowLinkToLocation(link, location, event)) {
|
|
1544
1623
|
event.preventDefault();
|
|
1545
1624
|
this.delegate.followedLinkToLocation(link, location);
|
|
@@ -1559,26 +1638,6 @@ class LinkClickObserver {
|
|
|
1559
1638
|
event.shiftKey
|
|
1560
1639
|
)
|
|
1561
1640
|
}
|
|
1562
|
-
|
|
1563
|
-
findLinkFromClickTarget(target) {
|
|
1564
|
-
return findClosestRecursively(target, "a[href]:not([target^=_]):not([download])")
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
getLocationForLink(link) {
|
|
1568
|
-
return expandURL(link.getAttribute("href") || "")
|
|
1569
|
-
}
|
|
1570
|
-
}
|
|
1571
|
-
|
|
1572
|
-
function doesNotTargetIFrame(anchor) {
|
|
1573
|
-
if (anchor.hasAttribute("target")) {
|
|
1574
|
-
for (const element of document.getElementsByName(anchor.target)) {
|
|
1575
|
-
if (element instanceof HTMLIFrameElement) return false
|
|
1576
|
-
}
|
|
1577
|
-
|
|
1578
|
-
return true
|
|
1579
|
-
} else {
|
|
1580
|
-
return true
|
|
1581
|
-
}
|
|
1582
1641
|
}
|
|
1583
1642
|
|
|
1584
1643
|
class FormLinkClickObserver {
|
|
@@ -1595,6 +1654,16 @@ class FormLinkClickObserver {
|
|
|
1595
1654
|
this.linkInterceptor.stop();
|
|
1596
1655
|
}
|
|
1597
1656
|
|
|
1657
|
+
// Link hover observer delegate
|
|
1658
|
+
|
|
1659
|
+
canPrefetchRequestToLocation(link, location) {
|
|
1660
|
+
return false
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
prefetchAndCacheRequestToLocation(link, location) {
|
|
1664
|
+
return
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1598
1667
|
// Link click observer delegate
|
|
1599
1668
|
|
|
1600
1669
|
willFollowLinkToLocation(link, location, originalEvent) {
|
|
@@ -3008,6 +3077,173 @@ class History {
|
|
|
3008
3077
|
}
|
|
3009
3078
|
}
|
|
3010
3079
|
|
|
3080
|
+
class LinkPrefetchObserver {
|
|
3081
|
+
started = false
|
|
3082
|
+
hoverTriggerEvent = "mouseenter"
|
|
3083
|
+
touchTriggerEvent = "touchstart"
|
|
3084
|
+
|
|
3085
|
+
constructor(delegate, eventTarget) {
|
|
3086
|
+
this.delegate = delegate;
|
|
3087
|
+
this.eventTarget = eventTarget;
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
start() {
|
|
3091
|
+
if (this.started) return
|
|
3092
|
+
|
|
3093
|
+
if (this.eventTarget.readyState === "loading") {
|
|
3094
|
+
this.eventTarget.addEventListener("DOMContentLoaded", this.#enable, { once: true });
|
|
3095
|
+
} else {
|
|
3096
|
+
this.#enable();
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
stop() {
|
|
3101
|
+
if (!this.started) return
|
|
3102
|
+
|
|
3103
|
+
this.eventTarget.removeEventListener(this.hoverTriggerEvent, this.#tryToPrefetchRequest, {
|
|
3104
|
+
capture: true,
|
|
3105
|
+
passive: true
|
|
3106
|
+
});
|
|
3107
|
+
this.eventTarget.removeEventListener(this.touchTriggerEvent, this.#tryToPrefetchRequest, {
|
|
3108
|
+
capture: true,
|
|
3109
|
+
passive: true
|
|
3110
|
+
});
|
|
3111
|
+
this.eventTarget.removeEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true);
|
|
3112
|
+
this.started = false;
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
#enable = () => {
|
|
3116
|
+
this.eventTarget.addEventListener(this.hoverTriggerEvent, this.#tryToPrefetchRequest, {
|
|
3117
|
+
capture: true,
|
|
3118
|
+
passive: true
|
|
3119
|
+
});
|
|
3120
|
+
this.eventTarget.addEventListener(this.touchTriggerEvent, this.#tryToPrefetchRequest, {
|
|
3121
|
+
capture: true,
|
|
3122
|
+
passive: true
|
|
3123
|
+
});
|
|
3124
|
+
this.eventTarget.addEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true);
|
|
3125
|
+
this.started = true;
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
#tryToPrefetchRequest = (event) => {
|
|
3129
|
+
if (getMetaContent("turbo-prefetch") !== "true") return
|
|
3130
|
+
|
|
3131
|
+
const target = event.target;
|
|
3132
|
+
const isLink = target.matches && target.matches("a[href]:not([target^=_]):not([download])");
|
|
3133
|
+
|
|
3134
|
+
if (isLink && this.#isPrefetchable(target)) {
|
|
3135
|
+
const link = target;
|
|
3136
|
+
const location = getLocationForLink(link);
|
|
3137
|
+
|
|
3138
|
+
if (this.delegate.canPrefetchRequestToLocation(link, location)) {
|
|
3139
|
+
const fetchRequest = new FetchRequest(
|
|
3140
|
+
this,
|
|
3141
|
+
FetchMethod.get,
|
|
3142
|
+
location,
|
|
3143
|
+
new URLSearchParams(),
|
|
3144
|
+
target
|
|
3145
|
+
);
|
|
3146
|
+
|
|
3147
|
+
prefetchCache.setLater(location.toString(), fetchRequest, this.#cacheTtl);
|
|
3148
|
+
|
|
3149
|
+
link.addEventListener("mouseleave", () => prefetchCache.clear(), { once: true });
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
#tryToUsePrefetchedRequest = (event) => {
|
|
3155
|
+
if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "get") {
|
|
3156
|
+
const cached = prefetchCache.get(event.detail.url.toString());
|
|
3157
|
+
|
|
3158
|
+
if (cached) {
|
|
3159
|
+
// User clicked link, use cache response
|
|
3160
|
+
event.detail.fetchRequest = cached;
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
prefetchCache.clear();
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
|
|
3167
|
+
prepareRequest(request) {
|
|
3168
|
+
const link = request.target;
|
|
3169
|
+
|
|
3170
|
+
request.headers["Sec-Purpose"] = "prefetch";
|
|
3171
|
+
|
|
3172
|
+
const turboFrame = link.closest("turbo-frame");
|
|
3173
|
+
const turboFrameTarget = link.getAttribute("data-turbo-frame") || turboFrame?.getAttribute("target") || turboFrame?.id;
|
|
3174
|
+
|
|
3175
|
+
if (turboFrameTarget && turboFrameTarget !== "_top") {
|
|
3176
|
+
request.headers["Turbo-Frame"] = turboFrameTarget;
|
|
3177
|
+
}
|
|
3178
|
+
|
|
3179
|
+
if (link.hasAttribute("data-turbo-stream")) {
|
|
3180
|
+
request.acceptResponseType("text/vnd.turbo-stream.html");
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
// Fetch request interface
|
|
3185
|
+
|
|
3186
|
+
requestSucceededWithResponse() {}
|
|
3187
|
+
|
|
3188
|
+
requestStarted(fetchRequest) {}
|
|
3189
|
+
|
|
3190
|
+
requestErrored(fetchRequest) {}
|
|
3191
|
+
|
|
3192
|
+
requestFinished(fetchRequest) {}
|
|
3193
|
+
|
|
3194
|
+
requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}
|
|
3195
|
+
|
|
3196
|
+
requestFailedWithResponse(fetchRequest, fetchResponse) {}
|
|
3197
|
+
|
|
3198
|
+
get #cacheTtl() {
|
|
3199
|
+
return Number(getMetaContent("turbo-prefetch-cache-time")) || cacheTtl
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
#isPrefetchable(link) {
|
|
3203
|
+
const href = link.getAttribute("href");
|
|
3204
|
+
|
|
3205
|
+
if (!href || href === "#" || link.dataset.turbo === "false" || link.dataset.turboPrefetch === "false") {
|
|
3206
|
+
return false
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
if (link.origin !== document.location.origin) {
|
|
3210
|
+
return false
|
|
3211
|
+
}
|
|
3212
|
+
|
|
3213
|
+
if (!["http:", "https:"].includes(link.protocol)) {
|
|
3214
|
+
return false
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
if (link.pathname + link.search === document.location.pathname + document.location.search) {
|
|
3218
|
+
return false
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
if (link.dataset.turboMethod && link.dataset.turboMethod !== "get") {
|
|
3222
|
+
return false
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
if (targetsIframe(link)) {
|
|
3226
|
+
return false
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
if (link.pathname + link.search === document.location.pathname + document.location.search) {
|
|
3230
|
+
return false
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
const turboPrefetchParent = findClosestRecursively(link, "[data-turbo-prefetch]");
|
|
3234
|
+
|
|
3235
|
+
if (turboPrefetchParent && turboPrefetchParent.dataset.turboPrefetch === "false") {
|
|
3236
|
+
return false
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
return true
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
const targetsIframe = (link) => {
|
|
3244
|
+
return !doesNotTargetIFrame(link)
|
|
3245
|
+
};
|
|
3246
|
+
|
|
3011
3247
|
class Navigator {
|
|
3012
3248
|
constructor(delegate) {
|
|
3013
3249
|
this.delegate = delegate;
|
|
@@ -3478,722 +3714,838 @@ class ErrorRenderer extends Renderer {
|
|
|
3478
3714
|
}
|
|
3479
3715
|
}
|
|
3480
3716
|
|
|
3481
|
-
|
|
3717
|
+
// base IIFE to define idiomorph
|
|
3718
|
+
var Idiomorph = (function () {
|
|
3482
3719
|
|
|
3483
|
-
//=============================================================================
|
|
3484
|
-
//
|
|
3485
|
-
//=============================================================================
|
|
3486
|
-
|
|
3720
|
+
//=============================================================================
|
|
3721
|
+
// AND NOW IT BEGINS...
|
|
3722
|
+
//=============================================================================
|
|
3723
|
+
let EMPTY_SET = new Set();
|
|
3487
3724
|
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3725
|
+
// default configuration values, updatable by users now
|
|
3726
|
+
let defaults = {
|
|
3727
|
+
morphStyle: "outerHTML",
|
|
3728
|
+
callbacks : {
|
|
3729
|
+
beforeNodeAdded: noOp,
|
|
3730
|
+
afterNodeAdded: noOp,
|
|
3731
|
+
beforeNodeMorphed: noOp,
|
|
3732
|
+
afterNodeMorphed: noOp,
|
|
3733
|
+
beforeNodeRemoved: noOp,
|
|
3734
|
+
afterNodeRemoved: noOp,
|
|
3735
|
+
beforeAttributeUpdated: noOp,
|
|
3495
3736
|
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
}
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
// when head promises resolve, call morph again, ignoring the head tag
|
|
3510
|
-
Promise.all(promises).then(function () {
|
|
3511
|
-
morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, {
|
|
3512
|
-
head: {
|
|
3513
|
-
block: false,
|
|
3514
|
-
ignore: true
|
|
3515
|
-
}
|
|
3516
|
-
}));
|
|
3517
|
-
});
|
|
3518
|
-
return;
|
|
3519
|
-
}
|
|
3520
|
-
}
|
|
3737
|
+
},
|
|
3738
|
+
head: {
|
|
3739
|
+
style: 'merge',
|
|
3740
|
+
shouldPreserve: function (elt) {
|
|
3741
|
+
return elt.getAttribute("im-preserve") === "true";
|
|
3742
|
+
},
|
|
3743
|
+
shouldReAppend: function (elt) {
|
|
3744
|
+
return elt.getAttribute("im-re-append") === "true";
|
|
3745
|
+
},
|
|
3746
|
+
shouldRemove: noOp,
|
|
3747
|
+
afterHeadMorphed: noOp,
|
|
3748
|
+
}
|
|
3749
|
+
};
|
|
3521
3750
|
|
|
3522
|
-
|
|
3751
|
+
//=============================================================================
|
|
3752
|
+
// Core Morphing Algorithm - morph, morphNormalizedContent, morphOldNodeTo, morphChildren
|
|
3753
|
+
//=============================================================================
|
|
3754
|
+
function morph(oldNode, newContent, config = {}) {
|
|
3523
3755
|
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3756
|
+
if (oldNode instanceof Document) {
|
|
3757
|
+
oldNode = oldNode.documentElement;
|
|
3758
|
+
}
|
|
3527
3759
|
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx);
|
|
3760
|
+
if (typeof newContent === 'string') {
|
|
3761
|
+
newContent = parseContent(newContent);
|
|
3762
|
+
}
|
|
3532
3763
|
|
|
3533
|
-
|
|
3534
|
-
let previousSibling = bestMatch?.previousSibling;
|
|
3535
|
-
let nextSibling = bestMatch?.nextSibling;
|
|
3764
|
+
let normalizedContent = normalizeContent(newContent);
|
|
3536
3765
|
|
|
3537
|
-
|
|
3538
|
-
let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx);
|
|
3766
|
+
let ctx = createMorphContext(oldNode, normalizedContent, config);
|
|
3539
3767
|
|
|
3540
|
-
|
|
3541
|
-
// if there was a best match, merge the siblings in too and return the
|
|
3542
|
-
// whole bunch
|
|
3543
|
-
return insertSiblings(previousSibling, morphedNode, nextSibling);
|
|
3544
|
-
} else {
|
|
3545
|
-
// otherwise nothing was added to the DOM
|
|
3546
|
-
return []
|
|
3768
|
+
return morphNormalizedContent(oldNode, normalizedContent, ctx);
|
|
3547
3769
|
}
|
|
3548
|
-
} else {
|
|
3549
|
-
throw "Do not understand how to morph style " + ctx.morphStyle;
|
|
3550
|
-
}
|
|
3551
|
-
}
|
|
3552
|
-
|
|
3553
3770
|
|
|
3771
|
+
function morphNormalizedContent(oldNode, normalizedNewContent, ctx) {
|
|
3772
|
+
if (ctx.head.block) {
|
|
3773
|
+
let oldHead = oldNode.querySelector('head');
|
|
3774
|
+
let newHead = normalizedNewContent.querySelector('head');
|
|
3775
|
+
if (oldHead && newHead) {
|
|
3776
|
+
let promises = handleHeadElement(newHead, oldHead, ctx);
|
|
3777
|
+
// when head promises resolve, call morph again, ignoring the head tag
|
|
3778
|
+
Promise.all(promises).then(function () {
|
|
3779
|
+
morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, {
|
|
3780
|
+
head: {
|
|
3781
|
+
block: false,
|
|
3782
|
+
ignore: true
|
|
3783
|
+
}
|
|
3784
|
+
}));
|
|
3785
|
+
});
|
|
3786
|
+
return;
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3554
3789
|
|
|
3555
|
-
|
|
3556
|
-
* @param oldNode root node to merge content into
|
|
3557
|
-
* @param newContent new content to merge
|
|
3558
|
-
* @param ctx the merge context
|
|
3559
|
-
* @returns {Element} the element that ended up in the DOM
|
|
3560
|
-
*/
|
|
3561
|
-
function morphOldNodeTo(oldNode, newContent, ctx) {
|
|
3562
|
-
if (ctx.ignoreActive && oldNode === document.activeElement) ; else if (newContent == null) {
|
|
3563
|
-
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return;
|
|
3564
|
-
|
|
3565
|
-
oldNode.remove();
|
|
3566
|
-
ctx.callbacks.afterNodeRemoved(oldNode);
|
|
3567
|
-
return null;
|
|
3568
|
-
} else if (!isSoftMatch(oldNode, newContent)) {
|
|
3569
|
-
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return;
|
|
3570
|
-
if (ctx.callbacks.beforeNodeAdded(newContent) === false) return;
|
|
3571
|
-
|
|
3572
|
-
oldNode.parentElement.replaceChild(newContent, oldNode);
|
|
3573
|
-
ctx.callbacks.afterNodeAdded(newContent);
|
|
3574
|
-
ctx.callbacks.afterNodeRemoved(oldNode);
|
|
3575
|
-
return newContent;
|
|
3576
|
-
} else {
|
|
3577
|
-
if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return;
|
|
3790
|
+
if (ctx.morphStyle === "innerHTML") {
|
|
3578
3791
|
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
syncNodeFrom(newContent, oldNode);
|
|
3583
|
-
morphChildren(newContent, oldNode, ctx);
|
|
3584
|
-
}
|
|
3585
|
-
ctx.callbacks.afterNodeMorphed(oldNode, newContent);
|
|
3586
|
-
return oldNode;
|
|
3587
|
-
}
|
|
3588
|
-
}
|
|
3792
|
+
// innerHTML, so we are only updating the children
|
|
3793
|
+
morphChildren(normalizedNewContent, oldNode, ctx);
|
|
3794
|
+
return oldNode.children;
|
|
3589
3795
|
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
*
|
|
3595
|
-
* Basic algorithm is, for each node in the new content:
|
|
3596
|
-
*
|
|
3597
|
-
* - if we have reached the end of the old parent, append the new content
|
|
3598
|
-
* - if the new content has an id set match with the current insertion point, morph
|
|
3599
|
-
* - search for an id set match
|
|
3600
|
-
* - if id set match found, morph
|
|
3601
|
-
* - otherwise search for a "soft" match
|
|
3602
|
-
* - if a soft match is found, morph
|
|
3603
|
-
* - otherwise, prepend the new node before the current insertion point
|
|
3604
|
-
*
|
|
3605
|
-
* The two search algorithms terminate if competing node matches appear to outweigh what can be achieved
|
|
3606
|
-
* with the current node. See findIdSetMatch() and findSoftMatch() for details.
|
|
3607
|
-
*
|
|
3608
|
-
* @param {Element} newParent the parent element of the new content
|
|
3609
|
-
* @param {Element } oldParent the old content that we are merging the new content into
|
|
3610
|
-
* @param ctx the merge context
|
|
3611
|
-
*/
|
|
3612
|
-
function morphChildren(newParent, oldParent, ctx) {
|
|
3796
|
+
} else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) {
|
|
3797
|
+
// otherwise find the best element match in the new content, morph that, and merge its siblings
|
|
3798
|
+
// into either side of the best match
|
|
3799
|
+
let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx);
|
|
3613
3800
|
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3801
|
+
// stash the siblings that will need to be inserted on either side of the best match
|
|
3802
|
+
let previousSibling = bestMatch?.previousSibling;
|
|
3803
|
+
let nextSibling = bestMatch?.nextSibling;
|
|
3617
3804
|
|
|
3618
|
-
|
|
3619
|
-
|
|
3805
|
+
// morph it
|
|
3806
|
+
let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx);
|
|
3620
3807
|
|
|
3621
|
-
|
|
3622
|
-
|
|
3808
|
+
if (bestMatch) {
|
|
3809
|
+
// if there was a best match, merge the siblings in too and return the
|
|
3810
|
+
// whole bunch
|
|
3811
|
+
return insertSiblings(previousSibling, morphedNode, nextSibling);
|
|
3812
|
+
} else {
|
|
3813
|
+
// otherwise nothing was added to the DOM
|
|
3814
|
+
return []
|
|
3815
|
+
}
|
|
3816
|
+
} else {
|
|
3817
|
+
throw "Do not understand how to morph style " + ctx.morphStyle;
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3623
3820
|
|
|
3624
|
-
// if we are at the end of the exiting parent's children, just append
|
|
3625
|
-
if (insertionPoint == null) {
|
|
3626
|
-
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
|
3627
3821
|
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3822
|
+
/**
|
|
3823
|
+
* @param possibleActiveElement
|
|
3824
|
+
* @param ctx
|
|
3825
|
+
* @returns {boolean}
|
|
3826
|
+
*/
|
|
3827
|
+
function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
|
|
3828
|
+
return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement;
|
|
3632
3829
|
}
|
|
3633
3830
|
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3831
|
+
/**
|
|
3832
|
+
* @param oldNode root node to merge content into
|
|
3833
|
+
* @param newContent new content to merge
|
|
3834
|
+
* @param ctx the merge context
|
|
3835
|
+
* @returns {Element} the element that ended up in the DOM
|
|
3836
|
+
*/
|
|
3837
|
+
function morphOldNodeTo(oldNode, newContent, ctx) {
|
|
3838
|
+
if (ctx.ignoreActive && oldNode === document.activeElement) ; else if (newContent == null) {
|
|
3839
|
+
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
|
|
3840
|
+
|
|
3841
|
+
oldNode.remove();
|
|
3842
|
+
ctx.callbacks.afterNodeRemoved(oldNode);
|
|
3843
|
+
return null;
|
|
3844
|
+
} else if (!isSoftMatch(oldNode, newContent)) {
|
|
3845
|
+
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
|
|
3846
|
+
if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode;
|
|
3847
|
+
|
|
3848
|
+
oldNode.parentElement.replaceChild(newContent, oldNode);
|
|
3849
|
+
ctx.callbacks.afterNodeAdded(newContent);
|
|
3850
|
+
ctx.callbacks.afterNodeRemoved(oldNode);
|
|
3851
|
+
return newContent;
|
|
3852
|
+
} else {
|
|
3853
|
+
if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return oldNode;
|
|
3854
|
+
|
|
3855
|
+
if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) ; else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") {
|
|
3856
|
+
handleHeadElement(newContent, oldNode, ctx);
|
|
3857
|
+
} else {
|
|
3858
|
+
syncNodeFrom(newContent, oldNode, ctx);
|
|
3859
|
+
if (!ignoreValueOfActiveElement(oldNode, ctx)) {
|
|
3860
|
+
morphChildren(newContent, oldNode, ctx);
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
ctx.callbacks.afterNodeMorphed(oldNode, newContent);
|
|
3864
|
+
return oldNode;
|
|
3865
|
+
}
|
|
3640
3866
|
}
|
|
3641
3867
|
|
|
3642
|
-
|
|
3643
|
-
|
|
3868
|
+
/**
|
|
3869
|
+
* This is the core algorithm for matching up children. The idea is to use id sets to try to match up
|
|
3870
|
+
* nodes as faithfully as possible. We greedily match, which allows us to keep the algorithm fast, but
|
|
3871
|
+
* by using id sets, we are able to better match up with content deeper in the DOM.
|
|
3872
|
+
*
|
|
3873
|
+
* Basic algorithm is, for each node in the new content:
|
|
3874
|
+
*
|
|
3875
|
+
* - if we have reached the end of the old parent, append the new content
|
|
3876
|
+
* - if the new content has an id set match with the current insertion point, morph
|
|
3877
|
+
* - search for an id set match
|
|
3878
|
+
* - if id set match found, morph
|
|
3879
|
+
* - otherwise search for a "soft" match
|
|
3880
|
+
* - if a soft match is found, morph
|
|
3881
|
+
* - otherwise, prepend the new node before the current insertion point
|
|
3882
|
+
*
|
|
3883
|
+
* The two search algorithms terminate if competing node matches appear to outweigh what can be achieved
|
|
3884
|
+
* with the current node. See findIdSetMatch() and findSoftMatch() for details.
|
|
3885
|
+
*
|
|
3886
|
+
* @param {Element} newParent the parent element of the new content
|
|
3887
|
+
* @param {Element } oldParent the old content that we are merging the new content into
|
|
3888
|
+
* @param ctx the merge context
|
|
3889
|
+
*/
|
|
3890
|
+
function morphChildren(newParent, oldParent, ctx) {
|
|
3891
|
+
|
|
3892
|
+
let nextNewChild = newParent.firstChild;
|
|
3893
|
+
let insertionPoint = oldParent.firstChild;
|
|
3894
|
+
let newChild;
|
|
3895
|
+
|
|
3896
|
+
// run through all the new content
|
|
3897
|
+
while (nextNewChild) {
|
|
3898
|
+
|
|
3899
|
+
newChild = nextNewChild;
|
|
3900
|
+
nextNewChild = newChild.nextSibling;
|
|
3901
|
+
|
|
3902
|
+
// if we are at the end of the exiting parent's children, just append
|
|
3903
|
+
if (insertionPoint == null) {
|
|
3904
|
+
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
|
3905
|
+
|
|
3906
|
+
oldParent.appendChild(newChild);
|
|
3907
|
+
ctx.callbacks.afterNodeAdded(newChild);
|
|
3908
|
+
removeIdsFromConsideration(ctx, newChild);
|
|
3909
|
+
continue;
|
|
3910
|
+
}
|
|
3644
3911
|
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3912
|
+
// if the current node has an id set match then morph
|
|
3913
|
+
if (isIdSetMatch(newChild, insertionPoint, ctx)) {
|
|
3914
|
+
morphOldNodeTo(insertionPoint, newChild, ctx);
|
|
3915
|
+
insertionPoint = insertionPoint.nextSibling;
|
|
3916
|
+
removeIdsFromConsideration(ctx, newChild);
|
|
3917
|
+
continue;
|
|
3918
|
+
}
|
|
3652
3919
|
|
|
3653
|
-
|
|
3654
|
-
|
|
3920
|
+
// otherwise search forward in the existing old children for an id set match
|
|
3921
|
+
let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx);
|
|
3655
3922
|
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3923
|
+
// if we found a potential match, remove the nodes until that point and morph
|
|
3924
|
+
if (idSetMatch) {
|
|
3925
|
+
insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx);
|
|
3926
|
+
morphOldNodeTo(idSetMatch, newChild, ctx);
|
|
3927
|
+
removeIdsFromConsideration(ctx, newChild);
|
|
3928
|
+
continue;
|
|
3929
|
+
}
|
|
3663
3930
|
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
|
3931
|
+
// no id set match found, so scan forward for a soft match for the current node
|
|
3932
|
+
let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx);
|
|
3667
3933
|
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3934
|
+
// if we found a soft match for the current node, morph
|
|
3935
|
+
if (softMatch) {
|
|
3936
|
+
insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx);
|
|
3937
|
+
morphOldNodeTo(softMatch, newChild, ctx);
|
|
3938
|
+
removeIdsFromConsideration(ctx, newChild);
|
|
3939
|
+
continue;
|
|
3940
|
+
}
|
|
3672
3941
|
|
|
3673
|
-
|
|
3674
|
-
|
|
3942
|
+
// abandon all hope of morphing, just insert the new child before the insertion point
|
|
3943
|
+
// and move on
|
|
3944
|
+
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
|
3675
3945
|
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
}
|
|
3946
|
+
oldParent.insertBefore(newChild, insertionPoint);
|
|
3947
|
+
ctx.callbacks.afterNodeAdded(newChild);
|
|
3948
|
+
removeIdsFromConsideration(ctx, newChild);
|
|
3949
|
+
}
|
|
3681
3950
|
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
//=============================================================================
|
|
3951
|
+
// remove any remaining old nodes that didn't match up with new content
|
|
3952
|
+
while (insertionPoint !== null) {
|
|
3685
3953
|
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
*
|
|
3690
|
-
* @param {Element} from the element to copy attributes & state from
|
|
3691
|
-
* @param {Element} to the element to copy attributes & state to
|
|
3692
|
-
*/
|
|
3693
|
-
function syncNodeFrom(from, to) {
|
|
3694
|
-
let type = from.nodeType;
|
|
3695
|
-
|
|
3696
|
-
// if is an element type, sync the attributes from the
|
|
3697
|
-
// new node into the new node
|
|
3698
|
-
if (type === 1 /* element type */) {
|
|
3699
|
-
const fromAttributes = from.attributes;
|
|
3700
|
-
const toAttributes = to.attributes;
|
|
3701
|
-
for (const fromAttribute of fromAttributes) {
|
|
3702
|
-
if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) {
|
|
3703
|
-
to.setAttribute(fromAttribute.name, fromAttribute.value);
|
|
3954
|
+
let tempNode = insertionPoint;
|
|
3955
|
+
insertionPoint = insertionPoint.nextSibling;
|
|
3956
|
+
removeNode(tempNode, ctx);
|
|
3704
3957
|
}
|
|
3705
3958
|
}
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3959
|
+
|
|
3960
|
+
//=============================================================================
|
|
3961
|
+
// Attribute Syncing Code
|
|
3962
|
+
//=============================================================================
|
|
3963
|
+
|
|
3964
|
+
/**
|
|
3965
|
+
* @param attr {String} the attribute to be mutated
|
|
3966
|
+
* @param to {Element} the element that is going to be updated
|
|
3967
|
+
* @param updateType {("update"|"remove")}
|
|
3968
|
+
* @param ctx the merge context
|
|
3969
|
+
* @returns {boolean} true if the attribute should be ignored, false otherwise
|
|
3970
|
+
*/
|
|
3971
|
+
function ignoreAttribute(attr, to, updateType, ctx) {
|
|
3972
|
+
if(attr === 'value' && ctx.ignoreActiveValue && to === document.activeElement){
|
|
3973
|
+
return true;
|
|
3709
3974
|
}
|
|
3975
|
+
return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false;
|
|
3710
3976
|
}
|
|
3711
|
-
}
|
|
3712
3977
|
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3978
|
+
/**
|
|
3979
|
+
* syncs a given node with another node, copying over all attributes and
|
|
3980
|
+
* inner element state from the 'from' node to the 'to' node
|
|
3981
|
+
*
|
|
3982
|
+
* @param {Element} from the element to copy attributes & state from
|
|
3983
|
+
* @param {Element} to the element to copy attributes & state to
|
|
3984
|
+
* @param ctx the merge context
|
|
3985
|
+
*/
|
|
3986
|
+
function syncNodeFrom(from, to, ctx) {
|
|
3987
|
+
let type = from.nodeType;
|
|
3988
|
+
|
|
3989
|
+
// if is an element type, sync the attributes from the
|
|
3990
|
+
// new node into the new node
|
|
3991
|
+
if (type === 1 /* element type */) {
|
|
3992
|
+
const fromAttributes = from.attributes;
|
|
3993
|
+
const toAttributes = to.attributes;
|
|
3994
|
+
for (const fromAttribute of fromAttributes) {
|
|
3995
|
+
if (ignoreAttribute(fromAttribute.name, to, 'update', ctx)) {
|
|
3996
|
+
continue;
|
|
3997
|
+
}
|
|
3998
|
+
if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) {
|
|
3999
|
+
to.setAttribute(fromAttribute.name, fromAttribute.value);
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
// iterate backwards to avoid skipping over items when a delete occurs
|
|
4003
|
+
for (let i = toAttributes.length - 1; 0 <= i; i--) {
|
|
4004
|
+
const toAttribute = toAttributes[i];
|
|
4005
|
+
if (ignoreAttribute(toAttribute.name, to, 'remove', ctx)) {
|
|
4006
|
+
continue;
|
|
4007
|
+
}
|
|
4008
|
+
if (!from.hasAttribute(toAttribute.name)) {
|
|
4009
|
+
to.removeAttribute(toAttribute.name);
|
|
4010
|
+
}
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
3719
4013
|
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
if (from instanceof HTMLInputElement &&
|
|
3727
|
-
to instanceof HTMLInputElement &&
|
|
3728
|
-
from.type !== 'file') {
|
|
3729
|
-
|
|
3730
|
-
to.value = from.value || '';
|
|
3731
|
-
syncAttribute(from, to, 'value');
|
|
3732
|
-
|
|
3733
|
-
// sync boolean attributes
|
|
3734
|
-
syncAttribute(from, to, 'checked');
|
|
3735
|
-
syncAttribute(from, to, 'disabled');
|
|
3736
|
-
} else if (from instanceof HTMLOptionElement) {
|
|
3737
|
-
syncAttribute(from, to, 'selected');
|
|
3738
|
-
} else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) {
|
|
3739
|
-
let fromValue = from.value;
|
|
3740
|
-
let toValue = to.value;
|
|
3741
|
-
if (fromValue !== toValue) {
|
|
3742
|
-
to.value = fromValue;
|
|
3743
|
-
}
|
|
3744
|
-
if (to.firstChild && to.firstChild.nodeValue !== fromValue) {
|
|
3745
|
-
to.firstChild.nodeValue = fromValue;
|
|
3746
|
-
}
|
|
3747
|
-
}
|
|
3748
|
-
}
|
|
4014
|
+
// sync text nodes
|
|
4015
|
+
if (type === 8 /* comment */ || type === 3 /* text */) {
|
|
4016
|
+
if (to.nodeValue !== from.nodeValue) {
|
|
4017
|
+
to.nodeValue = from.nodeValue;
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
3749
4020
|
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
} else {
|
|
3755
|
-
to.removeAttribute(attributeName);
|
|
4021
|
+
if (!ignoreValueOfActiveElement(to, ctx)) {
|
|
4022
|
+
// sync input values
|
|
4023
|
+
syncInputValue(from, to, ctx);
|
|
4024
|
+
}
|
|
3756
4025
|
}
|
|
3757
|
-
}
|
|
3758
|
-
}
|
|
3759
4026
|
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
4027
|
+
/**
|
|
4028
|
+
* @param from {Element} element to sync the value from
|
|
4029
|
+
* @param to {Element} element to sync the value to
|
|
4030
|
+
* @param attributeName {String} the attribute name
|
|
4031
|
+
* @param ctx the merge context
|
|
4032
|
+
*/
|
|
4033
|
+
function syncBooleanAttribute(from, to, attributeName, ctx) {
|
|
4034
|
+
if (from[attributeName] !== to[attributeName]) {
|
|
4035
|
+
let ignoreUpdate = ignoreAttribute(attributeName, to, 'update', ctx);
|
|
4036
|
+
if (!ignoreUpdate) {
|
|
4037
|
+
to[attributeName] = from[attributeName];
|
|
4038
|
+
}
|
|
4039
|
+
if (from[attributeName]) {
|
|
4040
|
+
if (!ignoreUpdate) {
|
|
4041
|
+
to.setAttribute(attributeName, from[attributeName]);
|
|
4042
|
+
}
|
|
4043
|
+
} else {
|
|
4044
|
+
if (!ignoreAttribute(attributeName, to, 'remove', ctx)) {
|
|
4045
|
+
to.removeAttribute(attributeName);
|
|
4046
|
+
}
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
4049
|
+
}
|
|
3764
4050
|
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
4051
|
+
/**
|
|
4052
|
+
* NB: many bothans died to bring us information:
|
|
4053
|
+
*
|
|
4054
|
+
* https://github.com/patrick-steele-idem/morphdom/blob/master/src/specialElHandlers.js
|
|
4055
|
+
* https://github.com/choojs/nanomorph/blob/master/lib/morph.jsL113
|
|
4056
|
+
*
|
|
4057
|
+
* @param from {Element} the element to sync the input value from
|
|
4058
|
+
* @param to {Element} the element to sync the input value to
|
|
4059
|
+
* @param ctx the merge context
|
|
4060
|
+
*/
|
|
4061
|
+
function syncInputValue(from, to, ctx) {
|
|
4062
|
+
if (from instanceof HTMLInputElement &&
|
|
4063
|
+
to instanceof HTMLInputElement &&
|
|
4064
|
+
from.type !== 'file') {
|
|
4065
|
+
|
|
4066
|
+
let fromValue = from.value;
|
|
4067
|
+
let toValue = to.value;
|
|
4068
|
+
|
|
4069
|
+
// sync boolean attributes
|
|
4070
|
+
syncBooleanAttribute(from, to, 'checked', ctx);
|
|
4071
|
+
syncBooleanAttribute(from, to, 'disabled', ctx);
|
|
4072
|
+
|
|
4073
|
+
if (!from.hasAttribute('value')) {
|
|
4074
|
+
if (!ignoreAttribute('value', to, 'remove', ctx)) {
|
|
4075
|
+
to.value = '';
|
|
4076
|
+
to.removeAttribute('value');
|
|
4077
|
+
}
|
|
4078
|
+
} else if (fromValue !== toValue) {
|
|
4079
|
+
if (!ignoreAttribute('value', to, 'update', ctx)) {
|
|
4080
|
+
to.setAttribute('value', fromValue);
|
|
4081
|
+
to.value = fromValue;
|
|
4082
|
+
}
|
|
4083
|
+
}
|
|
4084
|
+
} else if (from instanceof HTMLOptionElement) {
|
|
4085
|
+
syncBooleanAttribute(from, to, 'selected', ctx);
|
|
4086
|
+
} else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) {
|
|
4087
|
+
let fromValue = from.value;
|
|
4088
|
+
let toValue = to.value;
|
|
4089
|
+
if (ignoreAttribute('value', to, 'update', ctx)) {
|
|
4090
|
+
return;
|
|
4091
|
+
}
|
|
4092
|
+
if (fromValue !== toValue) {
|
|
4093
|
+
to.value = fromValue;
|
|
4094
|
+
}
|
|
4095
|
+
if (to.firstChild && to.firstChild.nodeValue !== fromValue) {
|
|
4096
|
+
to.firstChild.nodeValue = fromValue;
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
3769
4100
|
|
|
3770
|
-
|
|
4101
|
+
//=============================================================================
|
|
4102
|
+
// the HEAD tag can be handled specially, either w/ a 'merge' or 'append' style
|
|
4103
|
+
//=============================================================================
|
|
4104
|
+
function handleHeadElement(newHeadTag, currentHead, ctx) {
|
|
3771
4105
|
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
}
|
|
4106
|
+
let added = [];
|
|
4107
|
+
let removed = [];
|
|
4108
|
+
let preserved = [];
|
|
4109
|
+
let nodesToAppend = [];
|
|
3777
4110
|
|
|
3778
|
-
|
|
3779
|
-
for (const currentHeadElt of currentHead.children) {
|
|
4111
|
+
let headMergeStyle = ctx.head.style;
|
|
3780
4112
|
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
if (inNewContent || isPreserved) {
|
|
3786
|
-
if (isReAppended) {
|
|
3787
|
-
// remove the current version and let the new version replace it and re-execute
|
|
3788
|
-
removed.push(currentHeadElt);
|
|
3789
|
-
} else {
|
|
3790
|
-
// this element already exists and should not be re-appended, so remove it from
|
|
3791
|
-
// the new content map, preserving it in the DOM
|
|
3792
|
-
srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
|
|
3793
|
-
preserved.push(currentHeadElt);
|
|
4113
|
+
// put all new head elements into a Map, by their outerHTML
|
|
4114
|
+
let srcToNewHeadNodes = new Map();
|
|
4115
|
+
for (const newHeadChild of newHeadTag.children) {
|
|
4116
|
+
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
|
|
3794
4117
|
}
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
4118
|
+
|
|
4119
|
+
// for each elt in the current head
|
|
4120
|
+
for (const currentHeadElt of currentHead.children) {
|
|
4121
|
+
|
|
4122
|
+
// If the current head element is in the map
|
|
4123
|
+
let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
|
|
4124
|
+
let isReAppended = ctx.head.shouldReAppend(currentHeadElt);
|
|
4125
|
+
let isPreserved = ctx.head.shouldPreserve(currentHeadElt);
|
|
4126
|
+
if (inNewContent || isPreserved) {
|
|
4127
|
+
if (isReAppended) {
|
|
4128
|
+
// remove the current version and let the new version replace it and re-execute
|
|
4129
|
+
removed.push(currentHeadElt);
|
|
4130
|
+
} else {
|
|
4131
|
+
// this element already exists and should not be re-appended, so remove it from
|
|
4132
|
+
// the new content map, preserving it in the DOM
|
|
4133
|
+
srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
|
|
4134
|
+
preserved.push(currentHeadElt);
|
|
4135
|
+
}
|
|
4136
|
+
} else {
|
|
4137
|
+
if (headMergeStyle === "append") {
|
|
4138
|
+
// we are appending and this existing element is not new content
|
|
4139
|
+
// so if and only if it is marked for re-append do we do anything
|
|
4140
|
+
if (isReAppended) {
|
|
4141
|
+
removed.push(currentHeadElt);
|
|
4142
|
+
nodesToAppend.push(currentHeadElt);
|
|
4143
|
+
}
|
|
4144
|
+
} else {
|
|
4145
|
+
// if this is a merge, we remove this content since it is not in the new head
|
|
4146
|
+
if (ctx.head.shouldRemove(currentHeadElt) !== false) {
|
|
4147
|
+
removed.push(currentHeadElt);
|
|
4148
|
+
}
|
|
4149
|
+
}
|
|
3802
4150
|
}
|
|
3803
|
-
}
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
4151
|
+
}
|
|
4152
|
+
|
|
4153
|
+
// Push the remaining new head elements in the Map into the
|
|
4154
|
+
// nodes to append to the head tag
|
|
4155
|
+
nodesToAppend.push(...srcToNewHeadNodes.values());
|
|
4156
|
+
|
|
4157
|
+
let promises = [];
|
|
4158
|
+
for (const newNode of nodesToAppend) {
|
|
4159
|
+
let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild;
|
|
4160
|
+
if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
|
|
4161
|
+
if (newElt.href || newElt.src) {
|
|
4162
|
+
let resolve = null;
|
|
4163
|
+
let promise = new Promise(function (_resolve) {
|
|
4164
|
+
resolve = _resolve;
|
|
4165
|
+
});
|
|
4166
|
+
newElt.addEventListener('load', function () {
|
|
4167
|
+
resolve();
|
|
4168
|
+
});
|
|
4169
|
+
promises.push(promise);
|
|
4170
|
+
}
|
|
4171
|
+
currentHead.appendChild(newElt);
|
|
4172
|
+
ctx.callbacks.afterNodeAdded(newElt);
|
|
4173
|
+
added.push(newElt);
|
|
3807
4174
|
}
|
|
3808
4175
|
}
|
|
3809
|
-
}
|
|
3810
|
-
}
|
|
3811
4176
|
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
|
|
3820
|
-
if (newElt.href || newElt.src) {
|
|
3821
|
-
let resolve = null;
|
|
3822
|
-
let promise = new Promise(function (_resolve) {
|
|
3823
|
-
resolve = _resolve;
|
|
3824
|
-
});
|
|
3825
|
-
newElt.addEventListener('load',function() {
|
|
3826
|
-
resolve();
|
|
3827
|
-
});
|
|
3828
|
-
promises.push(promise);
|
|
4177
|
+
// remove all removed elements, after we have appended the new elements to avoid
|
|
4178
|
+
// additional network requests for things like style sheets
|
|
4179
|
+
for (const removedElement of removed) {
|
|
4180
|
+
if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
|
|
4181
|
+
currentHead.removeChild(removedElement);
|
|
4182
|
+
ctx.callbacks.afterNodeRemoved(removedElement);
|
|
4183
|
+
}
|
|
3829
4184
|
}
|
|
3830
|
-
currentHead.appendChild(newElt);
|
|
3831
|
-
ctx.callbacks.afterNodeAdded(newElt);
|
|
3832
|
-
added.push(newElt);
|
|
3833
|
-
}
|
|
3834
|
-
}
|
|
3835
4185
|
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
for (const removedElement of removed) {
|
|
3839
|
-
if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
|
|
3840
|
-
currentHead.removeChild(removedElement);
|
|
3841
|
-
ctx.callbacks.afterNodeRemoved(removedElement);
|
|
4186
|
+
ctx.head.afterHeadMorphed(currentHead, {added: added, kept: preserved, removed: removed});
|
|
4187
|
+
return promises;
|
|
3842
4188
|
}
|
|
3843
|
-
}
|
|
3844
4189
|
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
}
|
|
4190
|
+
function noOp() {
|
|
4191
|
+
}
|
|
3848
4192
|
|
|
3849
|
-
|
|
4193
|
+
/*
|
|
4194
|
+
Deep merges the config object and the Idiomoroph.defaults object to
|
|
4195
|
+
produce a final configuration object
|
|
4196
|
+
*/
|
|
4197
|
+
function mergeDefaults(config) {
|
|
4198
|
+
let finalConfig = {};
|
|
4199
|
+
// copy top level stuff into final config
|
|
4200
|
+
Object.assign(finalConfig, defaults);
|
|
4201
|
+
Object.assign(finalConfig, config);
|
|
4202
|
+
|
|
4203
|
+
// copy callbacks into final config (do this to deep merge the callbacks)
|
|
4204
|
+
finalConfig.callbacks = {};
|
|
4205
|
+
Object.assign(finalConfig.callbacks, defaults.callbacks);
|
|
4206
|
+
Object.assign(finalConfig.callbacks, config.callbacks);
|
|
4207
|
+
|
|
4208
|
+
// copy head config into final config (do this to deep merge the head)
|
|
4209
|
+
finalConfig.head = {};
|
|
4210
|
+
Object.assign(finalConfig.head, defaults.head);
|
|
4211
|
+
Object.assign(finalConfig.head, config.head);
|
|
4212
|
+
return finalConfig;
|
|
4213
|
+
}
|
|
3850
4214
|
|
|
3851
|
-
function createMorphContext(oldNode, newContent, config) {
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
afterNodeRemoved : noOp,
|
|
3867
|
-
|
|
3868
|
-
}, config.callbacks),
|
|
3869
|
-
head: Object.assign({
|
|
3870
|
-
style: 'merge',
|
|
3871
|
-
shouldPreserve : function(elt) {
|
|
3872
|
-
return elt.getAttribute("im-preserve") === "true";
|
|
3873
|
-
},
|
|
3874
|
-
shouldReAppend : function(elt) {
|
|
3875
|
-
return elt.getAttribute("im-re-append") === "true";
|
|
3876
|
-
},
|
|
3877
|
-
shouldRemove : noOp,
|
|
3878
|
-
afterHeadMorphed : noOp,
|
|
3879
|
-
}, config.head),
|
|
3880
|
-
}
|
|
3881
|
-
}
|
|
4215
|
+
function createMorphContext(oldNode, newContent, config) {
|
|
4216
|
+
config = mergeDefaults(config);
|
|
4217
|
+
return {
|
|
4218
|
+
target: oldNode,
|
|
4219
|
+
newContent: newContent,
|
|
4220
|
+
config: config,
|
|
4221
|
+
morphStyle: config.morphStyle,
|
|
4222
|
+
ignoreActive: config.ignoreActive,
|
|
4223
|
+
ignoreActiveValue: config.ignoreActiveValue,
|
|
4224
|
+
idMap: createIdMap(oldNode, newContent),
|
|
4225
|
+
deadIds: new Set(),
|
|
4226
|
+
callbacks: config.callbacks,
|
|
4227
|
+
head: config.head
|
|
4228
|
+
}
|
|
4229
|
+
}
|
|
3882
4230
|
|
|
3883
|
-
function isIdSetMatch(node1, node2, ctx) {
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
4231
|
+
function isIdSetMatch(node1, node2, ctx) {
|
|
4232
|
+
if (node1 == null || node2 == null) {
|
|
4233
|
+
return false;
|
|
4234
|
+
}
|
|
4235
|
+
if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) {
|
|
4236
|
+
if (node1.id !== "" && node1.id === node2.id) {
|
|
4237
|
+
return true;
|
|
4238
|
+
} else {
|
|
4239
|
+
return getIdIntersectionCount(ctx, node1, node2) > 0;
|
|
4240
|
+
}
|
|
4241
|
+
}
|
|
4242
|
+
return false;
|
|
3892
4243
|
}
|
|
3893
|
-
}
|
|
3894
|
-
return false;
|
|
3895
|
-
}
|
|
3896
4244
|
|
|
3897
|
-
function isSoftMatch(node1, node2) {
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
}
|
|
4245
|
+
function isSoftMatch(node1, node2) {
|
|
4246
|
+
if (node1 == null || node2 == null) {
|
|
4247
|
+
return false;
|
|
4248
|
+
}
|
|
4249
|
+
return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName
|
|
4250
|
+
}
|
|
3903
4251
|
|
|
3904
|
-
function removeNodesBetween(startInclusive, endExclusive, ctx) {
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
}
|
|
4252
|
+
function removeNodesBetween(startInclusive, endExclusive, ctx) {
|
|
4253
|
+
while (startInclusive !== endExclusive) {
|
|
4254
|
+
let tempNode = startInclusive;
|
|
4255
|
+
startInclusive = startInclusive.nextSibling;
|
|
4256
|
+
removeNode(tempNode, ctx);
|
|
4257
|
+
}
|
|
4258
|
+
removeIdsFromConsideration(ctx, endExclusive);
|
|
4259
|
+
return endExclusive.nextSibling;
|
|
4260
|
+
}
|
|
3913
4261
|
|
|
3914
|
-
//=============================================================================
|
|
3915
|
-
// Scans forward from the insertionPoint in the old parent looking for a potential id match
|
|
3916
|
-
// for the newChild. We stop if we find a potential id match for the new child OR
|
|
3917
|
-
// if the number of potential id matches we are discarding is greater than the
|
|
3918
|
-
// potential id matches for the new child
|
|
3919
|
-
//=============================================================================
|
|
3920
|
-
function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
|
|
4262
|
+
//=============================================================================
|
|
4263
|
+
// Scans forward from the insertionPoint in the old parent looking for a potential id match
|
|
4264
|
+
// for the newChild. We stop if we find a potential id match for the new child OR
|
|
4265
|
+
// if the number of potential id matches we are discarding is greater than the
|
|
4266
|
+
// potential id matches for the new child
|
|
4267
|
+
//=============================================================================
|
|
4268
|
+
function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
|
|
4269
|
+
|
|
4270
|
+
// max id matches we are willing to discard in our search
|
|
4271
|
+
let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent);
|
|
4272
|
+
|
|
4273
|
+
let potentialMatch = null;
|
|
4274
|
+
|
|
4275
|
+
// only search forward if there is a possibility of an id match
|
|
4276
|
+
if (newChildPotentialIdCount > 0) {
|
|
4277
|
+
let potentialMatch = insertionPoint;
|
|
4278
|
+
// if there is a possibility of an id match, scan forward
|
|
4279
|
+
// keep track of the potential id match count we are discarding (the
|
|
4280
|
+
// newChildPotentialIdCount must be greater than this to make it likely
|
|
4281
|
+
// worth it)
|
|
4282
|
+
let otherMatchCount = 0;
|
|
4283
|
+
while (potentialMatch != null) {
|
|
4284
|
+
|
|
4285
|
+
// If we have an id match, return the current potential match
|
|
4286
|
+
if (isIdSetMatch(newChild, potentialMatch, ctx)) {
|
|
4287
|
+
return potentialMatch;
|
|
4288
|
+
}
|
|
3921
4289
|
|
|
3922
|
-
|
|
3923
|
-
|
|
4290
|
+
// computer the other potential matches of this new content
|
|
4291
|
+
otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent);
|
|
4292
|
+
if (otherMatchCount > newChildPotentialIdCount) {
|
|
4293
|
+
// if we have more potential id matches in _other_ content, we
|
|
4294
|
+
// do not have a good candidate for an id match, so return null
|
|
4295
|
+
return null;
|
|
4296
|
+
}
|
|
3924
4297
|
|
|
3925
|
-
|
|
4298
|
+
// advanced to the next old content child
|
|
4299
|
+
potentialMatch = potentialMatch.nextSibling;
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
return potentialMatch;
|
|
4303
|
+
}
|
|
3926
4304
|
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
// if
|
|
3931
|
-
//
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
let otherMatchCount = 0;
|
|
3935
|
-
while (potentialMatch != null) {
|
|
4305
|
+
//=============================================================================
|
|
4306
|
+
// Scans forward from the insertionPoint in the old parent looking for a potential soft match
|
|
4307
|
+
// for the newChild. We stop if we find a potential soft match for the new child OR
|
|
4308
|
+
// if we find a potential id match in the old parents children OR if we find two
|
|
4309
|
+
// potential soft matches for the next two pieces of new content
|
|
4310
|
+
//=============================================================================
|
|
4311
|
+
function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
|
|
3936
4312
|
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
}
|
|
4313
|
+
let potentialSoftMatch = insertionPoint;
|
|
4314
|
+
let nextSibling = newChild.nextSibling;
|
|
4315
|
+
let siblingSoftMatchCount = 0;
|
|
3941
4316
|
|
|
3942
|
-
|
|
3943
|
-
otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent);
|
|
3944
|
-
if (otherMatchCount > newChildPotentialIdCount) {
|
|
3945
|
-
// if we have more potential id matches in _other_ content, we
|
|
3946
|
-
// do not have a good candidate for an id match, so return null
|
|
3947
|
-
return null;
|
|
3948
|
-
}
|
|
4317
|
+
while (potentialSoftMatch != null) {
|
|
3949
4318
|
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
}
|
|
4319
|
+
if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) {
|
|
4320
|
+
// the current potential soft match has a potential id set match with the remaining new
|
|
4321
|
+
// content so bail out of looking
|
|
4322
|
+
return null;
|
|
4323
|
+
}
|
|
3956
4324
|
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
// potential soft matches for the next two pieces of new content
|
|
3962
|
-
//=============================================================================
|
|
3963
|
-
function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
|
|
4325
|
+
// if we have a soft match with the current node, return it
|
|
4326
|
+
if (isSoftMatch(newChild, potentialSoftMatch)) {
|
|
4327
|
+
return potentialSoftMatch;
|
|
4328
|
+
}
|
|
3964
4329
|
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
4330
|
+
if (isSoftMatch(nextSibling, potentialSoftMatch)) {
|
|
4331
|
+
// the next new node has a soft match with this node, so
|
|
4332
|
+
// increment the count of future soft matches
|
|
4333
|
+
siblingSoftMatchCount++;
|
|
4334
|
+
nextSibling = nextSibling.nextSibling;
|
|
3968
4335
|
|
|
3969
|
-
|
|
4336
|
+
// If there are two future soft matches, bail to allow the siblings to soft match
|
|
4337
|
+
// so that we don't consume future soft matches for the sake of the current node
|
|
4338
|
+
if (siblingSoftMatchCount >= 2) {
|
|
4339
|
+
return null;
|
|
4340
|
+
}
|
|
4341
|
+
}
|
|
3970
4342
|
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
return null;
|
|
3975
|
-
}
|
|
4343
|
+
// advanced to the next old content child
|
|
4344
|
+
potentialSoftMatch = potentialSoftMatch.nextSibling;
|
|
4345
|
+
}
|
|
3976
4346
|
|
|
3977
|
-
// if we have a soft match with the current node, return it
|
|
3978
|
-
if (isSoftMatch(newChild, potentialSoftMatch)) {
|
|
3979
4347
|
return potentialSoftMatch;
|
|
3980
4348
|
}
|
|
3981
4349
|
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
//
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
return
|
|
4350
|
+
function parseContent(newContent) {
|
|
4351
|
+
let parser = new DOMParser();
|
|
4352
|
+
|
|
4353
|
+
// remove svgs to avoid false-positive matches on head, etc.
|
|
4354
|
+
let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
|
|
4355
|
+
|
|
4356
|
+
// if the newContent contains a html, head or body tag, we can simply parse it w/o wrapping
|
|
4357
|
+
if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) {
|
|
4358
|
+
let content = parser.parseFromString(newContent, "text/html");
|
|
4359
|
+
// if it is a full HTML document, return the document itself as the parent container
|
|
4360
|
+
if (contentWithSvgsRemoved.match(/<\/html>/)) {
|
|
4361
|
+
content.generatedByIdiomorph = true;
|
|
4362
|
+
return content;
|
|
4363
|
+
} else {
|
|
4364
|
+
// otherwise return the html element as the parent container
|
|
4365
|
+
let htmlElement = content.firstChild;
|
|
4366
|
+
if (htmlElement) {
|
|
4367
|
+
htmlElement.generatedByIdiomorph = true;
|
|
4368
|
+
return htmlElement;
|
|
4369
|
+
} else {
|
|
4370
|
+
return null;
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
} else {
|
|
4374
|
+
// if it is partial HTML, wrap it in a template tag to provide a parent element and also to help
|
|
4375
|
+
// deal with touchy tags like tr, tbody, etc.
|
|
4376
|
+
let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html");
|
|
4377
|
+
let content = responseDoc.body.querySelector('template').content;
|
|
4378
|
+
content.generatedByIdiomorph = true;
|
|
4379
|
+
return content
|
|
3992
4380
|
}
|
|
3993
4381
|
}
|
|
3994
4382
|
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
}
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
// if the newContent contains a html, head or body tag, we can simply parse it w/o wrapping
|
|
4009
|
-
if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) {
|
|
4010
|
-
let content = parser.parseFromString(newContent, "text/html");
|
|
4011
|
-
// if it is a full HTML document, return the document itself as the parent container
|
|
4012
|
-
if (contentWithSvgsRemoved.match(/<\/html>/)) {
|
|
4013
|
-
content.generatedByIdiomorph = true;
|
|
4014
|
-
return content;
|
|
4015
|
-
} else {
|
|
4016
|
-
// otherwise return the html element as the parent container
|
|
4017
|
-
let htmlElement = content.firstChild;
|
|
4018
|
-
if (htmlElement) {
|
|
4019
|
-
htmlElement.generatedByIdiomorph = true;
|
|
4020
|
-
return htmlElement;
|
|
4383
|
+
function normalizeContent(newContent) {
|
|
4384
|
+
if (newContent == null) {
|
|
4385
|
+
// noinspection UnnecessaryLocalVariableJS
|
|
4386
|
+
const dummyParent = document.createElement('div');
|
|
4387
|
+
return dummyParent;
|
|
4388
|
+
} else if (newContent.generatedByIdiomorph) {
|
|
4389
|
+
// the template tag created by idiomorph parsing can serve as a dummy parent
|
|
4390
|
+
return newContent;
|
|
4391
|
+
} else if (newContent instanceof Node) {
|
|
4392
|
+
// a single node is added as a child to a dummy parent
|
|
4393
|
+
const dummyParent = document.createElement('div');
|
|
4394
|
+
dummyParent.append(newContent);
|
|
4395
|
+
return dummyParent;
|
|
4021
4396
|
} else {
|
|
4022
|
-
|
|
4397
|
+
// all nodes in the array or HTMLElement collection are consolidated under
|
|
4398
|
+
// a single dummy parent element
|
|
4399
|
+
const dummyParent = document.createElement('div');
|
|
4400
|
+
for (const elt of [...newContent]) {
|
|
4401
|
+
dummyParent.append(elt);
|
|
4402
|
+
}
|
|
4403
|
+
return dummyParent;
|
|
4023
4404
|
}
|
|
4024
4405
|
}
|
|
4025
|
-
} else {
|
|
4026
|
-
// if it is partial HTML, wrap it in a template tag to provide a parent element and also to help
|
|
4027
|
-
// deal with touchy tags like tr, tbody, etc.
|
|
4028
|
-
let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html");
|
|
4029
|
-
let content = responseDoc.body.querySelector('template').content;
|
|
4030
|
-
content.generatedByIdiomorph = true;
|
|
4031
|
-
return content
|
|
4032
|
-
}
|
|
4033
|
-
}
|
|
4034
|
-
|
|
4035
|
-
function normalizeContent(newContent) {
|
|
4036
|
-
if (newContent == null) {
|
|
4037
|
-
// noinspection UnnecessaryLocalVariableJS
|
|
4038
|
-
const dummyParent = document.createElement('div');
|
|
4039
|
-
return dummyParent;
|
|
4040
|
-
} else if (newContent.generatedByIdiomorph) {
|
|
4041
|
-
// the template tag created by idiomorph parsing can serve as a dummy parent
|
|
4042
|
-
return newContent;
|
|
4043
|
-
} else if (newContent instanceof Node) {
|
|
4044
|
-
// a single node is added as a child to a dummy parent
|
|
4045
|
-
const dummyParent = document.createElement('div');
|
|
4046
|
-
dummyParent.append(newContent);
|
|
4047
|
-
return dummyParent;
|
|
4048
|
-
} else {
|
|
4049
|
-
// all nodes in the array or HTMLElement collection are consolidated under
|
|
4050
|
-
// a single dummy parent element
|
|
4051
|
-
const dummyParent = document.createElement('div');
|
|
4052
|
-
for (const elt of [...newContent]) {
|
|
4053
|
-
dummyParent.append(elt);
|
|
4054
|
-
}
|
|
4055
|
-
return dummyParent;
|
|
4056
|
-
}
|
|
4057
|
-
}
|
|
4058
4406
|
|
|
4059
|
-
function insertSiblings(previousSibling, morphedNode, nextSibling) {
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
}
|
|
4407
|
+
function insertSiblings(previousSibling, morphedNode, nextSibling) {
|
|
4408
|
+
let stack = [];
|
|
4409
|
+
let added = [];
|
|
4410
|
+
while (previousSibling != null) {
|
|
4411
|
+
stack.push(previousSibling);
|
|
4412
|
+
previousSibling = previousSibling.previousSibling;
|
|
4413
|
+
}
|
|
4414
|
+
while (stack.length > 0) {
|
|
4415
|
+
let node = stack.pop();
|
|
4416
|
+
added.push(node); // push added preceding siblings on in order and insert
|
|
4417
|
+
morphedNode.parentElement.insertBefore(node, morphedNode);
|
|
4418
|
+
}
|
|
4419
|
+
added.push(morphedNode);
|
|
4420
|
+
while (nextSibling != null) {
|
|
4421
|
+
stack.push(nextSibling);
|
|
4422
|
+
added.push(nextSibling); // here we are going in order, so push on as we scan, rather than add
|
|
4423
|
+
nextSibling = nextSibling.nextSibling;
|
|
4424
|
+
}
|
|
4425
|
+
while (stack.length > 0) {
|
|
4426
|
+
morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling);
|
|
4427
|
+
}
|
|
4428
|
+
return added;
|
|
4429
|
+
}
|
|
4082
4430
|
|
|
4083
|
-
function findBestNodeMatch(newContent, oldNode, ctx) {
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4431
|
+
function findBestNodeMatch(newContent, oldNode, ctx) {
|
|
4432
|
+
let currentElement;
|
|
4433
|
+
currentElement = newContent.firstChild;
|
|
4434
|
+
let bestElement = currentElement;
|
|
4435
|
+
let score = 0;
|
|
4436
|
+
while (currentElement) {
|
|
4437
|
+
let newScore = scoreElement(currentElement, oldNode, ctx);
|
|
4438
|
+
if (newScore > score) {
|
|
4439
|
+
bestElement = currentElement;
|
|
4440
|
+
score = newScore;
|
|
4441
|
+
}
|
|
4442
|
+
currentElement = currentElement.nextSibling;
|
|
4443
|
+
}
|
|
4444
|
+
return bestElement;
|
|
4093
4445
|
}
|
|
4094
|
-
currentElement = currentElement.nextSibling;
|
|
4095
|
-
}
|
|
4096
|
-
return bestElement;
|
|
4097
|
-
}
|
|
4098
4446
|
|
|
4099
|
-
function scoreElement(node1, node2, ctx) {
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
}
|
|
4447
|
+
function scoreElement(node1, node2, ctx) {
|
|
4448
|
+
if (isSoftMatch(node1, node2)) {
|
|
4449
|
+
return .5 + getIdIntersectionCount(ctx, node1, node2);
|
|
4450
|
+
}
|
|
4451
|
+
return 0;
|
|
4452
|
+
}
|
|
4105
4453
|
|
|
4106
|
-
function removeNode(tempNode, ctx) {
|
|
4107
|
-
|
|
4108
|
-
|
|
4454
|
+
function removeNode(tempNode, ctx) {
|
|
4455
|
+
removeIdsFromConsideration(ctx, tempNode);
|
|
4456
|
+
if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return;
|
|
4109
4457
|
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
}
|
|
4458
|
+
tempNode.remove();
|
|
4459
|
+
ctx.callbacks.afterNodeRemoved(tempNode);
|
|
4460
|
+
}
|
|
4113
4461
|
|
|
4114
|
-
//=============================================================================
|
|
4115
|
-
// ID Set Functions
|
|
4116
|
-
//=============================================================================
|
|
4462
|
+
//=============================================================================
|
|
4463
|
+
// ID Set Functions
|
|
4464
|
+
//=============================================================================
|
|
4117
4465
|
|
|
4118
|
-
function isIdInConsideration(ctx, id) {
|
|
4119
|
-
|
|
4120
|
-
}
|
|
4466
|
+
function isIdInConsideration(ctx, id) {
|
|
4467
|
+
return !ctx.deadIds.has(id);
|
|
4468
|
+
}
|
|
4121
4469
|
|
|
4122
|
-
function idIsWithinNode(ctx, id, targetNode) {
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
}
|
|
4470
|
+
function idIsWithinNode(ctx, id, targetNode) {
|
|
4471
|
+
let idSet = ctx.idMap.get(targetNode) || EMPTY_SET;
|
|
4472
|
+
return idSet.has(id);
|
|
4473
|
+
}
|
|
4126
4474
|
|
|
4127
|
-
function removeIdsFromConsideration(ctx, node) {
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
}
|
|
4475
|
+
function removeIdsFromConsideration(ctx, node) {
|
|
4476
|
+
let idSet = ctx.idMap.get(node) || EMPTY_SET;
|
|
4477
|
+
for (const id of idSet) {
|
|
4478
|
+
ctx.deadIds.add(id);
|
|
4479
|
+
}
|
|
4480
|
+
}
|
|
4133
4481
|
|
|
4134
|
-
function getIdIntersectionCount(ctx, node1, node2) {
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4482
|
+
function getIdIntersectionCount(ctx, node1, node2) {
|
|
4483
|
+
let sourceSet = ctx.idMap.get(node1) || EMPTY_SET;
|
|
4484
|
+
let matchCount = 0;
|
|
4485
|
+
for (const id of sourceSet) {
|
|
4486
|
+
// a potential match is an id in the source and potentialIdsSet, but
|
|
4487
|
+
// that has not already been merged into the DOM
|
|
4488
|
+
if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) {
|
|
4489
|
+
++matchCount;
|
|
4490
|
+
}
|
|
4491
|
+
}
|
|
4492
|
+
return matchCount;
|
|
4142
4493
|
}
|
|
4143
|
-
}
|
|
4144
|
-
return matchCount;
|
|
4145
|
-
}
|
|
4146
4494
|
|
|
4147
|
-
/**
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
function populateIdMapForNode(node, idMap) {
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4495
|
+
/**
|
|
4496
|
+
* A bottom up algorithm that finds all elements with ids inside of the node
|
|
4497
|
+
* argument and populates id sets for those nodes and all their parents, generating
|
|
4498
|
+
* a set of ids contained within all nodes for the entire hierarchy in the DOM
|
|
4499
|
+
*
|
|
4500
|
+
* @param node {Element}
|
|
4501
|
+
* @param {Map<Node, Set<String>>} idMap
|
|
4502
|
+
*/
|
|
4503
|
+
function populateIdMapForNode(node, idMap) {
|
|
4504
|
+
let nodeParent = node.parentElement;
|
|
4505
|
+
// find all elements with an id property
|
|
4506
|
+
let idElements = node.querySelectorAll('[id]');
|
|
4507
|
+
for (const elt of idElements) {
|
|
4508
|
+
let current = elt;
|
|
4509
|
+
// walk up the parent hierarchy of that element, adding the id
|
|
4510
|
+
// of element to the parent's id set
|
|
4511
|
+
while (current !== nodeParent && current != null) {
|
|
4512
|
+
let idSet = idMap.get(current);
|
|
4513
|
+
// if the id set doesn't exist, create it and insert it in the map
|
|
4514
|
+
if (idSet == null) {
|
|
4515
|
+
idSet = new Set();
|
|
4516
|
+
idMap.set(current, idSet);
|
|
4517
|
+
}
|
|
4518
|
+
idSet.add(elt.id);
|
|
4519
|
+
current = current.parentElement;
|
|
4520
|
+
}
|
|
4169
4521
|
}
|
|
4170
|
-
idSet.add(elt.id);
|
|
4171
|
-
current = current.parentElement;
|
|
4172
4522
|
}
|
|
4173
|
-
}
|
|
4174
|
-
}
|
|
4175
4523
|
|
|
4176
|
-
/**
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
function createIdMap(oldContent, newContent) {
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
}
|
|
4524
|
+
/**
|
|
4525
|
+
* This function computes a map of nodes to all ids contained within that node (inclusive of the
|
|
4526
|
+
* node). This map can be used to ask if two nodes have intersecting sets of ids, which allows
|
|
4527
|
+
* for a looser definition of "matching" than tradition id matching, and allows child nodes
|
|
4528
|
+
* to contribute to a parent nodes matching.
|
|
4529
|
+
*
|
|
4530
|
+
* @param {Element} oldContent the old content that will be morphed
|
|
4531
|
+
* @param {Element} newContent the new content to morph to
|
|
4532
|
+
* @returns {Map<Node, Set<String>>} a map of nodes to id sets for the
|
|
4533
|
+
*/
|
|
4534
|
+
function createIdMap(oldContent, newContent) {
|
|
4535
|
+
let idMap = new Map();
|
|
4536
|
+
populateIdMapForNode(oldContent, idMap);
|
|
4537
|
+
populateIdMapForNode(newContent, idMap);
|
|
4538
|
+
return idMap;
|
|
4539
|
+
}
|
|
4192
4540
|
|
|
4193
|
-
//=============================================================================
|
|
4194
|
-
// This is what ends up becoming the Idiomorph
|
|
4195
|
-
//=============================================================================
|
|
4196
|
-
|
|
4541
|
+
//=============================================================================
|
|
4542
|
+
// This is what ends up becoming the Idiomorph global object
|
|
4543
|
+
//=============================================================================
|
|
4544
|
+
return {
|
|
4545
|
+
morph,
|
|
4546
|
+
defaults
|
|
4547
|
+
}
|
|
4548
|
+
})();
|
|
4197
4549
|
|
|
4198
4550
|
class MorphRenderer extends Renderer {
|
|
4199
4551
|
async render() {
|
|
@@ -4221,7 +4573,7 @@ class MorphRenderer extends Renderer {
|
|
|
4221
4573
|
#morphElements(currentElement, newElement, morphStyle = "outerHTML") {
|
|
4222
4574
|
this.isMorphingTurboFrame = this.#isFrameReloadedWithMorph(currentElement);
|
|
4223
4575
|
|
|
4224
|
-
|
|
4576
|
+
Idiomorph.morph(currentElement, newElement, {
|
|
4225
4577
|
morphStyle: morphStyle,
|
|
4226
4578
|
callbacks: {
|
|
4227
4579
|
beforeNodeAdded: this.#shouldAddElement,
|
|
@@ -4353,8 +4705,13 @@ class PageRenderer extends Renderer {
|
|
|
4353
4705
|
const mergedHeadElements = this.mergeProvisionalElements();
|
|
4354
4706
|
const newStylesheetElements = this.copyNewHeadStylesheetElements();
|
|
4355
4707
|
this.copyNewHeadScriptElements();
|
|
4708
|
+
|
|
4356
4709
|
await mergedHeadElements;
|
|
4357
4710
|
await newStylesheetElements;
|
|
4711
|
+
|
|
4712
|
+
if (this.willRender) {
|
|
4713
|
+
this.removeUnusedDynamicStylesheetElements();
|
|
4714
|
+
}
|
|
4358
4715
|
}
|
|
4359
4716
|
|
|
4360
4717
|
async replaceBody() {
|
|
@@ -4386,6 +4743,12 @@ class PageRenderer extends Renderer {
|
|
|
4386
4743
|
}
|
|
4387
4744
|
}
|
|
4388
4745
|
|
|
4746
|
+
removeUnusedDynamicStylesheetElements() {
|
|
4747
|
+
for (const element of this.unusedDynamicStylesheetElements) {
|
|
4748
|
+
document.head.removeChild(element);
|
|
4749
|
+
}
|
|
4750
|
+
}
|
|
4751
|
+
|
|
4389
4752
|
async mergeProvisionalElements() {
|
|
4390
4753
|
const newHeadElements = [...this.newHeadProvisionalElements];
|
|
4391
4754
|
|
|
@@ -4451,6 +4814,16 @@ class PageRenderer extends Renderer {
|
|
|
4451
4814
|
await this.renderElement(this.currentElement, this.newElement);
|
|
4452
4815
|
}
|
|
4453
4816
|
|
|
4817
|
+
get unusedDynamicStylesheetElements() {
|
|
4818
|
+
return this.oldHeadStylesheetElements.filter((element) => {
|
|
4819
|
+
return element.getAttribute("data-turbo-track") === "dynamic"
|
|
4820
|
+
})
|
|
4821
|
+
}
|
|
4822
|
+
|
|
4823
|
+
get oldHeadStylesheetElements() {
|
|
4824
|
+
return this.currentHeadSnapshot.getStylesheetElementsNotInSnapshot(this.newHeadSnapshot)
|
|
4825
|
+
}
|
|
4826
|
+
|
|
4454
4827
|
get newHeadStylesheetElements() {
|
|
4455
4828
|
return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot)
|
|
4456
4829
|
}
|
|
@@ -4694,6 +5067,7 @@ class Session {
|
|
|
4694
5067
|
|
|
4695
5068
|
pageObserver = new PageObserver(this)
|
|
4696
5069
|
cacheObserver = new CacheObserver()
|
|
5070
|
+
linkPrefetchObserver = new LinkPrefetchObserver(this, document)
|
|
4697
5071
|
linkClickObserver = new LinkClickObserver(this, window)
|
|
4698
5072
|
formSubmitObserver = new FormSubmitObserver(this, document)
|
|
4699
5073
|
scrollObserver = new ScrollObserver(this)
|
|
@@ -4708,16 +5082,20 @@ class Session {
|
|
|
4708
5082
|
progressBarDelay = 500
|
|
4709
5083
|
started = false
|
|
4710
5084
|
formMode = "on"
|
|
5085
|
+
#pageRefreshDebouncePeriod = 150
|
|
4711
5086
|
|
|
4712
5087
|
constructor(recentRequests) {
|
|
4713
5088
|
this.recentRequests = recentRequests;
|
|
4714
5089
|
this.preloader = new Preloader(this, this.view.snapshotCache);
|
|
5090
|
+
this.debouncedRefresh = this.refresh;
|
|
5091
|
+
this.pageRefreshDebouncePeriod = this.pageRefreshDebouncePeriod;
|
|
4715
5092
|
}
|
|
4716
5093
|
|
|
4717
5094
|
start() {
|
|
4718
5095
|
if (!this.started) {
|
|
4719
5096
|
this.pageObserver.start();
|
|
4720
5097
|
this.cacheObserver.start();
|
|
5098
|
+
this.linkPrefetchObserver.start();
|
|
4721
5099
|
this.formLinkClickObserver.start();
|
|
4722
5100
|
this.linkClickObserver.start();
|
|
4723
5101
|
this.formSubmitObserver.start();
|
|
@@ -4739,6 +5117,7 @@ class Session {
|
|
|
4739
5117
|
if (this.started) {
|
|
4740
5118
|
this.pageObserver.stop();
|
|
4741
5119
|
this.cacheObserver.stop();
|
|
5120
|
+
this.linkPrefetchObserver.stop();
|
|
4742
5121
|
this.formLinkClickObserver.stop();
|
|
4743
5122
|
this.linkClickObserver.stop();
|
|
4744
5123
|
this.formSubmitObserver.stop();
|
|
@@ -4806,6 +5185,15 @@ class Session {
|
|
|
4806
5185
|
return this.history.restorationIdentifier
|
|
4807
5186
|
}
|
|
4808
5187
|
|
|
5188
|
+
get pageRefreshDebouncePeriod() {
|
|
5189
|
+
return this.#pageRefreshDebouncePeriod
|
|
5190
|
+
}
|
|
5191
|
+
|
|
5192
|
+
set pageRefreshDebouncePeriod(value) {
|
|
5193
|
+
this.refresh = debounce(this.debouncedRefresh.bind(this), value);
|
|
5194
|
+
this.#pageRefreshDebouncePeriod = value;
|
|
5195
|
+
}
|
|
5196
|
+
|
|
4809
5197
|
// Preloader delegate
|
|
4810
5198
|
|
|
4811
5199
|
shouldPreloadLink(element) {
|
|
@@ -4855,6 +5243,15 @@ class Session {
|
|
|
4855
5243
|
|
|
4856
5244
|
submittedFormLinkToLocation() {}
|
|
4857
5245
|
|
|
5246
|
+
// Link hover observer delegate
|
|
5247
|
+
|
|
5248
|
+
canPrefetchRequestToLocation(link, location) {
|
|
5249
|
+
return (
|
|
5250
|
+
this.elementIsNavigatable(link) &&
|
|
5251
|
+
locationIsVisitable(location, this.snapshot.rootLocation)
|
|
5252
|
+
)
|
|
5253
|
+
}
|
|
5254
|
+
|
|
4858
5255
|
// Link click observer delegate
|
|
4859
5256
|
|
|
4860
5257
|
willFollowLinkToLocation(link, location, event) {
|
|
@@ -4954,8 +5351,8 @@ class Session {
|
|
|
4954
5351
|
}
|
|
4955
5352
|
}
|
|
4956
5353
|
|
|
4957
|
-
allowsImmediateRender({ element },
|
|
4958
|
-
const event = this.notifyApplicationBeforeRender(element,
|
|
5354
|
+
allowsImmediateRender({ element }, options) {
|
|
5355
|
+
const event = this.notifyApplicationBeforeRender(element, options);
|
|
4959
5356
|
const {
|
|
4960
5357
|
defaultPrevented,
|
|
4961
5358
|
detail: { render }
|
|
@@ -4968,9 +5365,9 @@ class Session {
|
|
|
4968
5365
|
return !defaultPrevented
|
|
4969
5366
|
}
|
|
4970
5367
|
|
|
4971
|
-
viewRenderedSnapshot(_snapshot,
|
|
5368
|
+
viewRenderedSnapshot(_snapshot, _isPreview, renderMethod) {
|
|
4972
5369
|
this.view.lastRenderedLocation = this.history.location;
|
|
4973
|
-
this.notifyApplicationAfterRender(
|
|
5370
|
+
this.notifyApplicationAfterRender(renderMethod);
|
|
4974
5371
|
}
|
|
4975
5372
|
|
|
4976
5373
|
preloadOnLoadLinksForView(element) {
|
|
@@ -5026,15 +5423,15 @@ class Session {
|
|
|
5026
5423
|
return dispatch("turbo:before-cache")
|
|
5027
5424
|
}
|
|
5028
5425
|
|
|
5029
|
-
notifyApplicationBeforeRender(newBody,
|
|
5426
|
+
notifyApplicationBeforeRender(newBody, options) {
|
|
5030
5427
|
return dispatch("turbo:before-render", {
|
|
5031
|
-
detail: { newBody,
|
|
5428
|
+
detail: { newBody, ...options },
|
|
5032
5429
|
cancelable: true
|
|
5033
5430
|
})
|
|
5034
5431
|
}
|
|
5035
5432
|
|
|
5036
|
-
notifyApplicationAfterRender(
|
|
5037
|
-
return dispatch("turbo:render", { detail: {
|
|
5433
|
+
notifyApplicationAfterRender(renderMethod) {
|
|
5434
|
+
return dispatch("turbo:render", { detail: { renderMethod } })
|
|
5038
5435
|
}
|
|
5039
5436
|
|
|
5040
5437
|
notifyApplicationAfterPageLoad(timing = {}) {
|
|
@@ -5494,7 +5891,7 @@ class FrameController {
|
|
|
5494
5891
|
|
|
5495
5892
|
// View delegate
|
|
5496
5893
|
|
|
5497
|
-
allowsImmediateRender({ element: newFrame },
|
|
5894
|
+
allowsImmediateRender({ element: newFrame }, options) {
|
|
5498
5895
|
const event = dispatch("turbo:before-frame-render", {
|
|
5499
5896
|
target: this.element,
|
|
5500
5897
|
detail: { newFrame, ...options },
|