@hotwired/turbo 8.0.0-beta.1 → 8.0.0-beta.2

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,5 +1,5 @@
1
1
  /*!
2
- Turbo 8.0.0-beta.1
2
+ Turbo 8.0.0-beta.2
3
3
  Copyright © 2023 37signals LLC
4
4
  */
5
5
  /**
@@ -633,13 +633,33 @@ async function around(callback, reader) {
633
633
  return [before, after]
634
634
  }
635
635
 
636
- function fetch(url, options = {}) {
636
+ class LimitedSet extends Set {
637
+ constructor(maxSize) {
638
+ super();
639
+ this.maxSize = maxSize;
640
+ }
641
+
642
+ add(value) {
643
+ if (this.size >= this.maxSize) {
644
+ const iterator = this.values();
645
+ const oldestValue = iterator.next().value;
646
+ this.delete(oldestValue);
647
+ }
648
+ super.add(value);
649
+ }
650
+ }
651
+
652
+ const recentRequests = new LimitedSet(20);
653
+
654
+ const nativeFetch = window.fetch;
655
+
656
+ function fetchWithTurboHeaders(url, options = {}) {
637
657
  const modifiedHeaders = new Headers(options.headers || {});
638
658
  const requestUID = uuid();
639
- window.Turbo.session.recentRequests.add(requestUID);
659
+ recentRequests.add(requestUID);
640
660
  modifiedHeaders.append("X-Turbo-Request-Id", requestUID);
641
661
 
642
- return window.fetch(url, {
662
+ return nativeFetch(url, {
643
663
  ...options,
644
664
  headers: modifiedHeaders
645
665
  })
@@ -766,7 +786,7 @@ class FetchRequest {
766
786
  await this.#allowRequestToBeIntercepted(fetchOptions);
767
787
  try {
768
788
  this.delegate.requestStarted(this);
769
- const response = await fetch(this.url.href, fetchOptions);
789
+ const response = await fetchWithTurboHeaders(this.url.href, fetchOptions);
770
790
  return await this.receive(response)
771
791
  } catch (error) {
772
792
  if (error.name !== "AbortError") {
@@ -1046,6 +1066,7 @@ class FormSubmission {
1046
1066
  this.state = FormSubmissionState.waiting;
1047
1067
  this.submitter?.setAttribute("disabled", "");
1048
1068
  this.setSubmitsWith();
1069
+ markAsBusy(this.formElement);
1049
1070
  dispatch("turbo:submit-start", {
1050
1071
  target: this.formElement,
1051
1072
  detail: { formSubmission: this }
@@ -1084,6 +1105,7 @@ class FormSubmission {
1084
1105
  this.state = FormSubmissionState.stopped;
1085
1106
  this.submitter?.removeAttribute("disabled");
1086
1107
  this.resetSubmitterText();
1108
+ clearBusyState(this.formElement);
1087
1109
  dispatch("turbo:submit-end", {
1088
1110
  target: this.formElement,
1089
1111
  detail: { formSubmission: this, ...this.result }
@@ -1411,6 +1433,14 @@ class View {
1411
1433
  }
1412
1434
  }
1413
1435
 
1436
+ markVisitDirection(direction) {
1437
+ this.element.setAttribute("data-turbo-visit-direction", direction);
1438
+ }
1439
+
1440
+ unmarkVisitDirection() {
1441
+ this.element.removeAttribute("data-turbo-visit-direction");
1442
+ }
1443
+
1414
1444
  async renderSnapshot(renderer) {
1415
1445
  await renderer.render();
1416
1446
  }
@@ -1780,14 +1810,14 @@ class FrameRenderer extends Renderer {
1780
1810
  }
1781
1811
 
1782
1812
  async render() {
1783
- await nextAnimationFrame();
1813
+ await nextRepaint();
1784
1814
  this.preservingPermanentElements(() => {
1785
1815
  this.loadFrameElement();
1786
1816
  });
1787
1817
  this.scrollFrameIntoView();
1788
- await nextAnimationFrame();
1818
+ await nextRepaint();
1789
1819
  this.focusFirstAutofocusableElement();
1790
- await nextAnimationFrame();
1820
+ await nextRepaint();
1791
1821
  this.activateScriptElements();
1792
1822
  }
1793
1823
 
@@ -2213,6 +2243,12 @@ const SystemStatusCode = {
2213
2243
  contentTypeMismatch: -2
2214
2244
  };
2215
2245
 
2246
+ const Direction = {
2247
+ advance: "forward",
2248
+ restore: "back",
2249
+ replace: "none"
2250
+ };
2251
+
2216
2252
  class Visit {
2217
2253
  identifier = uuid() // Required by turbo-ios
2218
2254
  timingMetrics = {}
@@ -2242,7 +2278,8 @@ class Visit {
2242
2278
  willRender,
2243
2279
  updateHistory,
2244
2280
  shouldCacheSnapshot,
2245
- acceptsStreamResponse
2281
+ acceptsStreamResponse,
2282
+ direction
2246
2283
  } = {
2247
2284
  ...defaultOptions,
2248
2285
  ...options
@@ -2260,6 +2297,7 @@ class Visit {
2260
2297
  this.scrolled = !willRender;
2261
2298
  this.shouldCacheSnapshot = shouldCacheSnapshot;
2262
2299
  this.acceptsStreamResponse = acceptsStreamResponse;
2300
+ this.direction = direction || Direction[action];
2263
2301
  }
2264
2302
 
2265
2303
  get adapter() {
@@ -2512,7 +2550,7 @@ class Visit {
2512
2550
  // Scrolling
2513
2551
 
2514
2552
  performScroll() {
2515
- if (!this.scrolled && !this.view.forceReloaded && !this.view.snapshot.shouldPreserveScrollPosition) {
2553
+ if (!this.scrolled && !this.view.forceReloaded && !this.view.shouldPreserveScrollPosition(this)) {
2516
2554
  if (this.action == "restore") {
2517
2555
  this.scrollToRestoredPosition() || this.scrollToAnchor() || this.view.scrollToTop();
2518
2556
  } else {
@@ -2587,9 +2625,7 @@ class Visit {
2587
2625
 
2588
2626
  async render(callback) {
2589
2627
  this.cancelRender();
2590
- await new Promise((resolve) => {
2591
- this.frame = requestAnimationFrame(() => resolve());
2592
- });
2628
+ this.frame = await nextRepaint();
2593
2629
  await callback();
2594
2630
  delete this.frame;
2595
2631
  }
@@ -2867,6 +2903,7 @@ class History {
2867
2903
  restorationData = {}
2868
2904
  started = false
2869
2905
  pageLoaded = false
2906
+ currentIndex = 0
2870
2907
 
2871
2908
  constructor(delegate) {
2872
2909
  this.delegate = delegate;
@@ -2876,6 +2913,7 @@ class History {
2876
2913
  if (!this.started) {
2877
2914
  addEventListener("popstate", this.onPopState, false);
2878
2915
  addEventListener("load", this.onPageLoad, false);
2916
+ this.currentIndex = history.state?.turbo?.restorationIndex || 0;
2879
2917
  this.started = true;
2880
2918
  this.replace(new URL(window.location.href));
2881
2919
  }
@@ -2898,7 +2936,9 @@ class History {
2898
2936
  }
2899
2937
 
2900
2938
  update(method, location, restorationIdentifier = uuid()) {
2901
- const state = { turbo: { restorationIdentifier } };
2939
+ if (method === history.pushState) ++this.currentIndex;
2940
+
2941
+ const state = { turbo: { restorationIdentifier, restorationIndex: this.currentIndex } };
2902
2942
  method.call(history, state, "", location.href);
2903
2943
  this.location = location;
2904
2944
  this.restorationIdentifier = restorationIdentifier;
@@ -2942,9 +2982,11 @@ class History {
2942
2982
  const { turbo } = event.state || {};
2943
2983
  if (turbo) {
2944
2984
  this.location = new URL(window.location.href);
2945
- const { restorationIdentifier } = turbo;
2985
+ const { restorationIdentifier, restorationIndex } = turbo;
2946
2986
  this.restorationIdentifier = restorationIdentifier;
2947
- this.delegate.historyPoppedToLocationWithRestorationIdentifier(this.location, restorationIdentifier);
2987
+ const direction = restorationIndex > this.currentIndex ? "forward" : "back";
2988
+ this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(this.location, restorationIdentifier, direction);
2989
+ this.currentIndex = restorationIndex;
2948
2990
  }
2949
2991
  }
2950
2992
  }
@@ -3281,7 +3323,7 @@ async function withAutofocusFromFragment(fragment, callback) {
3281
3323
  }
3282
3324
 
3283
3325
  callback();
3284
- await nextAnimationFrame();
3326
+ await nextRepaint();
3285
3327
 
3286
3328
  const hasNoActiveElement = document.activeElement == null || document.activeElement == document.body;
3287
3329
 
@@ -4535,7 +4577,11 @@ class PageView extends View {
4535
4577
  }
4536
4578
 
4537
4579
  isPageRefresh(visit) {
4538
- return !visit || (this.lastRenderedLocation.href === visit.location.href && visit.action === "replace")
4580
+ return !visit || (this.lastRenderedLocation.pathname === visit.location.pathname && visit.action === "replace")
4581
+ }
4582
+
4583
+ shouldPreserveScrollPosition(visit) {
4584
+ return this.isPageRefresh(visit) && this.snapshot.shouldPreserveScrollPosition
4539
4585
  }
4540
4586
 
4541
4587
  get snapshot() {
@@ -4546,27 +4592,28 @@ class PageView extends View {
4546
4592
  class Preloader {
4547
4593
  selector = "a[data-turbo-preload]"
4548
4594
 
4549
- constructor(delegate) {
4595
+ constructor(delegate, snapshotCache) {
4550
4596
  this.delegate = delegate;
4551
- }
4552
-
4553
- get snapshotCache() {
4554
- return this.delegate.navigator.view.snapshotCache
4597
+ this.snapshotCache = snapshotCache;
4555
4598
  }
4556
4599
 
4557
4600
  start() {
4558
4601
  if (document.readyState === "loading") {
4559
- return document.addEventListener("DOMContentLoaded", () => {
4560
- this.preloadOnLoadLinksForView(document.body);
4561
- })
4602
+ document.addEventListener("DOMContentLoaded", this.#preloadAll);
4562
4603
  } else {
4563
4604
  this.preloadOnLoadLinksForView(document.body);
4564
4605
  }
4565
4606
  }
4566
4607
 
4608
+ stop() {
4609
+ document.removeEventListener("DOMContentLoaded", this.#preloadAll);
4610
+ }
4611
+
4567
4612
  preloadOnLoadLinksForView(element) {
4568
4613
  for (const link of element.querySelectorAll(this.selector)) {
4569
- this.preloadURL(link);
4614
+ if (this.delegate.shouldPreloadLink(link)) {
4615
+ this.preloadURL(link);
4616
+ }
4570
4617
  }
4571
4618
  }
4572
4619
 
@@ -4577,31 +4624,39 @@ class Preloader {
4577
4624
  return
4578
4625
  }
4579
4626
 
4627
+ const fetchRequest = new FetchRequest(this, FetchMethod.get, location, new URLSearchParams(), link);
4628
+ await fetchRequest.perform();
4629
+ }
4630
+
4631
+ // Fetch request delegate
4632
+
4633
+ prepareRequest(fetchRequest) {
4634
+ fetchRequest.headers["Sec-Purpose"] = "prefetch";
4635
+ }
4636
+
4637
+ async requestSucceededWithResponse(fetchRequest, fetchResponse) {
4580
4638
  try {
4581
- const response = await fetch(location.toString(), { headers: { "Sec-Purpose": "prefetch", Accept: "text/html" } });
4582
- const responseText = await response.text();
4583
- const snapshot = PageSnapshot.fromHTMLString(responseText);
4639
+ const responseHTML = await fetchResponse.responseHTML;
4640
+ const snapshot = PageSnapshot.fromHTMLString(responseHTML);
4584
4641
 
4585
- this.snapshotCache.put(location, snapshot);
4642
+ this.snapshotCache.put(fetchRequest.url, snapshot);
4586
4643
  } catch (_) {
4587
4644
  // If we cannot preload that is ok!
4588
4645
  }
4589
4646
  }
4590
- }
4591
4647
 
4592
- class LimitedSet extends Set {
4593
- constructor(maxSize) {
4594
- super();
4595
- this.maxSize = maxSize;
4596
- }
4648
+ requestStarted(fetchRequest) {}
4597
4649
 
4598
- add(value) {
4599
- if (this.size >= this.maxSize) {
4600
- const iterator = this.values();
4601
- const oldestValue = iterator.next().value;
4602
- this.delete(oldestValue);
4603
- }
4604
- super.add(value);
4650
+ requestErrored(fetchRequest) {}
4651
+
4652
+ requestFinished(fetchRequest) {}
4653
+
4654
+ requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}
4655
+
4656
+ requestFailedWithResponse(fetchRequest, fetchResponse) {}
4657
+
4658
+ #preloadAll = () => {
4659
+ this.preloadOnLoadLinksForView(document.body);
4605
4660
  }
4606
4661
  }
4607
4662
 
@@ -4634,7 +4689,6 @@ class Cache {
4634
4689
  class Session {
4635
4690
  navigator = new Navigator(this)
4636
4691
  history = new History(this)
4637
- preloader = new Preloader(this)
4638
4692
  view = new PageView(this, document.documentElement)
4639
4693
  adapter = new BrowserAdapter(this)
4640
4694
 
@@ -4648,7 +4702,6 @@ class Session {
4648
4702
  frameRedirector = new FrameRedirector(this, document.documentElement)
4649
4703
  streamMessageRenderer = new StreamMessageRenderer()
4650
4704
  cache = new Cache(this)
4651
- recentRequests = new LimitedSet(20)
4652
4705
 
4653
4706
  drive = true
4654
4707
  enabled = true
@@ -4656,6 +4709,11 @@ class Session {
4656
4709
  started = false
4657
4710
  formMode = "on"
4658
4711
 
4712
+ constructor(recentRequests) {
4713
+ this.recentRequests = recentRequests;
4714
+ this.preloader = new Preloader(this, this.view.snapshotCache);
4715
+ }
4716
+
4659
4717
  start() {
4660
4718
  if (!this.started) {
4661
4719
  this.pageObserver.start();
@@ -4688,6 +4746,7 @@ class Session {
4688
4746
  this.streamObserver.stop();
4689
4747
  this.frameRedirector.stop();
4690
4748
  this.history.stop();
4749
+ this.preloader.stop();
4691
4750
  this.started = false;
4692
4751
  }
4693
4752
  }
@@ -4747,13 +4806,33 @@ class Session {
4747
4806
  return this.history.restorationIdentifier
4748
4807
  }
4749
4808
 
4809
+ // Preloader delegate
4810
+
4811
+ shouldPreloadLink(element) {
4812
+ const isUnsafe = element.hasAttribute("data-turbo-method");
4813
+ const isStream = element.hasAttribute("data-turbo-stream");
4814
+ const frameTarget = element.getAttribute("data-turbo-frame");
4815
+ const frame = frameTarget == "_top" ?
4816
+ null :
4817
+ document.getElementById(frameTarget) || findClosestRecursively(element, "turbo-frame:not([disabled])");
4818
+
4819
+ if (isUnsafe || isStream || frame instanceof FrameElement) {
4820
+ return false
4821
+ } else {
4822
+ const location = new URL(element.href);
4823
+
4824
+ return this.elementIsNavigatable(element) && locationIsVisitable(location, this.snapshot.rootLocation)
4825
+ }
4826
+ }
4827
+
4750
4828
  // History delegate
4751
4829
 
4752
- historyPoppedToLocationWithRestorationIdentifier(location, restorationIdentifier) {
4830
+ historyPoppedToLocationWithRestorationIdentifierAndDirection(location, restorationIdentifier, direction) {
4753
4831
  if (this.enabled) {
4754
4832
  this.navigator.startVisit(location, restorationIdentifier, {
4755
4833
  action: "restore",
4756
- historyChanged: true
4834
+ historyChanged: true,
4835
+ direction
4757
4836
  });
4758
4837
  } else {
4759
4838
  this.adapter.pageInvalidated({
@@ -4809,6 +4888,7 @@ class Session {
4809
4888
  visitStarted(visit) {
4810
4889
  if (!visit.acceptsStreamResponse) {
4811
4890
  markAsBusy(document.documentElement);
4891
+ this.view.markVisitDirection(visit.direction);
4812
4892
  }
4813
4893
  extendURLWithDeprecatedProperties(visit.location);
4814
4894
  if (!visit.silent) {
@@ -4817,6 +4897,7 @@ class Session {
4817
4897
  }
4818
4898
 
4819
4899
  visitCompleted(visit) {
4900
+ this.view.unmarkVisitDirection();
4820
4901
  clearBusyState(document.documentElement);
4821
4902
  this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
4822
4903
  }
@@ -5055,7 +5136,7 @@ const deprecatedLocationPropertyDescriptors = {
5055
5136
  }
5056
5137
  };
5057
5138
 
5058
- const session = new Session();
5139
+ const session = new Session(recentRequests);
5059
5140
  const { cache, navigator: navigator$1 } = session;
5060
5141
 
5061
5142
  /**
@@ -5165,7 +5246,7 @@ var Turbo = /*#__PURE__*/Object.freeze({
5165
5246
  PageRenderer: PageRenderer,
5166
5247
  PageSnapshot: PageSnapshot,
5167
5248
  FrameRenderer: FrameRenderer,
5168
- fetch: fetch,
5249
+ fetch: fetchWithTurboHeaders,
5169
5250
  start: start,
5170
5251
  registerAdapter: registerAdapter,
5171
5252
  visit: visit,
@@ -6034,7 +6115,7 @@ if (customElements.get("turbo-stream-source") === undefined) {
6034
6115
  }
6035
6116
  })();
