@hotwired/turbo 8.0.20 → 8.0.21

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,104 +1,7 @@
1
1
  /*!
2
- Turbo 8.0.19
3
- Copyright © 2025 37signals LLC
2
+ Turbo 8.0.21
3
+ Copyright © 2026 37signals LLC
4
4
  */
5
- /**
6
- * The MIT License (MIT)
7
- *
8
- * Copyright (c) 2019 Javan Makhmali
9
- *
10
- * Permission is hereby granted, free of charge, to any person obtaining a copy
11
- * of this software and associated documentation files (the "Software"), to deal
12
- * in the Software without restriction, including without limitation the rights
13
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
- * copies of the Software, and to permit persons to whom the Software is
15
- * furnished to do so, subject to the following conditions:
16
- *
17
- * The above copyright notice and this permission notice shall be included in
18
- * all copies or substantial portions of the Software.
19
- *
20
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26
- * THE SOFTWARE.
27
- */
28
-
29
- (function (prototype) {
30
- if (typeof prototype.requestSubmit == "function") return
31
-
32
- prototype.requestSubmit = function (submitter) {
33
- if (submitter) {
34
- validateSubmitter(submitter, this);
35
- submitter.click();
36
- } else {
37
- submitter = document.createElement("input");
38
- submitter.type = "submit";
39
- submitter.hidden = true;
40
- this.appendChild(submitter);
41
- submitter.click();
42
- this.removeChild(submitter);
43
- }
44
- };
45
-
46
- function validateSubmitter(submitter, form) {
47
- submitter instanceof HTMLElement || raise(TypeError, "parameter 1 is not of type 'HTMLElement'");
48
- submitter.type == "submit" || raise(TypeError, "The specified element is not a submit button");
49
- submitter.form == form ||
50
- raise(DOMException, "The specified element is not owned by this form element", "NotFoundError");
51
- }
52
-
53
- function raise(errorConstructor, message, name) {
54
- throw new errorConstructor("Failed to execute 'requestSubmit' on 'HTMLFormElement': " + message + ".", name)
55
- }
56
- })(HTMLFormElement.prototype);
57
-
58
- const submittersByForm = new WeakMap();
59
-
60
- function findSubmitterFromClickTarget(target) {
61
- const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
62
- const candidate = element ? element.closest("input, button") : null;
63
- return candidate?.type == "submit" ? candidate : null
64
- }
65
-
66
- function clickCaptured(event) {
67
- const submitter = findSubmitterFromClickTarget(event.target);
68
-
69
- if (submitter && submitter.form) {
70
- submittersByForm.set(submitter.form, submitter);
71
- }
72
- }
73
-
74
- (function () {
75
- if ("submitter" in Event.prototype) return
76
-
77
- let prototype = window.Event.prototype;
78
- // Certain versions of Safari 15 have a bug where they won't
79
- // populate the submitter. This hurts TurboDrive's enable/disable detection.
80
- // See https://bugs.webkit.org/show_bug.cgi?id=229660
81
- if ("SubmitEvent" in window) {
82
- const prototypeOfSubmitEvent = window.SubmitEvent.prototype;
83
-
84
- if (/Apple Computer/.test(navigator.vendor) && !("submitter" in prototypeOfSubmitEvent)) {
85
- prototype = prototypeOfSubmitEvent;
86
- } else {
87
- return // polyfill not needed
88
- }
89
- }
90
-
91
- addEventListener("click", clickCaptured, true);
92
-
93
- Object.defineProperty(prototype, "submitter", {
94
- get() {
95
- if (this.type == "submit" && this.target instanceof HTMLFormElement) {
96
- return submittersByForm.get(this.target)
97
- }
98
- }
99
- });
100
- })();
101
-
102
5
  const FrameLoadingStyle = {
103
6
  eager: "eager",
104
7
  lazy: "lazy"
@@ -374,10 +277,6 @@ function nextEventLoopTick() {
374
277
  return new Promise((resolve) => setTimeout(() => resolve(), 0))
375
278
  }
376
279
 
377
- function nextMicrotask() {
378
- return Promise.resolve()
379
- }
380
-
381
280
  function parseHTMLDocument(html = "") {
382
281
  return new DOMParser().parseFromString(html, "text/html")
383
282
  }
@@ -406,7 +305,7 @@ function uuid() {
406
305
  } else if (i == 19) {
407
306
  return (Math.floor(Math.random() * 4) + 8).toString(16)
408
307
  } else {
409
- return Math.floor(Math.random() * 15).toString(16)
308
+ return Math.floor(Math.random() * 16).toString(16)
410
309
  }
411
310
  })
412
311
  .join("")
@@ -558,14 +457,13 @@ function findLinkFromClickTarget(target) {
558
457
  const link = findClosestRecursively(target, "a[href], a[xlink\\:href]");
559
458
 
560
459
  if (!link) return null
460
+ if (link.href.startsWith("#")) return null
561
461
  if (link.hasAttribute("download")) return null
562
- if (link.hasAttribute("target") && link.target !== "_self") return null
563
462
 
564
- return link
565
- }
463
+ const linkTarget = link.getAttribute("target");
464
+ if (linkTarget && linkTarget !== "_self") return null
566
465
 
567
- function getLocationForLink(link) {
568
- return expandURL(link.getAttribute("href") || "")
466
+ return link
569
467
  }
570
468
 
