turbo-rails 0.8.3 → 1.0.1

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) {
@@ -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
@@ -3356,8 +3533,6 @@ const now = () => (new Date).getTime();
3356
3533
 
3357
3534
  const secondsSince = time => (now() - time) / 1e3;
3358
3535
 
3359
- const clamp = (number, min, max) => Math.max(min, Math.min(max, number));
3360
-
3361
3536
  class ConnectionMonitor {
3362
3537
  constructor(connection) {
3363
3538
  this.visibilityDidChange = this.visibilityDidChange.bind(this);
@@ -3370,7 +3545,7 @@ class ConnectionMonitor {
3370
3545
  delete this.stoppedAt;
3371
3546
  this.startPolling();
3372
3547
  addEventListener("visibilitychange", this.visibilityDidChange);
3373
- logger.log(`ConnectionMonitor started. pollInterval = ${this.getPollInterval()} ms`);
3548
+ logger.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`);
3374
3549
  }
3375
3550
  }
3376
3551
  stop() {
@@ -3411,24 +3586,29 @@ class ConnectionMonitor {
3411
3586
  }), this.getPollInterval());
3412
3587
  }
3413
3588
  getPollInterval() {
3414
- const {min: min, max: max, multiplier: multiplier} = this.constructor.pollInterval;
3415
- const interval = multiplier * Math.log(this.reconnectAttempts + 1);
3416
- return Math.round(clamp(interval, min, max) * 1e3);
3589
+ const {staleThreshold: staleThreshold, reconnectionBackoffRate: reconnectionBackoffRate} = this.constructor;
3590
+ const backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10));
3591
+ const jitterMax = this.reconnectAttempts === 0 ? 1 : reconnectionBackoffRate;
3592
+ const jitter = jitterMax * Math.random();
3593
+ return staleThreshold * 1e3 * backoff * (1 + jitter);
3417
3594
  }
3418
3595
  reconnectIfStale() {
3419
3596
  if (this.connectionIsStale()) {
3420
- logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, pollInterval = ${this.getPollInterval()} ms, time disconnected = ${secondsSince(this.disconnectedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`);
3597
+ logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`);
3421
3598
  this.reconnectAttempts++;
3422
3599
  if (this.disconnectedRecently()) {
3423
- logger.log("ConnectionMonitor skipping reopening recent disconnect");
3600
+ logger.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(this.disconnectedAt)} s`);
3424
3601
  } else {
3425
3602
  logger.log("ConnectionMonitor reopening");
3426
3603
  this.connection.reopen();
3427
3604
  }
3428
3605
  }
3429
3606
  }
3607
+ get refreshedAt() {
3608
+ return this.pingedAt ? this.pingedAt : this.startedAt;
3609
+ }
3430
3610
  connectionIsStale() {
3431
- return secondsSince(this.pingedAt ? this.pingedAt : this.startedAt) > this.constructor.staleThreshold;
3611
+ return secondsSince(this.refreshedAt) > this.constructor.staleThreshold;
3432
3612
  }
3433
3613
  disconnectedRecently() {
3434
3614
  return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold;
@@ -3445,14 +3625,10 @@ class ConnectionMonitor {
3445
3625
  }
3446
3626
  }
3447
3627
 
3448
- ConnectionMonitor.pollInterval = {
3449
- min: 3,
3450
- max: 30,
3451
- multiplier: 5
3452
- };
3453
-
3454
3628
  ConnectionMonitor.staleThreshold = 6;
3455
3629
 
