@hotwired/turbo 7.0.0-rc.5 → 7.1.0-rc.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.0.0-rc.5
2
+ Turbo 7.1.0-rc.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
@@ -289,6 +356,29 @@ function uuid() {
289
356
  }
290
357
  }).join("");
291
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
+ }
292
382
 
293
383
  var FetchMethod;
294
384
  (function (FetchMethod) {
@@ -518,6 +608,9 @@ class FormSubmission {
518
608
  this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body, this.formElement);
519
609
  this.mustRedirect = mustRedirect;
520
610
  }
611
+ static confirmMethod(message, element) {
612
+ return confirm(message);
613
+ }
521
614
  get method() {
522
615
  var _a;
523
616
  const method = ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formmethod")) || this.formElement.getAttribute("method") || "";
@@ -551,8 +644,20 @@ class FormSubmission {
551
644
  return entries.concat(typeof value == "string" ? [[name, value]] : []);
552
645
  }, []);
553
646
  }
647
+ get confirmationMessage() {
648
+ return this.formElement.getAttribute("data-turbo-confirm");
649
+ }
650
+ get needsConfirmation() {
651
+ return this.confirmationMessage !== null;
652
+ }
554
653
  async start() {
555
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
+ }
556
661
  if (this.state == initialized) {
557
662
  this.state = requesting;
558
663
  return this.fetchRequest.perform();
@@ -576,7 +681,9 @@ class FormSubmission {
576
681
  }
577
682
  }
