@hotwired/turbo 7.2.4 → 7.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/turbo.es2017-esm.js +238 -128
  2. package/dist/turbo.es2017-umd.js +238 -128
  3. package/dist/types/core/bardo.d.ts +1 -1
  4. package/dist/types/core/drive/form_submission.d.ts +10 -6
  5. package/dist/types/core/drive/head_snapshot.d.ts +3 -3
  6. package/dist/types/core/drive/history.d.ts +3 -3
  7. package/dist/types/core/drive/navigator.d.ts +2 -2
  8. package/dist/types/core/drive/page_renderer.d.ts +4 -2
  9. package/dist/types/core/drive/page_view.d.ts +2 -2
  10. package/dist/types/core/drive/visit.d.ts +5 -5
  11. package/dist/types/core/errors.d.ts +2 -0
  12. package/dist/types/core/frames/frame_controller.d.ts +11 -9
  13. package/dist/types/core/frames/frame_view.d.ts +2 -2
  14. package/dist/types/core/index.d.ts +5 -4
  15. package/dist/types/core/native/browser_adapter.d.ts +1 -1
  16. package/dist/types/core/renderer.d.ts +2 -2
  17. package/dist/types/core/session.d.ts +12 -12
  18. package/dist/types/core/snapshot.d.ts +1 -1
  19. package/dist/types/core/streams/stream_actions.d.ts +2 -2
  20. package/dist/types/core/types.d.ts +3 -4
  21. package/dist/types/core/url.d.ts +1 -1
  22. package/dist/types/core/view.d.ts +1 -1
  23. package/dist/types/elements/frame_element.d.ts +1 -1
  24. package/dist/types/elements/stream_element.d.ts +2 -2
  25. package/dist/types/http/fetch_request.d.ts +8 -8
  26. package/dist/types/http/index.d.ts +1 -1
  27. package/dist/types/observers/appearance_observer.d.ts +6 -6
  28. package/dist/types/observers/cache_observer.d.ts +5 -1
  29. package/dist/types/observers/form_link_click_observer.d.ts +1 -1
  30. package/dist/types/observers/link_click_observer.d.ts +1 -1
  31. package/dist/types/polyfills/custom-elements-native-shim.d.ts +1 -0
  32. package/dist/types/tests/functional/frame_tests.d.ts +7 -0
  33. package/dist/types/tests/helpers/dom_test_case.d.ts +1 -2
  34. package/dist/types/tests/helpers/page.d.ts +15 -8
  35. package/dist/types/tests/unit/deprecated_adapter_support_tests.d.ts +1 -0
  36. package/dist/types/tests/unit/export_tests.d.ts +1 -5
  37. package/dist/types/tests/unit/stream_element_tests.d.ts +0 -10
  38. package/dist/types/util.d.ts +3 -1
  39. package/package.json +17 -11
  40. package/dist/types/tests/helpers/intern_test_case.d.ts +0 -20
  41. package/dist/types/tests/unit/deprecated_adapter_support_test.d.ts +0 -24
  42. package/dist/types/tests/unit/index.d.ts +0 -3
  43. /package/dist/types/tests/{functional → integration}/ujs_tests.d.ts +0 -0
@@ -1,6 +1,6 @@
1
1
  /*
2
- Turbo 7.2.4
3
- Copyright © 2022 37signals LLC
2
+ Turbo 7.3.0
3
+ Copyright © 2023 37signals LLC
4
4
  */
