@hotwired/turbo 7.0.1 → 7.1.0

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
3
3
  Copyright © 2021 Basecamp, LLC
4
4
  */
5
5
  (function () {
@@ -20,6 +20,58 @@ Copyright © 2021 Basecamp, LLC
20
20
  Object.setPrototypeOf(HTMLElement, BuiltInHTMLElement);
21
21
  })();
22
22
 
23
+ /**
24
+ * The MIT License (MIT)
25
+ *
26
+ * Copyright (c) 2019 Javan Makhmali
27
+ *
28
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
29
+ * of this software and associated documentation files (the "Software"), to deal
30
+ * in the Software without restriction, including without limitation the rights
31
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
32
+ * copies of the Software, and to permit persons to whom the Software is
33
+ * furnished to do so, subject to the following conditions:
34
+ *
35
+ * The above copyright notice and this permission notice shall be included in
36
+ * all copies or substantial portions of the Software.
37
+ *
38
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
39
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
40
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
41
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
42
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
43
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
44
+ * THE SOFTWARE.
45
+ */
46
+
47
+ (function(prototype) {
48
+ if (typeof prototype.requestSubmit == "function") return
49
+
50
+ prototype.requestSubmit = function(submitter) {
51
+ if (submitter) {
52
+ validateSubmitter(submitter, this);
53
+ submitter.click();
54
+ } else {
55
+ submitter = document.createElement("input");
56
+ submitter.type = "submit";
57
+ submitter.hidden = true;
58
+ this.appendChild(submitter);
59
+ submitter.click();
60
+ this.removeChild(submitter);
61
+ }
62
+ };
63
+
64
+ function validateSubmitter(submitter, form) {
65
+ submitter instanceof HTMLElement || raise(TypeError, "parameter 1 is not of type 'HTMLElement'");
66
+ submitter.type == "submit" || raise(TypeError, "The specified element is not a submit button");
67
+ submitter.form == form || raise(DOMException, "The specified element is not owned by this form element", "NotFoundError");
68
+ }
69
+
70
+ function raise(errorConstructor, message, name) {
71
+ throw new errorConstructor("Failed to execute 'requestSubmit' on 'HTMLFormElement': " + message + ".", name)
72
+ }
73
+ })(HTMLFormElement.prototype);
74
+
23
75
  const submittersByForm = new WeakMap;
24
76
  function findSubmitterFromClickTarget(target) {
25
77
  const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
@@ -165,6 +217,10 @@ function getAnchor(url) {
165
217
  return anchorMatch[1];
166
218
  }
167
219
  }
220
+ function getAction(form, submitter) {
221
+ const action = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formaction")) || form.getAttribute("action") || form.action;
222
+ return expandURL(action);
223
+ }
168
224
  function getExtension(url) {
169
225
  return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || "";
170
226
  }
@@ -175,6 +231,9 @@ function isPrefixedBy(baseURL, url) {
175
231
  const prefix = getPrefix(url);
176
232
  return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix);
177
233
  }
234
+ function locationIsVisitable(location, rootLocation) {
235
+ return isPrefixedBy(location, rootLocation) && isHTML(location);
236
+ }
178
237
  function getRequestURL(url) {
179
238
  const anchor = getAnchor(url);
180
239
  return anchor != null
@@ -297,6 +356,29 @@ function uuid() {
297
356
  }
298
357
  }).join("");
299
358
  }
359
+ function getAttribute(attributeName, ...elements) {
360
+ for (const value of elements.map(element => element === null || element === void 0 ? void 0 : element.getAttribute(attributeName))) {
361
+ if (typeof value == "string")
362
+ return value;
363
+ }
364
+ return null;
365
+ }
366
+ function markAsBusy(...elements) {
367
+ for (const element of elements) {
368
+ if (element.localName == "turbo-frame") {
369
+ element.setAttribute("busy", "");
370
+ }
371
+ element.setAttribute("aria-busy", "true");
372
+ }
373
+ }
374
+ function clearBusyState(...elements) {
375
+ for (const element of elements) {
376
+ if (element.localName == "turbo-frame") {
377
+ element.removeAttribute("busy");
378
+ }
379
+ element.removeAttribute("aria-busy");
380
+ }
381
+ }
300
382
 
301
383
  var FetchMethod;
