@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 (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;
@@ -39,12 +91,20 @@ Copyright © 2021 Basecamp, LLC
39
91
  }
40
92
  }
41
93
  (function () {
42
- if ("SubmitEvent" in window)
43
- return;
44
94
  if ("submitter" in Event.prototype)
45
95
  return;
96
+ let prototype;
97
+ if ("SubmitEvent" in window && /Apple Computer/.test(navigator.vendor)) {
98
+ prototype = window.SubmitEvent.prototype;
99
+ }
100
+ else if ("SubmitEvent" in window) {
101
+ return;
102
+ }
103
+ else {
104
+ prototype = window.Event.prototype;
105
+ }
46
106
  addEventListener("click", clickCaptured, true);
47
- Object.defineProperty(Event.prototype, "submitter", {
107
+ Object.defineProperty(prototype, "submitter", {
48
108
  get() {
49
109
  if (this.type == "submit" && this.target instanceof HTMLFormElement) {
50
110
  return submittersByForm.get(this.target);
@@ -163,6 +223,10 @@ Copyright © 2021 Basecamp, LLC
163
223
  return anchorMatch[1];
164
224
  }
165
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
+ }
166
230
  function getExtension(url) {
167
231
  return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || "";
168
232
  }
@@ -173,6 +237,9 @@ Copyright © 2021 Basecamp, LLC
173
237
  const prefix = getPrefix(url);
174
238
  return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix);
175
239
  }
240
+ function locationIsVisitable(location, rootLocation) {
241
+ return isPrefixedBy(location, rootLocation) && isHTML(location);
242
+ }
176
243
  function getRequestURL(url) {
177
244
  const anchor = getAnchor(url);
178
245
  return anchor != null
@@ -295,6 +362,29 @@ Copyright © 2021 Basecamp, LLC
295
362
  }
296
363
  }).join("");
297
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
+ }
298
388
 
299
389
  var FetchMethod;
300
390
  (function (FetchMethod) {
@@ -320,13 +410,10 @@ Copyright © 2021 Basecamp, LLC
320
410
  this.delegate = delegate;
321
411
  this.method = method;
322
412
  this.headers = this.defaultHeaders;
323
- if (this.isIdempotent) {
324
- this.url = mergeFormDataEntries(location, [...body.entries()]);
325
- }
326
- else {
327
- this.body = body;
328
- this.url = location;
329
- }
413
+ this.body = body;
414
+ this.url = this.isIdempotent ?
415
+ mergeFormDataEntries(new URL(location.href), this.entries) :
416
+ location;
330
417
  this.target = target;
331
418
  }
332
419
  get location() {
@@ -382,7 +469,7 @@ Copyright © 2021 Basecamp, LLC
382
469
  credentials: "same-origin",
383
470
  headers: this.headers,
384
471
  redirect: "follow",
385
- body: this.body,
472
+ body: this.isIdempotent ? null : this.body,
386
473
  signal: this.abortSignal,
387
474
  referrer: (_a = this.delegate.referrer) === null || _a === void 0 ? void 0 : _a.href
388
475
  };
@@ -404,7 +491,7 @@ Copyright © 2021 Basecamp, LLC
404
491
  cancelable: true,
405
492
  detail: {
406
493
  fetchOptions,
407
- url: this.url.href,
494
+ url: this.url,
408
495
  resume: this.resolveRequestPromise
409
496
  },
410
497
  target: this.target
@@ -414,18 +501,13 @@ Copyright © 2021 Basecamp, LLC
414
501
  }
415
502
  }
416
503
  function mergeFormDataEntries(url, entries) {
417
- const currentSearchParams = new URLSearchParams(url.search);
504
+ const searchParams = new URLSearchParams;
418
505
  for (const [name, value] of entries) {
419
506
  if (value instanceof File)
420
507
  continue;
421
- if (currentSearchParams.has(name)) {
422
- currentSearchParams.delete(name);
423
- url.searchParams.set(name, value);
424
- }
425
- else {
426
- url.searchParams.append(name, value);
427
- }
508
+ searchParams.append(name, value);
428
509
  }
510
+ url.search = searchParams.toString();
429
511
  return url;
430
512
  }
431
513
 
@@ -524,6 +606,9 @@ Copyright © 2021 Basecamp, LLC
524
606
  this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body, this.formElement);
525
607
  this.mustRedirect = mustRedirect;
526
608
  }
609
+ static confirmMethod(message, element) {
610
+ return confirm(message);
611
+ }
527
612
  get method() {
528
613
  var _a;
529
614
  const method = ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formmethod")) || this.formElement.getAttribute("method") || "";
@@ -557,8 +642,20 @@ Copyright © 2021 Basecamp, LLC
557
642
  return entries.concat(typeof value == "string" ? [[name, value]] : []);
558
643
  }, []);
