@hotwired/turbo 7.2.0-beta.1 → 7.2.0-beta.2

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.
Files changed (31) hide show
  1. package/dist/turbo.es2017-esm.js +388 -321
  2. package/dist/turbo.es2017-umd.js +388 -321
  3. package/dist/types/core/drive/error_renderer.d.ts +1 -1
  4. package/dist/types/core/drive/head_snapshot.d.ts +3 -3
  5. package/dist/types/core/drive/navigator.d.ts +3 -3
  6. package/dist/types/core/drive/page_renderer.d.ts +5 -5
  7. package/dist/types/core/drive/visit.d.ts +12 -1
  8. package/dist/types/core/frames/frame_controller.d.ts +26 -16
  9. package/dist/types/core/frames/frame_redirector.d.ts +12 -10
  10. package/dist/types/core/frames/frame_renderer.d.ts +1 -1
  11. package/dist/types/core/index.d.ts +5 -4
  12. package/dist/types/core/native/adapter.d.ts +1 -1
  13. package/dist/types/core/native/browser_adapter.d.ts +1 -1
  14. package/dist/types/core/renderer.d.ts +0 -2
  15. package/dist/types/core/session.d.ts +18 -11
  16. package/dist/types/core/streams/stream_message.d.ts +2 -6
  17. package/dist/types/core/types.d.ts +4 -0
  18. package/dist/types/core/view.d.ts +1 -1
  19. package/dist/types/elements/frame_element.d.ts +5 -5
  20. package/dist/types/elements/stream_element.d.ts +3 -1
  21. package/dist/types/http/fetch_request.d.ts +1 -0
  22. package/dist/types/observers/form_link_click_observer.d.ts +14 -0
  23. package/dist/types/observers/form_submit_observer.d.ts +2 -1
  24. package/dist/types/observers/link_click_observer.d.ts +3 -2
  25. package/dist/types/tests/functional/form_mode_tests.d.ts +1 -0
  26. package/dist/types/tests/unit/deprecated_adapter_support_test.d.ts +1 -1
  27. package/dist/types/util.d.ts +6 -0
  28. package/package.json +1 -1
  29. package/dist/types/core/frames/form_interceptor.d.ts +0 -12
  30. package/dist/types/core/frames/link_interceptor.d.ts +0 -16
  31. package/dist/types/observers/form_link_interceptor.d.ts +0 -14
@@ -1,5 +1,5 @@
1
1
  /*
2
- Turbo 7.2.0-beta.1
2
+ Turbo 7.2.0-beta.2
3
3
  Copyright © 2022 Basecamp, LLC
4
4
  */