6036
6117
 
6037
- window.Turbo = Turbo;
6118
+ window.Turbo = { ...Turbo, StreamActions };
6038
6119
  start();
6039
6120
 
6040
- export { FetchEnctype, FetchMethod, FetchRequest, FetchResponse, FrameElement, FrameLoadingStyle, FrameRenderer, PageRenderer, PageSnapshot, StreamActions, StreamElement, StreamSourceElement, cache, clearCache, connectStreamSource, disconnectStreamSource, fetch, fetchEnctypeFromString, fetchMethodFromString, isSafe, navigator$1 as navigator, registerAdapter, renderStreamMessage, session, setConfirmMethod, setFormMode, setProgressBarDelay, start, visit };
6121
+ export { FetchEnctype, FetchMethod, FetchRequest, FetchResponse, FrameElement, FrameLoadingStyle, FrameRenderer, PageRenderer, PageSnapshot, StreamActions, StreamElement, StreamSourceElement, cache, clearCache, connectStreamSource, disconnectStreamSource, fetchWithTurboHeaders as fetch, fetchEnctypeFromString, fetchMethodFromString, isSafe, navigator$1 as navigator, registerAdapter, renderStreamMessage, session, setConfirmMethod, setFormMode, setProgressBarDelay, start, visit };
@@ -1,5 +1,5 @@
1
1
  /*!
2
- Turbo 8.0.0-beta.1
2
+ Turbo 8.0.0-beta.2
3
3
  Copyright © 2023 37signals LLC
4
4
  */
