@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.
@@ -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 (global, factory) {
@@ -26,6 +26,58 @@ Copyright © 2021 Basecamp, LLC
26
26
  Object.setPrototypeOf(HTMLElement, BuiltInHTMLElement);
27
27
  })();
28
28
 
29
+ /**
30
+ * The MIT License (MIT)
31
+ *
32
+ * Copyright (c) 2019 Javan Makhmali
33
+ *
34
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
35
+ * of this software and associated documentation files (the "Software"), to deal
36
+ * in the Software without restriction, including without limitation the rights
37
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
38
+ * copies of the Software, and to permit persons to whom the Software is
39
+ * furnished to do so, subject to the following conditions:
40
+ *
41
+ * The above copyright notice and this permission notice shall be included in
42
+ * all copies or substantial portions of the Software.
43
+ *
44
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
45
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
46
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
47
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
48
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
49
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
50
+ * THE SOFTWARE.
51
+ */
52
+
53
+ (function(prototype) {
54
+ if (typeof prototype.requestSubmit == "function") return
55
+
56
+ prototype.requestSubmit = function(submitter) {
57
+ if (submitter) {
58
+ validateSubmitter(submitter, this);
59
+ submitter.click();
60
+ } else {
61
+ submitter = document.createElement("input");
62
+ submitter.type = "submit";
63
+ submitter.hidden = true;
64
+ this.appendChild(submitter);
65
+ submitter.click();
66
+ this.removeChild(submitter);
67
+ }
68
+ };
69
+
70
+ function validateSubmitter(submitter, form) {
71
+ submitter instanceof HTMLElement || raise(TypeError, "parameter 1 is not of type 'HTMLElement'");
72
+ submitter.type == "submit" || raise(TypeError, "The specified element is not a submit button");
73
+ submitter.form == form || raise(DOMException, "The specified element is not owned by this form element", "NotFoundError");
74
+ }
75
+
76
+ function raise(errorConstructor, message, name) {
77
+ throw new errorConstructor("Failed to execute 'requestSubmit' on 'HTMLFormElement': " + message + ".", name)
78
+ }
79
+ })(HTMLFormElement.prototype);
80
+
29
81
  const submittersByForm = new WeakMap;
30
82
  function findSubmitterFromClickTarget(target) {
31
83
  const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
@@ -171,6 +223,10 @@ Copyright © 2021 Basecamp, LLC
171
223
  return anchorMatch[1];
172
224
  }
173
225
  }
226
+ function getAction(form, submitter) {
227
+ const action = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formaction")) || form.getAttribute("action") || form.action;
228
+ return expandURL(action);
229
+ }
174
230
  function getExtension(url) {
175
231
  return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || "";
176
232
  }
@@ -181,6 +237,9 @@ Copyright © 2021 Basecamp, LLC
181
237
  const prefix = getPrefix(url);
182
238
  return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix);
183
239
  }
240
+ function locationIsVisitable(location, rootLocation) {
241
+ return isPrefixedBy(location, rootLocation) && isHTML(location);
242
+ }
184
243
  function getRequestURL(url) {
185
244
  const anchor = getAnchor(url);
186
245
  return anchor != null
@@ -303,6 +362,29 @@ Copyright © 2021 Basecamp, LLC
303
362
  }
304
363
  }).join("");
305
364
  }
365
+ function getAttribute(attributeName, ...elements) {
366
+ for (const value of elements.map(element => element === null || element === void 0 ? void 0 : element.getAttribute(attributeName))) {
367
+ if (typeof value == "string")
368
+ return value;
369
+ }
370
+ return null;
371
+ }
372
+ function markAsBusy(...elements) {
373
+ for (const element of elements) {
374
+ if (element.localName == "turbo-frame") {
375
+ element.setAttribute("busy", "");
376
+ }
377
+ element.setAttribute("aria-busy", "true");
378
+ }
379
+ }
380
+ function clearBusyState(...elements) {
381
+ for (const element of elements) {
382
+ if (element.localName == "turbo-frame") {
383
+ element.removeAttribute("busy");
384
+ }
385
+ element.removeAttribute("aria-busy");
386
+ }
387
+ }
306
388
 
307
389
  var FetchMethod;
