turbo-rails 0.8.0 → 0.9.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.
@@ -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