@hotwired/turbo 7.0.1 → 7.1.0-rc.1

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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ # Changelog
2
+
3
+ Please see [our GitHub "Releases" page](https://github.com/hotwired/turbo/releases).
@@ -1,5 +1,5 @@
1
1
  /*
2
- Turbo 7.0.0
2
+ Turbo 7.0.1
3
3
  Copyright © 2021 Basecamp, LLC
4
4
  */
5
5
  (function () {
@@ -20,6 +20,58 @@ Copyright © 2021 Basecamp, LLC
20
20
  Object.setPrototypeOf(HTMLElement, BuiltInHTMLElement);
21
21
  })();
22
22
 
23
+ /**
24
+ * The MIT License (MIT)
25
+ *
26
+ * Copyright (c) 2019 Javan Makhmali
27
+ *
28
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
29
+ * of this software and associated documentation files (the "Software"), to deal
30
+ * in the Software without restriction, including without limitation the rights
31
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
32
+ * copies of the Software, and to permit persons to whom the Software is
33
+ * furnished to do so, subject to the following conditions:
34
+ *
35
+ * The above copyright notice and this permission notice shall be included in
36
+ * all copies or substantial portions of the Software.
37
+ *
38
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
39
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
40
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
41
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
42
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
43
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
44
+ * THE SOFTWARE.
45
+ */
46
+
47
+ (function(prototype) {
48
+ if (typeof prototype.requestSubmit == "function") return
49
+
50
+ prototype.requestSubmit = function(submitter) {
51
+ if (submitter) {
52
+ validateSubmitter(submitter, this);
53
+ submitter.click();
54
+ } else {
55
+ submitter = document.createElement("input");
56
+ submitter.type = "submit";
57
+ submitter.hidden = true;
58
+ this.appendChild(submitter);
59
+ submitter.click();
60
+ this.removeChild(submitter);
61
+ }
62
+ };
63
+
64
+ function validateSubmitter(submitter, form) {
65
+ submitter instanceof HTMLElement || raise(TypeError, "parameter 1 is not of type 'HTMLElement'");
66
+ submitter.type == "submit" || raise(TypeError, "The specified element is not a submit button");
67
+ submitter.form == form || raise(DOMException, "The specified element is not owned by this form element", "NotFoundError");
68
+ }
69
+
70
+ function raise(errorConstructor, message, name) {
71
+ throw new errorConstructor("Failed to execute 'requestSubmit' on 'HTMLFormElement': " + message + ".", name)
72
+ }
73
+ })(HTMLFormElement.prototype);
74
+
23
75
  const submittersByForm = new WeakMap;
24
76
  function findSubmitterFromClickTarget(target) {
25
77
  const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
@@ -165,6 +217,10 @@ function getAnchor(url) {
165
217
  return anchorMatch[1];
166
218
  }
167
219
  }
220
+ function getAction(form, submitter) {
221
+ const action = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formaction")) || form.getAttribute("action") || form.action;
222
+ return expandURL(action);
223
+ }
168
224
  function getExtension(url) {
169
225
  return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || "";
170
226
  }
@@ -175,6 +231,9 @@ function isPrefixedBy(baseURL, url) {
175
231
  const prefix = getPrefix(url);
176
232
  return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix);
177
233
  }
234
+ function locationIsVisitable(location, rootLocation) {
235
+ return isPrefixedBy(location, rootLocation) && isHTML(location);
236
+ }
178
237
  function getRequestURL(url) {
179
238
  const anchor = getAnchor(url);
180
239
  return anchor != null
@@ -297,6 +356,29 @@ function uuid() {
297
356
  }
298
357
  }).join("");
299
358
  }