559
644
  }
645
+ get confirmationMessage() {
646
+ return this.formElement.getAttribute("data-turbo-confirm");
647
+ }
648
+ get needsConfirmation() {
649
+ return this.confirmationMessage !== null;
650
+ }
560
651
  async start() {
561
652
  const { initialized, requesting } = FormSubmissionState;
653
+ if (this.needsConfirmation) {
654
+ const answer = FormSubmission.confirmMethod(this.confirmationMessage, this.formElement);
655
+ if (!answer) {
656
+ return;
657
+ }
658
+ }
562
659
  if (this.state == initialized) {
563
660
  this.state = requesting;
564
661
  return this.fetchRequest.perform();
@@ -582,7 +679,9 @@ Copyright © 2021 Basecamp, LLC
582
679
  }
583
680
  }
584
681
  requestStarted(request) {
682
+ var _a;
585
683
  this.state = FormSubmissionState.waiting;
684
+ (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.setAttribute("disabled", "");
586
685
  dispatch("turbo:submit-start", { target: this.formElement, detail: { formSubmission: this } });
587
686
  this.delegate.formSubmissionStarted(this);
588
687
  }
@@ -612,7 +711,9 @@ Copyright © 2021 Basecamp, LLC
612
711
  this.delegate.formSubmissionErrored(this, error);
613
712
  }