308
390
  (function (FetchMethod) {
@@ -532,6 +614,9 @@ Copyright © 2021 Basecamp, LLC
532
614
  this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body, this.formElement);
533
615
  this.mustRedirect = mustRedirect;
534
616
  }
617
+ static confirmMethod(message, element) {
618
+ return confirm(message);
619
+ }
535
620
  get method() {
536
621
  var _a;
537
622
  const method = ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formmethod")) || this.formElement.getAttribute("method") || "";
@@ -565,8 +650,20 @@ Copyright © 2021 Basecamp, LLC
565
650
  return entries.concat(typeof value == "string" ? [[name, value]] : []);
566
651
  }, []);
567
652
  }
653
+ get confirmationMessage() {
654
+ return this.formElement.getAttribute("data-turbo-confirm");
655
+ }
656
+ get needsConfirmation() {
657
+ return this.confirmationMessage !== null;
658
+ }
568
659
  async start() {
569
660
  const { initialized, requesting } = FormSubmissionState;
661
+ if (this.needsConfirmation) {
662
+ const answer = FormSubmission.confirmMethod(this.confirmationMessage, this.formElement);
663
+ if (!answer) {
664
+ return;
665
+ }
666
+ }
570
667
  if (this.state == initialized) {
571
668
  this.state = requesting;
572
669
  return this.fetchRequest.perform();
@@ -590,7 +687,9 @@ Copyright © 2021 Basecamp, LLC
590
687
  }
591
688
  }
592
689
  requestStarted(request) {
690
+ var _a;
593
691
  this.state = FormSubmissionState.waiting;
692
+ (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.setAttribute("disabled", "");
594
693
  dispatch("turbo:submit-start", { target: this.formElement, detail: { formSubmission: this } });
595
694
  this.delegate.formSubmissionStarted(this);
596
695
  }
@@ -620,7 +719,9 @@ Copyright © 2021 Basecamp, LLC
620
719
  this.delegate.formSubmissionErrored(this, error);
621
720
  }
