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.
@@ -1,6 +1,6 @@
1
1
  /*!
2
- Turbo 8.0.0-beta.1
3
- Copyright © 2023 37signals LLC
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 fetch(url, options = {}) {
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
- window.Turbo.session.recentRequests.add(requestUID);
536
+ recentRequests.add(requestUID);
492
537
  modifiedHeaders.append("X-Turbo-Request-Id", requestUID);
493
- return window.fetch(url, {
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
- const response = await fetch(this.url.href, fetchOptions);
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
- } else if (this.requestMustRedirect(request) && responseSucceededWithoutRedirect(response)) {
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, isPreview, options);
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 = this.findLinkFromClickTarget(target);
1360
+ const link = findLinkFromClickTarget(target);
1263
1361
  if (link && doesNotTargetIFrame(link)) {
1264
- const location = this.getLocationForLink(link);
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 nextAnimationFrame();
1570
+ await nextRepaint();
1484
1571
  this.preservingPermanentElements((() => {
1485
1572
  this.loadFrameElement();
1486
1573
  }));
1487
1574
  this.scrollFrameIntoView();
1488
- await nextAnimationFrame();
1575
+ await nextRepaint();
1489
1576
  this.focusFirstAutofocusableElement();
1490
- await nextAnimationFrame();
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.snapshot.shouldPreserveScrollPosition) {
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 new Promise((resolve => {
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.delegate.historyPoppedToLocationWithRestorationIdentifier(this.location, restorationIdentifier);
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 nextAnimationFrame();
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
- let EMPTY_SET = new Set;
2860
-
2861
- function morph(oldNode, newContent, config = {}) {
2862
- if (oldNode instanceof Document) {
2863
- oldNode = oldNode.documentElement;
2864
- }
2865
- if (typeof newContent === "string") {
2866
- newContent = parseContent(newContent);
2867
- }
2868
- let normalizedContent = normalizeContent(newContent);
2869
- let ctx = createMorphContext(oldNode, normalizedContent, config);
2870
- return morphNormalizedContent(oldNode, normalizedContent, ctx);
2871
- }
2872
-
2873
- function morphNormalizedContent(oldNode, normalizedNewContent, ctx) {
2874
- if (ctx.head.block) {
2875
- let oldHead = oldNode.querySelector("head");
2876
- let newHead = normalizedNewContent.querySelector("head");
2877
- if (oldHead && newHead) {
2878
- let promises = handleHeadElement(newHead, oldHead, ctx);
2879
- Promise.all(promises).then((function() {
2880
- morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, {
2881
- head: {
2882
- block: false,
2883
- ignore: true
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
- return;
3136
+ return;
3137
+ }
2888
3138
  }
2889
- }
2890
- if (ctx.morphStyle === "innerHTML") {
2891
- morphChildren(normalizedNewContent, oldNode, ctx);
2892
- return oldNode.children;
2893
- } else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) {
2894
- let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx);
2895
- let previousSibling = bestMatch?.previousSibling;
2896
- let nextSibling = bestMatch?.nextSibling;
2897
- let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx);
2898
- if (bestMatch) {
2899
- return insertSiblings(previousSibling, morphedNode, nextSibling);
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
- return [];
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
- function morphChildren(newParent, oldParent, ctx) {
2935
- let nextNewChild = newParent.firstChild;
2936
- let insertionPoint = oldParent.firstChild;
2937
- let newChild;
2938
- while (nextNewChild) {
2939
- newChild = nextNewChild;
2940
- nextNewChild = newChild.nextSibling;
2941
- if (insertionPoint == null) {
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.appendChild(newChild);
3221
+ oldParent.insertBefore(newChild, insertionPoint);
2944
3222
  ctx.callbacks.afterNodeAdded(newChild);
2945
3223
  removeIdsFromConsideration(ctx, newChild);
2946
- continue;
2947
3224
  }
2948
- if (isIdSetMatch(newChild, insertionPoint, ctx)) {
2949
- morphOldNodeTo(insertionPoint, newChild, ctx);
3225
+ while (insertionPoint !== null) {
3226
+ let tempNode = insertionPoint;
2950
3227
  insertionPoint = insertionPoint.nextSibling;
2951
- removeIdsFromConsideration(ctx, newChild);
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
- while (insertionPoint !== null) {
2974
- let tempNode = insertionPoint;
2975
- insertionPoint = insertionPoint.nextSibling;
2976
- removeNode(tempNode, ctx);
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
- function syncNodeFrom(from, to) {
2981
- let type = from.nodeType;
2982
- if (type === 1) {
2983
- const fromAttributes = from.attributes;
2984
- const toAttributes = to.attributes;
2985
- for (const fromAttribute of fromAttributes) {
2986
- if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) {
2987
- to.setAttribute(fromAttribute.name, fromAttribute.value);
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
- for (const toAttribute of toAttributes) {
2991
- if (!from.hasAttribute(toAttribute.name)) {
2992
- to.removeAttribute(toAttribute.name);
3260
+ if (type === 8 || type === 3) {
3261
+ if (to.nodeValue !== from.nodeValue) {
3262
+ to.nodeValue = from.nodeValue;
2993
3263
  }
2994
3264
  }
2995
- }
2996
- if (type === 8 || type === 3) {
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
- if (from instanceof HTMLInputElement && to instanceof HTMLInputElement && from.type !== "file") {
3002
- to.value = from.value || "";
3003
- syncAttribute(from, to, "value");
3004
- syncAttribute(from, to, "checked");
3005
- syncAttribute(from, to, "disabled");
3006
- } else if (from instanceof HTMLOptionElement) {
3007
- syncAttribute(from, to, "selected");
3008
- } else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) {
3009
- let fromValue = from.value;
3010
- let toValue = to.value;
3011
- if (fromValue !== toValue) {
3012
- to.value = fromValue;
3013
- }
3014
- if (to.firstChild && to.firstChild.nodeValue !== fromValue) {
3015
- to.firstChild.nodeValue = fromValue;
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
- function syncAttribute(from, to, attributeName) {
3021
- if (from[attributeName] !== to[attributeName]) {
3022
- if (from[attributeName]) {
3023
- to.setAttribute(attributeName, from[attributeName]);
3024
- } else {
3025
- to.removeAttribute(attributeName);
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
- function handleHeadElement(newHeadTag, currentHead, ctx) {
3031
- let added = [];
3032
- let removed = [];
3033
- let preserved = [];
3034
- let nodesToAppend = [];
3035
- let headMergeStyle = ctx.head.style;
3036
- let srcToNewHeadNodes = new Map;
3037
- for (const newHeadChild of newHeadTag.children) {
3038
- srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
3039
- }
3040
- for (const currentHeadElt of currentHead.children) {
3041
- let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
3042
- let isReAppended = ctx.head.shouldReAppend(currentHeadElt);
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
- nodesToAppend.push(currentHeadElt);
3336
+ } else {
3337
+ srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
3338
+ preserved.push(currentHeadElt);
3056
3339
  }
3057
3340
  } else {
3058
- if (ctx.head.shouldRemove(currentHeadElt) !== false) {
3059
- removed.push(currentHeadElt);
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
- nodesToAppend.push(...srcToNewHeadNodes.values());
3065
- let promises = [];
3066
- for (const newNode of nodesToAppend) {
3067
- let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild;
3068
- if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
3069
- if (newElt.href || newElt.src) {
3070
- let resolve = null;
3071
- let promise = new Promise((function(_resolve) {
3072
- resolve = _resolve;
3073
- }));
3074
- newElt.addEventListener("load", (function() {
3075
- resolve();
3076
- }));
3077
- promises.push(promise);
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
- for (const removedElement of removed) {
3085
- if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
3086
- currentHead.removeChild(removedElement);
3087
- ctx.callbacks.afterNodeRemoved(removedElement);
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
- ctx.head.afterHeadMorphed(currentHead, {
3091
- added: added,
3092
- kept: preserved,
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
- return false;
3143
- }
3144
-
3145
- function isSoftMatch(node1, node2) {
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
- return potentialMatch;
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
- if (isSoftMatch(nextSibling, potentialSoftMatch)) {
3194
- siblingSoftMatchCount++;
3195
- nextSibling = nextSibling.nextSibling;
3196
- if (siblingSoftMatchCount >= 2) {
3197
- return null;
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
- potentialSoftMatch = potentialSoftMatch.nextSibling;
3459
+ return potentialMatch;
3201
3460
  }
3202
- return potentialSoftMatch;
3203
- }
3204
-
3205
- function parseContent(newContent) {
3206
- let parser = new DOMParser;
3207
- let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, "");
3208
- if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) {
3209
- let content = parser.parseFromString(newContent, "text/html");
3210
- if (contentWithSvgsRemoved.match(/<\/html>/)) {
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
- let htmlElement = content.firstChild;
3215
- if (htmlElement) {
3216
- htmlElement.generatedByIdiomorph = true;
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
- } else {
3223
- let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html");
3224
- let content = responseDoc.body.querySelector("template").content;
3225
- content.generatedByIdiomorph = true;
3226
- return content;
3227
- }
3228
- }
3229
-
3230
- function normalizeContent(newContent) {
3231
- if (newContent == null) {
3232
- const dummyParent = document.createElement("div");
3233
- return dummyParent;
3234
- } else if (newContent.generatedByIdiomorph) {
3235
- return newContent;
3236
- } else if (newContent instanceof Node) {
3237
- const dummyParent = document.createElement("div");
3238
- dummyParent.append(newContent);
3239
- return dummyParent;
3240
- } else {
3241
- const dummyParent = document.createElement("div");
3242
- for (const elt of [ ...newContent ]) {
3243
- dummyParent.append(elt);
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 dummyParent;
3561
+ return bestElement;
3246
3562
  }
3247
- }
3248
-
3249
- function insertSiblings(previousSibling, morphedNode, nextSibling) {
3250
- let stack = [];
3251
- let added = [];
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
- while (stack.length > 0) {
3257
- let node = stack.pop();
3258
- added.push(node);
3259
- morphedNode.parentElement.insertBefore(node, morphedNode);
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
- added.push(morphedNode);
3262
- while (nextSibling != null) {
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
- while (stack.length > 0) {
3268
- morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling);
3578
+ function idIsWithinNode(ctx, id, targetNode) {
3579
+ let idSet = ctx.idMap.get(targetNode) || EMPTY_SET;
3580
+ return idSet.has(id);
3269
3581
  }
3270
- return added;
3271
- }
3272
-
3273
- function findBestNodeMatch(newContent, oldNode, ctx) {
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
- return 0;
3294
- }
3295
-
3296
- function removeNode(tempNode, ctx) {
3297
- removeIdsFromConsideration(ctx, tempNode);
3298
- if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return;
3299
- tempNode.remove();
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
- return matchCount;
3328
- }
3329
-
3330
- function populateIdMapForNode(node, idMap) {
3331
- let nodeParent = node.parentElement;
3332
- let idElements = node.querySelectorAll("[id]");
3333
- for (const elt of idElements) {
3334
- let current = elt;
3335
- while (current !== nodeParent && current != null) {
3336
- let idSet = idMap.get(current);
3337
- if (idSet == null) {
3338
- idSet = new Set;
3339
- idMap.set(current, idSet);
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
- function createIdMap(oldContent, newContent) {
3348
- let idMap = new Map;
3349
- populateIdMapForNode(oldContent, idMap);
3350
- populateIdMapForNode(newContent, idMap);
3351
- return idMap;
3352
- }
3353
-
3354
- var idiomorph = {
3355
- morph: morph
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
- idiomorph.morph(currentElement, newElement, {
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.href === visit.location.href && visit.action === "replace";
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
- return document.addEventListener("DOMContentLoaded", (() => {
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.preloadURL(link);
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
- try {
3701
- const response = await fetch(location.toString(), {
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
- add(value) {
3720
- if (this.size >= this.maxSize) {
3721
- const iterator = this.values();
3722
- const oldestValue = iterator.next().value;
3723
- this.delete(oldestValue);
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
- historyPoppedToLocationWithRestorationIdentifier(location, restorationIdentifier) {
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}, isPreview, options) {
3934
- const event = this.notifyApplicationBeforeRender(element, isPreview, options);
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, isPreview, renderMethod) {
4255
+ viewRenderedSnapshot(_snapshot, _isPreview, renderMethod) {
3942
4256
  this.view.lastRenderedLocation = this.history.location;
3943
- this.notifyApplicationAfterRender(isPreview, renderMethod);
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, isPreview, options) {
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(isPreview, renderMethod) {
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: 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}, _isPreview, options) {
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 = Turbo;
5121
+ window.Turbo = {
5122
+ ...Turbo,
5123
+ StreamActions: StreamActions
5124
+ };
4810
5125
 
4811
5126
  start();
4812
5127
 
4813
- var turbo_es2017Esm = Object.freeze({
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: 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 { turbo_es2017Esm as Turbo, cable };
5834
+ export { Turbo$1 as Turbo, cable };