@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,6 +1,6 @@
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
5
  (function (global, factory) {
6
6
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
@@ -8,103 +8,6 @@ Copyright © 2025 37signals LLC
8
8
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Turbo = {}));
9
9
  })(this, (function (exports) { 'use strict';
10
10
 
11
- /**
12
- * The MIT License (MIT)
13
- *
14
- * Copyright (c) 2019 Javan Makhmali
15
- *
16
- * Permission is hereby granted, free of charge, to any person obtaining a copy
17
- * of this software and associated documentation files (the "Software"), to deal
18
- * in the Software without restriction, including without limitation the rights
19
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
20
- * copies of the Software, and to permit persons to whom the Software is
21
- * furnished to do so, subject to the following conditions:
22
- *
23
- * The above copyright notice and this permission notice shall be included in
24
- * all copies or substantial portions of the Software.
25
- *
26
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
27
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
28
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
29
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
30
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
31
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
32
- * THE SOFTWARE.
33
- */
34
-
35
- (function (prototype) {
36
- if (typeof prototype.requestSubmit == "function") return
37
-
38
- prototype.requestSubmit = function (submitter) {
39
- if (submitter) {
40
- validateSubmitter(submitter, this);
41
- submitter.click();
42
- } else {
43
- submitter = document.createElement("input");
44
- submitter.type = "submit";
45
- submitter.hidden = true;
46
- this.appendChild(submitter);
47
- submitter.click();
48
- this.removeChild(submitter);
49
- }
50
- };
51
-
52
- function validateSubmitter(submitter, form) {
53
- submitter instanceof HTMLElement || raise(TypeError, "parameter 1 is not of type 'HTMLElement'");
54
- submitter.type == "submit" || raise(TypeError, "The specified element is not a submit button");
55
- submitter.form == form ||
56
- raise(DOMException, "The specified element is not owned by this form element", "NotFoundError");
57
- }
58
-
59
- function raise(errorConstructor, message, name) {
60
- throw new errorConstructor("Failed to execute 'requestSubmit' on 'HTMLFormElement': " + message + ".", name)
61
- }
62
- })(HTMLFormElement.prototype);
63
-
64
- const submittersByForm = new WeakMap();
65
-
66
- function findSubmitterFromClickTarget(target) {
67
- const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
68
- const candidate = element ? element.closest("input, button") : null;
69
- return candidate?.type == "submit" ? candidate : null
70
- }
71
-
72
- function clickCaptured(event) {
73
- const submitter = findSubmitterFromClickTarget(event.target);
74
-
75
- if (submitter && submitter.form) {
76
- submittersByForm.set(submitter.form, submitter);
77
- }
78
- }
79
-
80
- (function () {
81
- if ("submitter" in Event.prototype) return
82
-
83
- let prototype = window.Event.prototype;
84
- // Certain versions of Safari 15 have a bug where they won't
85
- // populate the submitter. This hurts TurboDrive's enable/disable detection.
86
- // See https://bugs.webkit.org/show_bug.cgi?id=229660
87
- if ("SubmitEvent" in window) {
88
- const prototypeOfSubmitEvent = window.SubmitEvent.prototype;
89
-
90
- if (/Apple Computer/.test(navigator.vendor) && !("submitter" in prototypeOfSubmitEvent)) {
91
- prototype = prototypeOfSubmitEvent;
92
- } else {
93
- return // polyfill not needed
94
- }
95
- }
96
-
97
- addEventListener("click", clickCaptured, true);
98
-
99
- Object.defineProperty(prototype, "submitter", {
100
- get() {
101
- if (this.type == "submit" && this.target instanceof HTMLFormElement) {
102
- return submittersByForm.get(this.target)
103
- }
104
- }
105
- });
106
- })();
107
-
108
11
  const FrameLoadingStyle = {
109
12
  eager: "eager",
110
13
  lazy: "lazy"
@@ -380,10 +283,6 @@ Copyright © 2025 37signals LLC
380
283
  return new Promise((resolve) => setTimeout(() => resolve(), 0))
381
284
  }
382
285
 
383
- function nextMicrotask() {
384
- return Promise.resolve()
385
- }
386
-
387
286
  function parseHTMLDocument(html = "") {
388
287
  return new DOMParser().parseFromString(html, "text/html")
389
288
  }
@@ -412,7 +311,7 @@ Copyright © 2025 37signals LLC
412
311
  } else if (i == 19) {
413
312
  return (Math.floor(Math.random() * 4) + 8).toString(16)
414
313
  } else {
415
- return Math.floor(Math.random() * 15).toString(16)
314
+ return Math.floor(Math.random() * 16).toString(16)
416
315
  }
417
316
  })
418
317
  .join("")
@@ -564,14 +463,13 @@ Copyright © 2025 37signals LLC
564
463
  const link = findClosestRecursively(target, "a[href], a[xlink\\:href]");
565
464
 
566
465
  if (!link) return null
466
+ if (link.href.startsWith("#")) return null
567
467
  if (link.hasAttribute("download")) return null
568
- if (link.hasAttribute("target") && link.target !== "_self") return null
569
468
 
570
- return link
571
- }
469
+ const linkTarget = link.getAttribute("target");
470
+ if (linkTarget && linkTarget !== "_self") return null
572
471
 
573
- function getLocationForLink(link) {
574
- return expandURL(link.getAttribute("href") || "")
472
+ return link
575
473
  }
576
474
 
