turbo-rails 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -14,6 +14,31 @@
14
14
  Object.setPrototypeOf(HTMLElement, BuiltInHTMLElement);
15
15
  })();
16
16
 
17
+ (function(prototype) {
18
+ if (typeof prototype.requestSubmit == "function") return;
19
+ prototype.requestSubmit = function(submitter) {
20
+ if (submitter) {
21
+ validateSubmitter(submitter, this);
22
+ submitter.click();
23
+ } else {
24
+ submitter = document.createElement("input");
25
+ submitter.type = "submit";
26
+ submitter.hidden = true;
27
+ this.appendChild(submitter);
28
+ submitter.click();
29
+ this.removeChild(submitter);
30
+ }
31
+ };
32
+ function validateSubmitter(submitter, form) {
33
+ submitter instanceof HTMLElement || raise(TypeError, "parameter 1 is not of type 'HTMLElement'");
34
+ submitter.type == "submit" || raise(TypeError, "The specified element is not a submit button");
35
+ submitter.form == form || raise(DOMException, "The specified element is not owned by this form element", "NotFoundError");
36
+ }
37
+ function raise(errorConstructor, message, name) {
38
+ throw new errorConstructor("Failed to execute 'requestSubmit' on 'HTMLFormElement': " + message + ".", name);
39
+ }
40
+ })(HTMLFormElement.prototype);
41
+
17
42
  const submittersByForm = new WeakMap;
18
43
 
