@hotwired/turbo-rails 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,68 +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
- (function(prototype) {
6
- if (typeof prototype.requestSubmit == "function") return;
7
- prototype.requestSubmit = function(submitter) {
8
- if (submitter) {
9
- validateSubmitter(submitter, this);
10
- submitter.click();
11
- } else {
12
- submitter = document.createElement("input");
13
- submitter.type = "submit";
14
- submitter.hidden = true;
15
- this.appendChild(submitter);
16
- submitter.click();
17
- this.removeChild(submitter);
18
- }
19
- };
20
- function validateSubmitter(submitter, form) {
21
- submitter instanceof HTMLElement || raise(TypeError, "parameter 1 is not of type 'HTMLElement'");
22
- submitter.type == "submit" || raise(TypeError, "The specified element is not a submit button");
23
- submitter.form == form || raise(DOMException, "The specified element is not owned by this form element", "NotFoundError");
24
- }
25
- function raise(errorConstructor, message, name) {
26
- throw new errorConstructor("Failed to execute 'requestSubmit' on 'HTMLFormElement': " + message + ".", name);
27
- }
28
- })(HTMLFormElement.prototype);
29
-
30
- const submittersByForm = new WeakMap;
31
-
32
- function findSubmitterFromClickTarget(target) {
33
- const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
34
- const candidate = element ? element.closest("input, button") : null;
35
- return candidate?.type == "submit" ? candidate : null;
36
- }
37
-
38
- function clickCaptured(event) {
39
- const submitter = findSubmitterFromClickTarget(event.target);
40
- if (submitter && submitter.form) {
41
- submittersByForm.set(submitter.form, submitter);
42
- }
43
- }
44
-
45
- (function() {
46
- if ("submitter" in Event.prototype) return;
47
- let prototype = window.Event.prototype;
48
- if ("SubmitEvent" in window) {
49
- const prototypeOfSubmitEvent = window.SubmitEvent.prototype;
50
- if (/Apple Computer/.test(navigator.vendor) && !("submitter" in prototypeOfSubmitEvent)) {
51
- prototype = prototypeOfSubmitEvent;
52
- } else {
53
- return;
54
- }
55
- }
56
- addEventListener("click", clickCaptured, true);
57
- Object.defineProperty(prototype, "submitter", {
58
- get() {
59
- if (this.type == "submit" && this.target instanceof HTMLFormElement) {
60
- return submittersByForm.get(this.target);
61
- }
62
- }
63
- });
64
- })();
65
-
66
5
  const FrameLoadingStyle = {
67
6
  eager: "eager",
68
7
  lazy: "lazy"
@@ -240,10 +179,6 @@ function nextEventLoopTick() {
240
179
  return new Promise((resolve => setTimeout((() => resolve()), 0)));
241
180
  }
242
181
 
243
- function nextMicrotask() {
244
- return Promise.resolve();
245
- }
246
-
247
182
  function parseHTMLDocument(html = "") {
248
183
  return (new DOMParser).parseFromString(html, "text/html");
249
184
  }
@@ -273,7 +208,7 @@ function uuid() {
273
208
  } else if (i == 19) {
274
209
  return (Math.floor(Math.random() * 4) + 8).toString(16);
275
210
  } else {
276
- return Math.floor(Math.random() * 15).toString(16);
211
+ return Math.floor(Math.random() * 16).toString(16);
277
212
  }
278
213
  })).join("");
279
214
  }