577
475
  function debounce(fn, delay) {
@@ -662,6 +560,10 @@ Copyright © 2025 37signals LLC
662
560
  return isPrefixedBy(location, rootLocation) && !config.drive.unvisitableExtensions.has(getExtension(location))
663
561
  }
664
562
 
563
+ function getLocationForLink(link) {
564
+ return expandURL(link.getAttribute("href") || "")
565
+ }
566
+
665
567
  function getRequestURL(url) {
666
568
  const anchor = getAnchor(url);
667
569
  return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href
@@ -1077,35 +979,113 @@ Copyright © 2025 37signals LLC
1077
979
  return fragment
1078
980
  }
1079
981
 
1080
- const PREFETCH_DELAY = 100;
982
+ const identity = key => key;
1081
983
 
1082
- class PrefetchCache {
1083
- #prefetchTimeout = null
1084
- #prefetched = null
984
+ class LRUCache {
985
+ keys = []
986
+ entries = {}
987
+ #toCacheKey
988
+
989
+ constructor(size, toCacheKey = identity) {
990
+ this.size = size;
991
+ this.#toCacheKey = toCacheKey;
992
+ }
1085
993
 
1086
- get(url) {
1087
- if (this.#prefetched && this.#prefetched.url === url && this.#prefetched.expire > Date.now()) {
1088
- return this.#prefetched.request
994
+ has(key) {
995
+ return this.#toCacheKey(key) in this.entries
996
+ }
997
+
998
+ get(key) {
999
+ if (this.has(key)) {
1000
+ const entry = this.read(key);
1001
+ this.touch(key);
1002
+ return entry
1003
+ }
1004
+ }
1005
+
1006
+ put(key, entry) {
1007
+ this.write(key, entry);
1008
+ this.touch(key);
1009
+ return entry
1010
+ }
1011
+
1012
+ clear() {
1013
+ for (const key of Object.keys(this.entries)) {
1014
+ this.evict(key);
1015
+ }
1016
+ }
1017
+
1018
+ // Private
1019
+
1020
+ read(key) {
1021
+ return this.entries[this.#toCacheKey(key)]
1022
+ }
1023
+
1024
+ write(key, entry) {
1025
+ this.entries[this.#toCacheKey(key)] = entry;
1026
+ }
1027
+
1028
+ touch(key) {
1029
+ key = this.#toCacheKey(key);
1030
+ const index = this.keys.indexOf(key);
1031
+ if (index > -1) this.keys.splice(index, 1);
1032
+ this.keys.unshift(key);
1033
+ this.trim();
1034
+ }
1035
+
1036
+ trim() {
1037
+ for (const key of this.keys.splice(this.size)) {
1038
+ this.evict(key);
1089
1039
  }
1090
1040
  }
1091
1041
 
1092
- setLater(url, request, ttl) {
1093
- this.clear();
1042
+ evict(key) {
1043
+ delete this.entries[key];
1044
+ }
1045
+ }
1046
+
1047
+ const PREFETCH_DELAY = 100;
1048
+
1049
+ class PrefetchCache extends LRUCache {
1050
+ #prefetchTimeout = null
1051
+ #maxAges = {}
1052
+
1053
+ constructor(size = 1, prefetchDelay = PREFETCH_DELAY) {
1054
+ super(size, toCacheKey);
1055
+ this.prefetchDelay = prefetchDelay;
1056
+ }
1094
1057
 
1058
+ putLater(url, request, ttl) {
1095
1059
  this.#prefetchTimeout = setTimeout(() => {
1096
1060
  request.perform();
1097
- this.set(url, request, ttl);
1061
+ this.put(url, request, ttl);
1098
1062
  this.#prefetchTimeout = null;
1099
- }, PREFETCH_DELAY);
1063
+ }, this.prefetchDelay);
1100
1064
  }
1101
1065
 
1102
- set(url, request, ttl) {
1103
- this.#prefetched = { url, request, expire: new Date(new Date().getTime() + ttl) };
1066
+ put(url, request, ttl = cacheTtl) {
1067
+ super.put(url, request);
1068
+ this.#maxAges[toCacheKey(url)] = new Date(new Date().getTime() + ttl);
1104
1069
  }
1105
1070
 
1106
1071
  clear() {
1072
+ super.clear();
1107
1073
  if (this.#prefetchTimeout) clearTimeout(this.#prefetchTimeout);
1108
- this.#prefetched = null;
1074
+ }
1075
+
1076
+ evict(key) {
1077
+ super.evict(key);
1078
+ delete this.#maxAges[key];
1079
+ }
1080
+
1081
+ has(key) {
1082
+ if (super.has(key)) {
1083
+ const maxAge = this.#maxAges[toCacheKey(key)];
1084
+
1085
+ return maxAge && maxAge > Date.now()
1086
+ } else {
1087
+ return false
1088
+ }
1109
1089
  }
1110
1090
  }
1111
1091
 
@@ -3808,6 +3788,10 @@ Copyright © 2025 37signals LLC
3808
3788
  clonedPasswordInput.value = "";
3809
3789
  }
3810
3790
 
3791
+ for (const clonedNoscriptElement of clonedElement.querySelectorAll("noscript")) {
3792
+ clonedNoscriptElement.remove();
3793
+ }
3794
+
3811
3795
  return new PageSnapshot(this.documentElement, clonedElement, this.headSnapshot)
3812
3796
  }
3813
3797
 
@@ -3815,6 +3799,10 @@ Copyright © 2025 37signals LLC
3815
3799
  return this.documentElement.getAttribute("lang")
3816
3800
  }
3817
3801
 
3802
+ get dir() {
3803
+ return this.documentElement.getAttribute("dir")
3804
+ }
3805
+
3818
3806
  get headElement() {
3819
3807
  return this.headSnapshot.element
3820
3808
  }
@@ -3845,12 +3833,12 @@ Copyright © 2025 37signals LLC
3845
3833
  return viewTransitionEnabled && !window.matchMedia("(prefers-reduced-motion: reduce)").matches
3846
3834
  }