359
+ function getAttribute(attributeName, ...elements) {
360
+ for (const value of elements.map(element => element === null || element === void 0 ? void 0 : element.getAttribute(attributeName))) {
361
+ if (typeof value == "string")
362
+ return value;
363
+ }
364
+ return null;
365
+ }
366
+ function markAsBusy(...elements) {
367
+ for (const element of elements) {
368
+ if (element.localName == "turbo-frame") {
369
+ element.setAttribute("busy", "");
370
+ }
371
+ element.setAttribute("aria-busy", "true");
372
+ }
373
+ }
374
+ function clearBusyState(...elements) {
375
+ for (const element of elements) {
376
+ if (element.localName == "turbo-frame") {
377
+ element.removeAttribute("busy");
378
+ }
379
+ element.removeAttribute("aria-busy");
380
+ }
381
+ }
300
382
 
301
383
  var FetchMethod;
302
384
  (function (FetchMethod) {
@@ -526,6 +608,9 @@ class FormSubmission {
526
608
  this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body, this.formElement);
527
609
  this.mustRedirect = mustRedirect;
528
610
  }
611
+ static confirmMethod(message, element) {
612
+ return confirm(message);
613
+ }
529
614
  get method() {
530
615
  var _a;
531
616
  const method = ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formmethod")) || this.formElement.getAttribute("method") || "";
@@ -559,8 +644,20 @@ class FormSubmission {
559
644
  return entries.concat(typeof value == "string" ? [[name, value]] : []);
560
645
  }, []);
561
646
  }
