turbo-rails 2.0.0.pre.beta.1 → 2.0.0.pre.beta.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 };