@hotwired/turbo 7.0.0 → 7.1.0-rc.3

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.1.0-rc.3
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) {
@@ -314,13 +404,10 @@ class FetchRequest {
314
404
  this.delegate = delegate;
315
405
  this.method = method;
316
406
  this.headers = this.defaultHeaders;
317
- if (this.isIdempotent) {
318
- this.url = mergeFormDataEntries(location, [...body.entries()]);
319
- }
320
- else {
321
- this.body = body;
322
- this.url = location;
323
- }
407
+ this.body = body;
408
+ this.url = this.isIdempotent ?
409
+ mergeFormDataEntries(new URL(location.href), this.entries) :
410
+ location;
324
411
  this.target = target;
325
412
  }
326
413
  get location() {
@@ -376,7 +463,7 @@ class FetchRequest {
376
463
  credentials: "same-origin",
377
464
  headers: this.headers,
378
465
  redirect: "follow",
379
- body: this.body,
466
+ body: this.isIdempotent ? null : this.body,
380
467
  signal: this.abortSignal,
381
468
  referrer: (_a = this.delegate.referrer) === null || _a === void 0 ? void 0 : _a.href
382
469
  };
@@ -398,7 +485,7 @@ class FetchRequest {
398
485
  cancelable: true,
399
486
  detail: {
400
487
  fetchOptions,
401
- url: this.url.href,
488
+ url: this.url,
402
489
  resume: this.resolveRequestPromise
403
490
  },
404
491
  target: this.target
@@ -408,18 +495,13 @@ class FetchRequest {
408
495
  }
409
496
  }
410
497
  function mergeFormDataEntries(url, entries) {
411
- const currentSearchParams = new URLSearchParams(url.search);
498
+ const searchParams = new URLSearchParams;
412
499
  for (const [name, value] of entries) {
413
500
  if (value instanceof File)
414
501
  continue;
415
- if (currentSearchParams.has(name)) {
416
- currentSearchParams.delete(name);
417
- url.searchParams.set(name, value);
418
- }
419
- else {
420
- url.searchParams.append(name, value);
421
- }
502
+ searchParams.append(name, value);
422
503
  }
504
+ url.search = searchParams.toString();
423
505
  return url;
424
506
  }
425
507
 
@@ -518,6 +600,9 @@ class FormSubmission {
518
600
  this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body, this.formElement);
519
601
  this.mustRedirect = mustRedirect;
520
602
  }
603
+ static confirmMethod(message, element) {
604
+ return confirm(message);
605
+ }
521
606
  get method() {
522
607
  var _a;
523
608
  const method = ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formmethod")) || this.formElement.getAttribute("method") || "";
@@ -551,8 +636,20 @@ class FormSubmission {
551
636
  return entries.concat(typeof value == "string" ? [[name, value]] : []);
552
637
  }, []);
553
638
  }