614
713
  requestFinished(request) {
714
+ var _a;
615
715
  this.state = FormSubmissionState.stopped;
716
+ (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.removeAttribute("disabled");
616
717
  dispatch("turbo:submit-end", { target: this.formElement, detail: Object.assign({ formSubmission: this }, this.result) });
617
718
  this.delegate.formSubmissionFinished(this);
618
719
  }
@@ -688,10 +789,11 @@ Copyright © 2021 Basecamp, LLC
688
789
  class FormInterceptor {
689
790
  constructor(delegate, element) {
690
791
  this.submitBubbled = ((event) => {
691
- if (event.target instanceof HTMLFormElement) {
692
- const form = event.target;
792
+ const form = event.target;
793
+ if (!event.defaultPrevented && form instanceof HTMLFormElement && form.closest("turbo-frame, html") == this.element) {
693
794
  const submitter = event.submitter || undefined;
694
- if (this.delegate.shouldInterceptFormSubmission(form, submitter)) {
795
+ const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.method;
796
+ if (method != "dialog" && this.delegate.shouldInterceptFormSubmission(form, submitter)) {
695
797
  event.preventDefault();
696
798
  event.stopImmediatePropagation();
697
799
  this.delegate.formSubmissionIntercepted(form, submitter);
@@ -906,10 +1008,11 @@ Copyright © 2021 Basecamp, LLC
906
1008
  }
907
1009
 
908
1010
  class Renderer {
909
- constructor(currentSnapshot, newSnapshot, isPreview) {
1011
+ constructor(currentSnapshot, newSnapshot, isPreview, willRender = true) {
910
1012
  this.currentSnapshot = currentSnapshot;
911
1013
  this.newSnapshot = newSnapshot;
912
1014
  this.isPreview = isPreview;
1015
+ this.willRender = willRender;
913
1016
  this.promise = new Promise((resolve, reject) => this.resolvingFunctions = { resolve, reject });
914
1017
  }
915
1018
  get shouldRender() {
@@ -1285,7 +1388,9 @@ Copyright © 2021 Basecamp, LLC
1285
1388
  })(VisitState || (VisitState = {}));
1286
1389
  const defaultOptions = {
1287
1390
  action: "advance",
1288
- historyChanged: false
1391
+ historyChanged: false,
1392
+ visitCachedSnapshot: () => { },
1393
+ willRender: true,
1289
1394
  };
1290
1395
  var SystemStatusCode;
1291
1396
  (function (SystemStatusCode) {
@@ -1305,13 +1410,16 @@ Copyright © 2021 Basecamp, LLC
1305
1410
  this.delegate = delegate;
1306
1411
  this.location = location;
1307
1412
  this.restorationIdentifier = restorationIdentifier || uuid();
1308
- const { action, historyChanged, referrer, snapshotHTML, response } = Object.assign(Object.assign({}, defaultOptions), options);
1413
+ const { action, historyChanged, referrer, snapshotHTML, response, visitCachedSnapshot, willRender } = Object.assign(Object.assign({}, defaultOptions), options);
1309
1414
  this.action = action;
1310
1415
  this.historyChanged = historyChanged;
1311
1416
  this.referrer = referrer;
1312
1417
  this.snapshotHTML = snapshotHTML;
1313
1418
  this.response = response;
1314
1419
  this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
1420
+ this.visitCachedSnapshot = visitCachedSnapshot;
1421
+ this.willRender = willRender;
1422
+ this.scrolled = !willRender;
1315
1423
  }
1316
1424
  get adapter() {
1317
1425
  return this.delegate.adapter;
@@ -1413,7 +1521,7 @@ Copyright © 2021 Basecamp, LLC
1413
1521
  if (this.view.renderPromise)
1414
1522
  await this.view.renderPromise;
1415
1523
  if (isSuccessful(statusCode) && responseHTML != null) {
1416
- await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML));
1524
+ await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML), false, this.willRender);
1417
1525
  this.adapter.visitRendered(this);
1418
1526
  this.complete();
1419
1527
  }
@@ -1453,7 +1561,7 @@ Copyright © 2021 Basecamp, LLC
1453
1561
  else {
1454
1562
  if (this.view.renderPromise)
1455
1563
  await this.view.renderPromise;
1456
- await this.view.renderPage(snapshot, isPreview);
1564
+ await this.view.renderPage(snapshot, isPreview, this.willRender);
1457
1565
  this.adapter.visitRendered(this);
1458
1566
  if (!isPreview) {
1459
1567
  this.complete();
@@ -1463,7 +1571,8 @@ Copyright © 2021 Basecamp, LLC
1463
1571
  }
1464
1572
  }
1465
1573
  followRedirect() {
1466
- if (this.redirectedToLocation && !this.followedRedirect) {
1574
+ var _a;
1575
+ if (this.redirectedToLocation && !this.followedRedirect && ((_a = this.response) === null || _a === void 0 ? void 0 : _a.redirected)) {
1467
1576
  this.adapter.visitProposedToLocation(this.redirectedToLocation, {
1468
1577
  action: 'replace',
1469
1578
  response: this.response
@@ -1486,25 +1595,27 @@ Copyright © 2021 Basecamp, LLC
1486
1595
  }
1487
1596
  async requestSucceededWithResponse(request, response) {
1488
1597
  const responseHTML = await response.responseHTML;
1598
+ const { redirected, statusCode } = response;
1489
1599
  if (responseHTML == undefined) {
1490
- this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch });
1600
+ this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch, redirected });
1491
1601
  }
1492
1602
  else {
1493
1603
  this.redirectedToLocation = response.redirected ? response.location : undefined;
1494
- this.recordResponse({ statusCode: response.statusCode, responseHTML });
1604
+ this.recordResponse({ statusCode: statusCode, responseHTML, redirected });
1495
1605
  }
1496
1606
  }
1497
1607
  async requestFailedWithResponse(request, response) {
1498
1608
  const responseHTML = await response.responseHTML;
1609
+ const { redirected, statusCode } = response;
1499
1610
  if (responseHTML == undefined) {
1500
- this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch });
1611
+ this.recordResponse({ statusCode: SystemStatusCode.contentTypeMismatch, redirected });
1501
1612
  }
1502
1613
  else {
1503
- this.recordResponse({ statusCode: response.statusCode, responseHTML });
1614
+ this.recordResponse({ statusCode: statusCode, responseHTML, redirected });
1504
1615
  }
1505
1616
  }
1506
1617
  requestErrored(request, error) {
1507
- this.recordResponse({ statusCode: SystemStatusCode.networkFailure });
1618
+ this.recordResponse({ statusCode: SystemStatusCode.networkFailure, redirected: false });
1508
1619
  }
1509
1620
  requestFinished() {
1510
1621
  this.finishRequest();
@@ -1561,12 +1672,12 @@ Copyright © 2021 Basecamp, LLC
1561
1672
  return !this.hasCachedSnapshot();
1562
1673
  }
1563
1674
  else {
1564
- return true;
1675
+ return this.willRender;
1565
1676
  }
1566
1677
  }
1567
1678
  cacheSnapshot() {
1568
1679
  if (!this.snapshotCached) {
1569
- this.view.cacheSnapshot();
1680
+ this.view.cacheSnapshot().then(snapshot => snapshot && this.visitCachedSnapshot(snapshot));
1570
1681
  this.snapshotCached = true;
1571
1682
  }
1572
1683
  }
@@ -1602,10 +1713,10 @@ Copyright © 2021 Basecamp, LLC
1602
1713
  this.navigator.startVisit(location, uuid(), options);
1603
1714
  }
1604
1715
  visitStarted(visit) {
1716
+ visit.loadCachedSnapshot();
1605
1717
  visit.issueRequest();
1606
1718
  visit.changeHistory();
1607
1719
  visit.goToSamePageAnchor();
1608
- visit.loadCachedSnapshot();
1609
1720
  }
1610
1721
  visitRequestStarted(visit) {
1611
1722
  this.progressBar.setValue(0);
@@ -1716,7 +1827,7 @@ Copyright © 2021 Basecamp, LLC
1716
1827
  const form = event.target instanceof HTMLFormElement ? event.target : undefined;
1717
1828
  const submitter = event.submitter || undefined;
1718
1829
  if (form) {
1719
- const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.method;
1830
+ const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.getAttribute("method");
1720
1831
  if (method != "dialog" && this.delegate.willSubmitForm(form, submitter)) {
1721
1832
  event.preventDefault();
1722
1833
  this.delegate.formSubmitted(form, submitter);
@@ -1760,12 +1871,11 @@ Copyright © 2021 Basecamp, LLC
1760
1871
  linkClickIntercepted(element, url) {
1761
1872
  const frame = this.findFrameElement(element);
1762
1873
  if (frame) {
1763
- frame.setAttribute("reloadable", "");
1764
- frame.src = url;
1874
+ frame.delegate.linkClickIntercepted(element, url);
1765
1875
  }
1766
1876
  }
1767
1877
  shouldInterceptFormSubmission(element, submitter) {
1768
- return this.shouldRedirect(element, submitter);
1878
+ return this.shouldSubmit(element, submitter);
1769
1879
  }
1770
1880
  formSubmissionIntercepted(element, submitter) {
1771
1881
  const frame = this.findFrameElement(element, submitter);
@@ -1774,6 +1884,13 @@ Copyright © 2021 Basecamp, LLC
1774
1884
  frame.delegate.formSubmissionIntercepted(element, submitter);
1775
1885
  }
1776
1886
  }
1887
+ shouldSubmit(form, submitter) {
1888
+ var _a;
1889
+ const action = getAction(form, submitter);
1890
+ const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
1891
+ const rootLocation = expandURL((_a = meta === null || meta === void 0 ? void 0 : meta.content) !== null && _a !== void 0 ? _a : "/");
1892
+ return this.shouldRedirect(form, submitter) && locationIsVisitable(action, rootLocation);
1893
+ }
1777
1894
  shouldRedirect(element, submitter) {
1778
1895
  const frame = this.findFrameElement(element, submitter);
1779
1896
  return frame ? frame != element.closest("turbo-frame") : false;
@@ -1931,7 +2048,12 @@ Copyright © 2021 Basecamp, LLC
1931
2048
  }
1932
2049
  proposeVisit(location, options = {}) {
1933
2050
  if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
1934
- this.delegate.visitProposedToLocation(location, options);
2051
+ if (locationIsVisitable(location, this.view.snapshot.rootLocation)) {
2052
+ this.delegate.visitProposedToLocation(location, options);
2053
+ }
2054
+ else {
2055
+ window.location.href = location.toString();
2056
+ }
1935
2057
  }
1936
2058
  }
1937
2059
  startVisit(locatable, restorationIdentifier, options = {}) {
@@ -1942,12 +2064,7 @@ Copyright © 2021 Basecamp, LLC
1942
2064
  submitForm(form, submitter) {
1943
2065
  this.stop();
1944
2066
  this.formSubmission = new FormSubmission(this, form, submitter, true);
1945
- if (this.formSubmission.isIdempotent) {
1946
- this.proposeVisit(this.formSubmission.fetchRequest.url, { action: this.getActionForFormSubmission(this.formSubmission) });
1947
- }
1948
- else {
1949
- this.formSubmission.start();
1950
- }
2067
+ this.formSubmission.start();
1951
2068
  }
1952
2069
  stop() {
1953
2070
  if (this.formSubmission) {
@@ -1980,8 +2097,9 @@ Copyright © 2021 Basecamp, LLC
1980
2097
  if (formSubmission.method != FetchMethod.get) {
1981
2098
  this.view.clearSnapshotCache();
1982
2099
  }
1983
- const { statusCode } = fetchResponse;
1984
- const visitOptions = { response: { statusCode, responseHTML } };
2100
+ const { statusCode, redirected } = fetchResponse;
2101
+ const action = this.getActionForFormSubmission(formSubmission);
2102
+ const visitOptions = { action, response: { statusCode, responseHTML, redirected } };
1985
2103
  this.proposeVisit(fetchResponse.location, visitOptions);
1986
2104
  }
1987
2105
  }
@@ -2033,7 +2151,7 @@ Copyright © 2021 Basecamp, LLC
2033
2151
  }
2034
2152
  getActionForFormSubmission(formSubmission) {
2035
2153
  const { formElement, submitter } = formSubmission;
2036
- const action = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("data-turbo-action")) || formElement.getAttribute("data-turbo-action");
2154
+ const action = getAttribute("data-turbo-action", submitter, formElement);
2037
2155
  return isAction(action) ? action : "advance";
2038
2156
  }
2039
2157
  }
@@ -2227,7 +2345,9 @@ Copyright © 2021 Basecamp, LLC
2227
2345
  this.mergeHead();
2228
2346
  }
2229
2347
  async render() {
2230
- this.replaceBody();
2348
+ if (this.willRender) {
2349
+ this.replaceBody();
2350
+ }
2231
2351
  }
2232
2352
  finishRendering() {
2233
2353
  super.finishRendering();
@@ -2365,8 +2485,8 @@ Copyright © 2021 Basecamp, LLC
2365
2485
  this.snapshotCache = new SnapshotCache(10);
2366
2486
  this.lastRenderedLocation = new URL(location.href);
2367
2487
  }
2368
- renderPage(snapshot, isPreview = false) {
2369
- const renderer = new PageRenderer(this.snapshot, snapshot, isPreview);
2488
+ renderPage(snapshot, isPreview = false, willRender = true) {
2489
+ const renderer = new PageRenderer(this.snapshot, snapshot, isPreview, willRender);
2370
2490
  return this.render(renderer);
2371
2491
  }
2372
2492
  renderError(snapshot) {
@@ -2381,7 +2501,9 @@ Copyright © 2021 Basecamp, LLC
2381
2501
  this.delegate.viewWillCacheSnapshot();
2382
2502
  const { snapshot, lastRenderedLocation: location } = this;
2383
2503
  await nextEventLoopTick();
2384
- this.snapshotCache.put(location, snapshot.clone());
2504
+ const cachedSnapshot = snapshot.clone();
2505
+ this.snapshotCache.put(location, cachedSnapshot);
2506
+ return cachedSnapshot;
2385
2507
  }
2386
2508
  }
2387
2509
  getCachedSnapshotForLocation(location) {
@@ -2483,7 +2605,7 @@ Copyright © 2021 Basecamp, LLC
2483
2605
  }
2484
2606
  willFollowLinkToLocation(link, location) {
2485
2607
  return this.elementDriveEnabled(link)
2486
- && this.locationIsVisitable(location)
2608
+ && locationIsVisitable(location, this.snapshot.rootLocation)
2487
2609
  && this.applicationAllowsFollowingLinkToLocation(link, location);
2488
2610
  }
2489
2611
  followedLinkToLocation(link, location) {
@@ -2491,14 +2613,24 @@ Copyright © 2021 Basecamp, LLC
2491
2613
  this.convertLinkWithMethodClickToFormSubmission(link) || this.visit(location.href, { action });
2492
2614
  }
2493
2615
  convertLinkWithMethodClickToFormSubmission(link) {
2494
- var _a;
2495
2616
  const linkMethod = link.getAttribute("data-turbo-method");
2496
2617
  if (linkMethod) {
2497
2618
  const form = document.createElement("form");
2498
2619
  form.method = linkMethod;
2499
2620
  form.action = link.getAttribute("href") || "undefined";
2500
2621
  form.hidden = true;
2501
- (_a = link.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(form, link);
2622
+ if (link.hasAttribute("data-turbo-confirm")) {
2623
+ form.setAttribute("data-turbo-confirm", link.getAttribute("data-turbo-confirm"));
2624
+ }
2625
+ const frame = this.getTargetFrameForLink(link);
2626
+ if (frame) {
2627
+ form.setAttribute("data-turbo-frame", frame);
2628
+ form.addEventListener("turbo:submit-start", () => form.remove());
2629
+ }
2630
+ else {
2631
+ form.addEventListener("submit", () => form.remove());
2632
+ }
2633
+ document.body.appendChild(form);
2502
2634
  return dispatch("submit", { cancelable: true, target: form });
2503
2635
  }
2504
2636
  else {
@@ -2528,7 +2660,10 @@ Copyright © 2021 Basecamp, LLC
2528
2660
  this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
2529
2661
  }
2530
2662
  willSubmitForm(form, submitter) {
2531
- return this.elementDriveEnabled(form) && this.elementDriveEnabled(submitter);
2663
+ const action = getAction(form, submitter);
2664
+ return this.elementDriveEnabled(form)
2665
+ && (!submitter || this.elementDriveEnabled(submitter))
2666
+ && locationIsVisitable(expandURL(action), this.snapshot.rootLocation);
2532
2667
  }
2533
2668
  formSubmitted(form, submitter) {
2534
2669
  this.navigator.submitForm(form, submitter);
@@ -2584,6 +2719,7 @@ Copyright © 2021 Basecamp, LLC
2584
2719
  return dispatch("turbo:before-visit", { detail: { url: location.href }, cancelable: true });
2585
2720
  }
2586
2721
  notifyApplicationAfterVisitingLocation(location, action) {
2722
+ markAsBusy(document.documentElement);
2587
2723
  return dispatch("turbo:visit", { detail: { url: location.href, action } });
2588
2724
  }
2589
2725
  notifyApplicationBeforeCachingSnapshot() {
@@ -2596,6 +2732,7 @@ Copyright © 2021 Basecamp, LLC
2596
2732
  return dispatch("turbo:render");
2597
2733
  }
2598
2734
  notifyApplicationAfterPageLoad(timing = {}) {
2735
+ clearBusyState(document.documentElement);
2599
2736
  return dispatch("turbo:load", { detail: { url: this.location.href, timing } });
2600
2737
  }
2601
2738
  notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL) {
@@ -2630,8 +2767,17 @@ Copyright © 2021 Basecamp, LLC
2630
2767
  const action = link.getAttribute("data-turbo-action");
2631
2768
  return isAction(action) ? action : "advance";
2632
2769
  }
2633
- locationIsVisitable(location) {
2634
- return isPrefixedBy(location, this.snapshot.rootLocation) && isHTML(location);
2770
+ getTargetFrameForLink(link) {
2771
+ const frame = link.getAttribute("data-turbo-frame");
2772
+ if (frame) {
2773
+ return frame;
2774
+ }
2775
+ else {
2776
+ const container = link.closest("turbo-frame");
2777
+ if (container) {
2778
+ return container.id;
2779
+ }
2780
+ }
2635
2781
  }
2636
2782
  get snapshot() {
2637
2783
  return this.view.snapshot;
@@ -2649,7 +2795,7 @@ Copyright © 2021 Basecamp, LLC
2649
2795
  };
2650
2796
 
2651
2797
  const session = new Session;
2652
- const { navigator } = session;
2798
+ const { navigator: navigator$1 } = session;
2653
2799
  function start() {
2654
2800
  session.start();
2655
2801
  }
@@ -2674,10 +2820,13 @@ Copyright © 2021 Basecamp, LLC
2674
2820
  function setProgressBarDelay(delay) {
2675
2821
  session.setProgressBarDelay(delay);
2676
2822
  }
2823
+ function setConfirmMethod(confirmMethod) {
2824
+ FormSubmission.confirmMethod = confirmMethod;
2825
+ }
2677
2826
 
2678
2827
  var Turbo = /*#__PURE__*/Object.freeze({
2679
2828
  __proto__: null,
2680
- navigator: navigator,
2829
+ navigator: navigator$1,
2681
2830
  session: session,
2682
2831
  PageRenderer: PageRenderer,
2683
2832
  PageSnapshot: PageSnapshot,
@@ -2688,11 +2837,14 @@ Copyright © 2021 Basecamp, LLC
2688
2837
  disconnectStreamSource: disconnectStreamSource,
2689
2838
  renderStreamMessage: renderStreamMessage,
2690
2839
  clearCache: clearCache,
2691
- setProgressBarDelay: setProgressBarDelay
2840
+ setProgressBarDelay: setProgressBarDelay,
2841
+ setConfirmMethod: setConfirmMethod
2692
2842
  });
2693
2843
 
2694
2844
  class FrameController {
2695
2845
  constructor(element) {
2846
+ this.fetchResponseLoaded = (fetchResponse) => { };
2847
+ this.currentFetchRequest = null;
2696
2848
  this.resolveVisitPromise = () => { };
2697
2849
  this.connected = false;
2698
2850
  this.hasBeenLoaded = false;
@@ -2748,11 +2900,10 @@ Copyright © 2021 Basecamp, LLC
2748
2900
  this.currentURL = this.sourceURL;
2749
2901
  if (this.sourceURL) {
2750
2902
  try {
2751
- this.element.loaded = this.visit(this.sourceURL);
2903
+ this.element.loaded = this.visit(expandURL(this.sourceURL));
2752
2904
  this.appearanceObserver.stop();
2753
2905
  await this.element.loaded;
2754
2906
  this.hasBeenLoaded = true;
2755
- session.frameLoaded(this.element);
2756
2907
  }
2757
2908
  catch (error) {
2758
2909
  this.currentURL = previousURL;
@@ -2762,7 +2913,7 @@ Copyright © 2021 Basecamp, LLC
2762
2913
  }
2763
2914
  }
2764
2915
  async loadResponse(fetchResponse) {
2765
- if (fetchResponse.redirected) {
2916
+ if (fetchResponse.redirected || (fetchResponse.succeeded && fetchResponse.isHTML)) {
2766
2917
  this.sourceURL = fetchResponse.response.url;
2767
2918
  }
2768
2919
  try {
@@ -2770,17 +2921,22 @@ Copyright © 2021 Basecamp, LLC
2770
2921
  if (html) {
2771
2922
  const { body } = parseHTMLDocument(html);
2772
2923
  const snapshot = new Snapshot(await this.extractForeignFrameElement(body));
2773
- const renderer = new FrameRenderer(this.view.snapshot, snapshot, false);
2924
+ const renderer = new FrameRenderer(this.view.snapshot, snapshot, false, false);
2774
2925
  if (this.view.renderPromise)
2775
2926
  await this.view.renderPromise;
2776
2927
  await this.view.render(renderer);
2777
2928
  session.frameRendered(fetchResponse, this.element);
2929
+ session.frameLoaded(this.element);
2930
+ this.fetchResponseLoaded(fetchResponse);
2778
2931
  }
2779
2932
  }
2780
2933
  catch (error) {
2781
2934
  console.error(error);
2782
2935
  this.view.invalidate();
2783
2936
  }
2937
+ finally {
2938
+ this.fetchResponseLoaded = () => { };
2939
+ }
2784
2940
  }
2785
2941
  elementAppearedInViewport(element) {
2786
2942
  this.loadSourceURL();
@@ -2806,20 +2962,15 @@ Copyright © 2021 Basecamp, LLC
2806
2962
  }
2807
2963
  this.reloadable = false;
2808
2964
  this.formSubmission = new FormSubmission(this, element, submitter);
2809
- if (this.formSubmission.fetchRequest.isIdempotent) {
2810
- this.navigateFrame(element, this.formSubmission.fetchRequest.url.href, submitter);
2811
- }
2812
- else {
2813
- const { fetchRequest } = this.formSubmission;
2814
- this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
2815
- this.formSubmission.start();
2816
- }
2965
+ const { fetchRequest } = this.formSubmission;
2966
+ this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
2967
+ this.formSubmission.start();
2817
2968
  }
2818
2969
  prepareHeadersForRequest(headers, request) {
2819
2970
  headers["Turbo-Frame"] = this.id;
2820
2971
  }
2821
2972
  requestStarted(request) {
2822
- this.element.setAttribute("busy", "");
2973
+ markAsBusy(this.element);
2823
2974
  }
2824
2975
  requestPreventedHandlingResponse(request, response) {
2825
2976
  this.resolveVisitPromise();
@@ -2837,14 +2988,14 @@ Copyright © 2021 Basecamp, LLC
2837
2988
  this.resolveVisitPromise();
2838
2989
  }
2839
2990
  requestFinished(request) {
2840
- this.element.removeAttribute("busy");
2991
+ clearBusyState(this.element);
2841
2992
  }
2842
- formSubmissionStarted(formSubmission) {
2843
- const frame = this.findFrameElement(formSubmission.formElement);
2844
- frame.setAttribute("busy", "");
2993
+ formSubmissionStarted({ formElement }) {
2994
+ markAsBusy(formElement, this.findFrameElement(formElement));
2845
2995
  }
2846
2996
  formSubmissionSucceededWithResponse(formSubmission, response) {
2847
2997
  const frame = this.findFrameElement(formSubmission.formElement, formSubmission.submitter);
2998
+ this.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
2848
2999
  frame.delegate.loadResponse(response);
2849
3000
  }
2850
3001
  formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
@@ -2853,9 +3004,8 @@ Copyright © 2021 Basecamp, LLC
2853
3004
  formSubmissionErrored(formSubmission, error) {
2854
3005
  console.error(error);
2855
3006
  }
2856
- formSubmissionFinished(formSubmission) {
2857
- const frame = this.findFrameElement(formSubmission.formElement);
2858
- frame.removeAttribute("busy");
3007
+ formSubmissionFinished({ formElement }) {
3008
+ clearBusyState(formElement, this.findFrameElement(formElement));
2859
3009
  }
2860
3010
  allowsImmediateRender(snapshot, resume) {
2861
3011
  return true;
@@ -2865,10 +3015,14 @@ Copyright © 2021 Basecamp, LLC
2865
3015
  viewInvalidated() {
2866
3016
  }
2867
3017
  async visit(url) {
2868
- const request = new FetchRequest(this, FetchMethod.get, expandURL(url), undefined, this.element);
3018
+ var _a;
3019
+ const request = new FetchRequest(this, FetchMethod.get, url, url.searchParams, this.element);
3020
+ (_a = this.currentFetchRequest) === null || _a === void 0 ? void 0 : _a.cancel();
3021
+ this.currentFetchRequest = request;
2869
3022
  return new Promise(resolve => {
2870
3023
  this.resolveVisitPromise = () => {
2871
3024
  this.resolveVisitPromise = () => { };
3025
+ this.currentFetchRequest = null;
2872
3026
  resolve();
2873
3027
  };
2874
3028
  request.perform();
@@ -2876,12 +3030,27 @@ Copyright © 2021 Basecamp, LLC
2876
3030
  }
2877
3031
  navigateFrame(element, url, submitter) {
2878
3032
  const frame = this.findFrameElement(element, submitter);
3033
+ this.proposeVisitIfNavigatedWithAction(frame, element, submitter);
2879
3034
  frame.setAttribute("reloadable", "");
2880
3035
  frame.src = url;
2881
3036
  }
3037
+ proposeVisitIfNavigatedWithAction(frame, element, submitter) {
3038
+ const action = getAttribute("data-turbo-action", submitter, element, frame);
3039
+ if (isAction(action)) {
3040
+ const { visitCachedSnapshot } = new SnapshotSubstitution(frame);
3041
+ frame.delegate.fetchResponseLoaded = (fetchResponse) => {
3042
+ if (frame.src) {
3043
+ const { statusCode, redirected } = fetchResponse;
3044
+ const responseHTML = frame.ownerDocument.documentElement.outerHTML;
3045
+ const response = { statusCode, redirected, responseHTML };
3046
+ session.visit(frame.src, { action, response, visitCachedSnapshot, willRender: false });
3047
+ }
3048
+ };
3049
+ }
3050
+ }
2882
3051
  findFrameElement(element, submitter) {
2883
3052
  var _a;
2884
- const id = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("data-turbo-frame")) || element.getAttribute("data-turbo-frame") || this.element.getAttribute("target");
3053
+ const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
2885
3054
  return (_a = getFrameElementById(id)) !== null && _a !== void 0 ? _a : this.element;
2886
3055
  }
2887
3056
  async extractForeignFrameElement(container) {
@@ -2902,8 +3071,15 @@ Copyright © 2021 Basecamp, LLC
2902
3071
  }
2903
3072
  return new FrameElement();
2904
3073
  }
3074
+ formActionIsVisitable(form, submitter) {
3075
+ const action = getAction(form, submitter);
3076
+ return locationIsVisitable(expandURL(action), this.rootLocation);
3077
+ }
2905
3078
  shouldInterceptNavigation(element, submitter) {
2906
- const id = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("data-turbo-frame")) || element.getAttribute("data-turbo-frame") || this.element.getAttribute("target");
3079
+ const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
3080
+ if (element instanceof HTMLFormElement && !this.formActionIsVisitable(element, submitter)) {
3081
+ return false;
3082
+ }
2907
3083
  if (!this.enabled || id == "_top") {
2908
3084
  return false;
2909
3085
  }
@@ -2960,6 +3136,23 @@ Copyright © 2021 Basecamp, LLC
2960
3136
  get isActive() {
2961
3137
  return this.element.isActive && this.connected;
2962
3138
  }
3139
+ get rootLocation() {
3140
+ var _a;
3141
+ const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
3142
+ const root = (_a = meta === null || meta === void 0 ? void 0 : meta.content) !== null && _a !== void 0 ? _a : "/";
3143
+ return expandURL(root);
3144
+ }
3145
+ }
3146
+ class SnapshotSubstitution {
3147
+ constructor(element) {
3148
+ this.visitCachedSnapshot = ({ element }) => {
3149
+ var _a;
3150
+ const { id, clone } = this;
3151
+ (_a = element.querySelector("#" + id)) === null || _a === void 0 ? void 0 : _a.replaceWith(clone);
3152
+ };
3153
+ this.clone = element.cloneNode(true);
3154
+ this.id = element.id;
3155
+ }
2963
3156
  }
2964
3157
  function getFrameElementById(id) {
2965
3158
  if (id != null) {
@@ -2980,6 +3173,7 @@ Copyright © 2021 Basecamp, LLC
2980
3173
  }
2981
3174
  if (element instanceof FrameElement) {
2982
3175
  element.connectedCallback();
3176
+ element.disconnectedCallback();
2983
3177
  return element;
2984
3178
  }
2985
3179
  }
@@ -3155,10 +3349,11 @@ Copyright © 2021 Basecamp, LLC
3155
3349
  exports.clearCache = clearCache;
3156
3350
  exports.connectStreamSource = connectStreamSource;
3157
3351
  exports.disconnectStreamSource = disconnectStreamSource;
3158
- exports.navigator = navigator;
3352
+ exports.navigator = navigator$1;
3159
3353
  exports.registerAdapter = registerAdapter;
3160
3354
  exports.renderStreamMessage = renderStreamMessage;
3161
3355
  exports.session = session;
3356
+ exports.setConfirmMethod = setConfirmMethod;
3162
3357
  exports.setProgressBarDelay = setProgressBarDelay;
3163
3358
  exports.start = start;
3164
3359
  exports.visit = visit;