3847
3835
 
3848
- get shouldMorphPage() {
3849
- return this.getSetting("refresh-method") === "morph"
3836
+ get refreshMethod() {
3837
+ return this.getSetting("refresh-method")
3850
3838
  }
3851
3839
 
3852
- get shouldPreserveScrollPosition() {
3853
- return this.getSetting("refresh-scroll") === "preserve"
3840
+ get refreshScroll() {
3841
+ return this.getSetting("refresh-scroll")
3854
3842
  }
3855
3843
 
3856
3844
  // Private
@@ -3889,7 +3877,8 @@ Copyright © 2025 37signals LLC
3889
3877
  willRender: true,
3890
3878
  updateHistory: true,
3891
3879
  shouldCacheSnapshot: true,
3892
- acceptsStreamResponse: false
3880
+ acceptsStreamResponse: false,
3881
+ refresh: {}
3893
3882
  };
3894
3883
 
3895
3884
  const TimingMetric = {
@@ -3949,7 +3938,8 @@ Copyright © 2025 37signals LLC
3949
3938
  updateHistory,
3950
3939
  shouldCacheSnapshot,
3951
3940
  acceptsStreamResponse,
3952
- direction
3941
+ direction,
3942
+ refresh
3953
3943
  } = {
3954
3944
  ...defaultOptions,
3955
3945
  ...options
@@ -3960,7 +3950,6 @@ Copyright © 2025 37signals LLC
3960
3950
  this.snapshot = snapshot;
3961
3951
  this.snapshotHTML = snapshotHTML;
3962
3952
  this.response = response;
3963
- this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
3964
3953
  this.isPageRefresh = this.view.isPageRefresh(this);
3965
3954
  this.visitCachedSnapshot = visitCachedSnapshot;
3966
3955
  this.willRender = willRender;
@@ -3969,6 +3958,7 @@ Copyright © 2025 37signals LLC
3969
3958
  this.shouldCacheSnapshot = shouldCacheSnapshot;
3970
3959
  this.acceptsStreamResponse = acceptsStreamResponse;
3971
3960
  this.direction = direction || Direction[action];
3961
+ this.refresh = refresh;
3972
3962
  }
3973
3963
 
3974
3964
  get adapter() {
@@ -3987,10 +3977,6 @@ Copyright © 2025 37signals LLC
3987
3977
  return this.history.getRestorationDataForIdentifier(this.restorationIdentifier)
3988
3978
  }
3989
3979
 
3990
- get silent() {
3991
- return this.isSamePage
3992
- }
3993
-
3994
3980
  start() {
3995
3981
  if (this.state == VisitState.initialized) {
3996
3982
  this.recordTimingMetric(TimingMetric.visitStart);
@@ -4127,7 +4113,7 @@ Copyright © 2025 37signals LLC
4127
4113
  const isPreview = this.shouldIssueRequest();
4128
4114
  this.render(async () => {
4129
4115
  this.cacheSnapshot();
4130
- if (this.isSamePage || this.isPageRefresh) {
4116
+ if (this.isPageRefresh) {
4131
4117
  this.adapter.visitRendered(this);
4132
4118
  } else {
4133
4119
  if (this.view.renderPromise) await this.view.renderPromise;
@@ -4155,17 +4141,6 @@ Copyright © 2025 37signals LLC
4155
4141
  }
4156
4142
  }
4157
4143
 
4158
- goToSamePageAnchor() {
4159
- if (this.isSamePage) {
4160
- this.render(async () => {
4161
- this.cacheSnapshot();
4162
- this.performScroll();
4163
- this.changeHistory();
4164
- this.adapter.visitRendered(this);
4165
- });
4166
- }
4167
- }
4168
-
4169
4144
  // Fetch request delegate
4170
4145
 
4171
4146
  prepareRequest(request) {
@@ -4227,9 +4202,6 @@ Copyright © 2025 37signals LLC
4227
4202
  } else {
4228
4203
  this.scrollToAnchor() || this.view.scrollToTop();
4229
4204
  }
4230
- if (this.isSamePage) {
4231
- this.delegate.visitScrolledToSamePageLocation(this.view.lastRenderedLocation, this.location);
4232
- }
4233
4205
 
4234
4206
  this.scrolled = true;
4235
4207
  }
@@ -4268,9 +4240,7 @@ Copyright © 2025 37signals LLC
4268
4240
  }
4269
4241
 
4270
4242
  shouldIssueRequest() {
4271
- if (this.isSamePage) {
4272
- return false
4273
- } else if (this.action == "restore") {
4243
+ if (this.action == "restore") {
4274
4244
  return !this.hasCachedSnapshot()
4275
4245
  } else {
4276
4246
  return this.willRender
@@ -4334,7 +4304,6 @@ Copyright © 2025 37signals LLC
4334
4304
 
4335
4305
  visit.loadCachedSnapshot();
4336
4306
  visit.issueRequest();
4337
- visit.goToSamePageAnchor();
4338
4307
  }
4339
4308
 
4340
4309
  visitRequestStarted(visit) {
@@ -4451,7 +4420,6 @@ Copyright © 2025 37signals LLC
4451
4420
 
4452
4421
  class CacheObserver {
4453
4422
  selector = "[data-turbo-temporary]"
4454
- deprecatedSelector = "[data-turbo-cache=false]"
4455
4423
 
4456
4424
  started = false
4457
4425
 
@@ -4476,19 +4444,7 @@ Copyright © 2025 37signals LLC
4476
4444
  }
4477
4445
 
4478
4446
  get temporaryElements() {
4479
- return [...document.querySelectorAll(this.selector), ...this.temporaryElementsWithDeprecation]
4480
- }
4481
-
4482
- get temporaryElementsWithDeprecation() {
4483
- const elements = document.querySelectorAll(this.deprecatedSelector);
4484
-
4485
- if (elements.length) {
4486
- console.warn(
4487
- `The ${this.deprecatedSelector} selector is deprecated and will be removed in a future version. Use ${this.selector} instead.`
4488
- );
4489
- }
4490
-
4491
- return [...elements]
4447
+ return [...document.querySelectorAll(this.selector)]
4492
4448
  }
4493
4449
  }
4494
4450
 
@@ -4578,7 +4534,6 @@ Copyright © 2025 37signals LLC
4578
4534
  restorationIdentifier = uuid()
4579
4535
  restorationData = {}
4580
4536
  started = false
4581
- pageLoaded = false
4582
4537
  currentIndex = 0
4583
4538
 
4584
4539
  constructor(delegate) {
@@ -4588,7 +4543,6 @@ Copyright © 2025 37signals LLC
4588
4543
  start() {
4589
4544
  if (!this.started) {
4590
4545
  addEventListener("popstate", this.onPopState, false);
4591
- addEventListener("load", this.onPageLoad, false);
4592
4546
  this.currentIndex = history.state?.turbo?.restorationIndex || 0;
4593
4547
  this.started = true;
4594
4548
  this.replace(new URL(window.location.href));
@@ -4598,7 +4552,6 @@ Copyright © 2025 37signals LLC
4598
4552
  stop() {
4599
4553
  if (this.started) {
4600
4554
  removeEventListener("popstate", this.onPopState, false);
4601
- removeEventListener("load", this.onPageLoad, false);
4602
4555
  this.started = false;
4603
4556
  }
4604
4557
  }
@@ -4654,34 +4607,20 @@ Copyright © 2025 37signals LLC
4654
4607
  // Event handlers
4655
4608
 
4656
4609
  onPopState = (event) => {
4657
- if (this.shouldHandlePopState()) {
4658
- const { turbo } = event.state || {};
4659
- if (turbo) {
4660
- this.location = new URL(window.location.href);
4661
- const { restorationIdentifier, restorationIndex } = turbo;
4662
- this.restorationIdentifier = restorationIdentifier;
4663
- const direction = restorationIndex > this.currentIndex ? "forward" : "back";
4664
- this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(this.location, restorationIdentifier, direction);
4665
- this.currentIndex = restorationIndex;
4666
- }
4610
+ const { turbo } = event.state || {};
4611
+ this.location = new URL(window.location.href);
4612
+
4613
+ if (turbo) {
4614
+ const { restorationIdentifier, restorationIndex } = turbo;
4615
+ this.restorationIdentifier = restorationIdentifier;
4616
+ const direction = restorationIndex > this.currentIndex ? "forward" : "back";
4617
+ this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(this.location, restorationIdentifier, direction);
4618
+ this.currentIndex = restorationIndex;
4619
+ } else {
4620
+ this.currentIndex++;
4621
+ this.delegate.historyPoppedWithEmptyState(this.location);
4667
4622
  }
4668
4623
  }
4669
-
4670
- onPageLoad = async (_event) => {
4671
- await nextMicrotask();
4672
- this.pageLoaded = true;
4673
- }
4674
-
4675
- // Private
4676
-
4677
- shouldHandlePopState() {
4678
- // Safari dispatches a popstate event after window's load event, ignore it
4679
- return this.pageIsLoaded()
4680
- }
4681
-
4682
- pageIsLoaded() {
4683
- return this.pageLoaded || document.readyState == "complete"
4684
- }
4685
4624
  }
4686
4625
 
4687
4626
  class LinkPrefetchObserver {
@@ -4756,7 +4695,7 @@ Copyright © 2025 37signals LLC
4756
4695
 
4757
4696
  fetchRequest.fetchOptions.priority = "low";
4758
4697
 
4759
- prefetchCache.setLater(location.toString(), fetchRequest, this.#cacheTtl);
4698
+ prefetchCache.putLater(location, fetchRequest, this.#cacheTtl);
4760
4699
  }
4761
4700
  }
4762
4701
  }
@@ -4772,7 +4711,7 @@ Copyright © 2025 37signals LLC
4772
4711
 
4773
4712
  #tryToUsePrefetchedRequest = (event) => {
4774
4713
  if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "GET") {
4775
- const cached = prefetchCache.get(event.detail.url.toString());
4714
+ const cached = prefetchCache.get(event.detail.url);
4776
4715
 
4777
4716
  if (cached) {
4778
4717
  // User clicked link, use cache response
@@ -4962,7 +4901,7 @@ Copyright © 2025 37signals LLC
4962
4901
  } else {
4963
4902
  await this.view.renderPage(snapshot, false, true, this.currentVisit);
4964
4903
  }
4965
- if(!snapshot.shouldPreserveScrollPosition) {
4904
+ if (snapshot.refreshScroll !== "preserve") {
4966
4905
  this.view.scrollToTop();
4967
4906
  }
4968
4907
  this.view.clearSnapshotCache();
@@ -5002,20 +4941,10 @@ Copyright © 2025 37signals LLC
5002
4941
  delete this.currentVisit;
5003
4942
  }
5004
4943
 
4944
+ // Same-page links are no longer handled with a Visit.
4945
+ // This method is still needed for Turbo Native adapters.
5005
4946
  locationWithActionIsSamePage(location, action) {
5006
- const anchor = getAnchor(location);
5007
- const currentAnchor = getAnchor(this.view.lastRenderedLocation);
5008
- const isRestorationToTop = action === "restore" && typeof anchor === "undefined";
5009
-
5010
- return (
5011
- action !== "replace" &&
5012
- getRequestURL(location) === getRequestURL(this.view.lastRenderedLocation) &&
5013
- (isRestorationToTop || (anchor != null && anchor !== currentAnchor))
5014
- )
5015
- }
5016
-
5017
- visitScrolledToSamePageLocation(oldURL, newURL) {
5018
- this.delegate.visitScrolledToSamePageLocation(oldURL, newURL);
4947
+ return false
5019
4948
  }
5020
4949
 
5021
4950
  // Visits
@@ -5408,13 +5337,18 @@ Copyright © 2025 37signals LLC
5408
5337
 
5409
5338
  #setLanguage() {
5410
5339
  const { documentElement } = this.currentSnapshot;
5411
- const { lang } = this.newSnapshot;
5340
+ const { dir, lang } = this.newSnapshot;
5412
5341
 
5413
5342
  if (lang) {
5414
5343
  documentElement.setAttribute("lang", lang);
5415
5344
  } else {
5416
5345
  documentElement.removeAttribute("lang");
5417
5346
  }
5347
+ if (dir) {
5348
+ documentElement.setAttribute("dir", dir);
5349
+ } else {
5350
+ documentElement.removeAttribute("dir");
5351
+ }
5418
5352
  }
5419
5353
 
5420
5354
  async mergeHead() {
@@ -5516,9 +5450,16 @@ Copyright © 2025 37signals LLC
5516
5450
 
5517
5451
  activateNewBody() {
5518
5452
  document.adoptNode(this.newElement);
5453
+ this.removeNoscriptElements();
5519
5454
  this.activateNewBodyScriptElements();
5520
5455
  }
5521
5456
 
5457
+ removeNoscriptElements() {
5458
+ for (const noscriptElement of this.newElement.querySelectorAll("noscript")) {
5459
+ noscriptElement.remove();
5460
+ }
5461
+ }
5462
+
5522
5463
  activateNewBodyScriptElements() {
5523
5464
  for (const inertScriptElement of this.newBodyScriptElements) {
5524
5465
  const activatedScriptElement = activateScriptElement(inertScriptElement);
@@ -5594,58 +5535,13 @@ Copyright © 2025 37signals LLC
5594
5535
  }
5595
5536
  }
5596
5537
 
5597
- class SnapshotCache {
5598
- keys = []
5599
- snapshots = {}
5600
-
5538
+ class SnapshotCache extends LRUCache {
5601
5539
  constructor(size) {
5602
- this.size = size;
5603
- }
5604
-
5605
- has(location) {
5606
- return toCacheKey(location) in this.snapshots
5607
- }
5608
-
5609
- get(location) {
5610
- if (this.has(location)) {
5611
- const snapshot = this.read(location);
5612
- this.touch(location);
5613
- return snapshot
5614
- }
5615
- }
5616
-
5617
- put(location, snapshot) {
5618
- this.write(location, snapshot);
5619
- this.touch(location);
5620
- return snapshot
5540
+ super(size, toCacheKey);
5621
5541
  }
5622
5542
 
5623
- clear() {
5624
- this.snapshots = {};
5625
- }
5626
-
5627
- // Private
5628
-
5629
- read(location) {
5630
- return this.snapshots[toCacheKey(location)]
5631
- }
5632
-
5633
- write(location, snapshot) {
5634
- this.snapshots[toCacheKey(location)] = snapshot;
5635
- }
5636
-
5637
- touch(location) {
5638
- const key = toCacheKey(location);
5639
- const index = this.keys.indexOf(key);
5640
- if (index > -1) this.keys.splice(index, 1);
5641
- this.keys.unshift(key);
5642
- this.trim();
5643
- }
5644
-
5645
- trim() {
5646
- for (const key of this.keys.splice(this.size)) {
5647
- delete this.snapshots[key];
5648
- }
5543
+ get snapshots() {
5544
+ return this.entries
5649
5545
  }
5650
5546
  }
5651
5547
 
@@ -5659,7 +5555,7 @@ Copyright © 2025 37signals LLC
5659
5555
  }
5660
5556
 
5661
5557
  renderPage(snapshot, isPreview = false, willRender = true, visit) {
5662
- const shouldMorphPage = this.isPageRefresh(visit) && this.snapshot.shouldMorphPage;
5558
+ const shouldMorphPage = this.isPageRefresh(visit) && (visit?.refresh?.method || this.snapshot.refreshMethod) === "morph";
5663
5559
  const rendererClass = shouldMorphPage ? MorphingPageRenderer : PageRenderer;
5664
5560
 
5665
5561
  const renderer = new rendererClass(this.snapshot, snapshot, isPreview, willRender);
@@ -5703,7 +5599,7 @@ Copyright © 2025 37signals LLC
5703
5599
  }
5704
5600
 
5705
5601
  shouldPreserveScrollPosition(visit) {
5706
- return this.isPageRefresh(visit) && this.snapshot.shouldPreserveScrollPosition
5602
+ return this.isPageRefresh(visit) && (visit?.refresh?.scroll || this.snapshot.refreshScroll) === "preserve"
5707
5603
  }
5708
5604
 
5709
5605
  get snapshot() {
@@ -5893,11 +5789,14 @@ Copyright © 2025 37signals LLC
5893
5789
  }
5894
5790
  }
5895
5791
 
5896
- refresh(url, requestId) {
5792
+ refresh(url, options = {}) {
5793
+ options = typeof options === "string" ? { requestId: options } : options;
5794
+
5795
+ const { method, requestId, scroll } = options;
5897
5796
  const isRecentRequest = requestId && this.recentRequests.has(requestId);
5898
5797
  const isCurrentUrl = url === document.baseURI;
5899
5798
  if (!isRecentRequest && !this.navigator.currentVisit && isCurrentUrl) {
5900
- this.visit(url, { action: "replace", shouldCacheSnapshot: false });
5799
+ this.visit(url, { action: "replace", shouldCacheSnapshot: false, refresh: { method, scroll } });
5901
5800
  }
5902
5801
  }
5903
5802
 
@@ -6001,6 +5900,12 @@ Copyright © 2025 37signals LLC
6001
5900
  }
6002
5901
  }
6003
5902
 
5903
+ historyPoppedWithEmptyState(location) {
5904
+ this.history.replace(location);
5905
+ this.view.lastRenderedLocation = location;
5906
+ this.view.cacheSnapshot();
5907
+ }
5908
+
6004
5909
  // Scroll observer delegate
6005
5910
 
6006
5911
  scrollPositionChanged(position) {
@@ -6045,7 +5950,7 @@ Copyright © 2025 37signals LLC
6045
5950
  // Navigator delegate
6046
5951
 
6047
5952
  allowsVisitingLocationWithAction(location, action) {
6048
- return this.locationWithActionIsSamePage(location, action) || this.applicationAllowsVisitingLocation(location)
5953
+ return this.applicationAllowsVisitingLocation(location)
6049
5954
  }
6050
5955
 
6051
5956
  visitProposedToLocation(location, options) {
@@ -6061,9 +5966,7 @@ Copyright © 2025 37signals LLC
6061
5966
  this.view.markVisitDirection(visit.direction);
6062
5967
  }
6063
5968
  extendURLWithDeprecatedProperties(visit.location);
6064
- if (!visit.silent) {
6065
- this.notifyApplicationAfterVisitingLocation(visit.location, visit.action);
6066
- }
5969
+ this.notifyApplicationAfterVisitingLocation(visit.location, visit.action);
6067
5970
  }
6068
5971
 
6069
5972
  visitCompleted(visit) {
@@ -6072,14 +5975,6 @@ Copyright © 2025 37signals LLC
6072
5975
  this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
6073
5976
  }
6074
5977
 
6075
- locationWithActionIsSamePage(location, action) {
6076
- return this.navigator.locationWithActionIsSamePage(location, action)
6077
- }
6078
-
6079
- visitScrolledToSamePageLocation(oldURL, newURL) {
6080
- this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
6081
- }
6082
-
6083
5978
  // Form submit observer delegate
6084
5979
 
6085
5980
  willSubmitForm(form, submitter) {
@@ -6119,9 +6014,7 @@ Copyright © 2025 37signals LLC
6119
6014
  // Page view delegate
6120
6015
 
6121
6016
  viewWillCacheSnapshot() {
6122
- if (!this.navigator.currentVisit?.silent) {
6123
- this.notifyApplicationBeforeCachingSnapshot();
6124
- }
6017
+ this.notifyApplicationBeforeCachingSnapshot();
6125
6018
  }
6126
6019
 
6127
6020
  allowsImmediateRender({ element }, options) {
@@ -6213,15 +6106,6 @@ Copyright © 2025 37signals LLC
6213
6106
  })
6214
6107
  }
6215
6108
 
6216
- notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL) {
6217
- dispatchEvent(
6218
- new HashChangeEvent("hashchange", {
6219
- oldURL: oldURL.toString(),
6220
- newURL: newURL.toString()
6221
- })
6222
- );
6223
- }
6224
-
6225
6109
  notifyApplicationAfterFrameLoad(frame) {
6226
6110
  return dispatch("turbo:frame-load", { target: frame })
6227
6111
  }
@@ -6307,7 +6191,7 @@ Copyright © 2025 37signals LLC
6307
6191
  };
6308
6192
 
6309
6193
  const session = new Session(recentRequests);
6310
- const { cache, navigator: navigator$1 } = session;
6194
+ const { cache, navigator } = session;
6311
6195
 
6312
6196
  /**
6313
6197
  * Starts the main session.
@@ -6373,19 +6257,6 @@ Copyright © 2025 37signals LLC
6373
6257
  session.renderStreamMessage(message);
6374
6258
  }
6375
6259
 
6376
- /**
6377
- * Removes all entries from the Turbo Drive page cache.
6378
- * Call this when state has changed on the server that may affect cached pages.
6379
- *
6380
- * @deprecated since version 7.2.0 in favor of `Turbo.cache.clear()`
6381
- */
6382
- function clearCache() {
6383
- console.warn(
6384
- "Please replace `Turbo.clearCache()` with `Turbo.cache.clear()`. The top-level function is deprecated and will be removed in a future version of Turbo.`"
6385
- );
6386
- session.clearCache();
6387
- }
6388
-
6389
6260
  /**
6390
6261
  * Sets the delay after which the progress bar will appear during navigation.
6391
6262
  *
@@ -6445,7 +6316,7 @@ Copyright © 2025 37signals LLC
6445
6316
 
6446
6317
  var Turbo = /*#__PURE__*/Object.freeze({
6447
6318
  __proto__: null,
6448
- navigator: navigator$1,
6319
+ navigator: navigator,
6449
6320
  session: session,
6450
6321
  cache: cache,
6451
6322
  PageRenderer: PageRenderer,
@@ -6459,7 +6330,6 @@ Copyright © 2025 37signals LLC
6459
6330
  connectStreamSource: connectStreamSource,
6460
6331
  disconnectStreamSource: disconnectStreamSource,
6461
6332
  renderStreamMessage: renderStreamMessage,
6462
- clearCache: clearCache,
6463
6333
  setProgressBarDelay: setProgressBarDelay,
6464
6334
  setConfirmMethod: setConfirmMethod,
6465
6335
  setFormMode: setFormMode,
@@ -6514,11 +6384,17 @@ Copyright © 2025 37signals LLC
6514
6384
  this.formLinkClickObserver.stop();
6515
6385
  this.linkInterceptor.stop();
6516
6386
  this.formSubmitObserver.stop();
6387
+
6388
+ if (!this.element.hasAttribute("recurse")) {
6389
+ this.#currentFetchRequest?.cancel();
6390
+ }
6517
6391
  }
6518
6392
  }
6519
6393
 
6520
6394
  disabledChanged() {
6521
- if (this.loadingStyle == FrameLoadingStyle.eager) {
6395
+ if (this.disabled) {
6396
+ this.#currentFetchRequest?.cancel();
6397
+ } else if (this.loadingStyle == FrameLoadingStyle.eager) {
6522
6398
  this.#loadSourceURL();
6523
6399
  }
6524
6400
  }
@@ -6526,6 +6402,10 @@ Copyright © 2025 37signals LLC
6526
6402
  sourceURLChanged() {
6527
6403
  if (this.#isIgnoringChangesTo("src")) return
6528
6404
 
6405
+ if (!this.sourceURL) {
6406
+ this.#currentFetchRequest?.cancel();
6407
+ }
6408
+
6529
6409
  if (this.element.isConnected) {
6530
6410
  this.complete = false;
6531
6411
  }
@@ -6627,15 +6507,18 @@ Copyright © 2025 37signals LLC
6627
6507
  }
6628
6508
 
6629
6509
  this.formSubmission = new FormSubmission(this, element, submitter);
6510
+
6630
6511
  const { fetchRequest } = this.formSubmission;
6631
- this.prepareRequest(fetchRequest);
6512
+ const frame = this.#findFrameElement(element, submitter);
6513
+
6514
+ this.prepareRequest(fetchRequest, frame);
6632
6515
  this.formSubmission.start();
6633
6516
  }
6634
6517
 
6635
6518
  // Fetch request delegate
6636
6519
 
6637
- prepareRequest(request) {
6638
- request.headers["Turbo-Frame"] = this.id;
6520
+ prepareRequest(request, frame = this) {
6521
+ request.headers["Turbo-Frame"] = frame.id;
6639
6522
 
6640
6523
  if (this.currentNavigationElement?.hasAttribute("data-turbo-stream")) {
6641
6524
  request.acceptResponseType(StreamMessage.contentType);
@@ -6877,7 +6760,9 @@ Copyright © 2025 37signals LLC
6877
6760
 
6878
6761
  #findFrameElement(element, submitter) {
6879
6762
  const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
6880
- return getFrameElementById(id) ?? this.element
6763
+ const target = this.#getFrameElementById(id);
6764
+
6765
+ return target instanceof FrameElement ? target : this.element
6881
6766
  }
6882
6767
 
6883
6768
  async extractForeignFrameElement(container) {
@@ -6921,9 +6806,11 @@ Copyright © 2025 37signals LLC
6921
6806
  }
6922
6807
 
6923
6808
  if (id) {
6924
- const frameElement = getFrameElementById(id);
6809
+ const frameElement = this.#getFrameElementById(id);
6925
6810
  if (frameElement) {
6926
6811
  return !frameElement.disabled
6812
+ } else if (id == "_parent") {
6813
+ return false
6927
6814
  }
6928
6815
  }
6929
6816
 
@@ -6944,8 +6831,12 @@ Copyright © 2025 37signals LLC
6944
6831
  return this.element.id
6945
6832
  }
6946
6833
 
6834
+ get disabled() {
6835
+ return this.element.disabled
6836
+ }
6837
+
6947
6838
  get enabled() {
6948
- return !this.element.disabled
6839
+ return !this.disabled
6949
6840
  }
6950
6841
 
6951
6842
  get sourceURL() {
@@ -7005,13 +6896,15 @@ Copyright © 2025 37signals LLC
7005
6896
  callback();
7006
6897
  delete this.currentNavigationElement;
7007
6898
  }
7008
- }
7009
6899
 
7010
- function getFrameElementById(id) {
7011
- if (id != null) {
7012
- const element = document.getElementById(id);
7013
- if (element instanceof FrameElement) {
7014
- return element
6900
+ #getFrameElementById(id) {
6901
+ if (id != null) {
6902
+ const element = id === "_parent" ?
6903
+ this.element.parentElement.closest("turbo-frame") :
6904
+ document.getElementById(id);
6905
+ if (element instanceof FrameElement) {
6906
+ return element
6907
+ }
7015
6908
  }
7016
6909
  }
7017
6910
  }
@@ -7036,6 +6929,7 @@ Copyright © 2025 37signals LLC
7036
6929
 
7037
6930
  const StreamActions = {
7038
6931
  after() {
6932
+ this.removeDuplicateTargetSiblings();
7039
6933
  this.targetElements.forEach((e) => e.parentElement?.insertBefore(this.templateContent, e.nextSibling));
7040
6934
  },
7041
6935
 
@@ -7045,6 +6939,7 @@ Copyright © 2025 37signals LLC
7045
6939
  },
7046
6940
 
7047
6941
  before() {
6942
+ this.removeDuplicateTargetSiblings();
7048
6943
  this.targetElements.forEach((e) => e.parentElement?.insertBefore(this.templateContent, e));
7049
6944
  },
7050
6945
 
@@ -7083,7 +6978,11 @@ Copyright © 2025 37signals LLC
7083
6978
  },
7084
6979
 
7085
6980
  refresh() {
7086
- session.refresh(this.baseURI, this.requestId);
6981
+ const method = this.getAttribute("method");
6982
+ const requestId = this.requestId;
6983
+ const scroll = this.getAttribute("scroll");
6984
+
6985
+ session.refresh(this.baseURI, { method, requestId, scroll });
7087
6986
  }
7088
6987
  };
7089
6988
 
@@ -7161,6 +7060,23 @@ Copyright © 2025 37signals LLC
7161
7060
  return existingChildren.filter((c) => newChildrenIds.includes(c.getAttribute("id")))
7162
7061
  }
7163
7062
 
7063
+ /**
7064
+ * Removes duplicate siblings (by ID)
7065
+ */
7066
+ removeDuplicateTargetSiblings() {
7067
+ this.duplicateSiblings.forEach((c) => c.remove());
7068
+ }
7069
+
7070
+ /**
7071
+ * Gets the list of duplicate siblings (i.e. those with the same ID)
7072
+ */
7073
+ get duplicateSiblings() {
7074
+ const existingChildren = this.targetElements.flatMap((e) => [...e.parentElement.children]).filter((c) => !!c.id);
7075
+ const newChildrenIds = [...(this.templateContent?.children || [])].filter((c) => !!c.id).map((c) => c.id);
7076
+
7077
+ return existingChildren.filter((c) => newChildrenIds.includes(c.id))
7078
+ }
7079
+
7164
7080
  /**
7165
7081
  * Gets the action function to be performed.
7166
7082
  */
@@ -7312,11 +7228,11 @@ Copyright © 2025 37signals LLC
7312
7228
  }
7313
7229
 
7314
7230
  (() => {
7315
- let element = document.currentScript;
7316
- if (!element) return
7317
- if (element.hasAttribute("data-turbo-suppress-warning")) return
7231
+ const scriptElement = document.currentScript;
7232
+ if (!scriptElement) return
7233
+ if (scriptElement.hasAttribute("data-turbo-suppress-warning")) return
7318
7234
 
7319
- element = element.parentElement;
7235
+ let element = scriptElement.parentElement;
7320
7236
  while (element) {
7321
7237
  if (element == document.body) {
7322
7238
  return console.warn(
@@ -7330,7 +7246,7 @@ Copyright © 2025 37signals LLC
7330
7246
  ——
7331
7247
  Suppress this warning by adding a "data-turbo-suppress-warning" attribute to: %s
7332
7248
  `,
7333
- element.outerHTML
7249
+ scriptElement.outerHTML
7334
7250
  )
7335
7251
  }
7336
7252
 
@@ -7354,7 +7270,6 @@ Copyright © 2025 37signals LLC
7354
7270
  exports.StreamElement = StreamElement;
7355
7271
  exports.StreamSourceElement = StreamSourceElement;
7356
7272
  exports.cache = cache;
7357
- exports.clearCache = clearCache;
7358
7273
  exports.config = config;
7359
7274
  exports.connectStreamSource = connectStreamSource;
7360
7275
  exports.disconnectStreamSource = disconnectStreamSource;
@@ -7366,7 +7281,7 @@ Copyright © 2025 37signals LLC
7366
7281
  exports.morphChildren = morphChildren;
7367
7282
  exports.morphElements = morphElements;
7368
7283
  exports.morphTurboFrameElements = morphTurboFrameElements;
7369
- exports.navigator = navigator$1;
7284
+ exports.navigator = navigator;
7370
7285
  exports.registerAdapter = registerAdapter;
7371
7286
  exports.renderStreamMessage = renderStreamMessage;
7372
7287
  exports.session = session;