647
+ get confirmationMessage() {
648
+ return this.formElement.getAttribute("data-turbo-confirm");
649
+ }
650
+ get needsConfirmation() {
651
+ return this.confirmationMessage !== null;
652
+ }
562
653
  async start() {
563
654
  const { initialized, requesting } = FormSubmissionState;
655
+ if (this.needsConfirmation) {
656
+ const answer = FormSubmission.confirmMethod(this.confirmationMessage, this.formElement);
657
+ if (!answer) {
658
+ return;
659
+ }
660
+ }
564
661
  if (this.state == initialized) {
565
662
  this.state = requesting;
566
663
  return this.fetchRequest.perform();
@@ -584,7 +681,9 @@ class FormSubmission {
584
681
  }
585
682
  }
586
683
  requestStarted(request) {
684
+ var _a;
587
685
  this.state = FormSubmissionState.waiting;
686
+ (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.setAttribute("disabled", "");
588
687
  dispatch("turbo:submit-start", { target: this.formElement, detail: { formSubmission: this } });
589
688
  this.delegate.formSubmissionStarted(this);
590
689
  }
@@ -614,7 +713,9 @@ class FormSubmission {
614
713
  this.delegate.formSubmissionErrored(this, error);
615
714
  }
616
715
  requestFinished(request) {
716
+ var _a;
617
717
  this.state = FormSubmissionState.stopped;
718
+ (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.removeAttribute("disabled");
618
719
  dispatch("turbo:submit-end", { target: this.formElement, detail: Object.assign({ formSubmission: this }, this.result) });
619
720
  this.delegate.formSubmissionFinished(this);
620
721
  }
@@ -693,7 +794,8 @@ class FormInterceptor {
693
794
  const form = event.target;
694
795
  if (form instanceof HTMLFormElement && form.closest("turbo-frame, html") == this.element) {
695
796
  const submitter = event.submitter || undefined;
696
- if (this.delegate.shouldInterceptFormSubmission(form, submitter)) {
797
+ const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.method;
798
+ if (method != "dialog" && this.delegate.shouldInterceptFormSubmission(form, submitter)) {
697
799
  event.preventDefault();
698
800
  event.stopImmediatePropagation();
699
801
  this.delegate.formSubmissionIntercepted(form, submitter);
@@ -1287,7 +1389,8 @@ var VisitState;
1287
1389
  })(VisitState || (VisitState = {}));
1288
1390
  const defaultOptions = {
1289
1391
  action: "advance",
1290
- historyChanged: false
1392
+ delegate: {},
1393
+ historyChanged: false,
1291
1394
  };
1292
1395
  var SystemStatusCode;
1293
1396
  (function (SystemStatusCode) {
@@ -1307,13 +1410,14 @@ class Visit {
1307
1410
  this.delegate = delegate;
1308
1411
  this.location = location;
1309
1412
  this.restorationIdentifier = restorationIdentifier || uuid();
1310
- const { action, historyChanged, referrer, snapshotHTML, response } = Object.assign(Object.assign({}, defaultOptions), options);
1413
+ const { action, historyChanged, referrer, snapshotHTML, response, delegate: optionalDelegate } = Object.assign(Object.assign({}, defaultOptions), options);
1311
1414
  this.action = action;
1312
1415
  this.historyChanged = historyChanged;
1313
1416
  this.referrer = referrer;
1314
1417
  this.snapshotHTML = snapshotHTML;
1315
1418
  this.response = response;
1316
1419
  this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
1420
+ this.optionalDelegate = optionalDelegate;
1317
1421
  }
1318
1422
  get adapter() {
1319
1423
  return this.delegate.adapter;
@@ -1336,6 +1440,8 @@ class Visit {
1336
1440
  this.state = VisitState.started;
1337
1441
  this.adapter.visitStarted(this);
1338
1442
  this.delegate.visitStarted(this);
1443
+ if (this.optionalDelegate.visitStarted)
1444
+ this.optionalDelegate.visitStarted(this);
1339
1445
  }
1340
1446
  }
1341
1447
  cancel() {
@@ -1465,7 +1571,8 @@ class Visit {
1465
1571
  }
1466
1572
  }
1467
1573
  followRedirect() {
1468
- if (this.redirectedToLocation && !this.followedRedirect) {
1574
+ var _a;
1575
+ if (this.redirectedToLocation && !this.followedRedirect && ((_a = this.response) === null || _a === void 0 ? void 0 : _a.redirected)) {
1469
1576
  this.adapter.visitProposedToLocation(this.redirectedToLocation, {
1470
1577
  action: 'replace',
1471
1578
  response: this.response
@@ -1488,25 +1595,27 @@ class Visit {
1488
1595
  }
1489
1596
  async requestSucceededWithResponse(request, response) {
1490
1597
  const responseHTML = await response.responseHTML;
1598
+ const { redirected, statusCode } = response;
1491
1599
  if (responseHTML == undefined) {
1492
- this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch });
1600
+ this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch, redirected });
1493
1601
  }
1494
1602
  else {
1495
1603
  this.redirectedToLocation = response.redirected ? response.location : undefined;
1496
- this.recordResponse({ statusCode: response.statusCode, responseHTML });
1604
+ this.recordResponse({ statusCode: statusCode, responseHTML, redirected });
1497
1605
  }
1498
1606
  }
1499
1607
  async requestFailedWithResponse(request, response) {
1500
1608
  const responseHTML = await response.responseHTML;
1609
+ const { redirected, statusCode } = response;
1501
1610
  if (responseHTML == undefined) {
1502
- this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch });
1611
+ this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch, redirected });
1503
1612
  }
1504
1613
  else {
1505
- this.recordResponse({ statusCode: response.statusCode, responseHTML });
1614
+ this.recordResponse({ statusCode: statusCode, responseHTML, redirected });
1506
1615
  }
1507
1616
  }
1508
1617
  requestErrored(request, error) {
1509
- this.recordResponse({ statusCode: SystemStatusCode.networkFailure });
1618
+ this.recordResponse({ statusCode: SystemStatusCode.networkFailure, redirected: false });
1510
1619
  }
1511
1620
  requestFinished() {
1512
1621
  this.finishRequest();
@@ -1570,6 +1679,8 @@ class Visit {
1570
1679
  if (!this.snapshotCached) {
1571
1680
  this.view.cacheSnapshot();
1572
1681
  this.snapshotCached = true;
1682
+ if (this.optionalDelegate.visitCachedSnapshot)
1683
+ this.optionalDelegate.visitCachedSnapshot(this);
1573
1684
  }
1574
1685
  }
1575
1686
  async render(callback) {
@@ -1718,7 +1829,7 @@ class FormSubmitObserver {
1718
1829
  const form = event.target instanceof HTMLFormElement ? event.target : undefined;
1719
1830
  const submitter = event.submitter || undefined;
1720
1831
  if (form) {
1721
- const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.method;
1832
+ const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.getAttribute("method");
1722
1833
  if (method != "dialog" && this.delegate.willSubmitForm(form, submitter)) {
1723
1834
  event.preventDefault();
1724
1835
  this.delegate.formSubmitted(form, submitter);
@@ -1762,12 +1873,11 @@ class FrameRedirector {
1762
1873
  linkClickIntercepted(element, url) {
1763
1874
  const frame = this.findFrameElement(element);
1764
1875
  if (frame) {
1765
- frame.setAttribute("reloadable", "");
1766
- frame.src = url;
1876
+ frame.delegate.linkClickIntercepted(element, url);
1767
1877
  }
1768
1878
  }
1769
1879
  shouldInterceptFormSubmission(element, submitter) {
1770
- return this.shouldRedirect(element, submitter);
1880
+ return this.shouldSubmit(element, submitter);
1771
1881
  }
1772
1882
  formSubmissionIntercepted(element, submitter) {
1773
1883
  const frame = this.findFrameElement(element, submitter);
@@ -1776,6 +1886,13 @@ class FrameRedirector {
1776
1886
  frame.delegate.formSubmissionIntercepted(element, submitter);
1777
1887
  }
1778
1888
  }
1889
+ shouldSubmit(form, submitter) {
1890
+ var _a;
1891
+ const action = getAction(form, submitter);
1892
+ const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
1893
+ const rootLocation = expandURL((_a = meta === null || meta === void 0 ? void 0 : meta.content) !== null && _a !== void 0 ? _a : "/");
1894
+ return this.shouldRedirect(form, submitter) && locationIsVisitable(action, rootLocation);
1895
+ }
1779
1896
  shouldRedirect(element, submitter) {
1780
1897
  const frame = this.findFrameElement(element, submitter);
1781
1898
  return frame ? frame != element.closest("turbo-frame") : false;
@@ -1933,7 +2050,12 @@ class Navigator {
1933
2050
  }
1934
2051
  proposeVisit(location, options = {}) {
1935
2052
  if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
1936
- this.delegate.visitProposedToLocation(location, options);
2053
+ if (locationIsVisitable(location, this.view.snapshot.rootLocation)) {
2054
+ this.delegate.visitProposedToLocation(location, options);
2055
+ }
2056
+ else {
2057
+ window.location.href = location.toString();
2058
+ }
1937
2059
  }
1938
2060
  }
1939
2061
  startVisit(locatable, restorationIdentifier, options = {}) {
@@ -1944,12 +2066,7 @@ class Navigator {
1944
2066
  submitForm(form, submitter) {
1945
2067
  this.stop();
1946
2068
  this.formSubmission = new FormSubmission(this, form, submitter, true);
1947
- if (this.formSubmission.isIdempotent) {
1948
- this.proposeVisit(this.formSubmission.fetchRequest.url, { action: this.getActionForFormSubmission(this.formSubmission) });
1949
- }
1950
- else {
1951
- this.formSubmission.start();
1952
- }
2069
+ this.formSubmission.start();
1953
2070
  }
1954
2071
  stop() {
1955
2072
  if (this.formSubmission) {
@@ -1982,8 +2099,9 @@ class Navigator {
1982
2099
  if (formSubmission.method != FetchMethod.get) {
1983
2100
  this.view.clearSnapshotCache();
1984
2101
  }
1985
- const { statusCode } = fetchResponse;
1986
- const visitOptions = { response: { statusCode, responseHTML } };
2102
+ const { statusCode, redirected } = fetchResponse;
2103
+ const action = this.getActionForFormSubmission(formSubmission);
2104
+ const visitOptions = { action, response: { statusCode, responseHTML, redirected } };
1987
2105
  this.proposeVisit(fetchResponse.location, visitOptions);
1988
2106
  }
1989
2107
  }
@@ -2016,6 +2134,9 @@ class Navigator {
2016
2134
  visitCompleted(visit) {
2017
2135
  this.delegate.visitCompleted(visit);
2018
2136
  }
2137
+ visitCachedSnapshot(visit) {
2138
+ this.delegate.visitCachedSnapshot(visit);
2139
+ }
2019
2140
  locationWithActionIsSamePage(location, action) {
2020
2141
  const anchor = getAnchor(location);
2021
2142
  const currentAnchor = getAnchor(this.view.lastRenderedLocation);
@@ -2035,7 +2156,7 @@ class Navigator {
2035
2156
  }
2036
2157
  getActionForFormSubmission(formSubmission) {
2037
2158
  const { formElement, submitter } = formSubmission;
2038
- const action = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("data-turbo-action")) || formElement.getAttribute("data-turbo-action");
2159
+ const action = getAttribute("data-turbo-action", submitter, formElement);
2039
2160
  return isAction(action) ? action : "advance";
2040
2161
  }
2041
2162
  }
@@ -2485,7 +2606,7 @@ class Session {
2485
2606
  }
2486
2607
  willFollowLinkToLocation(link, location) {
2487
2608
  return this.elementDriveEnabled(link)
2488
- && this.locationIsVisitable(location)
2609
+ && locationIsVisitable(location, this.snapshot.rootLocation)
2489
2610
  && this.applicationAllowsFollowingLinkToLocation(link, location);
2490
2611
  }
2491
2612
  followedLinkToLocation(link, location) {
@@ -2493,14 +2614,24 @@ class Session {
2493
2614
  this.convertLinkWithMethodClickToFormSubmission(link) || this.visit(location.href, { action });
2494
2615
  }
2495
2616
  convertLinkWithMethodClickToFormSubmission(link) {
2496
- var _a;
2497
2617
  const linkMethod = link.getAttribute("data-turbo-method");
2498
2618
  if (linkMethod) {
2499
2619
  const form = document.createElement("form");
2500
2620
  form.method = linkMethod;
2501
2621
  form.action = link.getAttribute("href") || "undefined";
2502
2622
  form.hidden = true;
2503
- (_a = link.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(form, link);
2623
+ if (link.hasAttribute("data-turbo-confirm")) {
2624
+ form.setAttribute("data-turbo-confirm", link.getAttribute("data-turbo-confirm"));
2625
+ }
2626
+ const frame = this.getTargetFrameForLink(link);
2627
+ if (frame) {
2628
+ form.setAttribute("data-turbo-frame", frame);
2629
+ form.addEventListener("turbo:submit-start", () => form.remove());
2630
+ }
2631
+ else {
2632
+ form.addEventListener("submit", () => form.remove());
2633
+ }
2634
+ document.body.appendChild(form);
2504
2635
  return dispatch("submit", { cancelable: true, target: form });
2505
2636
  }
2506
2637
  else {
@@ -2523,6 +2654,8 @@ class Session {
2523
2654
  visitCompleted(visit) {
2524
2655
  this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
2525
2656
  }
2657
+ visitCachedSnapshot(visit) {
2658
+ }
2526
2659
  locationWithActionIsSamePage(location, action) {
2527
2660
  return this.navigator.locationWithActionIsSamePage(location, action);
2528
2661
  }
@@ -2530,7 +2663,10 @@ class Session {
2530
2663
  this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
2531
2664
  }
2532
2665
  willSubmitForm(form, submitter) {
2533
- return this.elementDriveEnabled(form) && (!submitter || this.elementDriveEnabled(submitter));
2666
+ const action = getAction(form, submitter);
2667
+ return this.elementDriveEnabled(form)
2668
+ && (!submitter || this.elementDriveEnabled(submitter))
2669
+ && locationIsVisitable(expandURL(action), this.snapshot.rootLocation);
2534
2670
  }
2535
2671
  formSubmitted(form, submitter) {
2536
2672
  this.navigator.submitForm(form, submitter);
@@ -2586,6 +2722,7 @@ class Session {
2586
2722
  return dispatch("turbo:before-visit", { detail: { url: location.href }, cancelable: true });
2587
2723
  }
2588
2724
  notifyApplicationAfterVisitingLocation(location, action) {
2725
+ markAsBusy(document.documentElement);
2589
2726
  return dispatch("turbo:visit", { detail: { url: location.href, action } });
2590
2727
  }
2591
2728
  notifyApplicationBeforeCachingSnapshot() {
@@ -2598,6 +2735,7 @@ class Session {
2598
2735
  return dispatch("turbo:render");
2599
2736
  }
2600
2737
  notifyApplicationAfterPageLoad(timing = {}) {
2738
+ clearBusyState(document.documentElement);
2601
2739
  return dispatch("turbo:load", { detail: { url: this.location.href, timing } });
2602
2740
  }
2603
2741
  notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL) {
@@ -2632,8 +2770,17 @@ class Session {
2632
2770
  const action = link.getAttribute("data-turbo-action");
2633
2771
  return isAction(action) ? action : "advance";
2634
2772
  }
2635
- locationIsVisitable(location) {
2636
- return isPrefixedBy(location, this.snapshot.rootLocation) && isHTML(location);
2773
+ getTargetFrameForLink(link) {
2774
+ const frame = link.getAttribute("data-turbo-frame");
2775
+ if (frame) {
2776
+ return frame;
2777
+ }
2778
+ else {
2779
+ const container = link.closest("turbo-frame");
2780
+ if (container) {
2781
+ return container.id;
2782
+ }
2783
+ }
2637
2784
  }
2638
2785
  get snapshot() {
2639
2786
  return this.view.snapshot;
@@ -2676,6 +2823,9 @@ function clearCache() {
2676
2823
  function setProgressBarDelay(delay) {
2677
2824
  session.setProgressBarDelay(delay);
2678
2825
  }
2826
+ function setConfirmMethod(confirmMethod) {
2827
+ FormSubmission.confirmMethod = confirmMethod;
2828
+ }
2679
2829
 
2680
2830
  var Turbo = /*#__PURE__*/Object.freeze({
2681
2831
  __proto__: null,
@@ -2690,11 +2840,13 @@ var Turbo = /*#__PURE__*/Object.freeze({
2690
2840
  disconnectStreamSource: disconnectStreamSource,
2691
2841
  renderStreamMessage: renderStreamMessage,
2692
2842
  clearCache: clearCache,
2693
- setProgressBarDelay: setProgressBarDelay
2843
+ setProgressBarDelay: setProgressBarDelay,
2844
+ setConfirmMethod: setConfirmMethod
2694
2845
  });
2695
2846
 
2696
2847
  class FrameController {
2697
2848
  constructor(element) {
2849
+ this.currentFetchRequest = null;
2698
2850
  this.resolveVisitPromise = () => { };
2699
2851
  this.connected = false;
2700
2852
  this.hasBeenLoaded = false;
@@ -2754,7 +2906,6 @@ class FrameController {
2754
2906
  this.appearanceObserver.stop();
2755
2907
  await this.element.loaded;
2756
2908
  this.hasBeenLoaded = true;
2757
- session.frameLoaded(this.element);
2758
2909
  }
2759
2910
  catch (error) {
2760
2911
  this.currentURL = previousURL;
@@ -2777,6 +2928,7 @@ class FrameController {
2777
2928
  await this.view.renderPromise;
2778
2929
  await this.view.render(renderer);
2779
2930
  session.frameRendered(fetchResponse, this.element);
2931
+ session.frameLoaded(this.element);
2780
2932
  }
2781
2933
  }
2782
2934
  catch (error) {
@@ -2808,20 +2960,15 @@ class FrameController {
2808
2960
  }
2809
2961
  this.reloadable = false;
2810
2962
  this.formSubmission = new FormSubmission(this, element, submitter);
2811
- if (this.formSubmission.fetchRequest.isIdempotent) {
2812
- this.navigateFrame(element, this.formSubmission.fetchRequest.url.href, submitter);
2813
- }
2814
- else {
2815
- const { fetchRequest } = this.formSubmission;
2816
- this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
2817
- this.formSubmission.start();
2818
- }
2963
+ const { fetchRequest } = this.formSubmission;
2964
+ this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
2965
+ this.formSubmission.start();
2819
2966
  }
2820
2967
  prepareHeadersForRequest(headers, request) {
2821
2968
  headers["Turbo-Frame"] = this.id;
2822
2969
  }
2823
2970
  requestStarted(request) {
2824
- this.element.setAttribute("busy", "");
2971
+ markAsBusy(this.element);
2825
2972
  }
2826
2973
  requestPreventedHandlingResponse(request, response) {
2827
2974
  this.resolveVisitPromise();
@@ -2839,14 +2986,14 @@ class FrameController {
2839
2986
  this.resolveVisitPromise();
2840
2987
  }
2841
2988
  requestFinished(request) {
2842
- this.element.removeAttribute("busy");
2989
+ clearBusyState(this.element);
2843
2990
  }
2844
- formSubmissionStarted(formSubmission) {
2845
- const frame = this.findFrameElement(formSubmission.formElement);
2846
- frame.setAttribute("busy", "");
2991
+ formSubmissionStarted({ formElement }) {
2992
+ markAsBusy(formElement, this.findFrameElement(formElement));
2847
2993
  }
2848
2994
  formSubmissionSucceededWithResponse(formSubmission, response) {
2849
2995
  const frame = this.findFrameElement(formSubmission.formElement, formSubmission.submitter);
2996
+ this.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
2850
2997
  frame.delegate.loadResponse(response);
2851
2998
  }
2852
2999
  formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
@@ -2855,9 +3002,8 @@ class FrameController {
2855
3002
  formSubmissionErrored(formSubmission, error) {
2856
3003
  console.error(error);
2857
3004
  }
2858
- formSubmissionFinished(formSubmission) {
2859
- const frame = this.findFrameElement(formSubmission.formElement);
2860
- frame.removeAttribute("busy");
3005
+ formSubmissionFinished({ formElement }) {
3006
+ clearBusyState(formElement, this.findFrameElement(formElement));
2861
3007
  }
2862
3008
  allowsImmediateRender(snapshot, resume) {
2863
3009
  return true;
@@ -2867,10 +3013,14 @@ class FrameController {
2867
3013
  viewInvalidated() {
2868
3014
  }
2869
3015
  async visit(url) {
2870
- const request = new FetchRequest(this, FetchMethod.get, expandURL(url), undefined, this.element);
3016
+ var _a;
3017
+ const request = new FetchRequest(this, FetchMethod.get, expandURL(url), new URLSearchParams, this.element);
3018
+ (_a = this.currentFetchRequest) === null || _a === void 0 ? void 0 : _a.cancel();
3019
+ this.currentFetchRequest = request;
2871
3020
  return new Promise(resolve => {
2872
3021
  this.resolveVisitPromise = () => {
2873
3022
  this.resolveVisitPromise = () => { };
3023
+ this.currentFetchRequest = null;
2874
3024
  resolve();
2875
3025
  };
2876
3026
  request.perform();
@@ -2878,12 +3028,29 @@ class FrameController {
2878
3028
  }
2879
3029
  navigateFrame(element, url, submitter) {
2880
3030
  const frame = this.findFrameElement(element, submitter);
3031
+ this.proposeVisitIfNavigatedWithAction(frame, element, submitter);
2881
3032
  frame.setAttribute("reloadable", "");
2882
3033
  frame.src = url;
2883
3034
  }
3035
+ proposeVisitIfNavigatedWithAction(frame, element, submitter) {
3036
+ const action = getAttribute("data-turbo-action", submitter, element, frame);
3037
+ if (isAction(action)) {
3038
+ const delegate = new SnapshotSubstitution(frame);
3039
+ const proposeVisit = (event) => {
3040
+ const { target, detail: { fetchResponse } } = event;
3041
+ if (target instanceof FrameElement && target.src) {
3042
+ const { statusCode, redirected } = fetchResponse;
3043
+ const responseHTML = target.ownerDocument.documentElement.outerHTML;
3044
+ const response = { statusCode, redirected, responseHTML };
3045
+ session.visit(target.src, { action, response, delegate });
3046
+ }
3047
+ };
3048
+ frame.addEventListener("turbo:frame-render", proposeVisit, { once: true });
3049
+ }
3050
+ }
2884
3051
  findFrameElement(element, submitter) {
2885
3052
  var _a;
2886
- const id = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("data-turbo-frame")) || element.getAttribute("data-turbo-frame") || this.element.getAttribute("target");
3053
+ const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
2887
3054
  return (_a = getFrameElementById(id)) !== null && _a !== void 0 ? _a : this.element;
2888
3055
  }
2889
3056
  async extractForeignFrameElement(container) {
@@ -2904,8 +3071,15 @@ class FrameController {
2904
3071
  }
2905
3072
  return new FrameElement();
2906
3073
  }
3074
+ formActionIsVisitable(form, submitter) {
3075
+ const action = getAction(form, submitter);
3076
+ return locationIsVisitable(expandURL(action), this.rootLocation);
3077
+ }
2907
3078
  shouldInterceptNavigation(element, submitter) {
2908
- const id = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("data-turbo-frame")) || element.getAttribute("data-turbo-frame") || this.element.getAttribute("target");
3079
+ const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
3080
+ if (element instanceof HTMLFormElement && !this.formActionIsVisitable(element, submitter)) {
3081
+ return false;
3082
+ }
2909
3083
  if (!this.enabled || id == "_top") {
2910
3084
  return false;
2911
3085
  }
@@ -2962,6 +3136,28 @@ class FrameController {
2962
3136
  get isActive() {
2963
3137
  return this.element.isActive && this.connected;
2964
3138
  }
3139
+ get rootLocation() {
3140
+ var _a;
3141
+ const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
3142
+ const root = (_a = meta === null || meta === void 0 ? void 0 : meta.content) !== null && _a !== void 0 ? _a : "/";
3143
+ return expandURL(root);
3144
+ }
3145
+ }
3146
+ class SnapshotSubstitution {
3147
+ constructor(element) {
3148
+ this.clone = element.cloneNode(true);
3149
+ this.id = element.id;
3150
+ }
3151
+ visitStarted(visit) {
3152
+ this.snapshot = visit.view.snapshot;
3153
+ }
3154
+ visitCachedSnapshot() {
3155
+ var _a;
3156
+ const { snapshot, id, clone } = this;
3157
+ if (snapshot) {
3158
+ (_a = snapshot.element.querySelector("#" + id)) === null || _a === void 0 ? void 0 : _a.replaceWith(clone);
3159
+ }
3160
+ }
2965
3161
  }
2966
3162
  function getFrameElementById(id) {
2967
3163
  if (id != null) {
@@ -3152,4 +3348,4 @@ customElements.define("turbo-stream", StreamElement);
3152
3348
  window.Turbo = Turbo;
3153
3349
  start();
3154
3350
 
3155
- export { PageRenderer, PageSnapshot, clearCache, connectStreamSource, disconnectStreamSource, navigator$1 as navigator, registerAdapter, renderStreamMessage, session, setProgressBarDelay, start, visit };
3351
+ export { PageRenderer, PageSnapshot, clearCache, connectStreamSource, disconnectStreamSource, navigator$1 as navigator, registerAdapter, renderStreamMessage, session, setConfirmMethod, setProgressBarDelay, start, visit };