622
721
  requestFinished(request) {
722
+ var _a;
623
723
  this.state = FormSubmissionState.stopped;
724
+ (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.removeAttribute("disabled");
624
725
  dispatch("turbo:submit-end", { target: this.formElement, detail: Object.assign({ formSubmission: this }, this.result) });
625
726
  this.delegate.formSubmissionFinished(this);
626
727
  }
@@ -699,7 +800,8 @@ Copyright © 2021 Basecamp, LLC
699
800
  const form = event.target;
700
801
  if (form instanceof HTMLFormElement && form.closest("turbo-frame, html") == this.element) {
701
802
  const submitter = event.submitter || undefined;
702
- if (this.delegate.shouldInterceptFormSubmission(form, submitter)) {
803
+ const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.method;
804
+ if (method != "dialog" && this.delegate.shouldInterceptFormSubmission(form, submitter)) {
703
805
  event.preventDefault();
704
806
  event.stopImmediatePropagation();
705
807
  this.delegate.formSubmissionIntercepted(form, submitter);
@@ -1293,7 +1395,8 @@ Copyright © 2021 Basecamp, LLC
1293
1395
  })(VisitState || (VisitState = {}));
1294
1396
  const defaultOptions = {
1295
1397
  action: "advance",
1296
- historyChanged: false
1398
+ delegate: {},
1399
+ historyChanged: false,
1297
1400
  };
1298
1401
  var SystemStatusCode;
1299
1402
  (function (SystemStatusCode) {
@@ -1313,13 +1416,14 @@ Copyright © 2021 Basecamp, LLC
1313
1416
  this.delegate = delegate;
1314
1417
  this.location = location;
1315
1418
  this.restorationIdentifier = restorationIdentifier || uuid();
1316
- const { action, historyChanged, referrer, snapshotHTML, response } = Object.assign(Object.assign({}, defaultOptions), options);
1419
+ const { action, historyChanged, referrer, snapshotHTML, response, delegate: optionalDelegate } = Object.assign(Object.assign({}, defaultOptions), options);
1317
1420
  this.action = action;
1318
1421
  this.historyChanged = historyChanged;
1319
1422
  this.referrer = referrer;
1320
1423
  this.snapshotHTML = snapshotHTML;
1321
1424
  this.response = response;
1322
1425
  this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
1426
+ this.optionalDelegate = optionalDelegate;
1323
1427
  }
1324
1428
  get adapter() {
1325
1429
  return this.delegate.adapter;
@@ -1342,6 +1446,8 @@ Copyright © 2021 Basecamp, LLC
1342
1446
  this.state = VisitState.started;
1343
1447
  this.adapter.visitStarted(this);
1344
1448
  this.delegate.visitStarted(this);
1449
+ if (this.optionalDelegate.visitStarted)
1450
+ this.optionalDelegate.visitStarted(this);
1345
1451
  }
1346
1452
  }
1347
1453
  cancel() {
@@ -1471,7 +1577,8 @@ Copyright © 2021 Basecamp, LLC
1471
1577
  }
1472
1578
  }
1473
1579
  followRedirect() {
1474
- if (this.redirectedToLocation && !this.followedRedirect) {
1580
+ var _a;
1581
+ if (this.redirectedToLocation && !this.followedRedirect && ((_a = this.response) === null || _a === void 0 ? void 0 : _a.redirected)) {
1475
1582
  this.adapter.visitProposedToLocation(this.redirectedToLocation, {
1476
1583
  action: 'replace',
1477
1584
  response: this.response
@@ -1494,25 +1601,27 @@ Copyright © 2021 Basecamp, LLC
1494
1601
  }
1495
1602
  async requestSucceededWithResponse(request, response) {
1496
1603
  const responseHTML = await response.responseHTML;
1604
+ const { redirected, statusCode } = response;
1497
1605
  if (responseHTML == undefined) {
1498
- this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch });
1606
+ this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch, redirected });
1499
1607
  }
1500
1608
  else {
1501
1609
  this.redirectedToLocation = response.redirected ? response.location : undefined;
1502
- this.recordResponse({ statusCode: response.statusCode, responseHTML });
1610
+ this.recordResponse({ statusCode: statusCode, responseHTML, redirected });
1503
1611
  }
1504
1612
  }
1505
1613
  async requestFailedWithResponse(request, response) {
1506
1614
  const responseHTML = await response.responseHTML;
1615
+ const { redirected, statusCode } = response;
1507
1616
  if (responseHTML == undefined) {
1508
- this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch });
1617
+ this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch, redirected });
1509
1618
  }
1510
1619
  else {
1511
- this.recordResponse({ statusCode: response.statusCode, responseHTML });
1620
+ this.recordResponse({ statusCode: statusCode, responseHTML, redirected });
1512
1621
  }
1513
1622
  }
1514
1623
  requestErrored(request, error) {
1515
- this.recordResponse({ statusCode: SystemStatusCode.networkFailure });
1624
+ this.recordResponse({ statusCode: SystemStatusCode.networkFailure, redirected: false });
1516
1625
  }
1517
1626
  requestFinished() {
1518
1627
  this.finishRequest();
@@ -1576,6 +1685,8 @@ Copyright © 2021 Basecamp, LLC
1576
1685
  if (!this.snapshotCached) {
1577
1686
  this.view.cacheSnapshot();
1578
1687
  this.snapshotCached = true;
1688
+ if (this.optionalDelegate.visitCachedSnapshot)
1689
+ this.optionalDelegate.visitCachedSnapshot(this);
1579
1690
  }
1580
1691
  }