578
683
  requestStarted(request) {
684
+ var _a;
579
685
  this.state = FormSubmissionState.waiting;
686
+ (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.setAttribute("disabled", "");
580
687
  dispatch("turbo:submit-start", { target: this.formElement, detail: { formSubmission: this } });
581
688
  this.delegate.formSubmissionStarted(this);
582
689
  }
@@ -606,7 +713,9 @@ class FormSubmission {
606
713
  this.delegate.formSubmissionErrored(this, error);
607
714
  }
608
715
  requestFinished(request) {
716
+ var _a;
609
717
  this.state = FormSubmissionState.stopped;
718
+ (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.removeAttribute("disabled");
610
719
  dispatch("turbo:submit-end", { target: this.formElement, detail: Object.assign({ formSubmission: this }, this.result) });
611
720
  this.delegate.formSubmissionFinished(this);
612
721
  }
@@ -682,10 +791,11 @@ class Snapshot {
682
791
  class FormInterceptor {
683
792
  constructor(delegate, element) {
684
793
  this.submitBubbled = ((event) => {
685
- if (event.target instanceof HTMLFormElement) {
686
- const form = event.target;
794
+ const form = event.target;
795
+ if (form instanceof HTMLFormElement && form.closest("turbo-frame, html") == this.element) {
687
796
  const submitter = event.submitter || undefined;
688
- 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)) {
689
799
  event.preventDefault();
690
800
  event.stopImmediatePropagation();
691
801
  this.delegate.formSubmissionIntercepted(form, submitter);
@@ -900,10 +1010,11 @@ function createPlaceholderForPermanentElement(permanentElement) {
900
1010
  }
901
1011
 
902
1012
  class Renderer {
903
- constructor(currentSnapshot, newSnapshot, isPreview) {
1013
+ constructor(currentSnapshot, newSnapshot, isPreview, willRender = true) {
904
1014
  this.currentSnapshot = currentSnapshot;
905
1015
  this.newSnapshot = newSnapshot;
906
1016
  this.isPreview = isPreview;
1017
+ this.willRender = willRender;
907
1018
  this.promise = new Promise((resolve, reject) => this.resolvingFunctions = { resolve, reject });
908
1019
  }
909
1020
  get shouldRender() {
@@ -1279,7 +1390,9 @@ var VisitState;
1279
1390
  })(VisitState || (VisitState = {}));
1280
1391
  const defaultOptions = {
1281
1392
  action: "advance",
1282
- historyChanged: false
1393
+ historyChanged: false,
1394
+ visitCachedSnapshot: () => { },
1395
+ willRender: true,
1283
1396
  };
1284
1397
  var SystemStatusCode;
1285
1398
  (function (SystemStatusCode) {
@@ -1299,13 +1412,16 @@ class Visit {
1299
1412
  this.delegate = delegate;
1300
1413
  this.location = location;
1301
1414
  this.restorationIdentifier = restorationIdentifier || uuid();
1302
- const { action, historyChanged, referrer, snapshotHTML, response } = Object.assign(Object.assign({}, defaultOptions), options);
1415
+ const { action, historyChanged, referrer, snapshotHTML, response, visitCachedSnapshot, willRender } = Object.assign(Object.assign({}, defaultOptions), options);
1303
1416
  this.action = action;
1304
1417
  this.historyChanged = historyChanged;
1305
1418
  this.referrer = referrer;
1306
1419
  this.snapshotHTML = snapshotHTML;
1307
1420
  this.response = response;
1308
1421
  this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
1422
+ this.visitCachedSnapshot = visitCachedSnapshot;
1423
+ this.willRender = willRender;
1424
+ this.scrolled = !willRender;
1309
1425
  }
1310
1426
  get adapter() {
1311
1427
  return this.delegate.adapter;
@@ -1407,7 +1523,7 @@ class Visit {
1407
1523
  if (this.view.renderPromise)
1408
1524
  await this.view.renderPromise;
1409
1525
  if (isSuccessful(statusCode) && responseHTML != null) {
1410
- await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML));
1526
+ await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML), false, this.willRender);
1411
1527
  this.adapter.visitRendered(this);
1412
1528
  this.complete();
1413
1529
  }
@@ -1447,7 +1563,7 @@ class Visit {
1447
1563
  else {
1448
1564
  if (this.view.renderPromise)
1449
1565
  await this.view.renderPromise;
1450
- await this.view.renderPage(snapshot, isPreview);
1566
+ await this.view.renderPage(snapshot, isPreview, this.willRender);
1451
1567
  this.adapter.visitRendered(this);
1452
1568
  if (!isPreview) {
1453
1569
  this.complete();
@@ -1457,7 +1573,8 @@ class Visit {
1457
1573
  }
1458
1574
  }
1459
1575
  followRedirect() {
1460
- if (this.redirectedToLocation && !this.followedRedirect) {
1576
+ var _a;
1577
+ if (this.redirectedToLocation && !this.followedRedirect && ((_a = this.response) === null || _a === void 0 ? void 0 : _a.redirected)) {
1461
1578
  this.adapter.visitProposedToLocation(this.redirectedToLocation, {
1462
1579
  action: 'replace',
1463
1580
  response: this.response
@@ -1480,25 +1597,27 @@ class Visit {
1480
1597
  }
1481
1598
  async requestSucceededWithResponse(request, response) {
1482
1599
  const responseHTML = await response.responseHTML;
1600
+ const { redirected, statusCode } = response;
1483
1601
  if (responseHTML == undefined) {
1484
- this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch });
1602
+ this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch, redirected });
1485
1603
  }
1486
1604
  else {
1487
1605
  this.redirectedToLocation = response.redirected ? response.location : undefined;
1488
- this.recordResponse({ statusCode: response.statusCode, responseHTML });
1606
+ this.recordResponse({ statusCode: statusCode, responseHTML, redirected });
1489
1607
  }
1490
1608
  }
1491
1609
  async requestFailedWithResponse(request, response) {
1492
1610
  const responseHTML = await response.responseHTML;
1611
+ const { redirected, statusCode } = response;
1493
1612
  if (responseHTML == undefined) {
1494
- this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch });
1613
+ this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch, redirected });
1495
1614
  }
1496
1615
  else {
1497
- this.recordResponse({ statusCode: response.statusCode, responseHTML });
1616
+ this.recordResponse({ statusCode: statusCode, responseHTML, redirected });
1498
1617
  }
1499
1618
  }
1500
1619
  requestErrored(request, error) {
1501
- this.recordResponse({ statusCode: SystemStatusCode.networkFailure });
1620
+ this.recordResponse({ statusCode: SystemStatusCode.networkFailure, redirected: false });
1502
1621
  }
1503
1622
  requestFinished() {
1504
1623
  this.finishRequest();
@@ -1555,12 +1674,12 @@ class Visit {
1555
1674
  return !this.hasCachedSnapshot();
1556
1675
  }
1557
1676
  else {
1558
- return true;
1677
+ return this.willRender;
1559
1678
  }
1560
1679
  }
1561
1680
  cacheSnapshot() {
1562
1681
  if (!this.snapshotCached) {
1563
- this.view.cacheSnapshot();
1682
+ this.view.cacheSnapshot().then(snapshot => snapshot && this.visitCachedSnapshot(snapshot));
1564
1683
  this.snapshotCached = true;
1565
1684
  }
1566
1685
  }
@@ -1710,7 +1829,7 @@ class FormSubmitObserver {
1710
1829
  const form = event.target instanceof HTMLFormElement ? event.target : undefined;
1711
1830
  const submitter = event.submitter || undefined;
1712
1831
  if (form) {
1713
- 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");
1714
1833
  if (method != "dialog" && this.delegate.willSubmitForm(form, submitter)) {
1715
1834
  event.preventDefault();
1716
1835
  this.delegate.formSubmitted(form, submitter);
@@ -1754,12 +1873,11 @@ class FrameRedirector {
1754
1873
  linkClickIntercepted(element, url) {
1755
1874
  const frame = this.findFrameElement(element);
1756
1875
  if (frame) {
1757
- frame.setAttribute("reloadable", "");
1758
- frame.src = url;
1876
+ frame.delegate.linkClickIntercepted(element, url);
1759
1877
  }
1760
1878
  }
1761
1879
  shouldInterceptFormSubmission(element, submitter) {
1762
- return this.shouldRedirect(element, submitter);
1880
+ return this.shouldSubmit(element, submitter);
1763
1881
  }
1764
1882
  formSubmissionIntercepted(element, submitter) {
1765
1883
  const frame = this.findFrameElement(element, submitter);
@@ -1768,6 +1886,13 @@ class FrameRedirector {
1768
1886
  frame.delegate.formSubmissionIntercepted(element, submitter);
1769
1887
  }
1770
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
+ }
1771
1896
  shouldRedirect(element, submitter) {
1772
1897
  const frame = this.findFrameElement(element, submitter);
1773
1898
  return frame ? frame != element.closest("turbo-frame") : false;
@@ -1925,7 +2050,12 @@ class Navigator {
1925
2050
  }
1926
2051
  proposeVisit(location, options = {}) {
1927
2052
  if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
1928
- 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
+ }
1929
2059
  }
1930
2060
  }
1931
2061
  startVisit(locatable, restorationIdentifier, options = {}) {
@@ -1936,12 +2066,7 @@ class Navigator {
1936
2066
  submitForm(form, submitter) {
1937
2067
  this.stop();
1938
2068
  this.formSubmission = new FormSubmission(this, form, submitter, true);
1939
- if (this.formSubmission.isIdempotent) {
1940
- this.proposeVisit(this.formSubmission.fetchRequest.url, { action: this.getActionForFormSubmission(this.formSubmission) });
1941
- }
1942
- else {
1943
- this.formSubmission.start();
1944
- }
2069
+ this.formSubmission.start();
1945
2070
  }
1946
2071
  stop() {
1947
2072
  if (this.formSubmission) {
@@ -1974,8 +2099,9 @@ class Navigator {
1974
2099
  if (formSubmission.method != FetchMethod.get) {
1975
2100
  this.view.clearSnapshotCache();
1976
2101
  }
1977
- const { statusCode } = fetchResponse;
1978
- const visitOptions = { response: { statusCode, responseHTML } };
2102
+ const { statusCode, redirected } = fetchResponse;
2103
+ const action = this.getActionForFormSubmission(formSubmission);
2104
+ const visitOptions = { action, response: { statusCode, responseHTML, redirected } };
1979
2105
  this.proposeVisit(fetchResponse.location, visitOptions);
1980
2106
  }
1981
2107
  }
@@ -2027,7 +2153,7 @@ class Navigator {
2027
2153
  }
2028
2154
  getActionForFormSubmission(formSubmission) {
2029
2155
  const { formElement, submitter } = formSubmission;
2030
- const action = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("data-turbo-action")) || formElement.getAttribute("data-turbo-action");
2156
+ const action = getAttribute("data-turbo-action", submitter, formElement);
2031
2157
  return isAction(action) ? action : "advance";
2032
2158
  }
2033
2159
  }
@@ -2221,7 +2347,9 @@ class PageRenderer extends Renderer {
2221
2347
  this.mergeHead();
2222
2348
  }
2223
2349
  async render() {
2224
- this.replaceBody();
2350
+ if (this.willRender) {
2351
+ this.replaceBody();
2352
+ }
2225
2353
  }
2226
2354
  finishRendering() {
2227
2355
  super.finishRendering();
@@ -2359,8 +2487,8 @@ class PageView extends View {
2359
2487
  this.snapshotCache = new SnapshotCache(10);
2360
2488
  this.lastRenderedLocation = new URL(location.href);
2361
2489
  }
2362
- renderPage(snapshot, isPreview = false) {
2363
- const renderer = new PageRenderer(this.snapshot, snapshot, isPreview);
2490
+ renderPage(snapshot, isPreview = false, willRender = true) {
2491
+ const renderer = new PageRenderer(this.snapshot, snapshot, isPreview, willRender);
2364
2492
  return this.render(renderer);
2365
2493
  }
2366
2494
  renderError(snapshot) {
@@ -2375,7 +2503,9 @@ class PageView extends View {
2375
2503
  this.delegate.viewWillCacheSnapshot();
2376
2504
  const { snapshot, lastRenderedLocation: location } = this;
2377
2505
  await nextEventLoopTick();
2378
- this.snapshotCache.put(location, snapshot.clone());
2506
+ const cachedSnapshot = snapshot.clone();
2507
+ this.snapshotCache.put(location, cachedSnapshot);
2508
+ return cachedSnapshot;
2379
2509
  }
2380
2510
  }
2381
2511
  getCachedSnapshotForLocation(location) {
@@ -2477,7 +2607,7 @@ class Session {
2477
2607
  }
2478
2608
  willFollowLinkToLocation(link, location) {
2479
2609
  return this.elementDriveEnabled(link)
2480
- && this.locationIsVisitable(location)
2610
+ && locationIsVisitable(location, this.snapshot.rootLocation)
2481
2611
  && this.applicationAllowsFollowingLinkToLocation(link, location);
2482
2612
  }
2483
2613
  followedLinkToLocation(link, location) {
@@ -2485,14 +2615,24 @@ class Session {
2485
2615
  this.convertLinkWithMethodClickToFormSubmission(link) || this.visit(location.href, { action });
2486
2616
  }
2487
2617
  convertLinkWithMethodClickToFormSubmission(link) {
2488
- var _a;
2489
2618
  const linkMethod = link.getAttribute("data-turbo-method");
2490
2619
  if (linkMethod) {
2491
2620
  const form = document.createElement("form");
2492
2621
  form.method = linkMethod;
2493
2622
  form.action = link.getAttribute("href") || "undefined";
2494
2623
  form.hidden = true;
2495
- (_a = link.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(form, link);
2624
+ if (link.hasAttribute("data-turbo-confirm")) {
2625
+ form.setAttribute("data-turbo-confirm", link.getAttribute("data-turbo-confirm"));
2626
+ }
2627
+ const frame = this.getTargetFrameForLink(link);
2628
+ if (frame) {
2629
+ form.setAttribute("data-turbo-frame", frame);
2630
+ form.addEventListener("turbo:submit-start", () => form.remove());
2631
+ }
2632
+ else {
2633
+ form.addEventListener("submit", () => form.remove());
2634
+ }
2635
+ document.body.appendChild(form);
2496
2636
  return dispatch("submit", { cancelable: true, target: form });
2497
2637
  }
2498
2638
  else {
@@ -2522,7 +2662,10 @@ class Session {
2522
2662
  this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
2523
2663
  }
2524
2664
  willSubmitForm(form, submitter) {
2525
- return this.elementDriveEnabled(form) && this.elementDriveEnabled(submitter);
2665
+ const action = getAction(form, submitter);
2666
+ return this.elementDriveEnabled(form)
2667
+ && (!submitter || this.elementDriveEnabled(submitter))
2668
+ && locationIsVisitable(expandURL(action), this.snapshot.rootLocation);
2526
2669
  }
2527
2670
  formSubmitted(form, submitter) {
2528
2671
  this.navigator.submitForm(form, submitter);
@@ -2578,6 +2721,7 @@ class Session {
2578
2721
  return dispatch("turbo:before-visit", { detail: { url: location.href }, cancelable: true });
2579
2722
  }
2580
2723
  notifyApplicationAfterVisitingLocation(location, action) {
2724
+ markAsBusy(document.documentElement);
2581
2725
  return dispatch("turbo:visit", { detail: { url: location.href, action } });
2582
2726
  }
2583
2727
  notifyApplicationBeforeCachingSnapshot() {
@@ -2590,6 +2734,7 @@ class Session {
2590
2734
  return dispatch("turbo:render");
2591
2735
  }
2592
2736
  notifyApplicationAfterPageLoad(timing = {}) {
2737
+ clearBusyState(document.documentElement);
2593
2738
  return dispatch("turbo:load", { detail: { url: this.location.href, timing } });
2594
2739
  }
2595
2740
  notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL) {
@@ -2624,8 +2769,17 @@ class Session {
2624
2769
  const action = link.getAttribute("data-turbo-action");
2625
2770
  return isAction(action) ? action : "advance";
2626
2771
  }
2627
- locationIsVisitable(location) {
2628
- return isPrefixedBy(location, this.snapshot.rootLocation) && isHTML(location);
2772
+ getTargetFrameForLink(link) {
2773
+ const frame = link.getAttribute("data-turbo-frame");
2774
+ if (frame) {
2775
+ return frame;
2776
+ }
2777
+ else {
2778
+ const container = link.closest("turbo-frame");
2779
+ if (container) {
2780
+ return container.id;
2781
+ }
2782
+ }
2629
2783
  }
2630
2784
  get snapshot() {
2631
2785
  return this.view.snapshot;
@@ -2643,7 +2797,7 @@ const deprecatedLocationPropertyDescriptors = {
2643
2797
  };
2644
2798
 
2645
2799
  const session = new Session;
2646
- const { navigator } = session;
2800
+ const { navigator: navigator$1 } = session;
2647
2801
  function start() {
2648
2802
  session.start();
2649
2803
  }
@@ -2668,10 +2822,13 @@ function clearCache() {
2668
2822
  function setProgressBarDelay(delay) {
2669
2823
  session.setProgressBarDelay(delay);
2670
2824
  }
2825
+ function setConfirmMethod(confirmMethod) {
2826
+ FormSubmission.confirmMethod = confirmMethod;
2827
+ }
2671
2828
 
2672
2829
  var Turbo = /*#__PURE__*/Object.freeze({
2673
2830
  __proto__: null,
2674
- navigator: navigator,
2831
+ navigator: navigator$1,
2675
2832
  session: session,
2676
2833
  PageRenderer: PageRenderer,
2677
2834
  PageSnapshot: PageSnapshot,
@@ -2682,11 +2839,14 @@ var Turbo = /*#__PURE__*/Object.freeze({
2682
2839
  disconnectStreamSource: disconnectStreamSource,
2683
2840
  renderStreamMessage: renderStreamMessage,
2684
2841
  clearCache: clearCache,
2685
- setProgressBarDelay: setProgressBarDelay
2842
+ setProgressBarDelay: setProgressBarDelay,
2843
+ setConfirmMethod: setConfirmMethod
2686
2844
  });
2687
2845
 
2688
2846
  class FrameController {
2689
2847
  constructor(element) {
2848
+ this.fetchResponseLoaded = (fetchResponse) => { };
2849
+ this.currentFetchRequest = null;
2690
2850
  this.resolveVisitPromise = () => { };
2691
2851
  this.connected = false;
2692
2852
  this.hasBeenLoaded = false;
@@ -2746,7 +2906,6 @@ class FrameController {
2746
2906
  this.appearanceObserver.stop();
2747
2907
  await this.element.loaded;
2748
2908
  this.hasBeenLoaded = true;
2749
- session.frameLoaded(this.element);
2750
2909
  }
2751
2910
  catch (error) {
2752
2911
  this.currentURL = previousURL;
@@ -2756,7 +2915,7 @@ class FrameController {
2756
2915
  }
2757
2916
  }
2758
2917
  async loadResponse(fetchResponse) {
2759
- if (fetchResponse.redirected) {
2918
+ if (fetchResponse.redirected || (fetchResponse.succeeded && fetchResponse.isHTML)) {
2760
2919
  this.sourceURL = fetchResponse.response.url;
2761
2920
  }
2762
2921
  try {
@@ -2764,17 +2923,22 @@ class FrameController {
2764
2923
  if (html) {
2765
2924
  const { body } = parseHTMLDocument(html);
2766
2925
  const snapshot = new Snapshot(await this.extractForeignFrameElement(body));
2767
- const renderer = new FrameRenderer(this.view.snapshot, snapshot, false);
2926
+ const renderer = new FrameRenderer(this.view.snapshot, snapshot, false, false);
2768
2927
  if (this.view.renderPromise)
2769
2928
  await this.view.renderPromise;
2770
2929
  await this.view.render(renderer);
2771
2930
  session.frameRendered(fetchResponse, this.element);
2931
+ session.frameLoaded(this.element);
2932
+ this.fetchResponseLoaded(fetchResponse);
2772
2933
  }
2773
2934
  }
2774
2935
  catch (error) {
2775
2936
  console.error(error);
2776
2937
  this.view.invalidate();
2777
2938
  }
2939
+ finally {
2940
+ this.fetchResponseLoaded = () => { };
2941
+ }
2778
2942
  }
2779
2943
  elementAppearedInViewport(element) {
2780
2944
  this.loadSourceURL();
@@ -2800,20 +2964,15 @@ class FrameController {
2800
2964
  }
2801
2965
  this.reloadable = false;
2802
2966
  this.formSubmission = new FormSubmission(this, element, submitter);
2803
- if (this.formSubmission.fetchRequest.isIdempotent) {
2804
- this.navigateFrame(element, this.formSubmission.fetchRequest.url.href, submitter);
2805
- }
2806
- else {
2807
- const { fetchRequest } = this.formSubmission;
2808
- this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
2809
- this.formSubmission.start();
2810
- }
2967
+ const { fetchRequest } = this.formSubmission;
2968
+ this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
2969
+ this.formSubmission.start();
2811
2970
  }
2812
2971
  prepareHeadersForRequest(headers, request) {
2813
2972
  headers["Turbo-Frame"] = this.id;
2814
2973
  }
2815
2974
  requestStarted(request) {
2816
- this.element.setAttribute("busy", "");
2975
+ markAsBusy(this.element);
2817
2976
  }
2818
2977
  requestPreventedHandlingResponse(request, response) {
2819
2978
  this.resolveVisitPromise();
@@ -2831,14 +2990,14 @@ class FrameController {
2831
2990
  this.resolveVisitPromise();
2832
2991
  }
2833
2992
  requestFinished(request) {
2834
- this.element.removeAttribute("busy");
2993
+ clearBusyState(this.element);
2835
2994
  }
2836
- formSubmissionStarted(formSubmission) {
2837
- const frame = this.findFrameElement(formSubmission.formElement);
2838
- frame.setAttribute("busy", "");
2995
+ formSubmissionStarted({ formElement }) {
2996
+ markAsBusy(formElement, this.findFrameElement(formElement));
2839
2997
  }
2840
2998
  formSubmissionSucceededWithResponse(formSubmission, response) {
2841
2999
  const frame = this.findFrameElement(formSubmission.formElement, formSubmission.submitter);
3000
+ this.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
2842
3001
  frame.delegate.loadResponse(response);
2843
3002
  }
2844
3003
  formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
@@ -2847,9 +3006,8 @@ class FrameController {
2847
3006
  formSubmissionErrored(formSubmission, error) {
2848
3007
  console.error(error);
2849
3008
  }
2850
- formSubmissionFinished(formSubmission) {
2851
- const frame = this.findFrameElement(formSubmission.formElement);
2852
- frame.removeAttribute("busy");
3009
+ formSubmissionFinished({ formElement }) {
3010
+ clearBusyState(formElement, this.findFrameElement(formElement));
2853
3011
  }
2854
3012
  allowsImmediateRender(snapshot, resume) {
2855
3013
  return true;
@@ -2859,10 +3017,14 @@ class FrameController {
2859
3017
  viewInvalidated() {
2860
3018
  }
2861
3019
  async visit(url) {
2862
- const request = new FetchRequest(this, FetchMethod.get, expandURL(url), undefined, this.element);
3020
+ var _a;
3021
+ const request = new FetchRequest(this, FetchMethod.get, expandURL(url), new URLSearchParams, this.element);
3022
+ (_a = this.currentFetchRequest) === null || _a === void 0 ? void 0 : _a.cancel();
3023
+ this.currentFetchRequest = request;
2863
3024
  return new Promise(resolve => {
2864
3025
  this.resolveVisitPromise = () => {
2865
3026
  this.resolveVisitPromise = () => { };
3027
+ this.currentFetchRequest = null;
2866
3028
  resolve();
2867
3029
  };
2868
3030
  request.perform();
@@ -2870,12 +3032,27 @@ class FrameController {
2870
3032
  }
2871
3033
  navigateFrame(element, url, submitter) {
2872
3034
  const frame = this.findFrameElement(element, submitter);
3035
+ this.proposeVisitIfNavigatedWithAction(frame, element, submitter);
2873
3036
  frame.setAttribute("reloadable", "");
2874
3037
  frame.src = url;
2875
3038
  }
3039
+ proposeVisitIfNavigatedWithAction(frame, element, submitter) {
3040
+ const action = getAttribute("data-turbo-action", submitter, element, frame);
3041
+ if (isAction(action)) {
3042
+ const { visitCachedSnapshot } = new SnapshotSubstitution(frame);
3043
+ frame.delegate.fetchResponseLoaded = (fetchResponse) => {
3044
+ if (frame.src) {
3045
+ const { statusCode, redirected } = fetchResponse;
3046
+ const responseHTML = frame.ownerDocument.documentElement.outerHTML;
3047
+ const response = { statusCode, redirected, responseHTML };
3048
+ session.visit(frame.src, { action, response, visitCachedSnapshot, willRender: false });
3049
+ }
3050
+ };
3051
+ }
3052
+ }
2876
3053
  findFrameElement(element, submitter) {
2877
3054
  var _a;
2878
- const id = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("data-turbo-frame")) || element.getAttribute("data-turbo-frame") || this.element.getAttribute("target");
3055
+ const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
2879
3056
  return (_a = getFrameElementById(id)) !== null && _a !== void 0 ? _a : this.element;
2880
3057
  }
2881
3058
  async extractForeignFrameElement(container) {
@@ -2896,8 +3073,15 @@ class FrameController {
2896
3073
  }
2897
3074
  return new FrameElement();
2898
3075
  }
3076
+ formActionIsVisitable(form, submitter) {
3077
+ const action = getAction(form, submitter);
3078
+ return locationIsVisitable(expandURL(action), this.rootLocation);
3079
+ }
2899
3080
  shouldInterceptNavigation(element, submitter) {
2900
- const id = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("data-turbo-frame")) || element.getAttribute("data-turbo-frame") || this.element.getAttribute("target");
3081
+ const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
3082
+ if (element instanceof HTMLFormElement && !this.formActionIsVisitable(element, submitter)) {
3083
+ return false;
3084
+ }
2901
3085
  if (!this.enabled || id == "_top") {
2902
3086
  return false;
2903
3087
  }
@@ -2954,6 +3138,23 @@ class FrameController {
2954
3138
  get isActive() {
2955
3139
  return this.element.isActive && this.connected;
2956
3140
  }
3141
+ get rootLocation() {
3142
+ var _a;
3143
+ const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
3144
+ const root = (_a = meta === null || meta === void 0 ? void 0 : meta.content) !== null && _a !== void 0 ? _a : "/";
3145
+ return expandURL(root);
3146
+ }
3147
+ }
3148
+ class SnapshotSubstitution {
3149
+ constructor(element) {
3150
+ this.visitCachedSnapshot = ({ element }) => {
3151
+ var _a;
3152
+ const { id, clone } = this;
3153
+ (_a = element.querySelector("#" + id)) === null || _a === void 0 ? void 0 : _a.replaceWith(clone);
3154
+ };
3155
+ this.clone = element.cloneNode(true);
3156
+ this.id = element.id;
3157
+ }
2957
3158
  }
2958
3159
  function getFrameElementById(id) {
2959
3160
  if (id != null) {
@@ -2974,6 +3175,7 @@ function activateElement(element, currentURL) {
2974
3175
  }
2975
3176
  if (element instanceof FrameElement) {
2976
3177
  element.connectedCallback();
3178
+ element.disconnectedCallback();
2977
3179
  return element;
2978
3180
  }
2979
3181
  }
@@ -3144,4 +3346,4 @@ customElements.define("turbo-stream", StreamElement);
3144
3346
  window.Turbo = Turbo;
3145
3347
  start();
3146
3348
 
3147
- export { PageRenderer, PageSnapshot, clearCache, connectStreamSource, disconnectStreamSource, navigator, registerAdapter, renderStreamMessage, session, setProgressBarDelay, start, visit };
3349
+ export { PageRenderer, PageSnapshot, clearCache, connectStreamSource, disconnectStreamSource, navigator$1 as navigator, registerAdapter, renderStreamMessage, session, setConfirmMethod, setProgressBarDelay, start, visit };