@@ -411,15 +346,13 @@ function doesNotTargetIFrame(name) {
411
346
  function findLinkFromClickTarget(target) {
412
347
  const link = findClosestRecursively(target, "a[href], a[xlink\\:href]");
413
348
  if (!link) return null;
349
+ if (link.href.startsWith("#")) return null;
414
350
  if (link.hasAttribute("download")) return null;
415
- if (link.hasAttribute("target") && link.target !== "_self") return null;
351
+ const linkTarget = link.getAttribute("target");
352
+ if (linkTarget && linkTarget !== "_self") return null;
416
353
  return link;
417
354
  }
418
355
 
419
- function getLocationForLink(link) {
420
- return expandURL(link.getAttribute("href") || "");
421
- }
422
-
423
356
  function debounce(fn, delay) {
424
357
  let timeoutId = null;
425
358
  return (...args) => {
@@ -500,6 +433,10 @@ function locationIsVisitable(location, rootLocation) {
500
433
  return isPrefixedBy(location, rootLocation) && !config.drive.unvisitableExtensions.has(getExtension(location));
501
434
  }
502
435
 
436
+ function getLocationForLink(link) {
437
+ return expandURL(link.getAttribute("href") || "");
438
+ }
439
+
503
440
  function getRequestURL(url) {
504
441
  const anchor = getAnchor(url);
505
442
  return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href;
@@ -871,34 +808,94 @@ function importStreamElements(fragment) {
871
808
  return fragment;
872
809
  }
873
810
 
811
+ const identity = key => key;
812
+
813
+ class LRUCache {
814
+ keys=[];
815
+ entries={};
816
+ #toCacheKey;
817
+ constructor(size, toCacheKey = identity) {
818
+ this.size = size;
819
+ this.#toCacheKey = toCacheKey;
820
+ }
821
+ has(key) {
822
+ return this.#toCacheKey(key) in this.entries;
823
+ }
824
+ get(key) {
825
+ if (this.has(key)) {
826
+ const entry = this.read(key);
827
+ this.touch(key);
828
+ return entry;
829
+ }
830
+ }
831
+ put(key, entry) {
832
+ this.write(key, entry);
833
+ this.touch(key);
834
+ return entry;
835
+ }
836
+ clear() {
837
+ for (const key of Object.keys(this.entries)) {
838
+ this.evict(key);
839
+ }
840
+ }
841
+ read(key) {
842
+ return this.entries[this.#toCacheKey(key)];
843
+ }
844
+ write(key, entry) {
845
+ this.entries[this.#toCacheKey(key)] = entry;
846
+ }
847
+ touch(key) {
848
+ key = this.#toCacheKey(key);
849
+ const index = this.keys.indexOf(key);
850
+ if (index > -1) this.keys.splice(index, 1);
851
+ this.keys.unshift(key);
852
+ this.trim();
853
+ }
854
+ trim() {
855
+ for (const key of this.keys.splice(this.size)) {
856
+ this.evict(key);
857
+ }
858
+ }
859
+ evict(key) {
860
+ delete this.entries[key];
861
+ }
862
+ }
863
+
874
864
  const PREFETCH_DELAY = 100;
875
865
 
876
- class PrefetchCache {
866
+ class PrefetchCache extends LRUCache {
877
867
  #prefetchTimeout=null;
878
- #prefetched=null;
879
- get(url) {
880
- if (this.#prefetched && this.#prefetched.url === url && this.#prefetched.expire > Date.now()) {
881
- return this.#prefetched.request;
882
- }
868
+ #maxAges={};
869
+ constructor(size = 1, prefetchDelay = PREFETCH_DELAY) {
870
+ super(size, toCacheKey);
871
+ this.prefetchDelay = prefetchDelay;
883
872
  }
884
- setLater(url, request, ttl) {
885
- this.clear();
873
+ putLater(url, request, ttl) {
886
874
  this.#prefetchTimeout = setTimeout((() => {
887
875
  request.perform();
888
- this.set(url, request, ttl);
876
+ this.put(url, request, ttl);
889
877
  this.#prefetchTimeout = null;
890
- }), PREFETCH_DELAY);
878
+ }), this.prefetchDelay);
891
879
  }
892
- set(url, request, ttl) {
893
- this.#prefetched = {
894
- url: url,
895
- request: request,
896
- expire: new Date((new Date).getTime() + ttl)
897
- };
880
+ put(url, request, ttl = cacheTtl) {
881
+ super.put(url, request);
882
+ this.#maxAges[toCacheKey(url)] = new Date((new Date).getTime() + ttl);
898
883
  }
899
884
  clear() {
885
+ super.clear();
900
886
  if (this.#prefetchTimeout) clearTimeout(this.#prefetchTimeout);
901
- this.#prefetched = null;
887
+ }
888
+ evict(key) {
889
+ super.evict(key);
890
+ delete this.#maxAges[key];
891
+ }
892
+ has(key) {
893
+ if (super.has(key)) {
894
+ const maxAge = this.#maxAges[toCacheKey(key)];
895
+ return maxAge && maxAge > Date.now();
896
+ } else {
897
+ return false;
898
+ }
902
899
  }
903
900
  }
904
901
 
@@ -2629,11 +2626,17 @@ class PageSnapshot extends Snapshot {
2629
2626
  for (const clonedPasswordInput of clonedElement.querySelectorAll('input[type="password"]')) {
2630
2627
  clonedPasswordInput.value = "";
2631
2628
  }
2629
+ for (const clonedNoscriptElement of clonedElement.querySelectorAll("noscript")) {
2630
+ clonedNoscriptElement.remove();
2631
+ }
2632
2632
  return new PageSnapshot(this.documentElement, clonedElement, this.headSnapshot);
2633
2633
  }
2634
2634
  get lang() {
2635
2635
  return this.documentElement.getAttribute("lang");
2636
2636
  }
2637
+ get dir() {
2638
+ return this.documentElement.getAttribute("dir");
2639
+ }
2637
2640
  get headElement() {
2638
2641
  return this.headSnapshot.element;
2639
2642
  }
@@ -2657,11 +2660,11 @@ class PageSnapshot extends Snapshot {
2657
2660
  const viewTransitionEnabled = this.getSetting("view-transition") === "true" || this.headSnapshot.getMetaValue("view-transition") === "same-origin";
2658
2661
  return viewTransitionEnabled && !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
2659
2662
  }
2660
- get shouldMorphPage() {
2661
- return this.getSetting("refresh-method") === "morph";
2663
+ get refreshMethod() {
2664
+ return this.getSetting("refresh-method");
2662
2665
  }
2663
- get shouldPreserveScrollPosition() {
2664
- return this.getSetting("refresh-scroll") === "preserve";
2666
+ get refreshScroll() {
2667
+ return this.getSetting("refresh-scroll");
2665
2668
  }
2666
2669
  getSetting(name) {
2667
2670
  return this.headSnapshot.getMetaValue(`turbo-${name}`);
@@ -2694,7 +2697,8 @@ const defaultOptions = {
2694
2697
  willRender: true,
2695
2698
  updateHistory: true,
2696
2699
  shouldCacheSnapshot: true,
2697
- acceptsStreamResponse: false
2700
+ acceptsStreamResponse: false,
2701
+ refresh: {}
2698
2702
  };
2699
2703
 
2700
2704
  const TimingMetric = {
@@ -2739,7 +2743,7 @@ class Visit {
2739
2743
  this.delegate = delegate;
2740
2744
  this.location = location;
2741
2745
  this.restorationIdentifier = restorationIdentifier || uuid();
2742
- const {action: action, historyChanged: historyChanged, referrer: referrer, snapshot: snapshot, snapshotHTML: snapshotHTML, response: response, visitCachedSnapshot: visitCachedSnapshot, willRender: willRender, updateHistory: updateHistory, shouldCacheSnapshot: shouldCacheSnapshot, acceptsStreamResponse: acceptsStreamResponse, direction: direction} = {
2746
+ const {action: action, historyChanged: historyChanged, referrer: referrer, snapshot: snapshot, snapshotHTML: snapshotHTML, response: response, visitCachedSnapshot: visitCachedSnapshot, willRender: willRender, updateHistory: updateHistory, shouldCacheSnapshot: shouldCacheSnapshot, acceptsStreamResponse: acceptsStreamResponse, direction: direction, refresh: refresh} = {
2743
2747
  ...defaultOptions,
2744
2748
  ...options
2745
2749
  };
@@ -2749,7 +2753,6 @@ class Visit {
2749
2753
  this.snapshot = snapshot;
2750
2754
  this.snapshotHTML = snapshotHTML;
2751
2755
  this.response = response;
2752
- this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
2753
2756
  this.isPageRefresh = this.view.isPageRefresh(this);
2754
2757
  this.visitCachedSnapshot = visitCachedSnapshot;
2755
2758
  this.willRender = willRender;
@@ -2758,6 +2761,7 @@ class Visit {
2758
2761
  this.shouldCacheSnapshot = shouldCacheSnapshot;
2759
2762
  this.acceptsStreamResponse = acceptsStreamResponse;
2760
2763
  this.direction = direction || Direction[action];
2764
+ this.refresh = refresh;
2761
2765
  }
2762
2766
  get adapter() {
2763
2767
  return this.delegate.adapter;
@@ -2771,9 +2775,6 @@ class Visit {
2771
2775
  get restorationData() {
2772
2776
  return this.history.getRestorationDataForIdentifier(this.restorationIdentifier);
2773
2777
  }
2774
- get silent() {
2775
- return this.isSamePage;
2776
- }
2777
2778
  start() {
2778
2779
  if (this.state == VisitState.initialized) {
2779
2780
  this.recordTimingMetric(TimingMetric.visitStart);
@@ -2892,7 +2893,7 @@ class Visit {
2892
2893
  const isPreview = this.shouldIssueRequest();
2893
2894
  this.render((async () => {
2894
2895
  this.cacheSnapshot();
2895
- if (this.isSamePage || this.isPageRefresh) {
2896
+ if (this.isPageRefresh) {
2896
2897
  this.adapter.visitRendered(this);
2897
2898
  } else {
2898
2899
  if (this.view.renderPromise) await this.view.renderPromise;
@@ -2916,16 +2917,6 @@ class Visit {
2916
2917
  this.followedRedirect = true;
2917
2918
  }
2918
2919
  }
2919
- goToSamePageAnchor() {
2920
- if (this.isSamePage) {
2921
- this.render((async () => {
2922
- this.cacheSnapshot();
2923
- this.performScroll();
2924
- this.changeHistory();
2925
- this.adapter.visitRendered(this);
2926
- }));
2927
- }
2928
- }
2929
2920
  prepareRequest(request) {
2930
2921
  if (this.acceptsStreamResponse) {
2931
2922
  request.acceptResponseType(StreamMessage.contentType);
@@ -2984,9 +2975,6 @@ class Visit {
2984
2975
  } else {
2985
2976
  this.scrollToAnchor() || this.view.scrollToTop();
2986
2977
  }
2987
- if (this.isSamePage) {
2988
- this.delegate.visitScrolledToSamePageLocation(this.view.lastRenderedLocation, this.location);
2989
- }
2990
2978
  this.scrolled = true;
2991
2979
  }
2992
2980
  }
@@ -3016,9 +3004,7 @@ class Visit {
3016
3004
  return typeof this.response == "object";
3017
3005
  }
3018
3006
  shouldIssueRequest() {
3019
- if (this.isSamePage) {
3020
- return false;
3021
- } else if (this.action == "restore") {
3007
+ if (this.action == "restore") {
3022
3008
  return !this.hasCachedSnapshot();
3023
3009
  } else {
3024
3010
  return this.willRender;
@@ -3073,7 +3059,6 @@ class BrowserAdapter {
3073
3059
  this.redirectedToLocation = null;
3074
3060
  visit.loadCachedSnapshot();
3075
3061
  visit.issueRequest();
3076
- visit.goToSamePageAnchor();
3077
3062
  }
3078
3063
  visitRequestStarted(visit) {
3079
3064
  this.progressBar.setValue(0);
@@ -3167,7 +3152,6 @@ class BrowserAdapter {
3167
3152
 
3168
3153
  class CacheObserver {
3169
3154
  selector="[data-turbo-temporary]";
3170
- deprecatedSelector="[data-turbo-cache=false]";
3171
3155
  started=false;
3172
3156
  start() {
3173
3157
  if (!this.started) {
@@ -3187,14 +3171,7 @@ class CacheObserver {
3187
3171
  }
3188
3172
  };
3189
3173
  get temporaryElements() {
3190
- return [ ...document.querySelectorAll(this.selector), ...this.temporaryElementsWithDeprecation ];
3191
- }
3192
- get temporaryElementsWithDeprecation() {
3193
- const elements = document.querySelectorAll(this.deprecatedSelector);
3194
- if (elements.length) {
3195
- console.warn(`The ${this.deprecatedSelector} selector is deprecated and will be removed in a future version. Use ${this.selector} instead.`);
3196
- }
3197
- return [ ...elements ];
3174
+ return [ ...document.querySelectorAll(this.selector) ];
3198
3175
  }
3199
3176
  }
3200
3177
 
@@ -3262,7 +3239,6 @@ class History {
3262
3239
  restorationIdentifier=uuid();
3263
3240
  restorationData={};
3264
3241
  started=false;
3265
- pageLoaded=false;
3266
3242
  currentIndex=0;
3267
3243
  constructor(delegate) {
3268
3244
  this.delegate = delegate;
@@ -3270,7 +3246,6 @@ class History {
3270
3246
  start() {
3271
3247
  if (!this.started) {
3272
3248
  addEventListener("popstate", this.onPopState, false);
3273
- addEventListener("load", this.onPageLoad, false);
3274
3249
  this.currentIndex = history.state?.turbo?.restorationIndex || 0;
3275
3250
  this.started = true;
3276
3251
  this.replace(new URL(window.location.href));
@@ -3279,7 +3254,6 @@ class History {
3279
3254
  stop() {
3280
3255
  if (this.started) {
3281
3256
  removeEventListener("popstate", this.onPopState, false);
3282
- removeEventListener("load", this.onPageLoad, false);
3283
3257
  this.started = false;
3284
3258
  }
3285
3259
  }
@@ -3325,28 +3299,19 @@ class History {
3325
3299
  }
3326
3300
  }
3327
3301
  onPopState=event => {
3328
- if (this.shouldHandlePopState()) {
3329
- const {turbo: turbo} = event.state || {};
3330
- if (turbo) {
3331
- this.location = new URL(window.location.href);
3332
- const {restorationIdentifier: restorationIdentifier, restorationIndex: restorationIndex} = turbo;
3333
- this.restorationIdentifier = restorationIdentifier;
3334
- const direction = restorationIndex > this.currentIndex ? "forward" : "back";
3335
- this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(this.location, restorationIdentifier, direction);
3336
- this.currentIndex = restorationIndex;
3337
- }
3302
+ const {turbo: turbo} = event.state || {};
3303
+ this.location = new URL(window.location.href);
3304
+ if (turbo) {
3305
+ const {restorationIdentifier: restorationIdentifier, restorationIndex: restorationIndex} = turbo;
3306
+ this.restorationIdentifier = restorationIdentifier;
3307
+ const direction = restorationIndex > this.currentIndex ? "forward" : "back";
3308
+ this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(this.location, restorationIdentifier, direction);
3309
+ this.currentIndex = restorationIndex;
3310
+ } else {
3311
+ this.currentIndex++;
3312
+ this.delegate.historyPoppedWithEmptyState(this.location);
3338
3313
  }
3339
3314
  };
3340
- onPageLoad=async _event => {
3341
- await nextMicrotask();
3342
- this.pageLoaded = true;
3343
- };
3344
- shouldHandlePopState() {
3345
- return this.pageIsLoaded();
3346
- }
3347
- pageIsLoaded() {
3348
- return this.pageLoaded || document.readyState == "complete";
3349
- }
3350
3315
  }
3351
3316
 
3352
3317
  class LinkPrefetchObserver {
@@ -3402,7 +3367,7 @@ class LinkPrefetchObserver {
3402
3367
  this.#prefetchedLink = link;
3403
3368
  const fetchRequest = new FetchRequest(this, FetchMethod.get, location, new URLSearchParams, target);
3404
3369
  fetchRequest.fetchOptions.priority = "low";
3405
- prefetchCache.setLater(location.toString(), fetchRequest, this.#cacheTtl);
3370
+ prefetchCache.putLater(location, fetchRequest, this.#cacheTtl);
3406
3371
  }
3407
3372
  }
3408
3373
  };
@@ -3415,7 +3380,7 @@ class LinkPrefetchObserver {
3415
3380
  };
3416
3381
  #tryToUsePrefetchedRequest=event => {
3417
3382
  if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "GET") {
3418
- const cached = prefetchCache.get(event.detail.url.toString());
3383
+ const cached = prefetchCache.get(event.detail.url);
3419
3384
  if (cached) {
3420
3385
  event.detail.fetchRequest = cached;
3421
3386
  }
@@ -3564,7 +3529,7 @@ class Navigator {
3564
3529
  } else {
3565
3530
  await this.view.renderPage(snapshot, false, true, this.currentVisit);
3566
3531
  }
3567
- if (!snapshot.shouldPreserveScrollPosition) {
3532
+ if (snapshot.refreshScroll !== "preserve") {
3568
3533
  this.view.scrollToTop();
3569
3534
  }
3570
3535
  this.view.clearSnapshotCache();
@@ -3592,13 +3557,7 @@ class Navigator {
3592
3557
  delete this.currentVisit;
3593
3558
  }
3594
3559
  locationWithActionIsSamePage(location, action) {
3595
- const anchor = getAnchor(location);
3596
- const currentAnchor = getAnchor(this.view.lastRenderedLocation);
3597
- const isRestorationToTop = action === "restore" && typeof anchor === "undefined";
3598
- return action !== "replace" && getRequestURL(location) === getRequestURL(this.view.lastRenderedLocation) && (isRestorationToTop || anchor != null && anchor !== currentAnchor);
3599
- }
3600
- visitScrolledToSamePageLocation(oldURL, newURL) {
3601
- this.delegate.visitScrolledToSamePageLocation(oldURL, newURL);
3560
+ return false;
3602
3561
  }
3603
3562
  get location() {
3604
3563
  return this.history.location;
@@ -3929,12 +3888,17 @@ class PageRenderer extends Renderer {
3929
3888
  }
3930
3889
  #setLanguage() {
3931
3890
  const {documentElement: documentElement} = this.currentSnapshot;
3932
- const {lang: lang} = this.newSnapshot;
3891
+ const {dir: dir, lang: lang} = this.newSnapshot;
3933
3892
  if (lang) {
3934
3893
  documentElement.setAttribute("lang", lang);
3935
3894
  } else {
3936
3895
  documentElement.removeAttribute("lang");
3937
3896
  }
3897
+ if (dir) {
3898
+ documentElement.setAttribute("dir", dir);
3899
+ } else {
3900
+ documentElement.removeAttribute("dir");
3901
+ }
3938
3902
  }
3939
3903
  async mergeHead() {
3940
3904
  const mergedHeadElements = this.mergeProvisionalElements();
@@ -4014,8 +3978,14 @@ class PageRenderer extends Renderer {
4014
3978
  }
4015
3979
  activateNewBody() {
4016
3980
  document.adoptNode(this.newElement);
3981
+ this.removeNoscriptElements();
4017
3982
  this.activateNewBodyScriptElements();
4018
3983
  }
3984
+ removeNoscriptElements() {
3985
+ for (const noscriptElement of this.newElement.querySelectorAll("noscript")) {
3986
+ noscriptElement.remove();
3987
+ }
3988
+ }
4019
3989
  activateNewBodyScriptElements() {
4020
3990
  for (const inertScriptElement of this.newBodyScriptElements) {
4021
3991
  const activatedScriptElement = activateScriptElement(inertScriptElement);
@@ -4079,47 +4049,12 @@ class MorphingPageRenderer extends PageRenderer {
4079
4049
  }
4080
4050
  }
4081
4051
 
4082
- class SnapshotCache {
4083
- keys=[];
4084
- snapshots={};
4052
+ class SnapshotCache extends LRUCache {
4085
4053
  constructor(size) {
4086
- this.size = size;
4087
- }
4088
- has(location) {
4089
- return toCacheKey(location) in this.snapshots;
4090
- }
4091
- get(location) {
4092
- if (this.has(location)) {
4093
- const snapshot = this.read(location);
4094
- this.touch(location);
4095
- return snapshot;
4096
- }
4097
- }
4098
- put(location, snapshot) {
4099
- this.write(location, snapshot);
4100
- this.touch(location);
4101
- return snapshot;
4054
+ super(size, toCacheKey);
4102
4055
  }
4103
- clear() {
4104
- this.snapshots = {};
4105
- }
4106
- read(location) {
4107
- return this.snapshots[toCacheKey(location)];
4108
- }
4109
- write(location, snapshot) {
4110
- this.snapshots[toCacheKey(location)] = snapshot;
4111
- }
4112
- touch(location) {
4113
- const key = toCacheKey(location);
4114
- const index = this.keys.indexOf(key);
4115
- if (index > -1) this.keys.splice(index, 1);
4116
- this.keys.unshift(key);
4117
- this.trim();
4118
- }
4119
- trim() {
4120
- for (const key of this.keys.splice(this.size)) {
4121
- delete this.snapshots[key];
4122
- }
4056
+ get snapshots() {
4057
+ return this.entries;
4123
4058
  }
4124
4059
  }
4125
4060
 
@@ -4131,7 +4066,7 @@ class PageView extends View {
4131
4066
  return this.snapshot.prefersViewTransitions && newSnapshot.prefersViewTransitions;
4132
4067
  }
4133
4068
  renderPage(snapshot, isPreview = false, willRender = true, visit) {
4134
- const shouldMorphPage = this.isPageRefresh(visit) && this.snapshot.shouldMorphPage;
4069
+ const shouldMorphPage = this.isPageRefresh(visit) && (visit?.refresh?.method || this.snapshot.refreshMethod) === "morph";
4135
4070
  const rendererClass = shouldMorphPage ? MorphingPageRenderer : PageRenderer;
4136
4071
  const renderer = new rendererClass(this.snapshot, snapshot, isPreview, willRender);
4137
4072
  if (!renderer.shouldRender) {
@@ -4166,7 +4101,7 @@ class PageView extends View {
4166
4101
  return !visit || this.lastRenderedLocation.pathname === visit.location.pathname && visit.action === "replace";
4167
4102
  }
4168
4103
  shouldPreserveScrollPosition(visit) {
4169
- return this.isPageRefresh(visit) && this.snapshot.shouldPreserveScrollPosition;
4104
+ return this.isPageRefresh(visit) && (visit?.refresh?.scroll || this.snapshot.refreshScroll) === "preserve";
4170
4105
  }
4171
4106
  get snapshot() {
4172
4107
  return PageSnapshot.fromElement(this.element);
@@ -4319,13 +4254,21 @@ class Session {
4319
4254
  this.navigator.proposeVisit(expandURL(location), options);
4320
4255
  }
4321
4256
  }
4322
- refresh(url, requestId) {
4257
+ refresh(url, options = {}) {
4258
+ options = typeof options === "string" ? {
4259
+ requestId: options
4260
+ } : options;
4261
+ const {method: method, requestId: requestId, scroll: scroll} = options;
4323
4262
  const isRecentRequest = requestId && this.recentRequests.has(requestId);
4324
4263
  const isCurrentUrl = url === document.baseURI;
4325
4264
  if (!isRecentRequest && !this.navigator.currentVisit && isCurrentUrl) {
4326
4265
  this.visit(url, {
4327
4266
  action: "replace",
4328
- shouldCacheSnapshot: false
4267
+ shouldCacheSnapshot: false,
4268
+ refresh: {
4269
+ method: method,
4270
+ scroll: scroll
4271
+ }
4329
4272
  });
4330
4273
  }
4331
4274
  }
@@ -4401,6 +4344,11 @@ class Session {
4401
4344
  });
4402
4345
  }
4403
4346
  }
4347
+ historyPoppedWithEmptyState(location) {
4348
+ this.history.replace(location);
4349
+ this.view.lastRenderedLocation = location;
4350
+ this.view.cacheSnapshot();
4351
+ }
4404
4352
  scrollPositionChanged(position) {
4405
4353
  this.history.updateRestorationData({
4406
4354
  scrollPosition: position
@@ -4425,7 +4373,7 @@ class Session {
4425
4373
  });
4426
4374
  }
4427
4375
  allowsVisitingLocationWithAction(location, action) {
4428
- return this.locationWithActionIsSamePage(location, action) || this.applicationAllowsVisitingLocation(location);
4376
+ return this.applicationAllowsVisitingLocation(location);
4429
4377
  }
4430
4378
  visitProposedToLocation(location, options) {
4431
4379
  extendURLWithDeprecatedProperties(location);
@@ -4437,21 +4385,13 @@ class Session {
4437
4385
  this.view.markVisitDirection(visit.direction);
4438
4386
  }
4439
4387
  extendURLWithDeprecatedProperties(visit.location);
4440
- if (!visit.silent) {
4441
- this.notifyApplicationAfterVisitingLocation(visit.location, visit.action);
4442
- }
4388
+ this.notifyApplicationAfterVisitingLocation(visit.location, visit.action);
4443
4389
  }
4444
4390
  visitCompleted(visit) {
4445
4391
  this.view.unmarkVisitDirection();
4446
4392
  clearBusyState(document.documentElement);
4447
4393
  this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
4448
4394
  }
4449
- locationWithActionIsSamePage(location, action) {
4450
- return this.navigator.locationWithActionIsSamePage(location, action);
4451
- }
4452
- visitScrolledToSamePageLocation(oldURL, newURL) {
4453
- this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
4454
- }
4455
4395
  willSubmitForm(form, submitter) {
4456
4396
  const action = getAction$1(form, submitter);
4457
4397
  return this.submissionIsNavigatable(form, submitter) && locationIsVisitable(expandURL(action), this.snapshot.rootLocation);
@@ -4473,9 +4413,7 @@ class Session {
4473
4413
  this.renderStreamMessage(message);
4474
4414
  }
4475
4415
  viewWillCacheSnapshot() {
4476
- if (!this.navigator.currentVisit?.silent) {
4477
- this.notifyApplicationBeforeCachingSnapshot();
4478
- }
4416
+ this.notifyApplicationBeforeCachingSnapshot();
4479
4417
  }
4480
4418
  allowsImmediateRender({element: element}, options) {
4481
4419
  const event = this.notifyApplicationBeforeRender(element, options);
@@ -4562,12 +4500,6 @@ class Session {
4562
4500
  }
4563
4501
  });
4564
4502
  }
4565
- notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL) {
4566
- dispatchEvent(new HashChangeEvent("hashchange", {
4567
- oldURL: oldURL.toString(),
4568
- newURL: newURL.toString()
4569
- }));
4570
- }
4571
4503
  notifyApplicationAfterFrameLoad(frame) {
4572
4504
  return dispatch("turbo:frame-load", {
4573
4505
  target: frame
@@ -4633,7 +4565,7 @@ const deprecatedLocationPropertyDescriptors = {
4633
4565
 
4634
4566
  const session = new Session(recentRequests);
4635
4567
 
4636
- const {cache: cache, navigator: navigator$1} = session;
4568
+ const {cache: cache, navigator: navigator} = session;
4637
4569
 
4638
4570
  function start() {
4639
4571
  session.start();
@@ -4659,11 +4591,6 @@ function renderStreamMessage(message) {
4659
4591
  session.renderStreamMessage(message);
4660
4592
  }
4661
4593
 
4662
- function clearCache() {
4663
- console.warn("Please replace `Turbo.clearCache()` with `Turbo.cache.clear()`. The top-level function is deprecated and will be removed in a future version of Turbo.`");
4664
- session.clearCache();
4665
- }
4666
-
4667
4594
  function setProgressBarDelay(delay) {
4668
4595
  console.warn("Please replace `Turbo.setProgressBarDelay(delay)` with `Turbo.config.drive.progressBarDelay = delay`. The top-level function is deprecated and will be removed in a future version of Turbo.`");
4669
4596
  config.drive.progressBarDelay = delay;
@@ -4689,7 +4616,7 @@ function morphTurboFrameElements(currentFrame, newFrame) {
4689
4616
 
4690
4617
  var Turbo = Object.freeze({
4691
4618
  __proto__: null,
4692
- navigator: navigator$1,
4619
+ navigator: navigator,
4693
4620
  session: session,
4694
4621
  cache: cache,
4695
4622
  PageRenderer: PageRenderer,
@@ -4703,7 +4630,6 @@ var Turbo = Object.freeze({
4703
4630
  connectStreamSource: connectStreamSource,
4704
4631
  disconnectStreamSource: disconnectStreamSource,
4705
4632
  renderStreamMessage: renderStreamMessage,
4706
- clearCache: clearCache,
4707
4633
  setProgressBarDelay: setProgressBarDelay,
4708
4634
  setConfirmMethod: setConfirmMethod,
4709
4635
  setFormMode: setFormMode,
@@ -4753,15 +4679,23 @@ class FrameController {
4753
4679
  this.formLinkClickObserver.stop();
4754
4680
  this.linkInterceptor.stop();
4755
4681
  this.formSubmitObserver.stop();
4682
+ if (!this.element.hasAttribute("recurse")) {
4683
+ this.#currentFetchRequest?.cancel();
4684
+ }
4756
4685
  }
4757
4686
  }
4758
4687
  disabledChanged() {
4759
- if (this.loadingStyle == FrameLoadingStyle.eager) {
4688
+ if (this.disabled) {
4689
+ this.#currentFetchRequest?.cancel();
4690
+ } else if (this.loadingStyle == FrameLoadingStyle.eager) {
4760
4691
  this.#loadSourceURL();
4761
4692
  }
4762
4693
  }
4763
4694
  sourceURLChanged() {
4764
4695
  if (this.#isIgnoringChangesTo("src")) return;
4696
+ if (!this.sourceURL) {
4697
+ this.#currentFetchRequest?.cancel();
4698
+ }
4765
4699
  if (this.element.isConnected) {
4766
4700
  this.complete = false;
4767
4701
  }
@@ -4839,11 +4773,12 @@ class FrameController {
4839
4773
  }
4840
4774
  this.formSubmission = new FormSubmission(this, element, submitter);
4841
4775
  const {fetchRequest: fetchRequest} = this.formSubmission;
4842
- this.prepareRequest(fetchRequest);
4776
+ const frame = this.#findFrameElement(element, submitter);
4777
+ this.prepareRequest(fetchRequest, frame);
4843
4778
  this.formSubmission.start();
4844
4779
  }
4845
- prepareRequest(request) {
4846
- request.headers["Turbo-Frame"] = this.id;
4780
+ prepareRequest(request, frame = this) {
4781
+ request.headers["Turbo-Frame"] = frame.id;
4847
4782
  if (this.currentNavigationElement?.hasAttribute("data-turbo-stream")) {
4848
4783
  request.acceptResponseType(StreamMessage.contentType);
4849
4784
  }
@@ -5037,7 +4972,8 @@ class FrameController {
5037
4972
  }
5038
4973
  #findFrameElement(element, submitter) {
5039
4974
  const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
5040
- return getFrameElementById(id) ?? this.element;
4975
+ const target = this.#getFrameElementById(id);
4976
+ return target instanceof FrameElement ? target : this.element;
5041
4977
  }
5042
4978
  async extractForeignFrameElement(container) {
5043
4979
  let element;
@@ -5071,9 +5007,11 @@ class FrameController {
5071
5007
  return false;
5072
5008
  }
5073
5009
  if (id) {
5074
- const frameElement = getFrameElementById(id);
5010
+ const frameElement = this.#getFrameElementById(id);
5075
5011
  if (frameElement) {
5076
5012
  return !frameElement.disabled;
5013
+ } else if (id == "_parent") {
5014
+ return false;
5077
5015
  }
5078
5016
  }
5079
5017
  if (!session.elementIsNavigatable(element)) {
@@ -5087,8 +5025,11 @@ class FrameController {
5087
5025
  get id() {
5088
5026
  return this.element.id;
5089
5027
  }
5028
+ get disabled() {
5029
+ return this.element.disabled;
5030
+ }
5090
5031
  get enabled() {
5091
- return !this.element.disabled;
5032
+ return !this.disabled;
5092
5033
  }
5093
5034
  get sourceURL() {
5094
5035
  if (this.element.src) {
@@ -5137,13 +5078,12 @@ class FrameController {
5137
5078
  callback();
5138
5079
  delete this.currentNavigationElement;
5139
5080
  }
5140
- }
5141
-
5142
- function getFrameElementById(id) {
5143
- if (id != null) {
5144
- const element = document.getElementById(id);
5145
- if (element instanceof FrameElement) {
5146
- return element;
5081
+ #getFrameElementById(id) {
5082
+ if (id != null) {
5083
+ const element = id === "_parent" ? this.element.parentElement.closest("turbo-frame") : document.getElementById(id);
5084
+ if (element instanceof FrameElement) {
5085
+ return element;
5086
+ }
5147
5087
  }
5148
5088
  }
5149
5089
  }
@@ -5167,6 +5107,7 @@ function activateElement(element, currentURL) {
5167
5107
 
5168
5108
  const StreamActions = {
5169
5109
  after() {
5110
+ this.removeDuplicateTargetSiblings();
5170
5111
  this.targetElements.forEach((e => e.parentElement?.insertBefore(this.templateContent, e.nextSibling)));
5171
5112
  },
5172
5113
  append() {
@@ -5174,6 +5115,7 @@ const StreamActions = {
5174
5115
  this.targetElements.forEach((e => e.append(this.templateContent)));
5175
5116
  },
5176
5117
  before() {
5118
+ this.removeDuplicateTargetSiblings();
5177
5119
  this.targetElements.forEach((e => e.parentElement?.insertBefore(this.templateContent, e)));
5178
5120
  },
5179
5121
  prepend() {
@@ -5205,7 +5147,14 @@ const StreamActions = {
5205
5147
  }));
5206
5148
  },
5207
5149
  refresh() {
5208
- session.refresh(this.baseURI, this.requestId);
5150
+ const method = this.getAttribute("method");
5151
+ const requestId = this.requestId;
5152
+ const scroll = this.getAttribute("scroll");
5153
+ session.refresh(this.baseURI, {
5154
+ method: method,
5155
+ requestId: requestId,
5156
+ scroll: scroll
5157
+ });
5209
5158
  }
5210
5159
  };
5211
5160
 
@@ -5244,6 +5193,14 @@ class StreamElement extends HTMLElement {
5244
5193
  const newChildrenIds = [ ...this.templateContent?.children || [] ].filter((c => !!c.getAttribute("id"))).map((c => c.getAttribute("id")));
5245
5194
  return existingChildren.filter((c => newChildrenIds.includes(c.getAttribute("id"))));
5246
5195
  }
5196
+ removeDuplicateTargetSiblings() {
5197
+ this.duplicateSiblings.forEach((c => c.remove()));
5198
+ }
5199
+ get duplicateSiblings() {
5200
+ const existingChildren = this.targetElements.flatMap((e => [ ...e.parentElement.children ])).filter((c => !!c.id));
5201
+ const newChildrenIds = [ ...this.templateContent?.children || [] ].filter((c => !!c.id)).map((c => c.id));
5202
+ return existingChildren.filter((c => newChildrenIds.includes(c.id)));
5203
+ }
5247
5204
  get performAction() {
5248
5205
  if (this.action) {
5249
5206
  const actionFunction = StreamActions[this.action];
@@ -5354,10 +5311,10 @@ if (customElements.get("turbo-stream-source") === undefined) {
5354
5311
  }
5355
5312
 
5356
5313
  (() => {
5357
- let element = document.currentScript;
5358
- if (!element) return;
5359
- if (element.hasAttribute("data-turbo-suppress-warning")) return;
5360
- element = element.parentElement;
5314
+ const scriptElement = document.currentScript;
5315
+ if (!scriptElement) return;
5316
+ if (scriptElement.hasAttribute("data-turbo-suppress-warning")) return;
5317
+ let element = scriptElement.parentElement;
5361
5318
  while (element) {
5362
5319
  if (element == document.body) {
5363
5320
  return console.warn(unindent`
@@ -5369,7 +5326,7 @@ if (customElements.get("turbo-stream-source") === undefined) {
5369
5326
 
5370
5327
  ——
5371
5328
  Suppress this warning by adding a "data-turbo-suppress-warning" attribute to: %s
5372
- `, element.outerHTML);
5329
+ `, scriptElement.outerHTML);
5373
5330
  }
5374
5331
  element = element.parentElement;
5375
5332
  }
@@ -5397,7 +5354,6 @@ var Turbo$1 = Object.freeze({
5397
5354
  StreamElement: StreamElement,
5398
5355
  StreamSourceElement: StreamSourceElement,
5399
5356
  cache: cache,
5400
- clearCache: clearCache,
5401
5357
  config: config,
5402
5358
  connectStreamSource: connectStreamSource,
5403
5359
  disconnectStreamSource: disconnectStreamSource,
@@ -5409,7 +5365,7 @@ var Turbo$1 = Object.freeze({
5409
5365
  morphChildren: morphChildren,
5410
5366
  morphElements: morphElements,
5411
5367
  morphTurboFrameElements: morphTurboFrameElements,
5412
- navigator: navigator$1,
5368
+ navigator: navigator,
5413
5369
  registerAdapter: registerAdapter,
5414
5370
  renderStreamMessage: renderStreamMessage,
5415
5371
  session: session,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotwired/turbo-rails",
3
- "version": "8.0.20",
3
+ "version": "8.0.21",
4
4
  "description": "The speed of a single-page web application without having to write any JavaScript",
5
5
  "module": "app/javascript/turbo/index.js",
6
6
  "main": "app/assets/javascripts/turbo.js",
@@ -19,7 +19,7 @@
19
19
  "release": "yarn publish && git commit -am \"$npm_package_name v$npm_package_version\" && git push"
20
20
  },
21
21
  "dependencies": {
22
- "@hotwired/turbo": "^8.0.20",
22
+ "@hotwired/turbo": "^8.0.21",
23
23
  "@rails/actioncable": ">=7.0"
24
24
  },
25
25
  "devDependencies": {