3630
+ ConnectionMonitor.reconnectionBackoffRate = .15;
3631
+
3456
3632
  var INTERNAL = {
3457
3633
  message_types: {
3458
3634
  welcome: "welcome",
@@ -3595,6 +3771,7 @@ Connection.prototype.events = {
3595
3771
  return this.monitor.recordPing();
3596
3772
 
3597
3773
  case message_types.confirmation:
3774
+ this.subscriptions.confirmSubscription(identifier);
3598
3775
  return this.subscriptions.notify(identifier, "connected");
3599
3776
 
3600
3777
  case message_types.rejection:
@@ -3662,9 +3839,47 @@ class Subscription {
3662
3839
  }
3663
3840
  }
3664
3841
 
3842
+ class SubscriptionGuarantor {
3843
+ constructor(subscriptions) {
3844
+ this.subscriptions = subscriptions;
3845
+ this.pendingSubscriptions = [];
3846
+ }
3847
+ guarantee(subscription) {
3848
+ if (this.pendingSubscriptions.indexOf(subscription) == -1) {
3849
+ logger.log(`SubscriptionGuarantor guaranteeing ${subscription.identifier}`);
3850
+ this.pendingSubscriptions.push(subscription);
3851
+ } else {
3852
+ logger.log(`SubscriptionGuarantor already guaranteeing ${subscription.identifier}`);
3853
+ }
3854
+ this.startGuaranteeing();
3855
+ }
3856
+ forget(subscription) {
3857
+ logger.log(`SubscriptionGuarantor forgetting ${subscription.identifier}`);
3858
+ this.pendingSubscriptions = this.pendingSubscriptions.filter((s => s !== subscription));
3859
+ }
3860
+ startGuaranteeing() {
3861
+ this.stopGuaranteeing();
3862
+ this.retrySubscribing();
3863
+ }
3864
+ stopGuaranteeing() {
3865
+ clearTimeout(this.retryTimeout);
3866
+ }
3867
+ retrySubscribing() {
3868
+ this.retryTimeout = setTimeout((() => {
3869
+ if (this.subscriptions && typeof this.subscriptions.subscribe === "function") {
3870
+ this.pendingSubscriptions.map((subscription => {
3871
+ logger.log(`SubscriptionGuarantor resubscribing ${subscription.identifier}`);
3872
+ this.subscriptions.subscribe(subscription);
3873
+ }));
3874
+ }
3875
+ }), 500);
3876
+ }
3877
+ }
3878
+
3665
3879
  class Subscriptions {
3666
3880
  constructor(consumer) {
3667
3881
  this.consumer = consumer;
3882
+ this.guarantor = new SubscriptionGuarantor(this);
3668
3883
  this.subscriptions = [];
3669
3884
  }
3670
3885
  create(channelName, mixin) {
@@ -3679,7 +3894,7 @@ class Subscriptions {
3679
3894
  this.subscriptions.push(subscription);
3680
3895
  this.consumer.ensureActiveConnection();
3681
3896
  this.notify(subscription, "initialized");
3682
- this.sendCommand(subscription, "subscribe");
3897
+ this.subscribe(subscription);
3683
3898
  return subscription;
3684
3899
  }
3685
3900
  remove(subscription) {
@@ -3697,6 +3912,7 @@ class Subscriptions {
3697
3912
  }));
3698
3913
  }
3699
3914
  forget(subscription) {
3915
+ this.guarantor.forget(subscription);
3700
3916
  this.subscriptions = this.subscriptions.filter((s => s !== subscription));
3701
3917
  return subscription;
3702
3918
  }
@@ -3704,7 +3920,7 @@ class Subscriptions {
3704
3920
  return this.subscriptions.filter((s => s.identifier === identifier));
3705
3921
  }
3706
3922
  reload() {
3707
- return this.subscriptions.map((subscription => this.sendCommand(subscription, "subscribe")));
3923
+ return this.subscriptions.map((subscription => this.subscribe(subscription)));
3708
3924
  }
3709
3925
  notifyAll(callbackName, ...args) {
3710
3926
  return this.subscriptions.map((subscription => this.notify(subscription, callbackName, ...args)));
@@ -3718,6 +3934,15 @@ class Subscriptions {
3718
3934
  }
3719
3935
  return subscriptions.map((subscription => typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined));
3720
3936
  }
3937
+ subscribe(subscription) {
3938
+ if (this.sendCommand(subscription, "subscribe")) {
3939
+ this.guarantor.guarantee(subscription);
3940
+ }
3941
+ }
3942
+ confirmSubscription(identifier) {
3943
+ logger.log(`Subscription confirmed ${identifier}`);
3944
+ this.findAll(identifier).map((subscription => this.guarantor.forget(subscription)));
3945
+ }
3721
3946
  sendCommand(subscription, command) {
3722
3947
  const {identifier: identifier} = subscription;
3723
3948
  return this.consumer.send({
@@ -3788,6 +4013,7 @@ var index = Object.freeze({
3788
4013
  INTERNAL: INTERNAL,
3789
4014
  Subscription: Subscription,
3790
4015
  Subscriptions: Subscriptions,
4016
+ SubscriptionGuarantor: SubscriptionGuarantor,
3791
4017
  adapters: adapters,
3792
4018
  createWebSocketURL: createWebSocketURL,
3793
4019
  logger: logger,