19
44
  function findSubmitterFromClickTarget(target) {
@@ -30,10 +55,17 @@ function clickCaptured(event) {
30
55
  }
31
56
 
32
57
  (function() {
33
- if ("SubmitEvent" in window) return;
34
58
  if ("submitter" in Event.prototype) return;
59
+ let prototype;
60
+ if ("SubmitEvent" in window && /Apple Computer/.test(navigator.vendor)) {
61
+ prototype = window.SubmitEvent.prototype;
62
+ } else if ("SubmitEvent" in window) {
63
+ return;
64
+ } else {
65
+ prototype = window.Event.prototype;
66
+ }
35
67
  addEventListener("click", clickCaptured, true);
36
- Object.defineProperty(Event.prototype, "submitter", {
68
+ Object.defineProperty(prototype, "submitter", {
37
69
  get() {
38
70
  if (this.type == "submit" && this.target instanceof HTMLFormElement) {
39
71
  return submittersByForm.get(this.target);
@@ -153,6 +185,11 @@ function getAnchor(url) {
153
185
  }
154
186
  }
155
187
 
188
+ function getAction(form, submitter) {
189
+ const action = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formaction")) || form.getAttribute("action") || form.action;
190
+ return expandURL(action);
191
+ }
192
+
156
193
  function getExtension(url) {
157
194
  return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || "";
158
195
  }
@@ -166,6 +203,10 @@ function isPrefixedBy(baseURL, url) {
166
203
  return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix);
167
204
  }
168
205
 
206
+ function locationIsVisitable(location, rootLocation) {
207
+ return isPrefixedBy(location, rootLocation) && isHTML(location);
208
+ }
209
+
169
210
  function getRequestURL(url) {
170
211
  const anchor = getAnchor(url);
171
212
  return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href;
@@ -301,6 +342,31 @@ function uuid() {
301
342
  })).join("");
302
343
  }
303
344
 
345
+ function getAttribute(attributeName, ...elements) {
346
+ for (const value of elements.map((element => element === null || element === void 0 ? void 0 : element.getAttribute(attributeName)))) {
347
+ if (typeof value == "string") return value;
348
+ }
349
+ return null;
350
+ }
351
+
352
+ function markAsBusy(...elements) {
353
+ for (const element of elements) {
354
+ if (element.localName == "turbo-frame") {
355
+ element.setAttribute("busy", "");
356
+ }
357
+ element.setAttribute("aria-busy", "true");
358
+ }
359
+ }
360
+
361
+ function clearBusyState(...elements) {
362
+ for (const element of elements) {
363
+ if (element.localName == "turbo-frame") {
364
+ element.removeAttribute("busy");
365
+ }
366
+ element.removeAttribute("aria-busy");
367
+ }
368
+ }
369
+
304
370
  var FetchMethod;
305
371
 
306
372
  (function(FetchMethod) {
@@ -337,12 +403,8 @@ class FetchRequest {
337
403
  this.delegate = delegate;
338
404
  this.method = method;
339
405
  this.headers = this.defaultHeaders;
340
- if (this.isIdempotent) {
341
- this.url = mergeFormDataEntries(location, [ ...body.entries() ]);
342
- } else {
343
- this.body = body;
344
- this.url = location;
345
- }
406
+ this.body = body;
407
+ this.url = location;
346
408
  this.target = target;
347
409
  }
348
410
  get location() {
@@ -400,7 +462,7 @@ class FetchRequest {
400
462
  credentials: "same-origin",
401
463
  headers: this.headers,
402
464
  redirect: "follow",
403
- body: this.body,
465
+ body: this.isIdempotent ? null : this.body,
404
466
  signal: this.abortSignal,
405
467
  referrer: (_a = this.delegate.referrer) === null || _a === void 0 ? void 0 : _a.href
406
468
  };
@@ -422,7 +484,7 @@ class FetchRequest {
422
484
  cancelable: true,
423
485
  detail: {
424
486
  fetchOptions: fetchOptions,
425
- url: this.url.href,
487
+ url: this.url,
426
488
  resume: this.resolveRequestPromise
427
489
  },
428
490
  target: this.target
@@ -431,20 +493,6 @@ class FetchRequest {
431
493
  }
432
494
  }
433
495
 
434
- function mergeFormDataEntries(url, entries) {
435
- const currentSearchParams = new URLSearchParams(url.search);
436
- for (const [name, value] of entries) {
437
- if (value instanceof File) continue;
438
- if (currentSearchParams.has(name)) {
439
- currentSearchParams.delete(name);
440
- url.searchParams.set(name, value);
441
- } else {
442
- url.searchParams.append(name, value);
443
- }
444
- }
445
- return url;
446
- }
447
-
448
496
  class AppearanceObserver {
449
497
  constructor(delegate, element) {
450
498
  this.started = false;
@@ -546,9 +594,16 @@ class FormSubmission {
546
594
  this.formElement = formElement;
547
595
  this.submitter = submitter;
548
596
  this.formData = buildFormData(formElement, submitter);
597
+ this.location = expandURL(this.action);
598
+ if (this.method == FetchMethod.get) {
599
+ mergeFormDataEntries(this.location, [ ...this.body.entries() ]);
600
+ }
549
601
  this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body, this.formElement);
550
602
  this.mustRedirect = mustRedirect;
551
603
  }
604
+ static confirmMethod(message, element) {
605
+ return confirm(message);
606
+ }
552
607
  get method() {
553
608
  var _a;
554
609
  const method = ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formmethod")) || this.formElement.getAttribute("method") || "";
@@ -559,9 +614,6 @@ class FormSubmission {
559
614
  const formElementAction = typeof this.formElement.action === "string" ? this.formElement.action : null;
560
615
  return ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formaction")) || this.formElement.getAttribute("action") || formElementAction || "";
561
616
  }
562
- get location() {
563
- return expandURL(this.action);
564
- }
565
617
  get body() {
566
618
  if (this.enctype == FormEnctype.urlEncoded || this.method == FetchMethod.get) {
567
619
  return new URLSearchParams(this.stringFormData);
@@ -579,8 +631,20 @@ class FormSubmission {
579
631
  get stringFormData() {
580
632
  return [ ...this.formData ].reduce(((entries, [name, value]) => entries.concat(typeof value == "string" ? [ [ name, value ] ] : [])), []);
581
633
  }
634
+ get confirmationMessage() {
635
+ return this.formElement.getAttribute("data-turbo-confirm");
636
+ }
637
+ get needsConfirmation() {
638
+ return this.confirmationMessage !== null;
639
+ }
582
640
  async start() {
583
641
  const {initialized: initialized, requesting: requesting} = FormSubmissionState;
642
+ if (this.needsConfirmation) {
643
+ const answer = FormSubmission.confirmMethod(this.confirmationMessage, this.formElement);
644
+ if (!answer) {
645
+ return;
646
+ }
647
+ }
584
648
  if (this.state == initialized) {
585
649
  this.state = requesting;
586
650
  return this.fetchRequest.perform();
@@ -604,7 +668,9 @@ class FormSubmission {
604
668
  }
605
669
  }
606
670
  requestStarted(request) {
671
+ var _a;
607
672
  this.state = FormSubmissionState.waiting;
673
+ (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.setAttribute("disabled", "");
608
674
  dispatch("turbo:submit-start", {
609
675
  target: this.formElement,
610
676
  detail: {
@@ -649,7 +715,9 @@ class FormSubmission {
649
715
  this.delegate.formSubmissionErrored(this, error);
650
716
  }
651
717
  requestFinished(request) {
718
+ var _a;
652
719
  this.state = FormSubmissionState.stopped;
720
+ (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.removeAttribute("disabled");
653
721
  dispatch("turbo:submit-end", {
654
722
  target: this.formElement,
655
723
  detail: Object.assign({
@@ -693,6 +761,16 @@ function responseSucceededWithoutRedirect(response) {
693
761
  return response.statusCode == 200 && !response.redirected;
694
762
  }
695
763
 
764
+ function mergeFormDataEntries(url, entries) {
765
+ const searchParams = new URLSearchParams;
766
+ for (const [name, value] of entries) {
767
+ if (value instanceof File) continue;
768
+ searchParams.append(name, value);
769
+ }
770
+ url.search = searchParams.toString();
771
+ return url;
772
+ }
773
+
696
774
  class Snapshot {
697
775
  constructor(element) {
698
776
  this.element = element;
@@ -734,10 +812,11 @@ class Snapshot {
734
812
  class FormInterceptor {
735
813
  constructor(delegate, element) {
736
814
  this.submitBubbled = event => {
737
- if (event.target instanceof HTMLFormElement) {
738
- const form = event.target;
815
+ const form = event.target;
816
+ if (!event.defaultPrevented && form instanceof HTMLFormElement && form.closest("turbo-frame, html") == this.element) {
739
817
  const submitter = event.submitter || undefined;
740
- if (this.delegate.shouldInterceptFormSubmission(form, submitter)) {
818
+ const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.method;
819
+ if (method != "dialog" && this.delegate.shouldInterceptFormSubmission(form, submitter)) {
741
820
  event.preventDefault();
742
821
  event.stopImmediatePropagation();
743
822
  this.delegate.formSubmissionIntercepted(form, submitter);
@@ -948,10 +1027,11 @@ function createPlaceholderForPermanentElement(permanentElement) {
948
1027
  }
949
1028
 
950
1029
  class Renderer {
951
- constructor(currentSnapshot, newSnapshot, isPreview) {
1030
+ constructor(currentSnapshot, newSnapshot, isPreview, willRender = true) {
952
1031
  this.currentSnapshot = currentSnapshot;
953
1032
  this.newSnapshot = newSnapshot;
954
1033
  this.isPreview = isPreview;
1034
+ this.willRender = willRender;
955
1035
  this.promise = new Promise(((resolve, reject) => this.resolvingFunctions = {
956
1036
  resolve: resolve,
957
1037
  reject: reject
@@ -1333,7 +1413,9 @@ var VisitState;
1333
1413
 
1334
1414
  const defaultOptions = {
1335
1415
  action: "advance",
1336
- historyChanged: false
1416
+ historyChanged: false,
1417
+ visitCachedSnapshot: () => {},
1418
+ willRender: true
1337
1419
  };
1338
1420
 
1339
1421
  var SystemStatusCode;
@@ -1356,13 +1438,16 @@ class Visit {
1356
1438
  this.delegate = delegate;
1357
1439
  this.location = location;
1358
1440
  this.restorationIdentifier = restorationIdentifier || uuid();
1359
- const {action: action, historyChanged: historyChanged, referrer: referrer, snapshotHTML: snapshotHTML, response: response} = Object.assign(Object.assign({}, defaultOptions), options);
1441
+ const {action: action, historyChanged: historyChanged, referrer: referrer, snapshotHTML: snapshotHTML, response: response, visitCachedSnapshot: visitCachedSnapshot, willRender: willRender} = Object.assign(Object.assign({}, defaultOptions), options);
1360
1442
  this.action = action;
1361
1443
  this.historyChanged = historyChanged;
1362
1444
  this.referrer = referrer;
1363
1445
  this.snapshotHTML = snapshotHTML;
1364
1446
  this.response = response;
1365
1447
  this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
1448
+ this.visitCachedSnapshot = visitCachedSnapshot;
1449
+ this.willRender = willRender;
1450
+ this.scrolled = !willRender;
1366
1451
  }
1367
1452
  get adapter() {
1368
1453
  return this.delegate.adapter;
@@ -1461,7 +1546,7 @@ class Visit {
1461
1546
  this.cacheSnapshot();
1462
1547
  if (this.view.renderPromise) await this.view.renderPromise;
1463
1548
  if (isSuccessful(statusCode) && responseHTML != null) {
1464
- await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML));
1549
+ await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML), false, this.willRender);
1465
1550
  this.adapter.visitRendered(this);
1466
1551
  this.complete();
1467
1552
  } else {
@@ -1498,7 +1583,7 @@ class Visit {
1498
1583
  this.adapter.visitRendered(this);
1499
1584
  } else {
1500
1585
  if (this.view.renderPromise) await this.view.renderPromise;
1501
- await this.view.renderPage(snapshot, isPreview);
1586
+ await this.view.renderPage(snapshot, isPreview, this.willRender);
1502
1587
  this.adapter.visitRendered(this);
1503
1588
  if (!isPreview) {
1504
1589
  this.complete();
@@ -1508,7 +1593,8 @@ class Visit {
1508
1593
  }
1509
1594
  }
1510
1595
  followRedirect() {
1511
- if (this.redirectedToLocation && !this.followedRedirect) {
1596
+ var _a;
1597
+ if (this.redirectedToLocation && !this.followedRedirect && ((_a = this.response) === null || _a === void 0 ? void 0 : _a.redirected)) {
1512
1598
  this.adapter.visitProposedToLocation(this.redirectedToLocation, {
1513
1599
  action: "replace",
1514
1600
  response: this.response
@@ -1530,34 +1616,41 @@ class Visit {
1530
1616
  requestPreventedHandlingResponse(request, response) {}
1531
1617
  async requestSucceededWithResponse(request, response) {
1532
1618
  const responseHTML = await response.responseHTML;
1619
+ const {redirected: redirected, statusCode: statusCode} = response;
1533
1620
  if (responseHTML == undefined) {
1534
1621
  this.recordResponse({
1535
- statusCode: SystemStatusCode.contentTypeMismatch
1622
+ statusCode: SystemStatusCode.contentTypeMismatch,
1623
+ redirected: redirected
1536
1624
  });
1537
1625
  } else {
1538
1626
  this.redirectedToLocation = response.redirected ? response.location : undefined;
1539
1627
  this.recordResponse({
1540
- statusCode: response.statusCode,
1541
- responseHTML: responseHTML
1628
+ statusCode: statusCode,
1629
+ responseHTML: responseHTML,
1630
+ redirected: redirected
1542
1631
  });
1543
1632
  }
1544
1633
  }
1545
1634
  async requestFailedWithResponse(request, response) {
1546
1635
  const responseHTML = await response.responseHTML;
1636
+ const {redirected: redirected, statusCode: statusCode} = response;
1547
1637
  if (responseHTML == undefined) {
1548
1638
  this.recordResponse({
1549
- statusCode: SystemStatusCode.contentTypeMismatch
1639
+ statusCode: SystemStatusCode.contentTypeMismatch,
1640
+ redirected: redirected
1550
1641
  });
1551
1642
  } else {
1552
1643
  this.recordResponse({
1553
- statusCode: response.statusCode,
1554
- responseHTML: responseHTML
1644
+ statusCode: statusCode,
1645
+ responseHTML: responseHTML,
1646
+ redirected: redirected
1555
1647
  });
1556
1648
  }
1557
1649
  }
1558
1650
  requestErrored(request, error) {
1559
1651
  this.recordResponse({
1560
- statusCode: SystemStatusCode.networkFailure
1652
+ statusCode: SystemStatusCode.networkFailure,
1653
+ redirected: false
1561
1654
  });
1562
1655
  }
1563
1656
  requestFinished() {
@@ -1615,12 +1708,12 @@ class Visit {
1615
1708
  } else if (this.action == "restore") {
1616
1709
  return !this.hasCachedSnapshot();
1617
1710
  } else {
1618
- return true;
1711
+ return this.willRender;
1619
1712
  }
1620
1713
  }
1621
1714
  cacheSnapshot() {
1622
1715
  if (!this.snapshotCached) {
1623
- this.view.cacheSnapshot();
1716
+ this.view.cacheSnapshot().then((snapshot => snapshot && this.visitCachedSnapshot(snapshot)));
1624
1717
  this.snapshotCached = true;
1625
1718
  }
1626
1719
  }
@@ -1657,10 +1750,10 @@ class BrowserAdapter {
1657
1750
  this.navigator.startVisit(location, uuid(), options);
1658
1751
  }
1659
1752
  visitStarted(visit) {
1753
+ visit.loadCachedSnapshot();
1660
1754
  visit.issueRequest();
1661
1755
  visit.changeHistory();
1662
1756
  visit.goToSamePageAnchor();
1663
- visit.loadCachedSnapshot();
1664
1757
  }
1665
1758
  visitRequestStarted(visit) {
1666
1759
  this.progressBar.setValue(0);
@@ -1768,7 +1861,7 @@ class FormSubmitObserver {
1768
1861
  const form = event.target instanceof HTMLFormElement ? event.target : undefined;
1769
1862
  const submitter = event.submitter || undefined;
1770
1863
  if (form) {
1771
- const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.method;
1864
+ const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.getAttribute("method");
1772
1865
  if (method != "dialog" && this.delegate.willSubmitForm(form, submitter)) {
1773
1866
  event.preventDefault();
1774
1867
  this.delegate.formSubmitted(form, submitter);
@@ -1812,12 +1905,11 @@ class FrameRedirector {
1812
1905
  linkClickIntercepted(element, url) {
1813
1906
  const frame = this.findFrameElement(element);
1814
1907
  if (frame) {
1815
- frame.setAttribute("reloadable", "");
1816
- frame.src = url;
1908
+ frame.delegate.linkClickIntercepted(element, url);
1817
1909
  }
1818
1910
  }
1819
1911
  shouldInterceptFormSubmission(element, submitter) {
1820
- return this.shouldRedirect(element, submitter);
1912
+ return this.shouldSubmit(element, submitter);
1821
1913
  }
1822
1914
  formSubmissionIntercepted(element, submitter) {
1823
1915
  const frame = this.findFrameElement(element, submitter);
@@ -1826,6 +1918,13 @@ class FrameRedirector {
1826
1918
  frame.delegate.formSubmissionIntercepted(element, submitter);
1827
1919
  }
1828
1920
  }
1921
+ shouldSubmit(form, submitter) {
1922
+ var _a;
1923
+ const action = getAction(form, submitter);
1924
+ const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
1925
+ const rootLocation = expandURL((_a = meta === null || meta === void 0 ? void 0 : meta.content) !== null && _a !== void 0 ? _a : "/");
1926
+ return this.shouldRedirect(form, submitter) && locationIsVisitable(action, rootLocation);
1927
+ }
1829
1928
  shouldRedirect(element, submitter) {
1830
1929
  const frame = this.findFrameElement(element, submitter);
1831
1930
  return frame ? frame != element.closest("turbo-frame") : false;
@@ -1981,7 +2080,11 @@ class Navigator {
1981
2080
  }
1982
2081
  proposeVisit(location, options = {}) {
1983
2082
  if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
1984
- this.delegate.visitProposedToLocation(location, options);
2083
+ if (locationIsVisitable(location, this.view.snapshot.rootLocation)) {
2084
+ this.delegate.visitProposedToLocation(location, options);
2085
+ } else {
2086
+ window.location.href = location.toString();
2087
+ }
1985
2088
  }
1986
2089
  }
1987
2090
  startVisit(locatable, restorationIdentifier, options = {}) {
@@ -1994,13 +2097,7 @@ class Navigator {
1994
2097
  submitForm(form, submitter) {
1995
2098
  this.stop();
1996
2099
  this.formSubmission = new FormSubmission(this, form, submitter, true);
1997
- if (this.formSubmission.isIdempotent) {
1998
- this.proposeVisit(this.formSubmission.fetchRequest.url, {
1999
- action: this.getActionForFormSubmission(this.formSubmission)
2000
- });
2001
- } else {
2002
- this.formSubmission.start();
2003
- }
2100
+ this.formSubmission.start();
2004
2101
  }
2005
2102
  stop() {
2006
2103
  if (this.formSubmission) {
@@ -2033,11 +2130,14 @@ class Navigator {
2033
2130
  if (formSubmission.method != FetchMethod.get) {
2034
2131
  this.view.clearSnapshotCache();
2035
2132
  }
2036
- const {statusCode: statusCode} = fetchResponse;
2133
+ const {statusCode: statusCode, redirected: redirected} = fetchResponse;
2134
+ const action = this.getActionForFormSubmission(formSubmission);
2037
2135
  const visitOptions = {
2136
+ action: action,
2038
2137
  response: {
2039
2138
  statusCode: statusCode,
2040
- responseHTML: responseHTML
2139
+ responseHTML: responseHTML,
2140
+ redirected: redirected
2041
2141
  }
2042
2142
  };
2043
2143
  this.proposeVisit(fetchResponse.location, visitOptions);
@@ -2088,7 +2188,7 @@ class Navigator {
2088
2188
  }
2089
2189
  getActionForFormSubmission(formSubmission) {
2090
2190
  const {formElement: formElement, submitter: submitter} = formSubmission;
2091
- const action = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("data-turbo-action")) || formElement.getAttribute("data-turbo-action");
2191
+ const action = getAttribute("data-turbo-action", submitter, formElement);
2092
2192
  return isAction(action) ? action : "advance";
2093
2193
  }
2094
2194
  }
@@ -2288,7 +2388,9 @@ class PageRenderer extends Renderer {
2288
2388
  this.mergeHead();
2289
2389
  }
2290
2390
  async render() {
2291
- this.replaceBody();
2391
+ if (this.willRender) {
2392
+ this.replaceBody();
2393
+ }
2292
2394
  }
2293
2395
  finishRendering() {
2294
2396
  super.finishRendering();
@@ -2424,8 +2526,8 @@ class PageView extends View {
2424
2526
  this.snapshotCache = new SnapshotCache(10);
2425
2527
  this.lastRenderedLocation = new URL(location.href);
2426
2528
  }
2427
- renderPage(snapshot, isPreview = false) {
2428
- const renderer = new PageRenderer(this.snapshot, snapshot, isPreview);
2529
+ renderPage(snapshot, isPreview = false, willRender = true) {
2530
+ const renderer = new PageRenderer(this.snapshot, snapshot, isPreview, willRender);
2429
2531
  return this.render(renderer);
2430
2532
  }
2431
2533
  renderError(snapshot) {
@@ -2440,7 +2542,9 @@ class PageView extends View {
2440
2542
  this.delegate.viewWillCacheSnapshot();
2441
2543
  const {snapshot: snapshot, lastRenderedLocation: location} = this;
2442
2544
  await nextEventLoopTick();
2443
- this.snapshotCache.put(location, snapshot.clone());
2545
+ const cachedSnapshot = snapshot.clone();
2546
+ this.snapshotCache.put(location, cachedSnapshot);
2547
+ return cachedSnapshot;
2444
2548
  }
2445
2549
  }
2446
2550
  getCachedSnapshotForLocation(location) {
@@ -2545,7 +2649,7 @@ class Session {
2545
2649
  });
2546
2650
  }
2547
2651
  willFollowLinkToLocation(link, location) {
2548
- return this.elementDriveEnabled(link) && this.locationIsVisitable(location) && this.applicationAllowsFollowingLinkToLocation(link, location);
2652
+ return this.elementDriveEnabled(link) && locationIsVisitable(location, this.snapshot.rootLocation) && this.applicationAllowsFollowingLinkToLocation(link, location);
2549
2653
  }
2550
2654
  followedLinkToLocation(link, location) {
2551
2655
  const action = this.getActionForLink(link);
@@ -2554,14 +2658,23 @@ class Session {
2554
2658
  });
2555
2659
  }
2556
2660
  convertLinkWithMethodClickToFormSubmission(link) {
2557
- var _a;
2558
2661
  const linkMethod = link.getAttribute("data-turbo-method");
2559
2662
  if (linkMethod) {
2560
2663
  const form = document.createElement("form");
2561
2664
  form.method = linkMethod;
2562
2665
  form.action = link.getAttribute("href") || "undefined";
2563
2666
  form.hidden = true;
2564
- (_a = link.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(form, link);
2667
+ if (link.hasAttribute("data-turbo-confirm")) {
2668
+ form.setAttribute("data-turbo-confirm", link.getAttribute("data-turbo-confirm"));
2669
+ }
2670
+ const frame = this.getTargetFrameForLink(link);
2671
+ if (frame) {
2672
+ form.setAttribute("data-turbo-frame", frame);
2673
+ form.addEventListener("turbo:submit-start", (() => form.remove()));
2674
+ } else {
2675
+ form.addEventListener("submit", (() => form.remove()));
2676
+ }
2677
+ document.body.appendChild(form);
2565
2678
  return dispatch("submit", {
2566
2679
  cancelable: true,
2567
2680
  target: form
@@ -2593,7 +2706,8 @@ class Session {
2593
2706
  this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
2594
2707
  }
2595
2708
  willSubmitForm(form, submitter) {
2596
- return this.elementDriveEnabled(form) && this.elementDriveEnabled(submitter);
2709
+ const action = getAction(form, submitter);
2710
+ return this.elementDriveEnabled(form) && (!submitter || this.elementDriveEnabled(submitter)) && locationIsVisitable(expandURL(action), this.snapshot.rootLocation);
2597
2711
  }
2598
2712
  formSubmitted(form, submitter) {
2599
2713
  this.navigator.submitForm(form, submitter);
@@ -2660,6 +2774,7 @@ class Session {
2660
2774
  });
2661
2775
  }
2662
2776
  notifyApplicationAfterVisitingLocation(location, action) {
2777
+ markAsBusy(document.documentElement);
2663
2778
  return dispatch("turbo:visit", {
2664
2779
  detail: {
2665
2780
  url: location.href,
@@ -2683,6 +2798,7 @@ class Session {
2683
2798
  return dispatch("turbo:render");
2684
2799
  }
2685
2800
  notifyApplicationAfterPageLoad(timing = {}) {
2801
+ clearBusyState(document.documentElement);
2686
2802
  return dispatch("turbo:load", {
2687
2803
  detail: {
2688
2804
  url: this.location.href,
@@ -2730,8 +2846,16 @@ class Session {
2730
2846
  const action = link.getAttribute("data-turbo-action");
2731
2847
  return isAction(action) ? action : "advance";
2732
2848
  }
2733
- locationIsVisitable(location) {
2734
- return isPrefixedBy(location, this.snapshot.rootLocation) && isHTML(location);
2849
+ getTargetFrameForLink(link) {
2850
+ const frame = link.getAttribute("data-turbo-frame");
2851
+ if (frame) {
2852
+ return frame;
2853
+ } else {
2854
+ const container = link.closest("turbo-frame");
2855
+ if (container) {
2856
+ return container.id;
2857
+ }
2858
+ }
2735
2859
  }
2736
2860
  get snapshot() {
2737
2861
  return this.view.snapshot;
@@ -2752,7 +2876,7 @@ const deprecatedLocationPropertyDescriptors = {
2752
2876
 
2753
2877
  const session = new Session;
2754
2878
 
2755
- const {navigator: navigator} = session;
2879
+ const {navigator: navigator$1} = session;
2756
2880
 
2757
2881
  function start() {
2758
2882
  session.start();
@@ -2786,9 +2910,13 @@ function setProgressBarDelay(delay) {
2786
2910
  session.setProgressBarDelay(delay);
2787
2911
  }
2788
2912
 
2913
+ function setConfirmMethod(confirmMethod) {
2914
+ FormSubmission.confirmMethod = confirmMethod;
2915
+ }
2916
+
2789
2917
  var Turbo = Object.freeze({
2790
2918
  __proto__: null,
2791
- navigator: navigator,
2919
+ navigator: navigator$1,
2792
2920
  session: session,
2793
2921
  PageRenderer: PageRenderer,
2794
2922
  PageSnapshot: PageSnapshot,
@@ -2799,11 +2927,14 @@ var Turbo = Object.freeze({
2799
2927
  disconnectStreamSource: disconnectStreamSource,
2800
2928
  renderStreamMessage: renderStreamMessage,
2801
2929
  clearCache: clearCache,
2802
- setProgressBarDelay: setProgressBarDelay
2930
+ setProgressBarDelay: setProgressBarDelay,
2931
+ setConfirmMethod: setConfirmMethod
2803
2932
  });
2804
2933
 
2805
2934
  class FrameController {
2806
2935
  constructor(element) {
2936
+ this.fetchResponseLoaded = fetchResponse => {};
2937
+ this.currentFetchRequest = null;
2807
2938
  this.resolveVisitPromise = () => {};
2808
2939
  this.connected = false;
2809
2940
  this.hasBeenLoaded = false;
@@ -2858,11 +2989,10 @@ class FrameController {
2858
2989
  this.currentURL = this.sourceURL;
2859
2990
  if (this.sourceURL) {
2860
2991
  try {
2861
- this.element.loaded = this.visit(this.sourceURL);
2992
+ this.element.loaded = this.visit(expandURL(this.sourceURL));
2862
2993
  this.appearanceObserver.stop();
2863
2994
  await this.element.loaded;
2864
2995
  this.hasBeenLoaded = true;
2865
- session.frameLoaded(this.element);
2866
2996
  } catch (error) {
2867
2997
  this.currentURL = previousURL;
2868
2998
  throw error;
@@ -2871,7 +3001,7 @@ class FrameController {
2871
3001
  }
2872
3002
  }
2873
3003
  async loadResponse(fetchResponse) {
2874
- if (fetchResponse.redirected) {
3004
+ if (fetchResponse.redirected || fetchResponse.succeeded && fetchResponse.isHTML) {
2875
3005
  this.sourceURL = fetchResponse.response.url;
2876
3006
  }
2877
3007
  try {
@@ -2879,14 +3009,18 @@ class FrameController {
2879
3009
  if (html) {
2880
3010
  const {body: body} = parseHTMLDocument(html);
2881
3011
  const snapshot = new Snapshot(await this.extractForeignFrameElement(body));
2882
- const renderer = new FrameRenderer(this.view.snapshot, snapshot, false);
3012
+ const renderer = new FrameRenderer(this.view.snapshot, snapshot, false, false);
2883
3013
  if (this.view.renderPromise) await this.view.renderPromise;
2884
3014
  await this.view.render(renderer);
2885
3015
  session.frameRendered(fetchResponse, this.element);
3016
+ session.frameLoaded(this.element);
3017
+ this.fetchResponseLoaded(fetchResponse);
2886
3018
  }
2887
3019
  } catch (error) {
2888
3020
  console.error(error);
2889
3021
  this.view.invalidate();
3022
+ } finally {
3023
+ this.fetchResponseLoaded = () => {};
2890
3024
  }
2891
3025
  }
2892
3026
  elementAppearedInViewport(element) {
@@ -2912,19 +3046,15 @@ class FrameController {
2912
3046
  }
2913
3047
  this.reloadable = false;
2914
3048
  this.formSubmission = new FormSubmission(this, element, submitter);
2915
- if (this.formSubmission.fetchRequest.isIdempotent) {
2916
- this.navigateFrame(element, this.formSubmission.fetchRequest.url.href, submitter);
2917
- } else {
2918
- const {fetchRequest: fetchRequest} = this.formSubmission;
2919
- this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
2920
- this.formSubmission.start();
2921
- }
3049
+ const {fetchRequest: fetchRequest} = this.formSubmission;
3050
+ this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
3051
+ this.formSubmission.start();
2922
3052
  }
2923
3053
  prepareHeadersForRequest(headers, request) {
2924
3054
  headers["Turbo-Frame"] = this.id;
2925
3055
  }
2926
3056
  requestStarted(request) {
2927
- this.element.setAttribute("busy", "");
3057
+ markAsBusy(this.element);
2928
3058
  }
2929
3059
  requestPreventedHandlingResponse(request, response) {
2930
3060
  this.resolveVisitPromise();
@@ -2942,14 +3072,14 @@ class FrameController {
2942
3072
  this.resolveVisitPromise();
2943
3073
  }
2944
3074
  requestFinished(request) {
2945
- this.element.removeAttribute("busy");
3075
+ clearBusyState(this.element);
2946
3076
  }
2947
- formSubmissionStarted(formSubmission) {
2948
- const frame = this.findFrameElement(formSubmission.formElement);
2949
- frame.setAttribute("busy", "");
3077
+ formSubmissionStarted({formElement: formElement}) {
3078
+ markAsBusy(formElement, this.findFrameElement(formElement));
2950
3079
  }
2951
3080
  formSubmissionSucceededWithResponse(formSubmission, response) {
2952
3081
  const frame = this.findFrameElement(formSubmission.formElement, formSubmission.submitter);
3082
+ this.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
2953
3083
  frame.delegate.loadResponse(response);
2954
3084
  }
2955
3085
  formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
@@ -2958,9 +3088,8 @@ class FrameController {
2958
3088
  formSubmissionErrored(formSubmission, error) {
2959
3089
  console.error(error);
2960
3090
  }
2961
- formSubmissionFinished(formSubmission) {
2962
- const frame = this.findFrameElement(formSubmission.formElement);
2963
- frame.removeAttribute("busy");
3091
+ formSubmissionFinished({formElement: formElement}) {
3092
+ clearBusyState(formElement, this.findFrameElement(formElement));
2964
3093
  }
2965
3094
  allowsImmediateRender(snapshot, resume) {
2966
3095
  return true;
@@ -2968,10 +3097,14 @@ class FrameController {
2968
3097
  viewRenderedSnapshot(snapshot, isPreview) {}
2969
3098
  viewInvalidated() {}
2970
3099
  async visit(url) {
2971
- const request = new FetchRequest(this, FetchMethod.get, expandURL(url), undefined, this.element);
3100
+ var _a;
3101
+ const request = new FetchRequest(this, FetchMethod.get, url, new URLSearchParams, this.element);
3102
+ (_a = this.currentFetchRequest) === null || _a === void 0 ? void 0 : _a.cancel();
3103
+ this.currentFetchRequest = request;
2972
3104
  return new Promise((resolve => {
2973
3105
  this.resolveVisitPromise = () => {
2974
3106
  this.resolveVisitPromise = () => {};
3107
+ this.currentFetchRequest = null;
2975
3108
  resolve();
2976
3109
  };
2977
3110
  request.perform();
@@ -2979,12 +3112,36 @@ class FrameController {
2979
3112
  }
2980
3113
  navigateFrame(element, url, submitter) {
2981
3114
  const frame = this.findFrameElement(element, submitter);
3115
+ this.proposeVisitIfNavigatedWithAction(frame, element, submitter);
2982
3116
  frame.setAttribute("reloadable", "");
2983
3117
  frame.src = url;
2984
3118
  }
3119
+ proposeVisitIfNavigatedWithAction(frame, element, submitter) {
3120
+ const action = getAttribute("data-turbo-action", submitter, element, frame);
3121
+ if (isAction(action)) {
3122
+ const {visitCachedSnapshot: visitCachedSnapshot} = new SnapshotSubstitution(frame);
3123
+ frame.delegate.fetchResponseLoaded = fetchResponse => {
3124
+ if (frame.src) {
3125
+ const {statusCode: statusCode, redirected: redirected} = fetchResponse;
3126
+ const responseHTML = frame.ownerDocument.documentElement.outerHTML;
3127
+ const response = {
3128
+ statusCode: statusCode,
3129
+ redirected: redirected,
3130
+ responseHTML: responseHTML
3131
+ };
3132
+ session.visit(frame.src, {
3133
+ action: action,
3134
+ response: response,
3135
+ visitCachedSnapshot: visitCachedSnapshot,
3136
+ willRender: false
3137
+ });
3138
+ }
3139
+ };
3140
+ }
3141
+ }
2985
3142
  findFrameElement(element, submitter) {
2986
3143
  var _a;
2987
- const id = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("data-turbo-frame")) || element.getAttribute("data-turbo-frame") || this.element.getAttribute("target");
3144
+ const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
2988
3145
  return (_a = getFrameElementById(id)) !== null && _a !== void 0 ? _a : this.element;
2989
3146
  }
2990
3147
  async extractForeignFrameElement(container) {
@@ -3004,8 +3161,15 @@ class FrameController {
3004
3161
  }
3005
3162
  return new FrameElement;
3006
3163
  }
3164
+ formActionIsVisitable(form, submitter) {
3165
+ const action = getAction(form, submitter);
3166
+ return locationIsVisitable(expandURL(action), this.rootLocation);
3167
+ }
3007
3168
  shouldInterceptNavigation(element, submitter) {
3008
- const id = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("data-turbo-frame")) || element.getAttribute("data-turbo-frame") || this.element.getAttribute("target");
3169
+ const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
3170
+ if (element instanceof HTMLFormElement && !this.formActionIsVisitable(element, submitter)) {
3171
+ return false;
3172
+ }
3009
3173
  if (!this.enabled || id == "_top") {
3010
3174
  return false;
3011
3175
  }
@@ -3061,6 +3225,24 @@ class FrameController {
3061
3225
  get isActive() {
3062
3226
  return this.element.isActive && this.connected;
3063
3227
  }
3228
+ get rootLocation() {
3229
+ var _a;
3230
+ const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
3231
+ const root = (_a = meta === null || meta === void 0 ? void 0 : meta.content) !== null && _a !== void 0 ? _a : "/";
3232
+ return expandURL(root);
3233
+ }
3234
+ }
3235
+
3236
+ class SnapshotSubstitution {
3237
+ constructor(element) {
3238
+ this.visitCachedSnapshot = ({element: element}) => {
3239
+ var _a;
3240
+ const {id: id, clone: clone} = this;
3241
+ (_a = element.querySelector("#" + id)) === null || _a === void 0 ? void 0 : _a.replaceWith(clone);
3242
+ };
3243
+ this.clone = element.cloneNode(true);
3244
+ this.id = element.id;
3245
+ }
3064
3246
  }
3065
3247
 
3066
3248
  function getFrameElementById(id) {
@@ -3083,6 +3265,7 @@ function activateElement(element, currentURL) {
3083
3265
  }
3084
3266
  if (element instanceof FrameElement) {
3085
3267
  element.connectedCallback();
3268
+ element.disconnectedCallback();
3086
3269
  return element;
3087
3270
  }
3088
3271
  }
@@ -3263,10 +3446,11 @@ var turbo_es2017Esm = Object.freeze({
3263
3446
  clearCache: clearCache,
3264
3447
  connectStreamSource: connectStreamSource,
3265
3448
  disconnectStreamSource: disconnectStreamSource,
3266
- navigator: navigator,
3449
+ navigator: navigator$1,
3267
3450
  registerAdapter: registerAdapter,
3268
3451
  renderStreamMessage: renderStreamMessage,
3269
3452
  session: session,
3453
+ setConfirmMethod: setConfirmMethod,
3270
3454
  setProgressBarDelay: setProgressBarDelay,
3271
3455
  start: start,
3272
3456
  visit: visit