1581
1692
  async render(callback) {
@@ -1724,7 +1835,7 @@ Copyright © 2021 Basecamp, LLC
1724
1835
  const form = event.target instanceof HTMLFormElement ? event.target : undefined;
1725
1836
  const submitter = event.submitter || undefined;
1726
1837
  if (form) {
1727
- const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.method;
1838
+ const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.getAttribute("method");
1728
1839
  if (method != "dialog" && this.delegate.willSubmitForm(form, submitter)) {
1729
1840
  event.preventDefault();
1730
1841
  this.delegate.formSubmitted(form, submitter);
@@ -1768,12 +1879,11 @@ Copyright © 2021 Basecamp, LLC
1768
1879
  linkClickIntercepted(element, url) {
1769
1880
  const frame = this.findFrameElement(element);
1770
1881
  if (frame) {
1771
- frame.setAttribute("reloadable", "");
1772
- frame.src = url;
1882
+ frame.delegate.linkClickIntercepted(element, url);
1773
1883
  }
1774
1884
  }
1775
1885
  shouldInterceptFormSubmission(element, submitter) {
1776
- return this.shouldRedirect(element, submitter);
1886
+ return this.shouldSubmit(element, submitter);
1777
1887
  }
1778
1888
  formSubmissionIntercepted(element, submitter) {
1779
1889
  const frame = this.findFrameElement(element, submitter);
@@ -1782,6 +1892,13 @@ Copyright © 2021 Basecamp, LLC
1782
1892
  frame.delegate.formSubmissionIntercepted(element, submitter);
1783
1893
  }
1784
1894
  }
1895
+ shouldSubmit(form, submitter) {
1896
+ var _a;
1897
+ const action = getAction(form, submitter);
1898
+ const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
1899
+ const rootLocation = expandURL((_a = meta === null || meta === void 0 ? void 0 : meta.content) !== null && _a !== void 0 ? _a : "/");
1900
+ return this.shouldRedirect(form, submitter) && locationIsVisitable(action, rootLocation);
1901
+ }
1785
1902
  shouldRedirect(element, submitter) {
1786
1903
  const frame = this.findFrameElement(element, submitter);
1787
1904
  return frame ? frame != element.closest("turbo-frame") : false;
@@ -1939,7 +2056,12 @@ Copyright © 2021 Basecamp, LLC
1939
2056
  }
1940
2057
  proposeVisit(location, options = {}) {
1941
2058
  if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
1942
- this.delegate.visitProposedToLocation(location, options);
2059
+ if (locationIsVisitable(location, this.view.snapshot.rootLocation)) {
2060
+ this.delegate.visitProposedToLocation(location, options);
2061
+ }
2062
+ else {
2063
+ window.location.href = location.toString();
2064
+ }
1943
2065
  }
1944
2066
  }
1945
2067
  startVisit(locatable, restorationIdentifier, options = {}) {
@@ -1950,12 +2072,7 @@ Copyright © 2021 Basecamp, LLC
1950
2072
  submitForm(form, submitter) {
1951
2073
  this.stop();
1952
2074
  this.formSubmission = new FormSubmission(this, form, submitter, true);
1953
- if (this.formSubmission.isIdempotent) {
1954
- this.proposeVisit(this.formSubmission.fetchRequest.url, { action: this.getActionForFormSubmission(this.formSubmission) });
1955
- }
1956
- else {
1957
- this.formSubmission.start();
1958
- }
2075
+ this.formSubmission.start();
1959
2076
  }
1960
2077
  stop() {
1961
2078
  if (this.formSubmission) {
@@ -1988,8 +2105,9 @@ Copyright © 2021 Basecamp, LLC
1988
2105
  if (formSubmission.method != FetchMethod.get) {
1989
2106
  this.view.clearSnapshotCache();
1990
2107
  }
1991
- const { statusCode } = fetchResponse;
1992
- const visitOptions = { response: { statusCode, responseHTML } };
2108
+ const { statusCode, redirected } = fetchResponse;
2109
+ const action = this.getActionForFormSubmission(formSubmission);
2110
+ const visitOptions = { action, response: { statusCode, responseHTML, redirected } };
1993
2111
  this.proposeVisit(fetchResponse.location, visitOptions);
1994
2112
  }
1995
2113
  }
@@ -2022,6 +2140,9 @@ Copyright © 2021 Basecamp, LLC
2022
2140
  visitCompleted(visit) {
2023
2141
  this.delegate.visitCompleted(visit);
2024
2142
  }
2143
+ visitCachedSnapshot(visit) {
2144
+ this.delegate.visitCachedSnapshot(visit);
2145
+ }
2025
2146
  locationWithActionIsSamePage(location, action) {
2026
2147
  const anchor = getAnchor(location);
2027
2148
  const currentAnchor = getAnchor(this.view.lastRenderedLocation);
@@ -2041,7 +2162,7 @@ Copyright © 2021 Basecamp, LLC
2041
2162
  }
2042
2163
  getActionForFormSubmission(formSubmission) {
2043
2164
  const { formElement, submitter } = formSubmission;
2044
- const action = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("data-turbo-action")) || formElement.getAttribute("data-turbo-action");
2165
+ const action = getAttribute("data-turbo-action", submitter, formElement);
2045
2166
  return isAction(action) ? action : "advance";
2046
2167
  }
2047
2168
  }
