@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-esm.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
|
/**
|
|
6
6
|
* The MIT License (MIT)
|
|
@@ -633,13 +633,61 @@ async function around(callback, reader) {
|
|
|
633
633
|
return [before, after]
|
|
634
634
|
}
|
|
635
635
|
|
|
636
|
-
function
|
|
636
|
+
function doesNotTargetIFrame(anchor) {
|
|
637
|
+
if (anchor.hasAttribute("target")) {
|
|
638
|
+
for (const element of document.getElementsByName(anchor.target)) {
|
|
639
|
+
if (element instanceof HTMLIFrameElement) return false
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return true
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function findLinkFromClickTarget(target) {
|
|
647
|
+
return findClosestRecursively(target, "a[href]:not([target^=_]):not([download])")
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function getLocationForLink(link) {
|
|
651
|
+
return expandURL(link.getAttribute("href") || "")
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function debounce(fn, delay) {
|
|
655
|
+
let timeoutId = null;
|
|
656
|
+
|
|
657
|
+
return (...args) => {
|
|
658
|
+
const callback = () => fn.apply(this, args);
|
|
659
|
+
clearTimeout(timeoutId);
|
|
660
|
+
timeoutId = setTimeout(callback, delay);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
class LimitedSet extends Set {
|
|
665
|
+
constructor(maxSize) {
|
|
666
|
+
super();
|
|
667
|
+
this.maxSize = maxSize;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
add(value) {
|
|
671
|
+
if (this.size >= this.maxSize) {
|
|
672
|
+
const iterator = this.values();
|
|
673
|
+
const oldestValue = iterator.next().value;
|
|
674
|
+
this.delete(oldestValue);
|
|
675
|
+
}
|
|
676
|
+
super.add(value);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const recentRequests = new LimitedSet(20);
|
|
681
|
+
|
|
682
|
+
const nativeFetch = window.fetch;
|
|
683
|
+
|
|
684
|
+
function fetchWithTurboHeaders(url, options = {}) {
|
|
637
685
|
const modifiedHeaders = new Headers(options.headers || {});
|
|
638
686
|
const requestUID = uuid();
|
|
639
|
-
|
|
687
|
+
recentRequests.add(requestUID);
|
|
640
688
|
modifiedHeaders.append("X-Turbo-Request-Id", requestUID);
|
|
641
689
|
|
|
642
|
-
return
|
|
690
|
+
return nativeFetch(url, {
|
|
643
691
|
...options,
|
|
644
692
|
headers: modifiedHeaders
|
|
645
693
|
})
|
|
@@ -763,10 +811,17 @@ class FetchRequest {
|
|
|
763
811
|
async perform() {
|
|
764
812
|
const { fetchOptions } = this;
|
|
765
813
|
this.delegate.prepareRequest(this);
|
|
766
|
-
await this.#allowRequestToBeIntercepted(fetchOptions);
|
|
814
|
+
const event = await this.#allowRequestToBeIntercepted(fetchOptions);
|
|
767
815
|
try {
|
|
768
816
|
this.delegate.requestStarted(this);
|
|
769
|
-
|
|
817
|
+
|
|
818
|
+
if (event.detail.fetchRequest) {
|
|
819
|
+
this.response = event.detail.fetchRequest.response;
|
|
820
|
+
} else {
|
|
821
|
+
this.response = fetchWithTurboHeaders(this.url.href, fetchOptions);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const response = await this.response;
|
|
770
825
|
return await this.receive(response)
|
|
771
826
|
} catch (error) {
|
|
772
827
|
if (error.name !== "AbortError") {
|
|
@@ -828,6 +883,8 @@ class FetchRequest {
|
|
|
828
883
|
});
|
|
829
884
|
this.url = event.detail.url;
|
|
830
885
|
if (event.defaultPrevented) await requestInterception;
|
|
886
|
+
|
|
887
|
+
return event
|
|
831
888
|
}
|
|
832
889
|
|
|
833
890
|
#willDelegateErrorHandling(error) {
|
|
@@ -938,6 +995,41 @@ function importStreamElements(fragment) {
|
|
|
938
995
|
return fragment
|
|
939
996
|
}
|
|
940
997
|
|
|
998
|
+
const PREFETCH_DELAY = 100;
|
|
999
|
+
|
|
1000
|
+
class PrefetchCache {
|
|
1001
|
+
#prefetchTimeout = null
|
|
1002
|
+
#prefetched = null
|
|
1003
|
+
|
|
1004
|
+
get(url) {
|
|
1005
|
+
if (this.#prefetched && this.#prefetched.url === url && this.#prefetched.expire > Date.now()) {
|
|
1006
|
+
return this.#prefetched.request
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
setLater(url, request, ttl) {
|
|
1011
|
+
this.clear();
|
|
1012
|
+
|
|
1013
|
+
this.#prefetchTimeout = setTimeout(() => {
|
|
1014
|
+
request.perform();
|
|
1015
|
+
this.set(url, request, ttl);
|
|
1016
|
+
this.#prefetchTimeout = null;
|
|
1017
|
+
}, PREFETCH_DELAY);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
set(url, request, ttl) {
|
|
1021
|
+
this.#prefetched = { url, request, expire: new Date(new Date().getTime() + ttl) };
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
clear() {
|
|
1025
|
+
if (this.#prefetchTimeout) clearTimeout(this.#prefetchTimeout);
|
|
1026
|
+
this.#prefetched = null;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
const cacheTtl = 10 * 1000;
|
|
1031
|
+
const prefetchCache = new PrefetchCache();
|
|
1032
|
+
|
|
941
1033
|
const FormSubmissionState = {
|
|
942
1034
|
initialized: "initialized",
|
|
943
1035
|
requesting: "requesting",
|
|
@@ -1046,6 +1138,7 @@ class FormSubmission {
|
|
|
1046
1138
|
this.state = FormSubmissionState.waiting;
|
|
1047
1139
|
this.submitter?.setAttribute("disabled", "");
|
|
1048
1140
|
this.setSubmitsWith();
|
|
1141
|
+
markAsBusy(this.formElement);
|
|
1049
1142
|
dispatch("turbo:submit-start", {
|
|
1050
1143
|
target: this.formElement,
|
|
1051
1144
|
detail: { formSubmission: this }
|
|
@@ -1054,13 +1147,20 @@ class FormSubmission {
|
|
|
1054
1147
|
}
|
|
1055
1148
|
|
|
1056
1149
|
requestPreventedHandlingResponse(request, response) {
|
|
1150
|
+
prefetchCache.clear();
|
|
1151
|
+
|
|
1057
1152
|
this.result = { success: response.succeeded, fetchResponse: response };
|
|
1058
1153
|
}
|
|
1059
1154
|
|
|
1060
1155
|
requestSucceededWithResponse(request, response) {
|
|
1061
1156
|
if (response.clientError || response.serverError) {
|
|
1062
1157
|
this.delegate.formSubmissionFailedWithResponse(this, response);
|
|
1063
|
-
|
|
1158
|
+
return
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
prefetchCache.clear();
|
|
1162
|
+
|
|
1163
|
+
if (this.requestMustRedirect(request) && responseSucceededWithoutRedirect(response)) {
|
|
1064
1164
|
const error = new Error("Form responses must redirect to another location");
|
|
1065
1165
|
this.delegate.formSubmissionErrored(this, error);
|
|
1066
1166
|
} else {
|
|
@@ -1084,6 +1184,7 @@ class FormSubmission {
|
|
|
1084
1184
|
this.state = FormSubmissionState.stopped;
|
|
1085
1185
|
this.submitter?.removeAttribute("disabled");
|
|
1086
1186
|
this.resetSubmitterText();
|
|
1187
|
+
clearBusyState(this.formElement);
|
|
1087
1188
|
dispatch("turbo:submit-end", {
|
|
1088
1189
|
target: this.formElement,
|
|
1089
1190
|
detail: { formSubmission: this, ...this.result }
|
|
@@ -1377,7 +1478,7 @@ class View {
|
|
|
1377
1478
|
|
|
1378
1479
|
const renderInterception = new Promise((resolve) => (this.#resolveInterceptionPromise = resolve));
|
|
1379
1480
|
const options = { resume: this.#resolveInterceptionPromise, render: this.renderer.renderElement };
|
|
1380
|
-
const immediateRender = this.delegate.allowsImmediateRender(snapshot,
|
|
1481
|
+
const immediateRender = this.delegate.allowsImmediateRender(snapshot, options);
|
|
1381
1482
|
if (!immediateRender) await renderInterception;
|
|
1382
1483
|
|
|
1383
1484
|
await this.renderSnapshot(renderer);
|
|
@@ -1411,6 +1512,14 @@ class View {
|
|
|
1411
1512
|
}
|
|
1412
1513
|
}
|
|
1413
1514
|
|
|
1515
|
+
markVisitDirection(direction) {
|
|
1516
|
+
this.element.setAttribute("data-turbo-visit-direction", direction);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
unmarkVisitDirection() {
|
|
1520
|
+
this.element.removeAttribute("data-turbo-visit-direction");
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1414
1523
|
async renderSnapshot(renderer) {
|
|
1415
1524
|
await renderer.render();
|
|
1416
1525
|
}
|
|
@@ -1507,9 +1616,9 @@ class LinkClickObserver {
|
|
|
1507
1616
|
clickBubbled = (event) => {
|
|
1508
1617
|
if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {
|
|
1509
1618
|
const target = (event.composedPath && event.composedPath()[0]) || event.target;
|
|
1510
|
-
const link =
|
|
1619
|
+
const link = findLinkFromClickTarget(target);
|
|
1511
1620
|
if (link && doesNotTargetIFrame(link)) {
|
|
1512
|
-
const location =
|
|
1621
|
+
const location = getLocationForLink(link);
|
|
1513
1622
|
if (this.delegate.willFollowLinkToLocation(link, location, event)) {
|
|
1514
1623
|
event.preventDefault();
|
|
1515
1624
|
this.delegate.followedLinkToLocation(link, location);
|
|
@@ -1529,26 +1638,6 @@ class LinkClickObserver {
|
|
|
1529
1638
|
event.shiftKey
|
|
1530
1639
|
)
|
|
1531
1640
|
}
|
|
1532
|
-
|
|
1533
|
-
findLinkFromClickTarget(target) {
|
|
1534
|
-
return findClosestRecursively(target, "a[href]:not([target^=_]):not([download])")
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
getLocationForLink(link) {
|
|
1538
|
-
return expandURL(link.getAttribute("href") || "")
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
function doesNotTargetIFrame(anchor) {
|
|
1543
|
-
if (anchor.hasAttribute("target")) {
|
|
1544
|
-
for (const element of document.getElementsByName(anchor.target)) {
|
|
1545
|
-
if (element instanceof HTMLIFrameElement) return false
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
return true
|
|
1549
|
-
} else {
|
|
1550
|
-
return true
|
|
1551
|
-
}
|
|
1552
1641
|
}
|
|
1553
1642
|
|
|
1554
1643
|
class FormLinkClickObserver {
|
|
@@ -1565,6 +1654,16 @@ class FormLinkClickObserver {
|
|
|
1565
1654
|
this.linkInterceptor.stop();
|
|
1566
1655
|
}
|
|
1567
1656
|
|
|
1657
|
+
// Link hover observer delegate
|
|
1658
|
+
|
|
1659
|
+
canPrefetchRequestToLocation(link, location) {
|
|
1660
|
+
return false
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
prefetchAndCacheRequestToLocation(link, location) {
|
|
1664
|
+
return
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1568
1667
|
// Link click observer delegate
|
|
1569
1668
|
|
|
1570
1669
|
willFollowLinkToLocation(link, location, originalEvent) {
|
|
@@ -1780,14 +1879,14 @@ class FrameRenderer extends Renderer {
|
|
|
1780
1879
|
}
|
|
1781
1880
|
|
|
1782
1881
|
async render() {
|
|
1783
|
-
await
|
|
1882
|
+
await nextRepaint();
|
|
1784
1883
|
this.preservingPermanentElements(() => {
|
|
1785
1884
|
this.loadFrameElement();
|
|
1786
1885
|
});
|
|
1787
1886
|
this.scrollFrameIntoView();
|
|
1788
|
-
await
|
|
1887
|
+
await nextRepaint();
|
|
1789
1888
|
this.focusFirstAutofocusableElement();
|
|
1790
|
-
await
|
|
1889
|
+
await nextRepaint();
|
|
1791
1890
|
this.activateScriptElements();
|
|
1792
1891
|
}
|
|
1793
1892
|
|
|
@@ -1838,6 +1937,8 @@ function readScrollBehavior(value, defaultValue) {
|
|
|
1838
1937
|
}
|
|
1839
1938
|
}
|
|
1840
1939
|
|
|
1940
|
+
const ProgressBarID = "turbo-progress-bar";
|
|
1941
|
+
|
|
1841
1942
|
class ProgressBar {
|
|
1842
1943
|
static animationDuration = 300 /*ms*/
|
|
1843
1944
|
|
|
@@ -1942,6 +2043,8 @@ class ProgressBar {
|
|
|
1942
2043
|
|
|
1943
2044
|
createStylesheetElement() {
|
|
1944
2045
|
const element = document.createElement("style");
|
|
2046
|
+
element.id = ProgressBarID;
|
|
2047
|
+
element.setAttribute("data-turbo-permanent", "");
|
|
1945
2048
|
element.type = "text/css";
|
|
1946
2049
|
element.textContent = ProgressBar.defaultCSS;
|
|
1947
2050
|
if (this.cspNonce) {
|
|
@@ -2213,6 +2316,12 @@ const SystemStatusCode = {
|
|
|
2213
2316
|
contentTypeMismatch: -2
|
|
2214
2317
|
};
|
|
2215
2318
|
|
|
2319
|
+
const Direction = {
|
|
2320
|
+
advance: "forward",
|
|
2321
|
+
restore: "back",
|
|
2322
|
+
replace: "none"
|
|
2323
|
+
};
|
|
2324
|
+
|
|
2216
2325
|
class Visit {
|
|
2217
2326
|
identifier = uuid() // Required by turbo-ios
|
|
2218
2327
|
timingMetrics = {}
|
|
@@ -2242,7 +2351,8 @@ class Visit {
|
|
|
2242
2351
|
willRender,
|
|
2243
2352
|
updateHistory,
|
|
2244
2353
|
shouldCacheSnapshot,
|
|
2245
|
-
acceptsStreamResponse
|
|
2354
|
+
acceptsStreamResponse,
|
|
2355
|
+
direction
|
|
2246
2356
|
} = {
|
|
2247
2357
|
...defaultOptions,
|
|
2248
2358
|
...options
|
|
@@ -2260,6 +2370,7 @@ class Visit {
|
|
|
2260
2370
|
this.scrolled = !willRender;
|
|
2261
2371
|
this.shouldCacheSnapshot = shouldCacheSnapshot;
|
|
2262
2372
|
this.acceptsStreamResponse = acceptsStreamResponse;
|
|
2373
|
+
this.direction = direction || Direction[action];
|
|
2263
2374
|
}
|
|
2264
2375
|
|
|
2265
2376
|
get adapter() {
|
|
@@ -2512,7 +2623,7 @@ class Visit {
|
|
|
2512
2623
|
// Scrolling
|
|
2513
2624
|
|
|
2514
2625
|
performScroll() {
|
|
2515
|
-
if (!this.scrolled && !this.view.forceReloaded && !this.view.
|
|
2626
|
+
if (!this.scrolled && !this.view.forceReloaded && !this.view.shouldPreserveScrollPosition(this)) {
|
|
2516
2627
|
if (this.action == "restore") {
|
|
2517
2628
|
this.scrollToRestoredPosition() || this.scrollToAnchor() || this.view.scrollToTop();
|
|
2518
2629
|
} else {
|
|
@@ -2587,9 +2698,7 @@ class Visit {
|
|
|
2587
2698
|
|
|
2588
2699
|
async render(callback) {
|
|
2589
2700
|
this.cancelRender();
|
|
2590
|
-
await
|
|
2591
|
-
this.frame = requestAnimationFrame(() => resolve());
|
|
2592
|
-
});
|
|
2701
|
+
this.frame = await nextRepaint();
|
|
2593
2702
|
await callback();
|
|
2594
2703
|
delete this.frame;
|
|
2595
2704
|
}
|
|
@@ -2867,6 +2976,7 @@ class History {
|
|
|
2867
2976
|
restorationData = {}
|
|
2868
2977
|
started = false
|
|
2869
2978
|
pageLoaded = false
|
|
2979
|
+
currentIndex = 0
|
|
2870
2980
|
|
|
2871
2981
|
constructor(delegate) {
|
|
2872
2982
|
this.delegate = delegate;
|
|
@@ -2876,6 +2986,7 @@ class History {
|
|
|
2876
2986
|
if (!this.started) {
|
|
2877
2987
|
addEventListener("popstate", this.onPopState, false);
|
|
2878
2988
|
addEventListener("load", this.onPageLoad, false);
|
|
2989
|
+
this.currentIndex = history.state?.turbo?.restorationIndex || 0;
|
|
2879
2990
|
this.started = true;
|
|
2880
2991
|
this.replace(new URL(window.location.href));
|
|
2881
2992
|
}
|
|
@@ -2898,7 +3009,9 @@ class History {
|
|
|
2898
3009
|
}
|
|
2899
3010
|
|
|
2900
3011
|
update(method, location, restorationIdentifier = uuid()) {
|
|
2901
|
-
|
|
3012
|
+
if (method === history.pushState) ++this.currentIndex;
|
|
3013
|
+
|
|
3014
|
+
const state = { turbo: { restorationIdentifier, restorationIndex: this.currentIndex } };
|
|
2902
3015
|
method.call(history, state, "", location.href);
|
|
2903
3016
|
this.location = location;
|
|
2904
3017
|
this.restorationIdentifier = restorationIdentifier;
|
|
@@ -2942,9 +3055,11 @@ class History {
|
|
|
2942
3055
|
const { turbo } = event.state || {};
|
|
2943
3056
|
if (turbo) {
|
|
2944
3057
|
this.location = new URL(window.location.href);
|
|
2945
|
-
const { restorationIdentifier } = turbo;
|
|
3058
|
+
const { restorationIdentifier, restorationIndex } = turbo;
|
|
2946
3059
|
this.restorationIdentifier = restorationIdentifier;
|
|
2947
|
-
this.
|
|
3060
|
+
const direction = restorationIndex > this.currentIndex ? "forward" : "back";
|
|
3061
|
+
this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(this.location, restorationIdentifier, direction);
|
|
3062
|
+
this.currentIndex = restorationIndex;
|
|
2948
3063
|
}
|
|
2949
3064
|
}
|
|
2950
3065
|
}
|
|
@@ -2966,6 +3081,176 @@ class History {
|
|
|
2966
3081
|
}
|
|
2967
3082
|
}
|
|
2968
3083
|
|
|
3084
|
+
class LinkPrefetchObserver {
|
|
3085
|
+
started = false
|
|
3086
|
+
hoverTriggerEvent = "mouseenter"
|
|
3087
|
+
touchTriggerEvent = "touchstart"
|
|
3088
|
+
|
|
3089
|
+
constructor(delegate, eventTarget) {
|
|
3090
|
+
this.delegate = delegate;
|
|
3091
|
+
this.eventTarget = eventTarget;
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
start() {
|
|
3095
|
+
if (this.started) return
|
|
3096
|
+
|
|
3097
|
+
if (this.eventTarget.readyState === "loading") {
|
|
3098
|
+
this.eventTarget.addEventListener("DOMContentLoaded", this.#enable, { once: true });
|
|
3099
|
+
} else {
|
|
3100
|
+
this.#enable();
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3104
|
+
stop() {
|
|
3105
|
+
if (!this.started) return
|
|
3106
|
+
|
|
3107
|
+
this.eventTarget.removeEventListener(this.hoverTriggerEvent, this.#tryToPrefetchRequest, {
|
|
3108
|
+
capture: true,
|
|
3109
|
+
passive: true
|
|
3110
|
+
});
|
|
3111
|
+
this.eventTarget.removeEventListener(this.touchTriggerEvent, this.#tryToPrefetchRequest, {
|
|
3112
|
+
capture: true,
|
|
3113
|
+
passive: true
|
|
3114
|
+
});
|
|
3115
|
+
this.eventTarget.removeEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true);
|
|
3116
|
+
this.started = false;
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
#enable = () => {
|
|
3120
|
+
this.eventTarget.addEventListener(this.hoverTriggerEvent, this.#tryToPrefetchRequest, {
|
|
3121
|
+
capture: true,
|
|
3122
|
+
passive: true
|
|
3123
|
+
});
|
|
3124
|
+
this.eventTarget.addEventListener(this.touchTriggerEvent, this.#tryToPrefetchRequest, {
|
|
3125
|
+
capture: true,
|
|
3126
|
+
passive: true
|
|
3127
|
+
});
|
|
3128
|
+
this.eventTarget.addEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true);
|
|
3129
|
+
this.started = true;
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
#tryToPrefetchRequest = (event) => {
|
|
3133
|
+
if (getMetaContent("turbo-prefetch") !== "true") return
|
|
3134
|
+
|
|
3135
|
+
const target = event.target;
|
|
3136
|
+
const isLink = target.matches && target.matches("a[href]:not([target^=_]):not([download])");
|
|
3137
|
+
|
|
3138
|
+
if (isLink && this.#isPrefetchable(target)) {
|
|
3139
|
+
const link = target;
|
|
3140
|
+
const location = getLocationForLink(link);
|
|
3141
|
+
|
|
3142
|
+
if (this.delegate.canPrefetchRequestToLocation(link, location)) {
|
|
3143
|
+
const fetchRequest = new FetchRequest(
|
|
3144
|
+
this,
|
|
3145
|
+
FetchMethod.get,
|
|
3146
|
+
location,
|
|
3147
|
+
new URLSearchParams(),
|
|
3148
|
+
target
|
|
3149
|
+
);
|
|
3150
|
+
|
|
3151
|
+
prefetchCache.setLater(location.toString(), fetchRequest, this.#cacheTtl);
|
|
3152
|
+
|
|
3153
|
+
link.addEventListener("mouseleave", () => prefetchCache.clear(), { once: true });
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
|
|
3158
|
+
#tryToUsePrefetchedRequest = (event) => {
|
|
3159
|
+
if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "get") {
|
|
3160
|
+
const cached = prefetchCache.get(event.detail.url.toString());
|
|
3161
|
+
|
|
3162
|
+
if (cached) {
|
|
3163
|
+
// User clicked link, use cache response
|
|
3164
|
+
event.detail.fetchRequest = cached;
|
|
3165
|
+
}
|
|
3166
|
+
|
|
3167
|
+
prefetchCache.clear();
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
|
|
3171
|
+
prepareRequest(request) {
|
|
3172
|
+
const link = request.target;
|
|
3173
|
+
|
|
3174
|
+
request.headers["Sec-Purpose"] = "prefetch";
|
|
3175
|
+
|
|
3176
|
+
if (link.dataset.turboFrame && link.dataset.turboFrame !== "_top") {
|
|
3177
|
+
request.headers["Turbo-Frame"] = link.dataset.turboFrame;
|
|
3178
|
+
} else if (link.dataset.turboFrame !== "_top") {
|
|
3179
|
+
const turboFrame = link.closest("turbo-frame");
|
|
3180
|
+
|
|
3181
|
+
if (turboFrame) {
|
|
3182
|
+
request.headers["Turbo-Frame"] = turboFrame.id;
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
if (link.hasAttribute("data-turbo-stream")) {
|
|
3187
|
+
request.acceptResponseType("text/vnd.turbo-stream.html");
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
// Fetch request interface
|
|
3192
|
+
|
|
3193
|
+
requestSucceededWithResponse() {}
|
|
3194
|
+
|
|
3195
|
+
requestStarted(fetchRequest) {}
|
|
3196
|
+
|
|
3197
|
+
requestErrored(fetchRequest) {}
|
|
3198
|
+
|
|
3199
|
+
requestFinished(fetchRequest) {}
|
|
3200
|
+
|
|
3201
|
+
requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}
|
|
3202
|
+
|
|
3203
|
+
requestFailedWithResponse(fetchRequest, fetchResponse) {}
|
|
3204
|
+
|
|
3205
|
+
get #cacheTtl() {
|
|
3206
|
+
return Number(getMetaContent("turbo-prefetch-cache-time")) || cacheTtl
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
#isPrefetchable(link) {
|
|
3210
|
+
const href = link.getAttribute("href");
|
|
3211
|
+
|
|
3212
|
+
if (!href || href === "#" || link.dataset.turbo === "false" || link.dataset.turboPrefetch === "false") {
|
|
3213
|
+
return false
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3216
|
+
if (link.origin !== document.location.origin) {
|
|
3217
|
+
return false
|
|
3218
|
+
}
|
|
3219
|
+
|
|
3220
|
+
if (!["http:", "https:"].includes(link.protocol)) {
|
|
3221
|
+
return false
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
if (link.pathname + link.search === document.location.pathname + document.location.search) {
|
|
3225
|
+
return false
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
if (link.dataset.turboMethod && link.dataset.turboMethod !== "get") {
|
|
3229
|
+
return false
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
if (targetsIframe(link)) {
|
|
3233
|
+
return false
|
|
3234
|
+
}
|
|
3235
|
+
|
|
3236
|
+
if (link.pathname + link.search === document.location.pathname + document.location.search) {
|
|
3237
|
+
return false
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
const turboPrefetchParent = findClosestRecursively(link, "[data-turbo-prefetch]");
|
|
3241
|
+
|
|
3242
|
+
if (turboPrefetchParent && turboPrefetchParent.dataset.turboPrefetch === "false") {
|
|
3243
|
+
return false
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
return true
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
const targetsIframe = (link) => {
|
|
3251
|
+
return !doesNotTargetIFrame(link)
|
|
3252
|
+
};
|
|
3253
|
+
|
|
2969
3254
|
class Navigator {
|
|
2970
3255
|
constructor(delegate) {
|
|
2971
3256
|
this.delegate = delegate;
|
|
@@ -3281,7 +3566,7 @@ async function withAutofocusFromFragment(fragment, callback) {
|
|
|
3281
3566
|
}
|
|
3282
3567
|
|
|
3283
3568
|
callback();
|
|
3284
|
-
await
|
|
3569
|
+
await nextRepaint();
|
|
3285
3570
|
|
|
3286
3571
|
const hasNoActiveElement = document.activeElement == null || document.activeElement == document.body;
|
|
3287
3572
|
|
|
@@ -3436,722 +3721,838 @@ class ErrorRenderer extends Renderer {
|
|
|
3436
3721
|
}
|
|
3437
3722
|
}
|
|
3438
3723
|
|
|
3439
|
-
|
|
3724
|
+
// base IIFE to define idiomorph
|
|
3725
|
+
var Idiomorph = (function () {
|
|
3440
3726
|
|
|
3441
|
-
//=============================================================================
|
|
3442
|
-
//
|
|
3443
|
-
//=============================================================================
|
|
3444
|
-
|
|
3727
|
+
//=============================================================================
|
|
3728
|
+
// AND NOW IT BEGINS...
|
|
3729
|
+
//=============================================================================
|
|
3730
|
+
let EMPTY_SET = new Set();
|
|
3445
3731
|
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
return morphNormalizedContent(oldNode, normalizedContent, ctx);
|
|
3459
|
-
}
|
|
3732
|
+
// default configuration values, updatable by users now
|
|
3733
|
+
let defaults = {
|
|
3734
|
+
morphStyle: "outerHTML",
|
|
3735
|
+
callbacks : {
|
|
3736
|
+
beforeNodeAdded: noOp,
|
|
3737
|
+
afterNodeAdded: noOp,
|
|
3738
|
+
beforeNodeMorphed: noOp,
|
|
3739
|
+
afterNodeMorphed: noOp,
|
|
3740
|
+
beforeNodeRemoved: noOp,
|
|
3741
|
+
afterNodeRemoved: noOp,
|
|
3742
|
+
beforeAttributeUpdated: noOp,
|
|
3460
3743
|
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
}));
|
|
3475
|
-
});
|
|
3476
|
-
return;
|
|
3477
|
-
}
|
|
3478
|
-
}
|
|
3744
|
+
},
|
|
3745
|
+
head: {
|
|
3746
|
+
style: 'merge',
|
|
3747
|
+
shouldPreserve: function (elt) {
|
|
3748
|
+
return elt.getAttribute("im-preserve") === "true";
|
|
3749
|
+
},
|
|
3750
|
+
shouldReAppend: function (elt) {
|
|
3751
|
+
return elt.getAttribute("im-re-append") === "true";
|
|
3752
|
+
},
|
|
3753
|
+
shouldRemove: noOp,
|
|
3754
|
+
afterHeadMorphed: noOp,
|
|
3755
|
+
}
|
|
3756
|
+
};
|
|
3479
3757
|
|
|
3480
|
-
|
|
3758
|
+
//=============================================================================
|
|
3759
|
+
// Core Morphing Algorithm - morph, morphNormalizedContent, morphOldNodeTo, morphChildren
|
|
3760
|
+
//=============================================================================
|
|
3761
|
+
function morph(oldNode, newContent, config = {}) {
|
|
3481
3762
|
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3763
|
+
if (oldNode instanceof Document) {
|
|
3764
|
+
oldNode = oldNode.documentElement;
|
|
3765
|
+
}
|
|
3485
3766
|
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx);
|
|
3767
|
+
if (typeof newContent === 'string') {
|
|
3768
|
+
newContent = parseContent(newContent);
|
|
3769
|
+
}
|
|
3490
3770
|
|
|
3491
|
-
|
|
3492
|
-
let previousSibling = bestMatch?.previousSibling;
|
|
3493
|
-
let nextSibling = bestMatch?.nextSibling;
|
|
3771
|
+
let normalizedContent = normalizeContent(newContent);
|
|
3494
3772
|
|
|
3495
|
-
|
|
3496
|
-
let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx);
|
|
3773
|
+
let ctx = createMorphContext(oldNode, normalizedContent, config);
|
|
3497
3774
|
|
|
3498
|
-
|
|
3499
|
-
// if there was a best match, merge the siblings in too and return the
|
|
3500
|
-
// whole bunch
|
|
3501
|
-
return insertSiblings(previousSibling, morphedNode, nextSibling);
|
|
3502
|
-
} else {
|
|
3503
|
-
// otherwise nothing was added to the DOM
|
|
3504
|
-
return []
|
|
3775
|
+
return morphNormalizedContent(oldNode, normalizedContent, ctx);
|
|
3505
3776
|
}
|
|
3506
|
-
} else {
|
|
3507
|
-
throw "Do not understand how to morph style " + ctx.morphStyle;
|
|
3508
|
-
}
|
|
3509
|
-
}
|
|
3510
|
-
|
|
3511
3777
|
|
|
3778
|
+
function morphNormalizedContent(oldNode, normalizedNewContent, ctx) {
|
|
3779
|
+
if (ctx.head.block) {
|
|
3780
|
+
let oldHead = oldNode.querySelector('head');
|
|
3781
|
+
let newHead = normalizedNewContent.querySelector('head');
|
|
3782
|
+
if (oldHead && newHead) {
|
|
3783
|
+
let promises = handleHeadElement(newHead, oldHead, ctx);
|
|
3784
|
+
// when head promises resolve, call morph again, ignoring the head tag
|
|
3785
|
+
Promise.all(promises).then(function () {
|
|
3786
|
+
morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, {
|
|
3787
|
+
head: {
|
|
3788
|
+
block: false,
|
|
3789
|
+
ignore: true
|
|
3790
|
+
}
|
|
3791
|
+
}));
|
|
3792
|
+
});
|
|
3793
|
+
return;
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3512
3796
|
|
|
3513
|
-
|
|
3514
|
-
* @param oldNode root node to merge content into
|
|
3515
|
-
* @param newContent new content to merge
|
|
3516
|
-
* @param ctx the merge context
|
|
3517
|
-
* @returns {Element} the element that ended up in the DOM
|
|
3518
|
-
*/
|
|
3519
|
-
function morphOldNodeTo(oldNode, newContent, ctx) {
|
|
3520
|
-
if (ctx.ignoreActive && oldNode === document.activeElement) ; else if (newContent == null) {
|
|
3521
|
-
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return;
|
|
3522
|
-
|
|
3523
|
-
oldNode.remove();
|
|
3524
|
-
ctx.callbacks.afterNodeRemoved(oldNode);
|
|
3525
|
-
return null;
|
|
3526
|
-
} else if (!isSoftMatch(oldNode, newContent)) {
|
|
3527
|
-
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return;
|
|
3528
|
-
if (ctx.callbacks.beforeNodeAdded(newContent) === false) return;
|
|
3529
|
-
|
|
3530
|
-
oldNode.parentElement.replaceChild(newContent, oldNode);
|
|
3531
|
-
ctx.callbacks.afterNodeAdded(newContent);
|
|
3532
|
-
ctx.callbacks.afterNodeRemoved(oldNode);
|
|
3533
|
-
return newContent;
|
|
3534
|
-
} else {
|
|
3535
|
-
if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return;
|
|
3797
|
+
if (ctx.morphStyle === "innerHTML") {
|
|
3536
3798
|
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
syncNodeFrom(newContent, oldNode);
|
|
3541
|
-
morphChildren(newContent, oldNode, ctx);
|
|
3542
|
-
}
|
|
3543
|
-
ctx.callbacks.afterNodeMorphed(oldNode, newContent);
|
|
3544
|
-
return oldNode;
|
|
3545
|
-
}
|
|
3546
|
-
}
|
|
3799
|
+
// innerHTML, so we are only updating the children
|
|
3800
|
+
morphChildren(normalizedNewContent, oldNode, ctx);
|
|
3801
|
+
return oldNode.children;
|
|
3547
3802
|
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
*
|
|
3553
|
-
* Basic algorithm is, for each node in the new content:
|
|
3554
|
-
*
|
|
3555
|
-
* - if we have reached the end of the old parent, append the new content
|
|
3556
|
-
* - if the new content has an id set match with the current insertion point, morph
|
|
3557
|
-
* - search for an id set match
|
|
3558
|
-
* - if id set match found, morph
|
|
3559
|
-
* - otherwise search for a "soft" match
|
|
3560
|
-
* - if a soft match is found, morph
|
|
3561
|
-
* - otherwise, prepend the new node before the current insertion point
|
|
3562
|
-
*
|
|
3563
|
-
* The two search algorithms terminate if competing node matches appear to outweigh what can be achieved
|
|
3564
|
-
* with the current node. See findIdSetMatch() and findSoftMatch() for details.
|
|
3565
|
-
*
|
|
3566
|
-
* @param {Element} newParent the parent element of the new content
|
|
3567
|
-
* @param {Element } oldParent the old content that we are merging the new content into
|
|
3568
|
-
* @param ctx the merge context
|
|
3569
|
-
*/
|
|
3570
|
-
function morphChildren(newParent, oldParent, ctx) {
|
|
3803
|
+
} else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) {
|
|
3804
|
+
// otherwise find the best element match in the new content, morph that, and merge its siblings
|
|
3805
|
+
// into either side of the best match
|
|
3806
|
+
let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx);
|
|
3571
3807
|
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3808
|
+
// stash the siblings that will need to be inserted on either side of the best match
|
|
3809
|
+
let previousSibling = bestMatch?.previousSibling;
|
|
3810
|
+
let nextSibling = bestMatch?.nextSibling;
|
|
3575
3811
|
|
|
3576
|
-
|
|
3577
|
-
|
|
3812
|
+
// morph it
|
|
3813
|
+
let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx);
|
|
3578
3814
|
|
|
3579
|
-
|
|
3580
|
-
|
|
3815
|
+
if (bestMatch) {
|
|
3816
|
+
// if there was a best match, merge the siblings in too and return the
|
|
3817
|
+
// whole bunch
|
|
3818
|
+
return insertSiblings(previousSibling, morphedNode, nextSibling);
|
|
3819
|
+
} else {
|
|
3820
|
+
// otherwise nothing was added to the DOM
|
|
3821
|
+
return []
|
|
3822
|
+
}
|
|
3823
|
+
} else {
|
|
3824
|
+
throw "Do not understand how to morph style " + ctx.morphStyle;
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3581
3827
|
|
|
3582
|
-
// if we are at the end of the exiting parent's children, just append
|
|
3583
|
-
if (insertionPoint == null) {
|
|
3584
|
-
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
|
3585
3828
|
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3829
|
+
/**
|
|
3830
|
+
* @param possibleActiveElement
|
|
3831
|
+
* @param ctx
|
|
3832
|
+
* @returns {boolean}
|
|
3833
|
+
*/
|
|
3834
|
+
function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
|
|
3835
|
+
return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement;
|
|
3590
3836
|
}
|
|
3591
3837
|
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3838
|
+
/**
|
|
3839
|
+
* @param oldNode root node to merge content into
|
|
3840
|
+
* @param newContent new content to merge
|
|
3841
|
+
* @param ctx the merge context
|
|
3842
|
+
* @returns {Element} the element that ended up in the DOM
|
|
3843
|
+
*/
|
|
3844
|
+
function morphOldNodeTo(oldNode, newContent, ctx) {
|
|
3845
|
+
if (ctx.ignoreActive && oldNode === document.activeElement) ; else if (newContent == null) {
|
|
3846
|
+
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
|
|
3847
|
+
|
|
3848
|
+
oldNode.remove();
|
|
3849
|
+
ctx.callbacks.afterNodeRemoved(oldNode);
|
|
3850
|
+
return null;
|
|
3851
|
+
} else if (!isSoftMatch(oldNode, newContent)) {
|
|
3852
|
+
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
|
|
3853
|
+
if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode;
|
|
3854
|
+
|
|
3855
|
+
oldNode.parentElement.replaceChild(newContent, oldNode);
|
|
3856
|
+
ctx.callbacks.afterNodeAdded(newContent);
|
|
3857
|
+
ctx.callbacks.afterNodeRemoved(oldNode);
|
|
3858
|
+
return newContent;
|
|
3859
|
+
} else {
|
|
3860
|
+
if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return oldNode;
|
|
3861
|
+
|
|
3862
|
+
if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) ; else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") {
|
|
3863
|
+
handleHeadElement(newContent, oldNode, ctx);
|
|
3864
|
+
} else {
|
|
3865
|
+
syncNodeFrom(newContent, oldNode, ctx);
|
|
3866
|
+
if (!ignoreValueOfActiveElement(oldNode, ctx)) {
|
|
3867
|
+
morphChildren(newContent, oldNode, ctx);
|
|
3868
|
+
}
|
|
3869
|
+
}
|
|
3870
|
+
ctx.callbacks.afterNodeMorphed(oldNode, newContent);
|
|
3871
|
+
return oldNode;
|
|
3872
|
+
}
|
|
3598
3873
|
}
|
|
3599
3874
|
|
|
3600
|
-
|
|
3601
|
-
|
|
3875
|
+
/**
|
|
3876
|
+
* This is the core algorithm for matching up children. The idea is to use id sets to try to match up
|
|
3877
|
+
* nodes as faithfully as possible. We greedily match, which allows us to keep the algorithm fast, but
|
|
3878
|
+
* by using id sets, we are able to better match up with content deeper in the DOM.
|
|
3879
|
+
*
|
|
3880
|
+
* Basic algorithm is, for each node in the new content:
|
|
3881
|
+
*
|
|
3882
|
+
* - if we have reached the end of the old parent, append the new content
|
|
3883
|
+
* - if the new content has an id set match with the current insertion point, morph
|
|
3884
|
+
* - search for an id set match
|
|
3885
|
+
* - if id set match found, morph
|
|
3886
|
+
* - otherwise search for a "soft" match
|
|
3887
|
+
* - if a soft match is found, morph
|
|
3888
|
+
* - otherwise, prepend the new node before the current insertion point
|
|
3889
|
+
*
|
|
3890
|
+
* The two search algorithms terminate if competing node matches appear to outweigh what can be achieved
|
|
3891
|
+
* with the current node. See findIdSetMatch() and findSoftMatch() for details.
|
|
3892
|
+
*
|
|
3893
|
+
* @param {Element} newParent the parent element of the new content
|
|
3894
|
+
* @param {Element } oldParent the old content that we are merging the new content into
|
|
3895
|
+
* @param ctx the merge context
|
|
3896
|
+
*/
|
|
3897
|
+
function morphChildren(newParent, oldParent, ctx) {
|
|
3898
|
+
|
|
3899
|
+
let nextNewChild = newParent.firstChild;
|
|
3900
|
+
let insertionPoint = oldParent.firstChild;
|
|
3901
|
+
let newChild;
|
|
3902
|
+
|
|
3903
|
+
// run through all the new content
|
|
3904
|
+
while (nextNewChild) {
|
|
3905
|
+
|
|
3906
|
+
newChild = nextNewChild;
|
|
3907
|
+
nextNewChild = newChild.nextSibling;
|
|
3908
|
+
|
|
3909
|
+
// if we are at the end of the exiting parent's children, just append
|
|
3910
|
+
if (insertionPoint == null) {
|
|
3911
|
+
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
|
3912
|
+
|
|
3913
|
+
oldParent.appendChild(newChild);
|
|
3914
|
+
ctx.callbacks.afterNodeAdded(newChild);
|
|
3915
|
+
removeIdsFromConsideration(ctx, newChild);
|
|
3916
|
+
continue;
|
|
3917
|
+
}
|
|
3602
3918
|
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3919
|
+
// if the current node has an id set match then morph
|
|
3920
|
+
if (isIdSetMatch(newChild, insertionPoint, ctx)) {
|
|
3921
|
+
morphOldNodeTo(insertionPoint, newChild, ctx);
|
|
3922
|
+
insertionPoint = insertionPoint.nextSibling;
|
|
3923
|
+
removeIdsFromConsideration(ctx, newChild);
|
|
3924
|
+
continue;
|
|
3925
|
+
}
|
|
3610
3926
|
|
|
3611
|
-
|
|
3612
|
-
|
|
3927
|
+
// otherwise search forward in the existing old children for an id set match
|
|
3928
|
+
let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx);
|
|
3613
3929
|
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3930
|
+
// if we found a potential match, remove the nodes until that point and morph
|
|
3931
|
+
if (idSetMatch) {
|
|
3932
|
+
insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx);
|
|
3933
|
+
morphOldNodeTo(idSetMatch, newChild, ctx);
|
|
3934
|
+
removeIdsFromConsideration(ctx, newChild);
|
|
3935
|
+
continue;
|
|
3936
|
+
}
|
|
3621
3937
|
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
|
3938
|
+
// no id set match found, so scan forward for a soft match for the current node
|
|
3939
|
+
let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx);
|
|
3625
3940
|
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3941
|
+
// if we found a soft match for the current node, morph
|
|
3942
|
+
if (softMatch) {
|
|
3943
|
+
insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx);
|
|
3944
|
+
morphOldNodeTo(softMatch, newChild, ctx);
|
|
3945
|
+
removeIdsFromConsideration(ctx, newChild);
|
|
3946
|
+
continue;
|
|
3947
|
+
}
|
|
3630
3948
|
|
|
3631
|
-
|
|
3632
|
-
|
|
3949
|
+
// abandon all hope of morphing, just insert the new child before the insertion point
|
|
3950
|
+
// and move on
|
|
3951
|
+
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
|
3633
3952
|
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
}
|
|
3953
|
+
oldParent.insertBefore(newChild, insertionPoint);
|
|
3954
|
+
ctx.callbacks.afterNodeAdded(newChild);
|
|
3955
|
+
removeIdsFromConsideration(ctx, newChild);
|
|
3956
|
+
}
|
|
3639
3957
|
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
//=============================================================================
|
|
3958
|
+
// remove any remaining old nodes that didn't match up with new content
|
|
3959
|
+
while (insertionPoint !== null) {
|
|
3643
3960
|
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
*
|
|
3648
|
-
* @param {Element} from the element to copy attributes & state from
|
|
3649
|
-
* @param {Element} to the element to copy attributes & state to
|
|
3650
|
-
*/
|
|
3651
|
-
function syncNodeFrom(from, to) {
|
|
3652
|
-
let type = from.nodeType;
|
|
3653
|
-
|
|
3654
|
-
// if is an element type, sync the attributes from the
|
|
3655
|
-
// new node into the new node
|
|
3656
|
-
if (type === 1 /* element type */) {
|
|
3657
|
-
const fromAttributes = from.attributes;
|
|
3658
|
-
const toAttributes = to.attributes;
|
|
3659
|
-
for (const fromAttribute of fromAttributes) {
|
|
3660
|
-
if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) {
|
|
3661
|
-
to.setAttribute(fromAttribute.name, fromAttribute.value);
|
|
3961
|
+
let tempNode = insertionPoint;
|
|
3962
|
+
insertionPoint = insertionPoint.nextSibling;
|
|
3963
|
+
removeNode(tempNode, ctx);
|
|
3662
3964
|
}
|
|
3663
3965
|
}
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3966
|
+
|
|
3967
|
+
//=============================================================================
|
|
3968
|
+
// Attribute Syncing Code
|
|
3969
|
+
//=============================================================================
|
|
3970
|
+
|
|
3971
|
+
/**
|
|
3972
|
+
* @param attr {String} the attribute to be mutated
|
|
3973
|
+
* @param to {Element} the element that is going to be updated
|
|
3974
|
+
* @param updateType {("update"|"remove")}
|
|
3975
|
+
* @param ctx the merge context
|
|
3976
|
+
* @returns {boolean} true if the attribute should be ignored, false otherwise
|
|
3977
|
+
*/
|
|
3978
|
+
function ignoreAttribute(attr, to, updateType, ctx) {
|
|
3979
|
+
if(attr === 'value' && ctx.ignoreActiveValue && to === document.activeElement){
|
|
3980
|
+
return true;
|
|
3667
3981
|
}
|
|
3982
|
+
return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false;
|
|
3668
3983
|
}
|
|
3669
|
-
}
|
|
3670
3984
|
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3985
|
+
/**
|
|
3986
|
+
* syncs a given node with another node, copying over all attributes and
|
|
3987
|
+
* inner element state from the 'from' node to the 'to' node
|
|
3988
|
+
*
|
|
3989
|
+
* @param {Element} from the element to copy attributes & state from
|
|
3990
|
+
* @param {Element} to the element to copy attributes & state to
|
|
3991
|
+
* @param ctx the merge context
|
|
3992
|
+
*/
|
|
3993
|
+
function syncNodeFrom(from, to, ctx) {
|
|
3994
|
+
let type = from.nodeType;
|
|
3995
|
+
|
|
3996
|
+
// if is an element type, sync the attributes from the
|
|
3997
|
+
// new node into the new node
|
|
3998
|
+
if (type === 1 /* element type */) {
|
|
3999
|
+
const fromAttributes = from.attributes;
|
|
4000
|
+
const toAttributes = to.attributes;
|
|
4001
|
+
for (const fromAttribute of fromAttributes) {
|
|
4002
|
+
if (ignoreAttribute(fromAttribute.name, to, 'update', ctx)) {
|
|
4003
|
+
continue;
|
|
4004
|
+
}
|
|
4005
|
+
if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) {
|
|
4006
|
+
to.setAttribute(fromAttribute.name, fromAttribute.value);
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
// iterate backwards to avoid skipping over items when a delete occurs
|
|
4010
|
+
for (let i = toAttributes.length - 1; 0 <= i; i--) {
|
|
4011
|
+
const toAttribute = toAttributes[i];
|
|
4012
|
+
if (ignoreAttribute(toAttribute.name, to, 'remove', ctx)) {
|
|
4013
|
+
continue;
|
|
4014
|
+
}
|
|
4015
|
+
if (!from.hasAttribute(toAttribute.name)) {
|
|
4016
|
+
to.removeAttribute(toAttribute.name);
|
|
4017
|
+
}
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
3677
4020
|
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
if (from instanceof HTMLInputElement &&
|
|
3685
|
-
to instanceof HTMLInputElement &&
|
|
3686
|
-
from.type !== 'file') {
|
|
3687
|
-
|
|
3688
|
-
to.value = from.value || '';
|
|
3689
|
-
syncAttribute(from, to, 'value');
|
|
3690
|
-
|
|
3691
|
-
// sync boolean attributes
|
|
3692
|
-
syncAttribute(from, to, 'checked');
|
|
3693
|
-
syncAttribute(from, to, 'disabled');
|
|
3694
|
-
} else if (from instanceof HTMLOptionElement) {
|
|
3695
|
-
syncAttribute(from, to, 'selected');
|
|
3696
|
-
} else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) {
|
|
3697
|
-
let fromValue = from.value;
|
|
3698
|
-
let toValue = to.value;
|
|
3699
|
-
if (fromValue !== toValue) {
|
|
3700
|
-
to.value = fromValue;
|
|
3701
|
-
}
|
|
3702
|
-
if (to.firstChild && to.firstChild.nodeValue !== fromValue) {
|
|
3703
|
-
to.firstChild.nodeValue = fromValue;
|
|
3704
|
-
}
|
|
3705
|
-
}
|
|
3706
|
-
}
|
|
4021
|
+
// sync text nodes
|
|
4022
|
+
if (type === 8 /* comment */ || type === 3 /* text */) {
|
|
4023
|
+
if (to.nodeValue !== from.nodeValue) {
|
|
4024
|
+
to.nodeValue = from.nodeValue;
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
3707
4027
|
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
} else {
|
|
3713
|
-
to.removeAttribute(attributeName);
|
|
4028
|
+
if (!ignoreValueOfActiveElement(to, ctx)) {
|
|
4029
|
+
// sync input values
|
|
4030
|
+
syncInputValue(from, to, ctx);
|
|
4031
|
+
}
|
|
3714
4032
|
}
|
|
3715
|
-
}
|
|
3716
|
-
}
|
|
3717
4033
|
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
4034
|
+
/**
|
|
4035
|
+
* @param from {Element} element to sync the value from
|
|
4036
|
+
* @param to {Element} element to sync the value to
|
|
4037
|
+
* @param attributeName {String} the attribute name
|
|
4038
|
+
* @param ctx the merge context
|
|
4039
|
+
*/
|
|
4040
|
+
function syncBooleanAttribute(from, to, attributeName, ctx) {
|
|
4041
|
+
if (from[attributeName] !== to[attributeName]) {
|
|
4042
|
+
let ignoreUpdate = ignoreAttribute(attributeName, to, 'update', ctx);
|
|
4043
|
+
if (!ignoreUpdate) {
|
|
4044
|
+
to[attributeName] = from[attributeName];
|
|
4045
|
+
}
|
|
4046
|
+
if (from[attributeName]) {
|
|
4047
|
+
if (!ignoreUpdate) {
|
|
4048
|
+
to.setAttribute(attributeName, from[attributeName]);
|
|
4049
|
+
}
|
|
4050
|
+
} else {
|
|
4051
|
+
if (!ignoreAttribute(attributeName, to, 'remove', ctx)) {
|
|
4052
|
+
to.removeAttribute(attributeName);
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
}
|
|
3722
4057
|
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
4058
|
+
/**
|
|
4059
|
+
* NB: many bothans died to bring us information:
|
|
4060
|
+
*
|
|
4061
|
+
* https://github.com/patrick-steele-idem/morphdom/blob/master/src/specialElHandlers.js
|
|
4062
|
+
* https://github.com/choojs/nanomorph/blob/master/lib/morph.jsL113
|
|
4063
|
+
*
|
|
4064
|
+
* @param from {Element} the element to sync the input value from
|
|
4065
|
+
* @param to {Element} the element to sync the input value to
|
|
4066
|
+
* @param ctx the merge context
|
|
4067
|
+
*/
|
|
4068
|
+
function syncInputValue(from, to, ctx) {
|
|
4069
|
+
if (from instanceof HTMLInputElement &&
|
|
4070
|
+
to instanceof HTMLInputElement &&
|
|
4071
|
+
from.type !== 'file') {
|
|
4072
|
+
|
|
4073
|
+
let fromValue = from.value;
|
|
4074
|
+
let toValue = to.value;
|
|
4075
|
+
|
|
4076
|
+
// sync boolean attributes
|
|
4077
|
+
syncBooleanAttribute(from, to, 'checked', ctx);
|
|
4078
|
+
syncBooleanAttribute(from, to, 'disabled', ctx);
|
|
4079
|
+
|
|
4080
|
+
if (!from.hasAttribute('value')) {
|
|
4081
|
+
if (!ignoreAttribute('value', to, 'remove', ctx)) {
|
|
4082
|
+
to.value = '';
|
|
4083
|
+
to.removeAttribute('value');
|
|
4084
|
+
}
|
|
4085
|
+
} else if (fromValue !== toValue) {
|
|
4086
|
+
if (!ignoreAttribute('value', to, 'update', ctx)) {
|
|
4087
|
+
to.setAttribute('value', fromValue);
|
|
4088
|
+
to.value = fromValue;
|
|
4089
|
+
}
|
|
4090
|
+
}
|
|
4091
|
+
} else if (from instanceof HTMLOptionElement) {
|
|
4092
|
+
syncBooleanAttribute(from, to, 'selected', ctx);
|
|
4093
|
+
} else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) {
|
|
4094
|
+
let fromValue = from.value;
|
|
4095
|
+
let toValue = to.value;
|
|
4096
|
+
if (ignoreAttribute('value', to, 'update', ctx)) {
|
|
4097
|
+
return;
|
|
4098
|
+
}
|
|
4099
|
+
if (fromValue !== toValue) {
|
|
4100
|
+
to.value = fromValue;
|
|
4101
|
+
}
|
|
4102
|
+
if (to.firstChild && to.firstChild.nodeValue !== fromValue) {
|
|
4103
|
+
to.firstChild.nodeValue = fromValue;
|
|
4104
|
+
}
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
3727
4107
|
|
|
3728
|
-
|
|
4108
|
+
//=============================================================================
|
|
4109
|
+
// the HEAD tag can be handled specially, either w/ a 'merge' or 'append' style
|
|
4110
|
+
//=============================================================================
|
|
4111
|
+
function handleHeadElement(newHeadTag, currentHead, ctx) {
|
|
3729
4112
|
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
}
|
|
4113
|
+
let added = [];
|
|
4114
|
+
let removed = [];
|
|
4115
|
+
let preserved = [];
|
|
4116
|
+
let nodesToAppend = [];
|
|
3735
4117
|
|
|
3736
|
-
|
|
3737
|
-
for (const currentHeadElt of currentHead.children) {
|
|
4118
|
+
let headMergeStyle = ctx.head.style;
|
|
3738
4119
|
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
if (inNewContent || isPreserved) {
|
|
3744
|
-
if (isReAppended) {
|
|
3745
|
-
// remove the current version and let the new version replace it and re-execute
|
|
3746
|
-
removed.push(currentHeadElt);
|
|
3747
|
-
} else {
|
|
3748
|
-
// this element already exists and should not be re-appended, so remove it from
|
|
3749
|
-
// the new content map, preserving it in the DOM
|
|
3750
|
-
srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
|
|
3751
|
-
preserved.push(currentHeadElt);
|
|
4120
|
+
// put all new head elements into a Map, by their outerHTML
|
|
4121
|
+
let srcToNewHeadNodes = new Map();
|
|
4122
|
+
for (const newHeadChild of newHeadTag.children) {
|
|
4123
|
+
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
|
|
3752
4124
|
}
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
4125
|
+
|
|
4126
|
+
// for each elt in the current head
|
|
4127
|
+
for (const currentHeadElt of currentHead.children) {
|
|
4128
|
+
|
|
4129
|
+
// If the current head element is in the map
|
|
4130
|
+
let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
|
|
4131
|
+
let isReAppended = ctx.head.shouldReAppend(currentHeadElt);
|
|
4132
|
+
let isPreserved = ctx.head.shouldPreserve(currentHeadElt);
|
|
4133
|
+
if (inNewContent || isPreserved) {
|
|
4134
|
+
if (isReAppended) {
|
|
4135
|
+
// remove the current version and let the new version replace it and re-execute
|
|
4136
|
+
removed.push(currentHeadElt);
|
|
4137
|
+
} else {
|
|
4138
|
+
// this element already exists and should not be re-appended, so remove it from
|
|
4139
|
+
// the new content map, preserving it in the DOM
|
|
4140
|
+
srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
|
|
4141
|
+
preserved.push(currentHeadElt);
|
|
4142
|
+
}
|
|
4143
|
+
} else {
|
|
4144
|
+
if (headMergeStyle === "append") {
|
|
4145
|
+
// we are appending and this existing element is not new content
|
|
4146
|
+
// so if and only if it is marked for re-append do we do anything
|
|
4147
|
+
if (isReAppended) {
|
|
4148
|
+
removed.push(currentHeadElt);
|
|
4149
|
+
nodesToAppend.push(currentHeadElt);
|
|
4150
|
+
}
|
|
4151
|
+
} else {
|
|
4152
|
+
// if this is a merge, we remove this content since it is not in the new head
|
|
4153
|
+
if (ctx.head.shouldRemove(currentHeadElt) !== false) {
|
|
4154
|
+
removed.push(currentHeadElt);
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
3760
4157
|
}
|
|
3761
|
-
}
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
4158
|
+
}
|
|
4159
|
+
|
|
4160
|
+
// Push the remaining new head elements in the Map into the
|
|
4161
|
+
// nodes to append to the head tag
|
|
4162
|
+
nodesToAppend.push(...srcToNewHeadNodes.values());
|
|
4163
|
+
|
|
4164
|
+
let promises = [];
|
|
4165
|
+
for (const newNode of nodesToAppend) {
|
|
4166
|
+
let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild;
|
|
4167
|
+
if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
|
|
4168
|
+
if (newElt.href || newElt.src) {
|
|
4169
|
+
let resolve = null;
|
|
4170
|
+
let promise = new Promise(function (_resolve) {
|
|
4171
|
+
resolve = _resolve;
|
|
4172
|
+
});
|
|
4173
|
+
newElt.addEventListener('load', function () {
|
|
4174
|
+
resolve();
|
|
4175
|
+
});
|
|
4176
|
+
promises.push(promise);
|
|
4177
|
+
}
|
|
4178
|
+
currentHead.appendChild(newElt);
|
|
4179
|
+
ctx.callbacks.afterNodeAdded(newElt);
|
|
4180
|
+
added.push(newElt);
|
|
3765
4181
|
}
|
|
3766
4182
|
}
|
|
3767
|
-
}
|
|
3768
|
-
}
|
|
3769
4183
|
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
|
|
3778
|
-
if (newElt.href || newElt.src) {
|
|
3779
|
-
let resolve = null;
|
|
3780
|
-
let promise = new Promise(function (_resolve) {
|
|
3781
|
-
resolve = _resolve;
|
|
3782
|
-
});
|
|
3783
|
-
newElt.addEventListener('load',function() {
|
|
3784
|
-
resolve();
|
|
3785
|
-
});
|
|
3786
|
-
promises.push(promise);
|
|
4184
|
+
// remove all removed elements, after we have appended the new elements to avoid
|
|
4185
|
+
// additional network requests for things like style sheets
|
|
4186
|
+
for (const removedElement of removed) {
|
|
4187
|
+
if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
|
|
4188
|
+
currentHead.removeChild(removedElement);
|
|
4189
|
+
ctx.callbacks.afterNodeRemoved(removedElement);
|
|
4190
|
+
}
|
|
3787
4191
|
}
|
|
3788
|
-
currentHead.appendChild(newElt);
|
|
3789
|
-
ctx.callbacks.afterNodeAdded(newElt);
|
|
3790
|
-
added.push(newElt);
|
|
3791
|
-
}
|
|
3792
|
-
}
|
|
3793
4192
|
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
for (const removedElement of removed) {
|
|
3797
|
-
if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
|
|
3798
|
-
currentHead.removeChild(removedElement);
|
|
3799
|
-
ctx.callbacks.afterNodeRemoved(removedElement);
|
|
4193
|
+
ctx.head.afterHeadMorphed(currentHead, {added: added, kept: preserved, removed: removed});
|
|
4194
|
+
return promises;
|
|
3800
4195
|
}
|
|
3801
|
-
}
|
|
3802
4196
|
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
}
|
|
4197
|
+
function noOp() {
|
|
4198
|
+
}
|
|
3806
4199
|
|
|
3807
|
-
|
|
4200
|
+
/*
|
|
4201
|
+
Deep merges the config object and the Idiomoroph.defaults object to
|
|
4202
|
+
produce a final configuration object
|
|
4203
|
+
*/
|
|
4204
|
+
function mergeDefaults(config) {
|
|
4205
|
+
let finalConfig = {};
|
|
4206
|
+
// copy top level stuff into final config
|
|
4207
|
+
Object.assign(finalConfig, defaults);
|
|
4208
|
+
Object.assign(finalConfig, config);
|
|
4209
|
+
|
|
4210
|
+
// copy callbacks into final config (do this to deep merge the callbacks)
|
|
4211
|
+
finalConfig.callbacks = {};
|
|
4212
|
+
Object.assign(finalConfig.callbacks, defaults.callbacks);
|
|
4213
|
+
Object.assign(finalConfig.callbacks, config.callbacks);
|
|
4214
|
+
|
|
4215
|
+
// copy head config into final config (do this to deep merge the head)
|
|
4216
|
+
finalConfig.head = {};
|
|
4217
|
+
Object.assign(finalConfig.head, defaults.head);
|
|
4218
|
+
Object.assign(finalConfig.head, config.head);
|
|
4219
|
+
return finalConfig;
|
|
4220
|
+
}
|
|
3808
4221
|
|
|
3809
|
-
function createMorphContext(oldNode, newContent, config) {
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
afterNodeRemoved : noOp,
|
|
3825
|
-
|
|
3826
|
-
}, config.callbacks),
|
|
3827
|
-
head: Object.assign({
|
|
3828
|
-
style: 'merge',
|
|
3829
|
-
shouldPreserve : function(elt) {
|
|
3830
|
-
return elt.getAttribute("im-preserve") === "true";
|
|
3831
|
-
},
|
|
3832
|
-
shouldReAppend : function(elt) {
|
|
3833
|
-
return elt.getAttribute("im-re-append") === "true";
|
|
3834
|
-
},
|
|
3835
|
-
shouldRemove : noOp,
|
|
3836
|
-
afterHeadMorphed : noOp,
|
|
3837
|
-
}, config.head),
|
|
3838
|
-
}
|
|
3839
|
-
}
|
|
4222
|
+
function createMorphContext(oldNode, newContent, config) {
|
|
4223
|
+
config = mergeDefaults(config);
|
|
4224
|
+
return {
|
|
4225
|
+
target: oldNode,
|
|
4226
|
+
newContent: newContent,
|
|
4227
|
+
config: config,
|
|
4228
|
+
morphStyle: config.morphStyle,
|
|
4229
|
+
ignoreActive: config.ignoreActive,
|
|
4230
|
+
ignoreActiveValue: config.ignoreActiveValue,
|
|
4231
|
+
idMap: createIdMap(oldNode, newContent),
|
|
4232
|
+
deadIds: new Set(),
|
|
4233
|
+
callbacks: config.callbacks,
|
|
4234
|
+
head: config.head
|
|
4235
|
+
}
|
|
4236
|
+
}
|
|
3840
4237
|
|
|
3841
|
-
function isIdSetMatch(node1, node2, ctx) {
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
4238
|
+
function isIdSetMatch(node1, node2, ctx) {
|
|
4239
|
+
if (node1 == null || node2 == null) {
|
|
4240
|
+
return false;
|
|
4241
|
+
}
|
|
4242
|
+
if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) {
|
|
4243
|
+
if (node1.id !== "" && node1.id === node2.id) {
|
|
4244
|
+
return true;
|
|
4245
|
+
} else {
|
|
4246
|
+
return getIdIntersectionCount(ctx, node1, node2) > 0;
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
return false;
|
|
3850
4250
|
}
|
|
3851
|
-
}
|
|
3852
|
-
return false;
|
|
3853
|
-
}
|
|
3854
4251
|
|
|
3855
|
-
function isSoftMatch(node1, node2) {
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
}
|
|
4252
|
+
function isSoftMatch(node1, node2) {
|
|
4253
|
+
if (node1 == null || node2 == null) {
|
|
4254
|
+
return false;
|
|
4255
|
+
}
|
|
4256
|
+
return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName
|
|
4257
|
+
}
|
|
3861
4258
|
|
|
3862
|
-
function removeNodesBetween(startInclusive, endExclusive, ctx) {
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
}
|
|
4259
|
+
function removeNodesBetween(startInclusive, endExclusive, ctx) {
|
|
4260
|
+
while (startInclusive !== endExclusive) {
|
|
4261
|
+
let tempNode = startInclusive;
|
|
4262
|
+
startInclusive = startInclusive.nextSibling;
|
|
4263
|
+
removeNode(tempNode, ctx);
|
|
4264
|
+
}
|
|
4265
|
+
removeIdsFromConsideration(ctx, endExclusive);
|
|
4266
|
+
return endExclusive.nextSibling;
|
|
4267
|
+
}
|
|
3871
4268
|
|
|
3872
|
-
//=============================================================================
|
|
3873
|
-
// Scans forward from the insertionPoint in the old parent looking for a potential id match
|
|
3874
|
-
// for the newChild. We stop if we find a potential id match for the new child OR
|
|
3875
|
-
// if the number of potential id matches we are discarding is greater than the
|
|
3876
|
-
// potential id matches for the new child
|
|
3877
|
-
//=============================================================================
|
|
3878
|
-
function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
|
|
4269
|
+
//=============================================================================
|
|
4270
|
+
// Scans forward from the insertionPoint in the old parent looking for a potential id match
|
|
4271
|
+
// for the newChild. We stop if we find a potential id match for the new child OR
|
|
4272
|
+
// if the number of potential id matches we are discarding is greater than the
|
|
4273
|
+
// potential id matches for the new child
|
|
4274
|
+
//=============================================================================
|
|
4275
|
+
function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
|
|
4276
|
+
|
|
4277
|
+
// max id matches we are willing to discard in our search
|
|
4278
|
+
let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent);
|
|
4279
|
+
|
|
4280
|
+
let potentialMatch = null;
|
|
4281
|
+
|
|
4282
|
+
// only search forward if there is a possibility of an id match
|
|
4283
|
+
if (newChildPotentialIdCount > 0) {
|
|
4284
|
+
let potentialMatch = insertionPoint;
|
|
4285
|
+
// if there is a possibility of an id match, scan forward
|
|
4286
|
+
// keep track of the potential id match count we are discarding (the
|
|
4287
|
+
// newChildPotentialIdCount must be greater than this to make it likely
|
|
4288
|
+
// worth it)
|
|
4289
|
+
let otherMatchCount = 0;
|
|
4290
|
+
while (potentialMatch != null) {
|
|
4291
|
+
|
|
4292
|
+
// If we have an id match, return the current potential match
|
|
4293
|
+
if (isIdSetMatch(newChild, potentialMatch, ctx)) {
|
|
4294
|
+
return potentialMatch;
|
|
4295
|
+
}
|
|
3879
4296
|
|
|
3880
|
-
|
|
3881
|
-
|
|
4297
|
+
// computer the other potential matches of this new content
|
|
4298
|
+
otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent);
|
|
4299
|
+
if (otherMatchCount > newChildPotentialIdCount) {
|
|
4300
|
+
// if we have more potential id matches in _other_ content, we
|
|
4301
|
+
// do not have a good candidate for an id match, so return null
|
|
4302
|
+
return null;
|
|
4303
|
+
}
|
|
3882
4304
|
|
|
3883
|
-
|
|
4305
|
+
// advanced to the next old content child
|
|
4306
|
+
potentialMatch = potentialMatch.nextSibling;
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4309
|
+
return potentialMatch;
|
|
4310
|
+
}
|
|
3884
4311
|
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
// if
|
|
3889
|
-
//
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
let otherMatchCount = 0;
|
|
3893
|
-
while (potentialMatch != null) {
|
|
4312
|
+
//=============================================================================
|
|
4313
|
+
// Scans forward from the insertionPoint in the old parent looking for a potential soft match
|
|
4314
|
+
// for the newChild. We stop if we find a potential soft match for the new child OR
|
|
4315
|
+
// if we find a potential id match in the old parents children OR if we find two
|
|
4316
|
+
// potential soft matches for the next two pieces of new content
|
|
4317
|
+
//=============================================================================
|
|
4318
|
+
function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
|
|
3894
4319
|
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
}
|
|
4320
|
+
let potentialSoftMatch = insertionPoint;
|
|
4321
|
+
let nextSibling = newChild.nextSibling;
|
|
4322
|
+
let siblingSoftMatchCount = 0;
|
|
3899
4323
|
|
|
3900
|
-
|
|
3901
|
-
otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent);
|
|
3902
|
-
if (otherMatchCount > newChildPotentialIdCount) {
|
|
3903
|
-
// if we have more potential id matches in _other_ content, we
|
|
3904
|
-
// do not have a good candidate for an id match, so return null
|
|
3905
|
-
return null;
|
|
3906
|
-
}
|
|
4324
|
+
while (potentialSoftMatch != null) {
|
|
3907
4325
|
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
}
|
|
4326
|
+
if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) {
|
|
4327
|
+
// the current potential soft match has a potential id set match with the remaining new
|
|
4328
|
+
// content so bail out of looking
|
|
4329
|
+
return null;
|
|
4330
|
+
}
|
|
3914
4331
|
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
// potential soft matches for the next two pieces of new content
|
|
3920
|
-
//=============================================================================
|
|
3921
|
-
function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
|
|
4332
|
+
// if we have a soft match with the current node, return it
|
|
4333
|
+
if (isSoftMatch(newChild, potentialSoftMatch)) {
|
|
4334
|
+
return potentialSoftMatch;
|
|
4335
|
+
}
|
|
3922
4336
|
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
4337
|
+
if (isSoftMatch(nextSibling, potentialSoftMatch)) {
|
|
4338
|
+
// the next new node has a soft match with this node, so
|
|
4339
|
+
// increment the count of future soft matches
|
|
4340
|
+
siblingSoftMatchCount++;
|
|
4341
|
+
nextSibling = nextSibling.nextSibling;
|
|
3926
4342
|
|
|
3927
|
-
|
|
4343
|
+
// If there are two future soft matches, bail to allow the siblings to soft match
|
|
4344
|
+
// so that we don't consume future soft matches for the sake of the current node
|
|
4345
|
+
if (siblingSoftMatchCount >= 2) {
|
|
4346
|
+
return null;
|
|
4347
|
+
}
|
|
4348
|
+
}
|
|
3928
4349
|
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
return null;
|
|
3933
|
-
}
|
|
4350
|
+
// advanced to the next old content child
|
|
4351
|
+
potentialSoftMatch = potentialSoftMatch.nextSibling;
|
|
4352
|
+
}
|
|
3934
4353
|
|
|
3935
|
-
// if we have a soft match with the current node, return it
|
|
3936
|
-
if (isSoftMatch(newChild, potentialSoftMatch)) {
|
|
3937
4354
|
return potentialSoftMatch;
|
|
3938
4355
|
}
|
|
3939
4356
|
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
//
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
return
|
|
4357
|
+
function parseContent(newContent) {
|
|
4358
|
+
let parser = new DOMParser();
|
|
4359
|
+
|
|
4360
|
+
// remove svgs to avoid false-positive matches on head, etc.
|
|
4361
|
+
let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
|
|
4362
|
+
|
|
4363
|
+
// if the newContent contains a html, head or body tag, we can simply parse it w/o wrapping
|
|
4364
|
+
if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) {
|
|
4365
|
+
let content = parser.parseFromString(newContent, "text/html");
|
|
4366
|
+
// if it is a full HTML document, return the document itself as the parent container
|
|
4367
|
+
if (contentWithSvgsRemoved.match(/<\/html>/)) {
|
|
4368
|
+
content.generatedByIdiomorph = true;
|
|
4369
|
+
return content;
|
|
4370
|
+
} else {
|
|
4371
|
+
// otherwise return the html element as the parent container
|
|
4372
|
+
let htmlElement = content.firstChild;
|
|
4373
|
+
if (htmlElement) {
|
|
4374
|
+
htmlElement.generatedByIdiomorph = true;
|
|
4375
|
+
return htmlElement;
|
|
4376
|
+
} else {
|
|
4377
|
+
return null;
|
|
4378
|
+
}
|
|
4379
|
+
}
|
|
4380
|
+
} else {
|
|
4381
|
+
// if it is partial HTML, wrap it in a template tag to provide a parent element and also to help
|
|
4382
|
+
// deal with touchy tags like tr, tbody, etc.
|
|
4383
|
+
let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html");
|
|
4384
|
+
let content = responseDoc.body.querySelector('template').content;
|
|
4385
|
+
content.generatedByIdiomorph = true;
|
|
4386
|
+
return content
|
|
3950
4387
|
}
|
|
3951
4388
|
}
|
|
3952
4389
|
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
}
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
// if the newContent contains a html, head or body tag, we can simply parse it w/o wrapping
|
|
3967
|
-
if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) {
|
|
3968
|
-
let content = parser.parseFromString(newContent, "text/html");
|
|
3969
|
-
// if it is a full HTML document, return the document itself as the parent container
|
|
3970
|
-
if (contentWithSvgsRemoved.match(/<\/html>/)) {
|
|
3971
|
-
content.generatedByIdiomorph = true;
|
|
3972
|
-
return content;
|
|
3973
|
-
} else {
|
|
3974
|
-
// otherwise return the html element as the parent container
|
|
3975
|
-
let htmlElement = content.firstChild;
|
|
3976
|
-
if (htmlElement) {
|
|
3977
|
-
htmlElement.generatedByIdiomorph = true;
|
|
3978
|
-
return htmlElement;
|
|
4390
|
+
function normalizeContent(newContent) {
|
|
4391
|
+
if (newContent == null) {
|
|
4392
|
+
// noinspection UnnecessaryLocalVariableJS
|
|
4393
|
+
const dummyParent = document.createElement('div');
|
|
4394
|
+
return dummyParent;
|
|
4395
|
+
} else if (newContent.generatedByIdiomorph) {
|
|
4396
|
+
// the template tag created by idiomorph parsing can serve as a dummy parent
|
|
4397
|
+
return newContent;
|
|
4398
|
+
} else if (newContent instanceof Node) {
|
|
4399
|
+
// a single node is added as a child to a dummy parent
|
|
4400
|
+
const dummyParent = document.createElement('div');
|
|
4401
|
+
dummyParent.append(newContent);
|
|
4402
|
+
return dummyParent;
|
|
3979
4403
|
} else {
|
|
3980
|
-
|
|
4404
|
+
// all nodes in the array or HTMLElement collection are consolidated under
|
|
4405
|
+
// a single dummy parent element
|
|
4406
|
+
const dummyParent = document.createElement('div');
|
|
4407
|
+
for (const elt of [...newContent]) {
|
|
4408
|
+
dummyParent.append(elt);
|
|
4409
|
+
}
|
|
4410
|
+
return dummyParent;
|
|
3981
4411
|
}
|
|
3982
4412
|
}
|
|
3983
|
-
} else {
|
|
3984
|
-
// if it is partial HTML, wrap it in a template tag to provide a parent element and also to help
|
|
3985
|
-
// deal with touchy tags like tr, tbody, etc.
|
|
3986
|
-
let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html");
|
|
3987
|
-
let content = responseDoc.body.querySelector('template').content;
|
|
3988
|
-
content.generatedByIdiomorph = true;
|
|
3989
|
-
return content
|
|
3990
|
-
}
|
|
3991
|
-
}
|
|
3992
|
-
|
|
3993
|
-
function normalizeContent(newContent) {
|
|
3994
|
-
if (newContent == null) {
|
|
3995
|
-
// noinspection UnnecessaryLocalVariableJS
|
|
3996
|
-
const dummyParent = document.createElement('div');
|
|
3997
|
-
return dummyParent;
|
|
3998
|
-
} else if (newContent.generatedByIdiomorph) {
|
|
3999
|
-
// the template tag created by idiomorph parsing can serve as a dummy parent
|
|
4000
|
-
return newContent;
|
|
4001
|
-
} else if (newContent instanceof Node) {
|
|
4002
|
-
// a single node is added as a child to a dummy parent
|
|
4003
|
-
const dummyParent = document.createElement('div');
|
|
4004
|
-
dummyParent.append(newContent);
|
|
4005
|
-
return dummyParent;
|
|
4006
|
-
} else {
|
|
4007
|
-
// all nodes in the array or HTMLElement collection are consolidated under
|
|
4008
|
-
// a single dummy parent element
|
|
4009
|
-
const dummyParent = document.createElement('div');
|
|
4010
|
-
for (const elt of [...newContent]) {
|
|
4011
|
-
dummyParent.append(elt);
|
|
4012
|
-
}
|
|
4013
|
-
return dummyParent;
|
|
4014
|
-
}
|
|
4015
|
-
}
|
|
4016
4413
|
|
|
4017
|
-
function insertSiblings(previousSibling, morphedNode, nextSibling) {
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
}
|
|
4414
|
+
function insertSiblings(previousSibling, morphedNode, nextSibling) {
|
|
4415
|
+
let stack = [];
|
|
4416
|
+
let added = [];
|
|
4417
|
+
while (previousSibling != null) {
|
|
4418
|
+
stack.push(previousSibling);
|
|
4419
|
+
previousSibling = previousSibling.previousSibling;
|
|
4420
|
+
}
|
|
4421
|
+
while (stack.length > 0) {
|
|
4422
|
+
let node = stack.pop();
|
|
4423
|
+
added.push(node); // push added preceding siblings on in order and insert
|
|
4424
|
+
morphedNode.parentElement.insertBefore(node, morphedNode);
|
|
4425
|
+
}
|
|
4426
|
+
added.push(morphedNode);
|
|
4427
|
+
while (nextSibling != null) {
|
|
4428
|
+
stack.push(nextSibling);
|
|
4429
|
+
added.push(nextSibling); // here we are going in order, so push on as we scan, rather than add
|
|
4430
|
+
nextSibling = nextSibling.nextSibling;
|
|
4431
|
+
}
|
|
4432
|
+
while (stack.length > 0) {
|
|
4433
|
+
morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling);
|
|
4434
|
+
}
|
|
4435
|
+
return added;
|
|
4436
|
+
}
|
|
4040
4437
|
|
|
4041
|
-
function findBestNodeMatch(newContent, oldNode, ctx) {
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4438
|
+
function findBestNodeMatch(newContent, oldNode, ctx) {
|
|
4439
|
+
let currentElement;
|
|
4440
|
+
currentElement = newContent.firstChild;
|
|
4441
|
+
let bestElement = currentElement;
|
|
4442
|
+
let score = 0;
|
|
4443
|
+
while (currentElement) {
|
|
4444
|
+
let newScore = scoreElement(currentElement, oldNode, ctx);
|
|
4445
|
+
if (newScore > score) {
|
|
4446
|
+
bestElement = currentElement;
|
|
4447
|
+
score = newScore;
|
|
4448
|
+
}
|
|
4449
|
+
currentElement = currentElement.nextSibling;
|
|
4450
|
+
}
|
|
4451
|
+
return bestElement;
|
|
4051
4452
|
}
|
|
4052
|
-
currentElement = currentElement.nextSibling;
|
|
4053
|
-
}
|
|
4054
|
-
return bestElement;
|
|
4055
|
-
}
|
|
4056
4453
|
|
|
4057
|
-
function scoreElement(node1, node2, ctx) {
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
}
|
|
4454
|
+
function scoreElement(node1, node2, ctx) {
|
|
4455
|
+
if (isSoftMatch(node1, node2)) {
|
|
4456
|
+
return .5 + getIdIntersectionCount(ctx, node1, node2);
|
|
4457
|
+
}
|
|
4458
|
+
return 0;
|
|
4459
|
+
}
|
|
4063
4460
|
|
|
4064
|
-
function removeNode(tempNode, ctx) {
|
|
4065
|
-
|
|
4066
|
-
|
|
4461
|
+
function removeNode(tempNode, ctx) {
|
|
4462
|
+
removeIdsFromConsideration(ctx, tempNode);
|
|
4463
|
+
if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return;
|
|
4067
4464
|
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
}
|
|
4465
|
+
tempNode.remove();
|
|
4466
|
+
ctx.callbacks.afterNodeRemoved(tempNode);
|
|
4467
|
+
}
|
|
4071
4468
|
|
|
4072
|
-
//=============================================================================
|
|
4073
|
-
// ID Set Functions
|
|
4074
|
-
//=============================================================================
|
|
4469
|
+
//=============================================================================
|
|
4470
|
+
// ID Set Functions
|
|
4471
|
+
//=============================================================================
|
|
4075
4472
|
|
|
4076
|
-
function isIdInConsideration(ctx, id) {
|
|
4077
|
-
|
|
4078
|
-
}
|
|
4473
|
+
function isIdInConsideration(ctx, id) {
|
|
4474
|
+
return !ctx.deadIds.has(id);
|
|
4475
|
+
}
|
|
4079
4476
|
|
|
4080
|
-
function idIsWithinNode(ctx, id, targetNode) {
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
}
|
|
4477
|
+
function idIsWithinNode(ctx, id, targetNode) {
|
|
4478
|
+
let idSet = ctx.idMap.get(targetNode) || EMPTY_SET;
|
|
4479
|
+
return idSet.has(id);
|
|
4480
|
+
}
|
|
4084
4481
|
|
|
4085
|
-
function removeIdsFromConsideration(ctx, node) {
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
}
|
|
4482
|
+
function removeIdsFromConsideration(ctx, node) {
|
|
4483
|
+
let idSet = ctx.idMap.get(node) || EMPTY_SET;
|
|
4484
|
+
for (const id of idSet) {
|
|
4485
|
+
ctx.deadIds.add(id);
|
|
4486
|
+
}
|
|
4487
|
+
}
|
|
4091
4488
|
|
|
4092
|
-
function getIdIntersectionCount(ctx, node1, node2) {
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4489
|
+
function getIdIntersectionCount(ctx, node1, node2) {
|
|
4490
|
+
let sourceSet = ctx.idMap.get(node1) || EMPTY_SET;
|
|
4491
|
+
let matchCount = 0;
|
|
4492
|
+
for (const id of sourceSet) {
|
|
4493
|
+
// a potential match is an id in the source and potentialIdsSet, but
|
|
4494
|
+
// that has not already been merged into the DOM
|
|
4495
|
+
if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) {
|
|
4496
|
+
++matchCount;
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4499
|
+
return matchCount;
|
|
4100
4500
|
}
|
|
4101
|
-
}
|
|
4102
|
-
return matchCount;
|
|
4103
|
-
}
|
|
4104
4501
|
|
|
4105
|
-
/**
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
function populateIdMapForNode(node, idMap) {
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4502
|
+
/**
|
|
4503
|
+
* A bottom up algorithm that finds all elements with ids inside of the node
|
|
4504
|
+
* argument and populates id sets for those nodes and all their parents, generating
|
|
4505
|
+
* a set of ids contained within all nodes for the entire hierarchy in the DOM
|
|
4506
|
+
*
|
|
4507
|
+
* @param node {Element}
|
|
4508
|
+
* @param {Map<Node, Set<String>>} idMap
|
|
4509
|
+
*/
|
|
4510
|
+
function populateIdMapForNode(node, idMap) {
|
|
4511
|
+
let nodeParent = node.parentElement;
|
|
4512
|
+
// find all elements with an id property
|
|
4513
|
+
let idElements = node.querySelectorAll('[id]');
|
|
4514
|
+
for (const elt of idElements) {
|
|
4515
|
+
let current = elt;
|
|
4516
|
+
// walk up the parent hierarchy of that element, adding the id
|
|
4517
|
+
// of element to the parent's id set
|
|
4518
|
+
while (current !== nodeParent && current != null) {
|
|
4519
|
+
let idSet = idMap.get(current);
|
|
4520
|
+
// if the id set doesn't exist, create it and insert it in the map
|
|
4521
|
+
if (idSet == null) {
|
|
4522
|
+
idSet = new Set();
|
|
4523
|
+
idMap.set(current, idSet);
|
|
4524
|
+
}
|
|
4525
|
+
idSet.add(elt.id);
|
|
4526
|
+
current = current.parentElement;
|
|
4527
|
+
}
|
|
4127
4528
|
}
|
|
4128
|
-
idSet.add(elt.id);
|
|
4129
|
-
current = current.parentElement;
|
|
4130
4529
|
}
|
|
4131
|
-
}
|
|
4132
|
-
}
|
|
4133
4530
|
|
|
4134
|
-
/**
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
function createIdMap(oldContent, newContent) {
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
}
|
|
4531
|
+
/**
|
|
4532
|
+
* This function computes a map of nodes to all ids contained within that node (inclusive of the
|
|
4533
|
+
* node). This map can be used to ask if two nodes have intersecting sets of ids, which allows
|
|
4534
|
+
* for a looser definition of "matching" than tradition id matching, and allows child nodes
|
|
4535
|
+
* to contribute to a parent nodes matching.
|
|
4536
|
+
*
|
|
4537
|
+
* @param {Element} oldContent the old content that will be morphed
|
|
4538
|
+
* @param {Element} newContent the new content to morph to
|
|
4539
|
+
* @returns {Map<Node, Set<String>>} a map of nodes to id sets for the
|
|
4540
|
+
*/
|
|
4541
|
+
function createIdMap(oldContent, newContent) {
|
|
4542
|
+
let idMap = new Map();
|
|
4543
|
+
populateIdMapForNode(oldContent, idMap);
|
|
4544
|
+
populateIdMapForNode(newContent, idMap);
|
|
4545
|
+
return idMap;
|
|
4546
|
+
}
|
|
4150
4547
|
|
|
4151
|
-
//=============================================================================
|
|
4152
|
-
// This is what ends up becoming the Idiomorph
|
|
4153
|
-
//=============================================================================
|
|
4154
|
-
|
|
4548
|
+
//=============================================================================
|
|
4549
|
+
// This is what ends up becoming the Idiomorph global object
|
|
4550
|
+
//=============================================================================
|
|
4551
|
+
return {
|
|
4552
|
+
morph,
|
|
4553
|
+
defaults
|
|
4554
|
+
}
|
|
4555
|
+
})();
|
|
4155
4556
|
|
|
4156
4557
|
class MorphRenderer extends Renderer {
|
|
4157
4558
|
async render() {
|
|
@@ -4179,7 +4580,7 @@ class MorphRenderer extends Renderer {
|
|
|
4179
4580
|
#morphElements(currentElement, newElement, morphStyle = "outerHTML") {
|
|
4180
4581
|
this.isMorphingTurboFrame = this.#isFrameReloadedWithMorph(currentElement);
|
|
4181
4582
|
|
|
4182
|
-
|
|
4583
|
+
Idiomorph.morph(currentElement, newElement, {
|
|
4183
4584
|
morphStyle: morphStyle,
|
|
4184
4585
|
callbacks: {
|
|
4185
4586
|
beforeNodeAdded: this.#shouldAddElement,
|
|
@@ -4311,8 +4712,13 @@ class PageRenderer extends Renderer {
|
|
|
4311
4712
|
const mergedHeadElements = this.mergeProvisionalElements();
|
|
4312
4713
|
const newStylesheetElements = this.copyNewHeadStylesheetElements();
|
|
4313
4714
|
this.copyNewHeadScriptElements();
|
|
4715
|
+
|
|
4314
4716
|
await mergedHeadElements;
|
|
4315
4717
|
await newStylesheetElements;
|
|
4718
|
+
|
|
4719
|
+
if (this.willRender) {
|
|
4720
|
+
this.removeUnusedHeadStylesheetElements();
|
|
4721
|
+
}
|
|
4316
4722
|
}
|
|
4317
4723
|
|
|
4318
4724
|
async replaceBody() {
|
|
@@ -4344,6 +4750,12 @@ class PageRenderer extends Renderer {
|
|
|
4344
4750
|
}
|
|
4345
4751
|
}
|
|
4346
4752
|
|
|
4753
|
+
removeUnusedHeadStylesheetElements() {
|
|
4754
|
+
for (const element of this.unusedHeadStylesheetElements) {
|
|
4755
|
+
document.head.removeChild(element);
|
|
4756
|
+
}
|
|
4757
|
+
}
|
|
4758
|
+
|
|
4347
4759
|
async mergeProvisionalElements() {
|
|
4348
4760
|
const newHeadElements = [...this.newHeadProvisionalElements];
|
|
4349
4761
|
|
|
@@ -4409,6 +4821,20 @@ class PageRenderer extends Renderer {
|
|
|
4409
4821
|
await this.renderElement(this.currentElement, this.newElement);
|
|
4410
4822
|
}
|
|
4411
4823
|
|
|
4824
|
+
get unusedHeadStylesheetElements() {
|
|
4825
|
+
return this.oldHeadStylesheetElements.filter((element) => {
|
|
4826
|
+
return !(element.hasAttribute("data-turbo-permanent") ||
|
|
4827
|
+
// Trix dynamically adds styles to the head that we want to keep around which have a
|
|
4828
|
+
// `data-tag-name` attribute. Long term we should moves those styles to Trix's CSS file
|
|
4829
|
+
// but for now we'll just skip removing them
|
|
4830
|
+
element.hasAttribute("data-tag-name"))
|
|
4831
|
+
})
|
|
4832
|
+
}
|
|
4833
|
+
|
|
4834
|
+
get oldHeadStylesheetElements() {
|
|
4835
|
+
return this.currentHeadSnapshot.getStylesheetElementsNotInSnapshot(this.newHeadSnapshot)
|
|
4836
|
+
}
|
|
4837
|
+
|
|
4412
4838
|
get newHeadStylesheetElements() {
|
|
4413
4839
|
return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot)
|
|
4414
4840
|
}
|
|
@@ -4535,7 +4961,11 @@ class PageView extends View {
|
|
|
4535
4961
|
}
|
|
4536
4962
|
|
|
4537
4963
|
isPageRefresh(visit) {
|
|
4538
|
-
return !visit || (this.lastRenderedLocation.
|
|
4964
|
+
return !visit || (this.lastRenderedLocation.pathname === visit.location.pathname && visit.action === "replace")
|
|
4965
|
+
}
|
|
4966
|
+
|
|
4967
|
+
shouldPreserveScrollPosition(visit) {
|
|
4968
|
+
return this.isPageRefresh(visit) && this.snapshot.shouldPreserveScrollPosition
|
|
4539
4969
|
}
|
|
4540
4970
|
|
|
4541
4971
|
get snapshot() {
|
|
@@ -4546,27 +4976,28 @@ class PageView extends View {
|
|
|
4546
4976
|
class Preloader {
|
|
4547
4977
|
selector = "a[data-turbo-preload]"
|
|
4548
4978
|
|
|
4549
|
-
constructor(delegate) {
|
|
4979
|
+
constructor(delegate, snapshotCache) {
|
|
4550
4980
|
this.delegate = delegate;
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
get snapshotCache() {
|
|
4554
|
-
return this.delegate.navigator.view.snapshotCache
|
|
4981
|
+
this.snapshotCache = snapshotCache;
|
|
4555
4982
|
}
|
|
4556
4983
|
|
|
4557
4984
|
start() {
|
|
4558
4985
|
if (document.readyState === "loading") {
|
|
4559
|
-
|
|
4560
|
-
this.preloadOnLoadLinksForView(document.body);
|
|
4561
|
-
})
|
|
4986
|
+
document.addEventListener("DOMContentLoaded", this.#preloadAll);
|
|
4562
4987
|
} else {
|
|
4563
4988
|
this.preloadOnLoadLinksForView(document.body);
|
|
4564
4989
|
}
|
|
4565
4990
|
}
|
|
4566
4991
|
|
|
4992
|
+
stop() {
|
|
4993
|
+
document.removeEventListener("DOMContentLoaded", this.#preloadAll);
|
|
4994
|
+
}
|
|
4995
|
+
|
|
4567
4996
|
preloadOnLoadLinksForView(element) {
|
|
4568
4997
|
for (const link of element.querySelectorAll(this.selector)) {
|
|
4569
|
-
this.
|
|
4998
|
+
if (this.delegate.shouldPreloadLink(link)) {
|
|
4999
|
+
this.preloadURL(link);
|
|
5000
|
+
}
|
|
4570
5001
|
}
|
|
4571
5002
|
}
|
|
4572
5003
|
|
|
@@ -4577,31 +5008,39 @@ class Preloader {
|
|
|
4577
5008
|
return
|
|
4578
5009
|
}
|
|
4579
5010
|
|
|
5011
|
+
const fetchRequest = new FetchRequest(this, FetchMethod.get, location, new URLSearchParams(), link);
|
|
5012
|
+
await fetchRequest.perform();
|
|
5013
|
+
}
|
|
5014
|
+
|
|
5015
|
+
// Fetch request delegate
|
|
5016
|
+
|
|
5017
|
+
prepareRequest(fetchRequest) {
|
|
5018
|
+
fetchRequest.headers["Sec-Purpose"] = "prefetch";
|
|
5019
|
+
}
|
|
5020
|
+
|
|
5021
|
+
async requestSucceededWithResponse(fetchRequest, fetchResponse) {
|
|
4580
5022
|
try {
|
|
4581
|
-
const
|
|
4582
|
-
const
|
|
4583
|
-
const snapshot = PageSnapshot.fromHTMLString(responseText);
|
|
5023
|
+
const responseHTML = await fetchResponse.responseHTML;
|
|
5024
|
+
const snapshot = PageSnapshot.fromHTMLString(responseHTML);
|
|
4584
5025
|
|
|
4585
|
-
this.snapshotCache.put(
|
|
5026
|
+
this.snapshotCache.put(fetchRequest.url, snapshot);
|
|
4586
5027
|
} catch (_) {
|
|
4587
5028
|
// If we cannot preload that is ok!
|
|
4588
5029
|
}
|
|
4589
5030
|
}
|
|
4590
|
-
}
|
|
4591
5031
|
|
|
4592
|
-
|
|
4593
|
-
constructor(maxSize) {
|
|
4594
|
-
super();
|
|
4595
|
-
this.maxSize = maxSize;
|
|
4596
|
-
}
|
|
5032
|
+
requestStarted(fetchRequest) {}
|
|
4597
5033
|
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
5034
|
+
requestErrored(fetchRequest) {}
|
|
5035
|
+
|
|
5036
|
+
requestFinished(fetchRequest) {}
|
|
5037
|
+
|
|
5038
|
+
requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}
|
|
5039
|
+
|
|
5040
|
+
requestFailedWithResponse(fetchRequest, fetchResponse) {}
|
|
5041
|
+
|
|
5042
|
+
#preloadAll = () => {
|
|
5043
|
+
this.preloadOnLoadLinksForView(document.body);
|
|
4605
5044
|
}
|
|
4606
5045
|
}
|
|
4607
5046
|
|
|
@@ -4634,12 +5073,12 @@ class Cache {
|
|
|
4634
5073
|
class Session {
|
|
4635
5074
|
navigator = new Navigator(this)
|
|
4636
5075
|
history = new History(this)
|
|
4637
|
-
preloader = new Preloader(this)
|
|
4638
5076
|
view = new PageView(this, document.documentElement)
|
|
4639
5077
|
adapter = new BrowserAdapter(this)
|
|
4640
5078
|
|
|
4641
5079
|
pageObserver = new PageObserver(this)
|
|
4642
5080
|
cacheObserver = new CacheObserver()
|
|
5081
|
+
linkPrefetchObserver = new LinkPrefetchObserver(this, document)
|
|
4643
5082
|
linkClickObserver = new LinkClickObserver(this, window)
|
|
4644
5083
|
formSubmitObserver = new FormSubmitObserver(this, document)
|
|
4645
5084
|
scrollObserver = new ScrollObserver(this)
|
|
@@ -4648,18 +5087,26 @@ class Session {
|
|
|
4648
5087
|
frameRedirector = new FrameRedirector(this, document.documentElement)
|
|
4649
5088
|
streamMessageRenderer = new StreamMessageRenderer()
|
|
4650
5089
|
cache = new Cache(this)
|
|
4651
|
-
recentRequests = new LimitedSet(20)
|
|
4652
5090
|
|
|
4653
5091
|
drive = true
|
|
4654
5092
|
enabled = true
|
|
4655
5093
|
progressBarDelay = 500
|
|
4656
5094
|
started = false
|
|
4657
5095
|
formMode = "on"
|
|
5096
|
+
#pageRefreshDebouncePeriod = 150
|
|
5097
|
+
|
|
5098
|
+
constructor(recentRequests) {
|
|
5099
|
+
this.recentRequests = recentRequests;
|
|
5100
|
+
this.preloader = new Preloader(this, this.view.snapshotCache);
|
|
5101
|
+
this.debouncedRefresh = this.refresh;
|
|
5102
|
+
this.pageRefreshDebouncePeriod = this.pageRefreshDebouncePeriod;
|
|
5103
|
+
}
|
|
4658
5104
|
|
|
4659
5105
|
start() {
|
|
4660
5106
|
if (!this.started) {
|
|
4661
5107
|
this.pageObserver.start();
|
|
4662
5108
|
this.cacheObserver.start();
|
|
5109
|
+
this.linkPrefetchObserver.start();
|
|
4663
5110
|
this.formLinkClickObserver.start();
|
|
4664
5111
|
this.linkClickObserver.start();
|
|
4665
5112
|
this.formSubmitObserver.start();
|
|
@@ -4681,6 +5128,7 @@ class Session {
|
|
|
4681
5128
|
if (this.started) {
|
|
4682
5129
|
this.pageObserver.stop();
|
|
4683
5130
|
this.cacheObserver.stop();
|
|
5131
|
+
this.linkPrefetchObserver.stop();
|
|
4684
5132
|
this.formLinkClickObserver.stop();
|
|
4685
5133
|
this.linkClickObserver.stop();
|
|
4686
5134
|
this.formSubmitObserver.stop();
|
|
@@ -4688,6 +5136,7 @@ class Session {
|
|
|
4688
5136
|
this.streamObserver.stop();
|
|
4689
5137
|
this.frameRedirector.stop();
|
|
4690
5138
|
this.history.stop();
|
|
5139
|
+
this.preloader.stop();
|
|
4691
5140
|
this.started = false;
|
|
4692
5141
|
}
|
|
4693
5142
|
}
|
|
@@ -4747,13 +5196,42 @@ class Session {
|
|
|
4747
5196
|
return this.history.restorationIdentifier
|
|
4748
5197
|
}
|
|
4749
5198
|
|
|
5199
|
+
get pageRefreshDebouncePeriod() {
|
|
5200
|
+
return this.#pageRefreshDebouncePeriod
|
|
5201
|
+
}
|
|
5202
|
+
|
|
5203
|
+
set pageRefreshDebouncePeriod(value) {
|
|
5204
|
+
this.refresh = debounce(this.debouncedRefresh.bind(this), value);
|
|
5205
|
+
this.#pageRefreshDebouncePeriod = value;
|
|
5206
|
+
}
|
|
5207
|
+
|
|
5208
|
+
// Preloader delegate
|
|
5209
|
+
|
|
5210
|
+
shouldPreloadLink(element) {
|
|
5211
|
+
const isUnsafe = element.hasAttribute("data-turbo-method");
|
|
5212
|
+
const isStream = element.hasAttribute("data-turbo-stream");
|
|
5213
|
+
const frameTarget = element.getAttribute("data-turbo-frame");
|
|
5214
|
+
const frame = frameTarget == "_top" ?
|
|
5215
|
+
null :
|
|
5216
|
+
document.getElementById(frameTarget) || findClosestRecursively(element, "turbo-frame:not([disabled])");
|
|
5217
|
+
|
|
5218
|
+
if (isUnsafe || isStream || frame instanceof FrameElement) {
|
|
5219
|
+
return false
|
|
5220
|
+
} else {
|
|
5221
|
+
const location = new URL(element.href);
|
|
5222
|
+
|
|
5223
|
+
return this.elementIsNavigatable(element) && locationIsVisitable(location, this.snapshot.rootLocation)
|
|
5224
|
+
}
|
|
5225
|
+
}
|
|
5226
|
+
|
|
4750
5227
|
// History delegate
|
|
4751
5228
|
|
|
4752
|
-
|
|
5229
|
+
historyPoppedToLocationWithRestorationIdentifierAndDirection(location, restorationIdentifier, direction) {
|
|
4753
5230
|
if (this.enabled) {
|
|
4754
5231
|
this.navigator.startVisit(location, restorationIdentifier, {
|
|
4755
5232
|
action: "restore",
|
|
4756
|
-
historyChanged: true
|
|
5233
|
+
historyChanged: true,
|
|
5234
|
+
direction
|
|
4757
5235
|
});
|
|
4758
5236
|
} else {
|
|
4759
5237
|
this.adapter.pageInvalidated({
|
|
@@ -4776,6 +5254,15 @@ class Session {
|
|
|
4776
5254
|
|
|
4777
5255
|
submittedFormLinkToLocation() {}
|
|
4778
5256
|
|
|
5257
|
+
// Link hover observer delegate
|
|
5258
|
+
|
|
5259
|
+
canPrefetchRequestToLocation(link, location) {
|
|
5260
|
+
return (
|
|
5261
|
+
this.elementIsNavigatable(link) &&
|
|
5262
|
+
locationIsVisitable(location, this.snapshot.rootLocation)
|
|
5263
|
+
)
|
|
5264
|
+
}
|
|
5265
|
+
|
|
4779
5266
|
// Link click observer delegate
|
|
4780
5267
|
|
|
4781
5268
|
willFollowLinkToLocation(link, location, event) {
|
|
@@ -4809,6 +5296,7 @@ class Session {
|
|
|
4809
5296
|
visitStarted(visit) {
|
|
4810
5297
|
if (!visit.acceptsStreamResponse) {
|
|
4811
5298
|
markAsBusy(document.documentElement);
|
|
5299
|
+
this.view.markVisitDirection(visit.direction);
|
|
4812
5300
|
}
|
|
4813
5301
|
extendURLWithDeprecatedProperties(visit.location);
|
|
4814
5302
|
if (!visit.silent) {
|
|
@@ -4817,6 +5305,7 @@ class Session {
|
|
|
4817
5305
|
}
|
|
4818
5306
|
|
|
4819
5307
|
visitCompleted(visit) {
|
|
5308
|
+
this.view.unmarkVisitDirection();
|
|
4820
5309
|
clearBusyState(document.documentElement);
|
|
4821
5310
|
this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
|
|
4822
5311
|
}
|
|
@@ -4873,8 +5362,8 @@ class Session {
|
|
|
4873
5362
|
}
|
|
4874
5363
|
}
|
|
4875
5364
|
|
|
4876
|
-
allowsImmediateRender({ element },
|
|
4877
|
-
const event = this.notifyApplicationBeforeRender(element,
|
|
5365
|
+
allowsImmediateRender({ element }, options) {
|
|
5366
|
+
const event = this.notifyApplicationBeforeRender(element, options);
|
|
4878
5367
|
const {
|
|
4879
5368
|
defaultPrevented,
|
|
4880
5369
|
detail: { render }
|
|
@@ -4887,9 +5376,9 @@ class Session {
|
|
|
4887
5376
|
return !defaultPrevented
|
|
4888
5377
|
}
|
|
4889
5378
|
|
|
4890
|
-
viewRenderedSnapshot(_snapshot,
|
|
5379
|
+
viewRenderedSnapshot(_snapshot, _isPreview, renderMethod) {
|
|
4891
5380
|
this.view.lastRenderedLocation = this.history.location;
|
|
4892
|
-
this.notifyApplicationAfterRender(
|
|
5381
|
+
this.notifyApplicationAfterRender(renderMethod);
|
|
4893
5382
|
}
|
|
4894
5383
|
|
|
4895
5384
|
preloadOnLoadLinksForView(element) {
|
|
@@ -4945,15 +5434,15 @@ class Session {
|
|
|
4945
5434
|
return dispatch("turbo:before-cache")
|
|
4946
5435
|
}
|
|
4947
5436
|
|
|
4948
|
-
notifyApplicationBeforeRender(newBody,
|
|
5437
|
+
notifyApplicationBeforeRender(newBody, options) {
|
|
4949
5438
|
return dispatch("turbo:before-render", {
|
|
4950
|
-
detail: { newBody,
|
|
5439
|
+
detail: { newBody, ...options },
|
|
4951
5440
|
cancelable: true
|
|
4952
5441
|
})
|
|
4953
5442
|
}
|
|
4954
5443
|
|
|
4955
|
-
notifyApplicationAfterRender(
|
|
4956
|
-
return dispatch("turbo:render", { detail: {
|
|
5444
|
+
notifyApplicationAfterRender(renderMethod) {
|
|
5445
|
+
return dispatch("turbo:render", { detail: { renderMethod } })
|
|
4957
5446
|
}
|
|
4958
5447
|
|
|
4959
5448
|
notifyApplicationAfterPageLoad(timing = {}) {
|
|
@@ -5055,7 +5544,7 @@ const deprecatedLocationPropertyDescriptors = {
|
|
|
5055
5544
|
}
|
|
5056
5545
|
};
|
|
5057
5546
|
|
|
5058
|
-
const session = new Session();
|
|
5547
|
+
const session = new Session(recentRequests);
|
|
5059
5548
|
const { cache, navigator: navigator$1 } = session;
|
|
5060
5549
|
|
|
5061
5550
|
/**
|
|
@@ -5165,7 +5654,7 @@ var Turbo = /*#__PURE__*/Object.freeze({
|
|
|
5165
5654
|
PageRenderer: PageRenderer,
|
|
5166
5655
|
PageSnapshot: PageSnapshot,
|
|
5167
5656
|
FrameRenderer: FrameRenderer,
|
|
5168
|
-
fetch:
|
|
5657
|
+
fetch: fetchWithTurboHeaders,
|
|
5169
5658
|
start: start,
|
|
5170
5659
|
registerAdapter: registerAdapter,
|
|
5171
5660
|
visit: visit,
|
|
@@ -5413,7 +5902,7 @@ class FrameController {
|
|
|
5413
5902
|
|
|
5414
5903
|
// View delegate
|
|
5415
5904
|
|
|
5416
|
-
allowsImmediateRender({ element: newFrame },
|
|
5905
|
+
allowsImmediateRender({ element: newFrame }, options) {
|
|
5417
5906
|
const event = dispatch("turbo:before-frame-render", {
|
|
5418
5907
|
target: this.element,
|
|
5419
5908
|
detail: { newFrame, ...options },
|
|
@@ -6034,7 +6523,7 @@ if (customElements.get("turbo-stream-source") === undefined) {
|
|
|
6034
6523
|
}
|
|
6035
6524
|
})();
|
|
6036
6525
|
|
|
6037
|
-
window.Turbo = Turbo;
|
|
6526
|
+
window.Turbo = { ...Turbo, StreamActions };
|
|
6038
6527
|
start();
|
|
6039
6528
|
|
|
6040
|
-
export { FetchEnctype, FetchMethod, FetchRequest, FetchResponse, FrameElement, FrameLoadingStyle, FrameRenderer, PageRenderer, PageSnapshot, StreamActions, StreamElement, StreamSourceElement, cache, clearCache, connectStreamSource, disconnectStreamSource, fetch, fetchEnctypeFromString, fetchMethodFromString, isSafe, navigator$1 as navigator, registerAdapter, renderStreamMessage, session, setConfirmMethod, setFormMode, setProgressBarDelay, start, visit };
|
|
6529
|
+
export { FetchEnctype, FetchMethod, FetchRequest, FetchResponse, FrameElement, FrameLoadingStyle, FrameRenderer, PageRenderer, PageSnapshot, StreamActions, StreamElement, StreamSourceElement, cache, clearCache, connectStreamSource, disconnectStreamSource, fetchWithTurboHeaders as fetch, fetchEnctypeFromString, fetchMethodFromString, isSafe, navigator$1 as navigator, registerAdapter, renderStreamMessage, session, setConfirmMethod, setFormMode, setProgressBarDelay, start, visit };
|