@hotwired/turbo 7.2.0 → 7.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  /*
2
- Turbo 7.2.0
2
+ Turbo 7.2.2
3
3
  Copyright © 2022 37signals LLC
4
4
  */
5
5
  (function () {
@@ -128,11 +128,7 @@ class FrameElement extends HTMLElement {
128
128
  this.delegate.disconnect();
129
129
  }
130
130
  reload() {
131
- const { src } = this;
132
- this.removeAttribute("complete");
133
- this.src = null;
134
- this.src = src;
135
- return this.loaded;
131
+ return this.delegate.sourceURLReloaded();
136
132
  }
137
133
  attributeChangedCallback(name) {
138
134
  if (name == "loading") {
@@ -918,6 +914,7 @@ class FormSubmitObserver {
918
914
  submissionDoesNotTargetIFrame(form, submitter) &&
919
915
  this.delegate.willSubmitForm(form, submitter)) {
920
916
  event.preventDefault();
917
+ event.stopImmediatePropagation();
921
918
  this.delegate.formSubmitted(form, submitter);
922
919
  }
923
920
  }
@@ -1054,6 +1051,48 @@ class FrameView extends View {
1054
1051
  }
1055
1052
  }
1056
1053
 
1054
+ class LinkInterceptor {
1055
+ constructor(delegate, element) {
1056
+ this.clickBubbled = (event) => {
1057
+ if (this.respondsToEventTarget(event.target)) {
1058
+ this.clickEvent = event;
1059
+ }
1060
+ else {
1061
+ delete this.clickEvent;
1062
+ }
1063
+ };
1064
+ this.linkClicked = ((event) => {
1065
+ if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) {
1066
+ if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url, event.detail.originalEvent)) {
1067
+ this.clickEvent.preventDefault();
1068
+ event.preventDefault();
1069
+ this.delegate.linkClickIntercepted(event.target, event.detail.url, event.detail.originalEvent);
1070
+ }
1071
+ }
1072
+ delete this.clickEvent;
1073
+ });
1074
+ this.willVisit = ((_event) => {
1075
+ delete this.clickEvent;
1076
+ });
1077
+ this.delegate = delegate;
1078
+ this.element = element;
1079
+ }
1080
+ start() {
1081
+ this.element.addEventListener("click", this.clickBubbled);
1082
+ document.addEventListener("turbo:click", this.linkClicked);
1083
+ document.addEventListener("turbo:before-visit", this.willVisit);
1084
+ }
1085
+ stop() {
1086
+ this.element.removeEventListener("click", this.clickBubbled);
1087
+ document.removeEventListener("turbo:click", this.linkClicked);
1088
+ document.removeEventListener("turbo:before-visit", this.willVisit);
1089
+ }
1090
+ respondsToEventTarget(target) {
1091
+ const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
1092
+ return element && element.closest("turbo-frame, html") == this.element;
1093
+ }
1094
+ }
1095
+
1057
1096
  class LinkClickObserver {
1058
1097
  constructor(delegate, eventTarget) {
1059
1098
  this.started = false;
@@ -1118,13 +1157,13 @@ function doesNotTargetIFrame(anchor) {
1118
1157
  class FormLinkClickObserver {
1119
1158
  constructor(delegate, element) {
1120
1159
  this.delegate = delegate;
1121
- this.linkClickObserver = new LinkClickObserver(this, element);
1160
+ this.linkInterceptor = new LinkClickObserver(this, element);
1122
1161
  }
1123
1162
  start() {
1124
- this.linkClickObserver.start();
1163
+ this.linkInterceptor.start();
1125
1164
  }
1126
1165
  stop() {
1127
- this.linkClickObserver.stop();
1166
+ this.linkInterceptor.stop();
1128
1167
  }
1129
1168
  willFollowLinkToLocation(link, location, originalEvent) {
1130
1169
  return (this.delegate.willSubmitFormLinkToLocation(link, location, originalEvent) &&
@@ -1646,10 +1685,11 @@ class Visit {
1646
1685
  this.delegate = delegate;
1647
1686
  this.location = location;
1648
1687
  this.restorationIdentifier = restorationIdentifier || uuid();
1649
- const { action, historyChanged, referrer, snapshotHTML, response, visitCachedSnapshot, willRender, updateHistory, shouldCacheSnapshot, acceptsStreamResponse, } = Object.assign(Object.assign({}, defaultOptions), options);
1688
+ const { action, historyChanged, referrer, snapshot, snapshotHTML, response, visitCachedSnapshot, willRender, updateHistory, shouldCacheSnapshot, acceptsStreamResponse, } = Object.assign(Object.assign({}, defaultOptions), options);
1650
1689
  this.action = action;
1651
1690
  this.historyChanged = historyChanged;
1652
1691
  this.referrer = referrer;
1692
+ this.snapshot = snapshot;
1653
1693
  this.snapshotHTML = snapshotHTML;
1654
1694
  this.response = response;
1655
1695
  this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
@@ -1937,7 +1977,7 @@ class Visit {
1937
1977
  }
1938
1978
  cacheSnapshot() {
1939
1979
  if (!this.snapshotCached) {
1940
- this.view.cacheSnapshot().then((snapshot) => snapshot && this.visitCachedSnapshot(snapshot));
1980
+ this.view.cacheSnapshot(this.snapshot).then((snapshot) => snapshot && this.visitCachedSnapshot(snapshot));
1941
1981
  this.snapshotCached = true;
1942
1982
  }
1943
1983
  }
@@ -2045,10 +2085,9 @@ class BrowserAdapter {
2045
2085
  }
2046
2086
  }
2047
2087
  reload(reason) {
2088
+ var _a;
2048
2089
  dispatch("turbo:reload", { detail: reason });
2049
- if (!this.location)
2050
- return;
2051
- window.location.href = this.location.toString();
2090
+ window.location.href = ((_a = this.location) === null || _a === void 0 ? void 0 : _a.toString()) || window.location.href;
2052
2091
  }
2053
2092
  get navigator() {
2054
2093
  return this.session.navigator;
@@ -2083,24 +2122,24 @@ class FrameRedirector {
2083
2122
  constructor(session, element) {
2084
2123
  this.session = session;
2085
2124
  this.element = element;
2086
- this.linkClickObserver = new LinkClickObserver(this, element);
2125
+ this.linkInterceptor = new LinkInterceptor(this, element);
2087
2126
  this.formSubmitObserver = new FormSubmitObserver(this, element);
2088
2127
  }
2089
2128
  start() {
2090
- this.linkClickObserver.start();
2129
+ this.linkInterceptor.start();
2091
2130
  this.formSubmitObserver.start();
2092
2131
  }
2093
2132
  stop() {
2094
- this.linkClickObserver.stop();
2133
+ this.linkInterceptor.stop();
2095
2134
  this.formSubmitObserver.stop();
2096
2135
  }
2097
- willFollowLinkToLocation(element, location, event) {
2098
- return this.shouldRedirect(element) && this.frameAllowsVisitingLocation(element, location, event);
2136
+ shouldInterceptLinkClick(element, _location, _event) {
2137
+ return this.shouldRedirect(element);
2099
2138
  }
2100
- followedLinkToLocation(element, url) {
2139
+ linkClickIntercepted(element, url, event) {
2101
2140
  const frame = this.findFrameElement(element);
2102
2141
  if (frame) {
2103
- frame.delegate.followedLinkToLocation(element, url);
2142
+ frame.delegate.linkClickIntercepted(element, url, event);
2104
2143
  }
2105
2144
  }
2106
2145
  willSubmitForm(element, submitter) {
@@ -2114,14 +2153,6 @@ class FrameRedirector {
2114
2153
  frame.delegate.formSubmitted(element, submitter);
2115
2154
  }
2116
2155
  }
2117
- frameAllowsVisitingLocation(target, { href: url }, originalEvent) {
2118
- const event = dispatch("turbo:click", {
2119
- target,
2120
- detail: { url, originalEvent },
2121
- cancelable: true,
2122
- });
2123
- return !event.defaultPrevented;
2124
- }
2125
2156
  shouldSubmit(form, submitter) {
2126
2157
  var _a;
2127
2158
  const action = getAction(form, submitter);
@@ -2748,10 +2779,10 @@ class PageView extends View {
2748
2779
  clearSnapshotCache() {
2749
2780
  this.snapshotCache.clear();
2750
2781
  }
2751
- async cacheSnapshot() {
2752
- if (this.shouldCacheSnapshot) {
2782
+ async cacheSnapshot(snapshot = this.snapshot) {
2783
+ if (snapshot.isCacheable) {
2753
2784
  this.delegate.viewWillCacheSnapshot();
2754
- const { snapshot, lastRenderedLocation: location } = this;
2785
+ const { lastRenderedLocation: location } = this;
2755
2786
  await nextEventLoopTick();
2756
2787
  const cachedSnapshot = snapshot.clone();
2757
2788
  this.snapshotCache.put(location, cachedSnapshot);
@@ -2764,9 +2795,6 @@ class PageView extends View {
2764
2795
  get snapshot() {
2765
2796
  return PageSnapshot.fromElement(this.element);
2766
2797
  }
2767
- get shouldCacheSnapshot() {
2768
- return this.snapshot.isCacheable;
2769
- }
2770
2798
  }
2771
2799
 
2772
2800
  class Preloader {
@@ -3240,7 +3268,7 @@ class FrameController {
3240
3268
  this.view = new FrameView(this, this.element);
3241
3269
  this.appearanceObserver = new AppearanceObserver(this, this.element);
3242
3270
  this.formLinkClickObserver = new FormLinkClickObserver(this, this.element);
3243
- this.linkClickObserver = new LinkClickObserver(this, this.element);
3271
+ this.linkInterceptor = new LinkInterceptor(this, this.element);
3244
3272
  this.restorationIdentifier = uuid();
3245
3273
  this.formSubmitObserver = new FormSubmitObserver(this, this.element);
3246
3274
  }
@@ -3254,7 +3282,7 @@ class FrameController {
3254
3282
  this.loadSourceURL();
3255
3283
  }
3256
3284
  this.formLinkClickObserver.start();
3257
- this.linkClickObserver.start();
3285
+ this.linkInterceptor.start();
3258
3286
  this.formSubmitObserver.start();
3259
3287
  }
3260
3288
  }
@@ -3263,7 +3291,7 @@ class FrameController {
3263
3291
  this.connected = false;
3264
3292
  this.appearanceObserver.stop();
3265
3293
  this.formLinkClickObserver.stop();
3266
- this.linkClickObserver.stop();
3294
+ this.linkInterceptor.stop();
3267
3295
  this.formSubmitObserver.stop();
3268
3296
  }
3269
3297
  }
@@ -3282,6 +3310,15 @@ class FrameController {
3282
3310
  this.loadSourceURL();
3283
3311
  }
3284
3312
  }
3313
+ sourceURLReloaded() {
3314
+ const { src } = this.element;
3315
+ this.ignoringChangesToAttribute("complete", () => {
3316
+ this.element.removeAttribute("complete");
3317
+ });
3318
+ this.element.src = null;
3319
+ this.element.src = src;
3320
+ return this.element.loaded;
3321
+ }
3285
3322
  completeChanged() {
3286
3323
  if (this.isIgnoringChangesTo("complete"))
3287
3324
  return;
@@ -3343,18 +3380,18 @@ class FrameController {
3343
3380
  this.loadSourceURL();
3344
3381
  }
3345
3382
  willSubmitFormLinkToLocation(link) {
3346
- return link.closest("turbo-frame") == this.element && this.shouldInterceptNavigation(link);
3383
+ return this.shouldInterceptNavigation(link);
3347
3384
  }
3348
3385
  submittedFormLinkToLocation(link, _location, form) {
3349
3386
  const frame = this.findFrameElement(link);
3350
3387
  if (frame)
3351
3388
  form.setAttribute("data-turbo-frame", frame.id);
3352
3389
  }
3353
- willFollowLinkToLocation(element, location, event) {
3354
- return this.shouldInterceptNavigation(element) && this.frameAllowsVisitingLocation(element, location, event);
3390
+ shouldInterceptLinkClick(element, _location, _event) {
3391
+ return this.shouldInterceptNavigation(element);
3355
3392
  }
3356
- followedLinkToLocation(element, location) {
3357
- this.navigateFrame(element, location.href);
3393
+ linkClickIntercepted(element, location) {
3394
+ this.navigateFrame(element, location);
3358
3395
  }
3359
3396
  willSubmitForm(element, submitter) {
3360
3397
  return element.closest("turbo-frame") == this.element && this.shouldInterceptNavigation(element, submitter);
@@ -3402,7 +3439,7 @@ class FrameController {
3402
3439
  }
3403
3440
  formSubmissionSucceededWithResponse(formSubmission, response) {
3404
3441
  const frame = this.findFrameElement(formSubmission.formElement, formSubmission.submitter);
3405
- this.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
3442
+ frame.delegate.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
3406
3443
  frame.delegate.loadResponse(response);
3407
3444
  }
3408
3445
  formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
@@ -3450,14 +3487,14 @@ class FrameController {
3450
3487
  }
3451
3488
  navigateFrame(element, url, submitter) {
3452
3489
  const frame = this.findFrameElement(element, submitter);
3453
- this.proposeVisitIfNavigatedWithAction(frame, element, submitter);
3490
+ this.pageSnapshot = PageSnapshot.fromElement(frame).clone();
3491
+ frame.delegate.proposeVisitIfNavigatedWithAction(frame, element, submitter);
3454
3492
  this.withCurrentNavigationElement(element, () => {
3455
3493
  frame.src = url;
3456
3494
  });
3457
3495
  }
3458
3496
  proposeVisitIfNavigatedWithAction(frame, element, submitter) {
3459
3497
  this.action = getVisitAction(submitter, element, frame);
3460
- this.frame = frame;
3461
3498
  if (isAction(this.action)) {
3462
3499
  const { visitCachedSnapshot } = frame.delegate;
3463
3500
  frame.delegate.fetchResponseLoaded = (fetchResponse) => {
@@ -3471,6 +3508,7 @@ class FrameController {
3471
3508
  willRender: false,
3472
3509
  updateHistory: false,
3473
3510
  restorationIdentifier: this.restorationIdentifier,
3511
+ snapshot: this.pageSnapshot,
3474
3512
  };
3475
3513
  if (this.action)
3476
3514
  options.action = this.action;
@@ -3480,9 +3518,9 @@ class FrameController {
3480
3518
  }
3481
3519
  }
3482
3520
  changeHistory() {
3483
- if (this.action && this.frame) {
3521
+ if (this.action) {
3484
3522
  const method = getHistoryMethodForAction(this.action);
3485
- session.history.update(method, expandURL(this.frame.src || ""), this.restorationIdentifier);
3523
+ session.history.update(method, expandURL(this.element.src || ""), this.restorationIdentifier);
3486
3524
  }
3487
3525
  }
3488
3526
  willHandleFrameMissingFromResponse(fetchResponse) {
@@ -3604,14 +3642,6 @@ class FrameController {
3604
3642
  const root = (_a = meta === null || meta === void 0 ? void 0 : meta.content) !== null && _a !== void 0 ? _a : "/";
3605
3643
  return expandURL(root);
3606
3644
  }
3607
- frameAllowsVisitingLocation(target, { href: url }, originalEvent) {
3608
- const event = dispatch("turbo:click", {
3609
- target,
3610
- detail: { url, originalEvent },
3611
- cancelable: true,
3612
- });
3613
- return !event.defaultPrevented;
3614
- }
3615
3645
  isIgnoringChangesTo(attributeName) {
3616
3646
  return this.ignoredAttributes.has(attributeName);
3617
3647
  }
@@ -1,5 +1,5 @@
1
1
  /*
2
- Turbo 7.2.0
2
+ Turbo 7.2.2
3
3
  Copyright © 2022 37signals LLC
4
4
  */
5
5
  (function (global, factory) {
@@ -134,11 +134,7 @@ Copyright © 2022 37signals LLC
134
134
  this.delegate.disconnect();
135
135
  }
136
136
  reload() {
137
- const { src } = this;
138
- this.removeAttribute("complete");
139
- this.src = null;
140
- this.src = src;
141
- return this.loaded;
137
+ return this.delegate.sourceURLReloaded();
142
138
  }
143
139
  attributeChangedCallback(name) {
144
140
  if (name == "loading") {
@@ -924,6 +920,7 @@ Copyright © 2022 37signals LLC
924
920
  submissionDoesNotTargetIFrame(form, submitter) &&
925
921
  this.delegate.willSubmitForm(form, submitter)) {
926
922
  event.preventDefault();
923
+ event.stopImmediatePropagation();
927
924
  this.delegate.formSubmitted(form, submitter);
928
925
  }
929
926
  }
@@ -1060,6 +1057,48 @@ Copyright © 2022 37signals LLC
1060
1057
  }
1061
1058
  }
1062
1059
 
1060
+ class LinkInterceptor {
1061
+ constructor(delegate, element) {
1062
+ this.clickBubbled = (event) => {
1063
+ if (this.respondsToEventTarget(event.target)) {
1064
+ this.clickEvent = event;
1065
+ }
1066
+ else {
1067
+ delete this.clickEvent;
1068
+ }
1069
+ };
1070
+ this.linkClicked = ((event) => {
1071
+ if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) {
1072
+ if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url, event.detail.originalEvent)) {
1073
+ this.clickEvent.preventDefault();
1074
+ event.preventDefault();
1075
+ this.delegate.linkClickIntercepted(event.target, event.detail.url, event.detail.originalEvent);
1076
+ }
1077
+ }
1078
+ delete this.clickEvent;
1079
+ });
1080
+ this.willVisit = ((_event) => {
1081
+ delete this.clickEvent;
1082
+ });
1083
+ this.delegate = delegate;
1084
+ this.element = element;
1085
+ }
1086
+ start() {
1087
+ this.element.addEventListener("click", this.clickBubbled);
1088
+ document.addEventListener("turbo:click", this.linkClicked);
1089
+ document.addEventListener("turbo:before-visit", this.willVisit);
1090
+ }
1091
+ stop() {
1092
+ this.element.removeEventListener("click", this.clickBubbled);
1093
+ document.removeEventListener("turbo:click", this.linkClicked);
1094
+ document.removeEventListener("turbo:before-visit", this.willVisit);
1095
+ }
1096
+ respondsToEventTarget(target) {
1097
+ const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
1098
+ return element && element.closest("turbo-frame, html") == this.element;
1099
+ }
1100
+ }
1101
+
1063
1102
  class LinkClickObserver {
1064
1103
  constructor(delegate, eventTarget) {
1065
1104
  this.started = false;
@@ -1124,13 +1163,13 @@ Copyright © 2022 37signals LLC
1124
1163
  class FormLinkClickObserver {
1125
1164
  constructor(delegate, element) {
1126
1165
  this.delegate = delegate;
1127
- this.linkClickObserver = new LinkClickObserver(this, element);
1166
+ this.linkInterceptor = new LinkClickObserver(this, element);
1128
1167
  }
1129
1168
  start() {
1130
- this.linkClickObserver.start();
1169
+ this.linkInterceptor.start();
1131
1170
  }
1132
1171
  stop() {
1133
- this.linkClickObserver.stop();
1172
+ this.linkInterceptor.stop();
1134
1173
  }
1135
1174
  willFollowLinkToLocation(link, location, originalEvent) {
1136
1175
  return (this.delegate.willSubmitFormLinkToLocation(link, location, originalEvent) &&
@@ -1652,10 +1691,11 @@ Copyright © 2022 37signals LLC
1652
1691
  this.delegate = delegate;
1653
1692
  this.location = location;
1654
1693
  this.restorationIdentifier = restorationIdentifier || uuid();
1655
- const { action, historyChanged, referrer, snapshotHTML, response, visitCachedSnapshot, willRender, updateHistory, shouldCacheSnapshot, acceptsStreamResponse, } = Object.assign(Object.assign({}, defaultOptions), options);
1694
+ const { action, historyChanged, referrer, snapshot, snapshotHTML, response, visitCachedSnapshot, willRender, updateHistory, shouldCacheSnapshot, acceptsStreamResponse, } = Object.assign(Object.assign({}, defaultOptions), options);
1656
1695
  this.action = action;
1657
1696
  this.historyChanged = historyChanged;
1658
1697
  this.referrer = referrer;
1698
+ this.snapshot = snapshot;
1659
1699
  this.snapshotHTML = snapshotHTML;
1660
1700
  this.response = response;
1661
1701
  this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
@@ -1943,7 +1983,7 @@ Copyright © 2022 37signals LLC
1943
1983
  }
1944
1984
  cacheSnapshot() {
1945
1985
  if (!this.snapshotCached) {
1946
- this.view.cacheSnapshot().then((snapshot) => snapshot && this.visitCachedSnapshot(snapshot));
1986
+ this.view.cacheSnapshot(this.snapshot).then((snapshot) => snapshot && this.visitCachedSnapshot(snapshot));
1947
1987
  this.snapshotCached = true;
1948
1988
  }
1949
1989
  }
@@ -2051,10 +2091,9 @@ Copyright © 2022 37signals LLC
2051
2091
  }
2052
2092
  }
2053
2093
  reload(reason) {
2094
+ var _a;
2054
2095
  dispatch("turbo:reload", { detail: reason });
2055
- if (!this.location)
2056
- return;
2057
- window.location.href = this.location.toString();
2096
+ window.location.href = ((_a = this.location) === null || _a === void 0 ? void 0 : _a.toString()) || window.location.href;
2058
2097
  }
2059
2098
  get navigator() {
2060
2099
  return this.session.navigator;
@@ -2089,24 +2128,24 @@ Copyright © 2022 37signals LLC
2089
2128
  constructor(session, element) {
2090
2129
  this.session = session;
2091
2130
  this.element = element;
2092
- this.linkClickObserver = new LinkClickObserver(this, element);
2131
+ this.linkInterceptor = new LinkInterceptor(this, element);
2093
2132
  this.formSubmitObserver = new FormSubmitObserver(this, element);
2094
2133
  }
2095
2134
  start() {
2096
- this.linkClickObserver.start();
2135
+ this.linkInterceptor.start();
2097
2136
  this.formSubmitObserver.start();
2098
2137
  }
2099
2138
  stop() {
2100
- this.linkClickObserver.stop();
2139
+ this.linkInterceptor.stop();
2101
2140
  this.formSubmitObserver.stop();
2102
2141
  }
2103
- willFollowLinkToLocation(element, location, event) {
2104
- return this.shouldRedirect(element) && this.frameAllowsVisitingLocation(element, location, event);
2142
+ shouldInterceptLinkClick(element, _location, _event) {
2143
+ return this.shouldRedirect(element);
2105
2144
  }
2106
- followedLinkToLocation(element, url) {
2145
+ linkClickIntercepted(element, url, event) {
2107
2146
  const frame = this.findFrameElement(element);
2108
2147
  if (frame) {
2109
- frame.delegate.followedLinkToLocation(element, url);
2148
+ frame.delegate.linkClickIntercepted(element, url, event);
2110
2149
  }
2111
2150
  }
2112
2151
  willSubmitForm(element, submitter) {
@@ -2120,14 +2159,6 @@ Copyright © 2022 37signals LLC
2120
2159
  frame.delegate.formSubmitted(element, submitter);
2121
2160
  }
2122
2161
  }
2123
- frameAllowsVisitingLocation(target, { href: url }, originalEvent) {
2124
- const event = dispatch("turbo:click", {
2125
- target,
2126
- detail: { url, originalEvent },
2127
- cancelable: true,
2128
- });
2129
- return !event.defaultPrevented;
2130
- }
2131
2162
  shouldSubmit(form, submitter) {
2132
2163
  var _a;
2133
2164
  const action = getAction(form, submitter);
@@ -2754,10 +2785,10 @@ Copyright © 2022 37signals LLC
2754
2785
  clearSnapshotCache() {
2755
2786
  this.snapshotCache.clear();
2756
2787
  }
2757
- async cacheSnapshot() {
2758
- if (this.shouldCacheSnapshot) {
2788
+ async cacheSnapshot(snapshot = this.snapshot) {
2789
+ if (snapshot.isCacheable) {
2759
2790
  this.delegate.viewWillCacheSnapshot();
2760
- const { snapshot, lastRenderedLocation: location } = this;
2791
+ const { lastRenderedLocation: location } = this;
2761
2792
  await nextEventLoopTick();
2762
2793
  const cachedSnapshot = snapshot.clone();
2763
2794
  this.snapshotCache.put(location, cachedSnapshot);
@@ -2770,9 +2801,6 @@ Copyright © 2022 37signals LLC
2770
2801
  get snapshot() {
2771
2802
  return PageSnapshot.fromElement(this.element);
2772
2803
  }
2773
- get shouldCacheSnapshot() {
2774
- return this.snapshot.isCacheable;
2775
- }
2776
2804
  }
2777
2805
 
2778
2806
  class Preloader {
@@ -3246,7 +3274,7 @@ Copyright © 2022 37signals LLC
3246
3274
  this.view = new FrameView(this, this.element);
3247
3275
  this.appearanceObserver = new AppearanceObserver(this, this.element);
3248
3276
  this.formLinkClickObserver = new FormLinkClickObserver(this, this.element);
3249
- this.linkClickObserver = new LinkClickObserver(this, this.element);
3277
+ this.linkInterceptor = new LinkInterceptor(this, this.element);
3250
3278
  this.restorationIdentifier = uuid();
3251
3279
  this.formSubmitObserver = new FormSubmitObserver(this, this.element);
3252
3280
  }
@@ -3260,7 +3288,7 @@ Copyright © 2022 37signals LLC
3260
3288
  this.loadSourceURL();
3261
3289
  }
3262
3290
  this.formLinkClickObserver.start();
3263
- this.linkClickObserver.start();
3291
+ this.linkInterceptor.start();
3264
3292
  this.formSubmitObserver.start();
3265
3293
  }
3266
3294
  }
@@ -3269,7 +3297,7 @@ Copyright © 2022 37signals LLC
3269
3297
  this.connected = false;
3270
3298
  this.appearanceObserver.stop();
3271
3299
  this.formLinkClickObserver.stop();
3272
- this.linkClickObserver.stop();
3300
+ this.linkInterceptor.stop();
3273
3301
  this.formSubmitObserver.stop();
3274
3302
  }
3275
3303
  }
@@ -3288,6 +3316,15 @@ Copyright © 2022 37signals LLC
3288
3316
  this.loadSourceURL();
3289
3317
  }
3290
3318
  }
3319
+ sourceURLReloaded() {
3320
+ const { src } = this.element;
3321
+ this.ignoringChangesToAttribute("complete", () => {
3322
+ this.element.removeAttribute("complete");
3323
+ });
3324
+ this.element.src = null;
3325
+ this.element.src = src;
3326
+ return this.element.loaded;
3327
+ }
3291
3328
  completeChanged() {
3292
3329
  if (this.isIgnoringChangesTo("complete"))
3293
3330
  return;
@@ -3349,18 +3386,18 @@ Copyright © 2022 37signals LLC
3349
3386
  this.loadSourceURL();
3350
3387
  }
3351
3388
  willSubmitFormLinkToLocation(link) {
3352
- return link.closest("turbo-frame") == this.element && this.shouldInterceptNavigation(link);
3389
+ return this.shouldInterceptNavigation(link);
3353
3390
  }
3354
3391
  submittedFormLinkToLocation(link, _location, form) {
3355
3392
  const frame = this.findFrameElement(link);
3356
3393
  if (frame)
3357
3394
  form.setAttribute("data-turbo-frame", frame.id);
3358
3395
  }
3359
- willFollowLinkToLocation(element, location, event) {
3360
- return this.shouldInterceptNavigation(element) && this.frameAllowsVisitingLocation(element, location, event);
3396
+ shouldInterceptLinkClick(element, _location, _event) {
3397
+ return this.shouldInterceptNavigation(element);
3361
3398
  }
3362
- followedLinkToLocation(element, location) {
3363
- this.navigateFrame(element, location.href);
3399
+ linkClickIntercepted(element, location) {
3400
+ this.navigateFrame(element, location);
3364
3401
  }
3365
3402
  willSubmitForm(element, submitter) {
3366
3403
  return element.closest("turbo-frame") == this.element && this.shouldInterceptNavigation(element, submitter);
@@ -3408,7 +3445,7 @@ Copyright © 2022 37signals LLC
3408
3445
  }
3409
3446
  formSubmissionSucceededWithResponse(formSubmission, response) {
3410
3447
  const frame = this.findFrameElement(formSubmission.formElement, formSubmission.submitter);
3411
- this.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
3448
+ frame.delegate.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
3412
3449
  frame.delegate.loadResponse(response);
3413
3450
  }
3414
3451
  formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
@@ -3456,14 +3493,14 @@ Copyright © 2022 37signals LLC
3456
3493
  }
3457
3494
  navigateFrame(element, url, submitter) {
3458
3495
  const frame = this.findFrameElement(element, submitter);
3459
- this.proposeVisitIfNavigatedWithAction(frame, element, submitter);
3496
+ this.pageSnapshot = PageSnapshot.fromElement(frame).clone();
3497
+ frame.delegate.proposeVisitIfNavigatedWithAction(frame, element, submitter);
3460
3498
  this.withCurrentNavigationElement(element, () => {
3461
3499
  frame.src = url;
3462
3500
  });
3463
3501
  }
3464
3502
  proposeVisitIfNavigatedWithAction(frame, element, submitter) {
3465
3503
  this.action = getVisitAction(submitter, element, frame);
3466
- this.frame = frame;
3467
3504
  if (isAction(this.action)) {
3468
3505
  const { visitCachedSnapshot } = frame.delegate;
3469
3506
  frame.delegate.fetchResponseLoaded = (fetchResponse) => {
@@ -3477,6 +3514,7 @@ Copyright © 2022 37signals LLC
3477
3514
  willRender: false,
3478
3515
  updateHistory: false,
3479
3516
  restorationIdentifier: this.restorationIdentifier,
3517
+ snapshot: this.pageSnapshot,
3480
3518
  };
3481
3519
  if (this.action)
3482
3520
  options.action = this.action;
@@ -3486,9 +3524,9 @@ Copyright © 2022 37signals LLC
3486
3524
  }
3487
3525
  }
3488
3526
  changeHistory() {
3489
- if (this.action && this.frame) {
3527
+ if (this.action) {
3490
3528
  const method = getHistoryMethodForAction(this.action);
3491
- session.history.update(method, expandURL(this.frame.src || ""), this.restorationIdentifier);
3529
+ session.history.update(method, expandURL(this.element.src || ""), this.restorationIdentifier);
3492
3530
  }
3493
3531
  }
3494
3532
  willHandleFrameMissingFromResponse(fetchResponse) {
@@ -3610,14 +3648,6 @@ Copyright © 2022 37signals LLC
3610
3648
  const root = (_a = meta === null || meta === void 0 ? void 0 : meta.content) !== null && _a !== void 0 ? _a : "/";
3611
3649
  return expandURL(root);
3612
3650
  }
3613
- frameAllowsVisitingLocation(target, { href: url }, originalEvent) {
3614
- const event = dispatch("turbo:click", {
3615
- target,
3616
- detail: { url, originalEvent },
3617
- cancelable: true,
3618
- });
3619
- return !event.defaultPrevented;
3620
- }
3621
3651
  isIgnoringChangesTo(attributeName) {
3622
3652
  return this.ignoredAttributes.has(attributeName);
3623
3653
  }
@@ -16,9 +16,8 @@ export declare class PageView extends View<HTMLBodyElement, PageSnapshot, PageVi
16
16
  renderPage(snapshot: PageSnapshot, isPreview?: boolean, willRender?: boolean, visit?: Visit): Promise<void>;
17
17
  renderError(snapshot: PageSnapshot, visit?: Visit): Promise<void>;
18
18
  clearSnapshotCache(): void;
19
- cacheSnapshot(): Promise<PageSnapshot | undefined>;
19
+ cacheSnapshot(snapshot?: PageSnapshot): Promise<PageSnapshot | undefined>;
20
20
  getCachedSnapshotForLocation(location: URL): PageSnapshot | undefined;
21
21
  get snapshot(): PageSnapshot;
22
- get shouldCacheSnapshot(): boolean;
23
22
  }
24
23
  export {};
@@ -35,6 +35,7 @@ export declare type VisitOptions = {
35
35
  action: Action;
36
36
  historyChanged: boolean;
37
37
  referrer?: URL;
38
+ snapshot?: PageSnapshot;
38
39
  snapshotHTML?: string;
39
40
  response?: VisitResponse;
40
41
  visitCachedSnapshot(snapshot: Snapshot): void;
@@ -79,6 +80,7 @@ export declare class Visit implements FetchRequestDelegate {
79
80
  snapshotHTML?: string;
80
81
  snapshotCached: boolean;
81
82
  state: VisitState;
83
+ snapshot?: PageSnapshot;
82
84
  constructor(delegate: VisitDelegate, location: URL, restorationIdentifier: string | undefined, options?: Partial<VisitOptions>);
83
85
  get adapter(): Adapter;
84
86
  get view(): PageView;
@@ -8,20 +8,21 @@ import { ViewDelegate, ViewRenderOptions } from "../view";
8
8
  import { Locatable } from "../url";
9
9
  import { FormSubmitObserver, FormSubmitObserverDelegate } from "../../observers/form_submit_observer";
10
10
  import { FrameView } from "./frame_view";
11
- import { LinkClickObserver, LinkClickObserverDelegate } from "../../observers/link_click_observer";
11
+ import { LinkInterceptor, LinkInterceptorDelegate } from "./link_interceptor";
12
12
  import { FormLinkClickObserver, FormLinkClickObserverDelegate } from "../../observers/form_link_click_observer";
13
13
  import { VisitOptions } from "../drive/visit";
14
+ import { PageSnapshot } from "../drive/page_snapshot";
14
15
  declare type VisitFallback = (location: Response | Locatable, options: Partial<VisitOptions>) => Promise<void>;
15
16
  export declare type TurboFrameMissingEvent = CustomEvent<{
16
17
  response: Response;
17
18
  visit: VisitFallback;
18
19
  }>;
19
- export declare class FrameController implements AppearanceObserverDelegate, FetchRequestDelegate, FormSubmitObserverDelegate, FormSubmissionDelegate, FrameElementDelegate, FormLinkClickObserverDelegate, LinkClickObserverDelegate, ViewDelegate<FrameElement, Snapshot<FrameElement>> {
20
+ export declare class FrameController implements AppearanceObserverDelegate, FetchRequestDelegate, FormSubmitObserverDelegate, FormSubmissionDelegate, FrameElementDelegate, FormLinkClickObserverDelegate, LinkInterceptorDelegate, ViewDelegate<FrameElement, Snapshot<FrameElement>> {
20
21
  readonly element: FrameElement;
21
22
  readonly view: FrameView;
22
23
  readonly appearanceObserver: AppearanceObserver;
23
24
  readonly formLinkClickObserver: FormLinkClickObserver;
24
- readonly linkClickObserver: LinkClickObserver;
25
+ readonly linkInterceptor: LinkInterceptor;
25
26
  readonly formSubmitObserver: FormSubmitObserver;
26
27
  formSubmission?: FormSubmission;
27
28
  fetchResponseLoaded: (_fetchResponse: FetchResponse) => void;
@@ -31,15 +32,16 @@ export declare class FrameController implements AppearanceObserverDelegate, Fetc
31
32
  private hasBeenLoaded;
32
33
  private ignoredAttributes;
33
34
  private action;
34
- private frame?;
35
35
  readonly restorationIdentifier: string;
36
36
  private previousFrameElement?;
37
37
  private currentNavigationElement?;
38
+ pageSnapshot?: PageSnapshot;
38
39
  constructor(element: FrameElement);
39
40
  connect(): void;
40
41
  disconnect(): void;
41
42
  disabledChanged(): void;
42
43
  sourceURLChanged(): void;
44
+ sourceURLReloaded(): Promise<void>;
43
45
  completeChanged(): void;
44
46
  loadingStyleChanged(): void;
45
47
  private loadSourceURL;
@@ -47,8 +49,8 @@ export declare class FrameController implements AppearanceObserverDelegate, Fetc
47
49
  elementAppearedInViewport(_element: Element): void;
48
50
  willSubmitFormLinkToLocation(link: Element): boolean;
49
51
  submittedFormLinkToLocation(link: Element, _location: URL, form: HTMLFormElement): void;
50
- willFollowLinkToLocation(element: Element, location: URL, event: MouseEvent): boolean;
51
- followedLinkToLocation(element: Element, location: URL): void;
52
+ shouldInterceptLinkClick(element: Element, _location: string, _event: MouseEvent): boolean;
53
+ linkClickIntercepted(element: Element, location: string): void;
52
54
  willSubmitForm(element: HTMLFormElement, submitter?: HTMLElement): boolean;
53
55
  formSubmitted(element: HTMLFormElement, submitter?: HTMLElement): void;
54
56
  prepareHeadersForRequest(headers: FetchRequestHeaders, request: FetchRequest): void;
@@ -71,7 +73,7 @@ export declare class FrameController implements AppearanceObserverDelegate, Fetc
71
73
  visitCachedSnapshot: ({ element }: Snapshot) => void;
72
74
  private visit;
73
75
  private navigateFrame;
74
- private proposeVisitIfNavigatedWithAction;
76
+ proposeVisitIfNavigatedWithAction(frame: FrameElement, element: Element, submitter?: HTMLElement): void;
75
77
  changeHistory(): void;
76
78
  private willHandleFrameMissingFromResponse;
77
79
  private visitResponse;
@@ -89,7 +91,6 @@ export declare class FrameController implements AppearanceObserverDelegate, Fetc
89
91
  set complete(value: boolean);
90
92
  get isActive(): boolean;
91
93
  get rootLocation(): URL;
92
- private frameAllowsVisitingLocation;
93
94
  private isIgnoringChangesTo;
94
95
  private ignoringChangesToAttribute;
95
96
  private withCurrentNavigationElement;
@@ -1,19 +1,18 @@
1
1
  import { FormSubmitObserver, FormSubmitObserverDelegate } from "../../observers/form_submit_observer";
2
- import { LinkClickObserver, LinkClickObserverDelegate } from "../../observers/link_click_observer";
2
+ import { LinkInterceptor, LinkInterceptorDelegate } from "./link_interceptor";
3
3
  import { Session } from "../session";
4
- export declare class FrameRedirector implements LinkClickObserverDelegate, FormSubmitObserverDelegate {
4
+ export declare class FrameRedirector implements LinkInterceptorDelegate, FormSubmitObserverDelegate {
5
5
  readonly session: Session;
6
6
  readonly element: Element;
7
- readonly linkClickObserver: LinkClickObserver;
7
+ readonly linkInterceptor: LinkInterceptor;
8
8
  readonly formSubmitObserver: FormSubmitObserver;
9
9
  constructor(session: Session, element: Element);
10
10
  start(): void;
11
11
  stop(): void;
12
- willFollowLinkToLocation(element: Element, location: URL, event: MouseEvent): boolean;
13
- followedLinkToLocation(element: Element, url: URL): void;
12
+ shouldInterceptLinkClick(element: Element, _location: string, _event: MouseEvent): boolean;
13
+ linkClickIntercepted(element: Element, url: string, event: MouseEvent): void;
14
14
  willSubmitForm(element: HTMLFormElement, submitter?: HTMLElement): boolean;
15
15
  formSubmitted(element: HTMLFormElement, submitter?: HTMLElement): void;
16
- private frameAllowsVisitingLocation;
17
16
  private shouldSubmit;
18
17
  private shouldRedirect;
19
18
  private findFrameElement;
@@ -0,0 +1,16 @@
1
+ export interface LinkInterceptorDelegate {
2
+ shouldInterceptLinkClick(element: Element, url: string, originalEvent: MouseEvent): boolean;
3
+ linkClickIntercepted(element: Element, url: string, originalEvent: MouseEvent): void;
4
+ }
5
+ export declare class LinkInterceptor {
6
+ readonly delegate: LinkInterceptorDelegate;
7
+ readonly element: Element;
8
+ private clickEvent?;
9
+ constructor(delegate: LinkInterceptorDelegate, element: Element);
10
+ start(): void;
11
+ stop(): void;
12
+ clickBubbled: (event: Event) => void;
13
+ linkClicked: EventListener;
14
+ willVisit: EventListener;
15
+ respondsToEventTarget(target: EventTarget | null): boolean | null;
16
+ }
@@ -1,20 +1,22 @@
1
1
  import { FetchResponse } from "../http/fetch_response";
2
2
  import { Snapshot } from "../core/snapshot";
3
- import { LinkClickObserverDelegate } from "../observers/link_click_observer";
3
+ import { LinkInterceptorDelegate } from "../core/frames/link_interceptor";
4
4
  import { FormSubmitObserverDelegate } from "../observers/form_submit_observer";
5
5
  export declare enum FrameLoadingStyle {
6
6
  eager = "eager",
7
7
  lazy = "lazy"
8
8
  }
9
9
  export declare type FrameElementObservedAttribute = keyof FrameElement & ("disabled" | "complete" | "loading" | "src");
10
- export interface FrameElementDelegate extends LinkClickObserverDelegate, FormSubmitObserverDelegate {
10
+ export interface FrameElementDelegate extends LinkInterceptorDelegate, FormSubmitObserverDelegate {
11
11
  connect(): void;
12
12
  disconnect(): void;
13
13
  completeChanged(): void;
14
14
  loadingStyleChanged(): void;
15
15
  sourceURLChanged(): void;
16
+ sourceURLReloaded(): Promise<void>;
16
17
  disabledChanged(): void;
17
18
  loadResponse(response: FetchResponse): void;
19
+ proposeVisitIfNavigatedWithAction(frame: FrameElement, element: Element, submitter?: HTMLElement): void;
18
20
  fetchResponseLoaded: (fetchResponse: FetchResponse) => void;
19
21
  visitCachedSnapshot: (snapshot: Snapshot) => void;
20
22
  isLoading: boolean;
@@ -4,7 +4,7 @@ export declare type FormLinkClickObserverDelegate = {
4
4
  submittedFormLinkToLocation(link: Element, location: URL, form: HTMLFormElement): void;
5
5
  };
6
6
  export declare class FormLinkClickObserver implements LinkClickObserverDelegate {
7
- readonly linkClickObserver: LinkClickObserver;
7
+ readonly linkInterceptor: LinkClickObserver;
8
8
  readonly delegate: FormLinkClickObserverDelegate;
9
9
  constructor(delegate: FormLinkClickObserverDelegate, element: HTMLElement);
10
10
  start(): void;
@@ -22,6 +22,7 @@ export declare function nextBeat(): Promise<void>;
22
22
  export declare function nextBody(_page: Page, timeout?: number): Promise<void>;
23
23
  export declare function nextEventNamed(page: Page, eventName: string): Promise<any>;
24
24
  export declare function nextEventOnTarget(page: Page, elementId: string, eventName: string): Promise<any>;
25
+ export declare function listenForEventOnTarget(page: Page, elementId: string, eventName: string): Promise<void>;
25
26
  export declare function nextAttributeMutationNamed(page: Page, elementId: string, attributeName: string): Promise<string | null>;
26
27
  export declare function noNextAttributeMutationNamed(page: Page, elementId: string, attributeName: string): Promise<boolean>;
27
28
  export declare function noNextEventNamed(page: Page, eventName: string): Promise<boolean>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotwired/turbo",
3
- "version": "7.2.0",
3
+ "version": "7.2.2",
4
4
  "description": "The speed of a single-page web application without having to write any JavaScript",
5
5
  "module": "dist/turbo.es2017-esm.js",
6
6
  "main": "dist/turbo.es2017-umd.js",