@@ -2491,7 +2612,7 @@ Copyright © 2021 Basecamp, LLC
2491
2612
  }
2492
2613
  willFollowLinkToLocation(link, location) {
2493
2614
  return this.elementDriveEnabled(link)
2494
- && this.locationIsVisitable(location)
2615
+ && locationIsVisitable(location, this.snapshot.rootLocation)
2495
2616
  && this.applicationAllowsFollowingLinkToLocation(link, location);
2496
2617
  }
2497
2618
  followedLinkToLocation(link, location) {
@@ -2499,14 +2620,24 @@ Copyright © 2021 Basecamp, LLC
2499
2620
  this.convertLinkWithMethodClickToFormSubmission(link) || this.visit(location.href, { action });
2500
2621
  }
2501
2622
  convertLinkWithMethodClickToFormSubmission(link) {
2502
- var _a;
2503
2623
  const linkMethod = link.getAttribute("data-turbo-method");
2504
2624
  if (linkMethod) {
2505
2625
  const form = document.createElement("form");
2506
2626
  form.method = linkMethod;
2507
2627
  form.action = link.getAttribute("href") || "undefined";
2508
2628
  form.hidden = true;
2509
- (_a = link.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(form, link);
2629
+ if (link.hasAttribute("data-turbo-confirm")) {
2630
+ form.setAttribute("data-turbo-confirm", link.getAttribute("data-turbo-confirm"));
2631
+ }
2632
+ const frame = this.getTargetFrameForLink(link);
2633
+ if (frame) {
2634
+ form.setAttribute("data-turbo-frame", frame);
2635
+ form.addEventListener("turbo:submit-start", () => form.remove());
2636
+ }
2637
+ else {
2638
+ form.addEventListener("submit", () => form.remove());
2639
+ }
2640
+ document.body.appendChild(form);
2510
2641
  return dispatch("submit", { cancelable: true, target: form });
2511
2642
  }
2512
2643
  else {
@@ -2529,6 +2660,8 @@ Copyright © 2021 Basecamp, LLC
2529
2660
  visitCompleted(visit) {
2530
2661
  this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
2531
2662
  }
2663
+ visitCachedSnapshot(visit) {
2664
+ }
2532
2665
  locationWithActionIsSamePage(location, action) {
2533
2666
  return this.navigator.locationWithActionIsSamePage(location, action);
2534
2667
  }
@@ -2536,7 +2669,10 @@ Copyright © 2021 Basecamp, LLC
2536
2669
  this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
2537
2670
  }
2538
2671
  willSubmitForm(form, submitter) {
2539
- return this.elementDriveEnabled(form) && (!submitter || this.elementDriveEnabled(submitter));
2672
+ const action = getAction(form, submitter);
2673
+ return this.elementDriveEnabled(form)
2674
+ && (!submitter || this.elementDriveEnabled(submitter))
2675
+ && locationIsVisitable(expandURL(action), this.snapshot.rootLocation);
2540
2676
  }
2541
2677
  formSubmitted(form, submitter) {
2542
2678
  this.navigator.submitForm(form, submitter);
@@ -2592,6 +2728,7 @@ Copyright © 2021 Basecamp, LLC
2592
2728
  return dispatch("turbo:before-visit", { detail: { url: location.href }, cancelable: true });
2593
2729
  }
2594
2730
  notifyApplicationAfterVisitingLocation(location, action) {
2731
+ markAsBusy(document.documentElement);
2595
2732
  return dispatch("turbo:visit", { detail: { url: location.href, action } });
2596
2733
  }
2597
2734
  notifyApplicationBeforeCachingSnapshot() {
@@ -2604,6 +2741,7 @@ Copyright © 2021 Basecamp, LLC
2604
2741
  return dispatch("turbo:render");
2605
2742
  }
2606
2743
  notifyApplicationAfterPageLoad(timing = {}) {
2744
+ clearBusyState(document.documentElement);
2607
2745
  return dispatch("turbo:load", { detail: { url: this.location.href, timing } });
2608
2746
  }
2609
2747
  notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL) {
@@ -2638,8 +2776,17 @@ Copyright © 2021 Basecamp, LLC
2638
2776
  const action = link.getAttribute("data-turbo-action");
2639
2777
  return isAction(action) ? action : "advance";
2640
2778
  }
2641
- locationIsVisitable(location) {
2642
- return isPrefixedBy(location, this.snapshot.rootLocation) && isHTML(location);
2779
+ getTargetFrameForLink(link) {
2780
+ const frame = link.getAttribute("data-turbo-frame");
2781
+ if (frame) {
2782
+ return frame;
2783
+ }
2784
+ else {
2785
+ const container = link.closest("turbo-frame");
2786
+ if (container) {
2787
+ return container.id;
2788
+ }
2789
+ }
2643
2790
  }
2644
2791
  get snapshot() {
2645
2792
  return this.view.snapshot;
@@ -2682,6 +2829,9 @@ Copyright © 2021 Basecamp, LLC
2682
2829
  function setProgressBarDelay(delay) {
2683
2830
  session.setProgressBarDelay(delay);
2684
2831
  }
2832
+ function setConfirmMethod(confirmMethod) {
2833
+ FormSubmission.confirmMethod = confirmMethod;
2834
+ }
2685
2835
 
2686
2836
  var Turbo = /*#__PURE__*/Object.freeze({
2687
2837
  __proto__: null,
@@ -2696,11 +2846,13 @@ Copyright © 2021 Basecamp, LLC
2696
2846
  disconnectStreamSource: disconnectStreamSource,
2697
2847
  renderStreamMessage: renderStreamMessage,
2698
2848
  clearCache: clearCache,
2699
- setProgressBarDelay: setProgressBarDelay
2849
+ setProgressBarDelay: setProgressBarDelay,
2850
+ setConfirmMethod: setConfirmMethod
2700
2851
  });
2701
2852
 
2702
2853
  class FrameController {
2703
2854
  constructor(element) {
2855
+ this.currentFetchRequest = null;
2704
2856
  this.resolveVisitPromise = () => { };
2705
2857
  this.connected = false;
2706
2858
  this.hasBeenLoaded = false;
@@ -2760,7 +2912,6 @@ Copyright © 2021 Basecamp, LLC
2760
2912
  this.appearanceObserver.stop();
2761
2913
  await this.element.loaded;
2762
2914
  this.hasBeenLoaded = true;
2763
- session.frameLoaded(this.element);
2764
2915
  }
2765
2916
  catch (error) {
2766
2917
  this.currentURL = previousURL;
@@ -2783,6 +2934,7 @@ Copyright © 2021 Basecamp, LLC
2783
2934
  await this.view.renderPromise;
2784
2935
  await this.view.render(renderer);
2785
2936
  session.frameRendered(fetchResponse, this.element);
2937
+ session.frameLoaded(this.element);
2786
2938
  }
2787
2939
  }
2788
2940
  catch (error) {
@@ -2814,20 +2966,15 @@ Copyright © 2021 Basecamp, LLC
2814
2966
  }
2815
2967
  this.reloadable = false;
2816
2968
  this.formSubmission = new FormSubmission(this, element, submitter);
2817
- if (this.formSubmission.fetchRequest.isIdempotent) {
2818
- this.navigateFrame(element, this.formSubmission.fetchRequest.url.href, submitter);
2819
- }
2820
- else {
2821
- const { fetchRequest } = this.formSubmission;
2822
- this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
2823
- this.formSubmission.start();
2824
- }
2969
+ const { fetchRequest } = this.formSubmission;
2970
+ this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
2971
+ this.formSubmission.start();
2825
2972
  }
2826
2973
  prepareHeadersForRequest(headers, request) {
2827
2974
  headers["Turbo-Frame"] = this.id;
2828
2975
  }
2829
2976
  requestStarted(request) {
2830
- this.element.setAttribute("busy", "");
2977
+ markAsBusy(this.element);
2831
2978
  }
2832
2979
  requestPreventedHandlingResponse(request, response) {
2833
2980
  this.resolveVisitPromise();
@@ -2845,14 +2992,14 @@ Copyright © 2021 Basecamp, LLC
2845
2992
  this.resolveVisitPromise();
2846
2993
  }
2847
2994
  requestFinished(request) {
2848
- this.element.removeAttribute("busy");
2995
+ clearBusyState(this.element);
2849
2996
  }
2850
- formSubmissionStarted(formSubmission) {
2851
- const frame = this.findFrameElement(formSubmission.formElement);
2852
- frame.setAttribute("busy", "");
2997
+ formSubmissionStarted({ formElement }) {
2998
+ markAsBusy(formElement, this.findFrameElement(formElement));
2853
2999
  }
2854
3000
  formSubmissionSucceededWithResponse(formSubmission, response) {
2855
3001
  const frame = this.findFrameElement(formSubmission.formElement, formSubmission.submitter);
3002
+ this.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
2856
3003
  frame.delegate.loadResponse(response);
2857
3004
  }
2858
3005
  formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
@@ -2861,9 +3008,8 @@ Copyright © 2021 Basecamp, LLC
2861
3008
  formSubmissionErrored(formSubmission, error) {
2862
3009
  console.error(error);
2863
3010
  }
2864
- formSubmissionFinished(formSubmission) {
2865
- const frame = this.findFrameElement(formSubmission.formElement);
2866
- frame.removeAttribute("busy");
3011
+ formSubmissionFinished({ formElement }) {
3012
+ clearBusyState(formElement, this.findFrameElement(formElement));
2867
3013
  }
2868
3014
  allowsImmediateRender(snapshot, resume) {
2869
3015
  return true;
@@ -2873,10 +3019,14 @@ Copyright © 2021 Basecamp, LLC
2873
3019
  viewInvalidated() {
2874
3020
  }
2875
3021
  async visit(url) {
2876
- const request = new FetchRequest(this, FetchMethod.get, expandURL(url), undefined, this.element);
3022
+ var _a;
3023
+ const request = new FetchRequest(this, FetchMethod.get, expandURL(url), new URLSearchParams, this.element);
3024
+ (_a = this.currentFetchRequest) === null || _a === void 0 ? void 0 : _a.cancel();
3025
+ this.currentFetchRequest = request;
2877
3026
  return new Promise(resolve => {
2878
3027
  this.resolveVisitPromise = () => {
2879
3028
  this.resolveVisitPromise = () => { };
3029
+ this.currentFetchRequest = null;
2880
3030
  resolve();
2881
3031
  };
2882
3032
  request.perform();
@@ -2884,12 +3034,29 @@ Copyright © 2021 Basecamp, LLC
2884
3034
  }
2885
3035
  navigateFrame(element, url, submitter) {
2886
3036
  const frame = this.findFrameElement(element, submitter);
3037
+ this.proposeVisitIfNavigatedWithAction(frame, element, submitter);
2887
3038
  frame.setAttribute("reloadable", "");
2888
3039
  frame.src = url;
2889
3040
  }
3041
+ proposeVisitIfNavigatedWithAction(frame, element, submitter) {
3042
+ const action = getAttribute("data-turbo-action", submitter, element, frame);
3043
+ if (isAction(action)) {
3044
+ const delegate = new SnapshotSubstitution(frame);
3045
+ const proposeVisit = (event) => {
3046
+ const { target, detail: { fetchResponse } } = event;
3047
+ if (target instanceof FrameElement && target.src) {
3048
+ const { statusCode, redirected } = fetchResponse;
3049
+ const responseHTML = target.ownerDocument.documentElement.outerHTML;
3050
+ const response = { statusCode, redirected, responseHTML };
3051
+ session.visit(target.src, { action, response, delegate });
3052
+ }
3053
+ };
3054
+ frame.addEventListener("turbo:frame-render", proposeVisit, { once: true });
3055
+ }
3056
+ }
2890
3057
  findFrameElement(element, submitter) {
2891
3058
  var _a;
2892
- const id = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("data-turbo-frame")) || element.getAttribute("data-turbo-frame") || this.element.getAttribute("target");
3059
+ const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
2893
3060
  return (_a = getFrameElementById(id)) !== null && _a !== void 0 ? _a : this.element;
2894
3061
  }
2895
3062
  async extractForeignFrameElement(container) {
@@ -2910,8 +3077,15 @@ Copyright © 2021 Basecamp, LLC
2910
3077
  }
2911
3078
  return new FrameElement();
2912
3079
  }
3080
+ formActionIsVisitable(form, submitter) {
3081
+ const action = getAction(form, submitter);
3082
+ return locationIsVisitable(expandURL(action), this.rootLocation);
3083
+ }
2913
3084
  shouldInterceptNavigation(element, submitter) {
2914
- const id = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("data-turbo-frame")) || element.getAttribute("data-turbo-frame") || this.element.getAttribute("target");
3085
+ const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
3086
+ if (element instanceof HTMLFormElement && !this.formActionIsVisitable(element, submitter)) {
3087
+ return false;
3088
+ }
2915
3089
  if (!this.enabled || id == "_top") {
2916
3090
  return false;
2917
3091
  }
@@ -2968,6 +3142,28 @@ Copyright © 2021 Basecamp, LLC
2968
3142
  get isActive() {
2969
3143
  return this.element.isActive && this.connected;
2970
3144
  }
3145
+ get rootLocation() {
3146
+ var _a;
3147
+ const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
3148
+ const root = (_a = meta === null || meta === void 0 ? void 0 : meta.content) !== null && _a !== void 0 ? _a : "/";
3149
+ return expandURL(root);
3150
+ }
3151
+ }
3152
+ class SnapshotSubstitution {
3153
+ constructor(element) {
3154
+ this.clone = element.cloneNode(true);
3155
+ this.id = element.id;
3156
+ }
3157
+ visitStarted(visit) {
3158
+ this.snapshot = visit.view.snapshot;
3159
+ }
3160
+ visitCachedSnapshot() {
3161
+ var _a;
3162
+ const { snapshot, id, clone } = this;
3163
+ if (snapshot) {
3164
+ (_a = snapshot.element.querySelector("#" + id)) === null || _a === void 0 ? void 0 : _a.replaceWith(clone);
3165
+ }
3166
+ }
2971
3167
  }
2972
3168
  function getFrameElementById(id) {
2973
3169
  if (id != null) {
@@ -3167,6 +3363,7 @@ Copyright © 2021 Basecamp, LLC
3167
3363
  exports.registerAdapter = registerAdapter;
3168
3364
  exports.renderStreamMessage = renderStreamMessage;
3169
3365
  exports.session = session;
3366
+ exports.setConfirmMethod = setConfirmMethod;
3170
3367
  exports.setProgressBarDelay = setProgressBarDelay;
3171
3368
  exports.start = start;
3172
3369
  exports.visit = visit;
@@ -36,6 +36,7 @@ export declare class FormSubmission {
36
36
  readonly mustRedirect: boolean;
37
37
  state: FormSubmissionState;
38
38
  result?: FormSubmissionResult;
39
+ static confirmMethod(message: string, element: HTMLFormElement): boolean;
39
40
  constructor(delegate: FormSubmissionDelegate, formElement: HTMLFormElement, submitter?: HTMLElement, mustRedirect?: boolean);
40
41
  get method(): FetchMethod;
41
42
  get action(): string;
@@ -44,6 +45,8 @@ export declare class FormSubmission {
44
45
  get enctype(): FormEnctype;
45
46
  get isIdempotent(): boolean;
46
47
  get stringFormData(): [string, string][];
48
+ get confirmationMessage(): string | null;
49
+ get needsConfirmation(): boolean;
47
50
  start(): Promise<void | FetchResponse>;
48
51
  stop(): true | undefined;
49
52
  prepareHeadersForRequest(headers: FetchRequestHeaders, request: FetchRequest): void;
@@ -27,6 +27,7 @@ export declare class Navigator {
27
27
  formSubmissionFinished(formSubmission: FormSubmission): void;
28
28
  visitStarted(visit: Visit): void;
29
29
  visitCompleted(visit: Visit): void;
30
+ visitCachedSnapshot(visit: Visit): void;
30
31
  locationWithActionIsSamePage(location: URL, action?: Action): boolean;
31
32
  visitScrolledToSamePageLocation(oldURL: URL, newURL: URL): void;
32
33
  get location(): URL;