@hotwired/turbo 7.0.0-rc.4 → 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.
@@ -1,5 +1,5 @@
1
1
  /*
2
- Turbo 7.0.0-rc.3
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;
@@ -33,12 +85,20 @@ function clickCaptured(event) {
33
85
  }
34
86
  }
35
87
  (function () {
36
- if ("SubmitEvent" in window)
37
- return;
38
88
  if ("submitter" in Event.prototype)
39
89
  return;
90
+ let prototype;
91
+ if ("SubmitEvent" in window && /Apple Computer/.test(navigator.vendor)) {
92
+ prototype = window.SubmitEvent.prototype;
93
+ }
94
+ else if ("SubmitEvent" in window) {
95
+ return;
96
+ }
97
+ else {
98
+ prototype = window.Event.prototype;
99
+ }
40
100
  addEventListener("click", clickCaptured, true);
41
- Object.defineProperty(Event.prototype, "submitter", {
101
+ Object.defineProperty(prototype, "submitter", {
42
102
  get() {
43
103
  if (this.type == "submit" && this.target instanceof HTMLFormElement) {
44
104
  return submittersByForm.get(this.target);
@@ -157,6 +217,10 @@ function getAnchor(url) {
157
217
  return anchorMatch[1];
158
218
  }
159
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
+ }
160
224
  function getExtension(url) {
161
225
  return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || "";
162
226
  }
@@ -167,6 +231,9 @@ function isPrefixedBy(baseURL, url) {
167
231
  const prefix = getPrefix(url);
168
232
  return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix);
169
233
  }
234
+ function locationIsVisitable(location, rootLocation) {
235
+ return isPrefixedBy(location, rootLocation) && isHTML(location);
236
+ }
170
237
  function getRequestURL(url) {
171
238
  const anchor = getAnchor(url);
172
239
  return anchor != null
@@ -241,7 +308,12 @@ class FetchResponse {
241
308
 
242
309
  function dispatch(eventName, { target, cancelable, detail } = {}) {
243
310
  const event = new CustomEvent(eventName, { cancelable, bubbles: true, detail });
244
- void (target || document.documentElement).dispatchEvent(event);
311
+ if (target && target.isConnected) {
312
+ target.dispatchEvent(event);
313
+ }
314
+ else {
315
+ document.documentElement.dispatchEvent(event);
316
+ }
245
317
  return event;
246
318
  }
247
319
  function nextAnimationFrame() {
@@ -284,6 +356,29 @@ function uuid() {
284
356
  }
285
357
  }).join("");
286
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
+ }
287
382
 
288
383
  var FetchMethod;
289
384
  (function (FetchMethod) {
@@ -513,6 +608,9 @@ class FormSubmission {
513
608
  this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body, this.formElement);
514
609
  this.mustRedirect = mustRedirect;
515
610
  }
611
+ static confirmMethod(message, element) {
612
+ return confirm(message);
613
+ }
516
614
  get method() {
517
615
  var _a;
518
616
  const method = ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formmethod")) || this.formElement.getAttribute("method") || "";
@@ -546,8 +644,20 @@ class FormSubmission {
546
644
  return entries.concat(typeof value == "string" ? [[name, value]] : []);
547
645
  }, []);
548
646
  }
647
+ get confirmationMessage() {
648
+ return this.formElement.getAttribute("data-turbo-confirm");
649
+ }
650
+ get needsConfirmation() {
651
+ return this.confirmationMessage !== null;
652
+ }
549
653
  async start() {
550
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
+ }
551
661
  if (this.state == initialized) {
552
662
  this.state = requesting;
553
663
  return this.fetchRequest.perform();
@@ -571,7 +681,9 @@ class FormSubmission {
571
681
  }
572
682
  }
573
683
  requestStarted(request) {
684
+ var _a;
574
685
  this.state = FormSubmissionState.waiting;
686
+ (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.setAttribute("disabled", "");
575
687
  dispatch("turbo:submit-start", { target: this.formElement, detail: { formSubmission: this } });
576
688
  this.delegate.formSubmissionStarted(this);
577
689
  }
@@ -601,7 +713,9 @@ class FormSubmission {
601
713
  this.delegate.formSubmissionErrored(this, error);
602
714
  }
603
715
  requestFinished(request) {
716
+ var _a;
604
717
  this.state = FormSubmissionState.stopped;
718
+ (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.removeAttribute("disabled");
605
719
  dispatch("turbo:submit-end", { target: this.formElement, detail: Object.assign({ formSubmission: this }, this.result) });
606
720
  this.delegate.formSubmissionFinished(this);
607
721
  }
@@ -677,10 +791,11 @@ class Snapshot {
677
791
  class FormInterceptor {
678
792
  constructor(delegate, element) {
679
793
  this.submitBubbled = ((event) => {
680
- if (event.target instanceof HTMLFormElement) {
681
- const form = event.target;
794
+ const form = event.target;
795
+ if (form instanceof HTMLFormElement && form.closest("turbo-frame, html") == this.element) {
682
796
  const submitter = event.submitter || undefined;
683
- 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)) {
684
799
  event.preventDefault();
685
800
  event.stopImmediatePropagation();
686
801
  this.delegate.formSubmissionIntercepted(form, submitter);
@@ -1274,7 +1389,8 @@ var VisitState;
1274
1389
  })(VisitState || (VisitState = {}));
1275
1390
  const defaultOptions = {
1276
1391
  action: "advance",
1277
- historyChanged: false
1392
+ delegate: {},
1393
+ historyChanged: false,
1278
1394
  };
1279
1395
  var SystemStatusCode;
1280
1396
  (function (SystemStatusCode) {
@@ -1294,13 +1410,14 @@ class Visit {
1294
1410
  this.delegate = delegate;
1295
1411
  this.location = location;
1296
1412
  this.restorationIdentifier = restorationIdentifier || uuid();
1297
- 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);
1298
1414
  this.action = action;
1299
1415
  this.historyChanged = historyChanged;
1300
1416
  this.referrer = referrer;
1301
1417
  this.snapshotHTML = snapshotHTML;
1302
1418
  this.response = response;
1303
1419
  this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
1420
+ this.optionalDelegate = optionalDelegate;
1304
1421
  }
1305
1422
  get adapter() {
1306
1423
  return this.delegate.adapter;
@@ -1323,6 +1440,8 @@ class Visit {
1323
1440
  this.state = VisitState.started;
1324
1441
  this.adapter.visitStarted(this);
1325
1442
  this.delegate.visitStarted(this);
1443
+ if (this.optionalDelegate.visitStarted)
1444
+ this.optionalDelegate.visitStarted(this);
1326
1445
  }
1327
1446
  }
1328
1447
  cancel() {
@@ -1452,7 +1571,8 @@ class Visit {
1452
1571
  }
1453
1572
  }
1454
1573
  followRedirect() {
1455
- 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)) {
1456
1576
  this.adapter.visitProposedToLocation(this.redirectedToLocation, {
1457
1577
  action: 'replace',
1458
1578
  response: this.response
@@ -1475,25 +1595,27 @@ class Visit {
1475
1595
  }
1476
1596
  async requestSucceededWithResponse(request, response) {
1477
1597
  const responseHTML = await response.responseHTML;
1598
+ const { redirected, statusCode } = response;
1478
1599
  if (responseHTML == undefined) {
1479
- this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch });
1600
+ this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch, redirected });
1480
1601
  }
1481
1602
  else {
1482
1603
  this.redirectedToLocation = response.redirected ? response.location : undefined;
1483
- this.recordResponse({ statusCode: response.statusCode, responseHTML });
1604
+ this.recordResponse({ statusCode: statusCode, responseHTML, redirected });
1484
1605
  }
1485
1606
  }
1486
1607
  async requestFailedWithResponse(request, response) {
1487
1608
  const responseHTML = await response.responseHTML;
1609
+ const { redirected, statusCode } = response;
1488
1610
  if (responseHTML == undefined) {
1489
- this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch });
1611
+ this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch, redirected });
1490
1612
  }
1491
1613
  else {
1492
- this.recordResponse({ statusCode: response.statusCode, responseHTML });
1614
+ this.recordResponse({ statusCode: statusCode, responseHTML, redirected });
1493
1615
  }
1494
1616
  }
1495
1617
  requestErrored(request, error) {
1496
- this.recordResponse({ statusCode: SystemStatusCode.networkFailure });
1618
+ this.recordResponse({ statusCode: SystemStatusCode.networkFailure, redirected: false });
1497
1619
  }
1498
1620
  requestFinished() {
1499
1621
  this.finishRequest();
@@ -1557,6 +1679,8 @@ class Visit {
1557
1679
  if (!this.snapshotCached) {
1558
1680
  this.view.cacheSnapshot();
1559
1681
  this.snapshotCached = true;
1682
+ if (this.optionalDelegate.visitCachedSnapshot)
1683
+ this.optionalDelegate.visitCachedSnapshot(this);
1560
1684
  }
1561
1685
  }
1562
1686
  async render(callback) {
@@ -1705,7 +1829,7 @@ class FormSubmitObserver {
1705
1829
  const form = event.target instanceof HTMLFormElement ? event.target : undefined;
1706
1830
  const submitter = event.submitter || undefined;
1707
1831
  if (form) {
1708
- 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");
1709
1833
  if (method != "dialog" && this.delegate.willSubmitForm(form, submitter)) {
1710
1834
  event.preventDefault();
1711
1835
  this.delegate.formSubmitted(form, submitter);
@@ -1749,12 +1873,11 @@ class FrameRedirector {
1749
1873
  linkClickIntercepted(element, url) {
1750
1874
  const frame = this.findFrameElement(element);
1751
1875
  if (frame) {
1752
- frame.setAttribute("reloadable", "");
1753
- frame.src = url;
1876
+ frame.delegate.linkClickIntercepted(element, url);
1754
1877
  }
1755
1878
  }
1756
1879
  shouldInterceptFormSubmission(element, submitter) {
1757
- return this.shouldRedirect(element, submitter);
1880
+ return this.shouldSubmit(element, submitter);
1758
1881
  }
1759
1882
  formSubmissionIntercepted(element, submitter) {
1760
1883
  const frame = this.findFrameElement(element, submitter);
@@ -1763,6 +1886,13 @@ class FrameRedirector {
1763
1886
  frame.delegate.formSubmissionIntercepted(element, submitter);
1764
1887
  }
1765
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
+ }
1766
1896
  shouldRedirect(element, submitter) {
1767
1897
  const frame = this.findFrameElement(element, submitter);
1768
1898
  return frame ? frame != element.closest("turbo-frame") : false;
@@ -1920,7 +2050,12 @@ class Navigator {
1920
2050
  }
1921
2051
  proposeVisit(location, options = {}) {
1922
2052
  if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
1923
- 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
+ }
1924
2059
  }
1925
2060
  }
1926
2061
  startVisit(locatable, restorationIdentifier, options = {}) {
@@ -1931,12 +2066,7 @@ class Navigator {
1931
2066
  submitForm(form, submitter) {
1932
2067
  this.stop();
1933
2068
  this.formSubmission = new FormSubmission(this, form, submitter, true);
1934
- if (this.formSubmission.isIdempotent) {
1935
- this.proposeVisit(this.formSubmission.fetchRequest.url, { action: this.getActionForFormSubmission(this.formSubmission) });
1936
- }
1937
- else {
1938
- this.formSubmission.start();
1939
- }
2069
+ this.formSubmission.start();
1940
2070
  }
1941
2071
  stop() {
1942
2072
  if (this.formSubmission) {
@@ -1969,8 +2099,9 @@ class Navigator {
1969
2099
  if (formSubmission.method != FetchMethod.get) {
1970
2100
  this.view.clearSnapshotCache();
1971
2101
  }
1972
- const { statusCode } = fetchResponse;
1973
- const visitOptions = { response: { statusCode, responseHTML } };
2102
+ const { statusCode, redirected } = fetchResponse;
2103
+ const action = this.getActionForFormSubmission(formSubmission);
2104
+ const visitOptions = { action, response: { statusCode, responseHTML, redirected } };
1974
2105
  this.proposeVisit(fetchResponse.location, visitOptions);
1975
2106
  }
1976
2107
  }
@@ -2003,6 +2134,9 @@ class Navigator {
2003
2134
  visitCompleted(visit) {
2004
2135
  this.delegate.visitCompleted(visit);
2005
2136
  }
2137
+ visitCachedSnapshot(visit) {
2138
+ this.delegate.visitCachedSnapshot(visit);
2139
+ }
2006
2140
  locationWithActionIsSamePage(location, action) {
2007
2141
  const anchor = getAnchor(location);
2008
2142
  const currentAnchor = getAnchor(this.view.lastRenderedLocation);
@@ -2022,7 +2156,7 @@ class Navigator {
2022
2156
  }
2023
2157
  getActionForFormSubmission(formSubmission) {
2024
2158
  const { formElement, submitter } = formSubmission;
2025
- 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);
2026
2160
  return isAction(action) ? action : "advance";
2027
2161
  }
2028
2162
  }
@@ -2472,7 +2606,7 @@ class Session {
2472
2606
  }
2473
2607
  willFollowLinkToLocation(link, location) {
2474
2608
  return this.elementDriveEnabled(link)
2475
- && this.locationIsVisitable(location)
2609
+ && locationIsVisitable(location, this.snapshot.rootLocation)
2476
2610
  && this.applicationAllowsFollowingLinkToLocation(link, location);
2477
2611
  }
2478
2612
  followedLinkToLocation(link, location) {
@@ -2480,13 +2614,24 @@ class Session {
2480
2614
  this.convertLinkWithMethodClickToFormSubmission(link) || this.visit(location.href, { action });
2481
2615
  }
2482
2616
  convertLinkWithMethodClickToFormSubmission(link) {
2483
- var _a;
2484
2617
  const linkMethod = link.getAttribute("data-turbo-method");
2485
2618
  if (linkMethod) {
2486
2619
  const form = document.createElement("form");
2487
2620
  form.method = linkMethod;
2488
2621
  form.action = link.getAttribute("href") || "undefined";
2489
- (_a = link.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(form, link);
2622
+ form.hidden = true;
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);
2490
2635
  return dispatch("submit", { cancelable: true, target: form });
2491
2636
  }
2492
2637
  else {
@@ -2509,6 +2654,8 @@ class Session {
2509
2654
  visitCompleted(visit) {
2510
2655
  this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
2511
2656
  }
2657
+ visitCachedSnapshot(visit) {
2658
+ }
2512
2659
  locationWithActionIsSamePage(location, action) {
2513
2660
  return this.navigator.locationWithActionIsSamePage(location, action);
2514
2661
  }
@@ -2516,7 +2663,10 @@ class Session {
2516
2663
  this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
2517
2664
  }
2518
2665
  willSubmitForm(form, submitter) {
2519
- return this.elementDriveEnabled(form) && 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);
2520
2670
  }
2521
2671
  formSubmitted(form, submitter) {
2522
2672
  this.navigator.submitForm(form, submitter);
@@ -2572,6 +2722,7 @@ class Session {
2572
2722
  return dispatch("turbo:before-visit", { detail: { url: location.href }, cancelable: true });
2573
2723
  }
2574
2724
  notifyApplicationAfterVisitingLocation(location, action) {
2725
+ markAsBusy(document.documentElement);
2575
2726
  return dispatch("turbo:visit", { detail: { url: location.href, action } });
2576
2727
  }
2577
2728
  notifyApplicationBeforeCachingSnapshot() {
@@ -2584,6 +2735,7 @@ class Session {
2584
2735
  return dispatch("turbo:render");
2585
2736
  }
2586
2737
  notifyApplicationAfterPageLoad(timing = {}) {
2738
+ clearBusyState(document.documentElement);
2587
2739
  return dispatch("turbo:load", { detail: { url: this.location.href, timing } });
2588
2740
  }
2589
2741
  notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL) {
@@ -2618,8 +2770,17 @@ class Session {
2618
2770
  const action = link.getAttribute("data-turbo-action");
2619
2771
  return isAction(action) ? action : "advance";
2620
2772
  }
2621
- locationIsVisitable(location) {
2622
- 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
+ }
2623
2784
  }
2624
2785
  get snapshot() {
2625
2786
  return this.view.snapshot;
@@ -2637,7 +2798,7 @@ const deprecatedLocationPropertyDescriptors = {
2637
2798
  };
2638
2799
 
2639
2800
  const session = new Session;
2640
- const { navigator } = session;
2801
+ const { navigator: navigator$1 } = session;
2641
2802
  function start() {
2642
2803
  session.start();
2643
2804
  }
@@ -2662,10 +2823,13 @@ function clearCache() {
2662
2823
  function setProgressBarDelay(delay) {
2663
2824
  session.setProgressBarDelay(delay);
2664
2825
  }
2826
+ function setConfirmMethod(confirmMethod) {
2827
+ FormSubmission.confirmMethod = confirmMethod;
2828
+ }
2665
2829
 
2666
2830
  var Turbo = /*#__PURE__*/Object.freeze({
2667
2831
  __proto__: null,
2668
- navigator: navigator,
2832
+ navigator: navigator$1,
2669
2833
  session: session,
2670
2834
  PageRenderer: PageRenderer,
2671
2835
  PageSnapshot: PageSnapshot,
@@ -2676,11 +2840,13 @@ var Turbo = /*#__PURE__*/Object.freeze({
2676
2840
  disconnectStreamSource: disconnectStreamSource,
2677
2841
  renderStreamMessage: renderStreamMessage,
2678
2842
  clearCache: clearCache,
2679
- setProgressBarDelay: setProgressBarDelay
2843
+ setProgressBarDelay: setProgressBarDelay,
2844
+ setConfirmMethod: setConfirmMethod
2680
2845
  });
2681
2846
 
2682
2847
  class FrameController {
2683
2848
  constructor(element) {
2849
+ this.currentFetchRequest = null;
2684
2850
  this.resolveVisitPromise = () => { };
2685
2851
  this.connected = false;
2686
2852
  this.hasBeenLoaded = false;
@@ -2740,7 +2906,6 @@ class FrameController {
2740
2906
  this.appearanceObserver.stop();
2741
2907
  await this.element.loaded;
2742
2908
  this.hasBeenLoaded = true;
2743
- session.frameLoaded(this.element);
2744
2909
  }
2745
2910
  catch (error) {
2746
2911
  this.currentURL = previousURL;
@@ -2763,6 +2928,7 @@ class FrameController {
2763
2928
  await this.view.renderPromise;
2764
2929
  await this.view.render(renderer);
2765
2930
  session.frameRendered(fetchResponse, this.element);
2931
+ session.frameLoaded(this.element);
2766
2932
  }
2767
2933
  }
2768
2934
  catch (error) {
@@ -2794,20 +2960,15 @@ class FrameController {
2794
2960
  }
2795
2961
  this.reloadable = false;
2796
2962
  this.formSubmission = new FormSubmission(this, element, submitter);
2797
- if (this.formSubmission.fetchRequest.isIdempotent) {
2798
- this.navigateFrame(element, this.formSubmission.fetchRequest.url.href, submitter);
2799
- }
2800
- else {
2801
- const { fetchRequest } = this.formSubmission;
2802
- this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
2803
- this.formSubmission.start();
2804
- }
2963
+ const { fetchRequest } = this.formSubmission;
2964
+ this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
2965
+ this.formSubmission.start();
2805
2966
  }
2806
2967
  prepareHeadersForRequest(headers, request) {
2807
2968
  headers["Turbo-Frame"] = this.id;
2808
2969
  }
2809
2970
  requestStarted(request) {
2810
- this.element.setAttribute("busy", "");
2971
+ markAsBusy(this.element);
2811
2972
  }
2812
2973
  requestPreventedHandlingResponse(request, response) {
2813
2974
  this.resolveVisitPromise();
@@ -2825,14 +2986,14 @@ class FrameController {
2825
2986
  this.resolveVisitPromise();
2826
2987
  }
2827
2988
  requestFinished(request) {
2828
- this.element.removeAttribute("busy");
2989
+ clearBusyState(this.element);
2829
2990
  }
2830
- formSubmissionStarted(formSubmission) {
2831
- const frame = this.findFrameElement(formSubmission.formElement);
2832
- frame.setAttribute("busy", "");
2991
+ formSubmissionStarted({ formElement }) {
2992
+ markAsBusy(formElement, this.findFrameElement(formElement));
2833
2993
  }
2834
2994
  formSubmissionSucceededWithResponse(formSubmission, response) {
2835
2995
  const frame = this.findFrameElement(formSubmission.formElement, formSubmission.submitter);
2996
+ this.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
2836
2997
  frame.delegate.loadResponse(response);
2837
2998
  }
2838
2999
  formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
@@ -2841,9 +3002,8 @@ class FrameController {
2841
3002
  formSubmissionErrored(formSubmission, error) {
2842
3003
  console.error(error);
2843
3004
  }
2844
- formSubmissionFinished(formSubmission) {
2845
- const frame = this.findFrameElement(formSubmission.formElement);
2846
- frame.removeAttribute("busy");
3005
+ formSubmissionFinished({ formElement }) {
3006
+ clearBusyState(formElement, this.findFrameElement(formElement));
2847
3007
  }
2848
3008
  allowsImmediateRender(snapshot, resume) {
2849
3009
  return true;
@@ -2853,10 +3013,14 @@ class FrameController {
2853
3013
  viewInvalidated() {
2854
3014
  }
2855
3015
  async visit(url) {
2856
- 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;
2857
3020
  return new Promise(resolve => {
2858
3021
  this.resolveVisitPromise = () => {
2859
3022
  this.resolveVisitPromise = () => { };
3023
+ this.currentFetchRequest = null;
2860
3024
  resolve();
2861
3025
  };
2862
3026
  request.perform();
@@ -2864,12 +3028,29 @@ class FrameController {
2864
3028
  }
2865
3029
  navigateFrame(element, url, submitter) {
2866
3030
  const frame = this.findFrameElement(element, submitter);
3031
+ this.proposeVisitIfNavigatedWithAction(frame, element, submitter);
2867
3032
  frame.setAttribute("reloadable", "");
2868
3033
  frame.src = url;
2869
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
+ }
2870
3051
  findFrameElement(element, submitter) {
2871
3052
  var _a;
2872
- 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");
2873
3054
  return (_a = getFrameElementById(id)) !== null && _a !== void 0 ? _a : this.element;
2874
3055
  }
2875
3056
  async extractForeignFrameElement(container) {
@@ -2890,8 +3071,15 @@ class FrameController {
2890
3071
  }
2891
3072
  return new FrameElement();
2892
3073
  }
3074
+ formActionIsVisitable(form, submitter) {
3075
+ const action = getAction(form, submitter);
3076
+ return locationIsVisitable(expandURL(action), this.rootLocation);
3077
+ }
2893
3078
  shouldInterceptNavigation(element, submitter) {
2894
- 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
+ }
2895
3083
  if (!this.enabled || id == "_top") {
2896
3084
  return false;
2897
3085
  }
@@ -2948,6 +3136,28 @@ class FrameController {
2948
3136
  get isActive() {
2949
3137
  return this.element.isActive && this.connected;
2950
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
+ }
2951
3161
  }
2952
3162
  function getFrameElementById(id) {
2953
3163
  if (id != null) {
@@ -3138,4 +3348,4 @@ customElements.define("turbo-stream", StreamElement);
3138
3348
  window.Turbo = Turbo;
3139
3349
  start();
3140
3350
 
3141
- export { PageRenderer, PageSnapshot, clearCache, connectStreamSource, disconnectStreamSource, 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 };