5
5
  (function () {
6
6
  if (window.Reflect === undefined ||
@@ -87,16 +87,13 @@ function clickCaptured(event) {
87
87
  (function () {
88
88
  if ("submitter" in Event.prototype)
89
89
  return;
90
- let prototype;
90
+ let prototype = window.Event.prototype;
91
91
  if ("SubmitEvent" in window && /Apple Computer/.test(navigator.vendor)) {
92
92
  prototype = window.SubmitEvent.prototype;
93
93
  }
94
94
  else if ("SubmitEvent" in window) {
95
95
  return;
96
96
  }
97
- else {
98
- prototype = window.Event.prototype;
99
- }
100
97
  addEventListener("click", clickCaptured, true);
101
98
  Object.defineProperty(prototype, "submitter", {
102
99
  get() {
@@ -113,14 +110,14 @@ var FrameLoadingStyle;
113
110
  FrameLoadingStyle["lazy"] = "lazy";
114
111
  })(FrameLoadingStyle || (FrameLoadingStyle = {}));
115
112
  class FrameElement extends HTMLElement {
113
+ static get observedAttributes() {
114
+ return ["disabled", "complete", "loading", "src"];
115
+ }
116
116
  constructor() {
117
117
  super();
118
118
  this.loaded = Promise.resolve();
119
119
  this.delegate = new FrameElement.delegateConstructor(this);
120
120
  }
121
- static get observedAttributes() {
122
- return ["disabled", "complete", "loading", "src"];
123
- }
124
121
  connectedCallback() {
125
122
  this.delegate.connect();
126
123
  }
@@ -307,10 +304,6 @@ class FetchResponse {
307
304
  }
308
305
  }
309
306
 
310
- function isAction(action) {
311
- return action == "advance" || action == "replace" || action == "restore";
312
- }
313
-
314
307
  function activateScriptElement(element) {
315
308
  if (element.getAttribute("data-turbo-eval") == "false") {
316
309
  return element;
@@ -341,6 +334,7 @@ function dispatch(eventName, { target, cancelable, detail } = {}) {
341
334
  const event = new CustomEvent(eventName, {
342
335
  cancelable,
343
336
  bubbles: true,
337
+ composed: true,
344
338
  detail,
345
339
  });
346
340
  if (target && target.isConnected) {
@@ -440,6 +434,9 @@ function getHistoryMethodForAction(action) {
440
434
  return history.pushState;
441
435
  }
442
436
  }
437
+ function isAction(action) {
438
+ return action == "advance" || action == "replace" || action == "restore";
439
+ }
443
440
  function getVisitAction(...elements) {
444
441
  const action = getAttribute("data-turbo-action", ...elements);
445
442
  return isAction(action) ? action : null;
@@ -461,6 +458,13 @@ function setMetaContent(name, content) {
461
458
  element.setAttribute("content", content);
462
459
  return element;
463
460
  }
461
+ function findClosestRecursively(element, selector) {
462
+ var _a;
463
+ if (element instanceof Element) {
464
+ return (element.closest(selector) ||
465
+ findClosestRecursively(element.assignedSlot || ((_a = element.getRootNode()) === null || _a === void 0 ? void 0 : _a.host), selector));
466
+ }
467
+ }
464
468
 
465
469
  var FetchMethod;
466
470
  (function (FetchMethod) {
@@ -508,9 +512,8 @@ class FetchRequest {
508
512
  this.abortController.abort();
509
513
  }
510
514
  async perform() {
511
- var _a, _b;
512
515
  const { fetchOptions } = this;
513
- (_b = (_a = this.delegate).prepareHeadersForRequest) === null || _b === void 0 ? void 0 : _b.call(_a, this.headers, this);
516
+ this.delegate.prepareRequest(this);
514
517
  await this.allowRequestToBeIntercepted(fetchOptions);
515
518
  try {
516
519
  this.delegate.requestStarted(this);
@@ -554,7 +557,7 @@ class FetchRequest {
554
557
  credentials: "same-origin",
555
558
  headers: this.headers,
556
559
  redirect: "follow",
557
- body: this.isIdempotent ? null : this.body,
560
+ body: this.isSafe ? null : this.body,
558
561
  signal: this.abortSignal,
559
562
  referrer: (_a = this.delegate.referrer) === null || _a === void 0 ? void 0 : _a.href,
560
563
  };
@@ -564,8 +567,8 @@ class FetchRequest {
564
567
  Accept: "text/html, application/xhtml+xml",
565
568
  };
566
569
  }
567
- get isIdempotent() {
568
- return this.method == FetchMethod.get;
570
+ get isSafe() {
571
+ return this.method === FetchMethod.get;
569
572
  }
570
573
  get abortSignal() {
571
574
  return this.abortController.signal;
@@ -625,9 +628,6 @@ class AppearanceObserver {
625
628
  }
626
629
 
627
630
  class StreamMessage {
628
- constructor(fragment) {
629
- this.fragment = importStreamElements(fragment);
630
- }
631
631
  static wrap(message) {
632
632
  if (typeof message == "string") {
633
633
  return new this(createDocumentFragment(message));
@@ -636,6 +636,9 @@ class StreamMessage {
636
636
  return message;
637
637
  }
638
638
  }
639
+ constructor(fragment) {
640
+ this.fragment = importStreamElements(fragment);
641
+ }
639
642
  }
640
643
  StreamMessage.contentType = "text/vnd.turbo-stream.html";
641
644
  function importStreamElements(fragment) {
@@ -675,6 +678,9 @@ function formEnctypeFromString(encoding) {
675
678
  }
676
679
  }
677
680
  class FormSubmission {
681
+ static confirmMethod(message, _element, _submitter) {
682
+ return Promise.resolve(confirm(message));
683
+ }
678
684
  constructor(delegate, formElement, submitter, mustRedirect = false) {
679
685
  this.state = FormSubmissionState.initialized;
680
686
  this.delegate = delegate;
@@ -688,9 +694,6 @@ class FormSubmission {
688
694
  this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body, this.formElement);
689
695
  this.mustRedirect = mustRedirect;
690
696
  }
691
- static confirmMethod(message, _element, _submitter) {
692
- return Promise.resolve(confirm(message));
693
- }
694
697
  get method() {
695
698
  var _a;
696
699
  const method = ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formmethod")) || this.formElement.getAttribute("method") || "";
@@ -718,8 +721,8 @@ class FormSubmission {
718
721
  var _a;
719
722
  return formEnctypeFromString(((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formenctype")) || this.formElement.enctype);
720
723
  }
721
- get isIdempotent() {
722
- return this.fetchRequest.isIdempotent;
724
+ get isSafe() {
725
+ return this.fetchRequest.isSafe;
723
726
  }
724
727
  get stringFormData() {
725
728
  return [...this.formData].reduce((entries, [name, value]) => {
@@ -748,11 +751,11 @@ class FormSubmission {
748
751
  return true;
749
752
  }
750
753
  }
751
- prepareHeadersForRequest(headers, request) {
752
- if (!request.isIdempotent) {
754
+ prepareRequest(request) {
755
+ if (!request.isSafe) {
753
756
  const token = getCookieValue(getMetaContent("csrf-param")) || getMetaContent("csrf-token");
754
757
  if (token) {
755
- headers["X-CSRF-Token"] = token;
758
+ request.headers["X-CSRF-Token"] = token;
756
759
  }
757
760
  }
758
761
  if (this.requestAcceptsTurboStreamResponse(request)) {
@@ -763,6 +766,7 @@ class FormSubmission {
763
766
  var _a;
764
767
  this.state = FormSubmissionState.waiting;
765
768
  (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.setAttribute("disabled", "");
769
+ this.setSubmitsWith();
766
770
  dispatch("turbo:submit-start", {
767
771
  target: this.formElement,
768
772
  detail: { formSubmission: this },
@@ -798,17 +802,46 @@ class FormSubmission {
798
802
  var _a;
799
803
  this.state = FormSubmissionState.stopped;
800
804
  (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.removeAttribute("disabled");
805
+ this.resetSubmitterText();
801
806
  dispatch("turbo:submit-end", {
802
807
  target: this.formElement,
803
808
  detail: Object.assign({ formSubmission: this }, this.result),
804
809
  });
805
810
  this.delegate.formSubmissionFinished(this);
806
811
  }
812
+ setSubmitsWith() {
813
+ if (!this.submitter || !this.submitsWith)
814
+ return;
815
+ if (this.submitter.matches("button")) {
816
+ this.originalSubmitText = this.submitter.innerHTML;
817
+ this.submitter.innerHTML = this.submitsWith;
818
+ }
819
+ else if (this.submitter.matches("input")) {
820
+ const input = this.submitter;
821
+ this.originalSubmitText = input.value;
822
+ input.value = this.submitsWith;
823
+ }
824
+ }
825
+ resetSubmitterText() {
826
+ if (!this.submitter || !this.originalSubmitText)
827
+ return;
828
+ if (this.submitter.matches("button")) {
829
+ this.submitter.innerHTML = this.originalSubmitText;
830
+ }
831
+ else if (this.submitter.matches("input")) {
832
+ const input = this.submitter;
833
+ input.value = this.originalSubmitText;
834
+ }
835
+ }
807
836
  requestMustRedirect(request) {
808
- return !request.isIdempotent && this.mustRedirect;
837
+ return !request.isSafe && this.mustRedirect;
809
838
  }
810
839
  requestAcceptsTurboStreamResponse(request) {
811
- return !request.isIdempotent || hasAttribute("data-turbo-stream", this.submitter, this.formElement);
840
+ return !request.isSafe || hasAttribute("data-turbo-stream", this.submitter, this.formElement);
841
+ }
842
+ get submitsWith() {
843
+ var _a;
844
+ return (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("data-turbo-submits-with");
812
845
  }
813
846
  }
814
847
  function buildFormData(formElement, submitter) {
@@ -940,12 +973,17 @@ function submissionDoesNotDismissDialog(form, submitter) {
940
973
  return method != "dialog";
941
974
  }
942
975
  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;
976
+ if ((submitter === null || submitter === void 0 ? void 0 : submitter.hasAttribute("formtarget")) || form.hasAttribute("target")) {
977
+ const target = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formtarget")) || form.target;
978
+ for (const element of document.getElementsByName(target)) {
979
+ if (element instanceof HTMLIFrameElement)
980
+ return false;
981
+ }
982
+ return true;
983
+ }
984
+ else {
985
+ return true;
947
986
  }
948
- return true;
949
987
  }
950
988
 
951
989
  class View {
@@ -1043,8 +1081,8 @@ class View {
1043
1081
  }
1044
1082
 
1045
1083
  class FrameView extends View {
1046
- invalidate() {
1047
- this.element.innerHTML = "";
1084
+ missing() {
1085
+ this.element.innerHTML = `<strong class="turbo-frame-error">Content missing</strong>`;
1048
1086
  }
1049
1087
  get snapshot() {
1050
1088
  return new Snapshot(this.element);
@@ -1138,20 +1176,23 @@ class LinkClickObserver {
1138
1176
  event.shiftKey);
1139
1177
  }
1140
1178
  findLinkFromClickTarget(target) {
1141
- if (target instanceof Element) {
1142
- return target.closest("a[href]:not([target^=_]):not([download])");
1143
- }
1179
+ return findClosestRecursively(target, "a[href]:not([target^=_]):not([download])");
1144
1180
  }
1145
1181
  getLocationForLink(link) {
1146
1182
  return expandURL(link.getAttribute("href") || "");
1147
1183
  }
1148
1184
  }
1149
1185
  function doesNotTargetIFrame(anchor) {
1150
- for (const element of document.getElementsByName(anchor.target)) {
1151
- if (element instanceof HTMLIFrameElement)
1152
- return false;
1186
+ if (anchor.hasAttribute("target")) {
1187
+ for (const element of document.getElementsByName(anchor.target)) {
1188
+ if (element instanceof HTMLIFrameElement)
1189
+ return false;
1190
+ }
1191
+ return true;
1192
+ }
1193
+ else {
1194
+ return true;
1153
1195
  }
1154
- return true;
1155
1196
  }
1156
1197
 
1157
1198
  class FormLinkClickObserver {
@@ -1170,10 +1211,14 @@ class FormLinkClickObserver {
1170
1211
  link.hasAttribute("data-turbo-method"));
1171
1212
  }
1172
1213
  followedLinkToLocation(link, location) {
1173
- const action = location.href;
1174
1214
  const form = document.createElement("form");
1215
+ const type = "hidden";
1216
+ for (const [name, value] of location.searchParams) {
1217
+ form.append(Object.assign(document.createElement("input"), { type, name, value }));
1218
+ }
1219
+ const action = Object.assign(location, { search: "" });
1175
1220
  form.setAttribute("data-turbo", "true");
1176
- form.setAttribute("action", action);
1221
+ form.setAttribute("action", action.href);
1177
1222
  form.setAttribute("hidden", "");
1178
1223
  const method = link.getAttribute("data-turbo-method");
1179
1224
  if (method)
@@ -1181,7 +1226,7 @@ class FormLinkClickObserver {
1181
1226
  const turboFrame = link.getAttribute("data-turbo-frame");
1182
1227
  if (turboFrame)
1183
1228
  form.setAttribute("data-turbo-frame", turboFrame);
1184
- const turboAction = link.getAttribute("data-turbo-action");
1229
+ const turboAction = getVisitAction(link);
1185
1230
  if (turboAction)
1186
1231
  form.setAttribute("data-turbo-action", turboAction);
1187
1232
  const turboConfirm = link.getAttribute("data-turbo-confirm");
@@ -1198,16 +1243,16 @@ class FormLinkClickObserver {
1198
1243
  }
1199
1244
 
1200
1245
  class Bardo {
1201
- constructor(delegate, permanentElementMap) {
1202
- this.delegate = delegate;
1203
- this.permanentElementMap = permanentElementMap;
1204
- }
1205
- static preservingPermanentElements(delegate, permanentElementMap, callback) {
1246
+ static async preservingPermanentElements(delegate, permanentElementMap, callback) {
1206
1247
  const bardo = new this(delegate, permanentElementMap);
1207
1248
  bardo.enter();
1208
- callback();
1249
+ await callback();
1209
1250
  bardo.leave();
1210
1251
  }
1252
+ constructor(delegate, permanentElementMap) {
1253
+ this.delegate = delegate;
1254
+ this.permanentElementMap = permanentElementMap;
1255
+ }
1211
1256
  enter() {
1212
1257
  for (const id in this.permanentElementMap) {
1213
1258
  const [currentPermanentElement, newPermanentElement] = this.permanentElementMap[id];
@@ -1274,8 +1319,8 @@ class Renderer {
1274
1319
  delete this.resolvingFunctions;
1275
1320
  }
1276
1321
  }
1277
- preservingPermanentElements(callback) {
1278
- Bardo.preservingPermanentElements(this, this.permanentElementMap, callback);
1322
+ async preservingPermanentElements(callback) {
1323
+ await Bardo.preservingPermanentElements(this, this.permanentElementMap, callback);
1279
1324
  }
1280
1325
  focusFirstAutofocusableElement() {
1281
1326
  const element = this.connectedSnapshot.firstAutofocusableElement;
@@ -1314,10 +1359,6 @@ function elementIsFocusable(element) {
1314
1359
  }
1315
1360
 
1316
1361
  class FrameRenderer extends Renderer {
1317
- constructor(delegate, currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) {
1318
- super(currentSnapshot, newSnapshot, renderElement, isPreview, willRender);
1319
- this.delegate = delegate;
1320
- }
1321
1362
  static renderElement(currentElement, newElement) {
1322
1363
  var _a;
1323
1364
  const destinationRange = document.createRange();
@@ -1330,6 +1371,10 @@ class FrameRenderer extends Renderer {
1330
1371
  currentElement.appendChild(sourceRange.extractContents());
1331
1372
  }
1332
1373
  }
1374
+ constructor(delegate, currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) {
1375
+ super(currentSnapshot, newSnapshot, renderElement, isPreview, willRender);
1376
+ this.delegate = delegate;
1377
+ }
1333
1378
  get shouldRender() {
1334
1379
  return true;
1335
1380
  }
@@ -1388,18 +1433,6 @@ function readScrollBehavior(value, defaultValue) {
1388
1433
  }
1389
1434
 
1390
1435
  class ProgressBar {
1391
- constructor() {
1392
- this.hiding = false;
1393
- this.value = 0;
1394
- this.visible = false;
1395
- this.trickle = () => {
1396
- this.setValue(this.value + Math.random() / 100);
1397
- };
1398
- this.stylesheetElement = this.createStylesheetElement();
1399
- this.progressElement = this.createProgressElement();
1400
- this.installStylesheetElement();
1401
- this.setValue(0);
1402
- }
1403
1436
  static get defaultCSS() {
1404
1437
  return unindent `
1405
1438
  .turbo-progress-bar {
@@ -1417,6 +1450,18 @@ class ProgressBar {
1417
1450
  }
1418
1451
  `;
1419
1452
  }
1453
+ constructor() {
1454
+ this.hiding = false;
1455
+ this.value = 0;
1456
+ this.visible = false;
1457
+ this.trickle = () => {
1458
+ this.setValue(this.value + Math.random() / 100);
1459
+ };
1460
+ this.stylesheetElement = this.createStylesheetElement();
1461
+ this.progressElement = this.createProgressElement();
1462
+ this.installStylesheetElement();
1463
+ this.setValue(0);
1464
+ }
1420
1465
  show() {
1421
1466
  if (!this.visible) {
1422
1467
  this.visible = true;
@@ -1587,10 +1632,6 @@ function elementWithoutNonce(element) {
1587
1632
  }
1588
1633
 
1589
1634
  class PageSnapshot extends Snapshot {
1590
- constructor(element, headSnapshot) {
1591
- super(element);
1592
- this.headSnapshot = headSnapshot;
1593
- }
1594
1635
  static fromHTMLString(html = "") {
1595
1636
  return this.fromDocument(parseHTMLDocument(html));
1596
1637
  }
@@ -1600,6 +1641,10 @@ class PageSnapshot extends Snapshot {
1600
1641
  static fromDocument({ head, body }) {
1601
1642
  return new this(body, new HeadSnapshot(head));
1602
1643
  }
1644
+ constructor(element, headSnapshot) {
1645
+ super(element);
1646
+ this.headSnapshot = headSnapshot;
1647
+ }
1603
1648
  clone() {
1604
1649
  const clonedElement = this.element.cloneNode(true);
1605
1650
  const selectElements = this.element.querySelectorAll("select");
@@ -1860,6 +1905,8 @@ class Visit {
1860
1905
  this.adapter.visitProposedToLocation(this.redirectedToLocation, {
1861
1906
  action: "replace",
1862
1907
  response: this.response,
1908
+ shouldCacheSnapshot: false,
1909
+ willRender: false,
1863
1910
  });
1864
1911
  this.followedRedirect = true;
1865
1912
  }
@@ -1874,7 +1921,7 @@ class Visit {
1874
1921
  });
1875
1922
  }
1876
1923
  }
1877
- prepareHeadersForRequest(headers, request) {
1924
+ prepareRequest(request) {
1878
1925
  if (this.acceptsStreamResponse) {
1879
1926
  request.acceptResponseType(StreamMessage.contentType);
1880
1927
  }
@@ -2097,10 +2144,11 @@ class BrowserAdapter {
2097
2144
 
2098
2145
  class CacheObserver {
2099
2146
  constructor() {
2147
+ this.selector = "[data-turbo-temporary]";
2148
+ this.deprecatedSelector = "[data-turbo-cache=false]";
2100
2149
  this.started = false;
2101
- this.removeStaleElements = ((_event) => {
2102
- const staleElements = [...document.querySelectorAll('[data-turbo-cache="false"]')];
2103
- for (const element of staleElements) {
2150
+ this.removeTemporaryElements = ((_event) => {
2151
+ for (const element of this.temporaryElements) {
2104
2152
  element.remove();
2105
2153
  }
2106
2154
  });
@@ -2108,15 +2156,25 @@ class CacheObserver {
2108
2156
  start() {
2109
2157
  if (!this.started) {
2110
2158
  this.started = true;
2111
- addEventListener("turbo:before-cache", this.removeStaleElements, false);
2159
+ addEventListener("turbo:before-cache", this.removeTemporaryElements, false);
2112
2160
  }
2113
2161
  }
2114
2162
  stop() {
2115
2163
  if (this.started) {
2116
2164
  this.started = false;
2117
- removeEventListener("turbo:before-cache", this.removeStaleElements, false);
2165
+ removeEventListener("turbo:before-cache", this.removeTemporaryElements, false);
2118
2166
  }
2119
2167
  }
2168
+ get temporaryElements() {
2169
+ return [...document.querySelectorAll(this.selector), ...this.temporaryElementsWithDeprecation];
2170
+ }
2171
+ get temporaryElementsWithDeprecation() {
2172
+ const elements = document.querySelectorAll(this.deprecatedSelector);
2173
+ if (elements.length) {
2174
+ console.warn(`The ${this.deprecatedSelector} selector is deprecated and will be removed in a future version. Use ${this.selector} instead.`);
2175
+ }
2176
+ return [...elements];
2177
+ }
2120
2178
  }
2121
2179
 
2122
2180
  class FrameRedirector {
@@ -2315,7 +2373,7 @@ class Navigator {
2315
2373
  if (formSubmission == this.formSubmission) {
2316
2374
  const responseHTML = await fetchResponse.responseHTML;
2317
2375
  if (responseHTML) {
2318
- const shouldCacheSnapshot = formSubmission.method == FetchMethod.get;
2376
+ const shouldCacheSnapshot = formSubmission.isSafe;
2319
2377
  if (!shouldCacheSnapshot) {
2320
2378
  this.view.clearSnapshotCache();
2321
2379
  }
@@ -2375,10 +2433,8 @@ class Navigator {
2375
2433
  get restorationIdentifier() {
2376
2434
  return this.history.restorationIdentifier;
2377
2435
  }
2378
- getActionForFormSubmission(formSubmission) {
2379
- const { formElement, submitter } = formSubmission;
2380
- const action = getAttribute("data-turbo-action", submitter, formElement);
2381
- return isAction(action) ? action : "advance";
2436
+ getActionForFormSubmission({ submitter, formElement }) {
2437
+ return getVisitAction(submitter, formElement) || "advance";
2382
2438
  }
2383
2439
  }
2384
2440
 
@@ -2620,7 +2676,7 @@ class PageRenderer extends Renderer {
2620
2676
  }
2621
2677
  async render() {
2622
2678
  if (this.willRender) {
2623
- this.replaceBody();
2679
+ await this.replaceBody();
2624
2680
  }
2625
2681
  }
2626
2682
  finishRendering() {
@@ -2639,16 +2695,16 @@ class PageRenderer extends Renderer {
2639
2695
  return this.newSnapshot.element;
2640
2696
  }
2641
2697
  async mergeHead() {
2698
+ const mergedHeadElements = this.mergeProvisionalElements();
2642
2699
  const newStylesheetElements = this.copyNewHeadStylesheetElements();
2643
2700
  this.copyNewHeadScriptElements();
2644
- this.removeCurrentHeadProvisionalElements();
2645
- this.copyNewHeadProvisionalElements();
2701
+ await mergedHeadElements;
2646
2702
  await newStylesheetElements;
2647
2703
  }
2648
- replaceBody() {
2649
- this.preservingPermanentElements(() => {
2704
+ async replaceBody() {
2705
+ await this.preservingPermanentElements(async () => {
2650
2706
  this.activateNewBody();
2651
- this.assignNewBody();
2707
+ await this.assignNewBody();
2652
2708
  });
2653
2709
  }
2654
2710
  get trackedElementsAreIdentical() {
@@ -2667,6 +2723,35 @@ class PageRenderer extends Renderer {
2667
2723
  document.head.appendChild(activateScriptElement(element));
2668
2724
  }
2669
2725
  }
2726
+ async mergeProvisionalElements() {
2727
+ const newHeadElements = [...this.newHeadProvisionalElements];
2728
+ for (const element of this.currentHeadProvisionalElements) {
2729
+ if (!this.isCurrentElementInElementList(element, newHeadElements)) {
2730
+ document.head.removeChild(element);
2731
+ }
2732
+ }
2733
+ for (const element of newHeadElements) {
2734
+ document.head.appendChild(element);
2735
+ }
2736
+ }
2737
+ isCurrentElementInElementList(element, elementList) {
2738
+ for (const [index, newElement] of elementList.entries()) {
2739
+ if (element.tagName == "TITLE") {
2740
+ if (newElement.tagName != "TITLE") {
2741
+ continue;
2742
+ }
2743
+ if (element.innerHTML == newElement.innerHTML) {
2744
+ elementList.splice(index, 1);
2745
+ return true;
2746
+ }
2747
+ }
2748
+ if (newElement.isEqualNode(element)) {
2749
+ elementList.splice(index, 1);
2750
+ return true;
2751
+ }
2752
+ }
2753
+ return false;
2754
+ }
2670
2755
  removeCurrentHeadProvisionalElements() {
2671
2756
  for (const element of this.currentHeadProvisionalElements) {
2672
2757
  document.head.removeChild(element);
@@ -2687,8 +2772,8 @@ class PageRenderer extends Renderer {
2687
2772
  inertScriptElement.replaceWith(activatedScriptElement);
2688
2773
  }
2689
2774
  }
2690
- assignNewBody() {
2691
- this.renderElement(this.currentElement, this.newElement);
2775
+ async assignNewBody() {
2776
+ await this.renderElement(this.currentElement, this.newElement);
2692
2777
  }
2693
2778
  get newHeadStylesheetElements() {
2694
2779
  return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot);
@@ -3105,8 +3190,8 @@ class Session {
3105
3190
  }
3106
3191
  }
3107
3192
  elementIsNavigatable(element) {
3108
- const container = element.closest("[data-turbo]");
3109
- const withinFrame = element.closest("turbo-frame");
3193
+ const container = findClosestRecursively(element, "[data-turbo]");
3194
+ const withinFrame = findClosestRecursively(element, "turbo-frame");
3110
3195
  if (this.drive || withinFrame) {
3111
3196
  if (container) {
3112
3197
  return container.getAttribute("data-turbo") != "false";
@@ -3125,8 +3210,7 @@ class Session {
3125
3210
  }
3126
3211
  }
3127
3212
  getActionForLink(link) {
3128
- const action = link.getAttribute("data-turbo-action");
3129
- return isAction(action) ? action : "advance";
3213
+ return getVisitAction(link) || "advance";
3130
3214
  }
3131
3215
  get snapshot() {
3132
3216
  return this.view.snapshot;
@@ -3186,7 +3270,10 @@ const StreamActions = {
3186
3270
  this.targetElements.forEach((e) => e.replaceWith(this.templateContent));
3187
3271
  },
3188
3272
  update() {
3189
- this.targetElements.forEach((e) => e.replaceChildren(this.templateContent));
3273
+ this.targetElements.forEach((targetElement) => {
3274
+ targetElement.innerHTML = "";
3275
+ targetElement.append(this.templateContent);
3276
+ });
3190
3277
  },
3191
3278
  };
3192
3279
 
@@ -3246,6 +3333,9 @@ var Turbo = /*#__PURE__*/Object.freeze({
3246
3333
  StreamActions: StreamActions
3247
3334
  });
3248
3335
 
3336
+ class TurboFrameMissingError extends Error {
3337
+ }
3338
+
3249
3339
  class FrameController {
3250
3340
  constructor(element) {
3251
3341
  this.fetchResponseLoaded = (_fetchResponse) => { };
@@ -3346,35 +3436,22 @@ class FrameController {
3346
3436
  try {
3347
3437
  const html = await fetchResponse.responseHTML;
3348
3438
  if (html) {
3349
- const { body } = parseHTMLDocument(html);
3350
- const newFrameElement = await this.extractForeignFrameElement(body);
3351
- if (newFrameElement) {
3352
- const snapshot = new Snapshot(newFrameElement);
3353
- const renderer = new FrameRenderer(this, this.view.snapshot, snapshot, FrameRenderer.renderElement, false, false);
3354
- if (this.view.renderPromise)
3355
- await this.view.renderPromise;
3356
- this.changeHistory();
3357
- await this.view.render(renderer);
3358
- this.complete = true;
3359
- session.frameRendered(fetchResponse, this.element);
3360
- session.frameLoaded(this.element);
3361
- this.fetchResponseLoaded(fetchResponse);
3439
+ const document = parseHTMLDocument(html);
3440
+ const pageSnapshot = PageSnapshot.fromDocument(document);
3441
+ if (pageSnapshot.isVisitable) {
3442
+ await this.loadFrameResponse(fetchResponse, document);
3362
3443
  }
3363
- else if (this.willHandleFrameMissingFromResponse(fetchResponse)) {
3364
- console.warn(`A matching frame for #${this.element.id} was missing from the response, transforming into full-page Visit.`);
3365
- this.visitResponse(fetchResponse.response);
3444
+ else {
3445
+ await this.handleUnvisitableFrameResponse(fetchResponse);
3366
3446
  }
3367
3447
  }
3368
3448
  }
3369
- catch (error) {
3370
- console.error(error);
3371
- this.view.invalidate();
3372
- }
3373
3449
  finally {
3374
3450
  this.fetchResponseLoaded = () => { };
3375
3451
  }
3376
3452
  }
3377
- elementAppearedInViewport(_element) {
3453
+ elementAppearedInViewport(element) {
3454
+ this.proposeVisitIfNavigatedWithAction(element, element);
3378
3455
  this.loadSourceURL();
3379
3456
  }
3380
3457
  willSubmitFormLinkToLocation(link) {
@@ -3400,12 +3477,12 @@ class FrameController {
3400
3477
  }
3401
3478
  this.formSubmission = new FormSubmission(this, element, submitter);
3402
3479
  const { fetchRequest } = this.formSubmission;
3403
- this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
3480
+ this.prepareRequest(fetchRequest);
3404
3481
  this.formSubmission.start();
3405
3482
  }
3406
- prepareHeadersForRequest(headers, request) {
3483
+ prepareRequest(request) {
3407
3484
  var _a;
3408
- headers["Turbo-Frame"] = this.id;
3485
+ request.headers["Turbo-Frame"] = this.id;
3409
3486
  if ((_a = this.currentNavigationElement) === null || _a === void 0 ? void 0 : _a.hasAttribute("data-turbo-stream")) {
3410
3487
  request.acceptResponseType(StreamMessage.contentType);
3411
3488
  }
@@ -3421,7 +3498,6 @@ class FrameController {
3421
3498
  this.resolveVisitPromise();
3422
3499
  }
3423
3500
  async requestFailedWithResponse(request, response) {
3424
- console.error(response);
3425
3501
  await this.loadResponse(response);
3426
3502
  this.resolveVisitPromise();
3427
3503
  }
@@ -3439,9 +3515,13 @@ class FrameController {
3439
3515
  const frame = this.findFrameElement(formSubmission.formElement, formSubmission.submitter);
3440
3516
  frame.delegate.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
3441
3517
  frame.delegate.loadResponse(response);
3518
+ if (!formSubmission.isSafe) {
3519
+ session.clearCache();
3520
+ }
3442
3521
  }
3443
3522
  formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
3444
3523
  this.element.delegate.loadResponse(fetchResponse);
3524
+ session.clearCache();
3445
3525
  }
3446
3526
  formSubmissionErrored(formSubmission, error) {
3447
3527
  console.error(error);
@@ -3469,6 +3549,24 @@ class FrameController {
3469
3549
  willRenderFrame(currentElement, _newElement) {
3470
3550
  this.previousFrameElement = currentElement.cloneNode(true);
3471
3551
  }
3552
+ async loadFrameResponse(fetchResponse, document) {
3553
+ const newFrameElement = await this.extractForeignFrameElement(document.body);
3554
+ if (newFrameElement) {
3555
+ const snapshot = new Snapshot(newFrameElement);
3556
+ const renderer = new FrameRenderer(this, this.view.snapshot, snapshot, FrameRenderer.renderElement, false, false);
3557
+ if (this.view.renderPromise)
3558
+ await this.view.renderPromise;
3559
+ this.changeHistory();
3560
+ await this.view.render(renderer);
3561
+ this.complete = true;
3562
+ session.frameRendered(fetchResponse, this.element);
3563
+ session.frameLoaded(this.element);
3564
+ this.fetchResponseLoaded(fetchResponse);
3565
+ }
3566
+ else if (this.willHandleFrameMissingFromResponse(fetchResponse)) {
3567
+ this.handleFrameMissingFromResponse(fetchResponse);
3568
+ }
3569
+ }
3472
3570
  async visit(url) {
3473
3571
  var _a;
3474
3572
  const request = new FetchRequest(this, FetchMethod.get, url, new URLSearchParams(), this.element);
@@ -3485,7 +3583,6 @@ class FrameController {
3485
3583
  }
3486
3584
  navigateFrame(element, url, submitter) {
3487
3585
  const frame = this.findFrameElement(element, submitter);
3488
- this.pageSnapshot = PageSnapshot.fromElement(frame).clone();
3489
3586
  frame.delegate.proposeVisitIfNavigatedWithAction(frame, element, submitter);
3490
3587
  this.withCurrentNavigationElement(element, () => {
3491
3588
  frame.src = url;
@@ -3493,7 +3590,8 @@ class FrameController {
3493
3590
  }
3494
3591
  proposeVisitIfNavigatedWithAction(frame, element, submitter) {
3495
3592
  this.action = getVisitAction(submitter, element, frame);
3496
- if (isAction(this.action)) {
3593
+ if (this.action) {
3594
+ const pageSnapshot = PageSnapshot.fromElement(frame).clone();
3497
3595
  const { visitCachedSnapshot } = frame.delegate;
3498
3596
  frame.delegate.fetchResponseLoaded = (fetchResponse) => {
3499
3597
  if (frame.src) {
@@ -3506,7 +3604,7 @@ class FrameController {
3506
3604
  willRender: false,
3507
3605
  updateHistory: false,
3508
3606
  restorationIdentifier: this.restorationIdentifier,
3509
- snapshot: this.pageSnapshot,
3607
+ snapshot: pageSnapshot,
3510
3608
  };
3511
3609
  if (this.action)
3512
3610
  options.action = this.action;
@@ -3521,6 +3619,10 @@ class FrameController {
3521
3619
  session.history.update(method, expandURL(this.element.src || ""), this.restorationIdentifier);
3522
3620
  }
3523
3621
  }
3622
+ async handleUnvisitableFrameResponse(fetchResponse) {
3623
+ console.warn(`The response (${fetchResponse.statusCode}) from <turbo-frame id="${this.element.id}"> is performing a full page visit due to turbo-visit-control.`);
3624
+ await this.visitResponse(fetchResponse.response);
3625
+ }
3524
3626
  willHandleFrameMissingFromResponse(fetchResponse) {
3525
3627
  this.element.setAttribute("complete", "");
3526
3628
  const response = fetchResponse.response;
@@ -3539,6 +3641,14 @@ class FrameController {
3539
3641
  });
3540
3642
  return !event.defaultPrevented;
3541
3643
  }
3644
+ handleFrameMissingFromResponse(fetchResponse) {
3645
+ this.view.missing();
3646
+ this.throwFrameMissingError(fetchResponse);
3647
+ }
3648
+ throwFrameMissingError(fetchResponse) {
3649
+ const message = `The response (${fetchResponse.statusCode}) did not contain the expected <turbo-frame id="${this.element.id}"> and will be ignored. To perform a full page visit instead, set turbo-visit-control to reload.`;
3650
+ throw new TurboFrameMissingError(message);
3651
+ }
3542
3652
  async visitResponse(response) {
3543
3653
  const wrapped = new FetchResponse(response);
3544
3654
  const responseHTML = await wrapped.responseHTML;