@hotwired/turbo 8.0.0-beta.1 → 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 +1194 -705
- package/dist/turbo.es2017-umd.js +1194 -705
- package/package.json +2 -4
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,13 +639,61 @@ Copyright © 2023 37signals LLC
|
|
|
639
639
|
return [before, after]
|
|
640
640
|
}
|
|
641
641
|
|
|
642
|
-
function
|
|
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
|
+
|
|
670
|
+
class LimitedSet extends Set {
|
|
671
|
+
constructor(maxSize) {
|
|
672
|
+
super();
|
|
673
|
+
this.maxSize = maxSize;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
add(value) {
|
|
677
|
+
if (this.size >= this.maxSize) {
|
|
678
|
+
const iterator = this.values();
|
|
679
|
+
const oldestValue = iterator.next().value;
|
|
680
|
+
this.delete(oldestValue);
|
|
681
|
+
}
|
|
682
|
+
super.add(value);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const recentRequests = new LimitedSet(20);
|
|
687
|
+
|
|
688
|
+
const nativeFetch = window.fetch;
|
|
689
|
+
|
|
690
|
+
function fetchWithTurboHeaders(url, options = {}) {
|
|
643
691
|
const modifiedHeaders = new Headers(options.headers || {});
|
|
644
692
|
const requestUID = uuid();
|
|
645
|
-
|
|
693
|
+
recentRequests.add(requestUID);
|
|
646
694
|
modifiedHeaders.append("X-Turbo-Request-Id", requestUID);
|
|
647
695
|
|
|
648
|
-
return
|
|
696
|
+
return nativeFetch(url, {
|
|
649
697
|
...options,
|
|
650
698
|
headers: modifiedHeaders
|
|
651
699
|
})
|
|
@@ -769,10 +817,17 @@ Copyright © 2023 37signals LLC
|
|
|
769
817
|
async perform() {
|
|
770
818
|
const { fetchOptions } = this;
|
|
771
819
|
this.delegate.prepareRequest(this);
|
|
772
|
-
await this.#allowRequestToBeIntercepted(fetchOptions);
|
|
820
|
+
const event = await this.#allowRequestToBeIntercepted(fetchOptions);
|
|
773
821
|
try {
|
|
774
822
|
this.delegate.requestStarted(this);
|
|
775
|
-
|
|
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;
|
|
776
831
|
return await this.receive(response)
|
|
777
832
|
} catch (error) {
|
|
778
833
|
if (error.name !== "AbortError") {
|
|
@@ -834,6 +889,8 @@ Copyright © 2023 37signals LLC
|
|
|
834
889
|
});
|
|
835
890
|
this.url = event.detail.url;
|
|
836
891
|
if (event.defaultPrevented) await requestInterception;
|
|
892
|
+
|
|
893
|
+
return event
|
|
837
894
|
}
|
|
838
895
|
|
|
839
896
|
#willDelegateErrorHandling(error) {
|
|
@@ -944,6 +1001,41 @@ Copyright © 2023 37signals LLC
|
|
|
944
1001
|
return fragment
|
|
945
1002
|
}
|
|
946
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
|
+
|
|
947
1039
|
const FormSubmissionState = {
|
|
948
1040
|
initialized: "initialized",
|
|
949
1041
|
requesting: "requesting",
|
|
@@ -1052,6 +1144,7 @@ Copyright © 2023 37signals LLC
|
|
|
1052
1144
|
this.state = FormSubmissionState.waiting;
|
|
1053
1145
|
this.submitter?.setAttribute("disabled", "");
|
|
1054
1146
|
this.setSubmitsWith();
|
|
1147
|
+
markAsBusy(this.formElement);
|
|
1055
1148
|
dispatch("turbo:submit-start", {
|
|
1056
1149
|
target: this.formElement,
|
|
1057
1150
|
detail: { formSubmission: this }
|
|
@@ -1060,13 +1153,20 @@ Copyright © 2023 37signals LLC
|
|
|
1060
1153
|
}
|
|
1061
1154
|
|
|
1062
1155
|
requestPreventedHandlingResponse(request, response) {
|
|
1156
|
+
prefetchCache.clear();
|
|
1157
|
+
|
|
1063
1158
|
this.result = { success: response.succeeded, fetchResponse: response };
|
|
1064
1159
|
}
|
|
1065
1160
|
|
|
1066
1161
|
requestSucceededWithResponse(request, response) {
|
|
1067
1162
|
if (response.clientError || response.serverError) {
|
|
1068
1163
|
this.delegate.formSubmissionFailedWithResponse(this, response);
|
|
1069
|
-
|
|
1164
|
+
return
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
prefetchCache.clear();
|
|
1168
|
+
|
|
1169
|
+
if (this.requestMustRedirect(request) && responseSucceededWithoutRedirect(response)) {
|
|
1070
1170
|
const error = new Error("Form responses must redirect to another location");
|
|
1071
1171
|
this.delegate.formSubmissionErrored(this, error);
|
|
1072
1172
|
} else {
|
|
@@ -1090,6 +1190,7 @@ Copyright © 2023 37signals LLC
|
|
|
1090
1190
|
this.state = FormSubmissionState.stopped;
|
|
1091
1191
|
this.submitter?.removeAttribute("disabled");
|
|
1092
1192
|
this.resetSubmitterText();
|
|
1193
|
+
clearBusyState(this.formElement);
|
|
1093
1194
|
dispatch("turbo:submit-end", {
|
|
1094
1195
|
target: this.formElement,
|
|
1095
1196
|
detail: { formSubmission: this, ...this.result }
|
|
@@ -1383,7 +1484,7 @@ Copyright © 2023 37signals LLC
|
|
|
1383
1484
|
|
|
1384
1485
|
const renderInterception = new Promise((resolve) => (this.#resolveInterceptionPromise = resolve));
|
|
1385
1486
|
const options = { resume: this.#resolveInterceptionPromise, render: this.renderer.renderElement };
|
|
1386
|
-
const immediateRender = this.delegate.allowsImmediateRender(snapshot,
|
|
1487
|
+
const immediateRender = this.delegate.allowsImmediateRender(snapshot, options);
|
|
1387
1488
|
if (!immediateRender) await renderInterception;
|
|
1388
1489
|
|
|
1389
1490
|
await this.renderSnapshot(renderer);
|
|
@@ -1417,6 +1518,14 @@ Copyright © 2023 37signals LLC
|
|
|
1417
1518
|
}
|
|
1418
1519
|
}
|
|
1419
1520
|
|
|
1521
|
+
markVisitDirection(direction) {
|
|
1522
|
+
this.element.setAttribute("data-turbo-visit-direction", direction);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
unmarkVisitDirection() {
|
|
1526
|
+
this.element.removeAttribute("data-turbo-visit-direction");
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1420
1529
|
async renderSnapshot(renderer) {
|
|
1421
1530
|
await renderer.render();
|
|
1422
1531
|
}
|
|
@@ -1513,9 +1622,9 @@ Copyright © 2023 37signals LLC
|
|
|
1513
1622
|
clickBubbled = (event) => {
|
|
1514
1623
|
if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {
|
|
1515
1624
|
const target = (event.composedPath && event.composedPath()[0]) || event.target;
|
|
1516
|
-
const link =
|
|
1625
|
+
const link = findLinkFromClickTarget(target);
|
|
1517
1626
|
if (link && doesNotTargetIFrame(link)) {
|
|
1518
|
-
const location =
|
|
1627
|
+
const location = getLocationForLink(link);
|
|
1519
1628
|
if (this.delegate.willFollowLinkToLocation(link, location, event)) {
|
|
1520
1629
|
event.preventDefault();
|
|
1521
1630
|
this.delegate.followedLinkToLocation(link, location);
|
|
@@ -1535,26 +1644,6 @@ Copyright © 2023 37signals LLC
|
|
|
1535
1644
|
event.shiftKey
|
|
1536
1645
|
)
|
|
1537
1646
|
}
|
|
1538
|
-
|
|
1539
|
-
findLinkFromClickTarget(target) {
|
|
1540
|
-
return findClosestRecursively(target, "a[href]:not([target^=_]):not([download])")
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
getLocationForLink(link) {
|
|
1544
|
-
return expandURL(link.getAttribute("href") || "")
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
function doesNotTargetIFrame(anchor) {
|
|
1549
|
-
if (anchor.hasAttribute("target")) {
|
|
1550
|
-
for (const element of document.getElementsByName(anchor.target)) {
|
|
1551
|
-
if (element instanceof HTMLIFrameElement) return false
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
return true
|
|
1555
|
-
} else {
|
|
1556
|
-
return true
|
|
1557
|
-
}
|
|
1558
1647
|
}
|
|
1559
1648
|
|
|
1560
1649
|
class FormLinkClickObserver {
|
|
@@ -1571,6 +1660,16 @@ Copyright © 2023 37signals LLC
|
|
|
1571
1660
|
this.linkInterceptor.stop();
|
|
1572
1661
|
}
|
|
1573
1662
|
|
|
1663
|
+
// Link hover observer delegate
|
|
1664
|
+
|
|
1665
|
+
canPrefetchRequestToLocation(link, location) {
|
|
1666
|
+
return false
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
prefetchAndCacheRequestToLocation(link, location) {
|
|
1670
|
+
return
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1574
1673
|
// Link click observer delegate
|
|
1575
1674
|
|
|
1576
1675
|
willFollowLinkToLocation(link, location, originalEvent) {
|
|
@@ -1786,14 +1885,14 @@ Copyright © 2023 37signals LLC
|
|
|
1786
1885
|
}
|
|
1787
1886
|
|
|
1788
1887
|
async render() {
|
|
1789
|
-
await
|
|
1888
|
+
await nextRepaint();
|
|
1790
1889
|
this.preservingPermanentElements(() => {
|
|
1791
1890
|
this.loadFrameElement();
|
|
1792
1891
|
});
|
|
1793
1892
|
this.scrollFrameIntoView();
|
|
1794
|
-
await
|
|
1893
|
+
await nextRepaint();
|
|
1795
1894
|
this.focusFirstAutofocusableElement();
|
|
1796
|
-
await
|
|
1895
|
+
await nextRepaint();
|
|
1797
1896
|
this.activateScriptElements();
|
|
1798
1897
|
}
|
|
1799
1898
|
|
|
@@ -1844,6 +1943,8 @@ Copyright © 2023 37signals LLC
|
|
|
1844
1943
|
}
|
|
1845
1944
|
}
|
|
1846
1945
|
|
|
1946
|
+
const ProgressBarID = "turbo-progress-bar";
|
|
1947
|
+
|
|
1847
1948
|
class ProgressBar {
|
|
1848
1949
|
static animationDuration = 300 /*ms*/
|
|
1849
1950
|
|
|
@@ -1948,6 +2049,8 @@ Copyright © 2023 37signals LLC
|
|
|
1948
2049
|
|
|
1949
2050
|
createStylesheetElement() {
|
|
1950
2051
|
const element = document.createElement("style");
|
|
2052
|
+
element.id = ProgressBarID;
|
|
2053
|
+
element.setAttribute("data-turbo-permanent", "");
|
|
1951
2054
|
element.type = "text/css";
|
|
1952
2055
|
element.textContent = ProgressBar.defaultCSS;
|
|
1953
2056
|
if (this.cspNonce) {
|
|
@@ -2219,6 +2322,12 @@ Copyright © 2023 37signals LLC
|
|
|
2219
2322
|
contentTypeMismatch: -2
|
|
2220
2323
|
};
|
|
2221
2324
|
|
|
2325
|
+
const Direction = {
|
|
2326
|
+
advance: "forward",
|
|
2327
|
+
restore: "back",
|
|
2328
|
+
replace: "none"
|
|
2329
|
+
};
|
|
2330
|
+
|
|
2222
2331
|
class Visit {
|
|
2223
2332
|
identifier = uuid() // Required by turbo-ios
|
|
2224
2333
|
timingMetrics = {}
|
|
@@ -2248,7 +2357,8 @@ Copyright © 2023 37signals LLC
|
|
|
2248
2357
|
willRender,
|
|
2249
2358
|
updateHistory,
|
|
2250
2359
|
shouldCacheSnapshot,
|
|
2251
|
-
acceptsStreamResponse
|
|
2360
|
+
acceptsStreamResponse,
|
|
2361
|
+
direction
|
|
2252
2362
|
} = {
|
|
2253
2363
|
...defaultOptions,
|
|
2254
2364
|
...options
|
|
@@ -2266,6 +2376,7 @@ Copyright © 2023 37signals LLC
|
|
|
2266
2376
|
this.scrolled = !willRender;
|
|
2267
2377
|
this.shouldCacheSnapshot = shouldCacheSnapshot;
|
|
2268
2378
|
this.acceptsStreamResponse = acceptsStreamResponse;
|
|
2379
|
+
this.direction = direction || Direction[action];
|
|
2269
2380
|
}
|
|
2270
2381
|
|
|
2271
2382
|
get adapter() {
|
|
@@ -2518,7 +2629,7 @@ Copyright © 2023 37signals LLC
|
|
|
2518
2629
|
// Scrolling
|
|
2519
2630
|
|
|
2520
2631
|
performScroll() {
|
|
2521
|
-
if (!this.scrolled && !this.view.forceReloaded && !this.view.
|
|
2632
|
+
if (!this.scrolled && !this.view.forceReloaded && !this.view.shouldPreserveScrollPosition(this)) {
|
|
2522
2633
|
if (this.action == "restore") {
|
|
2523
2634
|
this.scrollToRestoredPosition() || this.scrollToAnchor() || this.view.scrollToTop();
|
|
2524
2635
|
} else {
|
|
@@ -2593,9 +2704,7 @@ Copyright © 2023 37signals LLC
|
|
|
2593
2704
|
|
|
2594
2705
|
async render(callback) {
|
|
2595
2706
|
this.cancelRender();
|
|
2596
|
-
await
|
|
2597
|
-
this.frame = requestAnimationFrame(() => resolve());
|
|
2598
|
-
});
|
|
2707
|
+
this.frame = await nextRepaint();
|
|
2599
2708
|
await callback();
|
|
2600
2709
|
delete this.frame;
|
|
2601
2710
|
}
|
|
@@ -2873,6 +2982,7 @@ Copyright © 2023 37signals LLC
|
|
|
2873
2982
|
restorationData = {}
|
|
2874
2983
|
started = false
|
|
2875
2984
|
pageLoaded = false
|
|
2985
|
+
currentIndex = 0
|
|
2876
2986
|
|
|
2877
2987
|
constructor(delegate) {
|
|
2878
2988
|
this.delegate = delegate;
|
|
@@ -2882,6 +2992,7 @@ Copyright © 2023 37signals LLC
|
|
|
2882
2992
|
if (!this.started) {
|
|
2883
2993
|
addEventListener("popstate", this.onPopState, false);
|
|
2884
2994
|
addEventListener("load", this.onPageLoad, false);
|
|
2995
|
+
this.currentIndex = history.state?.turbo?.restorationIndex || 0;
|
|
2885
2996
|
this.started = true;
|
|
2886
2997
|
this.replace(new URL(window.location.href));
|
|
2887
2998
|
}
|
|
@@ -2904,7 +3015,9 @@ Copyright © 2023 37signals LLC
|
|
|
2904
3015
|
}
|
|
2905
3016
|
|
|
2906
3017
|
update(method, location, restorationIdentifier = uuid()) {
|
|
2907
|
-
|
|
3018
|
+
if (method === history.pushState) ++this.currentIndex;
|
|
3019
|
+
|
|
3020
|
+
const state = { turbo: { restorationIdentifier, restorationIndex: this.currentIndex } };
|
|
2908
3021
|
method.call(history, state, "", location.href);
|
|
2909
3022
|
this.location = location;
|
|
2910
3023
|
this.restorationIdentifier = restorationIdentifier;
|
|
@@ -2948,9 +3061,11 @@ Copyright © 2023 37signals LLC
|
|
|
2948
3061
|
const { turbo } = event.state || {};
|
|
2949
3062
|
if (turbo) {
|
|
2950
3063
|
this.location = new URL(window.location.href);
|
|
2951
|
-
const { restorationIdentifier } = turbo;
|
|
3064
|
+
const { restorationIdentifier, restorationIndex } = turbo;
|
|
2952
3065
|
this.restorationIdentifier = restorationIdentifier;
|
|
2953
|
-
this.
|
|
3066
|
+
const direction = restorationIndex > this.currentIndex ? "forward" : "back";
|
|
3067
|
+
this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(this.location, restorationIdentifier, direction);
|
|
3068
|
+
this.currentIndex = restorationIndex;
|
|
2954
3069
|
}
|
|
2955
3070
|
}
|
|
2956
3071
|
}
|
|
@@ -2972,6 +3087,176 @@ Copyright © 2023 37signals LLC
|
|
|
2972
3087
|
}
|
|
2973
3088
|
}
|
|
2974
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
|
+
|
|
2975
3260
|
class Navigator {
|
|
2976
3261
|
constructor(delegate) {
|
|
2977
3262
|
this.delegate = delegate;
|
|
@@ -3287,7 +3572,7 @@ Copyright © 2023 37signals LLC
|
|
|
3287
3572
|
}
|
|
3288
3573
|
|
|
3289
3574
|
callback();
|
|
3290
|
-
await
|
|
3575
|
+
await nextRepaint();
|
|
3291
3576
|
|
|
3292
3577
|
const hasNoActiveElement = document.activeElement == null || document.activeElement == document.body;
|
|
3293
3578
|
|
|
@@ -3442,722 +3727,838 @@ Copyright © 2023 37signals LLC
|
|
|
3442
3727
|
}
|
|
3443
3728
|
}
|
|
3444
3729
|
|
|
3445
|
-
|
|
3730
|
+
// base IIFE to define idiomorph
|
|
3731
|
+
var Idiomorph = (function () {
|
|
3446
3732
|
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3733
|
+
//=============================================================================
|
|
3734
|
+
// AND NOW IT BEGINS...
|
|
3735
|
+
//=============================================================================
|
|
3736
|
+
let EMPTY_SET = new Set();
|
|
3451
3737
|
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
return morphNormalizedContent(oldNode, normalizedContent, ctx);
|
|
3465
|
-
}
|
|
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,
|
|
3466
3749
|
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
}));
|
|
3481
|
-
});
|
|
3482
|
-
return;
|
|
3483
|
-
}
|
|
3484
|
-
}
|
|
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
|
+
};
|
|
3485
3763
|
|
|
3486
|
-
|
|
3764
|
+
//=============================================================================
|
|
3765
|
+
// Core Morphing Algorithm - morph, morphNormalizedContent, morphOldNodeTo, morphChildren
|
|
3766
|
+
//=============================================================================
|
|
3767
|
+
function morph(oldNode, newContent, config = {}) {
|
|
3487
3768
|
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3769
|
+
if (oldNode instanceof Document) {
|
|
3770
|
+
oldNode = oldNode.documentElement;
|
|
3771
|
+
}
|
|
3491
3772
|
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx);
|
|
3773
|
+
if (typeof newContent === 'string') {
|
|
3774
|
+
newContent = parseContent(newContent);
|
|
3775
|
+
}
|
|
3496
3776
|
|
|
3497
|
-
|
|
3498
|
-
let previousSibling = bestMatch?.previousSibling;
|
|
3499
|
-
let nextSibling = bestMatch?.nextSibling;
|
|
3777
|
+
let normalizedContent = normalizeContent(newContent);
|
|
3500
3778
|
|
|
3501
|
-
|
|
3502
|
-
let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx);
|
|
3779
|
+
let ctx = createMorphContext(oldNode, normalizedContent, config);
|
|
3503
3780
|
|
|
3504
|
-
|
|
3505
|
-
// if there was a best match, merge the siblings in too and return the
|
|
3506
|
-
// whole bunch
|
|
3507
|
-
return insertSiblings(previousSibling, morphedNode, nextSibling);
|
|
3508
|
-
} else {
|
|
3509
|
-
// otherwise nothing was added to the DOM
|
|
3510
|
-
return []
|
|
3781
|
+
return morphNormalizedContent(oldNode, normalizedContent, ctx);
|
|
3511
3782
|
}
|
|
3512
|
-
} else {
|
|
3513
|
-
throw "Do not understand how to morph style " + ctx.morphStyle;
|
|
3514
|
-
}
|
|
3515
|
-
}
|
|
3516
|
-
|
|
3517
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
|
+
}
|
|
3518
3802
|
|
|
3519
|
-
|
|
3520
|
-
* @param oldNode root node to merge content into
|
|
3521
|
-
* @param newContent new content to merge
|
|
3522
|
-
* @param ctx the merge context
|
|
3523
|
-
* @returns {Element} the element that ended up in the DOM
|
|
3524
|
-
*/
|
|
3525
|
-
function morphOldNodeTo(oldNode, newContent, ctx) {
|
|
3526
|
-
if (ctx.ignoreActive && oldNode === document.activeElement) ; else if (newContent == null) {
|
|
3527
|
-
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return;
|
|
3528
|
-
|
|
3529
|
-
oldNode.remove();
|
|
3530
|
-
ctx.callbacks.afterNodeRemoved(oldNode);
|
|
3531
|
-
return null;
|
|
3532
|
-
} else if (!isSoftMatch(oldNode, newContent)) {
|
|
3533
|
-
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return;
|
|
3534
|
-
if (ctx.callbacks.beforeNodeAdded(newContent) === false) return;
|
|
3535
|
-
|
|
3536
|
-
oldNode.parentElement.replaceChild(newContent, oldNode);
|
|
3537
|
-
ctx.callbacks.afterNodeAdded(newContent);
|
|
3538
|
-
ctx.callbacks.afterNodeRemoved(oldNode);
|
|
3539
|
-
return newContent;
|
|
3540
|
-
} else {
|
|
3541
|
-
if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return;
|
|
3803
|
+
if (ctx.morphStyle === "innerHTML") {
|
|
3542
3804
|
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
syncNodeFrom(newContent, oldNode);
|
|
3547
|
-
morphChildren(newContent, oldNode, ctx);
|
|
3548
|
-
}
|
|
3549
|
-
ctx.callbacks.afterNodeMorphed(oldNode, newContent);
|
|
3550
|
-
return oldNode;
|
|
3551
|
-
}
|
|
3552
|
-
}
|
|
3805
|
+
// innerHTML, so we are only updating the children
|
|
3806
|
+
morphChildren(normalizedNewContent, oldNode, ctx);
|
|
3807
|
+
return oldNode.children;
|
|
3553
3808
|
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
*
|
|
3559
|
-
* Basic algorithm is, for each node in the new content:
|
|
3560
|
-
*
|
|
3561
|
-
* - if we have reached the end of the old parent, append the new content
|
|
3562
|
-
* - if the new content has an id set match with the current insertion point, morph
|
|
3563
|
-
* - search for an id set match
|
|
3564
|
-
* - if id set match found, morph
|
|
3565
|
-
* - otherwise search for a "soft" match
|
|
3566
|
-
* - if a soft match is found, morph
|
|
3567
|
-
* - otherwise, prepend the new node before the current insertion point
|
|
3568
|
-
*
|
|
3569
|
-
* The two search algorithms terminate if competing node matches appear to outweigh what can be achieved
|
|
3570
|
-
* with the current node. See findIdSetMatch() and findSoftMatch() for details.
|
|
3571
|
-
*
|
|
3572
|
-
* @param {Element} newParent the parent element of the new content
|
|
3573
|
-
* @param {Element } oldParent the old content that we are merging the new content into
|
|
3574
|
-
* @param ctx the merge context
|
|
3575
|
-
*/
|
|
3576
|
-
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);
|
|
3577
3813
|
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
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;
|
|
3581
3817
|
|
|
3582
|
-
|
|
3583
|
-
|
|
3818
|
+
// morph it
|
|
3819
|
+
let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx);
|
|
3584
3820
|
|
|
3585
|
-
|
|
3586
|
-
|
|
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
|
+
}
|
|
3587
3833
|
|
|
3588
|
-
// if we are at the end of the exiting parent's children, just append
|
|
3589
|
-
if (insertionPoint == null) {
|
|
3590
|
-
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
|
3591
3834
|
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3835
|
+
/**
|
|
3836
|
+
* @param possibleActiveElement
|
|
3837
|
+
* @param ctx
|
|
3838
|
+
* @returns {boolean}
|
|
3839
|
+
*/
|
|
3840
|
+
function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
|
|
3841
|
+
return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement;
|
|
3596
3842
|
}
|
|
3597
3843
|
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
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
|
+
}
|
|
3604
3879
|
}
|
|
3605
3880
|
|
|
3606
|
-
|
|
3607
|
-
|
|
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
|
+
}
|
|
3608
3924
|
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
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
|
+
}
|
|
3616
3932
|
|
|
3617
|
-
|
|
3618
|
-
|
|
3933
|
+
// otherwise search forward in the existing old children for an id set match
|
|
3934
|
+
let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx);
|
|
3619
3935
|
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
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
|
+
}
|
|
3627
3943
|
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
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);
|
|
3631
3946
|
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
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
|
+
}
|
|
3636
3954
|
|
|
3637
|
-
|
|
3638
|
-
|
|
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;
|
|
3639
3958
|
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
}
|
|
3959
|
+
oldParent.insertBefore(newChild, insertionPoint);
|
|
3960
|
+
ctx.callbacks.afterNodeAdded(newChild);
|
|
3961
|
+
removeIdsFromConsideration(ctx, newChild);
|
|
3962
|
+
}
|
|
3645
3963
|
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
//=============================================================================
|
|
3964
|
+
// remove any remaining old nodes that didn't match up with new content
|
|
3965
|
+
while (insertionPoint !== null) {
|
|
3649
3966
|
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
*
|
|
3654
|
-
* @param {Element} from the element to copy attributes & state from
|
|
3655
|
-
* @param {Element} to the element to copy attributes & state to
|
|
3656
|
-
*/
|
|
3657
|
-
function syncNodeFrom(from, to) {
|
|
3658
|
-
let type = from.nodeType;
|
|
3659
|
-
|
|
3660
|
-
// if is an element type, sync the attributes from the
|
|
3661
|
-
// new node into the new node
|
|
3662
|
-
if (type === 1 /* element type */) {
|
|
3663
|
-
const fromAttributes = from.attributes;
|
|
3664
|
-
const toAttributes = to.attributes;
|
|
3665
|
-
for (const fromAttribute of fromAttributes) {
|
|
3666
|
-
if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) {
|
|
3667
|
-
to.setAttribute(fromAttribute.name, fromAttribute.value);
|
|
3967
|
+
let tempNode = insertionPoint;
|
|
3968
|
+
insertionPoint = insertionPoint.nextSibling;
|
|
3969
|
+
removeNode(tempNode, ctx);
|
|
3668
3970
|
}
|
|
3669
3971
|
}
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
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;
|
|
3673
3987
|
}
|
|
3988
|
+
return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false;
|
|
3674
3989
|
}
|
|
3675
|
-
}
|
|
3676
3990
|
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
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
|
+
}
|
|
3683
4026
|
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
if (from instanceof HTMLInputElement &&
|
|
3691
|
-
to instanceof HTMLInputElement &&
|
|
3692
|
-
from.type !== 'file') {
|
|
3693
|
-
|
|
3694
|
-
to.value = from.value || '';
|
|
3695
|
-
syncAttribute(from, to, 'value');
|
|
3696
|
-
|
|
3697
|
-
// sync boolean attributes
|
|
3698
|
-
syncAttribute(from, to, 'checked');
|
|
3699
|
-
syncAttribute(from, to, 'disabled');
|
|
3700
|
-
} else if (from instanceof HTMLOptionElement) {
|
|
3701
|
-
syncAttribute(from, to, 'selected');
|
|
3702
|
-
} else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) {
|
|
3703
|
-
let fromValue = from.value;
|
|
3704
|
-
let toValue = to.value;
|
|
3705
|
-
if (fromValue !== toValue) {
|
|
3706
|
-
to.value = fromValue;
|
|
3707
|
-
}
|
|
3708
|
-
if (to.firstChild && to.firstChild.nodeValue !== fromValue) {
|
|
3709
|
-
to.firstChild.nodeValue = fromValue;
|
|
3710
|
-
}
|
|
3711
|
-
}
|
|
3712
|
-
}
|
|
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
|
+
}
|
|
3713
4033
|
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
} else {
|
|
3719
|
-
to.removeAttribute(attributeName);
|
|
4034
|
+
if (!ignoreValueOfActiveElement(to, ctx)) {
|
|
4035
|
+
// sync input values
|
|
4036
|
+
syncInputValue(from, to, ctx);
|
|
4037
|
+
}
|
|
3720
4038
|
}
|
|
3721
|
-
}
|
|
3722
|
-
}
|
|
3723
4039
|
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
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
|
+
}
|
|
3728
4063
|
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
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
|
+
}
|
|
3733
4113
|
|
|
3734
|
-
|
|
4114
|
+
//=============================================================================
|
|
4115
|
+
// the HEAD tag can be handled specially, either w/ a 'merge' or 'append' style
|
|
4116
|
+
//=============================================================================
|
|
4117
|
+
function handleHeadElement(newHeadTag, currentHead, ctx) {
|
|
3735
4118
|
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
}
|
|
4119
|
+
let added = [];
|
|
4120
|
+
let removed = [];
|
|
4121
|
+
let preserved = [];
|
|
4122
|
+
let nodesToAppend = [];
|
|
3741
4123
|
|
|
3742
|
-
|
|
3743
|
-
for (const currentHeadElt of currentHead.children) {
|
|
4124
|
+
let headMergeStyle = ctx.head.style;
|
|
3744
4125
|
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
if (inNewContent || isPreserved) {
|
|
3750
|
-
if (isReAppended) {
|
|
3751
|
-
// remove the current version and let the new version replace it and re-execute
|
|
3752
|
-
removed.push(currentHeadElt);
|
|
3753
|
-
} else {
|
|
3754
|
-
// this element already exists and should not be re-appended, so remove it from
|
|
3755
|
-
// the new content map, preserving it in the DOM
|
|
3756
|
-
srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
|
|
3757
|
-
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);
|
|
3758
4130
|
}
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
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
|
+
}
|
|
3766
4163
|
}
|
|
3767
|
-
}
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
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);
|
|
3771
4187
|
}
|
|
3772
4188
|
}
|
|
3773
|
-
}
|
|
3774
|
-
}
|
|
3775
4189
|
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
|
|
3784
|
-
if (newElt.href || newElt.src) {
|
|
3785
|
-
let resolve = null;
|
|
3786
|
-
let promise = new Promise(function (_resolve) {
|
|
3787
|
-
resolve = _resolve;
|
|
3788
|
-
});
|
|
3789
|
-
newElt.addEventListener('load',function() {
|
|
3790
|
-
resolve();
|
|
3791
|
-
});
|
|
3792
|
-
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
|
+
}
|
|
3793
4197
|
}
|
|
3794
|
-
currentHead.appendChild(newElt);
|
|
3795
|
-
ctx.callbacks.afterNodeAdded(newElt);
|
|
3796
|
-
added.push(newElt);
|
|
3797
|
-
}
|
|
3798
|
-
}
|
|
3799
4198
|
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
for (const removedElement of removed) {
|
|
3803
|
-
if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
|
|
3804
|
-
currentHead.removeChild(removedElement);
|
|
3805
|
-
ctx.callbacks.afterNodeRemoved(removedElement);
|
|
4199
|
+
ctx.head.afterHeadMorphed(currentHead, {added: added, kept: preserved, removed: removed});
|
|
4200
|
+
return promises;
|
|
3806
4201
|
}
|
|
3807
|
-
}
|
|
3808
4202
|
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
}
|
|
4203
|
+
function noOp() {
|
|
4204
|
+
}
|
|
3812
4205
|
|
|
3813
|
-
|
|
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
|
+
}
|
|
3814
4227
|
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
afterNodeRemoved : noOp,
|
|
3831
|
-
|
|
3832
|
-
}, config.callbacks),
|
|
3833
|
-
head: Object.assign({
|
|
3834
|
-
style: 'merge',
|
|
3835
|
-
shouldPreserve : function(elt) {
|
|
3836
|
-
return elt.getAttribute("im-preserve") === "true";
|
|
3837
|
-
},
|
|
3838
|
-
shouldReAppend : function(elt) {
|
|
3839
|
-
return elt.getAttribute("im-re-append") === "true";
|
|
3840
|
-
},
|
|
3841
|
-
shouldRemove : noOp,
|
|
3842
|
-
afterHeadMorphed : noOp,
|
|
3843
|
-
}, config.head),
|
|
3844
|
-
}
|
|
3845
|
-
}
|
|
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
|
+
}
|
|
3846
4243
|
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
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;
|
|
3856
4256
|
}
|
|
3857
|
-
}
|
|
3858
|
-
return false;
|
|
3859
|
-
}
|
|
3860
4257
|
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
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
|
|
4263
|
+
}
|
|
3867
4264
|
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
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
|
+
}
|
|
3877
4274
|
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
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
|
+
}
|
|
3885
4302
|
|
|
3886
|
-
|
|
3887
|
-
|
|
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
|
+
}
|
|
3888
4310
|
|
|
3889
|
-
|
|
4311
|
+
// advanced to the next old content child
|
|
4312
|
+
potentialMatch = potentialMatch.nextSibling;
|
|
4313
|
+
}
|
|
4314
|
+
}
|
|
4315
|
+
return potentialMatch;
|
|
4316
|
+
}
|
|
3890
4317
|
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
// if
|
|
3895
|
-
//
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
let otherMatchCount = 0;
|
|
3899
|
-
while (potentialMatch != null) {
|
|
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) {
|
|
3900
4325
|
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
}
|
|
4326
|
+
let potentialSoftMatch = insertionPoint;
|
|
4327
|
+
let nextSibling = newChild.nextSibling;
|
|
4328
|
+
let siblingSoftMatchCount = 0;
|
|
3905
4329
|
|
|
3906
|
-
|
|
3907
|
-
otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent);
|
|
3908
|
-
if (otherMatchCount > newChildPotentialIdCount) {
|
|
3909
|
-
// if we have more potential id matches in _other_ content, we
|
|
3910
|
-
// do not have a good candidate for an id match, so return null
|
|
3911
|
-
return null;
|
|
3912
|
-
}
|
|
4330
|
+
while (potentialSoftMatch != null) {
|
|
3913
4331
|
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
}
|
|
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
|
+
}
|
|
3920
4337
|
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
// potential soft matches for the next two pieces of new content
|
|
3926
|
-
//=============================================================================
|
|
3927
|
-
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
|
+
}
|
|
3928
4342
|
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
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;
|
|
3932
4348
|
|
|
3933
|
-
|
|
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
|
+
}
|
|
3934
4355
|
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
return null;
|
|
3939
|
-
}
|
|
4356
|
+
// advanced to the next old content child
|
|
4357
|
+
potentialSoftMatch = potentialSoftMatch.nextSibling;
|
|
4358
|
+
}
|
|
3940
4359
|
|
|
3941
|
-
// if we have a soft match with the current node, return it
|
|
3942
|
-
if (isSoftMatch(newChild, potentialSoftMatch)) {
|
|
3943
4360
|
return potentialSoftMatch;
|
|
3944
4361
|
}
|
|
3945
4362
|
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
//
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
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
|
|
3956
4393
|
}
|
|
3957
4394
|
}
|
|
3958
4395
|
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
// if the newContent contains a html, head or body tag, we can simply parse it w/o wrapping
|
|
3973
|
-
if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) {
|
|
3974
|
-
let content = parser.parseFromString(newContent, "text/html");
|
|
3975
|
-
// if it is a full HTML document, return the document itself as the parent container
|
|
3976
|
-
if (contentWithSvgsRemoved.match(/<\/html>/)) {
|
|
3977
|
-
content.generatedByIdiomorph = true;
|
|
3978
|
-
return content;
|
|
3979
|
-
} else {
|
|
3980
|
-
// otherwise return the html element as the parent container
|
|
3981
|
-
let htmlElement = content.firstChild;
|
|
3982
|
-
if (htmlElement) {
|
|
3983
|
-
htmlElement.generatedByIdiomorph = true;
|
|
3984
|
-
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;
|
|
3985
4409
|
} else {
|
|
3986
|
-
|
|
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;
|
|
3987
4417
|
}
|
|
3988
4418
|
}
|
|
3989
|
-
} else {
|
|
3990
|
-
// if it is partial HTML, wrap it in a template tag to provide a parent element and also to help
|
|
3991
|
-
// deal with touchy tags like tr, tbody, etc.
|
|
3992
|
-
let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html");
|
|
3993
|
-
let content = responseDoc.body.querySelector('template').content;
|
|
3994
|
-
content.generatedByIdiomorph = true;
|
|
3995
|
-
return content
|
|
3996
|
-
}
|
|
3997
|
-
}
|
|
3998
|
-
|
|
3999
|
-
function normalizeContent(newContent) {
|
|
4000
|
-
if (newContent == null) {
|
|
4001
|
-
// noinspection UnnecessaryLocalVariableJS
|
|
4002
|
-
const dummyParent = document.createElement('div');
|
|
4003
|
-
return dummyParent;
|
|
4004
|
-
} else if (newContent.generatedByIdiomorph) {
|
|
4005
|
-
// the template tag created by idiomorph parsing can serve as a dummy parent
|
|
4006
|
-
return newContent;
|
|
4007
|
-
} else if (newContent instanceof Node) {
|
|
4008
|
-
// a single node is added as a child to a dummy parent
|
|
4009
|
-
const dummyParent = document.createElement('div');
|
|
4010
|
-
dummyParent.append(newContent);
|
|
4011
|
-
return dummyParent;
|
|
4012
|
-
} else {
|
|
4013
|
-
// all nodes in the array or HTMLElement collection are consolidated under
|
|
4014
|
-
// a single dummy parent element
|
|
4015
|
-
const dummyParent = document.createElement('div');
|
|
4016
|
-
for (const elt of [...newContent]) {
|
|
4017
|
-
dummyParent.append(elt);
|
|
4018
|
-
}
|
|
4019
|
-
return dummyParent;
|
|
4020
|
-
}
|
|
4021
|
-
}
|
|
4022
4419
|
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
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
|
+
}
|
|
4046
4443
|
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
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;
|
|
4057
4458
|
}
|
|
4058
|
-
currentElement = currentElement.nextSibling;
|
|
4059
|
-
}
|
|
4060
|
-
return bestElement;
|
|
4061
|
-
}
|
|
4062
4459
|
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4460
|
+
function scoreElement(node1, node2, ctx) {
|
|
4461
|
+
if (isSoftMatch(node1, node2)) {
|
|
4462
|
+
return .5 + getIdIntersectionCount(ctx, node1, node2);
|
|
4463
|
+
}
|
|
4464
|
+
return 0;
|
|
4465
|
+
}
|
|
4069
4466
|
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4467
|
+
function removeNode(tempNode, ctx) {
|
|
4468
|
+
removeIdsFromConsideration(ctx, tempNode);
|
|
4469
|
+
if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return;
|
|
4073
4470
|
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4471
|
+
tempNode.remove();
|
|
4472
|
+
ctx.callbacks.afterNodeRemoved(tempNode);
|
|
4473
|
+
}
|
|
4077
4474
|
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4475
|
+
//=============================================================================
|
|
4476
|
+
// ID Set Functions
|
|
4477
|
+
//=============================================================================
|
|
4081
4478
|
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4479
|
+
function isIdInConsideration(ctx, id) {
|
|
4480
|
+
return !ctx.deadIds.has(id);
|
|
4481
|
+
}
|
|
4085
4482
|
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4483
|
+
function idIsWithinNode(ctx, id, targetNode) {
|
|
4484
|
+
let idSet = ctx.idMap.get(targetNode) || EMPTY_SET;
|
|
4485
|
+
return idSet.has(id);
|
|
4486
|
+
}
|
|
4090
4487
|
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
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
|
+
}
|
|
4097
4494
|
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
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;
|
|
4106
4506
|
}
|
|
4107
|
-
}
|
|
4108
|
-
return matchCount;
|
|
4109
|
-
}
|
|
4110
4507
|
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
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
|
+
}
|
|
4133
4534
|
}
|
|
4134
|
-
idSet.add(elt.id);
|
|
4135
|
-
current = current.parentElement;
|
|
4136
4535
|
}
|
|
4137
|
-
}
|
|
4138
|
-
}
|
|
4139
4536
|
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
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
|
+
}
|
|
4156
4553
|
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4554
|
+
//=============================================================================
|
|
4555
|
+
// This is what ends up becoming the Idiomorph global object
|
|
4556
|
+
//=============================================================================
|
|
4557
|
+
return {
|
|
4558
|
+
morph,
|
|
4559
|
+
defaults
|
|
4560
|
+
}
|
|
4561
|
+
})();
|
|
4161
4562
|
|
|
4162
4563
|
class MorphRenderer extends Renderer {
|
|
4163
4564
|
async render() {
|
|
@@ -4185,7 +4586,7 @@ Copyright © 2023 37signals LLC
|
|
|
4185
4586
|
#morphElements(currentElement, newElement, morphStyle = "outerHTML") {
|
|
4186
4587
|
this.isMorphingTurboFrame = this.#isFrameReloadedWithMorph(currentElement);
|
|
4187
4588
|
|
|
4188
|
-
|
|
4589
|
+
Idiomorph.morph(currentElement, newElement, {
|
|
4189
4590
|
morphStyle: morphStyle,
|
|
4190
4591
|
callbacks: {
|
|
4191
4592
|
beforeNodeAdded: this.#shouldAddElement,
|
|
@@ -4317,8 +4718,13 @@ Copyright © 2023 37signals LLC
|
|
|
4317
4718
|
const mergedHeadElements = this.mergeProvisionalElements();
|
|
4318
4719
|
const newStylesheetElements = this.copyNewHeadStylesheetElements();
|
|
4319
4720
|
this.copyNewHeadScriptElements();
|
|
4721
|
+
|
|
4320
4722
|
await mergedHeadElements;
|
|
4321
4723
|
await newStylesheetElements;
|
|
4724
|
+
|
|
4725
|
+
if (this.willRender) {
|
|
4726
|
+
this.removeUnusedHeadStylesheetElements();
|
|
4727
|
+
}
|
|
4322
4728
|
}
|
|
4323
4729
|
|
|
4324
4730
|
async replaceBody() {
|
|
@@ -4350,6 +4756,12 @@ Copyright © 2023 37signals LLC
|
|
|
4350
4756
|
}
|
|
4351
4757
|
}
|
|
4352
4758
|
|
|
4759
|
+
removeUnusedHeadStylesheetElements() {
|
|
4760
|
+
for (const element of this.unusedHeadStylesheetElements) {
|
|
4761
|
+
document.head.removeChild(element);
|
|
4762
|
+
}
|
|
4763
|
+
}
|
|
4764
|
+
|
|
4353
4765
|
async mergeProvisionalElements() {
|
|
4354
4766
|
const newHeadElements = [...this.newHeadProvisionalElements];
|
|
4355
4767
|
|
|
@@ -4415,6 +4827,20 @@ Copyright © 2023 37signals LLC
|
|
|
4415
4827
|
await this.renderElement(this.currentElement, this.newElement);
|
|
4416
4828
|
}
|
|
4417
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
|
+
|
|
4418
4844
|
get newHeadStylesheetElements() {
|
|
4419
4845
|
return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot)
|
|
4420
4846
|
}
|
|
@@ -4541,7 +4967,11 @@ Copyright © 2023 37signals LLC
|
|
|
4541
4967
|
}
|
|
4542
4968
|
|
|
4543
4969
|
isPageRefresh(visit) {
|
|
4544
|
-
return !visit || (this.lastRenderedLocation.
|
|
4970
|
+
return !visit || (this.lastRenderedLocation.pathname === visit.location.pathname && visit.action === "replace")
|
|
4971
|
+
}
|
|
4972
|
+
|
|
4973
|
+
shouldPreserveScrollPosition(visit) {
|
|
4974
|
+
return this.isPageRefresh(visit) && this.snapshot.shouldPreserveScrollPosition
|
|
4545
4975
|
}
|
|
4546
4976
|
|
|
4547
4977
|
get snapshot() {
|
|
@@ -4552,27 +4982,28 @@ Copyright © 2023 37signals LLC
|
|
|
4552
4982
|
class Preloader {
|
|
4553
4983
|
selector = "a[data-turbo-preload]"
|
|
4554
4984
|
|
|
4555
|
-
constructor(delegate) {
|
|
4985
|
+
constructor(delegate, snapshotCache) {
|
|
4556
4986
|
this.delegate = delegate;
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
get snapshotCache() {
|
|
4560
|
-
return this.delegate.navigator.view.snapshotCache
|
|
4987
|
+
this.snapshotCache = snapshotCache;
|
|
4561
4988
|
}
|
|
4562
4989
|
|
|
4563
4990
|
start() {
|
|
4564
4991
|
if (document.readyState === "loading") {
|
|
4565
|
-
|
|
4566
|
-
this.preloadOnLoadLinksForView(document.body);
|
|
4567
|
-
})
|
|
4992
|
+
document.addEventListener("DOMContentLoaded", this.#preloadAll);
|
|
4568
4993
|
} else {
|
|
4569
4994
|
this.preloadOnLoadLinksForView(document.body);
|
|
4570
4995
|
}
|
|
4571
4996
|
}
|
|
4572
4997
|
|
|
4998
|
+
stop() {
|
|
4999
|
+
document.removeEventListener("DOMContentLoaded", this.#preloadAll);
|
|
5000
|
+
}
|
|
5001
|
+
|
|
4573
5002
|
preloadOnLoadLinksForView(element) {
|
|
4574
5003
|
for (const link of element.querySelectorAll(this.selector)) {
|
|
4575
|
-
this.
|
|
5004
|
+
if (this.delegate.shouldPreloadLink(link)) {
|
|
5005
|
+
this.preloadURL(link);
|
|
5006
|
+
}
|
|
4576
5007
|
}
|
|
4577
5008
|
}
|
|
4578
5009
|
|
|
@@ -4583,31 +5014,39 @@ Copyright © 2023 37signals LLC
|
|
|
4583
5014
|
return
|
|
4584
5015
|
}
|
|
4585
5016
|
|
|
5017
|
+
const fetchRequest = new FetchRequest(this, FetchMethod.get, location, new URLSearchParams(), link);
|
|
5018
|
+
await fetchRequest.perform();
|
|
5019
|
+
}
|
|
5020
|
+
|
|
5021
|
+
// Fetch request delegate
|
|
5022
|
+
|
|
5023
|
+
prepareRequest(fetchRequest) {
|
|
5024
|
+
fetchRequest.headers["Sec-Purpose"] = "prefetch";
|
|
5025
|
+
}
|
|
5026
|
+
|
|
5027
|
+
async requestSucceededWithResponse(fetchRequest, fetchResponse) {
|
|
4586
5028
|
try {
|
|
4587
|
-
const
|
|
4588
|
-
const
|
|
4589
|
-
const snapshot = PageSnapshot.fromHTMLString(responseText);
|
|
5029
|
+
const responseHTML = await fetchResponse.responseHTML;
|
|
5030
|
+
const snapshot = PageSnapshot.fromHTMLString(responseHTML);
|
|
4590
5031
|
|
|
4591
|
-
this.snapshotCache.put(
|
|
5032
|
+
this.snapshotCache.put(fetchRequest.url, snapshot);
|
|
4592
5033
|
} catch (_) {
|
|
4593
5034
|
// If we cannot preload that is ok!
|
|
4594
5035
|
}
|
|
4595
5036
|
}
|
|
4596
|
-
}
|
|
4597
5037
|
|
|
4598
|
-
|
|
4599
|
-
constructor(maxSize) {
|
|
4600
|
-
super();
|
|
4601
|
-
this.maxSize = maxSize;
|
|
4602
|
-
}
|
|
5038
|
+
requestStarted(fetchRequest) {}
|
|
4603
5039
|
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
5040
|
+
requestErrored(fetchRequest) {}
|
|
5041
|
+
|
|
5042
|
+
requestFinished(fetchRequest) {}
|
|
5043
|
+
|
|
5044
|
+
requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}
|
|
5045
|
+
|
|
5046
|
+
requestFailedWithResponse(fetchRequest, fetchResponse) {}
|
|
5047
|
+
|
|
5048
|
+
#preloadAll = () => {
|
|
5049
|
+
this.preloadOnLoadLinksForView(document.body);
|
|
4611
5050
|
}
|
|
4612
5051
|
}
|
|
4613
5052
|
|
|
@@ -4640,12 +5079,12 @@ Copyright © 2023 37signals LLC
|
|
|
4640
5079
|
class Session {
|
|
4641
5080
|
navigator = new Navigator(this)
|
|
4642
5081
|
history = new History(this)
|
|
4643
|
-
preloader = new Preloader(this)
|
|
4644
5082
|
view = new PageView(this, document.documentElement)
|
|
4645
5083
|
adapter = new BrowserAdapter(this)
|
|
4646
5084
|
|
|
4647
5085
|
pageObserver = new PageObserver(this)
|
|
4648
5086
|
cacheObserver = new CacheObserver()
|
|
5087
|
+
linkPrefetchObserver = new LinkPrefetchObserver(this, document)
|
|
4649
5088
|
linkClickObserver = new LinkClickObserver(this, window)
|
|
4650
5089
|
formSubmitObserver = new FormSubmitObserver(this, document)
|
|
4651
5090
|
scrollObserver = new ScrollObserver(this)
|
|
@@ -4654,18 +5093,26 @@ Copyright © 2023 37signals LLC
|
|
|
4654
5093
|
frameRedirector = new FrameRedirector(this, document.documentElement)
|
|
4655
5094
|
streamMessageRenderer = new StreamMessageRenderer()
|
|
4656
5095
|
cache = new Cache(this)
|
|
4657
|
-
recentRequests = new LimitedSet(20)
|
|
4658
5096
|
|
|
4659
5097
|
drive = true
|
|
4660
5098
|
enabled = true
|
|
4661
5099
|
progressBarDelay = 500
|
|
4662
5100
|
started = false
|
|
4663
5101
|
formMode = "on"
|
|
5102
|
+
#pageRefreshDebouncePeriod = 150
|
|
5103
|
+
|
|
5104
|
+
constructor(recentRequests) {
|
|
5105
|
+
this.recentRequests = recentRequests;
|
|
5106
|
+
this.preloader = new Preloader(this, this.view.snapshotCache);
|
|
5107
|
+
this.debouncedRefresh = this.refresh;
|
|
5108
|
+
this.pageRefreshDebouncePeriod = this.pageRefreshDebouncePeriod;
|
|
5109
|
+
}
|
|
4664
5110
|
|
|
4665
5111
|
start() {
|
|
4666
5112
|
if (!this.started) {
|
|
4667
5113
|
this.pageObserver.start();
|
|
4668
5114
|
this.cacheObserver.start();
|
|
5115
|
+
this.linkPrefetchObserver.start();
|
|
4669
5116
|
this.formLinkClickObserver.start();
|
|
4670
5117
|
this.linkClickObserver.start();
|
|
4671
5118
|
this.formSubmitObserver.start();
|
|
@@ -4687,6 +5134,7 @@ Copyright © 2023 37signals LLC
|
|
|
4687
5134
|
if (this.started) {
|
|
4688
5135
|
this.pageObserver.stop();
|
|
4689
5136
|
this.cacheObserver.stop();
|
|
5137
|
+
this.linkPrefetchObserver.stop();
|
|
4690
5138
|
this.formLinkClickObserver.stop();
|
|
4691
5139
|
this.linkClickObserver.stop();
|
|
4692
5140
|
this.formSubmitObserver.stop();
|
|
@@ -4694,6 +5142,7 @@ Copyright © 2023 37signals LLC
|
|
|
4694
5142
|
this.streamObserver.stop();
|
|
4695
5143
|
this.frameRedirector.stop();
|
|
4696
5144
|
this.history.stop();
|
|
5145
|
+
this.preloader.stop();
|
|
4697
5146
|
this.started = false;
|
|
4698
5147
|
}
|
|
4699
5148
|
}
|
|
@@ -4753,13 +5202,42 @@ Copyright © 2023 37signals LLC
|
|
|
4753
5202
|
return this.history.restorationIdentifier
|
|
4754
5203
|
}
|
|
4755
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
|
+
|
|
5214
|
+
// Preloader delegate
|
|
5215
|
+
|
|
5216
|
+
shouldPreloadLink(element) {
|
|
5217
|
+
const isUnsafe = element.hasAttribute("data-turbo-method");
|
|
5218
|
+
const isStream = element.hasAttribute("data-turbo-stream");
|
|
5219
|
+
const frameTarget = element.getAttribute("data-turbo-frame");
|
|
5220
|
+
const frame = frameTarget == "_top" ?
|
|
5221
|
+
null :
|
|
5222
|
+
document.getElementById(frameTarget) || findClosestRecursively(element, "turbo-frame:not([disabled])");
|
|
5223
|
+
|
|
5224
|
+
if (isUnsafe || isStream || frame instanceof FrameElement) {
|
|
5225
|
+
return false
|
|
5226
|
+
} else {
|
|
5227
|
+
const location = new URL(element.href);
|
|
5228
|
+
|
|
5229
|
+
return this.elementIsNavigatable(element) && locationIsVisitable(location, this.snapshot.rootLocation)
|
|
5230
|
+
}
|
|
5231
|
+
}
|
|
5232
|
+
|
|
4756
5233
|
// History delegate
|
|
4757
5234
|
|
|
4758
|
-
|
|
5235
|
+
historyPoppedToLocationWithRestorationIdentifierAndDirection(location, restorationIdentifier, direction) {
|
|
4759
5236
|
if (this.enabled) {
|
|
4760
5237
|
this.navigator.startVisit(location, restorationIdentifier, {
|
|
4761
5238
|
action: "restore",
|
|
4762
|
-
historyChanged: true
|
|
5239
|
+
historyChanged: true,
|
|
5240
|
+
direction
|
|
4763
5241
|
});
|
|
4764
5242
|
} else {
|
|
4765
5243
|
this.adapter.pageInvalidated({
|
|
@@ -4782,6 +5260,15 @@ Copyright © 2023 37signals LLC
|
|
|
4782
5260
|
|
|
4783
5261
|
submittedFormLinkToLocation() {}
|
|
4784
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
|
+
|
|
4785
5272
|
// Link click observer delegate
|
|
4786
5273
|
|
|
4787
5274
|
willFollowLinkToLocation(link, location, event) {
|
|
@@ -4815,6 +5302,7 @@ Copyright © 2023 37signals LLC
|
|
|
4815
5302
|
visitStarted(visit) {
|
|
4816
5303
|
if (!visit.acceptsStreamResponse) {
|
|
4817
5304
|
markAsBusy(document.documentElement);
|
|
5305
|
+
this.view.markVisitDirection(visit.direction);
|
|
4818
5306
|
}
|
|
4819
5307
|
extendURLWithDeprecatedProperties(visit.location);
|
|
4820
5308
|
if (!visit.silent) {
|
|
@@ -4823,6 +5311,7 @@ Copyright © 2023 37signals LLC
|
|
|
4823
5311
|
}
|
|
4824
5312
|
|
|
4825
5313
|
visitCompleted(visit) {
|
|
5314
|
+
this.view.unmarkVisitDirection();
|
|
4826
5315
|
clearBusyState(document.documentElement);
|
|
4827
5316
|
this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
|
|
4828
5317
|
}
|
|
@@ -4879,8 +5368,8 @@ Copyright © 2023 37signals LLC
|
|
|
4879
5368
|
}
|
|
4880
5369
|
}
|
|
4881
5370
|
|
|
4882
|
-
allowsImmediateRender({ element },
|
|
4883
|
-
const event = this.notifyApplicationBeforeRender(element,
|
|
5371
|
+
allowsImmediateRender({ element }, options) {
|
|
5372
|
+
const event = this.notifyApplicationBeforeRender(element, options);
|
|
4884
5373
|
const {
|
|
4885
5374
|
defaultPrevented,
|
|
4886
5375
|
detail: { render }
|
|
@@ -4893,9 +5382,9 @@ Copyright © 2023 37signals LLC
|
|
|
4893
5382
|
return !defaultPrevented
|
|
4894
5383
|
}
|
|
4895
5384
|
|
|
4896
|
-
viewRenderedSnapshot(_snapshot,
|
|
5385
|
+
viewRenderedSnapshot(_snapshot, _isPreview, renderMethod) {
|
|
4897
5386
|
this.view.lastRenderedLocation = this.history.location;
|
|
4898
|
-
this.notifyApplicationAfterRender(
|
|
5387
|
+
this.notifyApplicationAfterRender(renderMethod);
|
|
4899
5388
|
}
|
|
4900
5389
|
|
|
4901
5390
|
preloadOnLoadLinksForView(element) {
|
|
@@ -4951,15 +5440,15 @@ Copyright © 2023 37signals LLC
|
|
|
4951
5440
|
return dispatch("turbo:before-cache")
|
|
4952
5441
|
}
|
|
4953
5442
|
|
|
4954
|
-
notifyApplicationBeforeRender(newBody,
|
|
5443
|
+
notifyApplicationBeforeRender(newBody, options) {
|
|
4955
5444
|
return dispatch("turbo:before-render", {
|
|
4956
|
-
detail: { newBody,
|
|
5445
|
+
detail: { newBody, ...options },
|
|
4957
5446
|
cancelable: true
|
|
4958
5447
|
})
|
|
4959
5448
|
}
|
|
4960
5449
|
|
|
4961
|
-
notifyApplicationAfterRender(
|
|
4962
|
-
return dispatch("turbo:render", { detail: {
|
|
5450
|
+
notifyApplicationAfterRender(renderMethod) {
|
|
5451
|
+
return dispatch("turbo:render", { detail: { renderMethod } })
|
|
4963
5452
|
}
|
|
4964
5453
|
|
|
4965
5454
|
notifyApplicationAfterPageLoad(timing = {}) {
|
|
@@ -5061,7 +5550,7 @@ Copyright © 2023 37signals LLC
|
|
|
5061
5550
|
}
|
|
5062
5551
|
};
|
|
5063
5552
|
|
|
5064
|
-
const session = new Session();
|
|
5553
|
+
const session = new Session(recentRequests);
|
|
5065
5554
|
const { cache, navigator: navigator$1 } = session;
|
|
5066
5555
|
|
|
5067
5556
|
/**
|
|
@@ -5171,7 +5660,7 @@ Copyright © 2023 37signals LLC
|
|
|
5171
5660
|
PageRenderer: PageRenderer,
|
|
5172
5661
|
PageSnapshot: PageSnapshot,
|
|
5173
5662
|
FrameRenderer: FrameRenderer,
|
|
5174
|
-
fetch:
|
|
5663
|
+
fetch: fetchWithTurboHeaders,
|
|
5175
5664
|
start: start,
|
|
5176
5665
|
registerAdapter: registerAdapter,
|
|
5177
5666
|
visit: visit,
|
|
@@ -5419,7 +5908,7 @@ Copyright © 2023 37signals LLC
|
|
|
5419
5908
|
|
|
5420
5909
|
// View delegate
|
|
5421
5910
|
|
|
5422
|
-
allowsImmediateRender({ element: newFrame },
|
|
5911
|
+
allowsImmediateRender({ element: newFrame }, options) {
|
|
5423
5912
|
const event = dispatch("turbo:before-frame-render", {
|
|
5424
5913
|
target: this.element,
|
|
5425
5914
|
detail: { newFrame, ...options },
|
|
@@ -6040,7 +6529,7 @@ Copyright © 2023 37signals LLC
|
|
|
6040
6529
|
}
|
|
6041
6530
|
})();
|
|
6042
6531
|
|
|
6043
|
-
window.Turbo = Turbo;
|
|
6532
|
+
window.Turbo = { ...Turbo, StreamActions };
|
|
6044
6533
|
start();
|
|
6045
6534
|
|
|
6046
6535
|
exports.FetchEnctype = FetchEnctype;
|
|
@@ -6059,7 +6548,7 @@ Copyright © 2023 37signals LLC
|
|
|
6059
6548
|
exports.clearCache = clearCache;
|
|
6060
6549
|
exports.connectStreamSource = connectStreamSource;
|
|
6061
6550
|
exports.disconnectStreamSource = disconnectStreamSource;
|
|
6062
|
-
exports.fetch =
|
|
6551
|
+
exports.fetch = fetchWithTurboHeaders;
|
|
6063
6552
|
exports.fetchEnctypeFromString = fetchEnctypeFromString;
|
|
6064
6553
|
exports.fetchMethodFromString = fetchMethodFromString;
|
|
6065
6554
|
exports.isSafe = isSafe;
|