@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 (global, factory) {
@@ -138,6 +138,7 @@ Copyright © 2022 Basecamp, LLC
138
138
  this.removeAttribute("complete");
139
139
  this.src = null;
140
140
  this.src = src;
141
+ return this.loaded;
141
142
  }
142
143
  attributeChangedCallback(name) {
143
144
  if (name == "loading") {
@@ -316,6 +317,36 @@ Copyright © 2022 Basecamp, LLC
316
317
  }
317
318
  }
318
319
 
320
+ function isAction(action) {
321
+ return action == "advance" || action == "replace" || action == "restore";
322
+ }
323
+
324
+ function activateScriptElement(element) {
325
+ if (element.getAttribute("data-turbo-eval") == "false") {
326
+ return element;
327
+ }
328
+ else {
329
+ const createdScriptElement = document.createElement("script");
330
+ const cspNonce = getMetaContent("csp-nonce");
331
+ if (cspNonce) {
332
+ createdScriptElement.nonce = cspNonce;
333
+ }
334
+ createdScriptElement.textContent = element.textContent;
335
+ createdScriptElement.async = false;
336
+ copyElementAttributes(createdScriptElement, element);
337
+ return createdScriptElement;
338
+ }
339
+ }
340
+ function copyElementAttributes(destinationElement, sourceElement) {
341
+ for (const { name, value } of sourceElement.attributes) {
342
+ destinationElement.setAttribute(name, value);
343
+ }
344
+ }
345
+ function createDocumentFragment(html) {
346
+ const template = document.createElement("template");
347
+ template.innerHTML = html;
348
+ return template.content;
349
+ }
319
350
  function dispatch(eventName, { target, cancelable, detail } = {}) {
320
351
  const event = new CustomEvent(eventName, {
321
352
  cancelable,
@@ -395,6 +426,31 @@ Copyright © 2022 Basecamp, LLC
395
426
  element.removeAttribute("aria-busy");
396
427
  }
397
428
  }
429
+ function waitForLoad(element, timeoutInMilliseconds = 2000) {
430
+ return new Promise((resolve) => {
431
+ const onComplete = () => {
432
+ element.removeEventListener("error", onComplete);
433
+ element.removeEventListener("load", onComplete);
434
+ resolve();
435
+ };
436
+ element.addEventListener("load", onComplete, { once: true });
437
+ element.addEventListener("error", onComplete, { once: true });
438
+ setTimeout(resolve, timeoutInMilliseconds);
439
+ });
440
+ }
441
+ function getHistoryMethodForAction(action) {
442
+ switch (action) {
443
+ case "replace":
444
+ return history.replaceState;
445
+ case "advance":
446
+ case "restore":
447
+ return history.pushState;
448
+ }
449
+ }
450
+ function getVisitAction(...elements) {
451
+ const action = getAttribute("data-turbo-action", ...elements);
452
+ return isAction(action) ? action : null;
453
+ }
398
454
  function getMetaElement(name) {
399
455
  return document.querySelector(`meta[name="${name}"]`);
400
456
  }
@@ -519,6 +575,9 @@ Copyright © 2022 Basecamp, LLC
519
575
  get abortSignal() {
520
576
  return this.abortController.signal;
521
577
  }
578
+ acceptResponseType(mimeType) {
579
+ this.headers["Accept"] = [mimeType, this.headers["Accept"]].join(", ");
580
+ }
522
581
  async allowRequestToBeIntercepted(fetchOptions) {
523
582
  const requestInterception = new Promise((resolve) => (this.resolveRequestPromise = resolve));
524
583
  const event = dispatch("turbo:before-fetch-request", {
@@ -563,40 +622,29 @@ Copyright © 2022 Basecamp, LLC
563
622
  }
564
623
 
565
624
  class StreamMessage {
566
- constructor(html) {
567
- this.templateElement = document.createElement("template");
568
- this.templateElement.innerHTML = html;
625
+ constructor(fragment) {
626
+ this.fragment = importStreamElements(fragment);
569
627
  }
570
628
  static wrap(message) {
571
629
  if (typeof message == "string") {
572
- return new this(message);
630
+ return new this(createDocumentFragment(message));
573
631
  }
574
632
  else {
575
633
  return message;
576
634
  }
577
635
  }
578
- get fragment() {
579
- const fragment = document.createDocumentFragment();
580
- for (const element of this.foreignElements) {
581
- fragment.appendChild(document.importNode(element, true));
636
+ }
637
+ StreamMessage.contentType = "text/vnd.turbo-stream.html";
638
+ function importStreamElements(fragment) {
639
+ for (const element of fragment.querySelectorAll("turbo-stream")) {
640
+ const streamElement = document.importNode(element, true);
641
+ for (const inertScriptElement of streamElement.templateElement.content.querySelectorAll("script")) {
642
+ inertScriptElement.replaceWith(activateScriptElement(inertScriptElement));
582
643
  }
583
- return fragment;
584
- }
585
- get foreignElements() {
586
- return this.templateChildren.reduce((streamElements, child) => {
587
- if (child.tagName.toLowerCase() == "turbo-stream") {
588
- return [...streamElements, child];
589
- }
590
- else {
591
- return streamElements;
592
- }
593
- }, []);
594
- }
595
- get templateChildren() {
596
- return Array.from(this.templateElement.content.children);
644
+ element.replaceWith(streamElement);
597
645
  }
646
+ return fragment;
598
647
  }
599
- StreamMessage.contentType = "text/vnd.turbo-stream.html";
600
648
 
601
649
  var FormSubmissionState;
602
650
  (function (FormSubmissionState) {
@@ -711,7 +759,7 @@ Copyright © 2022 Basecamp, LLC
711
759
  }
712
760
  }
713
761
  if (this.requestAcceptsTurboStreamResponse(request)) {
714
- headers["Accept"] = [StreamMessage.contentType, headers["Accept"]].join(", ");
762
+ request.acceptResponseType(StreamMessage.contentType);
715
763
  }
716
764
  }
717
765
  requestStarted(_request) {
@@ -747,6 +795,10 @@ Copyright © 2022 Basecamp, LLC
747
795
  }
748
796
  requestErrored(request, error) {
749
797
  this.result = { success: false, error };
798
+ dispatch("turbo:fetch-request-error", {
799
+ target: this.formElement,
800
+ detail: { request, error },
801
+ });
750
802
  this.delegate.formSubmissionErrored(this, error);
751
803
  }
752
804
  requestFinished(_request) {
@@ -770,8 +822,8 @@ Copyright © 2022 Basecamp, LLC
770
822
  const formData = new FormData(formElement);
771
823
  const name = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("name");
772
824
  const value = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("value");
773
- if (name && value != null && formData.get(name) != value) {
774
- formData.append(name, value);
825
+ if (name) {
826
+ formData.append(name, value || "");
775
827
  }
776
828
  return formData;
777
829
  }
@@ -819,7 +871,14 @@ Copyright © 2022 Basecamp, LLC
819
871
  return this.element.isConnected;
820
872
  }
821
873
  get firstAutofocusableElement() {
822
- return this.element.querySelector("[autofocus]");
874
+ const inertDisabledOrHidden = "[inert], :disabled, [hidden], details:not([open]), dialog:not([open])";
875
+ for (const element of this.element.querySelectorAll("[autofocus]")) {
876
+ if (element.closest(inertDisabledOrHidden) == null)
877
+ return element;
878
+ else
879
+ continue;
880
+ }
881
+ return null;
823
882
  }
824
883
  get permanentElements() {
825
884
  return [...this.element.querySelectorAll("[id][data-turbo-permanent]")];
@@ -840,32 +899,54 @@ Copyright © 2022 Basecamp, LLC
840
899
  }
841
900
  }
842
901
 
843
- class FormInterceptor {
844
- constructor(delegate, element) {
902
+ class FormSubmitObserver {
903
+ constructor(delegate, eventTarget) {
904
+ this.started = false;
905
+ this.submitCaptured = () => {
906
+ this.eventTarget.removeEventListener("submit", this.submitBubbled, false);
907
+ this.eventTarget.addEventListener("submit", this.submitBubbled, false);
908
+ };
845
909
  this.submitBubbled = ((event) => {
846
- const form = event.target;
847
- if (!event.defaultPrevented &&
848
- form instanceof HTMLFormElement &&
849
- form.closest("turbo-frame, html") == this.element) {
910
+ if (!event.defaultPrevented) {
911
+ const form = event.target instanceof HTMLFormElement ? event.target : undefined;
850
912
  const submitter = event.submitter || undefined;
851
- const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.method;
852
- if (method != "dialog" && this.delegate.shouldInterceptFormSubmission(form, submitter)) {
913
+ if (form &&
914
+ submissionDoesNotDismissDialog(form, submitter) &&
915
+ submissionDoesNotTargetIFrame(form, submitter) &&
916
+ this.delegate.willSubmitForm(form, submitter)) {
853
917
  event.preventDefault();
854
- event.stopImmediatePropagation();
855
- this.delegate.formSubmissionIntercepted(form, submitter);
918
+ this.delegate.formSubmitted(form, submitter);
856
919
  }
857
920
  }
858
921
  });
859
922
  this.delegate = delegate;
860
- this.element = element;
923
+ this.eventTarget = eventTarget;
861
924
  }
862
925
  start() {
863
- this.element.addEventListener("submit", this.submitBubbled);
926
+ if (!this.started) {
927
+ this.eventTarget.addEventListener("submit", this.submitCaptured, true);
928
+ this.started = true;
929
+ }
864
930
  }
865
931
  stop() {
866
- this.element.removeEventListener("submit", this.submitBubbled);
932
+ if (this.started) {
933
+ this.eventTarget.removeEventListener("submit", this.submitCaptured, true);
934
+ this.started = false;
935
+ }
867
936
  }
868
937
  }
938
+ function submissionDoesNotDismissDialog(form, submitter) {
939
+ const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.getAttribute("method");
940
+ return method != "dialog";
941
+ }
942
+ function submissionDoesNotTargetIFrame(form, submitter) {
943
+ const target = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formtarget")) || form.target;
944
+ for (const element of document.getElementsByName(target)) {
945
+ if (element instanceof HTMLIFrameElement)
946
+ return false;
947
+ }
948
+ return true;
949
+ }
869
950
 
870
951
  class View {
871
952
  constructor(delegate, element) {
@@ -917,7 +998,7 @@ Copyright © 2022 Basecamp, LLC
917
998
  try {
918
999
  this.renderPromise = new Promise((resolve) => (this.resolveRenderPromise = resolve));
919
1000
  this.renderer = renderer;
920
- this.prepareToRenderSnapshot(renderer);
1001
+ await this.prepareToRenderSnapshot(renderer);
921
1002
  const renderInterception = new Promise((resolve) => (this.resolveInterceptionPromise = resolve));
922
1003
  const options = { resume: this.resolveInterceptionPromise, render: this.renderer.renderElement };
923
1004
  const immediateRender = this.delegate.allowsImmediateRender(snapshot, options);
@@ -941,9 +1022,9 @@ Copyright © 2022 Basecamp, LLC
941
1022
  invalidate(reason) {
942
1023
  this.delegate.viewInvalidated(reason);
943
1024
  }
944
- prepareToRenderSnapshot(renderer) {
1025
+ async prepareToRenderSnapshot(renderer) {
945
1026
  this.markAsPreview(renderer.isPreview);
946
- renderer.prepareToRender();
1027
+ await renderer.prepareToRender();
947
1028
  }
948
1029
  markAsPreview(isPreview) {
949
1030
  if (isPreview) {
@@ -970,64 +1051,84 @@ Copyright © 2022 Basecamp, LLC
970
1051
  }
971
1052
  }
972
1053
 
973
- class LinkInterceptor {
974
- constructor(delegate, element) {
975
- this.clickBubbled = (event) => {
976
- if (this.respondsToEventTarget(event.target)) {
977
- this.clickEvent = event;
978
- }
979
- else {
980
- delete this.clickEvent;
981
- }
1054
+ class LinkClickObserver {
1055
+ constructor(delegate, eventTarget) {
1056
+ this.started = false;
1057
+ this.clickCaptured = () => {
1058
+ this.eventTarget.removeEventListener("click", this.clickBubbled, false);
1059
+ this.eventTarget.addEventListener("click", this.clickBubbled, false);
982
1060
  };
983
- this.linkClicked = ((event) => {
984
- if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) {
985
- if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url)) {
986
- this.clickEvent.preventDefault();
987
- event.preventDefault();
988
- this.delegate.linkClickIntercepted(event.target, event.detail.url);
1061
+ this.clickBubbled = (event) => {
1062
+ if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {
1063
+ const target = (event.composedPath && event.composedPath()[0]) || event.target;
1064
+ const link = this.findLinkFromClickTarget(target);
1065
+ if (link && doesNotTargetIFrame(link)) {
1066
+ const location = this.getLocationForLink(link);
1067
+ if (this.delegate.willFollowLinkToLocation(link, location, event)) {
1068
+ event.preventDefault();
1069
+ this.delegate.followedLinkToLocation(link, location);
1070
+ }
989
1071
  }
990
1072
  }
991
- delete this.clickEvent;
992
- });
993
- this.willVisit = ((_event) => {
994
- delete this.clickEvent;
995
- });
1073
+ };
996
1074
  this.delegate = delegate;
997
- this.element = element;
1075
+ this.eventTarget = eventTarget;
998
1076
  }
999
1077
  start() {
1000
- this.element.addEventListener("click", this.clickBubbled);
1001
- document.addEventListener("turbo:click", this.linkClicked);
1002
- document.addEventListener("turbo:before-visit", this.willVisit);
1078
+ if (!this.started) {
1079
+ this.eventTarget.addEventListener("click", this.clickCaptured, true);
1080
+ this.started = true;
1081
+ }
1003
1082
  }
1004
1083
  stop() {
1005
- this.element.removeEventListener("click", this.clickBubbled);
1006
- document.removeEventListener("turbo:click", this.linkClicked);
1007
- document.removeEventListener("turbo:before-visit", this.willVisit);
1084
+ if (this.started) {
1085
+ this.eventTarget.removeEventListener("click", this.clickCaptured, true);
1086
+ this.started = false;
1087
+ }
1088
+ }
1089
+ clickEventIsSignificant(event) {
1090
+ return !((event.target && event.target.isContentEditable) ||
1091
+ event.defaultPrevented ||
1092
+ event.which > 1 ||
1093
+ event.altKey ||
1094
+ event.ctrlKey ||
1095
+ event.metaKey ||
1096
+ event.shiftKey);
1097
+ }
1098
+ findLinkFromClickTarget(target) {
1099
+ if (target instanceof Element) {
1100
+ return target.closest("a[href]:not([target^=_]):not([download])");
1101
+ }
1008
1102
  }
1009
- respondsToEventTarget(target) {
1010
- const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
1011
- return element && element.closest("turbo-frame, html") == this.element;
1103
+ getLocationForLink(link) {
1104
+ return expandURL(link.getAttribute("href") || "");
1012
1105
  }
1013
1106
  }
1107
+ function doesNotTargetIFrame(anchor) {
1108
+ for (const element of document.getElementsByName(anchor.target)) {
1109
+ if (element instanceof HTMLIFrameElement)
1110
+ return false;
1111
+ }
1112
+ return true;
1113
+ }
1014
1114
 
1015
- class FormLinkInterceptor {
1115
+ class FormLinkClickObserver {
1016
1116
  constructor(delegate, element) {
1017
1117
  this.delegate = delegate;
1018
- this.linkInterceptor = new LinkInterceptor(this, element);
1118
+ this.linkClickObserver = new LinkClickObserver(this, element);
1019
1119
  }
1020
1120
  start() {
1021
- this.linkInterceptor.start();
1121
+ this.linkClickObserver.start();
1022
1122
  }
1023
1123
  stop() {
1024
- this.linkInterceptor.stop();
1124
+ this.linkClickObserver.stop();
1025
1125
  }
1026
- shouldInterceptLinkClick(link) {
1027
- return (this.delegate.shouldInterceptFormLinkClick(link) &&
1028
- (link.hasAttribute("data-turbo-method") || link.hasAttribute("data-turbo-stream")));
1126
+ willFollowLinkToLocation(link, location, originalEvent) {
1127
+ return (this.delegate.willSubmitFormLinkToLocation(link, location, originalEvent) &&
1128
+ link.hasAttribute("data-turbo-method"));
1029
1129
  }
1030
- linkClickIntercepted(link, action) {
1130
+ followedLinkToLocation(link, location) {
1131
+ const action = location.href;
1031
1132
  const form = document.createElement("form");
1032
1133
  form.setAttribute("data-turbo", "true");
1033
1134
  form.setAttribute("action", action);
@@ -1044,7 +1145,7 @@ Copyright © 2022 Basecamp, LLC
1044
1145
  const turboStream = link.hasAttribute("data-turbo-stream");
1045
1146
  if (turboStream)
1046
1147
  form.setAttribute("data-turbo-stream", "");
1047
- this.delegate.formLinkClickIntercepted(link, form);
1148
+ this.delegate.submittedFormLinkToLocation(link, location, form);
1048
1149
  document.body.appendChild(form);
1049
1150
  form.requestSubmit();
1050
1151
  form.remove();
@@ -1128,21 +1229,6 @@ Copyright © 2022 Basecamp, LLC
1128
1229
  delete this.resolvingFunctions;
1129
1230
  }
1130
1231
  }
1131
- createScriptElement(element) {
1132
- if (element.getAttribute("data-turbo-eval") == "false") {
1133
- return element;
1134
- }
1135
- else {
1136
- const createdScriptElement = document.createElement("script");
1137
- if (this.cspNonce) {
1138
- createdScriptElement.nonce = this.cspNonce;
1139
- }
1140
- createdScriptElement.textContent = element.textContent;
1141
- createdScriptElement.async = false;
1142
- copyElementAttributes(createdScriptElement, element);
1143
- return createdScriptElement;
1144
- }
1145
- }
1146
1232
  preservingPermanentElements(callback) {
1147
1233
  Bardo.preservingPermanentElements(this, this.permanentElementMap, callback);
1148
1234
  }
@@ -1177,14 +1263,6 @@ Copyright © 2022 Basecamp, LLC
1177
1263
  get permanentElementMap() {
1178
1264
  return this.currentSnapshot.getPermanentElementMapForSnapshot(this.newSnapshot);
1179
1265
  }
1180
- get cspNonce() {
1181
- return getMetaContent("csp-nonce");
1182
- }
1183
- }
1184
- function copyElementAttributes(destinationElement, sourceElement) {
1185
- for (const { name, value } of [...sourceElement.attributes]) {
1186
- destinationElement.setAttribute(name, value);
1187
- }
1188
1266
  }
1189
1267
  function elementIsFocusable(element) {
1190
1268
  return element && typeof element.focus == "function";
@@ -1222,7 +1300,7 @@ Copyright © 2022 Basecamp, LLC
1222
1300
  this.activateScriptElements();
1223
1301
  }
1224
1302
  loadFrameElement() {
1225
- this.delegate.frameExtracted(this.newElement.cloneNode(true));
1303
+ this.delegate.willRenderFrame(this.currentElement, this.newElement);
1226
1304
  this.renderElement(this.currentElement, this.newElement);
1227
1305
  }
1228
1306
  scrollFrameIntoView() {
@@ -1239,7 +1317,7 @@ Copyright © 2022 Basecamp, LLC
1239
1317
  }
1240
1318
  activateScriptElements() {
1241
1319
  for (const inertScriptElement of this.newScriptElements) {
1242
- const activatedScriptElement = this.createScriptElement(inertScriptElement);
1320
+ const activatedScriptElement = activateScriptElement(inertScriptElement);
1243
1321
  inertScriptElement.replaceWith(activatedScriptElement);
1244
1322
  }
1245
1323
  }
@@ -1525,6 +1603,9 @@ Copyright © 2022 Basecamp, LLC
1525
1603
  historyChanged: false,
1526
1604
  visitCachedSnapshot: () => { },
1527
1605
  willRender: true,
1606
+ updateHistory: true,
1607
+ shouldCacheSnapshot: true,
1608
+ acceptsStreamResponse: false,
1528
1609
  };
1529
1610
  var SystemStatusCode;
1530
1611
  (function (SystemStatusCode) {
@@ -1539,12 +1620,15 @@ Copyright © 2022 Basecamp, LLC
1539
1620
  this.followedRedirect = false;
1540
1621
  this.historyChanged = false;
1541
1622
  this.scrolled = false;
1623
+ this.shouldCacheSnapshot = true;
1624
+ this.acceptsStreamResponse = false;
1542
1625
  this.snapshotCached = false;
1543
1626
  this.state = VisitState.initialized;
1544
1627
  this.delegate = delegate;
1545
1628
  this.location = location;
1546
1629
  this.restorationIdentifier = restorationIdentifier || uuid();
1547
- const { action, historyChanged, referrer, snapshotHTML, response, visitCachedSnapshot, willRender } = Object.assign(Object.assign({}, defaultOptions), options);
1630
+ this.promise = new Promise((resolve, reject) => (this.resolvingFunctions = { resolve, reject }));
1631
+ const { action, historyChanged, referrer, snapshotHTML, response, visitCachedSnapshot, willRender, updateHistory, shouldCacheSnapshot, acceptsStreamResponse, } = Object.assign(Object.assign({}, defaultOptions), options);
1548
1632
  this.action = action;
1549
1633
  this.historyChanged = historyChanged;
1550
1634
  this.referrer = referrer;
@@ -1553,7 +1637,10 @@ Copyright © 2022 Basecamp, LLC
1553
1637
  this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
1554
1638
  this.visitCachedSnapshot = visitCachedSnapshot;
1555
1639
  this.willRender = willRender;
1640
+ this.updateHistory = updateHistory;
1556
1641
  this.scrolled = !willRender;
1642
+ this.shouldCacheSnapshot = shouldCacheSnapshot;
1643
+ this.acceptsStreamResponse = acceptsStreamResponse;
1557
1644
  }
1558
1645
  get adapter() {
1559
1646
  return this.delegate.adapter;
@@ -1585,6 +1672,7 @@ Copyright © 2022 Basecamp, LLC
1585
1672
  }
1586
1673
  this.cancelRender();
1587
1674
  this.state = VisitState.canceled;
1675
+ this.resolvingFunctions.reject();
1588
1676
  }
1589
1677
  }
1590
1678
  complete() {
@@ -1596,19 +1684,21 @@ Copyright © 2022 Basecamp, LLC
1596
1684
  this.adapter.visitCompleted(this);
1597
1685
  this.delegate.visitCompleted(this);
1598
1686
  }
1687
+ this.resolvingFunctions.resolve();
1599
1688
  }
1600
1689
  }
1601
1690
  fail() {
1602
1691
  if (this.state == VisitState.started) {
1603
1692
  this.state = VisitState.failed;
1604
1693
  this.adapter.visitFailed(this);
1694
+ this.resolvingFunctions.reject();
1605
1695
  }
1606
1696
  }
1607
1697
  changeHistory() {
1608
1698
  var _a;
1609
- if (!this.historyChanged) {
1699
+ if (!this.historyChanged && this.updateHistory) {
1610
1700
  const actionForHistory = this.location.href === ((_a = this.referrer) === null || _a === void 0 ? void 0 : _a.href) ? "replace" : this.action;
1611
- const method = this.getHistoryMethodForAction(actionForHistory);
1701
+ const method = getHistoryMethodForAction(actionForHistory);
1612
1702
  this.history.update(method, this.location, this.restorationIdentifier);
1613
1703
  this.historyChanged = true;
1614
1704
  }
@@ -1653,11 +1743,13 @@ Copyright © 2022 Basecamp, LLC
1653
1743
  if (this.response) {
1654
1744
  const { statusCode, responseHTML } = this.response;
1655
1745
  this.render(async () => {
1656
- this.cacheSnapshot();
1746
+ if (this.shouldCacheSnapshot)
1747
+ this.cacheSnapshot();
1657
1748
  if (this.view.renderPromise)
1658
1749
  await this.view.renderPromise;
1659
1750
  if (isSuccessful(statusCode) && responseHTML != null) {
1660
1751
  await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML), false, this.willRender, this);
1752
+ this.performScroll();
1661
1753
  this.adapter.visitRendered(this);
1662
1754
  this.complete();
1663
1755
  }
@@ -1698,6 +1790,7 @@ Copyright © 2022 Basecamp, LLC
1698
1790
  if (this.view.renderPromise)
1699
1791
  await this.view.renderPromise;
1700
1792
  await this.view.renderPage(snapshot, isPreview, this.willRender, this);
1793
+ this.performScroll();
1701
1794
  this.adapter.visitRendered(this);
1702
1795
  if (!isPreview) {
1703
1796
  this.complete();
@@ -1721,10 +1814,16 @@ Copyright © 2022 Basecamp, LLC
1721
1814
  if (this.isSamePage) {
1722
1815
  this.render(async () => {
1723
1816
  this.cacheSnapshot();
1817
+ this.performScroll();
1724
1818
  this.adapter.visitRendered(this);
1725
1819
  });
1726
1820
  }
1727
1821
  }
1822
+ prepareHeadersForRequest(headers, request) {
1823
+ if (this.acceptsStreamResponse) {
1824
+ request.acceptResponseType(StreamMessage.contentType);
1825
+ }
1826
+ }
1728
1827
  requestStarted() {
1729
1828
  this.startRequest();
1730
1829
  }
@@ -1766,7 +1865,7 @@ Copyright © 2022 Basecamp, LLC
1766
1865
  this.finishRequest();
1767
1866
  }
1768
1867
  performScroll() {
1769
- if (!this.scrolled) {
1868
+ if (!this.scrolled && !this.view.forceReloaded) {
1770
1869
  if (this.action == "restore") {
1771
1870
  this.scrollToRestoredPosition() || this.scrollToAnchor() || this.view.scrollToTop();
1772
1871
  }
@@ -1835,9 +1934,6 @@ Copyright © 2022 Basecamp, LLC
1835
1934
  });
1836
1935
  await callback();
1837
1936
  delete this.frame;
1838
- if (!this.view.forceReloaded) {
1839
- this.performScroll();
1840
- }
1841
1937
  }
1842
1938
  cancelRender() {
1843
1939
  if (this.frame) {
@@ -1859,7 +1955,7 @@ Copyright © 2022 Basecamp, LLC
1859
1955
  this.session = session;
1860
1956
  }
1861
1957
  visitProposedToLocation(location, options) {
1862
- this.navigator.startVisit(location, uuid(), options);
1958
+ return this.navigator.startVisit(location, (options === null || options === void 0 ? void 0 : options.restorationIdentifier) || uuid(), options);
1863
1959
  }
1864
1960
  visitStarted(visit) {
1865
1961
  this.location = visit.location;
@@ -1969,84 +2065,39 @@ Copyright © 2022 Basecamp, LLC
1969
2065
  }
1970
2066
  }
1971
2067
 
1972
- class FormSubmitObserver {
1973
- constructor(delegate) {
1974
- this.started = false;
1975
- this.submitCaptured = () => {
1976
- removeEventListener("submit", this.submitBubbled, false);
1977
- addEventListener("submit", this.submitBubbled, false);
1978
- };
1979
- this.submitBubbled = ((event) => {
1980
- if (!event.defaultPrevented) {
1981
- const form = event.target instanceof HTMLFormElement ? event.target : undefined;
1982
- const submitter = event.submitter || undefined;
1983
- if (form &&
1984
- submissionDoesNotDismissDialog(form, submitter) &&
1985
- submissionDoesNotTargetIFrame(form, submitter) &&
1986
- this.delegate.willSubmitForm(form, submitter)) {
1987
- event.preventDefault();
1988
- this.delegate.formSubmitted(form, submitter);
1989
- }
1990
- }
1991
- });
1992
- this.delegate = delegate;
1993
- }
1994
- start() {
1995
- if (!this.started) {
1996
- addEventListener("submit", this.submitCaptured, true);
1997
- this.started = true;
1998
- }
1999
- }
2000
- stop() {
2001
- if (this.started) {
2002
- removeEventListener("submit", this.submitCaptured, true);
2003
- this.started = false;
2004
- }
2005
- }
2006
- }
2007
- function submissionDoesNotDismissDialog(form, submitter) {
2008
- const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.getAttribute("method");
2009
- return method != "dialog";
2010
- }
2011
- function submissionDoesNotTargetIFrame(form, submitter) {
2012
- const target = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formtarget")) || form.target;
2013
- for (const element of document.getElementsByName(target)) {
2014
- if (element instanceof HTMLIFrameElement)
2015
- return false;
2016
- }
2017
- return true;
2018
- }
2019
-
2020
2068
  class FrameRedirector {
2021
- constructor(element) {
2069
+ constructor(session, element) {
2070
+ this.session = session;
2022
2071
  this.element = element;
2023
- this.linkInterceptor = new LinkInterceptor(this, element);
2024
- this.formInterceptor = new FormInterceptor(this, element);
2072
+ this.linkClickObserver = new LinkClickObserver(this, element);
2073
+ this.formSubmitObserver = new FormSubmitObserver(this, element);
2025
2074
  }
2026
2075
  start() {
2027
- this.linkInterceptor.start();
2028
- this.formInterceptor.start();
2076
+ this.linkClickObserver.start();
2077
+ this.formSubmitObserver.start();
2029
2078
  }
2030
2079
  stop() {
2031
- this.linkInterceptor.stop();
2032
- this.formInterceptor.stop();
2080
+ this.linkClickObserver.stop();
2081
+ this.formSubmitObserver.stop();
2033
2082
  }
2034
- shouldInterceptLinkClick(element, _url) {
2083
+ willFollowLinkToLocation(element) {
2035
2084
  return this.shouldRedirect(element);
2036
2085
  }
2037
- linkClickIntercepted(element, url) {
2086
+ followedLinkToLocation(element, url) {
2038
2087
  const frame = this.findFrameElement(element);
2039
2088
  if (frame) {
2040
- frame.delegate.linkClickIntercepted(element, url);
2089
+ frame.delegate.followedLinkToLocation(element, url);
2041
2090
  }
2042
2091
  }
2043
- shouldInterceptFormSubmission(element, submitter) {
2044
- return this.shouldSubmit(element, submitter);
2092
+ willSubmitForm(element, submitter) {
2093
+ return (element.closest("turbo-frame") == null &&
2094
+ this.shouldSubmit(element, submitter) &&
2095
+ this.shouldRedirect(element, submitter));
2045
2096
  }
2046
- formSubmissionIntercepted(element, submitter) {
2097
+ formSubmitted(element, submitter) {
2047
2098
  const frame = this.findFrameElement(element, submitter);
2048
2099
  if (frame) {
2049
- frame.delegate.formSubmissionIntercepted(element, submitter);
2100
+ frame.delegate.formSubmitted(element, submitter);
2050
2101
  }
2051
2102
  }
2052
2103
  shouldSubmit(form, submitter) {
@@ -2057,8 +2108,16 @@ Copyright © 2022 Basecamp, LLC
2057
2108
  return this.shouldRedirect(form, submitter) && locationIsVisitable(action, rootLocation);
2058
2109
  }
2059
2110
  shouldRedirect(element, submitter) {
2060
- const frame = this.findFrameElement(element, submitter);
2061
- return frame ? frame != element.closest("turbo-frame") : false;
2111
+ const isNavigatable = element instanceof HTMLFormElement
2112
+ ? this.session.submissionIsNavigatable(element, submitter)
2113
+ : this.session.elementIsNavigatable(element);
2114
+ if (isNavigatable) {
2115
+ const frame = this.findFrameElement(element, submitter);
2116
+ return frame ? frame != element.closest("turbo-frame") : false;
2117
+ }
2118
+ else {
2119
+ return false;
2120
+ }
2062
2121
  }
2063
2122
  findFrameElement(element, submitter) {
2064
2123
  const id = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("data-turbo-frame")) || element.getAttribute("data-turbo-frame");
@@ -2150,70 +2209,6 @@ Copyright © 2022 Basecamp, LLC
2150
2209
  }
2151
2210
  }
2152
2211
 
2153
- class LinkClickObserver {
2154
- constructor(delegate) {
2155
- this.started = false;
2156
- this.clickCaptured = () => {
2157
- removeEventListener("click", this.clickBubbled, false);
2158
- addEventListener("click", this.clickBubbled, false);
2159
- };
2160
- this.clickBubbled = (event) => {
2161
- if (this.clickEventIsSignificant(event)) {
2162
- const target = (event.composedPath && event.composedPath()[0]) || event.target;
2163
- const link = this.findLinkFromClickTarget(target);
2164
- if (link && doesNotTargetIFrame(link)) {
2165
- const location = this.getLocationForLink(link);
2166
- if (this.delegate.willFollowLinkToLocation(link, location, event)) {
2167
- event.preventDefault();
2168
- this.delegate.followedLinkToLocation(link, location);
2169
- }
2170
- }
2171
- }
2172
- };
2173
- this.delegate = delegate;
2174
- }
2175
- start() {
2176
- if (!this.started) {
2177
- addEventListener("click", this.clickCaptured, true);
2178
- this.started = true;
2179
- }
2180
- }
2181
- stop() {
2182
- if (this.started) {
2183
- removeEventListener("click", this.clickCaptured, true);
2184
- this.started = false;
2185
- }
2186
- }
2187
- clickEventIsSignificant(event) {
2188
- return !((event.target && event.target.isContentEditable) ||
2189
- event.defaultPrevented ||
2190
- event.which > 1 ||
2191
- event.altKey ||
2192
- event.ctrlKey ||
2193
- event.metaKey ||
2194
- event.shiftKey);
2195
- }
2196
- findLinkFromClickTarget(target) {
2197
- if (target instanceof Element) {
2198
- return target.closest("a[href]:not([target^=_]):not([download])");
2199
- }
2200
- }
2201
- getLocationForLink(link) {
2202
- return expandURL(link.getAttribute("href") || "");
2203
- }
2204
- }
2205
- function doesNotTargetIFrame(anchor) {
2206
- for (const element of document.getElementsByName(anchor.target)) {
2207
- if (element instanceof HTMLIFrameElement)
2208
- return false;
2209
- }
2210
- return true;
2211
- }
2212
-
2213
- function isAction(action) {
2214
- return action == "advance" || action == "replace" || action == "restore";
2215
- }
2216
-
2217
2212
  class Navigator {
2218
2213
  constructor(delegate) {
2219
2214
  this.delegate = delegate;
@@ -2221,18 +2216,23 @@ Copyright © 2022 Basecamp, LLC
2221
2216
  proposeVisit(location, options = {}) {
2222
2217
  if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
2223
2218
  if (locationIsVisitable(location, this.view.snapshot.rootLocation)) {
2224
- this.delegate.visitProposedToLocation(location, options);
2219
+ return this.delegate.visitProposedToLocation(location, options);
2225
2220
  }
2226
2221
  else {
2227
2222
  window.location.href = location.toString();
2223
+ return Promise.resolve();
2228
2224
  }
2229
2225
  }
2226
+ else {
2227
+ return Promise.reject();
2228
+ }
2230
2229
  }
2231
2230
  startVisit(locatable, restorationIdentifier, options = {}) {
2232
2231
  this.lastVisit = this.currentVisit;
2233
2232
  this.stop();
2234
2233
  this.currentVisit = new Visit(this, expandURL(locatable), restorationIdentifier, Object.assign({ referrer: this.location }, options));
2235
2234
  this.currentVisit.start();
2235
+ return this.currentVisit.promise;
2236
2236
  }
2237
2237
  submitForm(form, submitter) {
2238
2238
  this.stop();
@@ -2267,13 +2267,15 @@ Copyright © 2022 Basecamp, LLC
2267
2267
  if (formSubmission == this.formSubmission) {
2268
2268
  const responseHTML = await fetchResponse.responseHTML;
2269
2269
  if (responseHTML) {
2270
- if (formSubmission.method != FetchMethod.get) {
2270
+ const shouldCacheSnapshot = formSubmission.method == FetchMethod.get;
2271
+ if (!shouldCacheSnapshot) {
2271
2272
  this.view.clearSnapshotCache();
2272
2273
  }
2273
2274
  const { statusCode, redirected } = fetchResponse;
2274
2275
  const action = this.getActionForFormSubmission(formSubmission);
2275
2276
  const visitOptions = {
2276
2277
  action,
2278
+ shouldCacheSnapshot,
2277
2279
  response: { statusCode, responseHTML, redirected },
2278
2280
  };
2279
2281
  this.proposeVisit(fetchResponse.location, visitOptions);
@@ -2472,7 +2474,7 @@ Copyright © 2022 Basecamp, LLC
2472
2474
  }
2473
2475
  }
2474
2476
  receiveMessageHTML(html) {
2475
- this.delegate.receivedMessageFromStream(new StreamMessage(html));
2477
+ this.delegate.receivedMessageFromStream(StreamMessage.wrap(html));
2476
2478
  }
2477
2479
  }
2478
2480
  function fetchResponseFromEvent(event) {
@@ -2506,7 +2508,7 @@ Copyright © 2022 Basecamp, LLC
2506
2508
  for (const replaceableElement of this.scriptElements) {
2507
2509
  const parentNode = replaceableElement.parentNode;
2508
2510
  if (parentNode) {
2509
- const element = this.createScriptElement(replaceableElement);
2511
+ const element = activateScriptElement(replaceableElement);
2510
2512
  parentNode.replaceChild(element, replaceableElement);
2511
2513
  }
2512
2514
  }
@@ -2515,7 +2517,7 @@ Copyright © 2022 Basecamp, LLC
2515
2517
  return this.newSnapshot.headSnapshot.element;
2516
2518
  }
2517
2519
  get scriptElements() {
2518
- return [...document.documentElement.querySelectorAll("script")];
2520
+ return document.documentElement.querySelectorAll("script");
2519
2521
  }
2520
2522
  }
2521
2523
 
@@ -2543,8 +2545,8 @@ Copyright © 2022 Basecamp, LLC
2543
2545
  };
2544
2546
  }
2545
2547
  }
2546
- prepareToRender() {
2547
- this.mergeHead();
2548
+ async prepareToRender() {
2549
+ await this.mergeHead();
2548
2550
  }
2549
2551
  async render() {
2550
2552
  if (this.willRender) {
@@ -2566,11 +2568,12 @@ Copyright © 2022 Basecamp, LLC
2566
2568
  get newElement() {
2567
2569
  return this.newSnapshot.element;
2568
2570
  }
2569
- mergeHead() {
2570
- this.copyNewHeadStylesheetElements();
2571
+ async mergeHead() {
2572
+ const newStylesheetElements = this.copyNewHeadStylesheetElements();
2571
2573
  this.copyNewHeadScriptElements();
2572
2574
  this.removeCurrentHeadProvisionalElements();
2573
2575
  this.copyNewHeadProvisionalElements();
2576
+ await newStylesheetElements;
2574
2577
  }
2575
2578
  replaceBody() {
2576
2579
  this.preservingPermanentElements(() => {
@@ -2581,14 +2584,17 @@ Copyright © 2022 Basecamp, LLC
2581
2584
  get trackedElementsAreIdentical() {
2582
2585
  return this.currentHeadSnapshot.trackedElementSignature == this.newHeadSnapshot.trackedElementSignature;
2583
2586
  }
2584
- copyNewHeadStylesheetElements() {
2587
+ async copyNewHeadStylesheetElements() {
2588
+ const loadingElements = [];
2585
2589
  for (const element of this.newHeadStylesheetElements) {
2590
+ loadingElements.push(waitForLoad(element));
2586
2591
  document.head.appendChild(element);
2587
2592
  }
2593
+ await Promise.all(loadingElements);
2588
2594
  }
2589
2595
  copyNewHeadScriptElements() {
2590
2596
  for (const element of this.newHeadScriptElements) {
2591
- document.head.appendChild(this.createScriptElement(element));
2597
+ document.head.appendChild(activateScriptElement(element));
2592
2598
  }
2593
2599
  }
2594
2600
  removeCurrentHeadProvisionalElements() {
@@ -2607,7 +2613,7 @@ Copyright © 2022 Basecamp, LLC
2607
2613
  }
2608
2614
  activateNewBodyScriptElements() {
2609
2615
  for (const inertScriptElement of this.newBodyScriptElements) {
2610
- const activatedScriptElement = this.createScriptElement(inertScriptElement);
2616
+ const activatedScriptElement = activateScriptElement(inertScriptElement);
2611
2617
  inertScriptElement.replaceWith(activatedScriptElement);
2612
2618
  }
2613
2619
  }
@@ -2770,12 +2776,12 @@ Copyright © 2022 Basecamp, LLC
2770
2776
  this.adapter = new BrowserAdapter(this);
2771
2777
  this.pageObserver = new PageObserver(this);
2772
2778
  this.cacheObserver = new CacheObserver();
2773
- this.linkClickObserver = new LinkClickObserver(this);
2774
- this.formSubmitObserver = new FormSubmitObserver(this);
2779
+ this.linkClickObserver = new LinkClickObserver(this, window);
2780
+ this.formSubmitObserver = new FormSubmitObserver(this, document);
2775
2781
  this.scrollObserver = new ScrollObserver(this);
2776
2782
  this.streamObserver = new StreamObserver(this);
2777
- this.formLinkInterceptor = new FormLinkInterceptor(this, document.documentElement);
2778
- this.frameRedirector = new FrameRedirector(document.documentElement);
2783
+ this.formLinkClickObserver = new FormLinkClickObserver(this, document.documentElement);
2784
+ this.frameRedirector = new FrameRedirector(this, document.documentElement);
2779
2785
  this.drive = true;
2780
2786
  this.enabled = true;
2781
2787
  this.progressBarDelay = 500;
@@ -2786,7 +2792,7 @@ Copyright © 2022 Basecamp, LLC
2786
2792
  if (!this.started) {
2787
2793
  this.pageObserver.start();
2788
2794
  this.cacheObserver.start();
2789
- this.formLinkInterceptor.start();
2795
+ this.formLinkClickObserver.start();
2790
2796
  this.linkClickObserver.start();
2791
2797
  this.formSubmitObserver.start();
2792
2798
  this.scrollObserver.start();
@@ -2805,7 +2811,7 @@ Copyright © 2022 Basecamp, LLC
2805
2811
  if (this.started) {
2806
2812
  this.pageObserver.stop();
2807
2813
  this.cacheObserver.stop();
2808
- this.formLinkInterceptor.stop();
2814
+ this.formLinkClickObserver.stop();
2809
2815
  this.linkClickObserver.stop();
2810
2816
  this.formSubmitObserver.stop();
2811
2817
  this.scrollObserver.stop();
@@ -2819,7 +2825,14 @@ Copyright © 2022 Basecamp, LLC
2819
2825
  this.adapter = adapter;
2820
2826
  }
2821
2827
  visit(location, options = {}) {
2822
- this.navigator.proposeVisit(expandURL(location), options);
2828
+ const frameElement = document.getElementById(options.frame || "");
2829
+ if (frameElement instanceof FrameElement) {
2830
+ frameElement.src = location.toString();
2831
+ return frameElement.loaded;
2832
+ }
2833
+ else {
2834
+ return this.navigator.proposeVisit(expandURL(location), options);
2835
+ }
2823
2836
  }
2824
2837
  connectStreamSource(source) {
2825
2838
  this.streamObserver.connectStreamSource(source);
@@ -2861,25 +2874,26 @@ Copyright © 2022 Basecamp, LLC
2861
2874
  scrollPositionChanged(position) {
2862
2875
  this.history.updateRestorationData({ scrollPosition: position });
2863
2876
  }
2864
- shouldInterceptFormLinkClick(_link) {
2865
- return true;
2877
+ willSubmitFormLinkToLocation(link, location) {
2878
+ return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation);
2866
2879
  }
2867
- formLinkClickIntercepted(_link, _form) { }
2880
+ submittedFormLinkToLocation() { }
2868
2881
  willFollowLinkToLocation(link, location, event) {
2869
- return (this.elementDriveEnabled(link) &&
2882
+ return (this.elementIsNavigatable(link) &&
2870
2883
  locationIsVisitable(location, this.snapshot.rootLocation) &&
2871
2884
  this.applicationAllowsFollowingLinkToLocation(link, location, event));
2872
2885
  }
2873
2886
  followedLinkToLocation(link, location) {
2874
2887
  const action = this.getActionForLink(link);
2875
- this.visit(location.href, { action });
2888
+ const acceptsStreamResponse = link.hasAttribute("data-turbo-stream");
2889
+ this.visit(location.href, { action, acceptsStreamResponse });
2876
2890
  }
2877
2891
  allowsVisitingLocationWithAction(location, action) {
2878
2892
  return this.locationWithActionIsSamePage(location, action) || this.applicationAllowsVisitingLocation(location);
2879
2893
  }
2880
2894
  visitProposedToLocation(location, options) {
2881
2895
  extendURLWithDeprecatedProperties(location);
2882
- this.adapter.visitProposedToLocation(location, options);
2896
+ return this.adapter.visitProposedToLocation(location, options);
2883
2897
  }
2884
2898
  visitStarted(visit) {
2885
2899
  extendURLWithDeprecatedProperties(visit.location);
@@ -2898,8 +2912,7 @@ Copyright © 2022 Basecamp, LLC
2898
2912
  }
2899
2913
  willSubmitForm(form, submitter) {
2900
2914
  const action = getAction(form, submitter);
2901
- return (this.elementDriveEnabled(form) &&
2902
- (!submitter || this.formElementDriveEnabled(submitter)) &&
2915
+ return (this.submissionIsNavigatable(form, submitter) &&
2903
2916
  locationIsVisitable(expandURL(action), this.snapshot.rootLocation));
2904
2917
  }
2905
2918
  formSubmitted(form, submitter) {
@@ -2948,6 +2961,10 @@ Copyright © 2022 Basecamp, LLC
2948
2961
  frameRendered(fetchResponse, frame) {
2949
2962
  this.notifyApplicationAfterFrameRender(fetchResponse, frame);
2950
2963
  }
2964
+ frameMissing(frame, fetchResponse) {
2965
+ console.warn(`Completing full-page visit as matching frame for #${frame.id} was missing from the response`);
2966
+ return this.visit(fetchResponse.location);
2967
+ }
2951
2968
  applicationAllowsFollowingLinkToLocation(link, location, ev) {
2952
2969
  const event = this.notifyApplicationAfterClickingLinkToLocation(link, location, ev);
2953
2970
  return !event.defaultPrevented;
@@ -3007,19 +3024,24 @@ Copyright © 2022 Basecamp, LLC
3007
3024
  cancelable: true,
3008
3025
  });
3009
3026
  }
3010
- formElementDriveEnabled(element) {
3027
+ submissionIsNavigatable(form, submitter) {
3011
3028
  if (this.formMode == "off") {
3012
3029
  return false;
3013
3030
  }
3014
- if (this.formMode == "optin") {
3015
- const form = element === null || element === void 0 ? void 0 : element.closest("form[data-turbo]");
3016
- return (form === null || form === void 0 ? void 0 : form.getAttribute("data-turbo")) == "true";
3031
+ else {
3032
+ const submitterIsNavigatable = submitter ? this.elementIsNavigatable(submitter) : true;
3033
+ if (this.formMode == "optin") {
3034
+ return submitterIsNavigatable && form.closest('[data-turbo="true"]') != null;
3035
+ }
3036
+ else {
3037
+ return submitterIsNavigatable && this.elementIsNavigatable(form);
3038
+ }
3017
3039
  }
3018
- return this.elementDriveEnabled(element);
3019
3040
  }
3020
- elementDriveEnabled(element) {
3021
- const container = element === null || element === void 0 ? void 0 : element.closest("[data-turbo]");
3022
- if (this.drive) {
3041
+ elementIsNavigatable(element) {
3042
+ const container = element.closest("[data-turbo]");
3043
+ const withinFrame = element.closest("turbo-frame");
3044
+ if (this.drive || withinFrame) {
3023
3045
  if (container) {
3024
3046
  return container.getAttribute("data-turbo") != "false";
3025
3047
  }
@@ -3112,7 +3134,7 @@ Copyright © 2022 Basecamp, LLC
3112
3134
  session.registerAdapter(adapter);
3113
3135
  }
3114
3136
  function visit(location, options) {
3115
- session.visit(location, options);
3137
+ return session.visit(location, options);
3116
3138
  }
3117
3139
  function connectStreamSource(source) {
3118
3140
  session.connectStreamSource(source);
@@ -3166,6 +3188,7 @@ Copyright © 2022 Basecamp, LLC
3166
3188
  this.connected = false;
3167
3189
  this.hasBeenLoaded = false;
3168
3190
  this.ignoredAttributes = new Set();
3191
+ this.action = null;
3169
3192
  this.visitCachedSnapshot = ({ element }) => {
3170
3193
  const frame = element.querySelector("#" + this.element.id);
3171
3194
  if (frame && this.previousFrameElement) {
@@ -3176,9 +3199,10 @@ Copyright © 2022 Basecamp, LLC
3176
3199
  this.element = element;
3177
3200
  this.view = new FrameView(this, this.element);
3178
3201
  this.appearanceObserver = new AppearanceObserver(this, this.element);
3179
- this.formLinkInterceptor = new FormLinkInterceptor(this, this.element);
3180
- this.linkInterceptor = new LinkInterceptor(this, this.element);
3181
- this.formInterceptor = new FormInterceptor(this, this.element);
3202
+ this.formLinkClickObserver = new FormLinkClickObserver(this, this.element);
3203
+ this.linkClickObserver = new LinkClickObserver(this, this.element);
3204
+ this.restorationIdentifier = uuid();
3205
+ this.formSubmitObserver = new FormSubmitObserver(this, this.element);
3182
3206
  }
3183
3207
  connect() {
3184
3208
  if (!this.connected) {
@@ -3189,18 +3213,18 @@ Copyright © 2022 Basecamp, LLC
3189
3213
  else {
3190
3214
  this.loadSourceURL();
3191
3215
  }
3192
- this.formLinkInterceptor.start();
3193
- this.linkInterceptor.start();
3194
- this.formInterceptor.start();
3216
+ this.formLinkClickObserver.start();
3217
+ this.linkClickObserver.start();
3218
+ this.formSubmitObserver.start();
3195
3219
  }
3196
3220
  }
3197
3221
  disconnect() {
3198
3222
  if (this.connected) {
3199
3223
  this.connected = false;
3200
3224
  this.appearanceObserver.stop();
3201
- this.formLinkInterceptor.stop();
3202
- this.linkInterceptor.stop();
3203
- this.formInterceptor.stop();
3225
+ this.formLinkClickObserver.stop();
3226
+ this.linkClickObserver.stop();
3227
+ this.formSubmitObserver.stop();
3204
3228
  }
3205
3229
  }
3206
3230
  disabledChanged() {
@@ -3248,15 +3272,22 @@ Copyright © 2022 Basecamp, LLC
3248
3272
  const html = await fetchResponse.responseHTML;
3249
3273
  if (html) {
3250
3274
  const { body } = parseHTMLDocument(html);
3251
- const snapshot = new Snapshot(await this.extractForeignFrameElement(body));
3252
- const renderer = new FrameRenderer(this, this.view.snapshot, snapshot, FrameRenderer.renderElement, false, false);
3253
- if (this.view.renderPromise)
3254
- await this.view.renderPromise;
3255
- await this.view.render(renderer);
3256
- this.complete = true;
3257
- session.frameRendered(fetchResponse, this.element);
3258
- session.frameLoaded(this.element);
3259
- this.fetchResponseLoaded(fetchResponse);
3275
+ const newFrameElement = await this.extractForeignFrameElement(body);
3276
+ if (newFrameElement) {
3277
+ const snapshot = new Snapshot(newFrameElement);
3278
+ const renderer = new FrameRenderer(this, this.view.snapshot, snapshot, FrameRenderer.renderElement, false, false);
3279
+ if (this.view.renderPromise)
3280
+ await this.view.renderPromise;
3281
+ this.changeHistory();
3282
+ await this.view.render(renderer);
3283
+ this.complete = true;
3284
+ session.frameRendered(fetchResponse, this.element);
3285
+ session.frameLoaded(this.element);
3286
+ this.fetchResponseLoaded(fetchResponse);
3287
+ }
3288
+ else if (this.sessionWillHandleMissingFrame(fetchResponse)) {
3289
+ await session.frameMissing(this.element, fetchResponse);
3290
+ }
3260
3291
  }
3261
3292
  }
3262
3293
  catch (error) {
@@ -3270,24 +3301,24 @@ Copyright © 2022 Basecamp, LLC
3270
3301
  elementAppearedInViewport(_element) {
3271
3302
  this.loadSourceURL();
3272
3303
  }
3273
- shouldInterceptFormLinkClick(link) {
3274
- return this.shouldInterceptNavigation(link);
3304
+ willSubmitFormLinkToLocation(link) {
3305
+ return link.closest("turbo-frame") == this.element && this.shouldInterceptNavigation(link);
3275
3306
  }
3276
- formLinkClickIntercepted(link, form) {
3307
+ submittedFormLinkToLocation(link, _location, form) {
3277
3308
  const frame = this.findFrameElement(link);
3278
3309
  if (frame)
3279
3310
  form.setAttribute("data-turbo-frame", frame.id);
3280
3311
  }
3281
- shouldInterceptLinkClick(element, _url) {
3312
+ willFollowLinkToLocation(element) {
3282
3313
  return this.shouldInterceptNavigation(element);
3283
3314
  }
3284
- linkClickIntercepted(element, url) {
3285
- this.navigateFrame(element, url);
3315
+ followedLinkToLocation(element, location) {
3316
+ this.navigateFrame(element, location.href);
3286
3317
  }
3287
- shouldInterceptFormSubmission(element, submitter) {
3288
- return this.shouldInterceptNavigation(element, submitter);
3318
+ willSubmitForm(element, submitter) {
3319
+ return element.closest("turbo-frame") == this.element && this.shouldInterceptNavigation(element, submitter);
3289
3320
  }
3290
- formSubmissionIntercepted(element, submitter) {
3321
+ formSubmitted(element, submitter) {
3291
3322
  if (this.formSubmission) {
3292
3323
  this.formSubmission.stop();
3293
3324
  }
@@ -3296,8 +3327,12 @@ Copyright © 2022 Basecamp, LLC
3296
3327
  this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
3297
3328
  this.formSubmission.start();
3298
3329
  }
3299
- prepareHeadersForRequest(headers, _request) {
3330
+ prepareHeadersForRequest(headers, request) {
3331
+ var _a;
3300
3332
  headers["Turbo-Frame"] = this.id;
3333
+ if ((_a = this.currentNavigationElement) === null || _a === void 0 ? void 0 : _a.hasAttribute("data-turbo-stream")) {
3334
+ request.acceptResponseType(StreamMessage.contentType);
3335
+ }
3301
3336
  }
3302
3337
  requestStarted(_request) {
3303
3338
  markAsBusy(this.element);
@@ -3315,6 +3350,10 @@ Copyright © 2022 Basecamp, LLC
3315
3350
  }
3316
3351
  requestErrored(request, error) {
3317
3352
  console.error(error);
3353
+ dispatch("turbo:fetch-request-error", {
3354
+ target: this.element,
3355
+ detail: { request, error },
3356
+ });
3318
3357
  this.resolveVisitPromise();
3319
3358
  }
3320
3359
  requestFinished(_request) {
@@ -3354,8 +3393,8 @@ Copyright © 2022 Basecamp, LLC
3354
3393
  session.preloadOnLoadLinksForView(element);
3355
3394
  }
3356
3395
  viewInvalidated() { }
3357
- frameExtracted(element) {
3358
- this.previousFrameElement = element;
3396
+ willRenderFrame(currentElement, _newElement) {
3397
+ this.previousFrameElement = currentElement.cloneNode(true);
3359
3398
  }
3360
3399
  async visit(url) {
3361
3400
  var _a;
@@ -3374,27 +3413,49 @@ Copyright © 2022 Basecamp, LLC
3374
3413
  navigateFrame(element, url, submitter) {
3375
3414
  const frame = this.findFrameElement(element, submitter);
3376
3415
  this.proposeVisitIfNavigatedWithAction(frame, element, submitter);
3377
- frame.src = url;
3416
+ this.withCurrentNavigationElement(element, () => {
3417
+ frame.src = url;
3418
+ });
3378
3419
  }
3379
3420
  proposeVisitIfNavigatedWithAction(frame, element, submitter) {
3380
- const action = getAttribute("data-turbo-action", submitter, element, frame);
3381
- if (isAction(action)) {
3421
+ this.action = getVisitAction(submitter, element, frame);
3422
+ this.frame = frame;
3423
+ if (isAction(this.action)) {
3382
3424
  const { visitCachedSnapshot } = frame.delegate;
3383
3425
  frame.delegate.fetchResponseLoaded = (fetchResponse) => {
3384
3426
  if (frame.src) {
3385
3427
  const { statusCode, redirected } = fetchResponse;
3386
3428
  const responseHTML = frame.ownerDocument.documentElement.outerHTML;
3387
3429
  const response = { statusCode, redirected, responseHTML };
3388
- session.visit(frame.src, {
3389
- action,
3430
+ const options = {
3390
3431
  response,
3391
3432
  visitCachedSnapshot,
3392
3433
  willRender: false,
3393
- });
3434
+ updateHistory: false,
3435
+ restorationIdentifier: this.restorationIdentifier,
3436
+ };
3437
+ if (this.action)
3438
+ options.action = this.action;
3439
+ session.visit(frame.src, options);
3394
3440
  }
3395
3441
  };
3396
3442
  }
3397
3443
  }
3444
+ changeHistory() {
3445
+ if (this.action && this.frame) {
3446
+ const method = getHistoryMethodForAction(this.action);
3447
+ session.history.update(method, expandURL(this.frame.src || ""), this.restorationIdentifier);
3448
+ }
3449
+ }
3450
+ sessionWillHandleMissingFrame(fetchResponse) {
3451
+ this.element.setAttribute("complete", "");
3452
+ const event = dispatch("turbo:frame-missing", {
3453
+ target: this.element,
3454
+ detail: { fetchResponse },
3455
+ cancelable: true,
3456
+ });
3457
+ return !event.defaultPrevented;
3458
+ }
3398
3459
  findFrameElement(element, submitter) {
3399
3460
  var _a;
3400
3461
  const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
@@ -3413,12 +3474,12 @@ Copyright © 2022 Basecamp, LLC
3413
3474
  await element.loaded;
3414
3475
  return await this.extractForeignFrameElement(element);
3415
3476
  }
3416
- console.error(`Response has no matching <turbo-frame id="${id}"> element`);
3417
3477
  }
3418
3478
  catch (error) {
3419
3479
  console.error(error);
3480
+ return new FrameElement();
3420
3481
  }
3421
- return new FrameElement();
3482
+ return null;
3422
3483
  }
3423
3484
  formActionIsVisitable(form, submitter) {
3424
3485
  const action = getAction(form, submitter);
@@ -3438,10 +3499,10 @@ Copyright © 2022 Basecamp, LLC
3438
3499
  return !frameElement.disabled;
3439
3500
  }
3440
3501
  }
3441
- if (!session.elementDriveEnabled(element)) {
3502
+ if (!session.elementIsNavigatable(element)) {
3442
3503
  return false;
3443
3504
  }
3444
- if (submitter && !session.elementDriveEnabled(submitter)) {
3505
+ if (submitter && !session.elementIsNavigatable(submitter)) {
3445
3506
  return false;
3446
3507
  }
3447
3508
  return true;
@@ -3498,6 +3559,11 @@ Copyright © 2022 Basecamp, LLC
3498
3559
  callback();
3499
3560
  this.ignoredAttributes.delete(attributeName);
3500
3561
  }
3562
+ withCurrentNavigationElement(element, callback) {
3563
+ this.currentNavigationElement = element;
3564
+ callback();
3565
+ delete this.currentNavigationElement;
3566
+ }
3501
3567
  }
3502
3568
  function getFrameElementById(id) {
3503
3569
  if (id != null) {
@@ -3610,6 +3676,7 @@ Copyright © 2022 Basecamp, LLC
3610
3676
  return new CustomEvent("turbo:before-stream-render", {
3611
3677
  bubbles: true,
3612
3678
  cancelable: true,
3679
+ detail: { newStream: this },
3613
3680
  });
3614
3681
  }
3615
3682
  get targetElementsById() {