302
384
  (function (FetchMethod) {
@@ -322,13 +404,8 @@ class FetchRequest {
322
404
  this.delegate = delegate;
323
405
  this.method = method;
324
406
  this.headers = this.defaultHeaders;
325
- if (this.isIdempotent) {
326
- this.url = mergeFormDataEntries(location, [...body.entries()]);
327
- }
328
- else {
329
- this.body = body;
330
- this.url = location;
331
- }
407
+ this.body = body;
408
+ this.url = location;
332
409
  this.target = target;
333
410
  }
334
411
  get location() {
@@ -384,7 +461,7 @@ class FetchRequest {
384
461
  credentials: "same-origin",
385
462
  headers: this.headers,
386
463
  redirect: "follow",
387
- body: this.body,
464
+ body: this.isIdempotent ? null : this.body,
388
465
  signal: this.abortSignal,
389
466
  referrer: (_a = this.delegate.referrer) === null || _a === void 0 ? void 0 : _a.href
390
467
  };
@@ -406,7 +483,7 @@ class FetchRequest {
406
483
  cancelable: true,
407
484
  detail: {
408
485
  fetchOptions,
409
- url: this.url.href,
486
+ url: this.url,
410
487
  resume: this.resolveRequestPromise
411
488
  },
412
489
  target: this.target
@@ -415,21 +492,6 @@ class FetchRequest {
415
492
  await requestInterception;
416
493
  }
417
494
  }
418
- function mergeFormDataEntries(url, entries) {
419
- const currentSearchParams = new URLSearchParams(url.search);
420
- for (const [name, value] of entries) {
421
- if (value instanceof File)
422
- continue;
423
- if (currentSearchParams.has(name)) {
424
- currentSearchParams.delete(name);
425
- url.searchParams.set(name, value);
426
- }
427
- else {
428
- url.searchParams.append(name, value);
429
- }
430
- }
431
- return url;
432
- }
433
495
 
434
496
  class AppearanceObserver {
435
497
  constructor(delegate, element) {
@@ -523,9 +585,16 @@ class FormSubmission {
523
585
  this.formElement = formElement;
524
586
  this.submitter = submitter;
525
587
  this.formData = buildFormData(formElement, submitter);
588
+ this.location = expandURL(this.action);
589
+ if (this.method == FetchMethod.get) {
590
+ mergeFormDataEntries(this.location, [...this.body.entries()]);
591
+ }
526
592
  this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body, this.formElement);
527
593
  this.mustRedirect = mustRedirect;
528
594
  }
595
+ static confirmMethod(message, element) {
596
+ return confirm(message);
597
+ }
529
598
  get method() {
530
599
  var _a;
531
600
  const method = ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formmethod")) || this.formElement.getAttribute("method") || "";
@@ -536,9 +605,6 @@ class FormSubmission {
536
605
  const formElementAction = typeof this.formElement.action === 'string' ? this.formElement.action : null;
537
606
  return ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formaction")) || this.formElement.getAttribute("action") || formElementAction || "";
538
607
  }
539
- get location() {
540
- return expandURL(this.action);
541
- }
542
608
  get body() {
543
609
  if (this.enctype == FormEnctype.urlEncoded || this.method == FetchMethod.get) {
544
610
  return new URLSearchParams(this.stringFormData);
@@ -559,8 +625,20 @@ class FormSubmission {
559
625
  return entries.concat(typeof value == "string" ? [[name, value]] : []);
560
626
  }, []);
561
627
  }
628
+ get confirmationMessage() {
629
+ return this.formElement.getAttribute("data-turbo-confirm");
630
+ }
631
+ get needsConfirmation() {
632
+ return this.confirmationMessage !== null;
633
+ }
562
634
  async start() {
563
635
  const { initialized, requesting } = FormSubmissionState;
636
+ if (this.needsConfirmation) {
637
+ const answer = FormSubmission.confirmMethod(this.confirmationMessage, this.formElement);
638
+ if (!answer) {
639
+ return;
640
+ }
641
+ }
564
642
  if (this.state == initialized) {
565
643
  this.state = requesting;
566
644
  return this.fetchRequest.perform();
@@ -584,7 +662,9 @@ class FormSubmission {
584
662
  }
585
663
  }
586
664
  requestStarted(request) {
665
+ var _a;
587
666
  this.state = FormSubmissionState.waiting;
667
+ (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.setAttribute("disabled", "");
588
668
  dispatch("turbo:submit-start", { target: this.formElement, detail: { formSubmission: this } });
589
669
  this.delegate.formSubmissionStarted(this);
590
670
  }
@@ -614,7 +694,9 @@ class FormSubmission {
614
694
  this.delegate.formSubmissionErrored(this, error);
615
695
  }
616
696
  requestFinished(request) {
697
+ var _a;
617
698
  this.state = FormSubmissionState.stopped;
699
+ (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.removeAttribute("disabled");
618
700
  dispatch("turbo:submit-end", { target: this.formElement, detail: Object.assign({ formSubmission: this }, this.result) });
619
701
  this.delegate.formSubmissionFinished(this);
620
702
  }
@@ -648,6 +730,16 @@ function getMetaContent(name) {
648
730
  function responseSucceededWithoutRedirect(response) {
649
731
  return response.statusCode == 200 && !response.redirected;
650
732
  }
733
+ function mergeFormDataEntries(url, entries) {
734
+ const searchParams = new URLSearchParams;
735
+ for (const [name, value] of entries) {
736
+ if (value instanceof File)
737
+ continue;
738
+ searchParams.append(name, value);
739
+ }
740
+ url.search = searchParams.toString();
741
+ return url;
742
+ }
651
743
 
652
744
  class Snapshot {
653
745
  constructor(element) {
@@ -691,9 +783,10 @@ class FormInterceptor {
691
783
  constructor(delegate, element) {
692
784
  this.submitBubbled = ((event) => {
693
785
  const form = event.target;
694
- if (form instanceof HTMLFormElement && form.closest("turbo-frame, html") == this.element) {
786
+ if (!event.defaultPrevented && form instanceof HTMLFormElement && form.closest("turbo-frame, html") == this.element) {
695
787
  const submitter = event.submitter || undefined;
696
- if (this.delegate.shouldInterceptFormSubmission(form, submitter)) {
788
+ const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.method;
789
+ if (method != "dialog" && this.delegate.shouldInterceptFormSubmission(form, submitter)) {
697
790
  event.preventDefault();
698
791
  event.stopImmediatePropagation();
699
792
  this.delegate.formSubmissionIntercepted(form, submitter);
@@ -908,10 +1001,11 @@ function createPlaceholderForPermanentElement(permanentElement) {
908
1001
  }
909
1002
 
910
1003
  class Renderer {
911
- constructor(currentSnapshot, newSnapshot, isPreview) {
1004
+ constructor(currentSnapshot, newSnapshot, isPreview, willRender = true) {
912
1005
  this.currentSnapshot = currentSnapshot;
913
1006
  this.newSnapshot = newSnapshot;
914
1007
  this.isPreview = isPreview;
1008
+ this.willRender = willRender;
915
1009
  this.promise = new Promise((resolve, reject) => this.resolvingFunctions = { resolve, reject });
916
1010
  }
917
1011
  get shouldRender() {
@@ -1287,7 +1381,9 @@ var VisitState;
1287
1381
  })(VisitState || (VisitState = {}));
1288
1382
  const defaultOptions = {
1289
1383
  action: "advance",
1290
- historyChanged: false
1384
+ historyChanged: false,
1385
+ visitCachedSnapshot: () => { },
1386
+ willRender: true,
1291
1387
  };
1292
1388
  var SystemStatusCode;
1293
1389
  (function (SystemStatusCode) {
@@ -1307,13 +1403,16 @@ class Visit {
1307
1403
  this.delegate = delegate;
1308
1404
  this.location = location;
1309
1405
  this.restorationIdentifier = restorationIdentifier || uuid();
1310
- const { action, historyChanged, referrer, snapshotHTML, response } = Object.assign(Object.assign({}, defaultOptions), options);
1406
+ const { action, historyChanged, referrer, snapshotHTML, response, visitCachedSnapshot, willRender } = Object.assign(Object.assign({}, defaultOptions), options);
1311
1407
  this.action = action;
1312
1408
  this.historyChanged = historyChanged;
1313
1409
  this.referrer = referrer;
1314
1410
  this.snapshotHTML = snapshotHTML;
1315
1411
  this.response = response;
1316
1412
  this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
1413
+ this.visitCachedSnapshot = visitCachedSnapshot;
1414
+ this.willRender = willRender;
1415
+ this.scrolled = !willRender;
1317
1416
  }
1318
1417
  get adapter() {
1319
1418
  return this.delegate.adapter;
@@ -1415,7 +1514,7 @@ class Visit {
1415
1514
  if (this.view.renderPromise)
1416
1515
  await this.view.renderPromise;
1417
1516
  if (isSuccessful(statusCode) && responseHTML != null) {
1418
- await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML));
1517
+ await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML), false, this.willRender);
1419
1518
  this.adapter.visitRendered(this);
1420
1519
  this.complete();
1421
1520
  }
@@ -1455,7 +1554,7 @@ class Visit {
1455
1554
  else {
1456
1555
  if (this.view.renderPromise)
1457
1556
  await this.view.renderPromise;
1458
- await this.view.renderPage(snapshot, isPreview);
1557
+ await this.view.renderPage(snapshot, isPreview, this.willRender);
1459
1558
  this.adapter.visitRendered(this);
1460
1559
  if (!isPreview) {
1461
1560
  this.complete();
@@ -1465,7 +1564,8 @@ class Visit {
1465
1564
  }
1466
1565
  }
1467
1566
  followRedirect() {
1468
- if (this.redirectedToLocation && !this.followedRedirect) {
1567
+ var _a;
1568
+ if (this.redirectedToLocation && !this.followedRedirect && ((_a = this.response) === null || _a === void 0 ? void 0 : _a.redirected)) {
1469
1569
  this.adapter.visitProposedToLocation(this.redirectedToLocation, {
1470
1570
  action: 'replace',
1471
1571
  response: this.response
@@ -1488,25 +1588,27 @@ class Visit {
1488
1588
  }
1489
1589
  async requestSucceededWithResponse(request, response) {
1490
1590
  const responseHTML = await response.responseHTML;
1591
+ const { redirected, statusCode } = response;
1491
1592
  if (responseHTML == undefined) {
1492
- this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch });
1593
+ this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch, redirected });
1493
1594
  }
1494
1595
  else {
1495
1596
  this.redirectedToLocation = response.redirected ? response.location : undefined;
1496
- this.recordResponse({ statusCode: response.statusCode, responseHTML });
1597
+ this.recordResponse({ statusCode: statusCode, responseHTML, redirected });
1497
1598
  }
1498
1599
  }
1499
1600
  async requestFailedWithResponse(request, response) {
1500
1601
  const responseHTML = await response.responseHTML;
1602
+ const { redirected, statusCode } = response;
1501
1603
  if (responseHTML == undefined) {
1502
- this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch });
1604
+ this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch, redirected });
1503
1605
  }
1504
1606
  else {
1505
- this.recordResponse({ statusCode: response.statusCode, responseHTML });
1607
+ this.recordResponse({ statusCode: statusCode, responseHTML, redirected });
1506
1608
  }
1507
1609
  }
1508
1610
  requestErrored(request, error) {
1509
- this.recordResponse({ statusCode: SystemStatusCode.networkFailure });
1611
+ this.recordResponse({ statusCode: SystemStatusCode.networkFailure, redirected: false });
1510
1612
  }
1511
1613
  requestFinished() {
1512
1614
  this.finishRequest();
@@ -1563,12 +1665,12 @@ class Visit {
1563
1665
  return !this.hasCachedSnapshot();
1564
1666
  }
1565
1667
  else {
1566
- return true;
1668
+ return this.willRender;
1567
1669
  }
1568
1670
  }
1569
1671
  cacheSnapshot() {
1570
1672
  if (!this.snapshotCached) {
1571
- this.view.cacheSnapshot();
1673
+ this.view.cacheSnapshot().then(snapshot => snapshot && this.visitCachedSnapshot(snapshot));
1572
1674
  this.snapshotCached = true;
1573
1675
  }
1574
1676
  }
@@ -1604,10 +1706,10 @@ class BrowserAdapter {
1604
1706
  this.navigator.startVisit(location, uuid(), options);
1605
1707
  }
1606
1708
  visitStarted(visit) {
1709
+ visit.loadCachedSnapshot();
1607
1710
  visit.issueRequest();
1608
1711
  visit.changeHistory();
1609
1712
  visit.goToSamePageAnchor();
1610
- visit.loadCachedSnapshot();
1611
1713
  }
1612
1714
  visitRequestStarted(visit) {
1613
1715
  this.progressBar.setValue(0);
@@ -1718,7 +1820,7 @@ class FormSubmitObserver {
1718
1820
  const form = event.target instanceof HTMLFormElement ? event.target : undefined;
1719
1821
  const submitter = event.submitter || undefined;
1720
1822
  if (form) {
1721
- const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.method;
1823
+ const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.getAttribute("method");
1722
1824
  if (method != "dialog" && this.delegate.willSubmitForm(form, submitter)) {
1723
1825
  event.preventDefault();
1724
1826
  this.delegate.formSubmitted(form, submitter);
@@ -1762,12 +1864,11 @@ class FrameRedirector {
1762
1864
  linkClickIntercepted(element, url) {
1763
1865
  const frame = this.findFrameElement(element);
1764
1866
  if (frame) {
1765
- frame.setAttribute("reloadable", "");
1766
- frame.src = url;
1867
+ frame.delegate.linkClickIntercepted(element, url);
1767
1868
  }
1768
1869
  }
1769
1870
  shouldInterceptFormSubmission(element, submitter) {
1770
- return this.shouldRedirect(element, submitter);
1871
+ return this.shouldSubmit(element, submitter);
1771
1872
  }
1772
1873
  formSubmissionIntercepted(element, submitter) {
1773
1874
  const frame = this.findFrameElement(element, submitter);
@@ -1776,6 +1877,13 @@ class FrameRedirector {
1776
1877
  frame.delegate.formSubmissionIntercepted(element, submitter);
1777
1878
  }
1778
1879
  }
1880
+ shouldSubmit(form, submitter) {
1881
+ var _a;
1882
+ const action = getAction(form, submitter);
1883
+ const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
1884
+ const rootLocation = expandURL((_a = meta === null || meta === void 0 ? void 0 : meta.content) !== null && _a !== void 0 ? _a : "/");
1885
+ return this.shouldRedirect(form, submitter) && locationIsVisitable(action, rootLocation);
1886
+ }
1779
1887
  shouldRedirect(element, submitter) {
1780
1888
  const frame = this.findFrameElement(element, submitter);
1781
1889
  return frame ? frame != element.closest("turbo-frame") : false;
@@ -1933,7 +2041,12 @@ class Navigator {
1933
2041
  }
1934
2042
  proposeVisit(location, options = {}) {
1935
2043
  if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
1936
- this.delegate.visitProposedToLocation(location, options);
2044
+ if (locationIsVisitable(location, this.view.snapshot.rootLocation)) {
2045
+ this.delegate.visitProposedToLocation(location, options);
2046
+ }
2047
+ else {
2048
+ window.location.href = location.toString();
2049
+ }
1937
2050
  }
1938
2051
  }
1939
2052
  startVisit(locatable, restorationIdentifier, options = {}) {
@@ -1944,12 +2057,7 @@ class Navigator {
1944
2057
  submitForm(form, submitter) {
1945
2058
  this.stop();
1946
2059
  this.formSubmission = new FormSubmission(this, form, submitter, true);
1947
- if (this.formSubmission.isIdempotent) {
1948
- this.proposeVisit(this.formSubmission.fetchRequest.url, { action: this.getActionForFormSubmission(this.formSubmission) });
1949
- }
1950
- else {
1951
- this.formSubmission.start();
1952
- }
2060
+ this.formSubmission.start();
1953
2061
  }
1954
2062
  stop() {
1955
2063
  if (this.formSubmission) {
@@ -1982,8 +2090,9 @@ class Navigator {
1982
2090
  if (formSubmission.method != FetchMethod.get) {
1983
2091
  this.view.clearSnapshotCache();
1984
2092
  }
1985
- const { statusCode } = fetchResponse;
1986
- const visitOptions = { response: { statusCode, responseHTML } };
2093
+ const { statusCode, redirected } = fetchResponse;
2094
+ const action = this.getActionForFormSubmission(formSubmission);
2095
+ const visitOptions = { action, response: { statusCode, responseHTML, redirected } };
1987
2096
  this.proposeVisit(fetchResponse.location, visitOptions);
1988
2097
  }
1989
2098
  }
@@ -2035,7 +2144,7 @@ class Navigator {
2035
2144
  }
2036
2145
  getActionForFormSubmission(formSubmission) {
2037
2146
  const { formElement, submitter } = formSubmission;
2038
- const action = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("data-turbo-action")) || formElement.getAttribute("data-turbo-action");
2147
+ const action = getAttribute("data-turbo-action", submitter, formElement);
2039
2148
  return isAction(action) ? action : "advance";
2040
2149
  }
2041
2150
  }
@@ -2229,7 +2338,9 @@ class PageRenderer extends Renderer {
2229
2338
  this.mergeHead();
2230
2339
  }
2231
2340
  async render() {
2232
- this.replaceBody();
2341
+ if (this.willRender) {
2342
+ this.replaceBody();
2343
+ }
2233
2344
  }
2234
2345
  finishRendering() {
2235
2346
  super.finishRendering();
@@ -2367,8 +2478,8 @@ class PageView extends View {
2367
2478
  this.snapshotCache = new SnapshotCache(10);
2368
2479
  this.lastRenderedLocation = new URL(location.href);
2369
2480
  }
2370
- renderPage(snapshot, isPreview = false) {
2371
- const renderer = new PageRenderer(this.snapshot, snapshot, isPreview);
2481
+ renderPage(snapshot, isPreview = false, willRender = true) {
2482
+ const renderer = new PageRenderer(this.snapshot, snapshot, isPreview, willRender);
2372
2483
  return this.render(renderer);
2373
2484
  }
2374
2485
  renderError(snapshot) {
@@ -2383,7 +2494,9 @@ class PageView extends View {
2383
2494
  this.delegate.viewWillCacheSnapshot();
2384
2495
  const { snapshot, lastRenderedLocation: location } = this;
2385
2496
  await nextEventLoopTick();
2386
- this.snapshotCache.put(location, snapshot.clone());
2497
+ const cachedSnapshot = snapshot.clone();
2498
+ this.snapshotCache.put(location, cachedSnapshot);
2499
+ return cachedSnapshot;
2387
2500
  }
2388
2501
  }
2389
2502
  getCachedSnapshotForLocation(location) {
@@ -2485,7 +2598,7 @@ class Session {
2485
2598
  }
2486
2599
  willFollowLinkToLocation(link, location) {
2487
2600
  return this.elementDriveEnabled(link)
2488
- && this.locationIsVisitable(location)
2601
+ && locationIsVisitable(location, this.snapshot.rootLocation)
2489
2602
  && this.applicationAllowsFollowingLinkToLocation(link, location);
2490
2603
  }
2491
2604
  followedLinkToLocation(link, location) {
@@ -2493,14 +2606,24 @@ class Session {
2493
2606
  this.convertLinkWithMethodClickToFormSubmission(link) || this.visit(location.href, { action });
2494
2607
  }
2495
2608
  convertLinkWithMethodClickToFormSubmission(link) {
2496
- var _a;
2497
2609
  const linkMethod = link.getAttribute("data-turbo-method");
2498
2610
  if (linkMethod) {
2499
2611
  const form = document.createElement("form");
2500
2612
  form.method = linkMethod;
2501
2613
  form.action = link.getAttribute("href") || "undefined";
2502
2614
  form.hidden = true;
2503
- (_a = link.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(form, link);
2615
+ if (link.hasAttribute("data-turbo-confirm")) {
2616
+ form.setAttribute("data-turbo-confirm", link.getAttribute("data-turbo-confirm"));
2617
+ }
2618
+ const frame = this.getTargetFrameForLink(link);
2619
+ if (frame) {
2620
+ form.setAttribute("data-turbo-frame", frame);
2621
+ form.addEventListener("turbo:submit-start", () => form.remove());
2622
+ }
2623
+ else {
2624
+ form.addEventListener("submit", () => form.remove());
2625
+ }
2626
+ document.body.appendChild(form);
2504
2627
  return dispatch("submit", { cancelable: true, target: form });
2505
2628
  }
2506
2629
  else {
@@ -2530,7 +2653,10 @@ class Session {
2530
2653
  this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
2531
2654
  }
2532
2655
  willSubmitForm(form, submitter) {
2533
- return this.elementDriveEnabled(form) && (!submitter || this.elementDriveEnabled(submitter));
2656
+ const action = getAction(form, submitter);
2657
+ return this.elementDriveEnabled(form)
2658
+ && (!submitter || this.elementDriveEnabled(submitter))
2659
+ && locationIsVisitable(expandURL(action), this.snapshot.rootLocation);
2534
2660
  }
2535
2661
  formSubmitted(form, submitter) {
2536
2662
  this.navigator.submitForm(form, submitter);
@@ -2586,6 +2712,7 @@ class Session {
2586
2712
  return dispatch("turbo:before-visit", { detail: { url: location.href }, cancelable: true });
2587
2713
  }
2588
2714
  notifyApplicationAfterVisitingLocation(location, action) {
2715
+ markAsBusy(document.documentElement);
2589
2716
  return dispatch("turbo:visit", { detail: { url: location.href, action } });
2590
2717
  }
2591
2718
  notifyApplicationBeforeCachingSnapshot() {
@@ -2598,6 +2725,7 @@ class Session {
2598
2725
  return dispatch("turbo:render");
2599
2726
  }
2600
2727
  notifyApplicationAfterPageLoad(timing = {}) {
2728
+ clearBusyState(document.documentElement);
2601
2729
  return dispatch("turbo:load", { detail: { url: this.location.href, timing } });
2602
2730
  }
2603
2731
  notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL) {
@@ -2632,8 +2760,17 @@ class Session {
2632
2760
  const action = link.getAttribute("data-turbo-action");
2633
2761
  return isAction(action) ? action : "advance";
2634
2762
  }
2635
- locationIsVisitable(location) {
2636
- return isPrefixedBy(location, this.snapshot.rootLocation) && isHTML(location);
2763
+ getTargetFrameForLink(link) {
2764
+ const frame = link.getAttribute("data-turbo-frame");
2765
+ if (frame) {
2766
+ return frame;
2767
+ }
2768
+ else {
2769
+ const container = link.closest("turbo-frame");
2770
+ if (container) {
2771
+ return container.id;
2772
+ }
2773
+ }
2637
2774
  }
2638
2775
  get snapshot() {
2639
2776
  return this.view.snapshot;
@@ -2676,6 +2813,9 @@ function clearCache() {
2676
2813
  function setProgressBarDelay(delay) {
2677
2814
  session.setProgressBarDelay(delay);
2678
2815
  }
2816
+ function setConfirmMethod(confirmMethod) {
2817
+ FormSubmission.confirmMethod = confirmMethod;
2818
+ }
2679
2819
 
2680
2820
  var Turbo = /*#__PURE__*/Object.freeze({
2681
2821
  __proto__: null,
@@ -2690,11 +2830,14 @@ var Turbo = /*#__PURE__*/Object.freeze({
2690
2830
  disconnectStreamSource: disconnectStreamSource,
2691
2831
  renderStreamMessage: renderStreamMessage,
2692
2832
  clearCache: clearCache,
2693
- setProgressBarDelay: setProgressBarDelay
2833
+ setProgressBarDelay: setProgressBarDelay,
2834
+ setConfirmMethod: setConfirmMethod
2694
2835
  });
2695
2836
 
2696
2837
  class FrameController {
2697
2838
  constructor(element) {
2839
+ this.fetchResponseLoaded = (fetchResponse) => { };
2840
+ this.currentFetchRequest = null;
2698
2841
  this.resolveVisitPromise = () => { };
2699
2842
  this.connected = false;
2700
2843
  this.hasBeenLoaded = false;
@@ -2750,11 +2893,10 @@ class FrameController {
2750
2893
  this.currentURL = this.sourceURL;
2751
2894
  if (this.sourceURL) {
2752
2895
  try {
2753
- this.element.loaded = this.visit(this.sourceURL);
2896
+ this.element.loaded = this.visit(expandURL(this.sourceURL));
2754
2897
  this.appearanceObserver.stop();
2755
2898
  await this.element.loaded;
2756
2899
  this.hasBeenLoaded = true;
2757
- session.frameLoaded(this.element);
2758
2900
  }
2759
2901
  catch (error) {
2760
2902
  this.currentURL = previousURL;
@@ -2764,7 +2906,7 @@ class FrameController {
2764
2906
  }
2765
2907
  }
2766
2908
  async loadResponse(fetchResponse) {
2767
- if (fetchResponse.redirected) {
2909
+ if (fetchResponse.redirected || (fetchResponse.succeeded && fetchResponse.isHTML)) {
2768
2910
  this.sourceURL = fetchResponse.response.url;
2769
2911
  }
2770
2912
  try {
@@ -2772,17 +2914,22 @@ class FrameController {
2772
2914
  if (html) {
2773
2915
  const { body } = parseHTMLDocument(html);
2774
2916
  const snapshot = new Snapshot(await this.extractForeignFrameElement(body));
2775
- const renderer = new FrameRenderer(this.view.snapshot, snapshot, false);
2917
+ const renderer = new FrameRenderer(this.view.snapshot, snapshot, false, false);
2776
2918
  if (this.view.renderPromise)
2777
2919
  await this.view.renderPromise;
2778
2920
  await this.view.render(renderer);
2779
2921
  session.frameRendered(fetchResponse, this.element);
2922
+ session.frameLoaded(this.element);
2923
+ this.fetchResponseLoaded(fetchResponse);
2780
2924
  }
2781
2925
  }
2782
2926
  catch (error) {
2783
2927
  console.error(error);
2784
2928
  this.view.invalidate();
2785
2929
  }
2930
+ finally {
2931
+ this.fetchResponseLoaded = () => { };
2932
+ }
2786
2933
  }
2787
2934
  elementAppearedInViewport(element) {
2788
2935
  this.loadSourceURL();
@@ -2808,20 +2955,15 @@ class FrameController {
2808
2955
  }
2809
2956
  this.reloadable = false;
2810
2957
  this.formSubmission = new FormSubmission(this, element, submitter);
2811
- if (this.formSubmission.fetchRequest.isIdempotent) {
2812
- this.navigateFrame(element, this.formSubmission.fetchRequest.url.href, submitter);
2813
- }
2814
- else {
2815
- const { fetchRequest } = this.formSubmission;
2816
- this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
2817
- this.formSubmission.start();
2818
- }
2958
+ const { fetchRequest } = this.formSubmission;
2959
+ this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
2960
+ this.formSubmission.start();
2819
2961
  }
2820
2962
  prepareHeadersForRequest(headers, request) {
2821
2963
  headers["Turbo-Frame"] = this.id;
2822
2964
  }
2823
2965
  requestStarted(request) {
2824
- this.element.setAttribute("busy", "");
2966
+ markAsBusy(this.element);
2825
2967
  }
2826
2968
  requestPreventedHandlingResponse(request, response) {
2827
2969
  this.resolveVisitPromise();
@@ -2839,14 +2981,14 @@ class FrameController {
2839
2981
  this.resolveVisitPromise();
2840
2982
  }
2841
2983
  requestFinished(request) {
2842
- this.element.removeAttribute("busy");
2984
+ clearBusyState(this.element);
2843
2985
  }
2844
- formSubmissionStarted(formSubmission) {
2845
- const frame = this.findFrameElement(formSubmission.formElement);
2846
- frame.setAttribute("busy", "");
2986
+ formSubmissionStarted({ formElement }) {
2987
+ markAsBusy(formElement, this.findFrameElement(formElement));
2847
2988
  }
2848
2989
  formSubmissionSucceededWithResponse(formSubmission, response) {
2849
2990
  const frame = this.findFrameElement(formSubmission.formElement, formSubmission.submitter);
2991
+ this.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
2850
2992
  frame.delegate.loadResponse(response);
2851
2993
  }
2852
2994
  formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
@@ -2855,9 +2997,8 @@ class FrameController {
2855
2997
  formSubmissionErrored(formSubmission, error) {
2856
2998
  console.error(error);
2857
2999
  }
2858
- formSubmissionFinished(formSubmission) {
2859
- const frame = this.findFrameElement(formSubmission.formElement);
2860
- frame.removeAttribute("busy");
3000
+ formSubmissionFinished({ formElement }) {
3001
+ clearBusyState(formElement, this.findFrameElement(formElement));
2861
3002
  }
2862
3003
  allowsImmediateRender(snapshot, resume) {
2863
3004
  return true;
@@ -2867,10 +3008,14 @@ class FrameController {
2867
3008
  viewInvalidated() {
2868
3009
  }
2869
3010
  async visit(url) {
2870
- const request = new FetchRequest(this, FetchMethod.get, expandURL(url), undefined, this.element);
3011
+ var _a;
3012
+ const request = new FetchRequest(this, FetchMethod.get, url, new URLSearchParams, this.element);
3013
+ (_a = this.currentFetchRequest) === null || _a === void 0 ? void 0 : _a.cancel();
3014
+ this.currentFetchRequest = request;
2871
3015
  return new Promise(resolve => {
2872
3016
  this.resolveVisitPromise = () => {
2873
3017
  this.resolveVisitPromise = () => { };
3018
+ this.currentFetchRequest = null;
2874
3019
  resolve();
2875
3020
  };
2876
3021
  request.perform();
@@ -2878,12 +3023,27 @@ class FrameController {
2878
3023
  }
2879
3024
  navigateFrame(element, url, submitter) {
2880
3025
  const frame = this.findFrameElement(element, submitter);
3026
+ this.proposeVisitIfNavigatedWithAction(frame, element, submitter);
2881
3027
  frame.setAttribute("reloadable", "");
2882
3028
  frame.src = url;
2883
3029
  }
3030
+ proposeVisitIfNavigatedWithAction(frame, element, submitter) {
3031
+ const action = getAttribute("data-turbo-action", submitter, element, frame);
3032
+ if (isAction(action)) {
3033
+ const { visitCachedSnapshot } = new SnapshotSubstitution(frame);
3034
+ frame.delegate.fetchResponseLoaded = (fetchResponse) => {
3035
+ if (frame.src) {
3036
+ const { statusCode, redirected } = fetchResponse;
3037
+ const responseHTML = frame.ownerDocument.documentElement.outerHTML;
3038
+ const response = { statusCode, redirected, responseHTML };
3039
+ session.visit(frame.src, { action, response, visitCachedSnapshot, willRender: false });
3040
+ }
3041
+ };
3042
+ }
3043
+ }
2884
3044
  findFrameElement(element, submitter) {
2885
3045
  var _a;
2886
- const id = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("data-turbo-frame")) || element.getAttribute("data-turbo-frame") || this.element.getAttribute("target");
3046
+ const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
2887
3047
  return (_a = getFrameElementById(id)) !== null && _a !== void 0 ? _a : this.element;
2888
3048
  }
2889
3049
  async extractForeignFrameElement(container) {
@@ -2904,8 +3064,15 @@ class FrameController {
2904
3064
  }
2905
3065
  return new FrameElement();
2906
3066
  }
3067
+ formActionIsVisitable(form, submitter) {
3068
+ const action = getAction(form, submitter);
3069
+ return locationIsVisitable(expandURL(action), this.rootLocation);
3070
+ }
2907
3071
  shouldInterceptNavigation(element, submitter) {
2908
- const id = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("data-turbo-frame")) || element.getAttribute("data-turbo-frame") || this.element.getAttribute("target");
3072
+ const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
3073
+ if (element instanceof HTMLFormElement && !this.formActionIsVisitable(element, submitter)) {
3074
+ return false;
3075
+ }
2909
3076
  if (!this.enabled || id == "_top") {
2910
3077
  return false;
2911
3078
  }
@@ -2962,6 +3129,23 @@ class FrameController {
2962
3129
  get isActive() {
2963
3130
  return this.element.isActive && this.connected;
2964
3131
  }
3132
+ get rootLocation() {
3133
+ var _a;
3134
+ const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
3135
+ const root = (_a = meta === null || meta === void 0 ? void 0 : meta.content) !== null && _a !== void 0 ? _a : "/";
3136
+ return expandURL(root);
3137
+ }
3138
+ }
3139
+ class SnapshotSubstitution {
3140
+ constructor(element) {
3141
+ this.visitCachedSnapshot = ({ element }) => {
3142
+ var _a;
3143
+ const { id, clone } = this;
3144
+ (_a = element.querySelector("#" + id)) === null || _a === void 0 ? void 0 : _a.replaceWith(clone);
3145
+ };
3146
+ this.clone = element.cloneNode(true);
3147
+ this.id = element.id;
3148
+ }
2965
3149
  }
2966
3150
  function getFrameElementById(id) {
2967
3151
  if (id != null) {
@@ -2982,6 +3166,7 @@ function activateElement(element, currentURL) {
2982
3166
  }
2983
3167
  if (element instanceof FrameElement) {
2984
3168
  element.connectedCallback();
3169
+ element.disconnectedCallback();
2985
3170
  return element;
2986
3171
  }
2987
3172
  }
@@ -3152,4 +3337,4 @@ customElements.define("turbo-stream", StreamElement);
3152
3337
  window.Turbo = Turbo;
3153
3338
  start();
3154
3339
 
3155
- export { PageRenderer, PageSnapshot, clearCache, connectStreamSource, disconnectStreamSource, navigator$1 as navigator, registerAdapter, renderStreamMessage, session, setProgressBarDelay, start, visit };
3340
+ export { PageRenderer, PageSnapshot, clearCache, connectStreamSource, disconnectStreamSource, navigator$1 as navigator, registerAdapter, renderStreamMessage, session, setConfirmMethod, setProgressBarDelay, start, visit };