639
+ get confirmationMessage() {
640
+ return this.formElement.getAttribute("data-turbo-confirm");
641
+ }
642
+ get needsConfirmation() {
643
+ return this.confirmationMessage !== null;
644
+ }
554
645
  async start() {
555
646
  const { initialized, requesting } = FormSubmissionState;
647
+ if (this.needsConfirmation) {
648
+ const answer = FormSubmission.confirmMethod(this.confirmationMessage, this.formElement);
649
+ if (!answer) {
650
+ return;
651
+ }
652
+ }
556
653
  if (this.state == initialized) {
557
654
  this.state = requesting;
558
655
  return this.fetchRequest.perform();
@@ -576,7 +673,9 @@ class FormSubmission {
576
673
  }
577
674
  }
578
675
  requestStarted(request) {
676
+ var _a;
579
677
  this.state = FormSubmissionState.waiting;
678
+ (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.setAttribute("disabled", "");
580
679
  dispatch("turbo:submit-start", { target: this.formElement, detail: { formSubmission: this } });
581
680
  this.delegate.formSubmissionStarted(this);
582
681
  }
@@ -606,7 +705,9 @@ class FormSubmission {
606
705
  this.delegate.formSubmissionErrored(this, error);
607
706
  }
608
707
  requestFinished(request) {
708
+ var _a;
609
709
  this.state = FormSubmissionState.stopped;
710
+ (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.removeAttribute("disabled");
610
711
  dispatch("turbo:submit-end", { target: this.formElement, detail: Object.assign({ formSubmission: this }, this.result) });
611
712
  this.delegate.formSubmissionFinished(this);
612
713
  }
@@ -682,10 +783,11 @@ class Snapshot {
682
783
  class FormInterceptor {
683
784
  constructor(delegate, element) {
684
785
  this.submitBubbled = ((event) => {
685
- if (event.target instanceof HTMLFormElement) {
686
- const form = event.target;
786
+ const form = event.target;
787
+ if (!event.defaultPrevented && form instanceof HTMLFormElement && form.closest("turbo-frame, html") == this.element) {
687
788
  const submitter = event.submitter || undefined;
688
- if (this.delegate.shouldInterceptFormSubmission(form, submitter)) {
789
+ const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.method;
790
+ if (method != "dialog" && this.delegate.shouldInterceptFormSubmission(form, submitter)) {
689
791
  event.preventDefault();
690
792
  event.stopImmediatePropagation();
691
793
  this.delegate.formSubmissionIntercepted(form, submitter);
@@ -900,10 +1002,11 @@ function createPlaceholderForPermanentElement(permanentElement) {
900
1002
  }
901
1003
 
902
1004
  class Renderer {
903
- constructor(currentSnapshot, newSnapshot, isPreview) {
1005
+ constructor(currentSnapshot, newSnapshot, isPreview, willRender = true) {
904
1006
  this.currentSnapshot = currentSnapshot;
905
1007
  this.newSnapshot = newSnapshot;
906
1008
  this.isPreview = isPreview;
1009
+ this.willRender = willRender;
907
1010
  this.promise = new Promise((resolve, reject) => this.resolvingFunctions = { resolve, reject });
908
1011
  }
909
1012
  get shouldRender() {
@@ -1279,7 +1382,9 @@ var VisitState;
1279
1382
  })(VisitState || (VisitState = {}));
1280
1383
  const defaultOptions = {
1281
1384
  action: "advance",
1282
- historyChanged: false
1385
+ historyChanged: false,
1386
+ visitCachedSnapshot: () => { },
1387
+ willRender: true,
1283
1388
  };
1284
1389
  var SystemStatusCode;
1285
1390
  (function (SystemStatusCode) {
@@ -1299,13 +1404,16 @@ class Visit {
1299
1404
  this.delegate = delegate;
1300
1405
  this.location = location;
1301
1406
  this.restorationIdentifier = restorationIdentifier || uuid();
1302
- const { action, historyChanged, referrer, snapshotHTML, response } = Object.assign(Object.assign({}, defaultOptions), options);
1407
+ const { action, historyChanged, referrer, snapshotHTML, response, visitCachedSnapshot, willRender } = Object.assign(Object.assign({}, defaultOptions), options);
1303
1408
  this.action = action;
1304
1409
  this.historyChanged = historyChanged;
1305
1410
  this.referrer = referrer;
1306
1411
  this.snapshotHTML = snapshotHTML;
1307
1412
  this.response = response;
1308
1413
  this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
1414
+ this.visitCachedSnapshot = visitCachedSnapshot;
1415
+ this.willRender = willRender;
1416
+ this.scrolled = !willRender;
1309
1417
  }
1310
1418
  get adapter() {
1311
1419
  return this.delegate.adapter;
@@ -1407,7 +1515,7 @@ class Visit {
1407
1515
  if (this.view.renderPromise)
1408
1516
  await this.view.renderPromise;
1409
1517
  if (isSuccessful(statusCode) && responseHTML != null) {
1410
- await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML));
1518
+ await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML), false, this.willRender);
1411
1519
  this.adapter.visitRendered(this);
1412
1520
  this.complete();
1413
1521
  }
@@ -1447,7 +1555,7 @@ class Visit {
1447
1555
  else {
1448
1556
  if (this.view.renderPromise)
1449
1557
  await this.view.renderPromise;
1450
- await this.view.renderPage(snapshot, isPreview);
1558
+ await this.view.renderPage(snapshot, isPreview, this.willRender);
1451
1559
  this.adapter.visitRendered(this);
1452
1560
  if (!isPreview) {
1453
1561
  this.complete();
@@ -1457,7 +1565,8 @@ class Visit {
1457
1565
  }
1458
1566
  }
1459
1567
  followRedirect() {
1460
- if (this.redirectedToLocation && !this.followedRedirect) {
1568
+ var _a;
1569
+ if (this.redirectedToLocation && !this.followedRedirect && ((_a = this.response) === null || _a === void 0 ? void 0 : _a.redirected)) {
1461
1570
  this.adapter.visitProposedToLocation(this.redirectedToLocation, {
1462
1571
  action: 'replace',
1463
1572
  response: this.response
@@ -1480,25 +1589,27 @@ class Visit {
1480
1589
  }
1481
1590
  async requestSucceededWithResponse(request, response) {
1482
1591
  const responseHTML = await response.responseHTML;
1592
+ const { redirected, statusCode } = response;
1483
1593
  if (responseHTML == undefined) {
1484
- this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch });
1594
+ this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch, redirected });
1485
1595
  }
1486
1596
  else {
1487
1597
  this.redirectedToLocation = response.redirected ? response.location : undefined;
1488
- this.recordResponse({ statusCode: response.statusCode, responseHTML });
1598
+ this.recordResponse({ statusCode: statusCode, responseHTML, redirected });
1489
1599
  }
1490
1600
  }
1491
1601
  async requestFailedWithResponse(request, response) {
1492
1602
  const responseHTML = await response.responseHTML;
1603
+ const { redirected, statusCode } = response;
1493
1604
  if (responseHTML == undefined) {
1494
- this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch });
1605
+ this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch, redirected });
1495
1606
  }
1496
1607
  else {
1497
- this.recordResponse({ statusCode: response.statusCode, responseHTML });
1608
+ this.recordResponse({ statusCode: statusCode, responseHTML, redirected });
1498
1609
  }
1499
1610
  }
1500
1611
  requestErrored(request, error) {
1501
- this.recordResponse({ statusCode: SystemStatusCode.networkFailure });
1612
+ this.recordResponse({ statusCode: SystemStatusCode.networkFailure, redirected: false });
1502
1613
  }
1503
1614
  requestFinished() {
1504
1615
  this.finishRequest();
@@ -1555,12 +1666,12 @@ class Visit {
1555
1666
  return !this.hasCachedSnapshot();
1556
1667
  }
1557
1668
  else {
1558
- return true;
1669
+ return this.willRender;
1559
1670
  }
1560
1671
  }
1561
1672
  cacheSnapshot() {
1562
1673
  if (!this.snapshotCached) {
1563
- this.view.cacheSnapshot();
1674
+ this.view.cacheSnapshot().then(snapshot => snapshot && this.visitCachedSnapshot(snapshot));
1564
1675
  this.snapshotCached = true;
1565
1676
  }
1566
1677
  }
@@ -1596,10 +1707,10 @@ class BrowserAdapter {
1596
1707
  this.navigator.startVisit(location, uuid(), options);
1597
1708
  }
1598
1709
  visitStarted(visit) {
1710
+ visit.loadCachedSnapshot();
1599
1711
  visit.issueRequest();
1600
1712
  visit.changeHistory();
1601
1713
  visit.goToSamePageAnchor();
1602
- visit.loadCachedSnapshot();
1603
1714
  }
1604
1715
  visitRequestStarted(visit) {
1605
1716
  this.progressBar.setValue(0);
@@ -1710,7 +1821,7 @@ class FormSubmitObserver {
1710
1821
  const form = event.target instanceof HTMLFormElement ? event.target : undefined;
1711
1822
  const submitter = event.submitter || undefined;
1712
1823
  if (form) {
1713
- const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.method;
1824
+ const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.getAttribute("method");
1714
1825
  if (method != "dialog" && this.delegate.willSubmitForm(form, submitter)) {
1715
1826
  event.preventDefault();
1716
1827
  this.delegate.formSubmitted(form, submitter);
@@ -1754,12 +1865,11 @@ class FrameRedirector {
1754
1865
  linkClickIntercepted(element, url) {
1755
1866
  const frame = this.findFrameElement(element);
1756
1867
  if (frame) {
1757
- frame.setAttribute("reloadable", "");
1758
- frame.src = url;
1868
+ frame.delegate.linkClickIntercepted(element, url);
1759
1869
  }
1760
1870
  }
1761
1871
  shouldInterceptFormSubmission(element, submitter) {
1762
- return this.shouldRedirect(element, submitter);
1872
+ return this.shouldSubmit(element, submitter);
1763
1873
  }
1764
1874
  formSubmissionIntercepted(element, submitter) {
1765
1875
  const frame = this.findFrameElement(element, submitter);
@@ -1768,6 +1878,13 @@ class FrameRedirector {
1768
1878
  frame.delegate.formSubmissionIntercepted(element, submitter);
1769
1879
  }
1770
1880
  }
1881
+ shouldSubmit(form, submitter) {
1882
+ var _a;
1883
+ const action = getAction(form, submitter);
1884
+ const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
1885
+ const rootLocation = expandURL((_a = meta === null || meta === void 0 ? void 0 : meta.content) !== null && _a !== void 0 ? _a : "/");
1886
+ return this.shouldRedirect(form, submitter) && locationIsVisitable(action, rootLocation);
1887
+ }
1771
1888
  shouldRedirect(element, submitter) {
1772
1889
  const frame = this.findFrameElement(element, submitter);
1773
1890
  return frame ? frame != element.closest("turbo-frame") : false;
@@ -1925,7 +2042,12 @@ class Navigator {
1925
2042
  }
1926
2043
  proposeVisit(location, options = {}) {
1927
2044
  if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
1928
- this.delegate.visitProposedToLocation(location, options);
2045
+ if (locationIsVisitable(location, this.view.snapshot.rootLocation)) {
2046
+ this.delegate.visitProposedToLocation(location, options);
2047
+ }
2048
+ else {
2049
+ window.location.href = location.toString();
2050
+ }
1929
2051
  }
1930
2052
  }
1931
2053
  startVisit(locatable, restorationIdentifier, options = {}) {
@@ -1936,12 +2058,7 @@ class Navigator {
1936
2058
  submitForm(form, submitter) {
1937
2059
  this.stop();
1938
2060
  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
- }
2061
+ this.formSubmission.start();
1945
2062
  }
1946
2063
  stop() {
1947
2064
  if (this.formSubmission) {
@@ -1974,8 +2091,9 @@ class Navigator {
1974
2091
  if (formSubmission.method != FetchMethod.get) {
1975
2092
  this.view.clearSnapshotCache();
1976
2093
  }
1977
- const { statusCode } = fetchResponse;
1978
- const visitOptions = { response: { statusCode, responseHTML } };
2094
+ const { statusCode, redirected } = fetchResponse;
2095
+ const action = this.getActionForFormSubmission(formSubmission);
2096
+ const visitOptions = { action, response: { statusCode, responseHTML, redirected } };
1979
2097
  this.proposeVisit(fetchResponse.location, visitOptions);
1980
2098
  }
1981
2099
  }
@@ -2027,7 +2145,7 @@ class Navigator {
2027
2145
  }
2028
2146
  getActionForFormSubmission(formSubmission) {
2029
2147
  const { formElement, submitter } = formSubmission;
2030
- const action = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("data-turbo-action")) || formElement.getAttribute("data-turbo-action");
2148
+ const action = getAttribute("data-turbo-action", submitter, formElement);
2031
2149
  return isAction(action) ? action : "advance";
2032
2150
  }
2033
2151
  }
@@ -2221,7 +2339,9 @@ class PageRenderer extends Renderer {
2221
2339
  this.mergeHead();
2222
2340
  }
2223
2341
  async render() {
2224
- this.replaceBody();
2342
+ if (this.willRender) {
2343
+ this.replaceBody();
2344
+ }
2225
2345
  }
2226
2346
  finishRendering() {
2227
2347
  super.finishRendering();
@@ -2359,8 +2479,8 @@ class PageView extends View {
2359
2479
  this.snapshotCache = new SnapshotCache(10);
2360
2480
  this.lastRenderedLocation = new URL(location.href);
2361
2481
  }
2362
- renderPage(snapshot, isPreview = false) {
2363
- const renderer = new PageRenderer(this.snapshot, snapshot, isPreview);
2482
+ renderPage(snapshot, isPreview = false, willRender = true) {
2483
+ const renderer = new PageRenderer(this.snapshot, snapshot, isPreview, willRender);
2364
2484
  return this.render(renderer);
2365
2485
  }
2366
2486
  renderError(snapshot) {
@@ -2375,7 +2495,9 @@ class PageView extends View {
2375
2495
  this.delegate.viewWillCacheSnapshot();
2376
2496
  const { snapshot, lastRenderedLocation: location } = this;
2377
2497
  await nextEventLoopTick();
2378
- this.snapshotCache.put(location, snapshot.clone());
2498
+ const cachedSnapshot = snapshot.clone();
2499
+ this.snapshotCache.put(location, cachedSnapshot);
2500
+ return cachedSnapshot;
2379
2501
  }
2380
2502
  }
2381
2503
  getCachedSnapshotForLocation(location) {
@@ -2477,7 +2599,7 @@ class Session {
2477
2599
  }
2478
2600
  willFollowLinkToLocation(link, location) {
2479
2601
  return this.elementDriveEnabled(link)
2480
- && this.locationIsVisitable(location)
2602
+ && locationIsVisitable(location, this.snapshot.rootLocation)
2481
2603
  && this.applicationAllowsFollowingLinkToLocation(link, location);
2482
2604
  }
2483
2605
  followedLinkToLocation(link, location) {
@@ -2485,14 +2607,24 @@ class Session {
2485
2607
  this.convertLinkWithMethodClickToFormSubmission(link) || this.visit(location.href, { action });
2486
2608
  }
2487
2609
  convertLinkWithMethodClickToFormSubmission(link) {
2488
- var _a;
2489
2610
  const linkMethod = link.getAttribute("data-turbo-method");
2490
2611
  if (linkMethod) {
2491
2612
  const form = document.createElement("form");
2492
2613
  form.method = linkMethod;
2493
2614
  form.action = link.getAttribute("href") || "undefined";
2494
2615
  form.hidden = true;
2495
- (_a = link.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(form, link);
2616
+ if (link.hasAttribute("data-turbo-confirm")) {
2617
+ form.setAttribute("data-turbo-confirm", link.getAttribute("data-turbo-confirm"));
2618
+ }
2619
+ const frame = this.getTargetFrameForLink(link);
2620
+ if (frame) {
2621
+ form.setAttribute("data-turbo-frame", frame);
2622
+ form.addEventListener("turbo:submit-start", () => form.remove());
2623
+ }
2624
+ else {
2625
+ form.addEventListener("submit", () => form.remove());
2626
+ }
2627
+ document.body.appendChild(form);
2496
2628
  return dispatch("submit", { cancelable: true, target: form });
2497
2629
  }
2498
2630
  else {
@@ -2522,7 +2654,10 @@ class Session {
2522
2654
  this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
2523
2655
  }
2524
2656
  willSubmitForm(form, submitter) {
2525
- return this.elementDriveEnabled(form) && this.elementDriveEnabled(submitter);
2657
+ const action = getAction(form, submitter);
2658
+ return this.elementDriveEnabled(form)
2659
+ && (!submitter || this.elementDriveEnabled(submitter))
2660
+ && locationIsVisitable(expandURL(action), this.snapshot.rootLocation);
2526
2661
  }
2527
2662
  formSubmitted(form, submitter) {
2528
2663
  this.navigator.submitForm(form, submitter);
@@ -2578,6 +2713,7 @@ class Session {
2578
2713
  return dispatch("turbo:before-visit", { detail: { url: location.href }, cancelable: true });
2579
2714
  }
2580
2715
  notifyApplicationAfterVisitingLocation(location, action) {
2716
+ markAsBusy(document.documentElement);
2581
2717
  return dispatch("turbo:visit", { detail: { url: location.href, action } });
2582
2718
  }
2583
2719
  notifyApplicationBeforeCachingSnapshot() {
@@ -2590,6 +2726,7 @@ class Session {
2590
2726
  return dispatch("turbo:render");
2591
2727
  }
2592
2728
  notifyApplicationAfterPageLoad(timing = {}) {
2729
+ clearBusyState(document.documentElement);
2593
2730
  return dispatch("turbo:load", { detail: { url: this.location.href, timing } });
2594
2731
  }
2595
2732
  notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL) {
@@ -2624,8 +2761,17 @@ class Session {
2624
2761
  const action = link.getAttribute("data-turbo-action");
2625
2762
  return isAction(action) ? action : "advance";
2626
2763
  }
2627
- locationIsVisitable(location) {
2628
- return isPrefixedBy(location, this.snapshot.rootLocation) && isHTML(location);
2764
+ getTargetFrameForLink(link) {
2765
+ const frame = link.getAttribute("data-turbo-frame");
2766
+ if (frame) {
2767
+ return frame;
2768
+ }
2769
+ else {
2770
+ const container = link.closest("turbo-frame");
2771
+ if (container) {
2772
+ return container.id;
2773
+ }
2774
+ }
2629
2775
  }
2630
2776
  get snapshot() {
2631
2777
  return this.view.snapshot;
@@ -2643,7 +2789,7 @@ const deprecatedLocationPropertyDescriptors = {
2643
2789
  };
2644
2790
 
2645
2791
  const session = new Session;
2646
- const { navigator } = session;
2792
+ const { navigator: navigator$1 } = session;
2647
2793
  function start() {
2648
2794
  session.start();
2649
2795
  }
@@ -2668,10 +2814,13 @@ function clearCache() {
2668
2814
  function setProgressBarDelay(delay) {
2669
2815
  session.setProgressBarDelay(delay);
2670
2816
  }
2817
+ function setConfirmMethod(confirmMethod) {
2818
+ FormSubmission.confirmMethod = confirmMethod;
2819
+ }
2671
2820
 
2672
2821
  var Turbo = /*#__PURE__*/Object.freeze({
2673
2822
  __proto__: null,
2674
- navigator: navigator,
2823
+ navigator: navigator$1,
2675
2824
  session: session,
2676
2825
  PageRenderer: PageRenderer,
2677
2826
  PageSnapshot: PageSnapshot,
@@ -2682,11 +2831,14 @@ var Turbo = /*#__PURE__*/Object.freeze({
2682
2831
  disconnectStreamSource: disconnectStreamSource,
2683
2832
  renderStreamMessage: renderStreamMessage,
2684
2833
  clearCache: clearCache,
2685
- setProgressBarDelay: setProgressBarDelay
2834
+ setProgressBarDelay: setProgressBarDelay,
2835
+ setConfirmMethod: setConfirmMethod
2686
2836
  });
2687
2837
 
2688
2838
  class FrameController {
2689
2839
  constructor(element) {
2840
+ this.fetchResponseLoaded = (fetchResponse) => { };
2841
+ this.currentFetchRequest = null;
2690
2842
  this.resolveVisitPromise = () => { };
2691
2843
  this.connected = false;
2692
2844
  this.hasBeenLoaded = false;
@@ -2742,11 +2894,10 @@ class FrameController {
2742
2894
  this.currentURL = this.sourceURL;
2743
2895
  if (this.sourceURL) {
2744
2896
  try {
2745
- this.element.loaded = this.visit(this.sourceURL);
2897
+ this.element.loaded = this.visit(expandURL(this.sourceURL));
2746
2898
  this.appearanceObserver.stop();
2747
2899
  await this.element.loaded;
2748
2900
  this.hasBeenLoaded = true;
2749
- session.frameLoaded(this.element);
2750
2901
  }
2751
2902
  catch (error) {
2752
2903
  this.currentURL = previousURL;
@@ -2756,7 +2907,7 @@ class FrameController {
2756
2907
  }
2757
2908
  }
2758
2909
  async loadResponse(fetchResponse) {
2759
- if (fetchResponse.redirected) {
2910
+ if (fetchResponse.redirected || (fetchResponse.succeeded && fetchResponse.isHTML)) {
2760
2911
  this.sourceURL = fetchResponse.response.url;
2761
2912
  }
2762
2913
  try {
@@ -2764,17 +2915,22 @@ class FrameController {
2764
2915
  if (html) {
2765
2916
  const { body } = parseHTMLDocument(html);
2766
2917
  const snapshot = new Snapshot(await this.extractForeignFrameElement(body));
2767
- const renderer = new FrameRenderer(this.view.snapshot, snapshot, false);
2918
+ const renderer = new FrameRenderer(this.view.snapshot, snapshot, false, false);
2768
2919
  if (this.view.renderPromise)
2769
2920
  await this.view.renderPromise;
2770
2921
  await this.view.render(renderer);
2771
2922
  session.frameRendered(fetchResponse, this.element);
2923
+ session.frameLoaded(this.element);
2924
+ this.fetchResponseLoaded(fetchResponse);
2772
2925
  }
2773
2926
  }
2774
2927
  catch (error) {
2775
2928
  console.error(error);
2776
2929
  this.view.invalidate();
2777
2930
  }
2931
+ finally {
2932
+ this.fetchResponseLoaded = () => { };
2933
+ }
2778
2934
  }
2779
2935
  elementAppearedInViewport(element) {
2780
2936
  this.loadSourceURL();
@@ -2800,20 +2956,15 @@ class FrameController {
2800
2956
  }
2801
2957
  this.reloadable = false;
2802
2958
  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
- }
2959
+ const { fetchRequest } = this.formSubmission;
2960
+ this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
2961
+ this.formSubmission.start();
2811
2962
  }
2812
2963
  prepareHeadersForRequest(headers, request) {
2813
2964
  headers["Turbo-Frame"] = this.id;
2814
2965
  }
2815
2966
  requestStarted(request) {
2816
- this.element.setAttribute("busy", "");
2967
+ markAsBusy(this.element);
2817
2968
  }
2818
2969
  requestPreventedHandlingResponse(request, response) {
2819
2970
  this.resolveVisitPromise();
@@ -2831,14 +2982,14 @@ class FrameController {
2831
2982
  this.resolveVisitPromise();
2832
2983
  }
2833
2984
  requestFinished(request) {
2834
- this.element.removeAttribute("busy");
2985
+ clearBusyState(this.element);
2835
2986
  }
2836
- formSubmissionStarted(formSubmission) {
2837
- const frame = this.findFrameElement(formSubmission.formElement);
2838
- frame.setAttribute("busy", "");
2987
+ formSubmissionStarted({ formElement }) {
2988
+ markAsBusy(formElement, this.findFrameElement(formElement));
2839
2989
  }
2840
2990
  formSubmissionSucceededWithResponse(formSubmission, response) {
2841
2991
  const frame = this.findFrameElement(formSubmission.formElement, formSubmission.submitter);
2992
+ this.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
2842
2993
  frame.delegate.loadResponse(response);
2843
2994
  }
2844
2995
  formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
@@ -2847,9 +2998,8 @@ class FrameController {
2847
2998
  formSubmissionErrored(formSubmission, error) {
2848
2999
  console.error(error);
2849
3000
  }
2850
- formSubmissionFinished(formSubmission) {
2851
- const frame = this.findFrameElement(formSubmission.formElement);
2852
- frame.removeAttribute("busy");
3001
+ formSubmissionFinished({ formElement }) {
3002
+ clearBusyState(formElement, this.findFrameElement(formElement));
2853
3003
  }
2854
3004
  allowsImmediateRender(snapshot, resume) {
2855
3005
  return true;
@@ -2859,10 +3009,14 @@ class FrameController {
2859
3009
  viewInvalidated() {
2860
3010
  }
2861
3011
  async visit(url) {
2862
- const request = new FetchRequest(this, FetchMethod.get, expandURL(url), undefined, this.element);
3012
+ var _a;
3013
+ const request = new FetchRequest(this, FetchMethod.get, url, url.searchParams, this.element);
3014
+ (_a = this.currentFetchRequest) === null || _a === void 0 ? void 0 : _a.cancel();
3015
+ this.currentFetchRequest = request;
2863
3016
  return new Promise(resolve => {
2864
3017
  this.resolveVisitPromise = () => {
2865
3018
  this.resolveVisitPromise = () => { };
3019
+ this.currentFetchRequest = null;
2866
3020
  resolve();
2867
3021
  };
2868
3022
  request.perform();
@@ -2870,12 +3024,27 @@ class FrameController {
2870
3024
  }
2871
3025
  navigateFrame(element, url, submitter) {
2872
3026
  const frame = this.findFrameElement(element, submitter);
3027
+ this.proposeVisitIfNavigatedWithAction(frame, element, submitter);
2873
3028
  frame.setAttribute("reloadable", "");
2874
3029
  frame.src = url;
2875
3030
  }
3031
+ proposeVisitIfNavigatedWithAction(frame, element, submitter) {
3032
+ const action = getAttribute("data-turbo-action", submitter, element, frame);
3033
+ if (isAction(action)) {
3034
+ const { visitCachedSnapshot } = new SnapshotSubstitution(frame);
3035
+ frame.delegate.fetchResponseLoaded = (fetchResponse) => {
3036
+ if (frame.src) {
3037
+ const { statusCode, redirected } = fetchResponse;
3038
+ const responseHTML = frame.ownerDocument.documentElement.outerHTML;
3039
+ const response = { statusCode, redirected, responseHTML };
3040
+ session.visit(frame.src, { action, response, visitCachedSnapshot, willRender: false });
3041
+ }
3042
+ };
3043
+ }
3044
+ }
2876
3045
  findFrameElement(element, submitter) {
2877
3046
  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");
3047
+ const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
2879
3048
  return (_a = getFrameElementById(id)) !== null && _a !== void 0 ? _a : this.element;
2880
3049
  }
2881
3050
  async extractForeignFrameElement(container) {
@@ -2896,8 +3065,15 @@ class FrameController {
2896
3065
  }
2897
3066
  return new FrameElement();
2898
3067
  }
3068
+ formActionIsVisitable(form, submitter) {
3069
+ const action = getAction(form, submitter);
3070
+ return locationIsVisitable(expandURL(action), this.rootLocation);
3071
+ }
2899
3072
  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");
3073
+ const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
3074
+ if (element instanceof HTMLFormElement && !this.formActionIsVisitable(element, submitter)) {
3075
+ return false;
3076
+ }
2901
3077
  if (!this.enabled || id == "_top") {
2902
3078
  return false;
2903
3079
  }
@@ -2954,6 +3130,23 @@ class FrameController {
2954
3130
  get isActive() {
2955
3131
  return this.element.isActive && this.connected;
2956
3132
  }
3133
+ get rootLocation() {
3134
+ var _a;
3135
+ const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
3136
+ const root = (_a = meta === null || meta === void 0 ? void 0 : meta.content) !== null && _a !== void 0 ? _a : "/";
3137
+ return expandURL(root);
3138
+ }
3139
+ }
3140
+ class SnapshotSubstitution {
3141
+ constructor(element) {
3142
+ this.visitCachedSnapshot = ({ element }) => {
3143
+ var _a;
3144
+ const { id, clone } = this;
3145
+ (_a = element.querySelector("#" + id)) === null || _a === void 0 ? void 0 : _a.replaceWith(clone);
3146
+ };
3147
+ this.clone = element.cloneNode(true);
3148
+ this.id = element.id;
3149
+ }
2957
3150
  }
2958
3151
  function getFrameElementById(id) {
2959
3152
  if (id != null) {
@@ -2974,6 +3167,7 @@ function activateElement(element, currentURL) {
2974
3167
  }
2975
3168
  if (element instanceof FrameElement) {
2976
3169
  element.connectedCallback();
3170
+ element.disconnectedCallback();
2977
3171
  return element;
2978
3172
  }
2979
3173
  }
@@ -3144,4 +3338,4 @@ customElements.define("turbo-stream", StreamElement);
3144
3338
  window.Turbo = Turbo;
3145
3339
  start();
3146
3340
 
3147
- export { PageRenderer, PageSnapshot, clearCache, connectStreamSource, disconnectStreamSource, navigator, registerAdapter, renderStreamMessage, session, setProgressBarDelay, start, visit };
3341
+ export { PageRenderer, PageSnapshot, clearCache, connectStreamSource, disconnectStreamSource, navigator$1 as navigator, registerAdapter, renderStreamMessage, session, setConfirmMethod, setProgressBarDelay, start, visit };