turbo-rails 2.0.0.pre.beta.1 → 2.0.0.pre.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.
- checksums.yaml +4 -4
- data/README.md +3 -6
- data/app/assets/javascripts/turbo.js +848 -531
- data/app/assets/javascripts/turbo.min.js +7 -7
- data/app/assets/javascripts/turbo.min.js.map +1 -1
- data/app/helpers/turbo/drive_helper.rb +58 -7
- data/app/javascript/turbo/index.js +2 -0
- data/app/jobs/turbo/streams/broadcast_job.rb +1 -1
- data/app/models/concerns/turbo/broadcastable.rb +6 -4
- data/app/models/turbo/streams/tag_builder.rb +20 -0
- data/lib/install/turbo_with_importmap.rb +1 -1
- data/lib/turbo/version.rb +1 -1
- data/lib/turbo-rails.rb +1 -0
- metadata +2 -2
@@ -1,6 +1,6 @@
|
|
1
1
|
/*!
|
2
|
-
Turbo 8.0.0-beta.
|
3
|
-
Copyright ©
|
2
|
+
Turbo 8.0.0-beta.3
|
3
|
+
Copyright © 2024 37signals LLC
|
4
4
|
*/
|
5
5
|
(function(prototype) {
|
6
6
|
if (typeof prototype.requestSubmit == "function") return;
|
@@ -485,12 +485,57 @@ async function around(callback, reader) {
|
|
485
485
|
return [ before, after ];
|
486
486
|
}
|
487
487
|
|
488
|
-
function
|
488
|
+
function doesNotTargetIFrame(anchor) {
|
489
|
+
if (anchor.hasAttribute("target")) {
|
490
|
+
for (const element of document.getElementsByName(anchor.target)) {
|
491
|
+
if (element instanceof HTMLIFrameElement) return false;
|
492
|
+
}
|
493
|
+
}
|
494
|
+
return true;
|
495
|
+
}
|
496
|
+
|
497
|
+
function findLinkFromClickTarget(target) {
|
498
|
+
return findClosestRecursively(target, "a[href]:not([target^=_]):not([download])");
|
499
|
+
}
|
500
|
+
|
501
|
+
function getLocationForLink(link) {
|
502
|
+
return expandURL(link.getAttribute("href") || "");
|
503
|
+
}
|
504
|
+
|
505
|
+
function debounce(fn, delay) {
|
506
|
+
let timeoutId = null;
|
507
|
+
return (...args) => {
|
508
|
+
const callback = () => fn.apply(this, args);
|
509
|
+
clearTimeout(timeoutId);
|
510
|
+
timeoutId = setTimeout(callback, delay);
|
511
|
+
};
|
512
|
+
}
|
513
|
+
|
514
|
+
class LimitedSet extends Set {
|
515
|
+
constructor(maxSize) {
|
516
|
+
super();
|
517
|
+
this.maxSize = maxSize;
|
518
|
+
}
|
519
|
+
add(value) {
|
520
|
+
if (this.size >= this.maxSize) {
|
521
|
+
const iterator = this.values();
|
522
|
+
const oldestValue = iterator.next().value;
|
523
|
+
this.delete(oldestValue);
|
524
|
+
}
|
525
|
+
super.add(value);
|
526
|
+
}
|
527
|
+
}
|
528
|
+
|
529
|
+
const recentRequests = new LimitedSet(20);
|
530
|
+
|
531
|
+
const nativeFetch = window.fetch;
|
532
|
+
|
533
|
+
function fetchWithTurboHeaders(url, options = {}) {
|
489
534
|
const modifiedHeaders = new Headers(options.headers || {});
|
490
535
|
const requestUID = uuid();
|
491
|
-
|
536
|
+
recentRequests.add(requestUID);
|
492
537
|
modifiedHeaders.append("X-Turbo-Request-Id", requestUID);
|
493
|
-
return
|
538
|
+
return nativeFetch(url, {
|
494
539
|
...options,
|
495
540
|
headers: modifiedHeaders
|
496
541
|
});
|
@@ -606,10 +651,15 @@ class FetchRequest {
|
|
606
651
|
async perform() {
|
607
652
|
const {fetchOptions: fetchOptions} = this;
|
608
653
|
this.delegate.prepareRequest(this);
|
609
|
-
await this.#allowRequestToBeIntercepted(fetchOptions);
|
654
|
+
const event = await this.#allowRequestToBeIntercepted(fetchOptions);
|
610
655
|
try {
|
611
656
|
this.delegate.requestStarted(this);
|
612
|
-
|
657
|
+
if (event.detail.fetchRequest) {
|
658
|
+
this.response = event.detail.fetchRequest.response;
|
659
|
+
} else {
|
660
|
+
this.response = fetchWithTurboHeaders(this.url.href, fetchOptions);
|
661
|
+
}
|
662
|
+
const response = await this.response;
|
613
663
|
return await this.receive(response);
|
614
664
|
} catch (error) {
|
615
665
|
if (error.name !== "AbortError") {
|
@@ -667,6 +717,7 @@ class FetchRequest {
|
|
667
717
|
});
|
668
718
|
this.url = event.detail.url;
|
669
719
|
if (event.defaultPrevented) await requestInterception;
|
720
|
+
return event;
|
670
721
|
}
|
671
722
|
#willDelegateErrorHandling(error) {
|
672
723
|
const event = dispatch("turbo:fetch-request-error", {
|
@@ -762,6 +813,41 @@ function importStreamElements(fragment) {
|
|
762
813
|
return fragment;
|
763
814
|
}
|
764
815
|
|
816
|
+
const PREFETCH_DELAY = 100;
|
817
|
+
|
818
|
+
class PrefetchCache {
|
819
|
+
#prefetchTimeout=null;
|
820
|
+
#prefetched=null;
|
821
|
+
get(url) {
|
822
|
+
if (this.#prefetched && this.#prefetched.url === url && this.#prefetched.expire > Date.now()) {
|
823
|
+
return this.#prefetched.request;
|
824
|
+
}
|
825
|
+
}
|
826
|
+
setLater(url, request, ttl) {
|
827
|
+
this.clear();
|
828
|
+
this.#prefetchTimeout = setTimeout((() => {
|
829
|
+
request.perform();
|
830
|
+
this.set(url, request, ttl);
|
831
|
+
this.#prefetchTimeout = null;
|
832
|
+
}), PREFETCH_DELAY);
|
833
|
+
}
|
834
|
+
set(url, request, ttl) {
|
835
|
+
this.#prefetched = {
|
836
|
+
url: url,
|
837
|
+
request: request,
|
838
|
+
expire: new Date((new Date).getTime() + ttl)
|
839
|
+
};
|
840
|
+
}
|
841
|
+
clear() {
|
842
|
+
if (this.#prefetchTimeout) clearTimeout(this.#prefetchTimeout);
|
843
|
+
this.#prefetched = null;
|
844
|
+
}
|
845
|
+
}
|
846
|
+
|
847
|
+
const cacheTtl = 10 * 1e3;
|
848
|
+
|
849
|
+
const prefetchCache = new PrefetchCache;
|
850
|
+
|
765
851
|
const FormSubmissionState = {
|
766
852
|
initialized: "initialized",
|
767
853
|
requesting: "requesting",
|
@@ -848,6 +934,7 @@ class FormSubmission {
|
|
848
934
|
this.state = FormSubmissionState.waiting;
|
849
935
|
this.submitter?.setAttribute("disabled", "");
|
850
936
|
this.setSubmitsWith();
|
937
|
+
markAsBusy(this.formElement);
|
851
938
|
dispatch("turbo:submit-start", {
|
852
939
|
target: this.formElement,
|
853
940
|
detail: {
|
@@ -857,6 +944,7 @@ class FormSubmission {
|
|
857
944
|
this.delegate.formSubmissionStarted(this);
|
858
945
|
}
|
859
946
|
requestPreventedHandlingResponse(request, response) {
|
947
|
+
prefetchCache.clear();
|
860
948
|
this.result = {
|
861
949
|
success: response.succeeded,
|
862
950
|
fetchResponse: response
|
@@ -865,7 +953,10 @@ class FormSubmission {
|
|
865
953
|
requestSucceededWithResponse(request, response) {
|
866
954
|
if (response.clientError || response.serverError) {
|
867
955
|
this.delegate.formSubmissionFailedWithResponse(this, response);
|
868
|
-
|
956
|
+
return;
|
957
|
+
}
|
958
|
+
prefetchCache.clear();
|
959
|
+
if (this.requestMustRedirect(request) && responseSucceededWithoutRedirect(response)) {
|
869
960
|
const error = new Error("Form responses must redirect to another location");
|
870
961
|
this.delegate.formSubmissionErrored(this, error);
|
871
962
|
} else {
|
@@ -895,6 +986,7 @@ class FormSubmission {
|
|
895
986
|
this.state = FormSubmissionState.stopped;
|
896
987
|
this.submitter?.removeAttribute("disabled");
|
897
988
|
this.resetSubmitterText();
|
989
|
+
clearBusyState(this.formElement);
|
898
990
|
dispatch("turbo:submit-end", {
|
899
991
|
target: this.formElement,
|
900
992
|
detail: {
|
@@ -1147,7 +1239,7 @@ class View {
|
|
1147
1239
|
resume: this.#resolveInterceptionPromise,
|
1148
1240
|
render: this.renderer.renderElement
|
1149
1241
|
};
|
1150
|
-
const immediateRender = this.delegate.allowsImmediateRender(snapshot,
|
1242
|
+
const immediateRender = this.delegate.allowsImmediateRender(snapshot, options);
|
1151
1243
|
if (!immediateRender) await renderInterception;
|
1152
1244
|
await this.renderSnapshot(renderer);
|
1153
1245
|
this.delegate.viewRenderedSnapshot(snapshot, isPreview, this.renderer.renderMethod);
|
@@ -1176,6 +1268,12 @@ class View {
|
|
1176
1268
|
this.element.removeAttribute("data-turbo-preview");
|
1177
1269
|
}
|
1178
1270
|
}
|
1271
|
+
markVisitDirection(direction) {
|
1272
|
+
this.element.setAttribute("data-turbo-visit-direction", direction);
|
1273
|
+
}
|
1274
|
+
unmarkVisitDirection() {
|
1275
|
+
this.element.removeAttribute("data-turbo-visit-direction");
|
1276
|
+
}
|
1179
1277
|
async renderSnapshot(renderer) {
|
1180
1278
|
await renderer.render();
|
1181
1279
|
}
|
@@ -1259,9 +1357,9 @@ class LinkClickObserver {
|
|
1259
1357
|
clickBubbled=event => {
|
1260
1358
|
if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {
|
1261
1359
|
const target = event.composedPath && event.composedPath()[0] || event.target;
|
1262
|
-
const link =
|
1360
|
+
const link = findLinkFromClickTarget(target);
|
1263
1361
|
if (link && doesNotTargetIFrame(link)) {
|
1264
|
-
const location =
|
1362
|
+
const location = getLocationForLink(link);
|
1265
1363
|
if (this.delegate.willFollowLinkToLocation(link, location, event)) {
|
1266
1364
|
event.preventDefault();
|
1267
1365
|
this.delegate.followedLinkToLocation(link, location);
|
@@ -1272,23 +1370,6 @@ class LinkClickObserver {
|
|
1272
1370
|
clickEventIsSignificant(event) {
|
1273
1371
|
return !(event.target && event.target.isContentEditable || event.defaultPrevented || event.which > 1 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey);
|
1274
1372
|
}
|
1275
|
-
findLinkFromClickTarget(target) {
|
1276
|
-
return findClosestRecursively(target, "a[href]:not([target^=_]):not([download])");
|
1277
|
-
}
|
1278
|
-
getLocationForLink(link) {
|
1279
|
-
return expandURL(link.getAttribute("href") || "");
|
1280
|
-
}
|
1281
|
-
}
|
1282
|
-
|
1283
|
-
function doesNotTargetIFrame(anchor) {
|
1284
|
-
if (anchor.hasAttribute("target")) {
|
1285
|
-
for (const element of document.getElementsByName(anchor.target)) {
|
1286
|
-
if (element instanceof HTMLIFrameElement) return false;
|
1287
|
-
}
|
1288
|
-
return true;
|
1289
|
-
} else {
|
1290
|
-
return true;
|
1291
|
-
}
|
1292
1373
|
}
|
1293
1374
|
|
1294
1375
|
class FormLinkClickObserver {
|
@@ -1302,6 +1383,12 @@ class FormLinkClickObserver {
|
|
1302
1383
|
stop() {
|
1303
1384
|
this.linkInterceptor.stop();
|
1304
1385
|
}
|
1386
|
+
canPrefetchRequestToLocation(link, location) {
|
1387
|
+
return false;
|
1388
|
+
}
|
1389
|
+
prefetchAndCacheRequestToLocation(link, location) {
|
1390
|
+
return;
|
1391
|
+
}
|
1305
1392
|
willFollowLinkToLocation(link, location, originalEvent) {
|
1306
1393
|
return this.delegate.willSubmitFormLinkToLocation(link, location, originalEvent) && (link.hasAttribute("data-turbo-method") || link.hasAttribute("data-turbo-stream"));
|
1307
1394
|
}
|
@@ -1480,14 +1567,14 @@ class FrameRenderer extends Renderer {
|
|
1480
1567
|
return true;
|
1481
1568
|
}
|
1482
1569
|
async render() {
|
1483
|
-
await
|
1570
|
+
await nextRepaint();
|
1484
1571
|
this.preservingPermanentElements((() => {
|
1485
1572
|
this.loadFrameElement();
|
1486
1573
|
}));
|
1487
1574
|
this.scrollFrameIntoView();
|
1488
|
-
await
|
1575
|
+
await nextRepaint();
|
1489
1576
|
this.focusFirstAutofocusableElement();
|
1490
|
-
await
|
1577
|
+
await nextRepaint();
|
1491
1578
|
this.activateScriptElements();
|
1492
1579
|
}
|
1493
1580
|
loadFrameElement() {
|
@@ -1536,6 +1623,8 @@ function readScrollBehavior(value, defaultValue) {
|
|
1536
1623
|
}
|
1537
1624
|
}
|
1538
1625
|
|
1626
|
+
const ProgressBarID = "turbo-progress-bar";
|
1627
|
+
|
1539
1628
|
class ProgressBar {
|
1540
1629
|
static animationDuration=300;
|
1541
1630
|
static get defaultCSS() {
|
@@ -1623,6 +1712,8 @@ class ProgressBar {
|
|
1623
1712
|
}
|
1624
1713
|
createStylesheetElement() {
|
1625
1714
|
const element = document.createElement("style");
|
1715
|
+
element.id = ProgressBarID;
|
1716
|
+
element.setAttribute("data-turbo-permanent", "");
|
1626
1717
|
element.type = "text/css";
|
1627
1718
|
element.textContent = ProgressBar.defaultCSS;
|
1628
1719
|
if (this.cspNonce) {
|
@@ -1846,6 +1937,12 @@ const SystemStatusCode = {
|
|
1846
1937
|
contentTypeMismatch: -2
|
1847
1938
|
};
|
1848
1939
|
|
1940
|
+
const Direction = {
|
1941
|
+
advance: "forward",
|
1942
|
+
restore: "back",
|
1943
|
+
replace: "none"
|
1944
|
+
};
|
1945
|
+
|
1849
1946
|
class Visit {
|
1850
1947
|
identifier=uuid();
|
1851
1948
|
timingMetrics={};
|
@@ -1861,7 +1958,7 @@ class Visit {
|
|
1861
1958
|
this.delegate = delegate;
|
1862
1959
|
this.location = location;
|
1863
1960
|
this.restorationIdentifier = restorationIdentifier || uuid();
|
1864
|
-
const {action: action, historyChanged: historyChanged, referrer: referrer, snapshot: snapshot, snapshotHTML: snapshotHTML, response: response, visitCachedSnapshot: visitCachedSnapshot, willRender: willRender, updateHistory: updateHistory, shouldCacheSnapshot: shouldCacheSnapshot, acceptsStreamResponse: acceptsStreamResponse} = {
|
1961
|
+
const {action: action, historyChanged: historyChanged, referrer: referrer, snapshot: snapshot, snapshotHTML: snapshotHTML, response: response, visitCachedSnapshot: visitCachedSnapshot, willRender: willRender, updateHistory: updateHistory, shouldCacheSnapshot: shouldCacheSnapshot, acceptsStreamResponse: acceptsStreamResponse, direction: direction} = {
|
1865
1962
|
...defaultOptions,
|
1866
1963
|
...options
|
1867
1964
|
};
|
@@ -1878,6 +1975,7 @@ class Visit {
|
|
1878
1975
|
this.scrolled = !willRender;
|
1879
1976
|
this.shouldCacheSnapshot = shouldCacheSnapshot;
|
1880
1977
|
this.acceptsStreamResponse = acceptsStreamResponse;
|
1978
|
+
this.direction = direction || Direction[action];
|
1881
1979
|
}
|
1882
1980
|
get adapter() {
|
1883
1981
|
return this.delegate.adapter;
|
@@ -2098,7 +2196,7 @@ class Visit {
|
|
2098
2196
|
this.finishRequest();
|
2099
2197
|
}
|
2100
2198
|
performScroll() {
|
2101
|
-
if (!this.scrolled && !this.view.forceReloaded && !this.view.
|
2199
|
+
if (!this.scrolled && !this.view.forceReloaded && !this.view.shouldPreserveScrollPosition(this)) {
|
2102
2200
|
if (this.action == "restore") {
|
2103
2201
|
this.scrollToRestoredPosition() || this.scrollToAnchor() || this.view.scrollToTop();
|
2104
2202
|
} else {
|
@@ -2162,9 +2260,7 @@ class Visit {
|
|
2162
2260
|
}
|
2163
2261
|
async render(callback) {
|
2164
2262
|
this.cancelRender();
|
2165
|
-
await
|
2166
|
-
this.frame = requestAnimationFrame((() => resolve()));
|
2167
|
-
}));
|
2263
|
+
this.frame = await nextRepaint();
|
2168
2264
|
await callback();
|
2169
2265
|
delete this.frame;
|
2170
2266
|
}
|
@@ -2386,6 +2482,7 @@ class History {
|
|
2386
2482
|
restorationData={};
|
2387
2483
|
started=false;
|
2388
2484
|
pageLoaded=false;
|
2485
|
+
currentIndex=0;
|
2389
2486
|
constructor(delegate) {
|
2390
2487
|
this.delegate = delegate;
|
2391
2488
|
}
|
@@ -2393,6 +2490,7 @@ class History {
|
|
2393
2490
|
if (!this.started) {
|
2394
2491
|
addEventListener("popstate", this.onPopState, false);
|
2395
2492
|
addEventListener("load", this.onPageLoad, false);
|
2493
|
+
this.currentIndex = history.state?.turbo?.restorationIndex || 0;
|
2396
2494
|
this.started = true;
|
2397
2495
|
this.replace(new URL(window.location.href));
|
2398
2496
|
}
|
@@ -2411,9 +2509,11 @@ class History {
|
|
2411
2509
|
this.update(history.replaceState, location, restorationIdentifier);
|
2412
2510
|
}
|
2413
2511
|
update(method, location, restorationIdentifier = uuid()) {
|
2512
|
+
if (method === history.pushState) ++this.currentIndex;
|
2414
2513
|
const state = {
|
2415
2514
|
turbo: {
|
2416
|
-
restorationIdentifier: restorationIdentifier
|
2515
|
+
restorationIdentifier: restorationIdentifier,
|
2516
|
+
restorationIndex: this.currentIndex
|
2417
2517
|
}
|
2418
2518
|
};
|
2419
2519
|
method.call(history, state, "", location.href);
|
@@ -2448,9 +2548,11 @@ class History {
|
|
2448
2548
|
const {turbo: turbo} = event.state || {};
|
2449
2549
|
if (turbo) {
|
2450
2550
|
this.location = new URL(window.location.href);
|
2451
|
-
const {restorationIdentifier: restorationIdentifier} = turbo;
|
2551
|
+
const {restorationIdentifier: restorationIdentifier, restorationIndex: restorationIndex} = turbo;
|
2452
2552
|
this.restorationIdentifier = restorationIdentifier;
|
2453
|
-
this.
|
2553
|
+
const direction = restorationIndex > this.currentIndex ? "forward" : "back";
|
2554
|
+
this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(this.location, restorationIdentifier, direction);
|
2555
|
+
this.currentIndex = restorationIndex;
|
2454
2556
|
}
|
2455
2557
|
}
|
2456
2558
|
};
|
@@ -2466,6 +2568,131 @@ class History {
|
|
2466
2568
|
}
|
2467
2569
|
}
|
2468
2570
|
|
2571
|
+
class LinkPrefetchObserver {
|
2572
|
+
started=false;
|
2573
|
+
hoverTriggerEvent="mouseenter";
|
2574
|
+
touchTriggerEvent="touchstart";
|
2575
|
+
constructor(delegate, eventTarget) {
|
2576
|
+
this.delegate = delegate;
|
2577
|
+
this.eventTarget = eventTarget;
|
2578
|
+
}
|
2579
|
+
start() {
|
2580
|
+
if (this.started) return;
|
2581
|
+
if (this.eventTarget.readyState === "loading") {
|
2582
|
+
this.eventTarget.addEventListener("DOMContentLoaded", this.#enable, {
|
2583
|
+
once: true
|
2584
|
+
});
|
2585
|
+
} else {
|
2586
|
+
this.#enable();
|
2587
|
+
}
|
2588
|
+
}
|
2589
|
+
stop() {
|
2590
|
+
if (!this.started) return;
|
2591
|
+
this.eventTarget.removeEventListener(this.hoverTriggerEvent, this.#tryToPrefetchRequest, {
|
2592
|
+
capture: true,
|
2593
|
+
passive: true
|
2594
|
+
});
|
2595
|
+
this.eventTarget.removeEventListener(this.touchTriggerEvent, this.#tryToPrefetchRequest, {
|
2596
|
+
capture: true,
|
2597
|
+
passive: true
|
2598
|
+
});
|
2599
|
+
this.eventTarget.removeEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true);
|
2600
|
+
this.started = false;
|
2601
|
+
}
|
2602
|
+
#enable=() => {
|
2603
|
+
this.eventTarget.addEventListener(this.hoverTriggerEvent, this.#tryToPrefetchRequest, {
|
2604
|
+
capture: true,
|
2605
|
+
passive: true
|
2606
|
+
});
|
2607
|
+
this.eventTarget.addEventListener(this.touchTriggerEvent, this.#tryToPrefetchRequest, {
|
2608
|
+
capture: true,
|
2609
|
+
passive: true
|
2610
|
+
});
|
2611
|
+
this.eventTarget.addEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true);
|
2612
|
+
this.started = true;
|
2613
|
+
};
|
2614
|
+
#tryToPrefetchRequest=event => {
|
2615
|
+
if (getMetaContent("turbo-prefetch") !== "true") return;
|
2616
|
+
const target = event.target;
|
2617
|
+
const isLink = target.matches && target.matches("a[href]:not([target^=_]):not([download])");
|
2618
|
+
if (isLink && this.#isPrefetchable(target)) {
|
2619
|
+
const link = target;
|
2620
|
+
const location = getLocationForLink(link);
|
2621
|
+
if (this.delegate.canPrefetchRequestToLocation(link, location)) {
|
2622
|
+
const fetchRequest = new FetchRequest(this, FetchMethod.get, location, new URLSearchParams, target);
|
2623
|
+
prefetchCache.setLater(location.toString(), fetchRequest, this.#cacheTtl);
|
2624
|
+
link.addEventListener("mouseleave", (() => prefetchCache.clear()), {
|
2625
|
+
once: true
|
2626
|
+
});
|
2627
|
+
}
|
2628
|
+
}
|
2629
|
+
};
|
2630
|
+
#tryToUsePrefetchedRequest=event => {
|
2631
|
+
if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "get") {
|
2632
|
+
const cached = prefetchCache.get(event.detail.url.toString());
|
2633
|
+
if (cached) {
|
2634
|
+
event.detail.fetchRequest = cached;
|
2635
|
+
}
|
2636
|
+
prefetchCache.clear();
|
2637
|
+
}
|
2638
|
+
};
|
2639
|
+
prepareRequest(request) {
|
2640
|
+
const link = request.target;
|
2641
|
+
request.headers["Sec-Purpose"] = "prefetch";
|
2642
|
+
if (link.dataset.turboFrame && link.dataset.turboFrame !== "_top") {
|
2643
|
+
request.headers["Turbo-Frame"] = link.dataset.turboFrame;
|
2644
|
+
} else if (link.dataset.turboFrame !== "_top") {
|
2645
|
+
const turboFrame = link.closest("turbo-frame");
|
2646
|
+
if (turboFrame) {
|
2647
|
+
request.headers["Turbo-Frame"] = turboFrame.id;
|
2648
|
+
}
|
2649
|
+
}
|
2650
|
+
if (link.hasAttribute("data-turbo-stream")) {
|
2651
|
+
request.acceptResponseType("text/vnd.turbo-stream.html");
|
2652
|
+
}
|
2653
|
+
}
|
2654
|
+
requestSucceededWithResponse() {}
|
2655
|
+
requestStarted(fetchRequest) {}
|
2656
|
+
requestErrored(fetchRequest) {}
|
2657
|
+
requestFinished(fetchRequest) {}
|
2658
|
+
requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}
|
2659
|
+
requestFailedWithResponse(fetchRequest, fetchResponse) {}
|
2660
|
+
get #cacheTtl() {
|
2661
|
+
return Number(getMetaContent("turbo-prefetch-cache-time")) || cacheTtl;
|
2662
|
+
}
|
2663
|
+
#isPrefetchable(link) {
|
2664
|
+
const href = link.getAttribute("href");
|
2665
|
+
if (!href || href === "#" || link.dataset.turbo === "false" || link.dataset.turboPrefetch === "false") {
|
2666
|
+
return false;
|
2667
|
+
}
|
2668
|
+
if (link.origin !== document.location.origin) {
|
2669
|
+
return false;
|
2670
|
+
}
|
2671
|
+
if (![ "http:", "https:" ].includes(link.protocol)) {
|
2672
|
+
return false;
|
2673
|
+
}
|
2674
|
+
if (link.pathname + link.search === document.location.pathname + document.location.search) {
|
2675
|
+
return false;
|
2676
|
+
}
|
2677
|
+
if (link.dataset.turboMethod && link.dataset.turboMethod !== "get") {
|
2678
|
+
return false;
|
2679
|
+
}
|
2680
|
+
if (targetsIframe(link)) {
|
2681
|
+
return false;
|
2682
|
+
}
|
2683
|
+
if (link.pathname + link.search === document.location.pathname + document.location.search) {
|
2684
|
+
return false;
|
2685
|
+
}
|
2686
|
+
const turboPrefetchParent = findClosestRecursively(link, "[data-turbo-prefetch]");
|
2687
|
+
if (turboPrefetchParent && turboPrefetchParent.dataset.turboPrefetch === "false") {
|
2688
|
+
return false;
|
2689
|
+
}
|
2690
|
+
return true;
|
2691
|
+
}
|
2692
|
+
}
|
2693
|
+
|
2694
|
+
const targetsIframe = link => !doesNotTargetIFrame(link);
|
2695
|
+
|
2469
2696
|
class Navigator {
|
2470
2697
|
constructor(delegate) {
|
2471
2698
|
this.delegate = delegate;
|
@@ -2725,7 +2952,7 @@ async function withAutofocusFromFragment(fragment, callback) {
|
|
2725
2952
|
elementWithAutofocus.id = willAutofocusId;
|
2726
2953
|
}
|
2727
2954
|
callback();
|
2728
|
-
await
|
2955
|
+
await nextRepaint();
|
2729
2956
|
const hasNoActiveElement = document.activeElement == null || document.activeElement == document.body;
|
2730
2957
|
if (hasNoActiveElement && willAutofocusId) {
|
2731
2958
|
const elementToAutofocus = document.getElementById(willAutofocusId);
|
@@ -2856,504 +3083,545 @@ class ErrorRenderer extends Renderer {
|
|
2856
3083
|
}
|
2857
3084
|
}
|
2858
3085
|
|
2859
|
-
|
2860
|
-
|
2861
|
-
|
2862
|
-
|
2863
|
-
|
2864
|
-
|
2865
|
-
|
2866
|
-
|
2867
|
-
|
2868
|
-
|
2869
|
-
|
2870
|
-
|
2871
|
-
}
|
2872
|
-
|
2873
|
-
|
2874
|
-
|
2875
|
-
|
2876
|
-
|
2877
|
-
|
2878
|
-
|
2879
|
-
|
2880
|
-
|
2881
|
-
|
2882
|
-
|
2883
|
-
|
2884
|
-
|
3086
|
+
var Idiomorph = function() {
|
3087
|
+
let EMPTY_SET = new Set;
|
3088
|
+
let defaults = {
|
3089
|
+
morphStyle: "outerHTML",
|
3090
|
+
callbacks: {
|
3091
|
+
beforeNodeAdded: noOp,
|
3092
|
+
afterNodeAdded: noOp,
|
3093
|
+
beforeNodeMorphed: noOp,
|
3094
|
+
afterNodeMorphed: noOp,
|
3095
|
+
beforeNodeRemoved: noOp,
|
3096
|
+
afterNodeRemoved: noOp,
|
3097
|
+
beforeAttributeUpdated: noOp
|
3098
|
+
},
|
3099
|
+
head: {
|
3100
|
+
style: "merge",
|
3101
|
+
shouldPreserve: function(elt) {
|
3102
|
+
return elt.getAttribute("im-preserve") === "true";
|
3103
|
+
},
|
3104
|
+
shouldReAppend: function(elt) {
|
3105
|
+
return elt.getAttribute("im-re-append") === "true";
|
3106
|
+
},
|
3107
|
+
shouldRemove: noOp,
|
3108
|
+
afterHeadMorphed: noOp
|
3109
|
+
}
|
3110
|
+
};
|
3111
|
+
function morph(oldNode, newContent, config = {}) {
|
3112
|
+
if (oldNode instanceof Document) {
|
3113
|
+
oldNode = oldNode.documentElement;
|
3114
|
+
}
|
3115
|
+
if (typeof newContent === "string") {
|
3116
|
+
newContent = parseContent(newContent);
|
3117
|
+
}
|
3118
|
+
let normalizedContent = normalizeContent(newContent);
|
3119
|
+
let ctx = createMorphContext(oldNode, normalizedContent, config);
|
3120
|
+
return morphNormalizedContent(oldNode, normalizedContent, ctx);
|
3121
|
+
}
|
3122
|
+
function morphNormalizedContent(oldNode, normalizedNewContent, ctx) {
|
3123
|
+
if (ctx.head.block) {
|
3124
|
+
let oldHead = oldNode.querySelector("head");
|
3125
|
+
let newHead = normalizedNewContent.querySelector("head");
|
3126
|
+
if (oldHead && newHead) {
|
3127
|
+
let promises = handleHeadElement(newHead, oldHead, ctx);
|
3128
|
+
Promise.all(promises).then((function() {
|
3129
|
+
morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, {
|
3130
|
+
head: {
|
3131
|
+
block: false,
|
3132
|
+
ignore: true
|
3133
|
+
}
|
3134
|
+
}));
|
2885
3135
|
}));
|
2886
|
-
|
2887
|
-
|
3136
|
+
return;
|
3137
|
+
}
|
2888
3138
|
}
|
2889
|
-
|
2890
|
-
|
2891
|
-
|
2892
|
-
|
2893
|
-
|
2894
|
-
|
2895
|
-
|
2896
|
-
|
2897
|
-
|
2898
|
-
|
2899
|
-
|
3139
|
+
if (ctx.morphStyle === "innerHTML") {
|
3140
|
+
morphChildren(normalizedNewContent, oldNode, ctx);
|
3141
|
+
return oldNode.children;
|
3142
|
+
} else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) {
|
3143
|
+
let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx);
|
3144
|
+
let previousSibling = bestMatch?.previousSibling;
|
3145
|
+
let nextSibling = bestMatch?.nextSibling;
|
3146
|
+
let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx);
|
3147
|
+
if (bestMatch) {
|
3148
|
+
return insertSiblings(previousSibling, morphedNode, nextSibling);
|
3149
|
+
} else {
|
3150
|
+
return [];
|
3151
|
+
}
|
2900
3152
|
} else {
|
2901
|
-
|
3153
|
+
throw "Do not understand how to morph style " + ctx.morphStyle;
|
2902
3154
|
}
|
2903
|
-
} else {
|
2904
|
-
throw "Do not understand how to morph style " + ctx.morphStyle;
|
2905
3155
|
}
|
2906
|
-
|
2907
|
-
|
2908
|
-
function morphOldNodeTo(oldNode, newContent, ctx) {
|
2909
|
-
if (ctx.ignoreActive && oldNode === document.activeElement) ; else if (newContent == null) {
|
2910
|
-
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return;
|
2911
|
-
oldNode.remove();
|
2912
|
-
ctx.callbacks.afterNodeRemoved(oldNode);
|
2913
|
-
return null;
|
2914
|
-
} else if (!isSoftMatch(oldNode, newContent)) {
|
2915
|
-
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return;
|
2916
|
-
if (ctx.callbacks.beforeNodeAdded(newContent) === false) return;
|
2917
|
-
oldNode.parentElement.replaceChild(newContent, oldNode);
|
2918
|
-
ctx.callbacks.afterNodeAdded(newContent);
|
2919
|
-
ctx.callbacks.afterNodeRemoved(oldNode);
|
2920
|
-
return newContent;
|
2921
|
-
} else {
|
2922
|
-
if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return;
|
2923
|
-
if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) ; else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") {
|
2924
|
-
handleHeadElement(newContent, oldNode, ctx);
|
2925
|
-
} else {
|
2926
|
-
syncNodeFrom(newContent, oldNode);
|
2927
|
-
morphChildren(newContent, oldNode, ctx);
|
2928
|
-
}
|
2929
|
-
ctx.callbacks.afterNodeMorphed(oldNode, newContent);
|
2930
|
-
return oldNode;
|
3156
|
+
function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
|
3157
|
+
return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement;
|
2931
3158
|
}
|
2932
|
-
|
2933
|
-
|
2934
|
-
|
2935
|
-
|
2936
|
-
|
2937
|
-
|
2938
|
-
|
2939
|
-
|
2940
|
-
|
2941
|
-
|
3159
|
+
function morphOldNodeTo(oldNode, newContent, ctx) {
|
3160
|
+
if (ctx.ignoreActive && oldNode === document.activeElement) ; else if (newContent == null) {
|
3161
|
+
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
|
3162
|
+
oldNode.remove();
|
3163
|
+
ctx.callbacks.afterNodeRemoved(oldNode);
|
3164
|
+
return null;
|
3165
|
+
} else if (!isSoftMatch(oldNode, newContent)) {
|
3166
|
+
if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
|
3167
|
+
if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode;
|
3168
|
+
oldNode.parentElement.replaceChild(newContent, oldNode);
|
3169
|
+
ctx.callbacks.afterNodeAdded(newContent);
|
3170
|
+
ctx.callbacks.afterNodeRemoved(oldNode);
|
3171
|
+
return newContent;
|
3172
|
+
} else {
|
3173
|
+
if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return oldNode;
|
3174
|
+
if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) ; else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") {
|
3175
|
+
handleHeadElement(newContent, oldNode, ctx);
|
3176
|
+
} else {
|
3177
|
+
syncNodeFrom(newContent, oldNode, ctx);
|
3178
|
+
if (!ignoreValueOfActiveElement(oldNode, ctx)) {
|
3179
|
+
morphChildren(newContent, oldNode, ctx);
|
3180
|
+
}
|
3181
|
+
}
|
3182
|
+
ctx.callbacks.afterNodeMorphed(oldNode, newContent);
|
3183
|
+
return oldNode;
|
3184
|
+
}
|
3185
|
+
}
|
3186
|
+
function morphChildren(newParent, oldParent, ctx) {
|
3187
|
+
let nextNewChild = newParent.firstChild;
|
3188
|
+
let insertionPoint = oldParent.firstChild;
|
3189
|
+
let newChild;
|
3190
|
+
while (nextNewChild) {
|
3191
|
+
newChild = nextNewChild;
|
3192
|
+
nextNewChild = newChild.nextSibling;
|
3193
|
+
if (insertionPoint == null) {
|
3194
|
+
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
3195
|
+
oldParent.appendChild(newChild);
|
3196
|
+
ctx.callbacks.afterNodeAdded(newChild);
|
3197
|
+
removeIdsFromConsideration(ctx, newChild);
|
3198
|
+
continue;
|
3199
|
+
}
|
3200
|
+
if (isIdSetMatch(newChild, insertionPoint, ctx)) {
|
3201
|
+
morphOldNodeTo(insertionPoint, newChild, ctx);
|
3202
|
+
insertionPoint = insertionPoint.nextSibling;
|
3203
|
+
removeIdsFromConsideration(ctx, newChild);
|
3204
|
+
continue;
|
3205
|
+
}
|
3206
|
+
let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx);
|
3207
|
+
if (idSetMatch) {
|
3208
|
+
insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx);
|
3209
|
+
morphOldNodeTo(idSetMatch, newChild, ctx);
|
3210
|
+
removeIdsFromConsideration(ctx, newChild);
|
3211
|
+
continue;
|
3212
|
+
}
|
3213
|
+
let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx);
|
3214
|
+
if (softMatch) {
|
3215
|
+
insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx);
|
3216
|
+
morphOldNodeTo(softMatch, newChild, ctx);
|
3217
|
+
removeIdsFromConsideration(ctx, newChild);
|
3218
|
+
continue;
|
3219
|
+
}
|
2942
3220
|
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
2943
|
-
oldParent.
|
3221
|
+
oldParent.insertBefore(newChild, insertionPoint);
|
2944
3222
|
ctx.callbacks.afterNodeAdded(newChild);
|
2945
3223
|
removeIdsFromConsideration(ctx, newChild);
|
2946
|
-
continue;
|
2947
3224
|
}
|
2948
|
-
|
2949
|
-
|
3225
|
+
while (insertionPoint !== null) {
|
3226
|
+
let tempNode = insertionPoint;
|
2950
3227
|
insertionPoint = insertionPoint.nextSibling;
|
2951
|
-
|
2952
|
-
continue;
|
3228
|
+
removeNode(tempNode, ctx);
|
2953
3229
|
}
|
2954
|
-
let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx);
|
2955
|
-
if (idSetMatch) {
|
2956
|
-
insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx);
|
2957
|
-
morphOldNodeTo(idSetMatch, newChild, ctx);
|
2958
|
-
removeIdsFromConsideration(ctx, newChild);
|
2959
|
-
continue;
|
2960
|
-
}
|
2961
|
-
let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx);
|
2962
|
-
if (softMatch) {
|
2963
|
-
insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx);
|
2964
|
-
morphOldNodeTo(softMatch, newChild, ctx);
|
2965
|
-
removeIdsFromConsideration(ctx, newChild);
|
2966
|
-
continue;
|
2967
|
-
}
|
2968
|
-
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
|
2969
|
-
oldParent.insertBefore(newChild, insertionPoint);
|
2970
|
-
ctx.callbacks.afterNodeAdded(newChild);
|
2971
|
-
removeIdsFromConsideration(ctx, newChild);
|
2972
3230
|
}
|
2973
|
-
|
2974
|
-
|
2975
|
-
|
2976
|
-
|
3231
|
+
function ignoreAttribute(attr, to, updateType, ctx) {
|
3232
|
+
if (attr === "value" && ctx.ignoreActiveValue && to === document.activeElement) {
|
3233
|
+
return true;
|
3234
|
+
}
|
3235
|
+
return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false;
|
2977
3236
|
}
|
2978
|
-
|
2979
|
-
|
2980
|
-
|
2981
|
-
|
2982
|
-
|
2983
|
-
|
2984
|
-
|
2985
|
-
|
2986
|
-
|
2987
|
-
to.
|
3237
|
+
function syncNodeFrom(from, to, ctx) {
|
3238
|
+
let type = from.nodeType;
|
3239
|
+
if (type === 1) {
|
3240
|
+
const fromAttributes = from.attributes;
|
3241
|
+
const toAttributes = to.attributes;
|
3242
|
+
for (const fromAttribute of fromAttributes) {
|
3243
|
+
if (ignoreAttribute(fromAttribute.name, to, "update", ctx)) {
|
3244
|
+
continue;
|
3245
|
+
}
|
3246
|
+
if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) {
|
3247
|
+
to.setAttribute(fromAttribute.name, fromAttribute.value);
|
3248
|
+
}
|
3249
|
+
}
|
3250
|
+
for (let i = toAttributes.length - 1; 0 <= i; i--) {
|
3251
|
+
const toAttribute = toAttributes[i];
|
3252
|
+
if (ignoreAttribute(toAttribute.name, to, "remove", ctx)) {
|
3253
|
+
continue;
|
3254
|
+
}
|
3255
|
+
if (!from.hasAttribute(toAttribute.name)) {
|
3256
|
+
to.removeAttribute(toAttribute.name);
|
3257
|
+
}
|
2988
3258
|
}
|
2989
3259
|
}
|
2990
|
-
|
2991
|
-
if (
|
2992
|
-
to.
|
3260
|
+
if (type === 8 || type === 3) {
|
3261
|
+
if (to.nodeValue !== from.nodeValue) {
|
3262
|
+
to.nodeValue = from.nodeValue;
|
2993
3263
|
}
|
2994
3264
|
}
|
2995
|
-
|
2996
|
-
|
2997
|
-
if (to.nodeValue !== from.nodeValue) {
|
2998
|
-
to.nodeValue = from.nodeValue;
|
3265
|
+
if (!ignoreValueOfActiveElement(to, ctx)) {
|
3266
|
+
syncInputValue(from, to, ctx);
|
2999
3267
|
}
|
3000
3268
|
}
|
3001
|
-
|
3002
|
-
|
3003
|
-
|
3004
|
-
|
3005
|
-
|
3006
|
-
|
3007
|
-
|
3008
|
-
|
3009
|
-
|
3010
|
-
|
3011
|
-
|
3012
|
-
|
3013
|
-
|
3014
|
-
|
3015
|
-
|
3269
|
+
function syncBooleanAttribute(from, to, attributeName, ctx) {
|
3270
|
+
if (from[attributeName] !== to[attributeName]) {
|
3271
|
+
let ignoreUpdate = ignoreAttribute(attributeName, to, "update", ctx);
|
3272
|
+
if (!ignoreUpdate) {
|
3273
|
+
to[attributeName] = from[attributeName];
|
3274
|
+
}
|
3275
|
+
if (from[attributeName]) {
|
3276
|
+
if (!ignoreUpdate) {
|
3277
|
+
to.setAttribute(attributeName, from[attributeName]);
|
3278
|
+
}
|
3279
|
+
} else {
|
3280
|
+
if (!ignoreAttribute(attributeName, to, "remove", ctx)) {
|
3281
|
+
to.removeAttribute(attributeName);
|
3282
|
+
}
|
3283
|
+
}
|
3016
3284
|
}
|
3017
3285
|
}
|
3018
|
-
|
3019
|
-
|
3020
|
-
|
3021
|
-
|
3022
|
-
|
3023
|
-
|
3024
|
-
|
3025
|
-
|
3286
|
+
function syncInputValue(from, to, ctx) {
|
3287
|
+
if (from instanceof HTMLInputElement && to instanceof HTMLInputElement && from.type !== "file") {
|
3288
|
+
let fromValue = from.value;
|
3289
|
+
let toValue = to.value;
|
3290
|
+
syncBooleanAttribute(from, to, "checked", ctx);
|
3291
|
+
syncBooleanAttribute(from, to, "disabled", ctx);
|
3292
|
+
if (!from.hasAttribute("value")) {
|
3293
|
+
if (!ignoreAttribute("value", to, "remove", ctx)) {
|
3294
|
+
to.value = "";
|
3295
|
+
to.removeAttribute("value");
|
3296
|
+
}
|
3297
|
+
} else if (fromValue !== toValue) {
|
3298
|
+
if (!ignoreAttribute("value", to, "update", ctx)) {
|
3299
|
+
to.setAttribute("value", fromValue);
|
3300
|
+
to.value = fromValue;
|
3301
|
+
}
|
3302
|
+
}
|
3303
|
+
} else if (from instanceof HTMLOptionElement) {
|
3304
|
+
syncBooleanAttribute(from, to, "selected", ctx);
|
3305
|
+
} else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) {
|
3306
|
+
let fromValue = from.value;
|
3307
|
+
let toValue = to.value;
|
3308
|
+
if (ignoreAttribute("value", to, "update", ctx)) {
|
3309
|
+
return;
|
3310
|
+
}
|
3311
|
+
if (fromValue !== toValue) {
|
3312
|
+
to.value = fromValue;
|
3313
|
+
}
|
3314
|
+
if (to.firstChild && to.firstChild.nodeValue !== fromValue) {
|
3315
|
+
to.firstChild.nodeValue = fromValue;
|
3316
|
+
}
|
3026
3317
|
}
|
3027
3318
|
}
|
3028
|
-
|
3029
|
-
|
3030
|
-
|
3031
|
-
|
3032
|
-
|
3033
|
-
|
3034
|
-
|
3035
|
-
|
3036
|
-
|
3037
|
-
|
3038
|
-
|
3039
|
-
|
3040
|
-
|
3041
|
-
|
3042
|
-
|
3043
|
-
let isPreserved = ctx.head.shouldPreserve(currentHeadElt);
|
3044
|
-
if (inNewContent || isPreserved) {
|
3045
|
-
if (isReAppended) {
|
3046
|
-
removed.push(currentHeadElt);
|
3047
|
-
} else {
|
3048
|
-
srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
|
3049
|
-
preserved.push(currentHeadElt);
|
3050
|
-
}
|
3051
|
-
} else {
|
3052
|
-
if (headMergeStyle === "append") {
|
3319
|
+
function handleHeadElement(newHeadTag, currentHead, ctx) {
|
3320
|
+
let added = [];
|
3321
|
+
let removed = [];
|
3322
|
+
let preserved = [];
|
3323
|
+
let nodesToAppend = [];
|
3324
|
+
let headMergeStyle = ctx.head.style;
|
3325
|
+
let srcToNewHeadNodes = new Map;
|
3326
|
+
for (const newHeadChild of newHeadTag.children) {
|
3327
|
+
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
|
3328
|
+
}
|
3329
|
+
for (const currentHeadElt of currentHead.children) {
|
3330
|
+
let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
|
3331
|
+
let isReAppended = ctx.head.shouldReAppend(currentHeadElt);
|
3332
|
+
let isPreserved = ctx.head.shouldPreserve(currentHeadElt);
|
3333
|
+
if (inNewContent || isPreserved) {
|
3053
3334
|
if (isReAppended) {
|
3054
3335
|
removed.push(currentHeadElt);
|
3055
|
-
|
3336
|
+
} else {
|
3337
|
+
srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
|
3338
|
+
preserved.push(currentHeadElt);
|
3056
3339
|
}
|
3057
3340
|
} else {
|
3058
|
-
if (
|
3059
|
-
|
3341
|
+
if (headMergeStyle === "append") {
|
3342
|
+
if (isReAppended) {
|
3343
|
+
removed.push(currentHeadElt);
|
3344
|
+
nodesToAppend.push(currentHeadElt);
|
3345
|
+
}
|
3346
|
+
} else {
|
3347
|
+
if (ctx.head.shouldRemove(currentHeadElt) !== false) {
|
3348
|
+
removed.push(currentHeadElt);
|
3349
|
+
}
|
3060
3350
|
}
|
3061
3351
|
}
|
3062
3352
|
}
|
3063
|
-
|
3064
|
-
|
3065
|
-
|
3066
|
-
|
3067
|
-
|
3068
|
-
|
3069
|
-
|
3070
|
-
|
3071
|
-
|
3072
|
-
|
3073
|
-
|
3074
|
-
|
3075
|
-
|
3076
|
-
|
3077
|
-
|
3353
|
+
nodesToAppend.push(...srcToNewHeadNodes.values());
|
3354
|
+
let promises = [];
|
3355
|
+
for (const newNode of nodesToAppend) {
|
3356
|
+
let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild;
|
3357
|
+
if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
|
3358
|
+
if (newElt.href || newElt.src) {
|
3359
|
+
let resolve = null;
|
3360
|
+
let promise = new Promise((function(_resolve) {
|
3361
|
+
resolve = _resolve;
|
3362
|
+
}));
|
3363
|
+
newElt.addEventListener("load", (function() {
|
3364
|
+
resolve();
|
3365
|
+
}));
|
3366
|
+
promises.push(promise);
|
3367
|
+
}
|
3368
|
+
currentHead.appendChild(newElt);
|
3369
|
+
ctx.callbacks.afterNodeAdded(newElt);
|
3370
|
+
added.push(newElt);
|
3078
3371
|
}
|
3079
|
-
currentHead.appendChild(newElt);
|
3080
|
-
ctx.callbacks.afterNodeAdded(newElt);
|
3081
|
-
added.push(newElt);
|
3082
3372
|
}
|
3083
|
-
|
3084
|
-
|
3085
|
-
|
3086
|
-
|
3087
|
-
|
3373
|
+
for (const removedElement of removed) {
|
3374
|
+
if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
|
3375
|
+
currentHead.removeChild(removedElement);
|
3376
|
+
ctx.callbacks.afterNodeRemoved(removedElement);
|
3377
|
+
}
|
3088
3378
|
}
|
3379
|
+
ctx.head.afterHeadMorphed(currentHead, {
|
3380
|
+
added: added,
|
3381
|
+
kept: preserved,
|
3382
|
+
removed: removed
|
3383
|
+
});
|
3384
|
+
return promises;
|
3385
|
+
}
|
3386
|
+
function noOp() {}
|
3387
|
+
function mergeDefaults(config) {
|
3388
|
+
let finalConfig = {};
|
3389
|
+
Object.assign(finalConfig, defaults);
|
3390
|
+
Object.assign(finalConfig, config);
|
3391
|
+
finalConfig.callbacks = {};
|
3392
|
+
Object.assign(finalConfig.callbacks, defaults.callbacks);
|
3393
|
+
Object.assign(finalConfig.callbacks, config.callbacks);
|
3394
|
+
finalConfig.head = {};
|
3395
|
+
Object.assign(finalConfig.head, defaults.head);
|
3396
|
+
Object.assign(finalConfig.head, config.head);
|
3397
|
+
return finalConfig;
|
3398
|
+
}
|
3399
|
+
function createMorphContext(oldNode, newContent, config) {
|
3400
|
+
config = mergeDefaults(config);
|
3401
|
+
return {
|
3402
|
+
target: oldNode,
|
3403
|
+
newContent: newContent,
|
3404
|
+
config: config,
|
3405
|
+
morphStyle: config.morphStyle,
|
3406
|
+
ignoreActive: config.ignoreActive,
|
3407
|
+
ignoreActiveValue: config.ignoreActiveValue,
|
3408
|
+
idMap: createIdMap(oldNode, newContent),
|
3409
|
+
deadIds: new Set,
|
3410
|
+
callbacks: config.callbacks,
|
3411
|
+
head: config.head
|
3412
|
+
};
|
3089
3413
|
}
|
3090
|
-
|
3091
|
-
|
3092
|
-
|
3093
|
-
removed: removed
|
3094
|
-
});
|
3095
|
-
return promises;
|
3096
|
-
}
|
3097
|
-
|
3098
|
-
function noOp() {}
|
3099
|
-
|
3100
|
-
function createMorphContext(oldNode, newContent, config) {
|
3101
|
-
return {
|
3102
|
-
target: oldNode,
|
3103
|
-
newContent: newContent,
|
3104
|
-
config: config,
|
3105
|
-
morphStyle: config.morphStyle,
|
3106
|
-
ignoreActive: config.ignoreActive,
|
3107
|
-
idMap: createIdMap(oldNode, newContent),
|
3108
|
-
deadIds: new Set,
|
3109
|
-
callbacks: Object.assign({
|
3110
|
-
beforeNodeAdded: noOp,
|
3111
|
-
afterNodeAdded: noOp,
|
3112
|
-
beforeNodeMorphed: noOp,
|
3113
|
-
afterNodeMorphed: noOp,
|
3114
|
-
beforeNodeRemoved: noOp,
|
3115
|
-
afterNodeRemoved: noOp
|
3116
|
-
}, config.callbacks),
|
3117
|
-
head: Object.assign({
|
3118
|
-
style: "merge",
|
3119
|
-
shouldPreserve: function(elt) {
|
3120
|
-
return elt.getAttribute("im-preserve") === "true";
|
3121
|
-
},
|
3122
|
-
shouldReAppend: function(elt) {
|
3123
|
-
return elt.getAttribute("im-re-append") === "true";
|
3124
|
-
},
|
3125
|
-
shouldRemove: noOp,
|
3126
|
-
afterHeadMorphed: noOp
|
3127
|
-
}, config.head)
|
3128
|
-
};
|
3129
|
-
}
|
3130
|
-
|
3131
|
-
function isIdSetMatch(node1, node2, ctx) {
|
3132
|
-
if (node1 == null || node2 == null) {
|
3133
|
-
return false;
|
3134
|
-
}
|
3135
|
-
if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) {
|
3136
|
-
if (node1.id !== "" && node1.id === node2.id) {
|
3137
|
-
return true;
|
3138
|
-
} else {
|
3139
|
-
return getIdIntersectionCount(ctx, node1, node2) > 0;
|
3414
|
+
function isIdSetMatch(node1, node2, ctx) {
|
3415
|
+
if (node1 == null || node2 == null) {
|
3416
|
+
return false;
|
3140
3417
|
}
|
3141
|
-
|
3142
|
-
|
3143
|
-
|
3144
|
-
|
3145
|
-
|
3146
|
-
if (node1 == null || node2 == null) {
|
3147
|
-
return false;
|
3148
|
-
}
|
3149
|
-
return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName;
|
3150
|
-
}
|
3151
|
-
|
3152
|
-
function removeNodesBetween(startInclusive, endExclusive, ctx) {
|
3153
|
-
while (startInclusive !== endExclusive) {
|
3154
|
-
let tempNode = startInclusive;
|
3155
|
-
startInclusive = startInclusive.nextSibling;
|
3156
|
-
removeNode(tempNode, ctx);
|
3157
|
-
}
|
3158
|
-
removeIdsFromConsideration(ctx, endExclusive);
|
3159
|
-
return endExclusive.nextSibling;
|
3160
|
-
}
|
3161
|
-
|
3162
|
-
function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
|
3163
|
-
let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent);
|
3164
|
-
let potentialMatch = null;
|
3165
|
-
if (newChildPotentialIdCount > 0) {
|
3166
|
-
let potentialMatch = insertionPoint;
|
3167
|
-
let otherMatchCount = 0;
|
3168
|
-
while (potentialMatch != null) {
|
3169
|
-
if (isIdSetMatch(newChild, potentialMatch, ctx)) {
|
3170
|
-
return potentialMatch;
|
3171
|
-
}
|
3172
|
-
otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent);
|
3173
|
-
if (otherMatchCount > newChildPotentialIdCount) {
|
3174
|
-
return null;
|
3418
|
+
if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) {
|
3419
|
+
if (node1.id !== "" && node1.id === node2.id) {
|
3420
|
+
return true;
|
3421
|
+
} else {
|
3422
|
+
return getIdIntersectionCount(ctx, node1, node2) > 0;
|
3175
3423
|
}
|
3176
|
-
potentialMatch = potentialMatch.nextSibling;
|
3177
3424
|
}
|
3425
|
+
return false;
|
3178
3426
|
}
|
3179
|
-
|
3180
|
-
|
3181
|
-
|
3182
|
-
function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
|
3183
|
-
let potentialSoftMatch = insertionPoint;
|
3184
|
-
let nextSibling = newChild.nextSibling;
|
3185
|
-
let siblingSoftMatchCount = 0;
|
3186
|
-
while (potentialSoftMatch != null) {
|
3187
|
-
if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) {
|
3188
|
-
return null;
|
3189
|
-
}
|
3190
|
-
if (isSoftMatch(newChild, potentialSoftMatch)) {
|
3191
|
-
return potentialSoftMatch;
|
3427
|
+
function isSoftMatch(node1, node2) {
|
3428
|
+
if (node1 == null || node2 == null) {
|
3429
|
+
return false;
|
3192
3430
|
}
|
3193
|
-
|
3194
|
-
|
3195
|
-
|
3196
|
-
|
3197
|
-
|
3431
|
+
return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName;
|
3432
|
+
}
|
3433
|
+
function removeNodesBetween(startInclusive, endExclusive, ctx) {
|
3434
|
+
while (startInclusive !== endExclusive) {
|
3435
|
+
let tempNode = startInclusive;
|
3436
|
+
startInclusive = startInclusive.nextSibling;
|
3437
|
+
removeNode(tempNode, ctx);
|
3438
|
+
}
|
3439
|
+
removeIdsFromConsideration(ctx, endExclusive);
|
3440
|
+
return endExclusive.nextSibling;
|
3441
|
+
}
|
3442
|
+
function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
|
3443
|
+
let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent);
|
3444
|
+
let potentialMatch = null;
|
3445
|
+
if (newChildPotentialIdCount > 0) {
|
3446
|
+
let potentialMatch = insertionPoint;
|
3447
|
+
let otherMatchCount = 0;
|
3448
|
+
while (potentialMatch != null) {
|
3449
|
+
if (isIdSetMatch(newChild, potentialMatch, ctx)) {
|
3450
|
+
return potentialMatch;
|
3451
|
+
}
|
3452
|
+
otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent);
|
3453
|
+
if (otherMatchCount > newChildPotentialIdCount) {
|
3454
|
+
return null;
|
3455
|
+
}
|
3456
|
+
potentialMatch = potentialMatch.nextSibling;
|
3198
3457
|
}
|
3199
3458
|
}
|
3200
|
-
|
3459
|
+
return potentialMatch;
|
3201
3460
|
}
|
3202
|
-
|
3203
|
-
|
3204
|
-
|
3205
|
-
|
3206
|
-
|
3207
|
-
|
3208
|
-
|
3209
|
-
|
3210
|
-
|
3461
|
+
function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
|
3462
|
+
let potentialSoftMatch = insertionPoint;
|
3463
|
+
let nextSibling = newChild.nextSibling;
|
3464
|
+
let siblingSoftMatchCount = 0;
|
3465
|
+
while (potentialSoftMatch != null) {
|
3466
|
+
if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) {
|
3467
|
+
return null;
|
3468
|
+
}
|
3469
|
+
if (isSoftMatch(newChild, potentialSoftMatch)) {
|
3470
|
+
return potentialSoftMatch;
|
3471
|
+
}
|
3472
|
+
if (isSoftMatch(nextSibling, potentialSoftMatch)) {
|
3473
|
+
siblingSoftMatchCount++;
|
3474
|
+
nextSibling = nextSibling.nextSibling;
|
3475
|
+
if (siblingSoftMatchCount >= 2) {
|
3476
|
+
return null;
|
3477
|
+
}
|
3478
|
+
}
|
3479
|
+
potentialSoftMatch = potentialSoftMatch.nextSibling;
|
3480
|
+
}
|
3481
|
+
return potentialSoftMatch;
|
3482
|
+
}
|
3483
|
+
function parseContent(newContent) {
|
3484
|
+
let parser = new DOMParser;
|
3485
|
+
let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, "");
|
3486
|
+
if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) {
|
3487
|
+
let content = parser.parseFromString(newContent, "text/html");
|
3488
|
+
if (contentWithSvgsRemoved.match(/<\/html>/)) {
|
3489
|
+
content.generatedByIdiomorph = true;
|
3490
|
+
return content;
|
3491
|
+
} else {
|
3492
|
+
let htmlElement = content.firstChild;
|
3493
|
+
if (htmlElement) {
|
3494
|
+
htmlElement.generatedByIdiomorph = true;
|
3495
|
+
return htmlElement;
|
3496
|
+
} else {
|
3497
|
+
return null;
|
3498
|
+
}
|
3499
|
+
}
|
3500
|
+
} else {
|
3501
|
+
let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html");
|
3502
|
+
let content = responseDoc.body.querySelector("template").content;
|
3211
3503
|
content.generatedByIdiomorph = true;
|
3212
3504
|
return content;
|
3505
|
+
}
|
3506
|
+
}
|
3507
|
+
function normalizeContent(newContent) {
|
3508
|
+
if (newContent == null) {
|
3509
|
+
const dummyParent = document.createElement("div");
|
3510
|
+
return dummyParent;
|
3511
|
+
} else if (newContent.generatedByIdiomorph) {
|
3512
|
+
return newContent;
|
3513
|
+
} else if (newContent instanceof Node) {
|
3514
|
+
const dummyParent = document.createElement("div");
|
3515
|
+
dummyParent.append(newContent);
|
3516
|
+
return dummyParent;
|
3213
3517
|
} else {
|
3214
|
-
|
3215
|
-
|
3216
|
-
|
3217
|
-
return htmlElement;
|
3218
|
-
} else {
|
3219
|
-
return null;
|
3518
|
+
const dummyParent = document.createElement("div");
|
3519
|
+
for (const elt of [ ...newContent ]) {
|
3520
|
+
dummyParent.append(elt);
|
3220
3521
|
}
|
3522
|
+
return dummyParent;
|
3221
3523
|
}
|
3222
|
-
}
|
3223
|
-
|
3224
|
-
let
|
3225
|
-
|
3226
|
-
|
3227
|
-
|
3228
|
-
|
3229
|
-
|
3230
|
-
|
3231
|
-
|
3232
|
-
|
3233
|
-
|
3234
|
-
|
3235
|
-
|
3236
|
-
|
3237
|
-
|
3238
|
-
|
3239
|
-
|
3240
|
-
|
3241
|
-
|
3242
|
-
|
3243
|
-
|
3524
|
+
}
|
3525
|
+
function insertSiblings(previousSibling, morphedNode, nextSibling) {
|
3526
|
+
let stack = [];
|
3527
|
+
let added = [];
|
3528
|
+
while (previousSibling != null) {
|
3529
|
+
stack.push(previousSibling);
|
3530
|
+
previousSibling = previousSibling.previousSibling;
|
3531
|
+
}
|
3532
|
+
while (stack.length > 0) {
|
3533
|
+
let node = stack.pop();
|
3534
|
+
added.push(node);
|
3535
|
+
morphedNode.parentElement.insertBefore(node, morphedNode);
|
3536
|
+
}
|
3537
|
+
added.push(morphedNode);
|
3538
|
+
while (nextSibling != null) {
|
3539
|
+
stack.push(nextSibling);
|
3540
|
+
added.push(nextSibling);
|
3541
|
+
nextSibling = nextSibling.nextSibling;
|
3542
|
+
}
|
3543
|
+
while (stack.length > 0) {
|
3544
|
+
morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling);
|
3545
|
+
}
|
3546
|
+
return added;
|
3547
|
+
}
|
3548
|
+
function findBestNodeMatch(newContent, oldNode, ctx) {
|
3549
|
+
let currentElement;
|
3550
|
+
currentElement = newContent.firstChild;
|
3551
|
+
let bestElement = currentElement;
|
3552
|
+
let score = 0;
|
3553
|
+
while (currentElement) {
|
3554
|
+
let newScore = scoreElement(currentElement, oldNode, ctx);
|
3555
|
+
if (newScore > score) {
|
3556
|
+
bestElement = currentElement;
|
3557
|
+
score = newScore;
|
3558
|
+
}
|
3559
|
+
currentElement = currentElement.nextSibling;
|
3244
3560
|
}
|
3245
|
-
return
|
3561
|
+
return bestElement;
|
3246
3562
|
}
|
3247
|
-
|
3248
|
-
|
3249
|
-
|
3250
|
-
|
3251
|
-
|
3252
|
-
while (previousSibling != null) {
|
3253
|
-
stack.push(previousSibling);
|
3254
|
-
previousSibling = previousSibling.previousSibling;
|
3563
|
+
function scoreElement(node1, node2, ctx) {
|
3564
|
+
if (isSoftMatch(node1, node2)) {
|
3565
|
+
return .5 + getIdIntersectionCount(ctx, node1, node2);
|
3566
|
+
}
|
3567
|
+
return 0;
|
3255
3568
|
}
|
3256
|
-
|
3257
|
-
|
3258
|
-
|
3259
|
-
|
3569
|
+
function removeNode(tempNode, ctx) {
|
3570
|
+
removeIdsFromConsideration(ctx, tempNode);
|
3571
|
+
if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return;
|
3572
|
+
tempNode.remove();
|
3573
|
+
ctx.callbacks.afterNodeRemoved(tempNode);
|
3260
3574
|
}
|
3261
|
-
|
3262
|
-
|
3263
|
-
stack.push(nextSibling);
|
3264
|
-
added.push(nextSibling);
|
3265
|
-
nextSibling = nextSibling.nextSibling;
|
3575
|
+
function isIdInConsideration(ctx, id) {
|
3576
|
+
return !ctx.deadIds.has(id);
|
3266
3577
|
}
|
3267
|
-
|
3268
|
-
|
3578
|
+
function idIsWithinNode(ctx, id, targetNode) {
|
3579
|
+
let idSet = ctx.idMap.get(targetNode) || EMPTY_SET;
|
3580
|
+
return idSet.has(id);
|
3269
3581
|
}
|
3270
|
-
|
3271
|
-
|
3272
|
-
|
3273
|
-
|
3274
|
-
let currentElement;
|
3275
|
-
currentElement = newContent.firstChild;
|
3276
|
-
let bestElement = currentElement;
|
3277
|
-
let score = 0;
|
3278
|
-
while (currentElement) {
|
3279
|
-
let newScore = scoreElement(currentElement, oldNode, ctx);
|
3280
|
-
if (newScore > score) {
|
3281
|
-
bestElement = currentElement;
|
3282
|
-
score = newScore;
|
3582
|
+
function removeIdsFromConsideration(ctx, node) {
|
3583
|
+
let idSet = ctx.idMap.get(node) || EMPTY_SET;
|
3584
|
+
for (const id of idSet) {
|
3585
|
+
ctx.deadIds.add(id);
|
3283
3586
|
}
|
3284
|
-
currentElement = currentElement.nextSibling;
|
3285
|
-
}
|
3286
|
-
return bestElement;
|
3287
|
-
}
|
3288
|
-
|
3289
|
-
function scoreElement(node1, node2, ctx) {
|
3290
|
-
if (isSoftMatch(node1, node2)) {
|
3291
|
-
return .5 + getIdIntersectionCount(ctx, node1, node2);
|
3292
3587
|
}
|
3293
|
-
|
3294
|
-
|
3295
|
-
|
3296
|
-
|
3297
|
-
|
3298
|
-
|
3299
|
-
|
3300
|
-
ctx.callbacks.afterNodeRemoved(tempNode);
|
3301
|
-
}
|
3302
|
-
|
3303
|
-
function isIdInConsideration(ctx, id) {
|
3304
|
-
return !ctx.deadIds.has(id);
|
3305
|
-
}
|
3306
|
-
|
3307
|
-
function idIsWithinNode(ctx, id, targetNode) {
|
3308
|
-
let idSet = ctx.idMap.get(targetNode) || EMPTY_SET;
|
3309
|
-
return idSet.has(id);
|
3310
|
-
}
|
3311
|
-
|
3312
|
-
function removeIdsFromConsideration(ctx, node) {
|
3313
|
-
let idSet = ctx.idMap.get(node) || EMPTY_SET;
|
3314
|
-
for (const id of idSet) {
|
3315
|
-
ctx.deadIds.add(id);
|
3316
|
-
}
|
3317
|
-
}
|
3318
|
-
|
3319
|
-
function getIdIntersectionCount(ctx, node1, node2) {
|
3320
|
-
let sourceSet = ctx.idMap.get(node1) || EMPTY_SET;
|
3321
|
-
let matchCount = 0;
|
3322
|
-
for (const id of sourceSet) {
|
3323
|
-
if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) {
|
3324
|
-
++matchCount;
|
3588
|
+
function getIdIntersectionCount(ctx, node1, node2) {
|
3589
|
+
let sourceSet = ctx.idMap.get(node1) || EMPTY_SET;
|
3590
|
+
let matchCount = 0;
|
3591
|
+
for (const id of sourceSet) {
|
3592
|
+
if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) {
|
3593
|
+
++matchCount;
|
3594
|
+
}
|
3325
3595
|
}
|
3326
|
-
|
3327
|
-
|
3328
|
-
|
3329
|
-
|
3330
|
-
|
3331
|
-
|
3332
|
-
|
3333
|
-
|
3334
|
-
|
3335
|
-
|
3336
|
-
|
3337
|
-
|
3338
|
-
|
3339
|
-
|
3596
|
+
return matchCount;
|
3597
|
+
}
|
3598
|
+
function populateIdMapForNode(node, idMap) {
|
3599
|
+
let nodeParent = node.parentElement;
|
3600
|
+
let idElements = node.querySelectorAll("[id]");
|
3601
|
+
for (const elt of idElements) {
|
3602
|
+
let current = elt;
|
3603
|
+
while (current !== nodeParent && current != null) {
|
3604
|
+
let idSet = idMap.get(current);
|
3605
|
+
if (idSet == null) {
|
3606
|
+
idSet = new Set;
|
3607
|
+
idMap.set(current, idSet);
|
3608
|
+
}
|
3609
|
+
idSet.add(elt.id);
|
3610
|
+
current = current.parentElement;
|
3340
3611
|
}
|
3341
|
-
idSet.add(elt.id);
|
3342
|
-
current = current.parentElement;
|
3343
3612
|
}
|
3344
3613
|
}
|
3345
|
-
|
3346
|
-
|
3347
|
-
|
3348
|
-
|
3349
|
-
|
3350
|
-
|
3351
|
-
return
|
3352
|
-
|
3353
|
-
|
3354
|
-
|
3355
|
-
|
3356
|
-
};
|
3614
|
+
function createIdMap(oldContent, newContent) {
|
3615
|
+
let idMap = new Map;
|
3616
|
+
populateIdMapForNode(oldContent, idMap);
|
3617
|
+
populateIdMapForNode(newContent, idMap);
|
3618
|
+
return idMap;
|
3619
|
+
}
|
3620
|
+
return {
|
3621
|
+
morph: morph,
|
3622
|
+
defaults: defaults
|
3623
|
+
};
|
3624
|
+
}();
|
3357
3625
|
|
3358
3626
|
class MorphRenderer extends Renderer {
|
3359
3627
|
async render() {
|
@@ -3374,7 +3642,7 @@ class MorphRenderer extends Renderer {
|
|
3374
3642
|
}
|
3375
3643
|
#morphElements(currentElement, newElement, morphStyle = "outerHTML") {
|
3376
3644
|
this.isMorphingTurboFrame = this.#isFrameReloadedWithMorph(currentElement);
|
3377
|
-
|
3645
|
+
Idiomorph.morph(currentElement, newElement, {
|
3378
3646
|
morphStyle: morphStyle,
|
3379
3647
|
callbacks: {
|
3380
3648
|
beforeNodeAdded: this.#shouldAddElement,
|
@@ -3487,6 +3755,9 @@ class PageRenderer extends Renderer {
|
|
3487
3755
|
this.copyNewHeadScriptElements();
|
3488
3756
|
await mergedHeadElements;
|
3489
3757
|
await newStylesheetElements;
|
3758
|
+
if (this.willRender) {
|
3759
|
+
this.removeUnusedHeadStylesheetElements();
|
3760
|
+
}
|
3490
3761
|
}
|
3491
3762
|
async replaceBody() {
|
3492
3763
|
await this.preservingPermanentElements((async () => {
|
@@ -3510,6 +3781,11 @@ class PageRenderer extends Renderer {
|
|
3510
3781
|
document.head.appendChild(activateScriptElement(element));
|
3511
3782
|
}
|
3512
3783
|
}
|
3784
|
+
removeUnusedHeadStylesheetElements() {
|
3785
|
+
for (const element of this.unusedHeadStylesheetElements) {
|
3786
|
+
document.head.removeChild(element);
|
3787
|
+
}
|
3788
|
+
}
|
3513
3789
|
async mergeProvisionalElements() {
|
3514
3790
|
const newHeadElements = [ ...this.newHeadProvisionalElements ];
|
3515
3791
|
for (const element of this.currentHeadProvisionalElements) {
|
@@ -3562,6 +3838,12 @@ class PageRenderer extends Renderer {
|
|
3562
3838
|
async assignNewBody() {
|
3563
3839
|
await this.renderElement(this.currentElement, this.newElement);
|
3564
3840
|
}
|
3841
|
+
get unusedHeadStylesheetElements() {
|
3842
|
+
return this.oldHeadStylesheetElements.filter((element => !(element.hasAttribute("data-turbo-permanent") || element.hasAttribute("data-tag-name"))));
|
3843
|
+
}
|
3844
|
+
get oldHeadStylesheetElements() {
|
3845
|
+
return this.currentHeadSnapshot.getStylesheetElementsNotInSnapshot(this.newHeadSnapshot);
|
3846
|
+
}
|
3565
3847
|
get newHeadStylesheetElements() {
|
3566
3848
|
return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot);
|
3567
3849
|
}
|
@@ -3663,7 +3945,10 @@ class PageView extends View {
|
|
3663
3945
|
return this.snapshotCache.get(location);
|
3664
3946
|
}
|
3665
3947
|
isPageRefresh(visit) {
|
3666
|
-
return !visit || this.lastRenderedLocation.
|
3948
|
+
return !visit || this.lastRenderedLocation.pathname === visit.location.pathname && visit.action === "replace";
|
3949
|
+
}
|
3950
|
+
shouldPreserveScrollPosition(visit) {
|
3951
|
+
return this.isPageRefresh(visit) && this.snapshot.shouldPreserveScrollPosition;
|
3667
3952
|
}
|
3668
3953
|
get snapshot() {
|
3669
3954
|
return PageSnapshot.fromElement(this.element);
|
@@ -3672,24 +3957,25 @@ class PageView extends View {
|
|
3672
3957
|
|
3673
3958
|
class Preloader {
|
3674
3959
|
selector="a[data-turbo-preload]";
|
3675
|
-
constructor(delegate) {
|
3960
|
+
constructor(delegate, snapshotCache) {
|
3676
3961
|
this.delegate = delegate;
|
3677
|
-
|
3678
|
-
get snapshotCache() {
|
3679
|
-
return this.delegate.navigator.view.snapshotCache;
|
3962
|
+
this.snapshotCache = snapshotCache;
|
3680
3963
|
}
|
3681
3964
|
start() {
|
3682
3965
|
if (document.readyState === "loading") {
|
3683
|
-
|
3684
|
-
this.preloadOnLoadLinksForView(document.body);
|
3685
|
-
}));
|
3966
|
+
document.addEventListener("DOMContentLoaded", this.#preloadAll);
|
3686
3967
|
} else {
|
3687
3968
|
this.preloadOnLoadLinksForView(document.body);
|
3688
3969
|
}
|
3689
3970
|
}
|
3971
|
+
stop() {
|
3972
|
+
document.removeEventListener("DOMContentLoaded", this.#preloadAll);
|
3973
|
+
}
|
3690
3974
|
preloadOnLoadLinksForView(element) {
|
3691
3975
|
for (const link of element.querySelectorAll(this.selector)) {
|
3692
|
-
this.
|
3976
|
+
if (this.delegate.shouldPreloadLink(link)) {
|
3977
|
+
this.preloadURL(link);
|
3978
|
+
}
|
3693
3979
|
}
|
3694
3980
|
}
|
3695
3981
|
async preloadURL(link) {
|
@@ -3697,33 +3983,27 @@ class Preloader {
|
|
3697
3983
|
if (this.snapshotCache.has(location)) {
|
3698
3984
|
return;
|
3699
3985
|
}
|
3700
|
-
|
3701
|
-
|
3702
|
-
headers: {
|
3703
|
-
"Sec-Purpose": "prefetch",
|
3704
|
-
Accept: "text/html"
|
3705
|
-
}
|
3706
|
-
});
|
3707
|
-
const responseText = await response.text();
|
3708
|
-
const snapshot = PageSnapshot.fromHTMLString(responseText);
|
3709
|
-
this.snapshotCache.put(location, snapshot);
|
3710
|
-
} catch (_) {}
|
3986
|
+
const fetchRequest = new FetchRequest(this, FetchMethod.get, location, new URLSearchParams, link);
|
3987
|
+
await fetchRequest.perform();
|
3711
3988
|
}
|
3712
|
-
|
3713
|
-
|
3714
|
-
class LimitedSet extends Set {
|
3715
|
-
constructor(maxSize) {
|
3716
|
-
super();
|
3717
|
-
this.maxSize = maxSize;
|
3989
|
+
prepareRequest(fetchRequest) {
|
3990
|
+
fetchRequest.headers["Sec-Purpose"] = "prefetch";
|
3718
3991
|
}
|
3719
|
-
|
3720
|
-
|
3721
|
-
const
|
3722
|
-
const
|
3723
|
-
this.
|
3724
|
-
}
|
3725
|
-
super.add(value);
|
3992
|
+
async requestSucceededWithResponse(fetchRequest, fetchResponse) {
|
3993
|
+
try {
|
3994
|
+
const responseHTML = await fetchResponse.responseHTML;
|
3995
|
+
const snapshot = PageSnapshot.fromHTMLString(responseHTML);
|
3996
|
+
this.snapshotCache.put(fetchRequest.url, snapshot);
|
3997
|
+
} catch (_) {}
|
3726
3998
|
}
|
3999
|
+
requestStarted(fetchRequest) {}
|
4000
|
+
requestErrored(fetchRequest) {}
|
4001
|
+
requestFinished(fetchRequest) {}
|
4002
|
+
requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}
|
4003
|
+
requestFailedWithResponse(fetchRequest, fetchResponse) {}
|
4004
|
+
#preloadAll=() => {
|
4005
|
+
this.preloadOnLoadLinksForView(document.body);
|
4006
|
+
};
|
3727
4007
|
}
|
3728
4008
|
|
3729
4009
|
class Cache {
|
@@ -3750,11 +4030,11 @@ class Cache {
|
|
3750
4030
|
class Session {
|
3751
4031
|
navigator=new Navigator(this);
|
3752
4032
|
history=new History(this);
|
3753
|
-
preloader=new Preloader(this);
|
3754
4033
|
view=new PageView(this, document.documentElement);
|
3755
4034
|
adapter=new BrowserAdapter(this);
|
3756
4035
|
pageObserver=new PageObserver(this);
|
3757
4036
|
cacheObserver=new CacheObserver;
|
4037
|
+
linkPrefetchObserver=new LinkPrefetchObserver(this, document);
|
3758
4038
|
linkClickObserver=new LinkClickObserver(this, window);
|
3759
4039
|
formSubmitObserver=new FormSubmitObserver(this, document);
|
3760
4040
|
scrollObserver=new ScrollObserver(this);
|
@@ -3763,16 +4043,23 @@ class Session {
|
|
3763
4043
|
frameRedirector=new FrameRedirector(this, document.documentElement);
|
3764
4044
|
streamMessageRenderer=new StreamMessageRenderer;
|
3765
4045
|
cache=new Cache(this);
|
3766
|
-
recentRequests=new LimitedSet(20);
|
3767
4046
|
drive=true;
|
3768
4047
|
enabled=true;
|
3769
4048
|
progressBarDelay=500;
|
3770
4049
|
started=false;
|
3771
4050
|
formMode="on";
|
4051
|
+
#pageRefreshDebouncePeriod=150;
|
4052
|
+
constructor(recentRequests) {
|
4053
|
+
this.recentRequests = recentRequests;
|
4054
|
+
this.preloader = new Preloader(this, this.view.snapshotCache);
|
4055
|
+
this.debouncedRefresh = this.refresh;
|
4056
|
+
this.pageRefreshDebouncePeriod = this.pageRefreshDebouncePeriod;
|
4057
|
+
}
|
3772
4058
|
start() {
|
3773
4059
|
if (!this.started) {
|
3774
4060
|
this.pageObserver.start();
|
3775
4061
|
this.cacheObserver.start();
|
4062
|
+
this.linkPrefetchObserver.start();
|
3776
4063
|
this.formLinkClickObserver.start();
|
3777
4064
|
this.linkClickObserver.start();
|
3778
4065
|
this.formSubmitObserver.start();
|
@@ -3792,6 +4079,7 @@ class Session {
|
|
3792
4079
|
if (this.started) {
|
3793
4080
|
this.pageObserver.stop();
|
3794
4081
|
this.cacheObserver.stop();
|
4082
|
+
this.linkPrefetchObserver.stop();
|
3795
4083
|
this.formLinkClickObserver.stop();
|
3796
4084
|
this.linkClickObserver.stop();
|
3797
4085
|
this.formSubmitObserver.stop();
|
@@ -3799,6 +4087,7 @@ class Session {
|
|
3799
4087
|
this.streamObserver.stop();
|
3800
4088
|
this.frameRedirector.stop();
|
3801
4089
|
this.history.stop();
|
4090
|
+
this.preloader.stop();
|
3802
4091
|
this.started = false;
|
3803
4092
|
}
|
3804
4093
|
}
|
@@ -3847,11 +4136,31 @@ class Session {
|
|
3847
4136
|
get restorationIdentifier() {
|
3848
4137
|
return this.history.restorationIdentifier;
|
3849
4138
|
}
|
3850
|
-
|
4139
|
+
get pageRefreshDebouncePeriod() {
|
4140
|
+
return this.#pageRefreshDebouncePeriod;
|
4141
|
+
}
|
4142
|
+
set pageRefreshDebouncePeriod(value) {
|
4143
|
+
this.refresh = debounce(this.debouncedRefresh.bind(this), value);
|
4144
|
+
this.#pageRefreshDebouncePeriod = value;
|
4145
|
+
}
|
4146
|
+
shouldPreloadLink(element) {
|
4147
|
+
const isUnsafe = element.hasAttribute("data-turbo-method");
|
4148
|
+
const isStream = element.hasAttribute("data-turbo-stream");
|
4149
|
+
const frameTarget = element.getAttribute("data-turbo-frame");
|
4150
|
+
const frame = frameTarget == "_top" ? null : document.getElementById(frameTarget) || findClosestRecursively(element, "turbo-frame:not([disabled])");
|
4151
|
+
if (isUnsafe || isStream || frame instanceof FrameElement) {
|
4152
|
+
return false;
|
4153
|
+
} else {
|
4154
|
+
const location = new URL(element.href);
|
4155
|
+
return this.elementIsNavigatable(element) && locationIsVisitable(location, this.snapshot.rootLocation);
|
4156
|
+
}
|
4157
|
+
}
|
4158
|
+
historyPoppedToLocationWithRestorationIdentifierAndDirection(location, restorationIdentifier, direction) {
|
3851
4159
|
if (this.enabled) {
|
3852
4160
|
this.navigator.startVisit(location, restorationIdentifier, {
|
3853
4161
|
action: "restore",
|
3854
|
-
historyChanged: true
|
4162
|
+
historyChanged: true,
|
4163
|
+
direction: direction
|
3855
4164
|
});
|
3856
4165
|
} else {
|
3857
4166
|
this.adapter.pageInvalidated({
|
@@ -3868,6 +4177,9 @@ class Session {
|
|
3868
4177
|
return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation);
|
3869
4178
|
}
|
3870
4179
|
submittedFormLinkToLocation() {}
|
4180
|
+
canPrefetchRequestToLocation(link, location) {
|
4181
|
+
return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation);
|
4182
|
+
}
|
3871
4183
|
willFollowLinkToLocation(link, location, event) {
|
3872
4184
|
return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation) && this.applicationAllowsFollowingLinkToLocation(link, location, event);
|
3873
4185
|
}
|
@@ -3889,6 +4201,7 @@ class Session {
|
|
3889
4201
|
visitStarted(visit) {
|
3890
4202
|
if (!visit.acceptsStreamResponse) {
|
3891
4203
|
markAsBusy(document.documentElement);
|
4204
|
+
this.view.markVisitDirection(visit.direction);
|
3892
4205
|
}
|
3893
4206
|
extendURLWithDeprecatedProperties(visit.location);
|
3894
4207
|
if (!visit.silent) {
|
@@ -3896,6 +4209,7 @@ class Session {
|
|
3896
4209
|
}
|
3897
4210
|
}
|
3898
4211
|
visitCompleted(visit) {
|
4212
|
+
this.view.unmarkVisitDirection();
|
3899
4213
|
clearBusyState(document.documentElement);
|
3900
4214
|
this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
|
3901
4215
|
}
|
@@ -3930,17 +4244,17 @@ class Session {
|
|
3930
4244
|
this.notifyApplicationBeforeCachingSnapshot();
|
3931
4245
|
}
|
3932
4246
|
}
|
3933
|
-
allowsImmediateRender({element: element},
|
3934
|
-
const event = this.notifyApplicationBeforeRender(element,
|
4247
|
+
allowsImmediateRender({element: element}, options) {
|
4248
|
+
const event = this.notifyApplicationBeforeRender(element, options);
|
3935
4249
|
const {defaultPrevented: defaultPrevented, detail: {render: render}} = event;
|
3936
4250
|
if (this.view.renderer && render) {
|
3937
4251
|
this.view.renderer.renderElement = render;
|
3938
4252
|
}
|
3939
4253
|
return !defaultPrevented;
|
3940
4254
|
}
|
3941
|
-
viewRenderedSnapshot(_snapshot,
|
4255
|
+
viewRenderedSnapshot(_snapshot, _isPreview, renderMethod) {
|
3942
4256
|
this.view.lastRenderedLocation = this.history.location;
|
3943
|
-
this.notifyApplicationAfterRender(
|
4257
|
+
this.notifyApplicationAfterRender(renderMethod);
|
3944
4258
|
}
|
3945
4259
|
preloadOnLoadLinksForView(element) {
|
3946
4260
|
this.preloader.preloadOnLoadLinksForView(element);
|
@@ -3991,20 +4305,18 @@ class Session {
|
|
3991
4305
|
notifyApplicationBeforeCachingSnapshot() {
|
3992
4306
|
return dispatch("turbo:before-cache");
|
3993
4307
|
}
|
3994
|
-
notifyApplicationBeforeRender(newBody,
|
4308
|
+
notifyApplicationBeforeRender(newBody, options) {
|
3995
4309
|
return dispatch("turbo:before-render", {
|
3996
4310
|
detail: {
|
3997
4311
|
newBody: newBody,
|
3998
|
-
isPreview: isPreview,
|
3999
4312
|
...options
|
4000
4313
|
},
|
4001
4314
|
cancelable: true
|
4002
4315
|
});
|
4003
4316
|
}
|
4004
|
-
notifyApplicationAfterRender(
|
4317
|
+
notifyApplicationAfterRender(renderMethod) {
|
4005
4318
|
return dispatch("turbo:render", {
|
4006
4319
|
detail: {
|
4007
|
-
isPreview: isPreview,
|
4008
4320
|
renderMethod: renderMethod
|
4009
4321
|
}
|
4010
4322
|
});
|
@@ -4086,7 +4398,7 @@ const deprecatedLocationPropertyDescriptors = {
|
|
4086
4398
|
}
|
4087
4399
|
};
|
4088
4400
|
|
4089
|
-
const session = new Session;
|
4401
|
+
const session = new Session(recentRequests);
|
4090
4402
|
|
4091
4403
|
const {cache: cache, navigator: navigator$1} = session;
|
4092
4404
|
|
@@ -4139,7 +4451,7 @@ var Turbo = Object.freeze({
|
|
4139
4451
|
PageRenderer: PageRenderer,
|
4140
4452
|
PageSnapshot: PageSnapshot,
|
4141
4453
|
FrameRenderer: FrameRenderer,
|
4142
|
-
fetch:
|
4454
|
+
fetch: fetchWithTurboHeaders,
|
4143
4455
|
start: start,
|
4144
4456
|
registerAdapter: registerAdapter,
|
4145
4457
|
visit: visit,
|
@@ -4332,7 +4644,7 @@ class FrameController {
|
|
4332
4644
|
formSubmissionFinished({formElement: formElement}) {
|
4333
4645
|
clearBusyState(formElement, this.#findFrameElement(formElement));
|
4334
4646
|
}
|
4335
|
-
allowsImmediateRender({element: newFrame},
|
4647
|
+
allowsImmediateRender({element: newFrame}, options) {
|
4336
4648
|
const event = dispatch("turbo:before-frame-render", {
|
4337
4649
|
target: this.element,
|
4338
4650
|
detail: {
|
@@ -4806,11 +5118,14 @@ if (customElements.get("turbo-stream-source") === undefined) {
|
|
4806
5118
|
}
|
4807
5119
|
})();
|
4808
5120
|
|
4809
|
-
window.Turbo =
|
5121
|
+
window.Turbo = {
|
5122
|
+
...Turbo,
|
5123
|
+
StreamActions: StreamActions
|
5124
|
+
};
|
4810
5125
|
|
4811
5126
|
start();
|
4812
5127
|
|
4813
|
-
var
|
5128
|
+
var Turbo$1 = Object.freeze({
|
4814
5129
|
__proto__: null,
|
4815
5130
|
FetchEnctype: FetchEnctype,
|
4816
5131
|
FetchMethod: FetchMethod,
|
@@ -4828,7 +5143,7 @@ var turbo_es2017Esm = Object.freeze({
|
|
4828
5143
|
clearCache: clearCache,
|
4829
5144
|
connectStreamSource: connectStreamSource,
|
4830
5145
|
disconnectStreamSource: disconnectStreamSource,
|
4831
|
-
fetch:
|
5146
|
+
fetch: fetchWithTurboHeaders,
|
4832
5147
|
fetchEnctypeFromString: fetchEnctypeFromString,
|
4833
5148
|
fetchMethodFromString: fetchMethodFromString,
|
4834
5149
|
isSafe: isSafe,
|
@@ -4979,6 +5294,8 @@ function isBodyInit(body) {
|
|
4979
5294
|
return body instanceof FormData || body instanceof URLSearchParams;
|
4980
5295
|
}
|
4981
5296
|
|
5297
|
+
window.Turbo = Turbo$1;
|
5298
|
+
|
4982
5299
|
addEventListener("turbo:before-fetch-request", encodeMethodIntoRequestBody);
|
4983
5300
|
|
4984
5301
|
var adapters = {
|
@@ -5514,4 +5831,4 @@ var index = Object.freeze({
|
|
5514
5831
|
getConfig: getConfig
|
5515
5832
|
});
|
5516
5833
|
|
5517
|
-
export {
|
5834
|
+
export { Turbo$1 as Turbo, cable };
|