5
5
  (function () {
@@ -132,6 +132,7 @@ class FrameElement extends HTMLElement {
132
132
  this.removeAttribute("complete");
133
133
  this.src = null;
134
134
  this.src = src;
135
+ return this.loaded;
135
136
  }
136
137
  attributeChangedCallback(name) {
137
138
  if (name == "loading") {
@@ -310,6 +311,36 @@ class FetchResponse {
310
311
  }
311
312
  }
312
313
 
314
+ function isAction(action) {
315
+ return action == "advance" || action == "replace" || action == "restore";
316
+ }
317
+
318
+ function activateScriptElement(element) {
319
+ if (element.getAttribute("data-turbo-eval") == "false") {
320
+ return element;
321
+ }
322
+ else {
323
+ const createdScriptElement = document.createElement("script");
324
+ const cspNonce = getMetaContent("csp-nonce");
325
+ if (cspNonce) {
326
+ createdScriptElement.nonce = cspNonce;
327
+ }
328
+ createdScriptElement.textContent = element.textContent;
329
+ createdScriptElement.async = false;
330
+ copyElementAttributes(createdScriptElement, element);
331
+ return createdScriptElement;
332
+ }
333
+ }
334
+ function copyElementAttributes(destinationElement, sourceElement) {
335
+ for (const { name, value } of sourceElement.attributes) {
336
+ destinationElement.setAttribute(name, value);
337
+ }
338
+ }
339
+ function createDocumentFragment(html) {
340
+ const template = document.createElement("template");
341
+ template.innerHTML = html;
342
+ return template.content;
343
+ }
313
344
  function dispatch(eventName, { target, cancelable, detail } = {}) {
314
345
  const event = new CustomEvent(eventName, {
315
346
  cancelable,
@@ -389,6 +420,31 @@ function clearBusyState(...elements) {
389
420
  element.removeAttribute("aria-busy");
390
421
  }
391
422
  }
423
+ function waitForLoad(element, timeoutInMilliseconds = 2000) {
424
+ return new Promise((resolve) => {
425
+ const onComplete = () => {
426
+ element.removeEventListener("error", onComplete);
427
+ element.removeEventListener("load", onComplete);
428
+ resolve();
429
+ };
430
+ element.addEventListener("load", onComplete, { once: true });
431
+ element.addEventListener("error", onComplete, { once: true });
432
+ setTimeout(resolve, timeoutInMilliseconds);
433
+ });
434
+ }
435
+ function getHistoryMethodForAction(action) {
436
+ switch (action) {
437
+ case "replace":
438
+ return history.replaceState;
439
+ case "advance":
440
+ case "restore":
441
+ return history.pushState;
442
+ }
443
+ }
444
+ function getVisitAction(...elements) {
445
+ const action = getAttribute("data-turbo-action", ...elements);
446
+ return isAction(action) ? action : null;
447
+ }
392
448
  function getMetaElement(name) {
393
449
  return document.querySelector(`meta[name="${name}"]`);
394
450
  }
@@ -513,6 +569,9 @@ class FetchRequest {
513
569
  get abortSignal() {
514
570
  return this.abortController.signal;
515
571
  }
572
+ acceptResponseType(mimeType) {
573
+ this.headers["Accept"] = [mimeType, this.headers["Accept"]].join(", ");
574
+ }
516
575
  async allowRequestToBeIntercepted(fetchOptions) {
517
576
  const requestInterception = new Promise((resolve) => (this.resolveRequestPromise = resolve));
518
577
  const event = dispatch("turbo:before-fetch-request", {
@@ -557,40 +616,29 @@ class AppearanceObserver {
557
616
  }
558
617
 
559
618
  class StreamMessage {
560
- constructor(html) {
561
- this.templateElement = document.createElement("template");
562
- this.templateElement.innerHTML = html;
619
+ constructor(fragment) {
620
+ this.fragment = importStreamElements(fragment);
563
621
  }
564
622
  static wrap(message) {
565
623
  if (typeof message == "string") {
566
- return new this(message);
624
+ return new this(createDocumentFragment(message));
567
625
  }
568
626
  else {
569
627
  return message;
570
628
  }
571
629
  }
572
- get fragment() {
573
- const fragment = document.createDocumentFragment();
574
- for (const element of this.foreignElements) {
575
- fragment.appendChild(document.importNode(element, true));
630
+ }
631
+ StreamMessage.contentType = "text/vnd.turbo-stream.html";
632
+ function importStreamElements(fragment) {
633
+ for (const element of fragment.querySelectorAll("turbo-stream")) {
634
+ const streamElement = document.importNode(element, true);
635
+ for (const inertScriptElement of streamElement.templateElement.content.querySelectorAll("script")) {
636
+ inertScriptElement.replaceWith(activateScriptElement(inertScriptElement));
576
637
  }
577
- return fragment;
578
- }
579
- get foreignElements() {
580
- return this.templateChildren.reduce((streamElements, child) => {
581
- if (child.tagName.toLowerCase() == "turbo-stream") {
582
- return [...streamElements, child];
583
- }
584
- else {
585
- return streamElements;
586
- }
587
- }, []);
588
- }
589
- get templateChildren() {
590
- return Array.from(this.templateElement.content.children);
638
+ element.replaceWith(streamElement);
591
639
  }
640
+ return fragment;
592
641
  }
593
- StreamMessage.contentType = "text/vnd.turbo-stream.html";
594
642
 
595
643
  var FormSubmissionState;
596
644
  (function (FormSubmissionState) {
@@ -705,7 +753,7 @@ class FormSubmission {
705
753
  }
706
754
  }
707
755
  if (this.requestAcceptsTurboStreamResponse(request)) {
708
- headers["Accept"] = [StreamMessage.contentType, headers["Accept"]].join(", ");
756
+ request.acceptResponseType(StreamMessage.contentType);
709
757
  }
710
758
  }
711
759
  requestStarted(_request) {
@@ -741,6 +789,10 @@ class FormSubmission {
741
789
  }
742
790
  requestErrored(request, error) {
743
791
  this.result = { success: false, error };
792
+ dispatch("turbo:fetch-request-error", {
793
+ target: this.formElement,
794
+ detail: { request, error },
795
+ });
744
796
  this.delegate.formSubmissionErrored(this, error);
745
797
  }
746
798
  requestFinished(_request) {
@@ -764,8 +816,8 @@ function buildFormData(formElement, submitter) {
764
816
  const formData = new FormData(formElement);
765
817
  const name = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("name");
766
818
  const value = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("value");
767
- if (name && value != null && formData.get(name) != value) {
768
- formData.append(name, value);
819
+ if (name) {
820
+ formData.append(name, value || "");
769
821
  }
770
822
  return formData;
771
823
  }
@@ -813,7 +865,14 @@ class Snapshot {
813
865
  return this.element.isConnected;
814
866
  }
815
867
  get firstAutofocusableElement() {
816
- return this.element.querySelector("[autofocus]");
868
+ const inertDisabledOrHidden = "[inert], :disabled, [hidden], details:not([open]), dialog:not([open])";
869
+ for (const element of this.element.querySelectorAll("[autofocus]")) {
870
+ if (element.closest(inertDisabledOrHidden) == null)
871
+ return element;
872
+ else
873
+ continue;
874
+ }
875
+ return null;
817
876
  }
818
877
  get permanentElements() {
819
878
  return [...this.element.querySelectorAll("[id][data-turbo-permanent]")];
@@ -834,32 +893,54 @@ class Snapshot {
834
893
  }
835
894
  }
836
895
 
837
- class FormInterceptor {
838
- constructor(delegate, element) {
896
+ class FormSubmitObserver {
897
+ constructor(delegate, eventTarget) {
898
+ this.started = false;
899
+ this.submitCaptured = () => {
900
+ this.eventTarget.removeEventListener("submit", this.submitBubbled, false);
901
+ this.eventTarget.addEventListener("submit", this.submitBubbled, false);
902
+ };
839
903
  this.submitBubbled = ((event) => {
840
- const form = event.target;
841
- if (!event.defaultPrevented &&
842
- form instanceof HTMLFormElement &&
843
- form.closest("turbo-frame, html") == this.element) {
904
+ if (!event.defaultPrevented) {
905
+ const form = event.target instanceof HTMLFormElement ? event.target : undefined;
844
906
  const submitter = event.submitter || undefined;
845
- const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.method;
846
- if (method != "dialog" && this.delegate.shouldInterceptFormSubmission(form, submitter)) {
907
+ if (form &&
908
+ submissionDoesNotDismissDialog(form, submitter) &&
909
+ submissionDoesNotTargetIFrame(form, submitter) &&
910
+ this.delegate.willSubmitForm(form, submitter)) {
847
911
  event.preventDefault();
848
- event.stopImmediatePropagation();
849
- this.delegate.formSubmissionIntercepted(form, submitter);
912
+ this.delegate.formSubmitted(form, submitter);
850
913
  }
851
914
  }
852
915
  });
853
916
  this.delegate = delegate;
854
- this.element = element;
917
+ this.eventTarget = eventTarget;
855
918
  }
856
919
  start() {
857
- this.element.addEventListener("submit", this.submitBubbled);
920
+ if (!this.started) {
921
+ this.eventTarget.addEventListener("submit", this.submitCaptured, true);
922
+ this.started = true;
923
+ }
858
924
  }
859
925
  stop() {
860
- this.element.removeEventListener("submit", this.submitBubbled);
926
+ if (this.started) {
927
+ this.eventTarget.removeEventListener("submit", this.submitCaptured, true);
928
+ this.started = false;
929
+ }
861
930
  }
862
931
  }
932
+ function submissionDoesNotDismissDialog(form, submitter) {
933
+ const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.getAttribute("method");
934
+ return method != "dialog";
935
+ }
936
+ function submissionDoesNotTargetIFrame(form, submitter) {
937
+ const target = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formtarget")) || form.target;
938
+ for (const element of document.getElementsByName(target)) {
939
+ if (element instanceof HTMLIFrameElement)
940
+ return false;
941
+ }
942
+ return true;
943
+ }
863
944
 
864
945
  class View {
865
946
  constructor(delegate, element) {
@@ -911,7 +992,7 @@ class View {
911
992
  try {
912
993
  this.renderPromise = new Promise((resolve) => (this.resolveRenderPromise = resolve));
913
994
  this.renderer = renderer;
914
- this.prepareToRenderSnapshot(renderer);
995
+ await this.prepareToRenderSnapshot(renderer);
915
996
  const renderInterception = new Promise((resolve) => (this.resolveInterceptionPromise = resolve));
916
997
  const options = { resume: this.resolveInterceptionPromise, render: this.renderer.renderElement };
917
998
  const immediateRender = this.delegate.allowsImmediateRender(snapshot, options);
@@ -935,9 +1016,9 @@ class View {
935
1016
  invalidate(reason) {
936
1017
  this.delegate.viewInvalidated(reason);
937
1018
  }
938
- prepareToRenderSnapshot(renderer) {
1019
+ async prepareToRenderSnapshot(renderer) {
939
1020
  this.markAsPreview(renderer.isPreview);
940
- renderer.prepareToRender();
1021
+ await renderer.prepareToRender();
941
1022
  }
942
1023
  markAsPreview(isPreview) {
943
1024
  if (isPreview) {
@@ -964,64 +1045,84 @@ class FrameView extends View {
964
1045
  }
965
1046
  }
966
1047
 
967
- class LinkInterceptor {
968
- constructor(delegate, element) {
969
- this.clickBubbled = (event) => {
970
- if (this.respondsToEventTarget(event.target)) {
971
- this.clickEvent = event;
972
- }
973
- else {
974
- delete this.clickEvent;
975
- }
1048
+ class LinkClickObserver {
1049
+ constructor(delegate, eventTarget) {
1050
+ this.started = false;
1051
+ this.clickCaptured = () => {
1052
+ this.eventTarget.removeEventListener("click", this.clickBubbled, false);
1053
+ this.eventTarget.addEventListener("click", this.clickBubbled, false);
976
1054
  };
977
- this.linkClicked = ((event) => {
978
- if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) {
979
- if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url)) {
980
- this.clickEvent.preventDefault();
981
- event.preventDefault();
982
- this.delegate.linkClickIntercepted(event.target, event.detail.url);
1055
+ this.clickBubbled = (event) => {
1056
+ if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {
1057
+ const target = (event.composedPath && event.composedPath()[0]) || event.target;
1058
+ const link = this.findLinkFromClickTarget(target);
1059
+ if (link && doesNotTargetIFrame(link)) {
1060
+ const location = this.getLocationForLink(link);
1061
+ if (this.delegate.willFollowLinkToLocation(link, location, event)) {
1062
+ event.preventDefault();
1063
+ this.delegate.followedLinkToLocation(link, location);
1064
+ }
983
1065
  }
984
1066
  }
985
- delete this.clickEvent;
986
- });
987
- this.willVisit = ((_event) => {
988
- delete this.clickEvent;
989
- });
1067
+ };
990
1068
  this.delegate = delegate;
991
- this.element = element;
1069
+ this.eventTarget = eventTarget;
992
1070
  }
993
1071
  start() {
994
- this.element.addEventListener("click", this.clickBubbled);
995
- document.addEventListener("turbo:click", this.linkClicked);
996
- document.addEventListener("turbo:before-visit", this.willVisit);
1072
+ if (!this.started) {
1073
+ this.eventTarget.addEventListener("click", this.clickCaptured, true);
1074
+ this.started = true;
1075
+ }
997
1076
  }
998
1077
  stop() {
999
- this.element.removeEventListener("click", this.clickBubbled);
1000
- document.removeEventListener("turbo:click", this.linkClicked);
1001
- document.removeEventListener("turbo:before-visit", this.willVisit);
1078
+ if (this.started) {
1079
+ this.eventTarget.removeEventListener("click", this.clickCaptured, true);
1080
+ this.started = false;
1081
+ }
1082
+ }
1083
+ clickEventIsSignificant(event) {
1084
+ return !((event.target && event.target.isContentEditable) ||
1085
+ event.defaultPrevented ||
1086
+ event.which > 1 ||
1087
+ event.altKey ||
1088
+ event.ctrlKey ||
1089
+ event.metaKey ||
1090
+ event.shiftKey);
1091
+ }
1092
+ findLinkFromClickTarget(target) {
1093
+ if (target instanceof Element) {
1094
+ return target.closest("a[href]:not([target^=_]):not([download])");
1095
+ }
1002
1096
  }
1003
- respondsToEventTarget(target) {
1004
- const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
1005
- return element && element.closest("turbo-frame, html") == this.element;
1097
+ getLocationForLink(link) {
1098
+ return expandURL(link.getAttribute("href") || "");
1006
1099
  }
1007
1100
  }
1101
+ function doesNotTargetIFrame(anchor) {
1102
+ for (const element of document.getElementsByName(anchor.target)) {
1103
+ if (element instanceof HTMLIFrameElement)
1104
+ return false;
1105
+ }
1106
+ return true;
1107
+ }
1008
1108
 
1009
- class FormLinkInterceptor {
1109
+ class FormLinkClickObserver {
1010
1110
  constructor(delegate, element) {
1011
1111
  this.delegate = delegate;
1012
- this.linkInterceptor = new LinkInterceptor(this, element);
1112
+ this.linkClickObserver = new LinkClickObserver(this, element);
1013
1113
  }
1014
1114
  start() {
1015
- this.linkInterceptor.start();
1115
+ this.linkClickObserver.start();
1016
1116
  }
1017
1117
  stop() {
1018
- this.linkInterceptor.stop();
1118
+ this.linkClickObserver.stop();
1019
1119
  }
1020
- shouldInterceptLinkClick(link) {
1021
- return (this.delegate.shouldInterceptFormLinkClick(link) &&
1022
- (link.hasAttribute("data-turbo-method") || link.hasAttribute("data-turbo-stream")));
1120
+ willFollowLinkToLocation(link, location, originalEvent) {
1121
+ return (this.delegate.willSubmitFormLinkToLocation(link, location, originalEvent) &&
1122
+ link.hasAttribute("data-turbo-method"));
1023
1123
  }
1024
- linkClickIntercepted(link, action) {
1124
+ followedLinkToLocation(link, location) {
1125
+ const action = location.href;
1025
1126
  const form = document.createElement("form");
1026
1127
  form.setAttribute("data-turbo", "true");
1027
1128
  form.setAttribute("action", action);
@@ -1038,7 +1139,7 @@ class FormLinkInterceptor {
1038
1139
  const turboStream = link.hasAttribute("data-turbo-stream");
1039
1140
  if (turboStream)
1040
1141
  form.setAttribute("data-turbo-stream", "");
1041
- this.delegate.formLinkClickIntercepted(link, form);
1142
+ this.delegate.submittedFormLinkToLocation(link, location, form);
1042
1143
  document.body.appendChild(form);
1043
1144
  form.requestSubmit();
1044
1145
  form.remove();
@@ -1122,21 +1223,6 @@ class Renderer {
1122
1223
  delete this.resolvingFunctions;
1123
1224
  }
1124
1225
  }
1125
- createScriptElement(element) {
1126
- if (element.getAttribute("data-turbo-eval") == "false") {
1127
- return element;
1128
- }
1129
- else {
1130
- const createdScriptElement = document.createElement("script");
1131
- if (this.cspNonce) {
1132
- createdScriptElement.nonce = this.cspNonce;
1133
- }
1134
- createdScriptElement.textContent = element.textContent;
1135
- createdScriptElement.async = false;
1136
- copyElementAttributes(createdScriptElement, element);
1137
- return createdScriptElement;
1138
- }
1139
- }
1140
1226
  preservingPermanentElements(callback) {
1141
1227
  Bardo.preservingPermanentElements(this, this.permanentElementMap, callback);
1142
1228
  }
@@ -1171,14 +1257,6 @@ class Renderer {
1171
1257
  get permanentElementMap() {
1172
1258
  return this.currentSnapshot.getPermanentElementMapForSnapshot(this.newSnapshot);
1173
1259
  }
1174
- get cspNonce() {
1175
- return getMetaContent("csp-nonce");
1176
- }
1177
- }
1178
- function copyElementAttributes(destinationElement, sourceElement) {
1179
- for (const { name, value } of [...sourceElement.attributes]) {
1180
- destinationElement.setAttribute(name, value);
1181
- }
1182
1260
  }
1183
1261
  function elementIsFocusable(element) {
1184
1262
  return element && typeof element.focus == "function";
@@ -1216,7 +1294,7 @@ class FrameRenderer extends Renderer {
1216
1294
  this.activateScriptElements();
1217
1295
  }
1218
1296
  loadFrameElement() {
1219
- this.delegate.frameExtracted(this.newElement.cloneNode(true));
1297
+ this.delegate.willRenderFrame(this.currentElement, this.newElement);
1220
1298
  this.renderElement(this.currentElement, this.newElement);
1221
1299
  }
1222
1300
  scrollFrameIntoView() {
@@ -1233,7 +1311,7 @@ class FrameRenderer extends Renderer {
1233
1311
  }
1234
1312
  activateScriptElements() {
1235
1313
  for (const inertScriptElement of this.newScriptElements) {
1236
- const activatedScriptElement = this.createScriptElement(inertScriptElement);
1314
+ const activatedScriptElement = activateScriptElement(inertScriptElement);
1237
1315
  inertScriptElement.replaceWith(activatedScriptElement);
1238
1316
  }
1239
1317
  }
@@ -1519,6 +1597,9 @@ const defaultOptions = {
1519
1597
  historyChanged: false,
1520
1598
  visitCachedSnapshot: () => { },
1521
1599
  willRender: true,
1600
+ updateHistory: true,
1601
+ shouldCacheSnapshot: true,
1602
+ acceptsStreamResponse: false,
1522
1603
  };
1523
1604
  var SystemStatusCode;
1524
1605
  (function (SystemStatusCode) {
@@ -1533,12 +1614,15 @@ class Visit {
1533
1614
  this.followedRedirect = false;
1534
1615
  this.historyChanged = false;
1535
1616
  this.scrolled = false;
1617
+ this.shouldCacheSnapshot = true;
1618
+ this.acceptsStreamResponse = false;
1536
1619
  this.snapshotCached = false;
1537
1620
  this.state = VisitState.initialized;
1538
1621
  this.delegate = delegate;
1539
1622
  this.location = location;
1540
1623
  this.restorationIdentifier = restorationIdentifier || uuid();
1541
- const { action, historyChanged, referrer, snapshotHTML, response, visitCachedSnapshot, willRender } = Object.assign(Object.assign({}, defaultOptions), options);
1624
+ this.promise = new Promise((resolve, reject) => (this.resolvingFunctions = { resolve, reject }));
1625
+ const { action, historyChanged, referrer, snapshotHTML, response, visitCachedSnapshot, willRender, updateHistory, shouldCacheSnapshot, acceptsStreamResponse, } = Object.assign(Object.assign({}, defaultOptions), options);
1542
1626
  this.action = action;
1543
1627
  this.historyChanged = historyChanged;
1544
1628
  this.referrer = referrer;
@@ -1547,7 +1631,10 @@ class Visit {
1547
1631
  this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
1548
1632
  this.visitCachedSnapshot = visitCachedSnapshot;
1549
1633
  this.willRender = willRender;
1634
+ this.updateHistory = updateHistory;
1550
1635
  this.scrolled = !willRender;
1636
+ this.shouldCacheSnapshot = shouldCacheSnapshot;
1637
+ this.acceptsStreamResponse = acceptsStreamResponse;
1551
1638
  }
1552
1639
  get adapter() {
1553
1640
  return this.delegate.adapter;
@@ -1579,6 +1666,7 @@ class Visit {
1579
1666
  }
1580
1667
  this.cancelRender();
1581
1668
  this.state = VisitState.canceled;
1669
+ this.resolvingFunctions.reject();
1582
1670
  }
1583
1671
  }
1584
1672
  complete() {
@@ -1590,19 +1678,21 @@ class Visit {
1590
1678
  this.adapter.visitCompleted(this);
1591
1679
  this.delegate.visitCompleted(this);
1592
1680
  }
1681
+ this.resolvingFunctions.resolve();
1593
1682
  }
1594
1683
  }
1595
1684
  fail() {
1596
1685
  if (this.state == VisitState.started) {
1597
1686
  this.state = VisitState.failed;
1598
1687
  this.adapter.visitFailed(this);
1688
+ this.resolvingFunctions.reject();
1599
1689
  }
1600
1690
  }
1601
1691
  changeHistory() {
1602
1692
  var _a;
1603
- if (!this.historyChanged) {
1693
+ if (!this.historyChanged && this.updateHistory) {
1604
1694
  const actionForHistory = this.location.href === ((_a = this.referrer) === null || _a === void 0 ? void 0 : _a.href) ? "replace" : this.action;
1605
- const method = this.getHistoryMethodForAction(actionForHistory);
1695
+ const method = getHistoryMethodForAction(actionForHistory);
1606
1696
  this.history.update(method, this.location, this.restorationIdentifier);
1607
1697
  this.historyChanged = true;
1608
1698
  }
@@ -1647,11 +1737,13 @@ class Visit {
1647
1737
  if (this.response) {
1648
1738
  const { statusCode, responseHTML } = this.response;
1649
1739
  this.render(async () => {
1650
- this.cacheSnapshot();
1740
+ if (this.shouldCacheSnapshot)
1741
+ this.cacheSnapshot();
1651
1742
  if (this.view.renderPromise)
1652
1743
  await this.view.renderPromise;
1653
1744
  if (isSuccessful(statusCode) && responseHTML != null) {
1654
1745
  await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML), false, this.willRender, this);
1746
+ this.performScroll();
1655
1747
  this.adapter.visitRendered(this);
1656
1748
  this.complete();
1657
1749
  }
@@ -1692,6 +1784,7 @@ class Visit {
1692
1784
  if (this.view.renderPromise)
1693
1785
  await this.view.renderPromise;
1694
1786
  await this.view.renderPage(snapshot, isPreview, this.willRender, this);
1787
+ this.performScroll();
1695
1788
  this.adapter.visitRendered(this);
1696
1789
  if (!isPreview) {
1697
1790
  this.complete();
@@ -1715,10 +1808,16 @@ class Visit {
1715
1808
  if (this.isSamePage) {
1716
1809
  this.render(async () => {
1717
1810
  this.cacheSnapshot();
1811
+ this.performScroll();
1718
1812
  this.adapter.visitRendered(this);
1719
1813
  });
1720
1814
  }
1721
1815
  }
1816
+ prepareHeadersForRequest(headers, request) {
1817
+ if (this.acceptsStreamResponse) {
1818
+ request.acceptResponseType(StreamMessage.contentType);
1819
+ }
1820
+ }
1722
1821
  requestStarted() {
1723
1822
  this.startRequest();
1724
1823
  }
@@ -1760,7 +1859,7 @@ class Visit {
1760
1859
  this.finishRequest();
1761
1860
  }
1762
1861
  performScroll() {
1763
- if (!this.scrolled) {
1862
+ if (!this.scrolled && !this.view.forceReloaded) {
1764
1863
  if (this.action == "restore") {
1765
1864
  this.scrollToRestoredPosition() || this.scrollToAnchor() || this.view.scrollToTop();
1766
1865
  }
@@ -1829,9 +1928,6 @@ class Visit {
1829
1928
  });
1830
1929
  await callback();
1831
1930
  delete this.frame;
1832
- if (!this.view.forceReloaded) {
1833
- this.performScroll();
1834
- }
1835
1931
  }
1836
1932
  cancelRender() {
1837
1933
  if (this.frame) {
@@ -1853,7 +1949,7 @@ class BrowserAdapter {
1853
1949
  this.session = session;
1854
1950
  }
1855
1951
  visitProposedToLocation(location, options) {
1856
- this.navigator.startVisit(location, uuid(), options);
1952
+ return this.navigator.startVisit(location, (options === null || options === void 0 ? void 0 : options.restorationIdentifier) || uuid(), options);
1857
1953
  }
1858
1954
  visitStarted(visit) {
1859
1955
  this.location = visit.location;
@@ -1963,84 +2059,39 @@ class CacheObserver {
1963
2059
  }
1964
2060
  }
1965
2061
 
1966
- class FormSubmitObserver {
1967
- constructor(delegate) {
1968
- this.started = false;
1969
- this.submitCaptured = () => {
1970
- removeEventListener("submit", this.submitBubbled, false);
1971
- addEventListener("submit", this.submitBubbled, false);
1972
- };
1973
- this.submitBubbled = ((event) => {
1974
- if (!event.defaultPrevented) {
1975
- const form = event.target instanceof HTMLFormElement ? event.target : undefined;
1976
- const submitter = event.submitter || undefined;
1977
- if (form &&
1978
- submissionDoesNotDismissDialog(form, submitter) &&
1979
- submissionDoesNotTargetIFrame(form, submitter) &&
1980
- this.delegate.willSubmitForm(form, submitter)) {
1981
- event.preventDefault();
1982
- this.delegate.formSubmitted(form, submitter);
1983
- }
1984
- }
1985
- });
1986
- this.delegate = delegate;
1987
- }
1988
- start() {
1989
- if (!this.started) {
1990
- addEventListener("submit", this.submitCaptured, true);
1991
- this.started = true;
1992
- }
1993
- }
1994
- stop() {
1995
- if (this.started) {
1996
- removeEventListener("submit", this.submitCaptured, true);
1997
- this.started = false;
1998
- }
1999
- }
2000
- }
2001
- function submissionDoesNotDismissDialog(form, submitter) {
2002
- const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.getAttribute("method");
2003
- return method != "dialog";
2004
- }
2005
- function submissionDoesNotTargetIFrame(form, submitter) {
2006
- const target = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formtarget")) || form.target;
2007
- for (const element of document.getElementsByName(target)) {
2008
- if (element instanceof HTMLIFrameElement)
2009
- return false;
2010
- }
2011
- return true;
2012
- }
2013
-
2014
2062
  class FrameRedirector {
2015
- constructor(element) {
2063
+ constructor(session, element) {
2064
+ this.session = session;
2016
2065
  this.element = element;
2017
- this.linkInterceptor = new LinkInterceptor(this, element);
2018
- this.formInterceptor = new FormInterceptor(this, element);
2066
+ this.linkClickObserver = new LinkClickObserver(this, element);
2067
+ this.formSubmitObserver = new FormSubmitObserver(this, element);
2019
2068
  }
2020
2069
  start() {
2021
- this.linkInterceptor.start();
2022
- this.formInterceptor.start();
2070
+ this.linkClickObserver.start();
2071
+ this.formSubmitObserver.start();
2023
2072
  }
2024
2073
  stop() {
2025
- this.linkInterceptor.stop();
2026
- this.formInterceptor.stop();
2074
+ this.linkClickObserver.stop();
2075
+ this.formSubmitObserver.stop();
2027
2076
  }
2028
- shouldInterceptLinkClick(element, _url) {
2077
+ willFollowLinkToLocation(element) {
2029
2078
  return this.shouldRedirect(element);
2030
2079
  }
2031
- linkClickIntercepted(element, url) {
2080
+ followedLinkToLocation(element, url) {
2032
2081
  const frame = this.findFrameElement(element);
2033
2082
  if (frame) {
2034
- frame.delegate.linkClickIntercepted(element, url);
2083
+ frame.delegate.followedLinkToLocation(element, url);
2035
2084
  }
2036
2085
  }
2037
- shouldInterceptFormSubmission(element, submitter) {
2038
- return this.shouldSubmit(element, submitter);
2086
+ willSubmitForm(element, submitter) {
2087
+ return (element.closest("turbo-frame") == null &&
2088
+ this.shouldSubmit(element, submitter) &&
2089
+ this.shouldRedirect(element, submitter));
2039
2090
  }
2040
- formSubmissionIntercepted(element, submitter) {
2091
+ formSubmitted(element, submitter) {
2041
2092
  const frame = this.findFrameElement(element, submitter);
2042
2093
  if (frame) {
2043
- frame.delegate.formSubmissionIntercepted(element, submitter);
2094
+ frame.delegate.formSubmitted(element, submitter);
2044
2095
  }
2045
2096
  }
2046
2097
  shouldSubmit(form, submitter) {
@@ -2051,8 +2102,16 @@ class FrameRedirector {
2051
2102
  return this.shouldRedirect(form, submitter) && locationIsVisitable(action, rootLocation);
2052
2103
  }
2053
2104
  shouldRedirect(element, submitter) {
2054
- const frame = this.findFrameElement(element, submitter);
2055
- return frame ? frame != element.closest("turbo-frame") : false;
2105
+ const isNavigatable = element instanceof HTMLFormElement
2106
+ ? this.session.submissionIsNavigatable(element, submitter)
2107
+ : this.session.elementIsNavigatable(element);
2108
+ if (isNavigatable) {
2109
+ const frame = this.findFrameElement(element, submitter);
2110
+ return frame ? frame != element.closest("turbo-frame") : false;
2111
+ }
2112
+ else {
2113
+ return false;
2114
+ }
2056
2115
  }
2057
2116
  findFrameElement(element, submitter) {
2058
2117
  const id = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("data-turbo-frame")) || element.getAttribute("data-turbo-frame");
@@ -2144,70 +2203,6 @@ class History {
2144
2203
  }
2145
2204
  }
2146
2205
 
2147
- class LinkClickObserver {
2148
- constructor(delegate) {
2149
- this.started = false;
2150
- this.clickCaptured = () => {
2151
- removeEventListener("click", this.clickBubbled, false);
2152
- addEventListener("click", this.clickBubbled, false);
2153
- };
2154
- this.clickBubbled = (event) => {
2155
- if (this.clickEventIsSignificant(event)) {
2156
- const target = (event.composedPath && event.composedPath()[0]) || event.target;
2157
- const link = this.findLinkFromClickTarget(target);
2158
- if (link && doesNotTargetIFrame(link)) {
2159
- const location = this.getLocationForLink(link);
2160
- if (this.delegate.willFollowLinkToLocation(link, location, event)) {
2161
- event.preventDefault();
2162
- this.delegate.followedLinkToLocation(link, location);
2163
- }
2164
- }
2165
- }
2166
- };
2167
- this.delegate = delegate;
2168
- }
2169
- start() {
2170
- if (!this.started) {
2171
- addEventListener("click", this.clickCaptured, true);
2172
- this.started = true;
2173
- }
2174
- }
2175
- stop() {
2176
- if (this.started) {
2177
- removeEventListener("click", this.clickCaptured, true);
2178
- this.started = false;
2179
- }
2180
- }
2181
- clickEventIsSignificant(event) {
2182
- return !((event.target && event.target.isContentEditable) ||
2183
- event.defaultPrevented ||
2184
- event.which > 1 ||
2185
- event.altKey ||
2186
- event.ctrlKey ||
2187
- event.metaKey ||
2188
- event.shiftKey);
2189
- }
2190
- findLinkFromClickTarget(target) {
2191
- if (target instanceof Element) {
2192
- return target.closest("a[href]:not([target^=_]):not([download])");
2193
- }
2194
- }
2195
- getLocationForLink(link) {
2196
- return expandURL(link.getAttribute("href") || "");
2197
- }
2198
- }
2199
- function doesNotTargetIFrame(anchor) {
2200
- for (const element of document.getElementsByName(anchor.target)) {
2201
- if (element instanceof HTMLIFrameElement)
2202
- return false;
2203
- }
2204
- return true;
2205
- }
2206
-
2207
- function isAction(action) {
2208
- return action == "advance" || action == "replace" || action == "restore";
2209
- }
2210
-
2211
2206
  class Navigator {
2212
2207
  constructor(delegate) {
2213
2208
  this.delegate = delegate;
@@ -2215,18 +2210,23 @@ class Navigator {
2215
2210
  proposeVisit(location, options = {}) {
2216
2211
  if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
2217
2212
  if (locationIsVisitable(location, this.view.snapshot.rootLocation)) {
2218
- this.delegate.visitProposedToLocation(location, options);
2213
+ return this.delegate.visitProposedToLocation(location, options);
2219
2214
  }
2220
2215
  else {
2221
2216
  window.location.href = location.toString();
2217
+ return Promise.resolve();
2222
2218
  }
2223
2219
  }
2220
+ else {
2221
+ return Promise.reject();
2222
+ }
2224
2223
  }
2225
2224
  startVisit(locatable, restorationIdentifier, options = {}) {
2226
2225
  this.lastVisit = this.currentVisit;
2227
2226
  this.stop();
2228
2227
  this.currentVisit = new Visit(this, expandURL(locatable), restorationIdentifier, Object.assign({ referrer: this.location }, options));
2229
2228
  this.currentVisit.start();
2229
+ return this.currentVisit.promise;
2230
2230
  }
2231
2231
  submitForm(form, submitter) {
2232
2232
  this.stop();
@@ -2261,13 +2261,15 @@ class Navigator {
2261
2261
  if (formSubmission == this.formSubmission) {
2262
2262
  const responseHTML = await fetchResponse.responseHTML;
2263
2263
  if (responseHTML) {
2264
- if (formSubmission.method != FetchMethod.get) {
2264
+ const shouldCacheSnapshot = formSubmission.method == FetchMethod.get;
2265
+ if (!shouldCacheSnapshot) {
2265
2266
  this.view.clearSnapshotCache();
2266
2267
  }
2267
2268
  const { statusCode, redirected } = fetchResponse;
2268
2269
  const action = this.getActionForFormSubmission(formSubmission);
2269
2270
  const visitOptions = {
2270
2271
  action,
2272
+ shouldCacheSnapshot,
2271
2273
  response: { statusCode, responseHTML, redirected },
2272
2274
  };
2273
2275
  this.proposeVisit(fetchResponse.location, visitOptions);
@@ -2466,7 +2468,7 @@ class StreamObserver {
2466
2468
  }
2467
2469
  }
2468
2470
  receiveMessageHTML(html) {
2469
- this.delegate.receivedMessageFromStream(new StreamMessage(html));
2471
+ this.delegate.receivedMessageFromStream(StreamMessage.wrap(html));
2470
2472
  }
2471
2473
  }
2472
2474
  function fetchResponseFromEvent(event) {
@@ -2500,7 +2502,7 @@ class ErrorRenderer extends Renderer {
2500
2502
  for (const replaceableElement of this.scriptElements) {
2501
2503
  const parentNode = replaceableElement.parentNode;
2502
2504
  if (parentNode) {
2503
- const element = this.createScriptElement(replaceableElement);
2505
+ const element = activateScriptElement(replaceableElement);
2504
2506
  parentNode.replaceChild(element, replaceableElement);
2505
2507
  }
2506
2508
  }
@@ -2509,7 +2511,7 @@ class ErrorRenderer extends Renderer {
2509
2511
  return this.newSnapshot.headSnapshot.element;
2510
2512
  }
2511
2513
  get scriptElements() {
2512
- return [...document.documentElement.querySelectorAll("script")];
2514
+ return document.documentElement.querySelectorAll("script");
2513
2515
  }
2514
2516
  }
2515
2517
 
@@ -2537,8 +2539,8 @@ class PageRenderer extends Renderer {
2537
2539
  };
2538
2540
  }
2539
2541
  }
2540
- prepareToRender() {
2541
- this.mergeHead();
2542
+ async prepareToRender() {
2543
+ await this.mergeHead();
2542
2544
  }
2543
2545
  async render() {
2544
2546
  if (this.willRender) {
@@ -2560,11 +2562,12 @@ class PageRenderer extends Renderer {
2560
2562
  get newElement() {
2561
2563
  return this.newSnapshot.element;
2562
2564
  }
2563
- mergeHead() {
2564
- this.copyNewHeadStylesheetElements();
2565
+ async mergeHead() {
2566
+ const newStylesheetElements = this.copyNewHeadStylesheetElements();
2565
2567
  this.copyNewHeadScriptElements();
2566
2568
  this.removeCurrentHeadProvisionalElements();
2567
2569
  this.copyNewHeadProvisionalElements();
2570
+ await newStylesheetElements;
2568
2571
  }
2569
2572
  replaceBody() {
2570
2573
  this.preservingPermanentElements(() => {
@@ -2575,14 +2578,17 @@ class PageRenderer extends Renderer {
2575
2578
  get trackedElementsAreIdentical() {
2576
2579
  return this.currentHeadSnapshot.trackedElementSignature == this.newHeadSnapshot.trackedElementSignature;
2577
2580
  }
2578
- copyNewHeadStylesheetElements() {
2581
+ async copyNewHeadStylesheetElements() {
2582
+ const loadingElements = [];
2579
2583
  for (const element of this.newHeadStylesheetElements) {
2584
+ loadingElements.push(waitForLoad(element));
2580
2585
  document.head.appendChild(element);
2581
2586
  }
2587
+ await Promise.all(loadingElements);
2582
2588
  }
2583
2589
  copyNewHeadScriptElements() {
2584
2590
  for (const element of this.newHeadScriptElements) {
2585
- document.head.appendChild(this.createScriptElement(element));
2591
+ document.head.appendChild(activateScriptElement(element));
2586
2592
  }
2587
2593
  }
2588
2594
  removeCurrentHeadProvisionalElements() {
@@ -2601,7 +2607,7 @@ class PageRenderer extends Renderer {
2601
2607
  }
2602
2608
  activateNewBodyScriptElements() {
2603
2609
  for (const inertScriptElement of this.newBodyScriptElements) {
2604
- const activatedScriptElement = this.createScriptElement(inertScriptElement);
2610
+ const activatedScriptElement = activateScriptElement(inertScriptElement);
2605
2611
  inertScriptElement.replaceWith(activatedScriptElement);
2606
2612
  }
2607
2613
  }
@@ -2764,12 +2770,12 @@ class Session {
2764
2770
  this.adapter = new BrowserAdapter(this);
2765
2771
  this.pageObserver = new PageObserver(this);
2766
2772
  this.cacheObserver = new CacheObserver();
2767
- this.linkClickObserver = new LinkClickObserver(this);
2768
- this.formSubmitObserver = new FormSubmitObserver(this);
2773
+ this.linkClickObserver = new LinkClickObserver(this, window);
2774
+ this.formSubmitObserver = new FormSubmitObserver(this, document);
2769
2775
  this.scrollObserver = new ScrollObserver(this);
2770
2776
  this.streamObserver = new StreamObserver(this);
2771
- this.formLinkInterceptor = new FormLinkInterceptor(this, document.documentElement);
2772
- this.frameRedirector = new FrameRedirector(document.documentElement);
2777
+ this.formLinkClickObserver = new FormLinkClickObserver(this, document.documentElement);
2778
+ this.frameRedirector = new FrameRedirector(this, document.documentElement);
2773
2779
  this.drive = true;
2774
2780
  this.enabled = true;
2775
2781
  this.progressBarDelay = 500;
@@ -2780,7 +2786,7 @@ class Session {
2780
2786
  if (!this.started) {
2781
2787
  this.pageObserver.start();
2782
2788
  this.cacheObserver.start();
2783
- this.formLinkInterceptor.start();
2789
+ this.formLinkClickObserver.start();
2784
2790
  this.linkClickObserver.start();
2785
2791
  this.formSubmitObserver.start();
2786
2792
  this.scrollObserver.start();
@@ -2799,7 +2805,7 @@ class Session {
2799
2805
  if (this.started) {
2800
2806
  this.pageObserver.stop();
2801
2807
  this.cacheObserver.stop();
2802
- this.formLinkInterceptor.stop();
2808
+ this.formLinkClickObserver.stop();
2803
2809
  this.linkClickObserver.stop();
2804
2810
  this.formSubmitObserver.stop();
2805
2811
  this.scrollObserver.stop();
@@ -2813,7 +2819,14 @@ class Session {
2813
2819
  this.adapter = adapter;
2814
2820
  }
2815
2821
  visit(location, options = {}) {
2816
- this.navigator.proposeVisit(expandURL(location), options);
2822
+ const frameElement = document.getElementById(options.frame || "");
2823
+ if (frameElement instanceof FrameElement) {
2824
+ frameElement.src = location.toString();
2825
+ return frameElement.loaded;
2826
+ }
2827
+ else {
2828
+ return this.navigator.proposeVisit(expandURL(location), options);
2829
+ }
2817
2830
  }
2818
2831
  connectStreamSource(source) {
2819
2832
  this.streamObserver.connectStreamSource(source);
@@ -2855,25 +2868,26 @@ class Session {
2855
2868
  scrollPositionChanged(position) {
2856
2869
  this.history.updateRestorationData({ scrollPosition: position });
2857
2870
  }
2858
- shouldInterceptFormLinkClick(_link) {
2859
- return true;
2871
+ willSubmitFormLinkToLocation(link, location) {
2872
+ return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation);
2860
2873
  }
2861
- formLinkClickIntercepted(_link, _form) { }
2874
+ submittedFormLinkToLocation() { }
2862
2875
  willFollowLinkToLocation(link, location, event) {
2863
- return (this.elementDriveEnabled(link) &&
2876
+ return (this.elementIsNavigatable(link) &&
2864
2877
  locationIsVisitable(location, this.snapshot.rootLocation) &&
2865
2878
  this.applicationAllowsFollowingLinkToLocation(link, location, event));
2866
2879
  }
2867
2880
  followedLinkToLocation(link, location) {
2868
2881
  const action = this.getActionForLink(link);
2869
- this.visit(location.href, { action });
2882
+ const acceptsStreamResponse = link.hasAttribute("data-turbo-stream");
2883
+ this.visit(location.href, { action, acceptsStreamResponse });
2870
2884
  }
2871
2885
  allowsVisitingLocationWithAction(location, action) {
2872
2886
  return this.locationWithActionIsSamePage(location, action) || this.applicationAllowsVisitingLocation(location);
2873
2887
  }
2874
2888
  visitProposedToLocation(location, options) {
2875
2889
  extendURLWithDeprecatedProperties(location);
2876
- this.adapter.visitProposedToLocation(location, options);
2890
+ return this.adapter.visitProposedToLocation(location, options);
2877
2891
  }
2878
2892
  visitStarted(visit) {
2879
2893
  extendURLWithDeprecatedProperties(visit.location);
@@ -2892,8 +2906,7 @@ class Session {
2892
2906
  }
2893
2907
  willSubmitForm(form, submitter) {
2894
2908
  const action = getAction(form, submitter);
2895
- return (this.elementDriveEnabled(form) &&
2896
- (!submitter || this.formElementDriveEnabled(submitter)) &&
2909
+ return (this.submissionIsNavigatable(form, submitter) &&
2897
2910
  locationIsVisitable(expandURL(action), this.snapshot.rootLocation));
2898
2911
  }
2899
2912
  formSubmitted(form, submitter) {
@@ -2942,6 +2955,10 @@ class Session {
2942
2955
  frameRendered(fetchResponse, frame) {
2943
2956
  this.notifyApplicationAfterFrameRender(fetchResponse, frame);
2944
2957
  }
2958
+ frameMissing(frame, fetchResponse) {
2959
+ console.warn(`Completing full-page visit as matching frame for #${frame.id} was missing from the response`);
2960
+ return this.visit(fetchResponse.location);
2961
+ }
2945
2962
  applicationAllowsFollowingLinkToLocation(link, location, ev) {
2946
2963
  const event = this.notifyApplicationAfterClickingLinkToLocation(link, location, ev);
2947
2964
  return !event.defaultPrevented;
@@ -3001,19 +3018,24 @@ class Session {
3001
3018
  cancelable: true,
3002
3019
  });
3003
3020
  }
3004
- formElementDriveEnabled(element) {
3021
+ submissionIsNavigatable(form, submitter) {
3005
3022
  if (this.formMode == "off") {
3006
3023
  return false;
3007
3024
  }
3008
- if (this.formMode == "optin") {
3009
- const form = element === null || element === void 0 ? void 0 : element.closest("form[data-turbo]");
3010
- return (form === null || form === void 0 ? void 0 : form.getAttribute("data-turbo")) == "true";
3025
+ else {
3026
+ const submitterIsNavigatable = submitter ? this.elementIsNavigatable(submitter) : true;
3027
+ if (this.formMode == "optin") {
3028
+ return submitterIsNavigatable && form.closest('[data-turbo="true"]') != null;
3029
+ }
3030
+ else {
3031
+ return submitterIsNavigatable && this.elementIsNavigatable(form);
3032
+ }
3011
3033
  }
3012
- return this.elementDriveEnabled(element);
3013
3034
  }
3014
- elementDriveEnabled(element) {
3015
- const container = element === null || element === void 0 ? void 0 : element.closest("[data-turbo]");
3016
- if (this.drive) {
3035
+ elementIsNavigatable(element) {
3036
+ const container = element.closest("[data-turbo]");
3037
+ const withinFrame = element.closest("turbo-frame");
3038
+ if (this.drive || withinFrame) {
3017
3039
  if (container) {
3018
3040
  return container.getAttribute("data-turbo") != "false";
3019
3041
  }
@@ -3106,7 +3128,7 @@ function registerAdapter(adapter) {
3106
3128
  session.registerAdapter(adapter);
3107
3129
  }
3108
3130
  function visit(location, options) {
3109
- session.visit(location, options);
3131
+ return session.visit(location, options);
3110
3132
  }
3111
3133
  function connectStreamSource(source) {
3112
3134
  session.connectStreamSource(source);
@@ -3160,6 +3182,7 @@ class FrameController {
3160
3182
  this.connected = false;
3161
3183
  this.hasBeenLoaded = false;
3162
3184
  this.ignoredAttributes = new Set();
3185
+ this.action = null;
3163
3186
  this.visitCachedSnapshot = ({ element }) => {
3164
3187
  const frame = element.querySelector("#" + this.element.id);
3165
3188
  if (frame && this.previousFrameElement) {
@@ -3170,9 +3193,10 @@ class FrameController {
3170
3193
  this.element = element;
3171
3194
  this.view = new FrameView(this, this.element);
3172
3195
  this.appearanceObserver = new AppearanceObserver(this, this.element);
3173
- this.formLinkInterceptor = new FormLinkInterceptor(this, this.element);
3174
- this.linkInterceptor = new LinkInterceptor(this, this.element);
3175
- this.formInterceptor = new FormInterceptor(this, this.element);
3196
+ this.formLinkClickObserver = new FormLinkClickObserver(this, this.element);
3197
+ this.linkClickObserver = new LinkClickObserver(this, this.element);
3198
+ this.restorationIdentifier = uuid();
3199
+ this.formSubmitObserver = new FormSubmitObserver(this, this.element);
3176
3200
  }
3177
3201
  connect() {
3178
3202
  if (!this.connected) {
@@ -3183,18 +3207,18 @@ class FrameController {
3183
3207
  else {
3184
3208
  this.loadSourceURL();
3185
3209
  }
3186
- this.formLinkInterceptor.start();
3187
- this.linkInterceptor.start();
3188
- this.formInterceptor.start();
3210
+ this.formLinkClickObserver.start();
3211
+ this.linkClickObserver.start();
3212
+ this.formSubmitObserver.start();
3189
3213
  }
3190
3214
  }
3191
3215
  disconnect() {
3192
3216
  if (this.connected) {
3193
3217
  this.connected = false;
3194
3218
  this.appearanceObserver.stop();
3195
- this.formLinkInterceptor.stop();
3196
- this.linkInterceptor.stop();
3197
- this.formInterceptor.stop();
3219
+ this.formLinkClickObserver.stop();
3220
+ this.linkClickObserver.stop();
3221
+ this.formSubmitObserver.stop();
3198
3222
  }
3199
3223
  }
3200
3224
  disabledChanged() {
@@ -3242,15 +3266,22 @@ class FrameController {
3242
3266
  const html = await fetchResponse.responseHTML;
3243
3267
  if (html) {
3244
3268
  const { body } = parseHTMLDocument(html);
3245
- const snapshot = new Snapshot(await this.extractForeignFrameElement(body));
3246
- const renderer = new FrameRenderer(this, this.view.snapshot, snapshot, FrameRenderer.renderElement, false, false);
3247
- if (this.view.renderPromise)
3248
- await this.view.renderPromise;
3249
- await this.view.render(renderer);
3250
- this.complete = true;
3251
- session.frameRendered(fetchResponse, this.element);
3252
- session.frameLoaded(this.element);
3253
- this.fetchResponseLoaded(fetchResponse);
3269
+ const newFrameElement = await this.extractForeignFrameElement(body);
3270
+ if (newFrameElement) {
3271
+ const snapshot = new Snapshot(newFrameElement);
3272
+ const renderer = new FrameRenderer(this, this.view.snapshot, snapshot, FrameRenderer.renderElement, false, false);
3273
+ if (this.view.renderPromise)
3274
+ await this.view.renderPromise;
3275
+ this.changeHistory();
3276
+ await this.view.render(renderer);
3277
+ this.complete = true;
3278
+ session.frameRendered(fetchResponse, this.element);
3279
+ session.frameLoaded(this.element);
3280
+ this.fetchResponseLoaded(fetchResponse);
3281
+ }
3282
+ else if (this.sessionWillHandleMissingFrame(fetchResponse)) {
3283
+ await session.frameMissing(this.element, fetchResponse);
3284
+ }
3254
3285
  }
3255
3286
  }
3256
3287
  catch (error) {
@@ -3264,24 +3295,24 @@ class FrameController {
3264
3295
  elementAppearedInViewport(_element) {
3265
3296
  this.loadSourceURL();
3266
3297
  }
3267
- shouldInterceptFormLinkClick(link) {
3268
- return this.shouldInterceptNavigation(link);
3298
+ willSubmitFormLinkToLocation(link) {
3299
+ return link.closest("turbo-frame") == this.element && this.shouldInterceptNavigation(link);
3269
3300
  }
3270
- formLinkClickIntercepted(link, form) {
3301
+ submittedFormLinkToLocation(link, _location, form) {
3271
3302
  const frame = this.findFrameElement(link);
3272
3303
  if (frame)
3273
3304
  form.setAttribute("data-turbo-frame", frame.id);
3274
3305
  }
3275
- shouldInterceptLinkClick(element, _url) {
3306
+ willFollowLinkToLocation(element) {
3276
3307
  return this.shouldInterceptNavigation(element);
3277
3308
  }
3278
- linkClickIntercepted(element, url) {
3279
- this.navigateFrame(element, url);
3309
+ followedLinkToLocation(element, location) {
3310
+ this.navigateFrame(element, location.href);
3280
3311
  }
3281
- shouldInterceptFormSubmission(element, submitter) {
3282
- return this.shouldInterceptNavigation(element, submitter);
3312
+ willSubmitForm(element, submitter) {
3313
+ return element.closest("turbo-frame") == this.element && this.shouldInterceptNavigation(element, submitter);
3283
3314
  }
3284
- formSubmissionIntercepted(element, submitter) {
3315
+ formSubmitted(element, submitter) {
3285
3316
  if (this.formSubmission) {
3286
3317
  this.formSubmission.stop();
3287
3318
  }
@@ -3290,8 +3321,12 @@ class FrameController {
3290
3321
  this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
3291
3322
  this.formSubmission.start();
3292
3323
  }
3293
- prepareHeadersForRequest(headers, _request) {
3324
+ prepareHeadersForRequest(headers, request) {
3325
+ var _a;
3294
3326
  headers["Turbo-Frame"] = this.id;
3327
+ if ((_a = this.currentNavigationElement) === null || _a === void 0 ? void 0 : _a.hasAttribute("data-turbo-stream")) {
3328
+ request.acceptResponseType(StreamMessage.contentType);
3329
+ }
3295
3330
  }
3296
3331
  requestStarted(_request) {
3297
3332
  markAsBusy(this.element);
@@ -3309,6 +3344,10 @@ class FrameController {
3309
3344
  }
3310
3345
  requestErrored(request, error) {
3311
3346
  console.error(error);
3347
+ dispatch("turbo:fetch-request-error", {
3348
+ target: this.element,
3349
+ detail: { request, error },
3350
+ });
3312
3351
  this.resolveVisitPromise();
3313
3352
  }
3314
3353
  requestFinished(_request) {
@@ -3348,8 +3387,8 @@ class FrameController {
3348
3387
  session.preloadOnLoadLinksForView(element);
3349
3388
  }
3350
3389
  viewInvalidated() { }
3351
- frameExtracted(element) {
3352
- this.previousFrameElement = element;
3390
+ willRenderFrame(currentElement, _newElement) {
3391
+ this.previousFrameElement = currentElement.cloneNode(true);
3353
3392
  }
3354
3393
  async visit(url) {
3355
3394
  var _a;
@@ -3368,27 +3407,49 @@ class FrameController {
3368
3407
  navigateFrame(element, url, submitter) {
3369
3408
  const frame = this.findFrameElement(element, submitter);
3370
3409
  this.proposeVisitIfNavigatedWithAction(frame, element, submitter);
3371
- frame.src = url;
3410
+ this.withCurrentNavigationElement(element, () => {
3411
+ frame.src = url;
3412
+ });
3372
3413
  }
3373
3414
  proposeVisitIfNavigatedWithAction(frame, element, submitter) {
3374
- const action = getAttribute("data-turbo-action", submitter, element, frame);
3375
- if (isAction(action)) {
3415
+ this.action = getVisitAction(submitter, element, frame);
3416
+ this.frame = frame;
3417
+ if (isAction(this.action)) {
3376
3418
  const { visitCachedSnapshot } = frame.delegate;
3377
3419
  frame.delegate.fetchResponseLoaded = (fetchResponse) => {
3378
3420
  if (frame.src) {
3379
3421
  const { statusCode, redirected } = fetchResponse;
3380
3422
  const responseHTML = frame.ownerDocument.documentElement.outerHTML;
3381
3423
  const response = { statusCode, redirected, responseHTML };
3382
- session.visit(frame.src, {
3383
- action,
3424
+ const options = {
3384
3425
  response,
3385
3426
  visitCachedSnapshot,
3386
3427
  willRender: false,
3387
- });
3428
+ updateHistory: false,
3429
+ restorationIdentifier: this.restorationIdentifier,
3430
+ };
3431
+ if (this.action)
3432
+ options.action = this.action;
3433
+ session.visit(frame.src, options);
3388
3434
  }
3389
3435
  };
3390
3436
  }
3391
3437
  }
3438
+ changeHistory() {
3439
+ if (this.action && this.frame) {
3440
+ const method = getHistoryMethodForAction(this.action);
3441
+ session.history.update(method, expandURL(this.frame.src || ""), this.restorationIdentifier);
3442
+ }
3443
+ }
3444
+ sessionWillHandleMissingFrame(fetchResponse) {
3445
+ this.element.setAttribute("complete", "");
3446
+ const event = dispatch("turbo:frame-missing", {
3447
+ target: this.element,
3448
+ detail: { fetchResponse },
3449
+ cancelable: true,
3450
+ });
3451
+ return !event.defaultPrevented;
3452
+ }
3392
3453
  findFrameElement(element, submitter) {
3393
3454
  var _a;
3394
3455
  const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
@@ -3407,12 +3468,12 @@ class FrameController {
3407
3468
  await element.loaded;
3408
3469
  return await this.extractForeignFrameElement(element);
3409
3470
  }
3410
- console.error(`Response has no matching <turbo-frame id="${id}"> element`);
3411
3471
  }
3412
3472
  catch (error) {
3413
3473
  console.error(error);
3474
+ return new FrameElement();
3414
3475
  }
3415
- return new FrameElement();
3476
+ return null;
3416
3477
  }
3417
3478
  formActionIsVisitable(form, submitter) {
3418
3479
  const action = getAction(form, submitter);
@@ -3432,10 +3493,10 @@ class FrameController {
3432
3493
  return !frameElement.disabled;
3433
3494
  }
3434
3495
  }
3435
- if (!session.elementDriveEnabled(element)) {
3496
+ if (!session.elementIsNavigatable(element)) {
3436
3497
  return false;
3437
3498
  }
3438
- if (submitter && !session.elementDriveEnabled(submitter)) {
3499
+ if (submitter && !session.elementIsNavigatable(submitter)) {
3439
3500
  return false;
3440
3501
  }
3441
3502
  return true;
@@ -3492,6 +3553,11 @@ class FrameController {
3492
3553
  callback();
3493
3554
  this.ignoredAttributes.delete(attributeName);
3494
3555
  }
3556
+ withCurrentNavigationElement(element, callback) {
3557
+ this.currentNavigationElement = element;
3558
+ callback();
3559
+ delete this.currentNavigationElement;
3560
+ }
3495
3561
  }
3496
3562
  function getFrameElementById(id) {
3497
3563
  if (id != null) {
@@ -3604,6 +3670,7 @@ class StreamElement extends HTMLElement {
3604
3670
  return new CustomEvent("turbo:before-stream-render", {
3605
3671
  bubbles: true,
3606
3672
  cancelable: true,
3673
+ detail: { newStream: this },
3607
3674
  });
3608
3675
  }
3609
3676
  get targetElementsById() {