turbo-rails 0.8.2 → 1.0.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) {
@@ -160,6 +185,11 @@ function getAnchor(url) {
160
185
  }
161
186
  }
162
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
+
163
193
  function getExtension(url) {
164
194
  return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || "";
165
195
  }
@@ -173,6 +203,10 @@ function isPrefixedBy(baseURL, url) {
173
203
  return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix);
174
204
  }
175
205
 
206
+ function locationIsVisitable(location, rootLocation) {
207
+ return isPrefixedBy(location, rootLocation) && isHTML(location);
208
+ }
209
+
176
210
  function getRequestURL(url) {
177
211
  const anchor = getAnchor(url);
178
212
  return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href;
@@ -308,6 +342,31 @@ function uuid() {
308
342
  })).join("");
309
343
  }
310
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
+
311
370
  var FetchMethod;
312
371
 
313
372
  (function(FetchMethod) {
@@ -344,12 +403,8 @@ class FetchRequest {
344
403
  this.delegate = delegate;
345
404
  this.method = method;
346
405
  this.headers = this.defaultHeaders;
347
- if (this.isIdempotent) {
348
- this.url = mergeFormDataEntries(location, [ ...body.entries() ]);
349
- } else {
350
- this.body = body;
351
- this.url = location;
352
- }
406
+ this.body = body;
407
+ this.url = location;
353
408
  this.target = target;
354
409
  }
355
410
  get location() {
@@ -407,7 +462,7 @@ class FetchRequest {
407
462
  credentials: "same-origin",
408
463
  headers: this.headers,
409
464
  redirect: "follow",
410
- body: this.body,
465
+ body: this.isIdempotent ? null : this.body,
411
466
  signal: this.abortSignal,
412
467
  referrer: (_a = this.delegate.referrer) === null || _a === void 0 ? void 0 : _a.href
413
468
  };
@@ -429,7 +484,7 @@ class FetchRequest {
429
484
  cancelable: true,
430
485
  detail: {
431
486
  fetchOptions: fetchOptions,
432
- url: this.url.href,
487
+ url: this.url,
433
488
  resume: this.resolveRequestPromise
434
489
  },
435
490
  target: this.target
@@ -438,20 +493,6 @@ class FetchRequest {
438
493
  }
439
494
  }
440
495
 
441
- function mergeFormDataEntries(url, entries) {
442
- const currentSearchParams = new URLSearchParams(url.search);
443
- for (const [name, value] of entries) {
444
- if (value instanceof File) continue;
445
- if (currentSearchParams.has(name)) {
446
- currentSearchParams.delete(name);
447
- url.searchParams.set(name, value);
448
- } else {
449
- url.searchParams.append(name, value);
450
- }
451
- }
452
- return url;
453
- }
454
-
455
496
  class AppearanceObserver {
456
497
  constructor(delegate, element) {
457
498
  this.started = false;
@@ -553,9 +594,16 @@ class FormSubmission {
553
594
  this.formElement = formElement;
554
595
  this.submitter = submitter;
555
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
+ }
556
601
  this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body, this.formElement);
557
602
  this.mustRedirect = mustRedirect;
558
603
  }
604
+ static confirmMethod(message, element) {
605
+ return confirm(message);
606
+ }
559
607
  get method() {
560
608
  var _a;
561
609
  const method = ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formmethod")) || this.formElement.getAttribute("method") || "";
@@ -566,9 +614,6 @@ class FormSubmission {
566
614
  const formElementAction = typeof this.formElement.action === "string" ? this.formElement.action : null;
567
615
  return ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formaction")) || this.formElement.getAttribute("action") || formElementAction || "";
568
616
  }
569
- get location() {
570
- return expandURL(this.action);
571
- }
572
617
  get body() {
573
618
  if (this.enctype == FormEnctype.urlEncoded || this.method == FetchMethod.get) {
574
619
  return new URLSearchParams(this.stringFormData);
@@ -586,8 +631,20 @@ class FormSubmission {
586
631
  get stringFormData() {
587
632
  return [ ...this.formData ].reduce(((entries, [name, value]) => entries.concat(typeof value == "string" ? [ [ name, value ] ] : [])), []);
588
633
  }
634
+ get confirmationMessage() {
635
+ return this.formElement.getAttribute("data-turbo-confirm");
636
+ }
637
+ get needsConfirmation() {
638
+ return this.confirmationMessage !== null;
639
+ }
589
640
  async start() {
590
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
+ }
591
648
  if (this.state == initialized) {
592
649
  this.state = requesting;
593
650
  return this.fetchRequest.perform();
@@ -611,7 +668,9 @@ class FormSubmission {
611
668
  }
612
669
  }
613
670
  requestStarted(request) {
671
+ var _a;
614
672
  this.state = FormSubmissionState.waiting;
673
+ (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.setAttribute("disabled", "");
615
674
  dispatch("turbo:submit-start", {
616
675
  target: this.formElement,
617
676
  detail: {
@@ -656,7 +715,9 @@ class FormSubmission {
656
715
  this.delegate.formSubmissionErrored(this, error);
657
716
  }
658
717
  requestFinished(request) {
718
+ var _a;
659
719
  this.state = FormSubmissionState.stopped;
720
+ (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.removeAttribute("disabled");
660
721
  dispatch("turbo:submit-end", {
661
722
  target: this.formElement,
662
723
  detail: Object.assign({
@@ -700,6 +761,16 @@ function responseSucceededWithoutRedirect(response) {
700
761
  return response.statusCode == 200 && !response.redirected;
701
762
  }
702
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
+
703
774
  class Snapshot {
704
775
  constructor(element) {
705
776
  this.element = element;
@@ -742,9 +813,10 @@ class FormInterceptor {
742
813
  constructor(delegate, element) {
743
814
  this.submitBubbled = event => {
744
815
  const form = event.target;
745
- if (form instanceof HTMLFormElement && form.closest("turbo-frame, html") == this.element) {
816
+ if (!event.defaultPrevented && form instanceof HTMLFormElement && form.closest("turbo-frame, html") == this.element) {
746
817
  const submitter = event.submitter || undefined;
747
- 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)) {
748
820
  event.preventDefault();
749
821
  event.stopImmediatePropagation();
750
822
  this.delegate.formSubmissionIntercepted(form, submitter);
@@ -955,10 +1027,11 @@ function createPlaceholderForPermanentElement(permanentElement) {
955
1027
  }
956
1028
 
957
1029
  class Renderer {
958
- constructor(currentSnapshot, newSnapshot, isPreview) {
1030
+ constructor(currentSnapshot, newSnapshot, isPreview, willRender = true) {
959
1031
  this.currentSnapshot = currentSnapshot;
960
1032
  this.newSnapshot = newSnapshot;
961
1033
  this.isPreview = isPreview;
1034
+ this.willRender = willRender;
962
1035
  this.promise = new Promise(((resolve, reject) => this.resolvingFunctions = {
963
1036
  resolve: resolve,
964
1037
  reject: reject
@@ -1340,7 +1413,9 @@ var VisitState;
1340
1413
 
1341
1414
  const defaultOptions = {
1342
1415
  action: "advance",
1343
- historyChanged: false
1416
+ historyChanged: false,
1417
+ visitCachedSnapshot: () => {},
1418
+ willRender: true
1344
1419
  };
1345
1420
 
1346
1421
  var SystemStatusCode;
@@ -1363,13 +1438,16 @@ class Visit {
1363
1438
  this.delegate = delegate;
1364
1439
  this.location = location;
1365
1440
  this.restorationIdentifier = restorationIdentifier || uuid();
1366
- 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);
1367
1442
  this.action = action;
1368
1443
  this.historyChanged = historyChanged;
1369
1444
  this.referrer = referrer;
1370
1445
  this.snapshotHTML = snapshotHTML;
1371
1446
  this.response = response;
1372
1447
  this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
1448
+ this.visitCachedSnapshot = visitCachedSnapshot;
1449
+ this.willRender = willRender;
1450
+ this.scrolled = !willRender;
1373
1451
  }
1374
1452
  get adapter() {
1375
1453
  return this.delegate.adapter;
@@ -1468,7 +1546,7 @@ class Visit {
1468
1546
  this.cacheSnapshot();
1469
1547
  if (this.view.renderPromise) await this.view.renderPromise;
1470
1548
  if (isSuccessful(statusCode) && responseHTML != null) {
1471
- await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML));
1549
+ await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML), false, this.willRender);
1472
1550
  this.adapter.visitRendered(this);
1473
1551
  this.complete();
1474
1552
  } else {
@@ -1505,7 +1583,7 @@ class Visit {
1505
1583
  this.adapter.visitRendered(this);
1506
1584
  } else {
1507
1585
  if (this.view.renderPromise) await this.view.renderPromise;
1508
- await this.view.renderPage(snapshot, isPreview);
1586
+ await this.view.renderPage(snapshot, isPreview, this.willRender);
1509
1587
  this.adapter.visitRendered(this);
1510
1588
  if (!isPreview) {
1511
1589
  this.complete();
@@ -1515,7 +1593,8 @@ class Visit {
1515
1593
  }
1516
1594
  }
1517
1595
  followRedirect() {
1518
- 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)) {
1519
1598
  this.adapter.visitProposedToLocation(this.redirectedToLocation, {
1520
1599
  action: "replace",
1521
1600
  response: this.response
@@ -1537,34 +1616,41 @@ class Visit {
1537
1616
  requestPreventedHandlingResponse(request, response) {}
1538
1617
  async requestSucceededWithResponse(request, response) {
1539
1618
  const responseHTML = await response.responseHTML;
1619
+ const {redirected: redirected, statusCode: statusCode} = response;
1540
1620
  if (responseHTML == undefined) {
1541
1621
  this.recordResponse({
1542
- statusCode: SystemStatusCode.contentTypeMismatch
1622
+ statusCode: SystemStatusCode.contentTypeMismatch,
1623
+ redirected: redirected
1543
1624
  });
1544
1625
  } else {
1545
1626
  this.redirectedToLocation = response.redirected ? response.location : undefined;
1546
1627
  this.recordResponse({
1547
- statusCode: response.statusCode,
1548
- responseHTML: responseHTML
1628
+ statusCode: statusCode,
1629
+ responseHTML: responseHTML,
1630
+ redirected: redirected
1549
1631
  });
1550
1632
  }
1551
1633
  }
1552
1634
  async requestFailedWithResponse(request, response) {
1553
1635
  const responseHTML = await response.responseHTML;
1636
+ const {redirected: redirected, statusCode: statusCode} = response;
1554
1637
  if (responseHTML == undefined) {
1555
1638
  this.recordResponse({
1556
- statusCode: SystemStatusCode.contentTypeMismatch
1639
+ statusCode: SystemStatusCode.contentTypeMismatch,
1640
+ redirected: redirected
1557
1641
  });
1558
1642
  } else {
1559
1643
  this.recordResponse({
1560
- statusCode: response.statusCode,
1561
- responseHTML: responseHTML
1644
+ statusCode: statusCode,
1645
+ responseHTML: responseHTML,
1646
+ redirected: redirected
1562
1647
  });
1563
1648
  }
1564
1649
  }
1565
1650
  requestErrored(request, error) {
1566
1651
  this.recordResponse({
1567
- statusCode: SystemStatusCode.networkFailure
1652
+ statusCode: SystemStatusCode.networkFailure,
1653
+ redirected: false
1568
1654
  });
1569
1655
  }
1570
1656
  requestFinished() {
@@ -1622,12 +1708,12 @@ class Visit {
1622
1708
  } else if (this.action == "restore") {
1623
1709
  return !this.hasCachedSnapshot();
1624
1710
  } else {
1625
- return true;
1711
+ return this.willRender;
1626
1712
  }
1627
1713
  }
1628
1714
  cacheSnapshot() {
1629
1715
  if (!this.snapshotCached) {
1630
- this.view.cacheSnapshot();
1716
+ this.view.cacheSnapshot().then((snapshot => snapshot && this.visitCachedSnapshot(snapshot)));
1631
1717
  this.snapshotCached = true;
1632
1718
  }
1633
1719
  }
@@ -1664,10 +1750,10 @@ class BrowserAdapter {
1664
1750
  this.navigator.startVisit(location, uuid(), options);
1665
1751
  }
1666
1752
  visitStarted(visit) {
1753
+ visit.loadCachedSnapshot();
1667
1754
  visit.issueRequest();
1668
1755
  visit.changeHistory();
1669
1756
  visit.goToSamePageAnchor();
1670
- visit.loadCachedSnapshot();
1671
1757
  }
1672
1758
  visitRequestStarted(visit) {
1673
1759
  this.progressBar.setValue(0);
@@ -1775,7 +1861,7 @@ class FormSubmitObserver {
1775
1861
  const form = event.target instanceof HTMLFormElement ? event.target : undefined;
1776
1862
  const submitter = event.submitter || undefined;
1777
1863
  if (form) {
1778
- 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");
1779
1865
  if (method != "dialog" && this.delegate.willSubmitForm(form, submitter)) {
1780
1866
  event.preventDefault();
1781
1867
  this.delegate.formSubmitted(form, submitter);
@@ -1819,12 +1905,11 @@ class FrameRedirector {
1819
1905
  linkClickIntercepted(element, url) {
1820
1906
  const frame = this.findFrameElement(element);
1821
1907
  if (frame) {
1822
- frame.setAttribute("reloadable", "");
1823
- frame.src = url;
1908
+ frame.delegate.linkClickIntercepted(element, url);
1824
1909
  }
1825
1910
  }
1826
1911
  shouldInterceptFormSubmission(element, submitter) {
1827
- return this.shouldRedirect(element, submitter);
1912
+ return this.shouldSubmit(element, submitter);
1828
1913
  }
1829
1914
  formSubmissionIntercepted(element, submitter) {
1830
1915
  const frame = this.findFrameElement(element, submitter);
@@ -1833,6 +1918,13 @@ class FrameRedirector {
1833
1918
  frame.delegate.formSubmissionIntercepted(element, submitter);
1834
1919
  }
1835
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
+ }
1836
1928
  shouldRedirect(element, submitter) {
1837
1929
  const frame = this.findFrameElement(element, submitter);
1838
1930
  return frame ? frame != element.closest("turbo-frame") : false;
@@ -1988,7 +2080,11 @@ class Navigator {
1988
2080
  }
1989
2081
  proposeVisit(location, options = {}) {
1990
2082
  if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
1991
- 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
+ }
1992
2088
  }
1993
2089
  }
1994
2090
  startVisit(locatable, restorationIdentifier, options = {}) {
@@ -2001,13 +2097,7 @@ class Navigator {
2001
2097
  submitForm(form, submitter) {
2002
2098
  this.stop();
2003
2099
  this.formSubmission = new FormSubmission(this, form, submitter, true);
2004
- if (this.formSubmission.isIdempotent) {
2005
- this.proposeVisit(this.formSubmission.fetchRequest.url, {
2006
- action: this.getActionForFormSubmission(this.formSubmission)
2007
- });
2008
- } else {
2009
- this.formSubmission.start();
2010
- }
2100
+ this.formSubmission.start();
2011
2101
  }
2012
2102
  stop() {
2013
2103
  if (this.formSubmission) {
@@ -2040,11 +2130,14 @@ class Navigator {
2040
2130
  if (formSubmission.method != FetchMethod.get) {
2041
2131
  this.view.clearSnapshotCache();
2042
2132
  }
2043
- const {statusCode: statusCode} = fetchResponse;
2133
+ const {statusCode: statusCode, redirected: redirected} = fetchResponse;
2134
+ const action = this.getActionForFormSubmission(formSubmission);
2044
2135
  const visitOptions = {
2136
+ action: action,
2045
2137
  response: {
2046
2138
  statusCode: statusCode,
2047
- responseHTML: responseHTML
2139
+ responseHTML: responseHTML,
2140
+ redirected: redirected
2048
2141
  }
2049
2142
  };
2050
2143
  this.proposeVisit(fetchResponse.location, visitOptions);
@@ -2095,7 +2188,7 @@ class Navigator {
2095
2188
  }
2096
2189
  getActionForFormSubmission(formSubmission) {
2097
2190
  const {formElement: formElement, submitter: submitter} = formSubmission;
2098
- 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);
2099
2192
  return isAction(action) ? action : "advance";
2100
2193
  }
2101
2194
  }
@@ -2295,7 +2388,9 @@ class PageRenderer extends Renderer {
2295
2388
  this.mergeHead();
2296
2389
  }
2297
2390
  async render() {
2298
- this.replaceBody();
2391
+ if (this.willRender) {
2392
+ this.replaceBody();
2393
+ }
2299
2394
  }
2300
2395
  finishRendering() {
2301
2396
  super.finishRendering();
@@ -2431,8 +2526,8 @@ class PageView extends View {
2431
2526
  this.snapshotCache = new SnapshotCache(10);
2432
2527
  this.lastRenderedLocation = new URL(location.href);
2433
2528
  }
2434
- renderPage(snapshot, isPreview = false) {
2435
- 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);
2436
2531
  return this.render(renderer);
2437
2532
  }
2438
2533
  renderError(snapshot) {
@@ -2447,7 +2542,9 @@ class PageView extends View {
2447
2542
  this.delegate.viewWillCacheSnapshot();
2448
2543
  const {snapshot: snapshot, lastRenderedLocation: location} = this;
2449
2544
  await nextEventLoopTick();
2450
- this.snapshotCache.put(location, snapshot.clone());
2545
+ const cachedSnapshot = snapshot.clone();
2546
+ this.snapshotCache.put(location, cachedSnapshot);
2547
+ return cachedSnapshot;
2451
2548
  }
2452
2549
  }
2453
2550
  getCachedSnapshotForLocation(location) {
@@ -2552,7 +2649,7 @@ class Session {
2552
2649
  });
2553
2650
  }
2554
2651
  willFollowLinkToLocation(link, location) {
2555
- 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);
2556
2653
  }
2557
2654
  followedLinkToLocation(link, location) {
2558
2655
  const action = this.getActionForLink(link);
@@ -2561,14 +2658,23 @@ class Session {
2561
2658
  });
2562
2659
  }
2563
2660
  convertLinkWithMethodClickToFormSubmission(link) {
2564
- var _a;
2565
2661
  const linkMethod = link.getAttribute("data-turbo-method");
2566
2662
  if (linkMethod) {
2567
2663
  const form = document.createElement("form");
2568
2664
  form.method = linkMethod;
2569
2665
  form.action = link.getAttribute("href") || "undefined";
2570
2666
  form.hidden = true;
2571
- (_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);
2572
2678
  return dispatch("submit", {
2573
2679
  cancelable: true,
2574
2680
  target: form
@@ -2600,7 +2706,8 @@ class Session {
2600
2706
  this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
2601
2707
  }
2602
2708
  willSubmitForm(form, submitter) {
2603
- return this.elementDriveEnabled(form) && (!submitter || this.elementDriveEnabled(submitter));
2709
+ const action = getAction(form, submitter);
2710
+ return this.elementDriveEnabled(form) && (!submitter || this.elementDriveEnabled(submitter)) && locationIsVisitable(expandURL(action), this.snapshot.rootLocation);
2604
2711
  }
2605
2712
  formSubmitted(form, submitter) {
2606
2713
  this.navigator.submitForm(form, submitter);
@@ -2667,6 +2774,7 @@ class Session {
2667
2774
  });
2668
2775
  }
2669
2776
  notifyApplicationAfterVisitingLocation(location, action) {
2777
+ markAsBusy(document.documentElement);
2670
2778
  return dispatch("turbo:visit", {
2671
2779
  detail: {
2672
2780
  url: location.href,
@@ -2690,6 +2798,7 @@ class Session {
2690
2798
  return dispatch("turbo:render");
2691
2799
  }
2692
2800
  notifyApplicationAfterPageLoad(timing = {}) {
2801
+ clearBusyState(document.documentElement);
2693
2802
  return dispatch("turbo:load", {
2694
2803
  detail: {
2695
2804
  url: this.location.href,
@@ -2737,8 +2846,16 @@ class Session {
2737
2846
  const action = link.getAttribute("data-turbo-action");
2738
2847
  return isAction(action) ? action : "advance";
2739
2848
  }
2740
- locationIsVisitable(location) {
2741
- 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
+ }
2742
2859
  }
2743
2860
  get snapshot() {
2744
2861
  return this.view.snapshot;
@@ -2793,6 +2910,10 @@ function setProgressBarDelay(delay) {
2793
2910
  session.setProgressBarDelay(delay);
2794
2911
  }
2795
2912
 
2913
+ function setConfirmMethod(confirmMethod) {
2914
+ FormSubmission.confirmMethod = confirmMethod;
2915
+ }
2916
+
2796
2917
  var Turbo = Object.freeze({
2797
2918
  __proto__: null,
2798
2919
  navigator: navigator$1,
@@ -2806,11 +2927,14 @@ var Turbo = Object.freeze({
2806
2927
  disconnectStreamSource: disconnectStreamSource,
2807
2928
  renderStreamMessage: renderStreamMessage,
2808
2929
  clearCache: clearCache,
2809
- setProgressBarDelay: setProgressBarDelay
2930
+ setProgressBarDelay: setProgressBarDelay,
2931
+ setConfirmMethod: setConfirmMethod
2810
2932
  });
2811
2933
 
2812
2934
  class FrameController {
2813
2935
  constructor(element) {
2936
+ this.fetchResponseLoaded = fetchResponse => {};
2937
+ this.currentFetchRequest = null;
2814
2938
  this.resolveVisitPromise = () => {};
2815
2939
  this.connected = false;
2816
2940
  this.hasBeenLoaded = false;
@@ -2865,11 +2989,10 @@ class FrameController {
2865
2989
  this.currentURL = this.sourceURL;
2866
2990
  if (this.sourceURL) {
2867
2991
  try {
2868
- this.element.loaded = this.visit(this.sourceURL);
2992
+ this.element.loaded = this.visit(expandURL(this.sourceURL));
2869
2993
  this.appearanceObserver.stop();
2870
2994
  await this.element.loaded;
2871
2995
  this.hasBeenLoaded = true;
2872
- session.frameLoaded(this.element);
2873
2996
  } catch (error) {
2874
2997
  this.currentURL = previousURL;
2875
2998
  throw error;
@@ -2878,7 +3001,7 @@ class FrameController {
2878
3001
  }
2879
3002
  }
2880
3003
  async loadResponse(fetchResponse) {
2881
- if (fetchResponse.redirected) {
3004
+ if (fetchResponse.redirected || fetchResponse.succeeded && fetchResponse.isHTML) {
2882
3005
  this.sourceURL = fetchResponse.response.url;
2883
3006
  }
2884
3007
  try {
@@ -2886,14 +3009,18 @@ class FrameController {
2886
3009
  if (html) {
2887
3010
  const {body: body} = parseHTMLDocument(html);
2888
3011
  const snapshot = new Snapshot(await this.extractForeignFrameElement(body));
2889
- const renderer = new FrameRenderer(this.view.snapshot, snapshot, false);
3012
+ const renderer = new FrameRenderer(this.view.snapshot, snapshot, false, false);
2890
3013
  if (this.view.renderPromise) await this.view.renderPromise;
2891
3014
  await this.view.render(renderer);
2892
3015
  session.frameRendered(fetchResponse, this.element);
3016
+ session.frameLoaded(this.element);
3017
+ this.fetchResponseLoaded(fetchResponse);
2893
3018
  }
2894
3019
  } catch (error) {
2895
3020
  console.error(error);
2896
3021
  this.view.invalidate();
3022
+ } finally {
3023
+ this.fetchResponseLoaded = () => {};
2897
3024
  }
2898
3025
  }
2899
3026
  elementAppearedInViewport(element) {
@@ -2919,19 +3046,15 @@ class FrameController {
2919
3046
  }
2920
3047
  this.reloadable = false;
2921
3048
  this.formSubmission = new FormSubmission(this, element, submitter);
2922
- if (this.formSubmission.fetchRequest.isIdempotent) {
2923
- this.navigateFrame(element, this.formSubmission.fetchRequest.url.href, submitter);
2924
- } else {
2925
- const {fetchRequest: fetchRequest} = this.formSubmission;
2926
- this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
2927
- this.formSubmission.start();
2928
- }
3049
+ const {fetchRequest: fetchRequest} = this.formSubmission;
3050
+ this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
3051
+ this.formSubmission.start();
2929
3052
  }
2930
3053
  prepareHeadersForRequest(headers, request) {
2931
3054
  headers["Turbo-Frame"] = this.id;
2932
3055
  }
2933
3056
  requestStarted(request) {
2934
- this.element.setAttribute("busy", "");
3057
+ markAsBusy(this.element);
2935
3058
  }
2936
3059
  requestPreventedHandlingResponse(request, response) {
2937
3060
  this.resolveVisitPromise();
@@ -2949,14 +3072,14 @@ class FrameController {
2949
3072
  this.resolveVisitPromise();
2950
3073
  }
2951
3074
  requestFinished(request) {
2952
- this.element.removeAttribute("busy");
3075
+ clearBusyState(this.element);
2953
3076
  }
2954
- formSubmissionStarted(formSubmission) {
2955
- const frame = this.findFrameElement(formSubmission.formElement);
2956
- frame.setAttribute("busy", "");
3077
+ formSubmissionStarted({formElement: formElement}) {
3078
+ markAsBusy(formElement, this.findFrameElement(formElement));
2957
3079
  }
2958
3080
  formSubmissionSucceededWithResponse(formSubmission, response) {
2959
3081
  const frame = this.findFrameElement(formSubmission.formElement, formSubmission.submitter);
3082
+ this.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
2960
3083
  frame.delegate.loadResponse(response);
2961
3084
  }
2962
3085
  formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
@@ -2965,9 +3088,8 @@ class FrameController {
2965
3088
  formSubmissionErrored(formSubmission, error) {
2966
3089
  console.error(error);
2967
3090
  }
2968
- formSubmissionFinished(formSubmission) {
2969
- const frame = this.findFrameElement(formSubmission.formElement);
2970
- frame.removeAttribute("busy");
3091
+ formSubmissionFinished({formElement: formElement}) {
3092
+ clearBusyState(formElement, this.findFrameElement(formElement));
2971
3093
  }
2972
3094
  allowsImmediateRender(snapshot, resume) {
2973
3095
  return true;
@@ -2975,10 +3097,14 @@ class FrameController {
2975
3097
  viewRenderedSnapshot(snapshot, isPreview) {}
2976
3098
  viewInvalidated() {}
2977
3099
  async visit(url) {
2978
- 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;
2979
3104
  return new Promise((resolve => {
2980
3105
  this.resolveVisitPromise = () => {
2981
3106
  this.resolveVisitPromise = () => {};
3107
+ this.currentFetchRequest = null;
2982
3108
  resolve();
2983
3109
  };
2984
3110
  request.perform();
@@ -2986,12 +3112,36 @@ class FrameController {
2986
3112
  }
2987
3113
  navigateFrame(element, url, submitter) {
2988
3114
  const frame = this.findFrameElement(element, submitter);
3115
+ this.proposeVisitIfNavigatedWithAction(frame, element, submitter);
2989
3116
  frame.setAttribute("reloadable", "");
2990
3117
  frame.src = url;
2991
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
+ }
2992
3142
  findFrameElement(element, submitter) {
2993
3143
  var _a;
2994
- 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");
2995
3145
  return (_a = getFrameElementById(id)) !== null && _a !== void 0 ? _a : this.element;
2996
3146
  }
2997
3147
  async extractForeignFrameElement(container) {
@@ -3011,8 +3161,15 @@ class FrameController {
3011
3161
  }
3012
3162
  return new FrameElement;
3013
3163
  }
3164
+ formActionIsVisitable(form, submitter) {
3165
+ const action = getAction(form, submitter);
3166
+ return locationIsVisitable(expandURL(action), this.rootLocation);
3167
+ }
3014
3168
  shouldInterceptNavigation(element, submitter) {
3015
- 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
+ }
3016
3173
  if (!this.enabled || id == "_top") {
3017
3174
  return false;
3018
3175
  }
@@ -3068,6 +3225,24 @@ class FrameController {
3068
3225
  get isActive() {
3069
3226
  return this.element.isActive && this.connected;
3070
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
+ }
3071
3246
  }
3072
3247
 
3073
3248
  function getFrameElementById(id) {
@@ -3090,6 +3265,7 @@ function activateElement(element, currentURL) {
3090
3265
  }
3091
3266
  if (element instanceof FrameElement) {
3092
3267
  element.connectedCallback();
3268
+ element.disconnectedCallback();
3093
3269
  return element;
3094
3270
  }
3095
3271
  }
@@ -3274,6 +3450,7 @@ var turbo_es2017Esm = Object.freeze({
3274
3450
  registerAdapter: registerAdapter,
3275
3451
  renderStreamMessage: renderStreamMessage,
3276
3452
  session: session,
3453
+ setConfirmMethod: setConfirmMethod,
3277
3454
  setProgressBarDelay: setProgressBarDelay,
3278
3455
  start: start,
3279
3456
  visit: visit