5
5
  (function (global, factory) {
@@ -639,13 +639,33 @@ Copyright © 2023 37signals LLC
639
639
  return [before, after]
640
640
  }
641
641
 
642
- function fetch(url, options = {}) {
642
+ class LimitedSet extends Set {
643
+ constructor(maxSize) {
644
+ super();
645
+ this.maxSize = maxSize;
646
+ }
647
+
648
+ add(value) {
649
+ if (this.size >= this.maxSize) {
650
+ const iterator = this.values();
651
+ const oldestValue = iterator.next().value;
652
+ this.delete(oldestValue);
653
+ }
654
+ super.add(value);
655
+ }
656
+ }
657
+
658
+ const recentRequests = new LimitedSet(20);
659
+
660
+ const nativeFetch = window.fetch;
661
+
662
+ function fetchWithTurboHeaders(url, options = {}) {
643
663
  const modifiedHeaders = new Headers(options.headers || {});
644
664
  const requestUID = uuid();
645
- window.Turbo.session.recentRequests.add(requestUID);
665
+ recentRequests.add(requestUID);
646
666
  modifiedHeaders.append("X-Turbo-Request-Id", requestUID);
647
667
 
648
- return window.fetch(url, {
668
+ return nativeFetch(url, {
649
669
  ...options,
650
670
  headers: modifiedHeaders
651
671
  })
@@ -772,7 +792,7 @@ Copyright © 2023 37signals LLC
772
792
  await this.#allowRequestToBeIntercepted(fetchOptions);
773
793
  try {
774
794
  this.delegate.requestStarted(this);
775
- const response = await fetch(this.url.href, fetchOptions);
795
+ const response = await fetchWithTurboHeaders(this.url.href, fetchOptions);
776
796
  return await this.receive(response)
777
797
  } catch (error) {
778
798
  if (error.name !== "AbortError") {
@@ -1052,6 +1072,7 @@ Copyright © 2023 37signals LLC
1052
1072
  this.state = FormSubmissionState.waiting;
1053
1073
  this.submitter?.setAttribute("disabled", "");
1054
1074
  this.setSubmitsWith();
1075
+ markAsBusy(this.formElement);
1055
1076
  dispatch("turbo:submit-start", {
1056
1077
  target: this.formElement,
1057
1078
  detail: { formSubmission: this }
@@ -1090,6 +1111,7 @@ Copyright © 2023 37signals LLC
1090
1111
  this.state = FormSubmissionState.stopped;
1091
1112
  this.submitter?.removeAttribute("disabled");
1092
1113
  this.resetSubmitterText();
1114
+ clearBusyState(this.formElement);
1093
1115
  dispatch("turbo:submit-end", {
1094
1116
  target: this.formElement,
1095
1117
  detail: { formSubmission: this, ...this.result }
@@ -1417,6 +1439,14 @@ Copyright © 2023 37signals LLC
1417
1439
  }
1418
1440
  }
1419
1441
 
1442
+ markVisitDirection(direction) {
1443
+ this.element.setAttribute("data-turbo-visit-direction", direction);
1444
+ }
1445
+
1446
+ unmarkVisitDirection() {
1447
+ this.element.removeAttribute("data-turbo-visit-direction");
1448
+ }
1449
+
1420
1450
  async renderSnapshot(renderer) {
1421
1451
  await renderer.render();
1422
1452
  }
@@ -1786,14 +1816,14 @@ Copyright © 2023 37signals LLC
1786
1816
  }
1787
1817
 
1788
1818
  async render() {
1789
- await nextAnimationFrame();
1819
+ await nextRepaint();
1790
1820
  this.preservingPermanentElements(() => {
1791
1821
  this.loadFrameElement();
1792
1822
  });
1793
1823
  this.scrollFrameIntoView();
1794
- await nextAnimationFrame();
1824
+ await nextRepaint();
1795
1825
  this.focusFirstAutofocusableElement();
1796
- await nextAnimationFrame();
1826
+ await nextRepaint();
1797
1827
  this.activateScriptElements();
1798
1828
  }
1799
1829
 
@@ -2219,6 +2249,12 @@ Copyright © 2023 37signals LLC
2219
2249
  contentTypeMismatch: -2
2220
2250
  };
2221
2251
 
2252
+ const Direction = {
2253
+ advance: "forward",
2254
+ restore: "back",
2255
+ replace: "none"
2256
+ };
2257
+
2222
2258
  class Visit {
2223
2259
  identifier = uuid() // Required by turbo-ios
2224
2260
  timingMetrics = {}
@@ -2248,7 +2284,8 @@ Copyright © 2023 37signals LLC
2248
2284
  willRender,
2249
2285
  updateHistory,
2250
2286
  shouldCacheSnapshot,
2251
- acceptsStreamResponse
2287
+ acceptsStreamResponse,
2288
+ direction
2252
2289
  } = {
2253
2290
  ...defaultOptions,
2254
2291
  ...options
@@ -2266,6 +2303,7 @@ Copyright © 2023 37signals LLC
2266
2303
  this.scrolled = !willRender;
2267
2304
  this.shouldCacheSnapshot = shouldCacheSnapshot;
2268
2305
  this.acceptsStreamResponse = acceptsStreamResponse;
2306
+ this.direction = direction || Direction[action];
2269
2307
  }
2270
2308
 
2271
2309
  get adapter() {
@@ -2518,7 +2556,7 @@ Copyright © 2023 37signals LLC
2518
2556
  // Scrolling
2519
2557
 
2520
2558
  performScroll() {
2521
- if (!this.scrolled && !this.view.forceReloaded && !this.view.snapshot.shouldPreserveScrollPosition) {
2559
+ if (!this.scrolled && !this.view.forceReloaded && !this.view.shouldPreserveScrollPosition(this)) {
2522
2560
  if (this.action == "restore") {
2523
2561
  this.scrollToRestoredPosition() || this.scrollToAnchor() || this.view.scrollToTop();
2524
2562
  } else {
@@ -2593,9 +2631,7 @@ Copyright © 2023 37signals LLC
2593
2631
 
2594
2632
  async render(callback) {
2595
2633
  this.cancelRender();
2596
- await new Promise((resolve) => {
2597
- this.frame = requestAnimationFrame(() => resolve());
2598
- });
2634
+ this.frame = await nextRepaint();
2599
2635
  await callback();
2600
2636
  delete this.frame;
2601
2637
  }
@@ -2873,6 +2909,7 @@ Copyright © 2023 37signals LLC
2873
2909
  restorationData = {}
2874
2910
  started = false
2875
2911
  pageLoaded = false
2912
+ currentIndex = 0
2876
2913
 
2877
2914
  constructor(delegate) {
2878
2915
  this.delegate = delegate;
@@ -2882,6 +2919,7 @@ Copyright © 2023 37signals LLC
2882
2919
  if (!this.started) {
2883
2920
  addEventListener("popstate", this.onPopState, false);
2884
2921
  addEventListener("load", this.onPageLoad, false);
2922
+ this.currentIndex = history.state?.turbo?.restorationIndex || 0;
2885
2923
  this.started = true;
2886
2924
  this.replace(new URL(window.location.href));
2887
2925
  }
@@ -2904,7 +2942,9 @@ Copyright © 2023 37signals LLC
2904
2942
  }
2905
2943
 
2906
2944
  update(method, location, restorationIdentifier = uuid()) {
2907
- const state = { turbo: { restorationIdentifier } };
2945
+ if (method === history.pushState) ++this.currentIndex;
2946
+
2947
+ const state = { turbo: { restorationIdentifier, restorationIndex: this.currentIndex } };
2908
2948
  method.call(history, state, "", location.href);
2909
2949
  this.location = location;
2910
2950
  this.restorationIdentifier = restorationIdentifier;
@@ -2948,9 +2988,11 @@ Copyright © 2023 37signals LLC
2948
2988
  const { turbo } = event.state || {};
2949
2989
  if (turbo) {
2950
2990
  this.location = new URL(window.location.href);
2951
- const { restorationIdentifier } = turbo;
2991
+ const { restorationIdentifier, restorationIndex } = turbo;
2952
2992
  this.restorationIdentifier = restorationIdentifier;
2953
- this.delegate.historyPoppedToLocationWithRestorationIdentifier(this.location, restorationIdentifier);
2993
+ const direction = restorationIndex > this.currentIndex ? "forward" : "back";
2994
+ this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(this.location, restorationIdentifier, direction);
2995
+ this.currentIndex = restorationIndex;
2954
2996
  }
2955
2997
  }
2956
2998
  }
@@ -3287,7 +3329,7 @@ Copyright © 2023 37signals LLC
3287
3329
  }
3288
3330
 
3289
3331
  callback();
3290
- await nextAnimationFrame();
3332
+ await nextRepaint();
3291
3333
 
3292
3334
  const hasNoActiveElement = document.activeElement == null || document.activeElement == document.body;
3293
3335
 
@@ -4541,7 +4583,11 @@ Copyright © 2023 37signals LLC
4541
4583
  }
4542
4584
 
4543
4585
  isPageRefresh(visit) {
4544
- return !visit || (this.lastRenderedLocation.href === visit.location.href && visit.action === "replace")
4586
+ return !visit || (this.lastRenderedLocation.pathname === visit.location.pathname && visit.action === "replace")
4587
+ }
4588
+
4589
+ shouldPreserveScrollPosition(visit) {
4590
+ return this.isPageRefresh(visit) && this.snapshot.shouldPreserveScrollPosition
4545
4591
  }
4546
4592
 
4547
4593
  get snapshot() {
@@ -4552,27 +4598,28 @@ Copyright © 2023 37signals LLC
4552
4598
  class Preloader {
4553
4599
  selector = "a[data-turbo-preload]"
4554
4600
 
4555
- constructor(delegate) {
4601
+ constructor(delegate, snapshotCache) {
4556
4602
  this.delegate = delegate;
4557
- }
4558
-
4559
- get snapshotCache() {
4560
- return this.delegate.navigator.view.snapshotCache
4603
+ this.snapshotCache = snapshotCache;
4561
4604
  }
4562
4605
 
4563
4606
  start() {
4564
4607
  if (document.readyState === "loading") {
4565
- return document.addEventListener("DOMContentLoaded", () => {
4566
- this.preloadOnLoadLinksForView(document.body);
4567
- })
4608
+ document.addEventListener("DOMContentLoaded", this.#preloadAll);
4568
4609
  } else {
4569
4610
  this.preloadOnLoadLinksForView(document.body);
4570
4611
  }
4571
4612
  }
4572
4613
 
4614
+ stop() {
4615
+ document.removeEventListener("DOMContentLoaded", this.#preloadAll);
4616
+ }
4617
+
4573
4618
  preloadOnLoadLinksForView(element) {
4574
4619
  for (const link of element.querySelectorAll(this.selector)) {
4575
- this.preloadURL(link);
4620
+ if (this.delegate.shouldPreloadLink(link)) {
4621
+ this.preloadURL(link);
4622
+ }
4576
4623
  }
4577
4624
  }
4578
4625
 
@@ -4583,31 +4630,39 @@ Copyright © 2023 37signals LLC
4583
4630
  return
4584
4631
  }
4585
4632
 
4633
+ const fetchRequest = new FetchRequest(this, FetchMethod.get, location, new URLSearchParams(), link);
4634
+ await fetchRequest.perform();
4635
+ }
4636
+
4637
+ // Fetch request delegate
4638
+
4639
+ prepareRequest(fetchRequest) {
4640
+ fetchRequest.headers["Sec-Purpose"] = "prefetch";
4641
+ }
4642
+
4643
+ async requestSucceededWithResponse(fetchRequest, fetchResponse) {
4586
4644
  try {
4587
- const response = await fetch(location.toString(), { headers: { "Sec-Purpose": "prefetch", Accept: "text/html" } });
4588
- const responseText = await response.text();
4589
- const snapshot = PageSnapshot.fromHTMLString(responseText);
4645
+ const responseHTML = await fetchResponse.responseHTML;
4646
+ const snapshot = PageSnapshot.fromHTMLString(responseHTML);
4590
4647
 
4591
- this.snapshotCache.put(location, snapshot);
4648
+ this.snapshotCache.put(fetchRequest.url, snapshot);
4592
4649
  } catch (_) {
4593
4650
  // If we cannot preload that is ok!
4594
4651
  }
4595
4652
  }
4596
- }
4597
4653
 
4598
- class LimitedSet extends Set {
4599
- constructor(maxSize) {
4600
- super();
4601
- this.maxSize = maxSize;
4602
- }
4654
+ requestStarted(fetchRequest) {}
4603
4655
 
4604
- add(value) {
4605
- if (this.size >= this.maxSize) {
4606
- const iterator = this.values();
4607
- const oldestValue = iterator.next().value;
4608
- this.delete(oldestValue);
4609
- }
4610
- super.add(value);
4656
+ requestErrored(fetchRequest) {}
4657
+
4658
+ requestFinished(fetchRequest) {}
4659
+
4660
+ requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}
4661
+
4662
+ requestFailedWithResponse(fetchRequest, fetchResponse) {}
4663
+
4664
+ #preloadAll = () => {
4665
+ this.preloadOnLoadLinksForView(document.body);
4611
4666
  }
4612
4667
  }
4613
4668
 
@@ -4640,7 +4695,6 @@ Copyright © 2023 37signals LLC
4640
4695
  class Session {
4641
4696
  navigator = new Navigator(this)
4642
4697
  history = new History(this)
4643
- preloader = new Preloader(this)
4644
4698
  view = new PageView(this, document.documentElement)
4645
4699
  adapter = new BrowserAdapter(this)
4646
4700
 
@@ -4654,7 +4708,6 @@ Copyright © 2023 37signals LLC
4654
4708
  frameRedirector = new FrameRedirector(this, document.documentElement)
4655
4709
  streamMessageRenderer = new StreamMessageRenderer()
4656
4710
  cache = new Cache(this)
4657
- recentRequests = new LimitedSet(20)
4658
4711
 
4659
4712
  drive = true
4660
4713
  enabled = true
@@ -4662,6 +4715,11 @@ Copyright © 2023 37signals LLC
4662
4715
  started = false
4663
4716
  formMode = "on"
4664
4717
 
4718
+ constructor(recentRequests) {
4719
+ this.recentRequests = recentRequests;
4720
+ this.preloader = new Preloader(this, this.view.snapshotCache);
4721
+ }
4722
+
4665
4723
  start() {
4666
4724
  if (!this.started) {
4667
4725
  this.pageObserver.start();
@@ -4694,6 +4752,7 @@ Copyright © 2023 37signals LLC
4694
4752
  this.streamObserver.stop();
4695
4753
  this.frameRedirector.stop();
4696
4754
  this.history.stop();
4755
+ this.preloader.stop();
4697
4756
  this.started = false;
4698
4757
  }
4699
4758
  }
@@ -4753,13 +4812,33 @@ Copyright © 2023 37signals LLC
4753
4812
  return this.history.restorationIdentifier
4754
4813
  }
4755
4814
 
4815
+ // Preloader delegate
4816
+
4817
+ shouldPreloadLink(element) {
4818
+ const isUnsafe = element.hasAttribute("data-turbo-method");
4819
+ const isStream = element.hasAttribute("data-turbo-stream");
4820
+ const frameTarget = element.getAttribute("data-turbo-frame");
4821
+ const frame = frameTarget == "_top" ?
4822
+ null :
4823
+ document.getElementById(frameTarget) || findClosestRecursively(element, "turbo-frame:not([disabled])");
4824
+
4825
+ if (isUnsafe || isStream || frame instanceof FrameElement) {
4826
+ return false
4827
+ } else {
4828
+ const location = new URL(element.href);
4829
+
4830
+ return this.elementIsNavigatable(element) && locationIsVisitable(location, this.snapshot.rootLocation)
4831
+ }
4832
+ }
4833
+
4756
4834
  // History delegate
4757
4835
 
4758
- historyPoppedToLocationWithRestorationIdentifier(location, restorationIdentifier) {
4836
+ historyPoppedToLocationWithRestorationIdentifierAndDirection(location, restorationIdentifier, direction) {
4759
4837
  if (this.enabled) {
4760
4838
  this.navigator.startVisit(location, restorationIdentifier, {
4761
4839
  action: "restore",
4762
- historyChanged: true
4840
+ historyChanged: true,
4841
+ direction
4763
4842
  });
4764
4843
  } else {
4765
4844
  this.adapter.pageInvalidated({
@@ -4815,6 +4894,7 @@ Copyright © 2023 37signals LLC
4815
4894
  visitStarted(visit) {
4816
4895
  if (!visit.acceptsStreamResponse) {
4817
4896
  markAsBusy(document.documentElement);
4897
+ this.view.markVisitDirection(visit.direction);
4818
4898
  }
4819
4899
  extendURLWithDeprecatedProperties(visit.location);
4820
4900
  if (!visit.silent) {
@@ -4823,6 +4903,7 @@ Copyright © 2023 37signals LLC
4823
4903
  }
4824
4904
 
4825
4905
  visitCompleted(visit) {
4906
+ this.view.unmarkVisitDirection();
4826
4907
  clearBusyState(document.documentElement);
4827
4908
  this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
4828
4909
  }
@@ -5061,7 +5142,7 @@ Copyright © 2023 37signals LLC
5061
5142
  }
5062
5143
  };
5063
5144
 
5064
- const session = new Session();
5145
+ const session = new Session(recentRequests);
5065
5146
  const { cache, navigator: navigator$1 } = session;
5066
5147
 
5067
5148
  /**
@@ -5171,7 +5252,7 @@ Copyright © 2023 37signals LLC
5171
5252
  PageRenderer: PageRenderer,
5172
5253
  PageSnapshot: PageSnapshot,
5173
5254
  FrameRenderer: FrameRenderer,
5174
- fetch: fetch,
5255
+ fetch: fetchWithTurboHeaders,
5175
5256
  start: start,
5176
5257
  registerAdapter: registerAdapter,
5177
5258
  visit: visit,
@@ -6040,7 +6121,7 @@ Copyright © 2023 37signals LLC
6040
6121
  }
6041
6122
  })();
6042
6123
 
6043
- window.Turbo = Turbo;
6124
+ window.Turbo = { ...Turbo, StreamActions };
6044
6125
  start();
6045
6126
 
6046
6127
  exports.FetchEnctype = FetchEnctype;
@@ -6059,7 +6140,7 @@ Copyright © 2023 37signals LLC
6059
6140
  exports.clearCache = clearCache;
6060
6141
  exports.connectStreamSource = connectStreamSource;
6061
6142
  exports.disconnectStreamSource = disconnectStreamSource;
6062
- exports.fetch = fetch;
6143
+ exports.fetch = fetchWithTurboHeaders;
6063
6144
  exports.fetchEnctypeFromString = fetchEnctypeFromString;
6064
6145
  exports.fetchMethodFromString = fetchMethodFromString;
6065
6146
  exports.isSafe = isSafe;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotwired/turbo",
3
- "version": "8.0.0-beta.1",
3
+ "version": "8.0.0-beta.2",
4
4
  "description": "The speed of a single-page web application without having to write any JavaScript",
5
5
  "module": "dist/turbo.es2017-esm.js",
6
6
  "main": "dist/turbo.es2017-umd.js",
@@ -32,9 +32,6 @@
32
32
  "publishConfig": {
33
33
  "access": "public"
34
34
  },
35
- "dependencies": {
36
- "idiomorph": "https://github.com/basecamp/idiomorph#rollout-build"
37
- },
38
35
  "devDependencies": {
39
36
  "@open-wc/testing": "^3.1.7",
40
37
  "@playwright/test": "^1.28.0",
@@ -47,6 +44,7 @@
47
44
  "chai": "~4.3.4",
48
45
  "eslint": "^8.13.0",
49
46
  "express": "^4.18.2",
47
+ "idiomorph": "https://github.com/basecamp/idiomorph#rollout-build",
50
48
  "multer": "^1.4.2",
51
49
  "rollup": "^2.35.1"
52
50
  },