turbo-rails 1.3.2 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -56,13 +56,11 @@ function clickCaptured(event) {
56
56
 
57
57
  (function() {
58
58
  if ("submitter" in Event.prototype) return;
59
- let prototype;
59
+ let prototype = window.Event.prototype;
60
60
  if ("SubmitEvent" in window && /Apple Computer/.test(navigator.vendor)) {
61
61
  prototype = window.SubmitEvent.prototype;
62
62
  } else if ("SubmitEvent" in window) {
63
63
  return;
64
- } else {
65
- prototype = window.Event.prototype;
66
64
  }
67
65
  addEventListener("click", clickCaptured, true);
68
66
  Object.defineProperty(prototype, "submitter", {
@@ -82,14 +80,14 @@ var FrameLoadingStyle;
82
80
  })(FrameLoadingStyle || (FrameLoadingStyle = {}));
83
81
 
84
82
  class FrameElement extends HTMLElement {
83
+ static get observedAttributes() {
84
+ return [ "disabled", "complete", "loading", "src" ];
85
+ }
85
86
  constructor() {
86
87
  super();
87
88
  this.loaded = Promise.resolve();
88
89
  this.delegate = new FrameElement.delegateConstructor(this);
89
90
  }
90
- static get observedAttributes() {
91
- return [ "disabled", "complete", "loading", "src" ];
92
- }
93
91
  connectedCallback() {
94
92
  this.delegate.connect();
95
93
  }
@@ -282,10 +280,6 @@ class FetchResponse {
282
280
  }
283
281
  }
284
282
 
285
- function isAction(action) {
286
- return action == "advance" || action == "replace" || action == "restore";
287
- }
288
-
289
283
  function activateScriptElement(element) {
290
284
  if (element.getAttribute("data-turbo-eval") == "false") {
291
285
  return element;
@@ -318,6 +312,7 @@ function dispatch(eventName, {target: target, cancelable: cancelable, detail: de
318
312
  const event = new CustomEvent(eventName, {
319
313
  cancelable: cancelable,
320
314
  bubbles: true,
315
+ composed: true,
321
316
  detail: detail
322
317
  });
323
318
  if (target && target.isConnected) {
@@ -431,6 +426,10 @@ function getHistoryMethodForAction(action) {
431
426
  }
432
427
  }
433
428
 
429
+ function isAction(action) {
430
+ return action == "advance" || action == "replace" || action == "restore";
431
+ }
432
+
434
433
  function getVisitAction(...elements) {
435
434
  const action = getAttribute("data-turbo-action", ...elements);
436
435
  return isAction(action) ? action : null;
@@ -456,6 +455,13 @@ function setMetaContent(name, content) {
456
455
  return element;
457
456
  }
458
457
 
458
+ function findClosestRecursively(element, selector) {
459
+ var _a;
460
+ if (element instanceof Element) {
461
+ return element.closest(selector) || findClosestRecursively(element.assignedSlot || ((_a = element.getRootNode()) === null || _a === void 0 ? void 0 : _a.host), selector);
462
+ }
463
+ }
464
+
459
465
  var FetchMethod;
460
466
 
461
467
  (function(FetchMethod) {
@@ -509,9 +515,8 @@ class FetchRequest {
509
515
  this.abortController.abort();
510
516
  }
511
517
  async perform() {
512
- var _a, _b;
513
518
  const {fetchOptions: fetchOptions} = this;
514
- (_b = (_a = this.delegate).prepareHeadersForRequest) === null || _b === void 0 ? void 0 : _b.call(_a, this.headers, this);
519
+ this.delegate.prepareRequest(this);
515
520
  await this.allowRequestToBeIntercepted(fetchOptions);
516
521
  try {
517
522
  this.delegate.requestStarted(this);
@@ -553,7 +558,7 @@ class FetchRequest {
553
558
  credentials: "same-origin",
554
559
  headers: this.headers,
555
560
  redirect: "follow",
556
- body: this.isIdempotent ? null : this.body,
561
+ body: this.isSafe ? null : this.body,
557
562
  signal: this.abortSignal,
558
563
  referrer: (_a = this.delegate.referrer) === null || _a === void 0 ? void 0 : _a.href
559
564
  };
@@ -563,8 +568,8 @@ class FetchRequest {
563
568
  Accept: "text/html, application/xhtml+xml"
564
569
  };
565
570
  }
566
- get isIdempotent() {
567
- return this.method == FetchMethod.get;
571
+ get isSafe() {
572
+ return this.method === FetchMethod.get;
568
573
  }
569
574
  get abortSignal() {
570
575
  return this.abortController.signal;
@@ -626,9 +631,6 @@ class AppearanceObserver {
626
631
  }
627
632
 
628
633
  class StreamMessage {
629
- constructor(fragment) {
630
- this.fragment = importStreamElements(fragment);
631
- }
632
634
  static wrap(message) {
633
635
  if (typeof message == "string") {
634
636
  return new this(createDocumentFragment(message));
@@ -636,6 +638,9 @@ class StreamMessage {
636
638
  return message;
637
639
  }
638
640
  }
641
+ constructor(fragment) {
642
+ this.fragment = importStreamElements(fragment);
643
+ }
639
644
  }
640
645
 
641
646
  StreamMessage.contentType = "text/vnd.turbo-stream.html";
@@ -684,6 +689,9 @@ function formEnctypeFromString(encoding) {
684
689
  }
685
690
 
686
691
  class FormSubmission {
692
+ static confirmMethod(message, _element, _submitter) {
693
+ return Promise.resolve(confirm(message));
694
+ }
687
695
  constructor(delegate, formElement, submitter, mustRedirect = false) {
688
696
  this.state = FormSubmissionState.initialized;
689
697
  this.delegate = delegate;
@@ -697,9 +705,6 @@ class FormSubmission {
697
705
  this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body, this.formElement);
698
706
  this.mustRedirect = mustRedirect;
699
707
  }
700
- static confirmMethod(message, _element, _submitter) {
701
- return Promise.resolve(confirm(message));
702
- }
703
708
  get method() {
704
709
  var _a;
705
710
  const method = ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formmethod")) || this.formElement.getAttribute("method") || "";
@@ -725,8 +730,8 @@ class FormSubmission {
725
730
  var _a;
726
731
  return formEnctypeFromString(((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formenctype")) || this.formElement.enctype);
727
732
  }
728
- get isIdempotent() {
729
- return this.fetchRequest.isIdempotent;
733
+ get isSafe() {
734
+ return this.fetchRequest.isSafe;
730
735
  }
731
736
  get stringFormData() {
732
737
  return [ ...this.formData ].reduce(((entries, [name, value]) => entries.concat(typeof value == "string" ? [ [ name, value ] ] : [])), []);
@@ -753,11 +758,11 @@ class FormSubmission {
753
758
  return true;
754
759
  }
755
760
  }
756
- prepareHeadersForRequest(headers, request) {
757
- if (!request.isIdempotent) {
761
+ prepareRequest(request) {
762
+ if (!request.isSafe) {
758
763
  const token = getCookieValue(getMetaContent("csrf-param")) || getMetaContent("csrf-token");
759
764
  if (token) {
760
- headers["X-CSRF-Token"] = token;
765
+ request.headers["X-CSRF-Token"] = token;
761
766
  }
762
767
  }
763
768
  if (this.requestAcceptsTurboStreamResponse(request)) {
@@ -768,6 +773,7 @@ class FormSubmission {
768
773
  var _a;
769
774
  this.state = FormSubmissionState.waiting;
770
775
  (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.setAttribute("disabled", "");
776
+ this.setSubmitsWith();
771
777
  dispatch("turbo:submit-start", {
772
778
  target: this.formElement,
773
779
  detail: {
@@ -815,6 +821,7 @@ class FormSubmission {
815
821
  var _a;
816
822
  this.state = FormSubmissionState.stopped;
817
823
  (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.removeAttribute("disabled");
824
+ this.resetSubmitterText();
818
825
  dispatch("turbo:submit-end", {
819
826
  target: this.formElement,
820
827
  detail: Object.assign({
@@ -823,11 +830,35 @@ class FormSubmission {
823
830
  });
824
831
  this.delegate.formSubmissionFinished(this);
825
832
  }
833
+ setSubmitsWith() {
834
+ if (!this.submitter || !this.submitsWith) return;
835
+ if (this.submitter.matches("button")) {
836
+ this.originalSubmitText = this.submitter.innerHTML;
837
+ this.submitter.innerHTML = this.submitsWith;
838
+ } else if (this.submitter.matches("input")) {
839
+ const input = this.submitter;
840
+ this.originalSubmitText = input.value;
841
+ input.value = this.submitsWith;
842
+ }
843
+ }
844
+ resetSubmitterText() {
845
+ if (!this.submitter || !this.originalSubmitText) return;
846
+ if (this.submitter.matches("button")) {
847
+ this.submitter.innerHTML = this.originalSubmitText;
848
+ } else if (this.submitter.matches("input")) {
849
+ const input = this.submitter;
850
+ input.value = this.originalSubmitText;
851
+ }
852
+ }
826
853
  requestMustRedirect(request) {
827
- return !request.isIdempotent && this.mustRedirect;
854
+ return !request.isSafe && this.mustRedirect;
828
855
  }
829
856
  requestAcceptsTurboStreamResponse(request) {
830
- return !request.isIdempotent || hasAttribute("data-turbo-stream", this.submitter, this.formElement);
857
+ return !request.isSafe || hasAttribute("data-turbo-stream", this.submitter, this.formElement);
858
+ }
859
+ get submitsWith() {
860
+ var _a;
861
+ return (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("data-turbo-submits-with");
831
862
  }
832
863
  }
833
864
 
@@ -960,11 +991,15 @@ function submissionDoesNotDismissDialog(form, submitter) {
960
991
  }
961
992
 
962
993
  function submissionDoesNotTargetIFrame(form, submitter) {
963
- const target = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formtarget")) || form.target;
964
- for (const element of document.getElementsByName(target)) {
965
- if (element instanceof HTMLIFrameElement) return false;
994
+ if ((submitter === null || submitter === void 0 ? void 0 : submitter.hasAttribute("formtarget")) || form.hasAttribute("target")) {
995
+ const target = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formtarget")) || form.target;
996
+ for (const element of document.getElementsByName(target)) {
997
+ if (element instanceof HTMLIFrameElement) return false;
998
+ }
999
+ return true;
1000
+ } else {
1001
+ return true;
966
1002
  }
967
- return true;
968
1003
  }
969
1004
 
970
1005
  class View {
@@ -1065,8 +1100,8 @@ class View {
1065
1100
  }
1066
1101
 
1067
1102
  class FrameView extends View {
1068
- invalidate() {
1069
- this.element.innerHTML = "";
1103
+ missing() {
1104
+ this.element.innerHTML = `<strong class="turbo-frame-error">Content missing</strong>`;
1070
1105
  }
1071
1106
  get snapshot() {
1072
1107
  return new Snapshot(this.element);
@@ -1153,9 +1188,7 @@ class LinkClickObserver {
1153
1188
  return !(event.target && event.target.isContentEditable || event.defaultPrevented || event.which > 1 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey);
1154
1189
  }
1155
1190
  findLinkFromClickTarget(target) {
1156
- if (target instanceof Element) {
1157
- return target.closest("a[href]:not([target^=_]):not([download])");
1158
- }
1191
+ return findClosestRecursively(target, "a[href]:not([target^=_]):not([download])");
1159
1192
  }
1160
1193
  getLocationForLink(link) {
1161
1194
  return expandURL(link.getAttribute("href") || "");
@@ -1163,10 +1196,14 @@ class LinkClickObserver {
1163
1196
  }
1164
1197
 
1165
1198
  function doesNotTargetIFrame(anchor) {
1166
- for (const element of document.getElementsByName(anchor.target)) {
1167
- if (element instanceof HTMLIFrameElement) return false;
1199
+ if (anchor.hasAttribute("target")) {
1200
+ for (const element of document.getElementsByName(anchor.target)) {
1201
+ if (element instanceof HTMLIFrameElement) return false;
1202
+ }
1203
+ return true;
1204
+ } else {
1205
+ return true;
1168
1206
  }
1169
- return true;
1170
1207
  }
1171
1208
 
1172
1209
  class FormLinkClickObserver {
@@ -1184,16 +1221,26 @@ class FormLinkClickObserver {
1184
1221
  return this.delegate.willSubmitFormLinkToLocation(link, location, originalEvent) && link.hasAttribute("data-turbo-method");
1185
1222
  }
1186
1223
  followedLinkToLocation(link, location) {
1187
- const action = location.href;
1188
1224
  const form = document.createElement("form");
1225
+ const type = "hidden";
1226
+ for (const [name, value] of location.searchParams) {
1227
+ form.append(Object.assign(document.createElement("input"), {
1228
+ type: type,
1229
+ name: name,
1230
+ value: value
1231
+ }));
1232
+ }
1233
+ const action = Object.assign(location, {
1234
+ search: ""
1235
+ });
1189
1236
  form.setAttribute("data-turbo", "true");
1190
- form.setAttribute("action", action);
1237
+ form.setAttribute("action", action.href);
1191
1238
  form.setAttribute("hidden", "");
1192
1239
  const method = link.getAttribute("data-turbo-method");
1193
1240
  if (method) form.setAttribute("method", method);
1194
1241
  const turboFrame = link.getAttribute("data-turbo-frame");
1195
1242
  if (turboFrame) form.setAttribute("data-turbo-frame", turboFrame);
1196
- const turboAction = link.getAttribute("data-turbo-action");
1243
+ const turboAction = getVisitAction(link);
1197
1244
  if (turboAction) form.setAttribute("data-turbo-action", turboAction);
1198
1245
  const turboConfirm = link.getAttribute("data-turbo-confirm");
1199
1246
  if (turboConfirm) form.setAttribute("data-turbo-confirm", turboConfirm);
@@ -1209,16 +1256,16 @@ class FormLinkClickObserver {
1209
1256
  }
1210
1257
 
1211
1258
  class Bardo {
1212
- constructor(delegate, permanentElementMap) {
1213
- this.delegate = delegate;
1214
- this.permanentElementMap = permanentElementMap;
1215
- }
1216
- static preservingPermanentElements(delegate, permanentElementMap, callback) {
1259
+ static async preservingPermanentElements(delegate, permanentElementMap, callback) {
1217
1260
  const bardo = new this(delegate, permanentElementMap);
1218
1261
  bardo.enter();
1219
- callback();
1262
+ await callback();
1220
1263
  bardo.leave();
1221
1264
  }
1265
+ constructor(delegate, permanentElementMap) {
1266
+ this.delegate = delegate;
1267
+ this.permanentElementMap = permanentElementMap;
1268
+ }
1222
1269
  enter() {
1223
1270
  for (const id in this.permanentElementMap) {
1224
1271
  const [currentPermanentElement, newPermanentElement] = this.permanentElementMap[id];
@@ -1289,8 +1336,8 @@ class Renderer {
1289
1336
  delete this.resolvingFunctions;
1290
1337
  }
1291
1338
  }
1292
- preservingPermanentElements(callback) {
1293
- Bardo.preservingPermanentElements(this, this.permanentElementMap, callback);
1339
+ async preservingPermanentElements(callback) {
1340
+ await Bardo.preservingPermanentElements(this, this.permanentElementMap, callback);
1294
1341
  }
1295
1342
  focusFirstAutofocusableElement() {
1296
1343
  const element = this.connectedSnapshot.firstAutofocusableElement;
@@ -1329,10 +1376,6 @@ function elementIsFocusable(element) {
1329
1376
  }
1330
1377
 
1331
1378
  class FrameRenderer extends Renderer {
1332
- constructor(delegate, currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) {
1333
- super(currentSnapshot, newSnapshot, renderElement, isPreview, willRender);
1334
- this.delegate = delegate;
1335
- }
1336
1379
  static renderElement(currentElement, newElement) {
1337
1380
  var _a;
1338
1381
  const destinationRange = document.createRange();
@@ -1345,6 +1388,10 @@ class FrameRenderer extends Renderer {
1345
1388
  currentElement.appendChild(sourceRange.extractContents());
1346
1389
  }
1347
1390
  }
1391
+ constructor(delegate, currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) {
1392
+ super(currentSnapshot, newSnapshot, renderElement, isPreview, willRender);
1393
+ this.delegate = delegate;
1394
+ }
1348
1395
  get shouldRender() {
1349
1396
  return true;
1350
1397
  }
@@ -1406,18 +1453,6 @@ function readScrollBehavior(value, defaultValue) {
1406
1453
  }
1407
1454
 
1408
1455
  class ProgressBar {
1409
- constructor() {
1410
- this.hiding = false;
1411
- this.value = 0;
1412
- this.visible = false;
1413
- this.trickle = () => {
1414
- this.setValue(this.value + Math.random() / 100);
1415
- };
1416
- this.stylesheetElement = this.createStylesheetElement();
1417
- this.progressElement = this.createProgressElement();
1418
- this.installStylesheetElement();
1419
- this.setValue(0);
1420
- }
1421
1456
  static get defaultCSS() {
1422
1457
  return unindent`
1423
1458
  .turbo-progress-bar {
@@ -1435,6 +1470,18 @@ class ProgressBar {
1435
1470
  }
1436
1471
  `;
1437
1472
  }
1473
+ constructor() {
1474
+ this.hiding = false;
1475
+ this.value = 0;
1476
+ this.visible = false;
1477
+ this.trickle = () => {
1478
+ this.setValue(this.value + Math.random() / 100);
1479
+ };
1480
+ this.stylesheetElement = this.createStylesheetElement();
1481
+ this.progressElement = this.createProgressElement();
1482
+ this.installStylesheetElement();
1483
+ this.setValue(0);
1484
+ }
1438
1485
  show() {
1439
1486
  if (!this.visible) {
1440
1487
  this.visible = true;
@@ -1603,10 +1650,6 @@ function elementWithoutNonce(element) {
1603
1650
  }
1604
1651
 
1605
1652
  class PageSnapshot extends Snapshot {
1606
- constructor(element, headSnapshot) {
1607
- super(element);
1608
- this.headSnapshot = headSnapshot;
1609
- }
1610
1653
  static fromHTMLString(html = "") {
1611
1654
  return this.fromDocument(parseHTMLDocument(html));
1612
1655
  }
@@ -1616,6 +1659,10 @@ class PageSnapshot extends Snapshot {
1616
1659
  static fromDocument({head: head, body: body}) {
1617
1660
  return new this(body, new HeadSnapshot(head));
1618
1661
  }
1662
+ constructor(element, headSnapshot) {
1663
+ super(element);
1664
+ this.headSnapshot = headSnapshot;
1665
+ }
1619
1666
  clone() {
1620
1667
  const clonedElement = this.element.cloneNode(true);
1621
1668
  const selectElements = this.element.querySelectorAll("select");
@@ -1873,7 +1920,9 @@ class Visit {
1873
1920
  if (this.redirectedToLocation && !this.followedRedirect && ((_a = this.response) === null || _a === void 0 ? void 0 : _a.redirected)) {
1874
1921
  this.adapter.visitProposedToLocation(this.redirectedToLocation, {
1875
1922
  action: "replace",
1876
- response: this.response
1923
+ response: this.response,
1924
+ shouldCacheSnapshot: false,
1925
+ willRender: false
1877
1926
  });
1878
1927
  this.followedRedirect = true;
1879
1928
  }
@@ -1888,7 +1937,7 @@ class Visit {
1888
1937
  }));
1889
1938
  }
1890
1939
  }
1891
- prepareHeadersForRequest(headers, request) {
1940
+ prepareRequest(request) {
1892
1941
  if (this.acceptsStreamResponse) {
1893
1942
  request.acceptResponseType(StreamMessage.contentType);
1894
1943
  }
@@ -2118,10 +2167,11 @@ class BrowserAdapter {
2118
2167
 
2119
2168
  class CacheObserver {
2120
2169
  constructor() {
2170
+ this.selector = "[data-turbo-temporary]";
2171
+ this.deprecatedSelector = "[data-turbo-cache=false]";
2121
2172
  this.started = false;
2122
- this.removeStaleElements = _event => {
2123
- const staleElements = [ ...document.querySelectorAll('[data-turbo-cache="false"]') ];
2124
- for (const element of staleElements) {
2173
+ this.removeTemporaryElements = _event => {
2174
+ for (const element of this.temporaryElements) {
2125
2175
  element.remove();
2126
2176
  }
2127
2177
  };
@@ -2129,14 +2179,24 @@ class CacheObserver {
2129
2179
  start() {
2130
2180
  if (!this.started) {
2131
2181
  this.started = true;
2132
- addEventListener("turbo:before-cache", this.removeStaleElements, false);
2182
+ addEventListener("turbo:before-cache", this.removeTemporaryElements, false);
2133
2183
  }
2134
2184
  }
2135
2185
  stop() {
2136
2186
  if (this.started) {
2137
2187
  this.started = false;
2138
- removeEventListener("turbo:before-cache", this.removeStaleElements, false);
2188
+ removeEventListener("turbo:before-cache", this.removeTemporaryElements, false);
2189
+ }
2190
+ }
2191
+ get temporaryElements() {
2192
+ return [ ...document.querySelectorAll(this.selector), ...this.temporaryElementsWithDeprecation ];
2193
+ }
2194
+ get temporaryElementsWithDeprecation() {
2195
+ const elements = document.querySelectorAll(this.deprecatedSelector);
2196
+ if (elements.length) {
2197
+ console.warn(`The ${this.deprecatedSelector} selector is deprecated and will be removed in a future version. Use ${this.selector} instead.`);
2139
2198
  }
2199
+ return [ ...elements ];
2140
2200
  }
2141
2201
  }
2142
2202
 
@@ -2336,7 +2396,7 @@ class Navigator {
2336
2396
  if (formSubmission == this.formSubmission) {
2337
2397
  const responseHTML = await fetchResponse.responseHTML;
2338
2398
  if (responseHTML) {
2339
- const shouldCacheSnapshot = formSubmission.method == FetchMethod.get;
2399
+ const shouldCacheSnapshot = formSubmission.isSafe;
2340
2400
  if (!shouldCacheSnapshot) {
2341
2401
  this.view.clearSnapshotCache();
2342
2402
  }
@@ -2397,10 +2457,8 @@ class Navigator {
2397
2457
  get restorationIdentifier() {
2398
2458
  return this.history.restorationIdentifier;
2399
2459
  }
2400
- getActionForFormSubmission(formSubmission) {
2401
- const {formElement: formElement, submitter: submitter} = formSubmission;
2402
- const action = getAttribute("data-turbo-action", submitter, formElement);
2403
- return isAction(action) ? action : "advance";
2460
+ getActionForFormSubmission({submitter: submitter, formElement: formElement}) {
2461
+ return getVisitAction(submitter, formElement) || "advance";
2404
2462
  }
2405
2463
  }
2406
2464
 
@@ -2648,7 +2706,7 @@ class PageRenderer extends Renderer {
2648
2706
  }
2649
2707
  async render() {
2650
2708
  if (this.willRender) {
2651
- this.replaceBody();
2709
+ await this.replaceBody();
2652
2710
  }
2653
2711
  }
2654
2712
  finishRendering() {
@@ -2667,16 +2725,16 @@ class PageRenderer extends Renderer {
2667
2725
  return this.newSnapshot.element;
2668
2726
  }
2669
2727
  async mergeHead() {
2728
+ const mergedHeadElements = this.mergeProvisionalElements();
2670
2729
  const newStylesheetElements = this.copyNewHeadStylesheetElements();
2671
2730
  this.copyNewHeadScriptElements();
2672
- this.removeCurrentHeadProvisionalElements();
2673
- this.copyNewHeadProvisionalElements();
2731
+ await mergedHeadElements;
2674
2732
  await newStylesheetElements;
2675
2733
  }
2676
- replaceBody() {
2677
- this.preservingPermanentElements((() => {
2734
+ async replaceBody() {
2735
+ await this.preservingPermanentElements((async () => {
2678
2736
  this.activateNewBody();
2679
- this.assignNewBody();
2737
+ await this.assignNewBody();
2680
2738
  }));
2681
2739
  }
2682
2740
  get trackedElementsAreIdentical() {
@@ -2695,6 +2753,35 @@ class PageRenderer extends Renderer {
2695
2753
  document.head.appendChild(activateScriptElement(element));
2696
2754
  }
2697
2755
  }
2756
+ async mergeProvisionalElements() {
2757
+ const newHeadElements = [ ...this.newHeadProvisionalElements ];
2758
+ for (const element of this.currentHeadProvisionalElements) {
2759
+ if (!this.isCurrentElementInElementList(element, newHeadElements)) {
2760
+ document.head.removeChild(element);
2761
+ }
2762
+ }
2763
+ for (const element of newHeadElements) {
2764
+ document.head.appendChild(element);
2765
+ }
2766
+ }
2767
+ isCurrentElementInElementList(element, elementList) {
2768
+ for (const [index, newElement] of elementList.entries()) {
2769
+ if (element.tagName == "TITLE") {
2770
+ if (newElement.tagName != "TITLE") {
2771
+ continue;
2772
+ }
2773
+ if (element.innerHTML == newElement.innerHTML) {
2774
+ elementList.splice(index, 1);
2775
+ return true;
2776
+ }
2777
+ }
2778
+ if (newElement.isEqualNode(element)) {
2779
+ elementList.splice(index, 1);
2780
+ return true;
2781
+ }
2782
+ }
2783
+ return false;
2784
+ }
2698
2785
  removeCurrentHeadProvisionalElements() {
2699
2786
  for (const element of this.currentHeadProvisionalElements) {
2700
2787
  document.head.removeChild(element);
@@ -2715,8 +2802,8 @@ class PageRenderer extends Renderer {
2715
2802
  inertScriptElement.replaceWith(activatedScriptElement);
2716
2803
  }
2717
2804
  }
2718
- assignNewBody() {
2719
- this.renderElement(this.currentElement, this.newElement);
2805
+ async assignNewBody() {
2806
+ await this.renderElement(this.currentElement, this.newElement);
2720
2807
  }
2721
2808
  get newHeadStylesheetElements() {
2722
2809
  return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot);
@@ -3150,8 +3237,8 @@ class Session {
3150
3237
  }
3151
3238
  }
3152
3239
  elementIsNavigatable(element) {
3153
- const container = element.closest("[data-turbo]");
3154
- const withinFrame = element.closest("turbo-frame");
3240
+ const container = findClosestRecursively(element, "[data-turbo]");
3241
+ const withinFrame = findClosestRecursively(element, "turbo-frame");
3155
3242
  if (this.drive || withinFrame) {
3156
3243
  if (container) {
3157
3244
  return container.getAttribute("data-turbo") != "false";
@@ -3167,8 +3254,7 @@ class Session {
3167
3254
  }
3168
3255
  }
3169
3256
  getActionForLink(link) {
3170
- const action = link.getAttribute("data-turbo-action");
3171
- return isAction(action) ? action : "advance";
3257
+ return getVisitAction(link) || "advance";
3172
3258
  }
3173
3259
  get snapshot() {
3174
3260
  return this.view.snapshot;
@@ -3236,7 +3322,10 @@ const StreamActions = {
3236
3322
  this.targetElements.forEach((e => e.replaceWith(this.templateContent)));
3237
3323
  },
3238
3324
  update() {
3239
- this.targetElements.forEach((e => e.replaceChildren(this.templateContent)));
3325
+ this.targetElements.forEach((targetElement => {
3326
+ targetElement.innerHTML = "";
3327
+ targetElement.append(this.templateContent);
3328
+ }));
3240
3329
  }
3241
3330
  };
3242
3331
 
@@ -3308,6 +3397,8 @@ var Turbo = Object.freeze({
3308
3397
  StreamActions: StreamActions
3309
3398
  });
3310
3399
 
3400
+ class TurboFrameMissingError extends Error {}
3401
+
3311
3402
  class FrameController {
3312
3403
  constructor(element) {
3313
3404
  this.fetchResponseLoaded = _fetchResponse => {};
@@ -3404,31 +3495,20 @@ class FrameController {
3404
3495
  try {
3405
3496
  const html = await fetchResponse.responseHTML;
3406
3497
  if (html) {
3407
- const {body: body} = parseHTMLDocument(html);
3408
- const newFrameElement = await this.extractForeignFrameElement(body);
3409
- if (newFrameElement) {
3410
- const snapshot = new Snapshot(newFrameElement);
3411
- const renderer = new FrameRenderer(this, this.view.snapshot, snapshot, FrameRenderer.renderElement, false, false);
3412
- if (this.view.renderPromise) await this.view.renderPromise;
3413
- this.changeHistory();
3414
- await this.view.render(renderer);
3415
- this.complete = true;
3416
- session.frameRendered(fetchResponse, this.element);
3417
- session.frameLoaded(this.element);
3418
- this.fetchResponseLoaded(fetchResponse);
3419
- } else if (this.willHandleFrameMissingFromResponse(fetchResponse)) {
3420
- console.warn(`A matching frame for #${this.element.id} was missing from the response, transforming into full-page Visit.`);
3421
- this.visitResponse(fetchResponse.response);
3498
+ const document = parseHTMLDocument(html);
3499
+ const pageSnapshot = PageSnapshot.fromDocument(document);
3500
+ if (pageSnapshot.isVisitable) {
3501
+ await this.loadFrameResponse(fetchResponse, document);
3502
+ } else {
3503
+ await this.handleUnvisitableFrameResponse(fetchResponse);
3422
3504
  }
3423
3505
  }
3424
- } catch (error) {
3425
- console.error(error);
3426
- this.view.invalidate();
3427
3506
  } finally {
3428
3507
  this.fetchResponseLoaded = () => {};
3429
3508
  }
3430
3509
  }
3431
- elementAppearedInViewport(_element) {
3510
+ elementAppearedInViewport(element) {
3511
+ this.proposeVisitIfNavigatedWithAction(element, element);
3432
3512
  this.loadSourceURL();
3433
3513
  }
3434
3514
  willSubmitFormLinkToLocation(link) {
@@ -3453,12 +3533,12 @@ class FrameController {
3453
3533
  }
3454
3534
  this.formSubmission = new FormSubmission(this, element, submitter);
3455
3535
  const {fetchRequest: fetchRequest} = this.formSubmission;
3456
- this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
3536
+ this.prepareRequest(fetchRequest);
3457
3537
  this.formSubmission.start();
3458
3538
  }
3459
- prepareHeadersForRequest(headers, request) {
3539
+ prepareRequest(request) {
3460
3540
  var _a;
3461
- headers["Turbo-Frame"] = this.id;
3541
+ request.headers["Turbo-Frame"] = this.id;
3462
3542
  if ((_a = this.currentNavigationElement) === null || _a === void 0 ? void 0 : _a.hasAttribute("data-turbo-stream")) {
3463
3543
  request.acceptResponseType(StreamMessage.contentType);
3464
3544
  }
@@ -3474,7 +3554,6 @@ class FrameController {
3474
3554
  this.resolveVisitPromise();
3475
3555
  }
3476
3556
  async requestFailedWithResponse(request, response) {
3477
- console.error(response);
3478
3557
  await this.loadResponse(response);
3479
3558
  this.resolveVisitPromise();
3480
3559
  }
@@ -3492,9 +3571,13 @@ class FrameController {
3492
3571
  const frame = this.findFrameElement(formSubmission.formElement, formSubmission.submitter);
3493
3572
  frame.delegate.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
3494
3573
  frame.delegate.loadResponse(response);
3574
+ if (!formSubmission.isSafe) {
3575
+ session.clearCache();
3576
+ }
3495
3577
  }
3496
3578
  formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
3497
3579
  this.element.delegate.loadResponse(fetchResponse);
3580
+ session.clearCache();
3498
3581
  }
3499
3582
  formSubmissionErrored(formSubmission, error) {
3500
3583
  console.error(error);
@@ -3524,6 +3607,22 @@ class FrameController {
3524
3607
  willRenderFrame(currentElement, _newElement) {
3525
3608
  this.previousFrameElement = currentElement.cloneNode(true);
3526
3609
  }
3610
+ async loadFrameResponse(fetchResponse, document) {
3611
+ const newFrameElement = await this.extractForeignFrameElement(document.body);
3612
+ if (newFrameElement) {
3613
+ const snapshot = new Snapshot(newFrameElement);
3614
+ const renderer = new FrameRenderer(this, this.view.snapshot, snapshot, FrameRenderer.renderElement, false, false);
3615
+ if (this.view.renderPromise) await this.view.renderPromise;
3616
+ this.changeHistory();
3617
+ await this.view.render(renderer);
3618
+ this.complete = true;
3619
+ session.frameRendered(fetchResponse, this.element);
3620
+ session.frameLoaded(this.element);
3621
+ this.fetchResponseLoaded(fetchResponse);
3622
+ } else if (this.willHandleFrameMissingFromResponse(fetchResponse)) {
3623
+ this.handleFrameMissingFromResponse(fetchResponse);
3624
+ }
3625
+ }
3527
3626
  async visit(url) {
3528
3627
  var _a;
3529
3628
  const request = new FetchRequest(this, FetchMethod.get, url, new URLSearchParams, this.element);
@@ -3540,7 +3639,6 @@ class FrameController {
3540
3639
  }
3541
3640
  navigateFrame(element, url, submitter) {
3542
3641
  const frame = this.findFrameElement(element, submitter);
3543
- this.pageSnapshot = PageSnapshot.fromElement(frame).clone();
3544
3642
  frame.delegate.proposeVisitIfNavigatedWithAction(frame, element, submitter);
3545
3643
  this.withCurrentNavigationElement(element, (() => {
3546
3644
  frame.src = url;
@@ -3548,7 +3646,8 @@ class FrameController {
3548
3646
  }
3549
3647
  proposeVisitIfNavigatedWithAction(frame, element, submitter) {
3550
3648
  this.action = getVisitAction(submitter, element, frame);
3551
- if (isAction(this.action)) {
3649
+ if (this.action) {
3650
+ const pageSnapshot = PageSnapshot.fromElement(frame).clone();
3552
3651
  const {visitCachedSnapshot: visitCachedSnapshot} = frame.delegate;
3553
3652
  frame.delegate.fetchResponseLoaded = fetchResponse => {
3554
3653
  if (frame.src) {
@@ -3565,7 +3664,7 @@ class FrameController {
3565
3664
  willRender: false,
3566
3665
  updateHistory: false,
3567
3666
  restorationIdentifier: this.restorationIdentifier,
3568
- snapshot: this.pageSnapshot
3667
+ snapshot: pageSnapshot
3569
3668
  };
3570
3669
  if (this.action) options.action = this.action;
3571
3670
  session.visit(frame.src, options);
@@ -3579,6 +3678,10 @@ class FrameController {
3579
3678
  session.history.update(method, expandURL(this.element.src || ""), this.restorationIdentifier);
3580
3679
  }
3581
3680
  }
3681
+ async handleUnvisitableFrameResponse(fetchResponse) {
3682
+ console.warn(`The response (${fetchResponse.statusCode}) from <turbo-frame id="${this.element.id}"> is performing a full page visit due to turbo-visit-control.`);
3683
+ await this.visitResponse(fetchResponse.response);
3684
+ }
3582
3685
  willHandleFrameMissingFromResponse(fetchResponse) {
3583
3686
  this.element.setAttribute("complete", "");
3584
3687
  const response = fetchResponse.response;
@@ -3599,6 +3702,14 @@ class FrameController {
3599
3702
  });
3600
3703
  return !event.defaultPrevented;
3601
3704
  }
3705
+ handleFrameMissingFromResponse(fetchResponse) {
3706
+ this.view.missing();
3707
+ this.throwFrameMissingError(fetchResponse);
3708
+ }
3709
+ throwFrameMissingError(fetchResponse) {
3710
+ 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.`;
3711
+ throw new TurboFrameMissingError(message);
3712
+ }
3602
3713
  async visitResponse(response) {
3603
3714
  const wrapped = new FetchResponse(response);
3604
3715
  const responseHTML = await wrapped.responseHTML;
@@ -3993,7 +4104,9 @@ class TurboCableStreamSourceElement extends HTMLElement {
3993
4104
  async connectedCallback() {
3994
4105
  connectStreamSource(this);
3995
4106
  this.subscription = await subscribeTo(this.channel, {
3996
- received: this.dispatchMessageEvent.bind(this)
4107
+ received: this.dispatchMessageEvent.bind(this),
4108
+ connected: this.subscriptionConnected.bind(this),
4109
+ disconnected: this.subscriptionDisconnected.bind(this)
3997
4110
  });
3998
4111
  }
3999
4112
  disconnectedCallback() {
@@ -4006,6 +4119,12 @@ class TurboCableStreamSourceElement extends HTMLElement {
4006
4119
  });
4007
4120
  return this.dispatchEvent(event);
4008
4121
  }
4122
+ subscriptionConnected() {
4123
+ this.setAttribute("connected", "");
4124
+ }
4125
+ subscriptionDisconnected() {
4126
+ this.removeAttribute("connected");
4127
+ }
4009
4128
  get channel() {
4010
4129
  const channel = this.getAttribute("channel");
4011
4130
  const signed_stream_name = this.getAttribute("signed-stream-name");
@@ -4019,18 +4138,21 @@ class TurboCableStreamSourceElement extends HTMLElement {
4019
4138
  }
4020
4139
  }
4021
4140
 
4022
- customElements.define("turbo-cable-stream-source", TurboCableStreamSourceElement);
4141
+ if (customElements.get("turbo-cable-stream-source") === undefined) {
4142
+ customElements.define("turbo-cable-stream-source", TurboCableStreamSourceElement);
4143
+ }
4023
4144
 
4024
4145
  function encodeMethodIntoRequestBody(event) {
4025
4146
  if (event.target instanceof HTMLFormElement) {
4026
4147
  const {target: form, detail: {fetchOptions: fetchOptions}} = event;
4027
4148
  form.addEventListener("turbo:submit-start", (({detail: {formSubmission: {submitter: submitter}}}) => {
4028
- const method = submitter && submitter.formMethod || fetchOptions.body && fetchOptions.body.get("_method") || form.getAttribute("method");
4149
+ const body = isBodyInit(fetchOptions.body) ? fetchOptions.body : new URLSearchParams;
4150
+ const method = determineFetchMethod(submitter, body, form);
4029
4151
  if (!/get/i.test(method)) {
4030
4152
  if (/post/i.test(method)) {
4031
- fetchOptions.body.delete("_method");
4153
+ body.delete("_method");
4032
4154
  } else {
4033
- fetchOptions.body.set("_method", method);
4155
+ body.set("_method", method);
4034
4156
  }
4035
4157
  fetchOptions.method = "post";
4036
4158
  }
@@ -4040,6 +4162,37 @@ function encodeMethodIntoRequestBody(event) {
4040
4162
  }
4041
4163
  }
4042
4164
 
4165
+ function determineFetchMethod(submitter, body, form) {
4166
+ const formMethod = determineFormMethod(submitter);
4167
+ const overrideMethod = body.get("_method");
4168
+ const method = form.getAttribute("method") || "get";
4169
+ if (typeof formMethod == "string") {
4170
+ return formMethod;
4171
+ } else if (typeof overrideMethod == "string") {
4172
+ return overrideMethod;
4173
+ } else {
4174
+ return method;
4175
+ }
4176
+ }
4177
+
4178
+ function determineFormMethod(submitter) {
4179
+ if (submitter instanceof HTMLButtonElement || submitter instanceof HTMLInputElement) {
4180
+ if (submitter.name === "_method") {
4181
+ return submitter.value;
4182
+ } else if (submitter.hasAttribute("formmethod")) {
4183
+ return submitter.formMethod;
4184
+ } else {
4185
+ return null;
4186
+ }
4187
+ } else {
4188
+ return null;
4189
+ }
4190
+ }
4191
+
4192
+ function isBodyInit(body) {
4193
+ return body instanceof FormData || body instanceof URLSearchParams;
4194
+ }
4195
+
4043
4196
  addEventListener("turbo:before-fetch-request", encodeMethodIntoRequestBody);
4044
4197
 
4045
4198
  var adapters = {