571
469
  function debounce(fn, delay) {
@@ -656,6 +554,10 @@ function locationIsVisitable(location, rootLocation) {
656
554
  return isPrefixedBy(location, rootLocation) && !config.drive.unvisitableExtensions.has(getExtension(location))
657
555
  }
658
556
 
557
+ function getLocationForLink(link) {
558
+ return expandURL(link.getAttribute("href") || "")
559
+ }
560
+
659
561
  function getRequestURL(url) {
660
562
  const anchor = getAnchor(url);
661
563
  return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href
@@ -1071,35 +973,113 @@ function importStreamElements(fragment) {
1071
973
  return fragment
1072
974
  }
1073
975
 
1074
- const PREFETCH_DELAY = 100;
976
+ const identity = key => key;
1075
977
 
1076
- class PrefetchCache {
1077
- #prefetchTimeout = null
1078
- #prefetched = null
978
+ class LRUCache {
979
+ keys = []
980
+ entries = {}
981
+ #toCacheKey
982
+
983
+ constructor(size, toCacheKey = identity) {
984
+ this.size = size;
985
+ this.#toCacheKey = toCacheKey;
986
+ }
1079
987
 
1080
- get(url) {
1081
- if (this.#prefetched && this.#prefetched.url === url && this.#prefetched.expire > Date.now()) {
1082
- return this.#prefetched.request
988
+ has(key) {
989
+ return this.#toCacheKey(key) in this.entries
990
+ }
991
+
992
+ get(key) {
993
+ if (this.has(key)) {
994
+ const entry = this.read(key);
995
+ this.touch(key);
996
+ return entry
997
+ }
998
+ }
999
+
1000
+ put(key, entry) {
1001
+ this.write(key, entry);
1002
+ this.touch(key);
1003
+ return entry
1004
+ }
1005
+
1006
+ clear() {
1007
+ for (const key of Object.keys(this.entries)) {
1008
+ this.evict(key);
1009
+ }
1010
+ }
1011
+
1012
+ // Private
1013
+
1014
+ read(key) {
1015
+ return this.entries[this.#toCacheKey(key)]
1016
+ }
1017
+
1018
+ write(key, entry) {
1019
+ this.entries[this.#toCacheKey(key)] = entry;
1020
+ }
1021
+
1022
+ touch(key) {
1023
+ key = this.#toCacheKey(key);
1024
+ const index = this.keys.indexOf(key);
1025
+ if (index > -1) this.keys.splice(index, 1);
1026
+ this.keys.unshift(key);
1027
+ this.trim();
1028
+ }
1029
+
1030
+ trim() {
1031
+ for (const key of this.keys.splice(this.size)) {
1032
+ this.evict(key);
1083
1033
  }
1084
1034
  }
1085
1035
 
1086
- setLater(url, request, ttl) {
1087
- this.clear();
1036
+ evict(key) {
1037
+ delete this.entries[key];
1038
+ }
1039
+ }
1040
+
1041
+ const PREFETCH_DELAY = 100;
1042
+
1043
+ class PrefetchCache extends LRUCache {
1044
+ #prefetchTimeout = null
1045
+ #maxAges = {}
1046
+
1047
+ constructor(size = 1, prefetchDelay = PREFETCH_DELAY) {
1048
+ super(size, toCacheKey);
1049
+ this.prefetchDelay = prefetchDelay;
1050
+ }
1088
1051
 
1052
+ putLater(url, request, ttl) {
1089
1053
  this.#prefetchTimeout = setTimeout(() => {
1090
1054
  request.perform();
1091
- this.set(url, request, ttl);
1055
+ this.put(url, request, ttl);
1092
1056
  this.#prefetchTimeout = null;
1093
- }, PREFETCH_DELAY);
1057
+ }, this.prefetchDelay);
1094
1058
  }
1095
1059
 
1096
- set(url, request, ttl) {
1097
- this.#prefetched = { url, request, expire: new Date(new Date().getTime() + ttl) };
1060
+ put(url, request, ttl = cacheTtl) {
1061
+ super.put(url, request);
1062
+ this.#maxAges[toCacheKey(url)] = new Date(new Date().getTime() + ttl);
1098
1063
  }
1099
1064
 
1100
1065
  clear() {
1066
+ super.clear();
1101
1067
  if (this.#prefetchTimeout) clearTimeout(this.#prefetchTimeout);
1102
- this.#prefetched = null;
1068
+ }
1069
+
1070
+ evict(key) {
1071
+ super.evict(key);
1072
+ delete this.#maxAges[key];
1073
+ }
1074
+
1075
+ has(key) {
1076
+ if (super.has(key)) {
1077
+ const maxAge = this.#maxAges[toCacheKey(key)];
1078
+
1079
+ return maxAge && maxAge > Date.now()
1080
+ } else {
1081
+ return false
1082
+ }
1103
1083
  }
1104
1084
  }
1105
1085
 
@@ -3802,6 +3782,10 @@ class PageSnapshot extends Snapshot {
3802
3782
  clonedPasswordInput.value = "";
3803
3783
  }
3804
3784
 
3785
+ for (const clonedNoscriptElement of clonedElement.querySelectorAll("noscript")) {
3786
+ clonedNoscriptElement.remove();
3787
+ }
3788
+
3805
3789
  return new PageSnapshot(this.documentElement, clonedElement, this.headSnapshot)
3806
3790
  }
3807
3791
 
@@ -3809,6 +3793,10 @@ class PageSnapshot extends Snapshot {
3809
3793
  return this.documentElement.getAttribute("lang")
3810
3794
  }
3811
3795
 
3796
+ get dir() {
3797
+ return this.documentElement.getAttribute("dir")
3798
+ }
3799
+
3812
3800
  get headElement() {
3813
3801
  return this.headSnapshot.element
3814
3802
  }
@@ -3839,12 +3827,12 @@ class PageSnapshot extends Snapshot {
3839
3827
  return viewTransitionEnabled && !window.matchMedia("(prefers-reduced-motion: reduce)").matches
3840
3828
  }
3841
3829
 
3842
- get shouldMorphPage() {
3843
- return this.getSetting("refresh-method") === "morph"
3830
+ get refreshMethod() {
3831
+ return this.getSetting("refresh-method")
3844
3832
  }
3845
3833
 
3846
- get shouldPreserveScrollPosition() {
3847
- return this.getSetting("refresh-scroll") === "preserve"
3834
+ get refreshScroll() {
3835
+ return this.getSetting("refresh-scroll")
3848
3836
  }
3849
3837
 
3850
3838
  // Private
@@ -3883,7 +3871,8 @@ const defaultOptions = {
3883
3871
  willRender: true,
3884
3872
  updateHistory: true,
3885
3873
  shouldCacheSnapshot: true,
3886
- acceptsStreamResponse: false
3874
+ acceptsStreamResponse: false,
3875
+ refresh: {}
3887
3876
  };
3888
3877
 
3889
3878
  const TimingMetric = {
@@ -3943,7 +3932,8 @@ class Visit {
3943
3932
  updateHistory,
3944
3933
  shouldCacheSnapshot,
3945
3934
  acceptsStreamResponse,
3946
- direction
3935
+ direction,
3936
+ refresh
3947
3937
  } = {
3948
3938
  ...defaultOptions,
3949
3939
  ...options
@@ -3954,7 +3944,6 @@ class Visit {
3954
3944
  this.snapshot = snapshot;
3955
3945
  this.snapshotHTML = snapshotHTML;
3956
3946
  this.response = response;
3957
- this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
3958
3947
  this.isPageRefresh = this.view.isPageRefresh(this);
3959
3948
  this.visitCachedSnapshot = visitCachedSnapshot;
3960
3949
  this.willRender = willRender;
@@ -3963,6 +3952,7 @@ class Visit {
3963
3952
  this.shouldCacheSnapshot = shouldCacheSnapshot;
3964
3953
  this.acceptsStreamResponse = acceptsStreamResponse;
3965
3954
  this.direction = direction || Direction[action];
3955
+ this.refresh = refresh;
3966
3956
  }
3967
3957
 
3968
3958
  get adapter() {
@@ -3981,10 +3971,6 @@ class Visit {
3981
3971
  return this.history.getRestorationDataForIdentifier(this.restorationIdentifier)
3982
3972
  }
3983
3973
 
3984
- get silent() {
3985
- return this.isSamePage
3986
- }
3987
-
3988
3974
  start() {
3989
3975
  if (this.state == VisitState.initialized) {
3990
3976
  this.recordTimingMetric(TimingMetric.visitStart);
@@ -4121,7 +4107,7 @@ class Visit {
4121
4107
  const isPreview = this.shouldIssueRequest();
4122
4108
  this.render(async () => {
4123
4109
  this.cacheSnapshot();
4124
- if (this.isSamePage || this.isPageRefresh) {
4110
+ if (this.isPageRefresh) {
4125
4111
  this.adapter.visitRendered(this);
4126
4112
  } else {
4127
4113
  if (this.view.renderPromise) await this.view.renderPromise;
@@ -4149,17 +4135,6 @@ class Visit {
4149
4135
  }
4150
4136
  }
4151
4137
 
4152
- goToSamePageAnchor() {
4153
- if (this.isSamePage) {
4154
- this.render(async () => {
4155
- this.cacheSnapshot();
4156
- this.performScroll();
4157
- this.changeHistory();
4158
- this.adapter.visitRendered(this);
4159
- });
4160
- }
4161
- }
4162
-
4163
4138
  // Fetch request delegate
4164
4139
 
4165
4140
  prepareRequest(request) {
@@ -4221,9 +4196,6 @@ class Visit {
4221
4196
  } else {
4222
4197
  this.scrollToAnchor() || this.view.scrollToTop();
4223
4198
  }
4224
- if (this.isSamePage) {
4225
- this.delegate.visitScrolledToSamePageLocation(this.view.lastRenderedLocation, this.location);
4226
- }
4227
4199
 
4228
4200
  this.scrolled = true;
4229
4201
  }
@@ -4262,9 +4234,7 @@ class Visit {
4262
4234
  }
4263
4235
 
4264
4236
  shouldIssueRequest() {
4265
- if (this.isSamePage) {
4266
- return false
4267
- } else if (this.action == "restore") {
4237
+ if (this.action == "restore") {
4268
4238
  return !this.hasCachedSnapshot()
4269
4239
  } else {
4270
4240
  return this.willRender
@@ -4328,7 +4298,6 @@ class BrowserAdapter {
4328
4298
 
4329
4299
  visit.loadCachedSnapshot();
4330
4300
  visit.issueRequest();
4331
- visit.goToSamePageAnchor();
4332
4301
  }
4333
4302
 
4334
4303
  visitRequestStarted(visit) {
@@ -4445,7 +4414,6 @@ class BrowserAdapter {
4445
4414
 
4446
4415
  class CacheObserver {
4447
4416
  selector = "[data-turbo-temporary]"
4448
- deprecatedSelector = "[data-turbo-cache=false]"
4449
4417
 
4450
4418
  started = false
4451
4419
 
@@ -4470,19 +4438,7 @@ class CacheObserver {
4470
4438
  }
4471
4439
 
4472
4440
  get temporaryElements() {
4473
- return [...document.querySelectorAll(this.selector), ...this.temporaryElementsWithDeprecation]
4474
- }
4475
-
4476
- get temporaryElementsWithDeprecation() {
4477
- const elements = document.querySelectorAll(this.deprecatedSelector);
4478
-
4479
- if (elements.length) {
4480
- console.warn(
4481
- `The ${this.deprecatedSelector} selector is deprecated and will be removed in a future version. Use ${this.selector} instead.`
4482
- );
4483
- }
4484
-
4485
- return [...elements]
4441
+ return [...document.querySelectorAll(this.selector)]
4486
4442
  }
4487
4443
  }
4488
4444
 
@@ -4572,7 +4528,6 @@ class History {
4572
4528
  restorationIdentifier = uuid()
4573
4529
  restorationData = {}
4574
4530
  started = false
4575
- pageLoaded = false
4576
4531
  currentIndex = 0
4577
4532
 
4578
4533
  constructor(delegate) {
@@ -4582,7 +4537,6 @@ class History {
4582
4537
  start() {
4583
4538
  if (!this.started) {
4584
4539
  addEventListener("popstate", this.onPopState, false);
4585
- addEventListener("load", this.onPageLoad, false);
4586
4540
  this.currentIndex = history.state?.turbo?.restorationIndex || 0;
4587
4541
  this.started = true;
4588
4542
  this.replace(new URL(window.location.href));
@@ -4592,7 +4546,6 @@ class History {
4592
4546
  stop() {
4593
4547
  if (this.started) {
4594
4548
  removeEventListener("popstate", this.onPopState, false);
4595
- removeEventListener("load", this.onPageLoad, false);
4596
4549
  this.started = false;
4597
4550
  }
4598
4551
  }
@@ -4648,34 +4601,20 @@ class History {
4648
4601
  // Event handlers
4649
4602
 
4650
4603
  onPopState = (event) => {
4651
- if (this.shouldHandlePopState()) {
4652
- const { turbo } = event.state || {};
4653
- if (turbo) {
4654
- this.location = new URL(window.location.href);
4655
- const { restorationIdentifier, restorationIndex } = turbo;
4656
- this.restorationIdentifier = restorationIdentifier;
4657
- const direction = restorationIndex > this.currentIndex ? "forward" : "back";
4658
- this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(this.location, restorationIdentifier, direction);
4659
- this.currentIndex = restorationIndex;
4660
- }
4604
+ const { turbo } = event.state || {};
4605
+ this.location = new URL(window.location.href);
4606
+
4607
+ if (turbo) {
4608
+ const { restorationIdentifier, restorationIndex } = turbo;
4609
+ this.restorationIdentifier = restorationIdentifier;
4610
+ const direction = restorationIndex > this.currentIndex ? "forward" : "back";
4611
+ this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(this.location, restorationIdentifier, direction);
4612
+ this.currentIndex = restorationIndex;
4613
+ } else {
4614
+ this.currentIndex++;
4615
+ this.delegate.historyPoppedWithEmptyState(this.location);
4661
4616
  }
4662
4617
  }
4663
-
4664
- onPageLoad = async (_event) => {
4665
- await nextMicrotask();
4666
- this.pageLoaded = true;
4667
- }
4668
-
4669
- // Private
4670
-
4671
- shouldHandlePopState() {
4672
- // Safari dispatches a popstate event after window's load event, ignore it
4673
- return this.pageIsLoaded()
4674
- }
4675
-
4676
- pageIsLoaded() {
4677
- return this.pageLoaded || document.readyState == "complete"
4678
- }
4679
4618
  }
4680
4619
 
4681
4620
  class LinkPrefetchObserver {
@@ -4750,7 +4689,7 @@ class LinkPrefetchObserver {
4750
4689
 
4751
4690
  fetchRequest.fetchOptions.priority = "low";
4752
4691
 
4753
- prefetchCache.setLater(location.toString(), fetchRequest, this.#cacheTtl);
4692
+ prefetchCache.putLater(location, fetchRequest, this.#cacheTtl);
4754
4693
  }
4755
4694
  }
4756
4695
  }
@@ -4766,7 +4705,7 @@ class LinkPrefetchObserver {
4766
4705
 
4767
4706
  #tryToUsePrefetchedRequest = (event) => {
4768
4707
  if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "GET") {
4769
- const cached = prefetchCache.get(event.detail.url.toString());
4708
+ const cached = prefetchCache.get(event.detail.url);
4770
4709
 
4771
4710
  if (cached) {
4772
4711
  // User clicked link, use cache response
@@ -4956,7 +4895,7 @@ class Navigator {
4956
4895
  } else {
4957
4896
  await this.view.renderPage(snapshot, false, true, this.currentVisit);
4958
4897
  }
4959
- if(!snapshot.shouldPreserveScrollPosition) {
4898
+ if (snapshot.refreshScroll !== "preserve") {
4960
4899
  this.view.scrollToTop();
4961
4900
  }
4962
4901
  this.view.clearSnapshotCache();
@@ -4996,20 +4935,10 @@ class Navigator {
4996
4935
  delete this.currentVisit;
4997
4936
  }
4998
4937
 
4938
+ // Same-page links are no longer handled with a Visit.
4939
+ // This method is still needed for Turbo Native adapters.
4999
4940
  locationWithActionIsSamePage(location, action) {
5000
- const anchor = getAnchor(location);
5001
- const currentAnchor = getAnchor(this.view.lastRenderedLocation);
5002
- const isRestorationToTop = action === "restore" && typeof anchor === "undefined";
5003
-
5004
- return (
5005
- action !== "replace" &&
5006
- getRequestURL(location) === getRequestURL(this.view.lastRenderedLocation) &&
5007
- (isRestorationToTop || (anchor != null && anchor !== currentAnchor))
5008
- )
5009
- }
5010
-
5011
- visitScrolledToSamePageLocation(oldURL, newURL) {
5012
- this.delegate.visitScrolledToSamePageLocation(oldURL, newURL);
4941
+ return false
5013
4942
  }
5014
4943
 
5015
4944
  // Visits
@@ -5402,13 +5331,18 @@ class PageRenderer extends Renderer {
5402
5331
 
5403
5332
  #setLanguage() {
5404
5333
  const { documentElement } = this.currentSnapshot;
5405
- const { lang } = this.newSnapshot;
5334
+ const { dir, lang } = this.newSnapshot;
5406
5335
 
5407
5336
  if (lang) {
5408
5337
  documentElement.setAttribute("lang", lang);
5409
5338
  } else {
5410
5339
  documentElement.removeAttribute("lang");
5411
5340
  }
5341
+ if (dir) {
5342
+ documentElement.setAttribute("dir", dir);
5343
+ } else {
5344
+ documentElement.removeAttribute("dir");
5345
+ }
5412
5346
  }
5413
5347
 
5414
5348
  async mergeHead() {
@@ -5510,9 +5444,16 @@ class PageRenderer extends Renderer {
5510
5444
 
5511
5445
  activateNewBody() {
5512
5446
  document.adoptNode(this.newElement);
5447
+ this.removeNoscriptElements();
5513
5448
  this.activateNewBodyScriptElements();
5514
5449
  }
5515
5450
 
5451
+ removeNoscriptElements() {
5452
+ for (const noscriptElement of this.newElement.querySelectorAll("noscript")) {
5453
+ noscriptElement.remove();
5454
+ }
5455
+ }
5456
+
5516
5457
  activateNewBodyScriptElements() {
5517
5458
  for (const inertScriptElement of this.newBodyScriptElements) {
5518
5459
  const activatedScriptElement = activateScriptElement(inertScriptElement);
@@ -5588,58 +5529,13 @@ class MorphingPageRenderer extends PageRenderer {
5588
5529
  }
5589
5530
  }
5590
5531
 
5591
- class SnapshotCache {
5592
- keys = []
5593
- snapshots = {}
5594
-
5532
+ class SnapshotCache extends LRUCache {
5595
5533
  constructor(size) {
5596
- this.size = size;
5597
- }
5598
-
5599
- has(location) {
5600
- return toCacheKey(location) in this.snapshots
5601
- }
5602
-
5603
- get(location) {
5604
- if (this.has(location)) {
5605
- const snapshot = this.read(location);
5606
- this.touch(location);
5607
- return snapshot
5608
- }
5609
- }
5610
-
5611
- put(location, snapshot) {
5612
- this.write(location, snapshot);
5613
- this.touch(location);
5614
- return snapshot
5534
+ super(size, toCacheKey);
5615
5535
  }
5616
5536
 
5617
- clear() {
5618
- this.snapshots = {};
5619
- }
5620
-
5621
- // Private
5622
-
5623
- read(location) {
5624
- return this.snapshots[toCacheKey(location)]
5625
- }
5626
-
5627
- write(location, snapshot) {
5628
- this.snapshots[toCacheKey(location)] = snapshot;
5629
- }
5630
-
5631
- touch(location) {
5632
- const key = toCacheKey(location);
5633
- const index = this.keys.indexOf(key);
5634
- if (index > -1) this.keys.splice(index, 1);
5635
- this.keys.unshift(key);
5636
- this.trim();
5637
- }
5638
-
5639
- trim() {
5640
- for (const key of this.keys.splice(this.size)) {
5641
- delete this.snapshots[key];
5642
- }
5537
+ get snapshots() {
5538
+ return this.entries
5643
5539
  }
5644
5540
  }
5645
5541
 
@@ -5653,7 +5549,7 @@ class PageView extends View {
5653
5549
  }
5654
5550
 
5655
5551
  renderPage(snapshot, isPreview = false, willRender = true, visit) {
5656
- const shouldMorphPage = this.isPageRefresh(visit) && this.snapshot.shouldMorphPage;
5552
+ const shouldMorphPage = this.isPageRefresh(visit) && (visit?.refresh?.method || this.snapshot.refreshMethod) === "morph";
5657
5553
  const rendererClass = shouldMorphPage ? MorphingPageRenderer : PageRenderer;
5658
5554
 
5659
5555
  const renderer = new rendererClass(this.snapshot, snapshot, isPreview, willRender);
@@ -5697,7 +5593,7 @@ class PageView extends View {
5697
5593
  }
5698
5594
 
5699
5595
  shouldPreserveScrollPosition(visit) {
5700
- return this.isPageRefresh(visit) && this.snapshot.shouldPreserveScrollPosition
5596
+ return this.isPageRefresh(visit) && (visit?.refresh?.scroll || this.snapshot.refreshScroll) === "preserve"
5701
5597
  }
5702
5598
 
5703
5599
  get snapshot() {
@@ -5887,11 +5783,14 @@ class Session {
5887
5783
  }
5888
5784
  }
5889
5785
 
5890
- refresh(url, requestId) {
5786
+ refresh(url, options = {}) {
5787
+ options = typeof options === "string" ? { requestId: options } : options;
5788
+
5789
+ const { method, requestId, scroll } = options;
5891
5790
  const isRecentRequest = requestId && this.recentRequests.has(requestId);
5892
5791
  const isCurrentUrl = url === document.baseURI;
5893
5792
  if (!isRecentRequest && !this.navigator.currentVisit && isCurrentUrl) {
5894
- this.visit(url, { action: "replace", shouldCacheSnapshot: false });
5793
+ this.visit(url, { action: "replace", shouldCacheSnapshot: false, refresh: { method, scroll } });
5895
5794
  }
5896
5795
  }
5897
5796
 
@@ -5995,6 +5894,12 @@ class Session {
5995
5894
  }
5996
5895
  }
5997
5896
 
5897
+ historyPoppedWithEmptyState(location) {
5898
+ this.history.replace(location);
5899
+ this.view.lastRenderedLocation = location;
5900
+ this.view.cacheSnapshot();
5901
+ }
5902
+
5998
5903
  // Scroll observer delegate
5999
5904
 
6000
5905
  scrollPositionChanged(position) {
@@ -6039,7 +5944,7 @@ class Session {
6039
5944
  // Navigator delegate
6040
5945
 
6041
5946
  allowsVisitingLocationWithAction(location, action) {
6042
- return this.locationWithActionIsSamePage(location, action) || this.applicationAllowsVisitingLocation(location)
5947
+ return this.applicationAllowsVisitingLocation(location)
6043
5948
  }
6044
5949
 
6045
5950
  visitProposedToLocation(location, options) {
@@ -6055,9 +5960,7 @@ class Session {
6055
5960
  this.view.markVisitDirection(visit.direction);
6056
5961
  }
6057
5962
  extendURLWithDeprecatedProperties(visit.location);
6058
- if (!visit.silent) {
6059
- this.notifyApplicationAfterVisitingLocation(visit.location, visit.action);
6060
- }
5963
+ this.notifyApplicationAfterVisitingLocation(visit.location, visit.action);
6061
5964
  }
6062
5965
 
6063
5966
  visitCompleted(visit) {
@@ -6066,14 +5969,6 @@ class Session {
6066
5969
  this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
6067
5970
  }
6068
5971
 
6069
- locationWithActionIsSamePage(location, action) {
6070
- return this.navigator.locationWithActionIsSamePage(location, action)
6071
- }
6072
-
6073
- visitScrolledToSamePageLocation(oldURL, newURL) {
6074
- this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
6075
- }
6076
-
6077
5972
  // Form submit observer delegate
6078
5973
 
6079
5974
  willSubmitForm(form, submitter) {
@@ -6113,9 +6008,7 @@ class Session {
6113
6008
  // Page view delegate
6114
6009
 
6115
6010
  viewWillCacheSnapshot() {
6116
- if (!this.navigator.currentVisit?.silent) {
6117
- this.notifyApplicationBeforeCachingSnapshot();
6118
- }
6011
+ this.notifyApplicationBeforeCachingSnapshot();
6119
6012
  }
6120
6013
 
6121
6014
  allowsImmediateRender({ element }, options) {
@@ -6207,15 +6100,6 @@ class Session {
6207
6100
  })
6208
6101
  }
6209
6102
 
6210
- notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL) {
6211
- dispatchEvent(
6212
- new HashChangeEvent("hashchange", {
6213
- oldURL: oldURL.toString(),
6214
- newURL: newURL.toString()
6215
- })
6216
- );
6217
- }
6218
-
6219
6103
  notifyApplicationAfterFrameLoad(frame) {
6220
6104
  return dispatch("turbo:frame-load", { target: frame })
6221
6105
  }
@@ -6301,7 +6185,7 @@ const deprecatedLocationPropertyDescriptors = {
6301
6185
  };
6302
6186
 
6303
6187
  const session = new Session(recentRequests);
6304
- const { cache, navigator: navigator$1 } = session;
6188
+ const { cache, navigator } = session;
6305
6189
 
6306
6190
  /**
6307
6191
  * Starts the main session.
@@ -6367,19 +6251,6 @@ function renderStreamMessage(message) {
6367
6251
  session.renderStreamMessage(message);
6368
6252
  }
6369
6253
 
6370
- /**
6371
- * Removes all entries from the Turbo Drive page cache.
6372
- * Call this when state has changed on the server that may affect cached pages.
6373
- *
6374
- * @deprecated since version 7.2.0 in favor of `Turbo.cache.clear()`
6375
- */
6376
- function clearCache() {
6377
- console.warn(
6378
- "Please replace `Turbo.clearCache()` with `Turbo.cache.clear()`. The top-level function is deprecated and will be removed in a future version of Turbo.`"
6379
- );
6380
- session.clearCache();
6381
- }
6382
-
6383
6254
  /**
6384
6255
  * Sets the delay after which the progress bar will appear during navigation.
6385
6256
  *
@@ -6439,7 +6310,7 @@ function morphTurboFrameElements(currentFrame, newFrame) {
6439
6310
 
6440
6311
  var Turbo = /*#__PURE__*/Object.freeze({
6441
6312
  __proto__: null,
6442
- navigator: navigator$1,
6313
+ navigator: navigator,
6443
6314
  session: session,
6444
6315
  cache: cache,
6445
6316
  PageRenderer: PageRenderer,
@@ -6453,7 +6324,6 @@ var Turbo = /*#__PURE__*/Object.freeze({
6453
6324
  connectStreamSource: connectStreamSource,
6454
6325
  disconnectStreamSource: disconnectStreamSource,
6455
6326
  renderStreamMessage: renderStreamMessage,
6456
- clearCache: clearCache,
6457
6327
  setProgressBarDelay: setProgressBarDelay,
6458
6328
  setConfirmMethod: setConfirmMethod,
6459
6329
  setFormMode: setFormMode,
@@ -6508,11 +6378,17 @@ class FrameController {
6508
6378
  this.formLinkClickObserver.stop();
6509
6379
  this.linkInterceptor.stop();
6510
6380
  this.formSubmitObserver.stop();
6381
+
6382
+ if (!this.element.hasAttribute("recurse")) {
6383
+ this.#currentFetchRequest?.cancel();
6384
+ }
6511
6385
  }
6512
6386
  }
6513
6387
 
6514
6388
  disabledChanged() {
6515
- if (this.loadingStyle == FrameLoadingStyle.eager) {
6389
+ if (this.disabled) {
6390
+ this.#currentFetchRequest?.cancel();
6391
+ } else if (this.loadingStyle == FrameLoadingStyle.eager) {
6516
6392
  this.#loadSourceURL();
6517
6393
  }
6518
6394
  }
@@ -6520,6 +6396,10 @@ class FrameController {
6520
6396
  sourceURLChanged() {
6521
6397
  if (this.#isIgnoringChangesTo("src")) return
6522
6398
 
6399
+ if (!this.sourceURL) {
6400
+ this.#currentFetchRequest?.cancel();
6401
+ }
6402
+
6523
6403
  if (this.element.isConnected) {
6524
6404
  this.complete = false;
6525
6405
  }
@@ -6621,15 +6501,18 @@ class FrameController {
6621
6501
  }
6622
6502
 
6623
6503
  this.formSubmission = new FormSubmission(this, element, submitter);
6504
+
6624
6505
  const { fetchRequest } = this.formSubmission;
6625
- this.prepareRequest(fetchRequest);
6506
+ const frame = this.#findFrameElement(element, submitter);
6507
+
6508
+ this.prepareRequest(fetchRequest, frame);
6626
6509
  this.formSubmission.start();
6627
6510
  }
6628
6511
 
6629
6512
  // Fetch request delegate
6630
6513
 
6631
- prepareRequest(request) {
6632
- request.headers["Turbo-Frame"] = this.id;
6514
+ prepareRequest(request, frame = this) {
6515
+ request.headers["Turbo-Frame"] = frame.id;
6633
6516
 
6634
6517
  if (this.currentNavigationElement?.hasAttribute("data-turbo-stream")) {
6635
6518
  request.acceptResponseType(StreamMessage.contentType);
@@ -6871,7 +6754,9 @@ class FrameController {
6871
6754
 
6872
6755
  #findFrameElement(element, submitter) {
6873
6756
  const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
6874
- return getFrameElementById(id) ?? this.element
6757
+ const target = this.#getFrameElementById(id);
6758
+
6759
+ return target instanceof FrameElement ? target : this.element
6875
6760
  }
6876
6761
 
6877
6762
  async extractForeignFrameElement(container) {
@@ -6915,9 +6800,11 @@ class FrameController {
6915
6800
  }
6916
6801
 
6917
6802
  if (id) {
6918
- const frameElement = getFrameElementById(id);
6803
+ const frameElement = this.#getFrameElementById(id);
6919
6804
  if (frameElement) {
6920
6805
  return !frameElement.disabled
6806
+ } else if (id == "_parent") {
6807
+ return false
6921
6808
  }
6922
6809
  }
6923
6810
 
@@ -6938,8 +6825,12 @@ class FrameController {
6938
6825
  return this.element.id
6939
6826
  }
6940
6827
 
6828
+ get disabled() {
6829
+ return this.element.disabled
6830
+ }
6831
+
6941
6832
  get enabled() {
6942
- return !this.element.disabled
6833
+ return !this.disabled
6943
6834
  }
6944
6835
 
6945
6836
  get sourceURL() {
@@ -6999,13 +6890,15 @@ class FrameController {
6999
6890
  callback();
7000
6891
  delete this.currentNavigationElement;
7001
6892
  }
7002
- }
7003
6893
 
7004
- function getFrameElementById(id) {
7005
- if (id != null) {
7006
- const element = document.getElementById(id);
7007
- if (element instanceof FrameElement) {
7008
- return element
6894
+ #getFrameElementById(id) {
6895
+ if (id != null) {
6896
+ const element = id === "_parent" ?
6897
+ this.element.parentElement.closest("turbo-frame") :
6898
+ document.getElementById(id);
6899
+ if (element instanceof FrameElement) {
6900
+ return element
6901
+ }
7009
6902
  }
7010
6903
  }
7011
6904
  }
@@ -7030,6 +6923,7 @@ function activateElement(element, currentURL) {
7030
6923
 
7031
6924
  const StreamActions = {
7032
6925
  after() {
6926
+ this.removeDuplicateTargetSiblings();
7033
6927
  this.targetElements.forEach((e) => e.parentElement?.insertBefore(this.templateContent, e.nextSibling));
7034
6928
  },
7035
6929
 
@@ -7039,6 +6933,7 @@ const StreamActions = {
7039
6933
  },
7040
6934
 
7041
6935
  before() {
6936
+ this.removeDuplicateTargetSiblings();
7042
6937
  this.targetElements.forEach((e) => e.parentElement?.insertBefore(this.templateContent, e));
7043
6938
  },
7044
6939
 
@@ -7077,7 +6972,11 @@ const StreamActions = {
7077
6972
  },
7078
6973
 
7079
6974
  refresh() {
7080
- session.refresh(this.baseURI, this.requestId);
6975
+ const method = this.getAttribute("method");
6976
+ const requestId = this.requestId;
6977
+ const scroll = this.getAttribute("scroll");
6978
+
6979
+ session.refresh(this.baseURI, { method, requestId, scroll });
7081
6980
  }
7082
6981
  };
7083
6982
 
@@ -7155,6 +7054,23 @@ class StreamElement extends HTMLElement {
7155
7054
  return existingChildren.filter((c) => newChildrenIds.includes(c.getAttribute("id")))
7156
7055
  }
7157
7056
 
7057
+ /**
7058
+ * Removes duplicate siblings (by ID)
7059
+ */
7060
+ removeDuplicateTargetSiblings() {
7061
+ this.duplicateSiblings.forEach((c) => c.remove());
7062
+ }
7063
+
7064
+ /**
7065
+ * Gets the list of duplicate siblings (i.e. those with the same ID)
7066
+ */
7067
+ get duplicateSiblings() {
7068
+ const existingChildren = this.targetElements.flatMap((e) => [...e.parentElement.children]).filter((c) => !!c.id);
7069
+ const newChildrenIds = [...(this.templateContent?.children || [])].filter((c) => !!c.id).map((c) => c.id);
7070
+
7071
+ return existingChildren.filter((c) => newChildrenIds.includes(c.id))
7072
+ }
7073
+
7158
7074
  /**
7159
7075
  * Gets the action function to be performed.
7160
7076
  */
@@ -7306,11 +7222,11 @@ if (customElements.get("turbo-stream-source") === undefined) {
7306
7222
  }
7307
7223
 
7308
7224
  (() => {
7309
- let element = document.currentScript;
7310
- if (!element) return
7311
- if (element.hasAttribute("data-turbo-suppress-warning")) return
7225
+ const scriptElement = document.currentScript;
7226
+ if (!scriptElement) return
7227
+ if (scriptElement.hasAttribute("data-turbo-suppress-warning")) return
7312
7228
 
7313
- element = element.parentElement;
7229
+ let element = scriptElement.parentElement;
7314
7230
  while (element) {
7315
7231
  if (element == document.body) {
7316
7232
  return console.warn(
@@ -7324,7 +7240,7 @@ if (customElements.get("turbo-stream-source") === undefined) {
7324
7240
  ——
7325
7241
  Suppress this warning by adding a "data-turbo-suppress-warning" attribute to: %s
7326
7242
  `,
7327
- element.outerHTML
7243
+ scriptElement.outerHTML
7328
7244
  )
7329
7245
  }
7330
7246
 
@@ -7335,4 +7251,4 @@ if (customElements.get("turbo-stream-source") === undefined) {
7335
7251
  window.Turbo = { ...Turbo, StreamActions };
7336
7252
  start();
7337
7253
 
7338
- export { FetchEnctype, FetchMethod, FetchRequest, FetchResponse, FrameElement, FrameLoadingStyle, FrameRenderer, PageRenderer, PageSnapshot, StreamActions, StreamElement, StreamSourceElement, cache, clearCache, config, connectStreamSource, disconnectStreamSource, fetchWithTurboHeaders as fetch, fetchEnctypeFromString, fetchMethodFromString, isSafe, morphBodyElements, morphChildren, morphElements, morphTurboFrameElements, navigator$1 as navigator, registerAdapter, renderStreamMessage, session, setConfirmMethod, setFormMode, setProgressBarDelay, start, visit };
7254
+ export { FetchEnctype, FetchMethod, FetchRequest, FetchResponse, FrameElement, FrameLoadingStyle, FrameRenderer, PageRenderer, PageSnapshot, StreamActions, StreamElement, StreamSourceElement, cache, config, connectStreamSource, disconnectStreamSource, fetchWithTurboHeaders as fetch, fetchEnctypeFromString, fetchMethodFromString, isSafe, morphBodyElements, morphChildren, morphElements, morphTurboFrameElements, navigator, registerAdapter, renderStreamMessage, session, setConfirmMethod, setFormMode, setProgressBarDelay, start, visit };