turbo-rails 1.1.1 → 1.4.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.
@@ -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", "loading", "src" ];
92
- }
93
91
  connectedCallback() {
94
92
  this.delegate.connect();
95
93
  }
@@ -97,13 +95,13 @@ class FrameElement extends HTMLElement {
97
95
  this.delegate.disconnect();
98
96
  }
99
97
  reload() {
100
- const {src: src} = this;
101
- this.src = null;
102
- this.src = src;
98
+ return this.delegate.sourceURLReloaded();
103
99
  }
104
100
  attributeChangedCallback(name) {
105
101
  if (name == "loading") {
106
102
  this.delegate.loadingStyleChanged();
103
+ } else if (name == "complete") {
104
+ this.delegate.completeChanged();
107
105
  } else if (name == "src") {
108
106
  this.delegate.sourceURLChanged();
109
107
  } else {
@@ -195,7 +193,7 @@ function getExtension(url) {
195
193
  }
196
194
 
197
195
  function isHTML(url) {
198
- return !!getExtension(url).match(/^(?:|\.(?:htm|html|xhtml))$/);
196
+ return !!getExtension(url).match(/^(?:|\.(?:htm|html|xhtml|php))$/);
199
197
  }
200
198
 
201
199
  function isPrefixedBy(baseURL, url) {
@@ -282,10 +280,39 @@ class FetchResponse {
282
280
  }
283
281
  }
284
282
 
283
+ function activateScriptElement(element) {
284
+ if (element.getAttribute("data-turbo-eval") == "false") {
285
+ return element;
286
+ } else {
287
+ const createdScriptElement = document.createElement("script");
288
+ const cspNonce = getMetaContent("csp-nonce");
289
+ if (cspNonce) {
290
+ createdScriptElement.nonce = cspNonce;
291
+ }
292
+ createdScriptElement.textContent = element.textContent;
293
+ createdScriptElement.async = false;
294
+ copyElementAttributes(createdScriptElement, element);
295
+ return createdScriptElement;
296
+ }
297
+ }
298
+
299
+ function copyElementAttributes(destinationElement, sourceElement) {
300
+ for (const {name: name, value: value} of sourceElement.attributes) {
301
+ destinationElement.setAttribute(name, value);
302
+ }
303
+ }
304
+
305
+ function createDocumentFragment(html) {
306
+ const template = document.createElement("template");
307
+ template.innerHTML = html;
308
+ return template.content;
309
+ }
310
+
285
311
  function dispatch(eventName, {target: target, cancelable: cancelable, detail: detail} = {}) {
286
312
  const event = new CustomEvent(eventName, {
287
313
  cancelable: cancelable,
288
314
  bubbles: true,
315
+ composed: true,
289
316
  detail: detail
290
317
  });
291
318
  if (target && target.isConnected) {
@@ -327,7 +354,7 @@ function interpolate(strings, values) {
327
354
  }
328
355
 
329
356
  function uuid() {
330
- return Array.apply(null, {
357
+ return Array.from({
331
358
  length: 36
332
359
  }).map(((_, i) => {
333
360
  if (i == 8 || i == 13 || i == 18 || i == 23) {
@@ -349,6 +376,10 @@ function getAttribute(attributeName, ...elements) {
349
376
  return null;
350
377
  }
351
378
 
379
+ function hasAttribute(attributeName, ...elements) {
380
+ return elements.some((element => element && element.hasAttribute(attributeName)));
381
+ }
382
+
352
383
  function markAsBusy(...elements) {
353
384
  for (const element of elements) {
354
385
  if (element.localName == "turbo-frame") {
@@ -367,6 +398,70 @@ function clearBusyState(...elements) {
367
398
  }
368
399
  }
369
400
 
401
+ function waitForLoad(element, timeoutInMilliseconds = 2e3) {
402
+ return new Promise((resolve => {
403
+ const onComplete = () => {
404
+ element.removeEventListener("error", onComplete);
405
+ element.removeEventListener("load", onComplete);
406
+ resolve();
407
+ };
408
+ element.addEventListener("load", onComplete, {
409
+ once: true
410
+ });
411
+ element.addEventListener("error", onComplete, {
412
+ once: true
413
+ });
414
+ setTimeout(resolve, timeoutInMilliseconds);
415
+ }));
416
+ }
417
+
418
+ function getHistoryMethodForAction(action) {
419
+ switch (action) {
420
+ case "replace":
421
+ return history.replaceState;
422
+
423
+ case "advance":
424
+ case "restore":
425
+ return history.pushState;
426
+ }
427
+ }
428
+
429
+ function isAction(action) {
430
+ return action == "advance" || action == "replace" || action == "restore";
431
+ }
432
+
433
+ function getVisitAction(...elements) {
434
+ const action = getAttribute("data-turbo-action", ...elements);
435
+ return isAction(action) ? action : null;
436
+ }
437
+
438
+ function getMetaElement(name) {
439
+ return document.querySelector(`meta[name="${name}"]`);
440
+ }
441
+
442
+ function getMetaContent(name) {
443
+ const element = getMetaElement(name);
444
+ return element && element.content;
445
+ }
446
+
447
+ function setMetaContent(name, content) {
448
+ let element = getMetaElement(name);
449
+ if (!element) {
450
+ element = document.createElement("meta");
451
+ element.setAttribute("name", name);
452
+ document.head.appendChild(element);
453
+ }
454
+ element.setAttribute("content", content);
455
+ return element;
456
+ }
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
+
370
465
  var FetchMethod;
371
466
 
372
467
  (function(FetchMethod) {
@@ -399,7 +494,7 @@ function fetchMethodFromString(method) {
399
494
  class FetchRequest {
400
495
  constructor(delegate, method, location, body = new URLSearchParams, target = null) {
401
496
  this.abortController = new AbortController;
402
- this.resolveRequestPromise = value => {};
497
+ this.resolveRequestPromise = _value => {};
403
498
  this.delegate = delegate;
404
499
  this.method = method;
405
500
  this.headers = this.defaultHeaders;
@@ -420,9 +515,8 @@ class FetchRequest {
420
515
  this.abortController.abort();
421
516
  }
422
517
  async perform() {
423
- var _a, _b;
424
518
  const {fetchOptions: fetchOptions} = this;
425
- (_b = (_a = this.delegate).prepareHeadersForRequest) === null || _b === void 0 ? void 0 : _b.call(_a, this.headers, this);
519
+ this.delegate.prepareRequest(this);
426
520
  await this.allowRequestToBeIntercepted(fetchOptions);
427
521
  try {
428
522
  this.delegate.requestStarted(this);
@@ -430,7 +524,9 @@ class FetchRequest {
430
524
  return await this.receive(response);
431
525
  } catch (error) {
432
526
  if (error.name !== "AbortError") {
433
- this.delegate.requestErrored(this, error);
527
+ if (this.willDelegateErrorHandling(error)) {
528
+ this.delegate.requestErrored(this, error);
529
+ }
434
530
  throw error;
435
531
  }
436
532
  } finally {
@@ -462,7 +558,7 @@ class FetchRequest {
462
558
  credentials: "same-origin",
463
559
  headers: this.headers,
464
560
  redirect: "follow",
465
- body: this.isIdempotent ? null : this.body,
561
+ body: this.isSafe ? null : this.body,
466
562
  signal: this.abortSignal,
467
563
  referrer: (_a = this.delegate.referrer) === null || _a === void 0 ? void 0 : _a.href
468
564
  };
@@ -472,12 +568,15 @@ class FetchRequest {
472
568
  Accept: "text/html, application/xhtml+xml"
473
569
  };
474
570
  }
475
- get isIdempotent() {
476
- return this.method == FetchMethod.get;
571
+ get isSafe() {
572
+ return this.method === FetchMethod.get;
477
573
  }
478
574
  get abortSignal() {
479
575
  return this.abortController.signal;
480
576
  }
577
+ acceptResponseType(mimeType) {
578
+ this.headers["Accept"] = [ mimeType, this.headers["Accept"] ].join(", ");
579
+ }
481
580
  async allowRequestToBeIntercepted(fetchOptions) {
482
581
  const requestInterception = new Promise((resolve => this.resolveRequestPromise = resolve));
483
582
  const event = dispatch("turbo:before-fetch-request", {
@@ -491,6 +590,17 @@ class FetchRequest {
491
590
  });
492
591
  if (event.defaultPrevented) await requestInterception;
493
592
  }
593
+ willDelegateErrorHandling(error) {
594
+ const event = dispatch("turbo:fetch-request-error", {
595
+ target: this.target,
596
+ cancelable: true,
597
+ detail: {
598
+ request: this,
599
+ error: error
600
+ }
601
+ });
602
+ return !event.defaultPrevented;
603
+ }
494
604
  }
495
605
 
496
606
  class AppearanceObserver {
@@ -521,40 +631,31 @@ class AppearanceObserver {
521
631
  }
522
632
 
523
633
  class StreamMessage {
524
- constructor(html) {
525
- this.templateElement = document.createElement("template");
526
- this.templateElement.innerHTML = html;
527
- }
528
634
  static wrap(message) {
529
635
  if (typeof message == "string") {
530
- return new this(message);
636
+ return new this(createDocumentFragment(message));
531
637
  } else {
532
638
  return message;
533
639
  }
534
640
  }
535
- get fragment() {
536
- const fragment = document.createDocumentFragment();
537
- for (const element of this.foreignElements) {
538
- fragment.appendChild(document.importNode(element, true));
539
- }
540
- return fragment;
541
- }
542
- get foreignElements() {
543
- return this.templateChildren.reduce(((streamElements, child) => {
544
- if (child.tagName.toLowerCase() == "turbo-stream") {
545
- return [ ...streamElements, child ];
546
- } else {
547
- return streamElements;
548
- }
549
- }), []);
550
- }
551
- get templateChildren() {
552
- return Array.from(this.templateElement.content.children);
641
+ constructor(fragment) {
642
+ this.fragment = importStreamElements(fragment);
553
643
  }
554
644
  }
555
645
 
556
646
  StreamMessage.contentType = "text/vnd.turbo-stream.html";
557
647
 
648
+ function importStreamElements(fragment) {
649
+ for (const element of fragment.querySelectorAll("turbo-stream")) {
650
+ const streamElement = document.importNode(element, true);
651
+ for (const inertScriptElement of streamElement.templateElement.content.querySelectorAll("script")) {
652
+ inertScriptElement.replaceWith(activateScriptElement(inertScriptElement));
653
+ }
654
+ element.replaceWith(streamElement);
655
+ }
656
+ return fragment;
657
+ }
658
+
558
659
  var FormSubmissionState;
559
660
 
560
661
  (function(FormSubmissionState) {
@@ -588,6 +689,9 @@ function formEnctypeFromString(encoding) {
588
689
  }
589
690
 
590
691
  class FormSubmission {
692
+ static confirmMethod(message, _element, _submitter) {
693
+ return Promise.resolve(confirm(message));
694
+ }
591
695
  constructor(delegate, formElement, submitter, mustRedirect = false) {
592
696
  this.state = FormSubmissionState.initialized;
593
697
  this.delegate = delegate;
@@ -601,9 +705,6 @@ class FormSubmission {
601
705
  this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body, this.formElement);
602
706
  this.mustRedirect = mustRedirect;
603
707
  }
604
- static confirmMethod(message, element) {
605
- return confirm(message);
606
- }
607
708
  get method() {
608
709
  var _a;
609
710
  const method = ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formmethod")) || this.formElement.getAttribute("method") || "";
@@ -612,7 +713,11 @@ class FormSubmission {
612
713
  get action() {
613
714
  var _a;
614
715
  const formElementAction = typeof this.formElement.action === "string" ? this.formElement.action : null;
615
- return ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formaction")) || this.formElement.getAttribute("action") || formElementAction || "";
716
+ if ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.hasAttribute("formaction")) {
717
+ return this.submitter.getAttribute("formaction") || "";
718
+ } else {
719
+ return this.formElement.getAttribute("action") || formElementAction || "";
720
+ }
616
721
  }
617
722
  get body() {
618
723
  if (this.enctype == FormEnctype.urlEncoded || this.method == FetchMethod.get) {
@@ -625,22 +730,17 @@ class FormSubmission {
625
730
  var _a;
626
731
  return formEnctypeFromString(((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formenctype")) || this.formElement.enctype);
627
732
  }
628
- get isIdempotent() {
629
- return this.fetchRequest.isIdempotent;
733
+ get isSafe() {
734
+ return this.fetchRequest.isSafe;
630
735
  }
631
736
  get stringFormData() {
632
737
  return [ ...this.formData ].reduce(((entries, [name, value]) => entries.concat(typeof value == "string" ? [ [ name, value ] ] : [])), []);
633
738
  }
634
- get confirmationMessage() {
635
- return this.formElement.getAttribute("data-turbo-confirm");
636
- }
637
- get needsConfirmation() {
638
- return this.confirmationMessage !== null;
639
- }
640
739
  async start() {
641
740
  const {initialized: initialized, requesting: requesting} = FormSubmissionState;
642
- if (this.needsConfirmation) {
643
- const answer = FormSubmission.confirmMethod(this.confirmationMessage, this.formElement);
741
+ const confirmationMessage = getAttribute("data-turbo-confirm", this.submitter, this.formElement);
742
+ if (typeof confirmationMessage === "string") {
743
+ const answer = await FormSubmission.confirmMethod(confirmationMessage, this.formElement, this.submitter);
644
744
  if (!answer) {
645
745
  return;
646
746
  }
@@ -658,19 +758,22 @@ class FormSubmission {
658
758
  return true;
659
759
  }
660
760
  }
661
- prepareHeadersForRequest(headers, request) {
662
- if (!request.isIdempotent) {
761
+ prepareRequest(request) {
762
+ if (!request.isSafe) {
663
763
  const token = getCookieValue(getMetaContent("csrf-param")) || getMetaContent("csrf-token");
664
764
  if (token) {
665
- headers["X-CSRF-Token"] = token;
765
+ request.headers["X-CSRF-Token"] = token;
666
766
  }
667
- headers["Accept"] = [ StreamMessage.contentType, headers["Accept"] ].join(", ");
767
+ }
768
+ if (this.requestAcceptsTurboStreamResponse(request)) {
769
+ request.acceptResponseType(StreamMessage.contentType);
668
770
  }
669
771
  }
670
- requestStarted(request) {
772
+ requestStarted(_request) {
671
773
  var _a;
672
774
  this.state = FormSubmissionState.waiting;
673
775
  (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.setAttribute("disabled", "");
776
+ this.setSubmitsWith();
674
777
  dispatch("turbo:submit-start", {
675
778
  target: this.formElement,
676
779
  detail: {
@@ -714,10 +817,11 @@ class FormSubmission {
714
817
  };
715
818
  this.delegate.formSubmissionErrored(this, error);
716
819
  }
717
- requestFinished(request) {
820
+ requestFinished(_request) {
718
821
  var _a;
719
822
  this.state = FormSubmissionState.stopped;
720
823
  (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.removeAttribute("disabled");
824
+ this.resetSubmitterText();
721
825
  dispatch("turbo:submit-end", {
722
826
  target: this.formElement,
723
827
  detail: Object.assign({
@@ -726,8 +830,35 @@ class FormSubmission {
726
830
  });
727
831
  this.delegate.formSubmissionFinished(this);
728
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
+ }
729
853
  requestMustRedirect(request) {
730
- return !request.isIdempotent && this.mustRedirect;
854
+ return !request.isSafe && this.mustRedirect;
855
+ }
856
+ requestAcceptsTurboStreamResponse(request) {
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");
731
862
  }
732
863
  }
733
864
 
@@ -735,8 +866,8 @@ function buildFormData(formElement, submitter) {
735
866
  const formData = new FormData(formElement);
736
867
  const name = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("name");
737
868
  const value = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("value");
738
- if (name && value != null && formData.get(name) != value) {
739
- formData.append(name, value);
869
+ if (name) {
870
+ formData.append(name, value || "");
740
871
  }
741
872
  return formData;
742
873
  }
@@ -752,11 +883,6 @@ function getCookieValue(cookieName) {
752
883
  }
753
884
  }
754
885
 
755
- function getMetaContent(name) {
756
- const element = document.querySelector(`meta[name="${name}"]`);
757
- return element && element.content;
758
- }
759
-
760
886
  function responseSucceededWithoutRedirect(response) {
761
887
  return response.statusCode == 200 && !response.redirected;
762
888
  }
@@ -775,6 +901,9 @@ class Snapshot {
775
901
  constructor(element) {
776
902
  this.element = element;
777
903
  }
904
+ get activeElement() {
905
+ return this.element.ownerDocument.activeElement;
906
+ }
778
907
  get children() {
779
908
  return [ ...this.element.children ];
780
909
  }
@@ -788,13 +917,17 @@ class Snapshot {
788
917
  return this.element.isConnected;
789
918
  }
790
919
  get firstAutofocusableElement() {
791
- return this.element.querySelector("[autofocus]");
920
+ const inertDisabledOrHidden = "[inert], :disabled, [hidden], details:not([open]), dialog:not([open])";
921
+ for (const element of this.element.querySelectorAll("[autofocus]")) {
922
+ if (element.closest(inertDisabledOrHidden) == null) return element; else continue;
923
+ }
924
+ return null;
792
925
  }
793
926
  get permanentElements() {
794
- return [ ...this.element.querySelectorAll("[id][data-turbo-permanent]") ];
927
+ return queryPermanentElementsAll(this.element);
795
928
  }
796
929
  getPermanentElementById(id) {
797
- return this.element.querySelector(`#${id}[data-turbo-permanent]`);
930
+ return getPermanentElementById(this.element, id);
798
931
  }
799
932
  getPermanentElementMapForSnapshot(snapshot) {
800
933
  const permanentElementMap = {};
@@ -809,35 +942,70 @@ class Snapshot {
809
942
  }
810
943
  }
811
944
 
812
- class FormInterceptor {
813
- constructor(delegate, element) {
945
+ function getPermanentElementById(node, id) {
946
+ return node.querySelector(`#${id}[data-turbo-permanent]`);
947
+ }
948
+
949
+ function queryPermanentElementsAll(node) {
950
+ return node.querySelectorAll("[id][data-turbo-permanent]");
951
+ }
952
+
953
+ class FormSubmitObserver {
954
+ constructor(delegate, eventTarget) {
955
+ this.started = false;
956
+ this.submitCaptured = () => {
957
+ this.eventTarget.removeEventListener("submit", this.submitBubbled, false);
958
+ this.eventTarget.addEventListener("submit", this.submitBubbled, false);
959
+ };
814
960
  this.submitBubbled = event => {
815
- const form = event.target;
816
- if (!event.defaultPrevented && form instanceof HTMLFormElement && form.closest("turbo-frame, html") == this.element) {
961
+ if (!event.defaultPrevented) {
962
+ const form = event.target instanceof HTMLFormElement ? event.target : undefined;
817
963
  const submitter = event.submitter || undefined;
818
- const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.method;
819
- if (method != "dialog" && this.delegate.shouldInterceptFormSubmission(form, submitter)) {
964
+ if (form && submissionDoesNotDismissDialog(form, submitter) && submissionDoesNotTargetIFrame(form, submitter) && this.delegate.willSubmitForm(form, submitter)) {
820
965
  event.preventDefault();
821
966
  event.stopImmediatePropagation();
822
- this.delegate.formSubmissionIntercepted(form, submitter);
967
+ this.delegate.formSubmitted(form, submitter);
823
968
  }
824
969
  }
825
970
  };
826
971
  this.delegate = delegate;
827
- this.element = element;
972
+ this.eventTarget = eventTarget;
828
973
  }
829
974
  start() {
830
- this.element.addEventListener("submit", this.submitBubbled);
975
+ if (!this.started) {
976
+ this.eventTarget.addEventListener("submit", this.submitCaptured, true);
977
+ this.started = true;
978
+ }
831
979
  }
832
980
  stop() {
833
- this.element.removeEventListener("submit", this.submitBubbled);
981
+ if (this.started) {
982
+ this.eventTarget.removeEventListener("submit", this.submitCaptured, true);
983
+ this.started = false;
984
+ }
985
+ }
986
+ }
987
+
988
+ function submissionDoesNotDismissDialog(form, submitter) {
989
+ const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.getAttribute("method");
990
+ return method != "dialog";
991
+ }
992
+
993
+ function submissionDoesNotTargetIFrame(form, submitter) {
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;
834
1002
  }
835
1003
  }
836
1004
 
837
1005
  class View {
838
1006
  constructor(delegate, element) {
839
- this.resolveRenderPromise = value => {};
840
- this.resolveInterceptionPromise = value => {};
1007
+ this.resolveRenderPromise = _value => {};
1008
+ this.resolveInterceptionPromise = _value => {};
841
1009
  this.delegate = delegate;
842
1010
  this.element = element;
843
1011
  }
@@ -888,12 +1056,17 @@ class View {
888
1056
  try {
889
1057
  this.renderPromise = new Promise((resolve => this.resolveRenderPromise = resolve));
890
1058
  this.renderer = renderer;
891
- this.prepareToRenderSnapshot(renderer);
1059
+ await this.prepareToRenderSnapshot(renderer);
892
1060
  const renderInterception = new Promise((resolve => this.resolveInterceptionPromise = resolve));
893
- const immediateRender = this.delegate.allowsImmediateRender(snapshot, this.resolveInterceptionPromise);
1061
+ const options = {
1062
+ resume: this.resolveInterceptionPromise,
1063
+ render: this.renderer.renderElement
1064
+ };
1065
+ const immediateRender = this.delegate.allowsImmediateRender(snapshot, options);
894
1066
  if (!immediateRender) await renderInterception;
895
1067
  await this.renderSnapshot(renderer);
896
1068
  this.delegate.viewRenderedSnapshot(snapshot, isPreview);
1069
+ this.delegate.preloadOnLoadLinksForView(this.element);
897
1070
  this.finishRenderingSnapshot(renderer);
898
1071
  } finally {
899
1072
  delete this.renderer;
@@ -901,15 +1074,15 @@ class View {
901
1074
  delete this.renderPromise;
902
1075
  }
903
1076
  } else {
904
- this.invalidate();
1077
+ this.invalidate(renderer.reloadReason);
905
1078
  }
906
1079
  }
907
- invalidate() {
908
- this.delegate.viewInvalidated();
1080
+ invalidate(reason) {
1081
+ this.delegate.viewInvalidated(reason);
909
1082
  }
910
- prepareToRenderSnapshot(renderer) {
1083
+ async prepareToRenderSnapshot(renderer) {
911
1084
  this.markAsPreview(renderer.isPreview);
912
- renderer.prepareToRender();
1085
+ await renderer.prepareToRender();
913
1086
  }
914
1087
  markAsPreview(isPreview) {
915
1088
  if (isPreview) {
@@ -927,8 +1100,8 @@ class View {
927
1100
  }
928
1101
 
929
1102
  class FrameView extends View {
930
- invalidate() {
931
- this.element.innerHTML = "";
1103
+ missing() {
1104
+ this.element.innerHTML = `<strong class="turbo-frame-error">Content missing</strong>`;
932
1105
  }
933
1106
  get snapshot() {
934
1107
  return new Snapshot(this.element);
@@ -946,15 +1119,15 @@ class LinkInterceptor {
946
1119
  };
947
1120
  this.linkClicked = event => {
948
1121
  if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) {
949
- if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url)) {
1122
+ if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url, event.detail.originalEvent)) {
950
1123
  this.clickEvent.preventDefault();
951
1124
  event.preventDefault();
952
- this.delegate.linkClickIntercepted(event.target, event.detail.url);
1125
+ this.delegate.linkClickIntercepted(event.target, event.detail.url, event.detail.originalEvent);
953
1126
  }
954
1127
  }
955
1128
  delete this.clickEvent;
956
1129
  };
957
- this.willVisit = () => {
1130
+ this.willVisit = _event => {
958
1131
  delete this.clickEvent;
959
1132
  };
960
1133
  this.delegate = delegate;
@@ -976,19 +1149,127 @@ class LinkInterceptor {
976
1149
  }
977
1150
  }
978
1151
 
979
- class Bardo {
980
- constructor(permanentElementMap) {
981
- this.permanentElementMap = permanentElementMap;
1152
+ class LinkClickObserver {
1153
+ constructor(delegate, eventTarget) {
1154
+ this.started = false;
1155
+ this.clickCaptured = () => {
1156
+ this.eventTarget.removeEventListener("click", this.clickBubbled, false);
1157
+ this.eventTarget.addEventListener("click", this.clickBubbled, false);
1158
+ };
1159
+ this.clickBubbled = event => {
1160
+ if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {
1161
+ const target = event.composedPath && event.composedPath()[0] || event.target;
1162
+ const link = this.findLinkFromClickTarget(target);
1163
+ if (link && doesNotTargetIFrame(link)) {
1164
+ const location = this.getLocationForLink(link);
1165
+ if (this.delegate.willFollowLinkToLocation(link, location, event)) {
1166
+ event.preventDefault();
1167
+ this.delegate.followedLinkToLocation(link, location);
1168
+ }
1169
+ }
1170
+ }
1171
+ };
1172
+ this.delegate = delegate;
1173
+ this.eventTarget = eventTarget;
1174
+ }
1175
+ start() {
1176
+ if (!this.started) {
1177
+ this.eventTarget.addEventListener("click", this.clickCaptured, true);
1178
+ this.started = true;
1179
+ }
1180
+ }
1181
+ stop() {
1182
+ if (this.started) {
1183
+ this.eventTarget.removeEventListener("click", this.clickCaptured, true);
1184
+ this.started = false;
1185
+ }
1186
+ }
1187
+ clickEventIsSignificant(event) {
1188
+ return !(event.target && event.target.isContentEditable || event.defaultPrevented || event.which > 1 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey);
1189
+ }
1190
+ findLinkFromClickTarget(target) {
1191
+ return findClosestRecursively(target, "a[href]:not([target^=_]):not([download])");
1192
+ }
1193
+ getLocationForLink(link) {
1194
+ return expandURL(link.getAttribute("href") || "");
1195
+ }
1196
+ }
1197
+
1198
+ function doesNotTargetIFrame(anchor) {
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;
1206
+ }
1207
+ }
1208
+
1209
+ class FormLinkClickObserver {
1210
+ constructor(delegate, element) {
1211
+ this.delegate = delegate;
1212
+ this.linkInterceptor = new LinkClickObserver(this, element);
1213
+ }
1214
+ start() {
1215
+ this.linkInterceptor.start();
1216
+ }
1217
+ stop() {
1218
+ this.linkInterceptor.stop();
1219
+ }
1220
+ willFollowLinkToLocation(link, location, originalEvent) {
1221
+ return this.delegate.willSubmitFormLinkToLocation(link, location, originalEvent) && link.hasAttribute("data-turbo-method");
1222
+ }
1223
+ followedLinkToLocation(link, location) {
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
+ });
1236
+ form.setAttribute("data-turbo", "true");
1237
+ form.setAttribute("action", action.href);
1238
+ form.setAttribute("hidden", "");
1239
+ const method = link.getAttribute("data-turbo-method");
1240
+ if (method) form.setAttribute("method", method);
1241
+ const turboFrame = link.getAttribute("data-turbo-frame");
1242
+ if (turboFrame) form.setAttribute("data-turbo-frame", turboFrame);
1243
+ const turboAction = getVisitAction(link);
1244
+ if (turboAction) form.setAttribute("data-turbo-action", turboAction);
1245
+ const turboConfirm = link.getAttribute("data-turbo-confirm");
1246
+ if (turboConfirm) form.setAttribute("data-turbo-confirm", turboConfirm);
1247
+ const turboStream = link.hasAttribute("data-turbo-stream");
1248
+ if (turboStream) form.setAttribute("data-turbo-stream", "");
1249
+ this.delegate.submittedFormLinkToLocation(link, location, form);
1250
+ document.body.appendChild(form);
1251
+ form.addEventListener("turbo:submit-end", (() => form.remove()), {
1252
+ once: true
1253
+ });
1254
+ requestAnimationFrame((() => form.requestSubmit()));
982
1255
  }
983
- static preservingPermanentElements(permanentElementMap, callback) {
984
- const bardo = new this(permanentElementMap);
1256
+ }
1257
+
1258
+ class Bardo {
1259
+ static async preservingPermanentElements(delegate, permanentElementMap, callback) {
1260
+ const bardo = new this(delegate, permanentElementMap);
985
1261
  bardo.enter();
986
- callback();
1262
+ await callback();
987
1263
  bardo.leave();
988
1264
  }
1265
+ constructor(delegate, permanentElementMap) {
1266
+ this.delegate = delegate;
1267
+ this.permanentElementMap = permanentElementMap;
1268
+ }
989
1269
  enter() {
990
1270
  for (const id in this.permanentElementMap) {
991
- const [, newPermanentElement] = this.permanentElementMap[id];
1271
+ const [currentPermanentElement, newPermanentElement] = this.permanentElementMap[id];
1272
+ this.delegate.enteringBardo(currentPermanentElement, newPermanentElement);
992
1273
  this.replaceNewPermanentElementWithPlaceholder(newPermanentElement);
993
1274
  }
994
1275
  }
@@ -997,6 +1278,7 @@ class Bardo {
997
1278
  const [currentPermanentElement] = this.permanentElementMap[id];
998
1279
  this.replaceCurrentPermanentElementWithClone(currentPermanentElement);
999
1280
  this.replacePlaceholderWithPermanentElement(currentPermanentElement);
1281
+ this.delegate.leavingBardo(currentPermanentElement);
1000
1282
  }
1001
1283
  }
1002
1284
  replaceNewPermanentElementWithPlaceholder(permanentElement) {
@@ -1027,11 +1309,13 @@ function createPlaceholderForPermanentElement(permanentElement) {
1027
1309
  }
1028
1310
 
1029
1311
  class Renderer {
1030
- constructor(currentSnapshot, newSnapshot, isPreview, willRender = true) {
1312
+ constructor(currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) {
1313
+ this.activeElement = null;
1031
1314
  this.currentSnapshot = currentSnapshot;
1032
1315
  this.newSnapshot = newSnapshot;
1033
1316
  this.isPreview = isPreview;
1034
1317
  this.willRender = willRender;
1318
+ this.renderElement = renderElement;
1035
1319
  this.promise = new Promise(((resolve, reject) => this.resolvingFunctions = {
1036
1320
  resolve: resolve,
1037
1321
  reject: reject
@@ -1040,6 +1324,9 @@ class Renderer {
1040
1324
  get shouldRender() {
1041
1325
  return true;
1042
1326
  }
1327
+ get reloadReason() {
1328
+ return;
1329
+ }
1043
1330
  prepareToRender() {
1044
1331
  return;
1045
1332
  }
@@ -1049,22 +1336,8 @@ class Renderer {
1049
1336
  delete this.resolvingFunctions;
1050
1337
  }
1051
1338
  }
1052
- createScriptElement(element) {
1053
- if (element.getAttribute("data-turbo-eval") == "false") {
1054
- return element;
1055
- } else {
1056
- const createdScriptElement = document.createElement("script");
1057
- if (this.cspNonce) {
1058
- createdScriptElement.nonce = this.cspNonce;
1059
- }
1060
- createdScriptElement.textContent = element.textContent;
1061
- createdScriptElement.async = false;
1062
- copyElementAttributes(createdScriptElement, element);
1063
- return createdScriptElement;
1064
- }
1065
- }
1066
- preservingPermanentElements(callback) {
1067
- Bardo.preservingPermanentElements(this.permanentElementMap, callback);
1339
+ async preservingPermanentElements(callback) {
1340
+ await Bardo.preservingPermanentElements(this, this.permanentElementMap, callback);
1068
1341
  }
1069
1342
  focusFirstAutofocusableElement() {
1070
1343
  const element = this.connectedSnapshot.firstAutofocusableElement;
@@ -1072,6 +1345,18 @@ class Renderer {
1072
1345
  element.focus();
1073
1346
  }
1074
1347
  }
1348
+ enteringBardo(currentPermanentElement) {
1349
+ if (this.activeElement) return;
1350
+ if (currentPermanentElement.contains(this.currentSnapshot.activeElement)) {
1351
+ this.activeElement = this.currentSnapshot.activeElement;
1352
+ }
1353
+ }
1354
+ leavingBardo(currentPermanentElement) {
1355
+ if (currentPermanentElement.contains(this.activeElement) && this.activeElement instanceof HTMLElement) {
1356
+ this.activeElement.focus();
1357
+ this.activeElement = null;
1358
+ }
1359
+ }
1075
1360
  get connectedSnapshot() {
1076
1361
  return this.newSnapshot.isConnected ? this.newSnapshot : this.currentSnapshot;
1077
1362
  }
@@ -1084,16 +1369,6 @@ class Renderer {
1084
1369
  get permanentElementMap() {
1085
1370
  return this.currentSnapshot.getPermanentElementMapForSnapshot(this.newSnapshot);
1086
1371
  }
1087
- get cspNonce() {
1088
- var _a;
1089
- return (_a = document.head.querySelector('meta[name="csp-nonce"]')) === null || _a === void 0 ? void 0 : _a.getAttribute("content");
1090
- }
1091
- }
1092
-
1093
- function copyElementAttributes(destinationElement, sourceElement) {
1094
- for (const {name: name, value: value} of [ ...sourceElement.attributes ]) {
1095
- destinationElement.setAttribute(name, value);
1096
- }
1097
1372
  }
1098
1373
 
1099
1374
  function elementIsFocusable(element) {
@@ -1101,6 +1376,22 @@ function elementIsFocusable(element) {
1101
1376
  }
1102
1377
 
1103
1378
  class FrameRenderer extends Renderer {
1379
+ static renderElement(currentElement, newElement) {
1380
+ var _a;
1381
+ const destinationRange = document.createRange();
1382
+ destinationRange.selectNodeContents(currentElement);
1383
+ destinationRange.deleteContents();
1384
+ const frameElement = newElement;
1385
+ const sourceRange = (_a = frameElement.ownerDocument) === null || _a === void 0 ? void 0 : _a.createRange();
1386
+ if (sourceRange) {
1387
+ sourceRange.selectNodeContents(frameElement);
1388
+ currentElement.appendChild(sourceRange.extractContents());
1389
+ }
1390
+ }
1391
+ constructor(delegate, currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) {
1392
+ super(currentSnapshot, newSnapshot, renderElement, isPreview, willRender);
1393
+ this.delegate = delegate;
1394
+ }
1104
1395
  get shouldRender() {
1105
1396
  return true;
1106
1397
  }
@@ -1116,24 +1407,18 @@ class FrameRenderer extends Renderer {
1116
1407
  this.activateScriptElements();
1117
1408
  }
1118
1409
  loadFrameElement() {
1119
- var _a;
1120
- const destinationRange = document.createRange();
1121
- destinationRange.selectNodeContents(this.currentElement);
1122
- destinationRange.deleteContents();
1123
- const frameElement = this.newElement;
1124
- const sourceRange = (_a = frameElement.ownerDocument) === null || _a === void 0 ? void 0 : _a.createRange();
1125
- if (sourceRange) {
1126
- sourceRange.selectNodeContents(frameElement);
1127
- this.currentElement.appendChild(sourceRange.extractContents());
1128
- }
1410
+ this.delegate.willRenderFrame(this.currentElement, this.newElement);
1411
+ this.renderElement(this.currentElement, this.newElement);
1129
1412
  }
1130
1413
  scrollFrameIntoView() {
1131
1414
  if (this.currentElement.autoscroll || this.newElement.autoscroll) {
1132
1415
  const element = this.currentElement.firstElementChild;
1133
1416
  const block = readScrollLogicalPosition(this.currentElement.getAttribute("data-autoscroll-block"), "end");
1417
+ const behavior = readScrollBehavior(this.currentElement.getAttribute("data-autoscroll-behavior"), "auto");
1134
1418
  if (element) {
1135
1419
  element.scrollIntoView({
1136
- block: block
1420
+ block: block,
1421
+ behavior: behavior
1137
1422
  });
1138
1423
  return true;
1139
1424
  }
@@ -1142,7 +1427,7 @@ class FrameRenderer extends Renderer {
1142
1427
  }
1143
1428
  activateScriptElements() {
1144
1429
  for (const inertScriptElement of this.newScriptElements) {
1145
- const activatedScriptElement = this.createScriptElement(inertScriptElement);
1430
+ const activatedScriptElement = activateScriptElement(inertScriptElement);
1146
1431
  inertScriptElement.replaceWith(activatedScriptElement);
1147
1432
  }
1148
1433
  }
@@ -1159,19 +1444,15 @@ function readScrollLogicalPosition(value, defaultValue) {
1159
1444
  }
1160
1445
  }
1161
1446
 
1162
- class ProgressBar {
1163
- constructor() {
1164
- this.hiding = false;
1165
- this.value = 0;
1166
- this.visible = false;
1167
- this.trickle = () => {
1168
- this.setValue(this.value + Math.random() / 100);
1169
- };
1170
- this.stylesheetElement = this.createStylesheetElement();
1171
- this.progressElement = this.createProgressElement();
1172
- this.installStylesheetElement();
1173
- this.setValue(0);
1447
+ function readScrollBehavior(value, defaultValue) {
1448
+ if (value == "auto" || value == "smooth") {
1449
+ return value;
1450
+ } else {
1451
+ return defaultValue;
1174
1452
  }
1453
+ }
1454
+
1455
+ class ProgressBar {
1175
1456
  static get defaultCSS() {
1176
1457
  return unindent`
1177
1458
  .turbo-progress-bar {
@@ -1181,7 +1462,7 @@ class ProgressBar {
1181
1462
  left: 0;
1182
1463
  height: 3px;
1183
1464
  background: #0076ff;
1184
- z-index: 9999;
1465
+ z-index: 2147483647;
1185
1466
  transition:
1186
1467
  width ${ProgressBar.animationDuration}ms ease-out,
1187
1468
  opacity ${ProgressBar.animationDuration / 2}ms ${ProgressBar.animationDuration / 2}ms ease-in;
@@ -1189,6 +1470,18 @@ class ProgressBar {
1189
1470
  }
1190
1471
  `;
1191
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
+ }
1192
1485
  show() {
1193
1486
  if (!this.visible) {
1194
1487
  this.visible = true;
@@ -1247,6 +1540,9 @@ class ProgressBar {
1247
1540
  const element = document.createElement("style");
1248
1541
  element.type = "text/css";
1249
1542
  element.textContent = ProgressBar.defaultCSS;
1543
+ if (this.cspNonce) {
1544
+ element.nonce = this.cspNonce;
1545
+ }
1250
1546
  return element;
1251
1547
  }
1252
1548
  createProgressElement() {
@@ -1254,6 +1550,9 @@ class ProgressBar {
1254
1550
  element.className = "turbo-progress-bar";
1255
1551
  return element;
1256
1552
  }
1553
+ get cspNonce() {
1554
+ return getMetaContent("csp-nonce");
1555
+ }
1257
1556
  }
1258
1557
 
1259
1558
  ProgressBar.animationDuration = 300;
@@ -1324,22 +1623,22 @@ function elementIsTracked(element) {
1324
1623
  }
1325
1624
 
1326
1625
  function elementIsScript(element) {
1327
- const tagName = element.tagName.toLowerCase();
1626
+ const tagName = element.localName;
1328
1627
  return tagName == "script";
1329
1628
  }
1330
1629
 
1331
1630
  function elementIsNoscript(element) {
1332
- const tagName = element.tagName.toLowerCase();
1631
+ const tagName = element.localName;
1333
1632
  return tagName == "noscript";
1334
1633
  }
1335
1634
 
1336
1635
  function elementIsStylesheet(element) {
1337
- const tagName = element.tagName.toLowerCase();
1636
+ const tagName = element.localName;
1338
1637
  return tagName == "style" || tagName == "link" && element.getAttribute("rel") == "stylesheet";
1339
1638
  }
1340
1639
 
1341
1640
  function elementIsMetaElementWithName(element, name) {
1342
- const tagName = element.tagName.toLowerCase();
1641
+ const tagName = element.localName;
1343
1642
  return tagName == "meta" && element.getAttribute("name") == name;
1344
1643
  }
1345
1644
 
@@ -1351,10 +1650,6 @@ function elementWithoutNonce(element) {
1351
1650
  }
1352
1651
 
1353
1652
  class PageSnapshot extends Snapshot {
1354
- constructor(element, headSnapshot) {
1355
- super(element);
1356
- this.headSnapshot = headSnapshot;
1357
- }
1358
1653
  static fromHTMLString(html = "") {
1359
1654
  return this.fromDocument(parseHTMLDocument(html));
1360
1655
  }
@@ -1364,8 +1659,23 @@ class PageSnapshot extends Snapshot {
1364
1659
  static fromDocument({head: head, body: body}) {
1365
1660
  return new this(body, new HeadSnapshot(head));
1366
1661
  }
1662
+ constructor(element, headSnapshot) {
1663
+ super(element);
1664
+ this.headSnapshot = headSnapshot;
1665
+ }
1367
1666
  clone() {
1368
- return new PageSnapshot(this.element.cloneNode(true), this.headSnapshot);
1667
+ const clonedElement = this.element.cloneNode(true);
1668
+ const selectElements = this.element.querySelectorAll("select");
1669
+ const clonedSelectElements = clonedElement.querySelectorAll("select");
1670
+ for (const [index, source] of selectElements.entries()) {
1671
+ const clone = clonedSelectElements[index];
1672
+ for (const option of clone.selectedOptions) option.selected = false;
1673
+ for (const option of source.selectedOptions) clone.options[option.index].selected = true;
1674
+ }
1675
+ for (const clonedPasswordInput of clonedElement.querySelectorAll('input[type="password"]')) {
1676
+ clonedPasswordInput.value = "";
1677
+ }
1678
+ return new PageSnapshot(clonedElement, this.headSnapshot);
1369
1679
  }
1370
1680
  get headElement() {
1371
1681
  return this.headSnapshot.element;
@@ -1415,7 +1725,10 @@ const defaultOptions = {
1415
1725
  action: "advance",
1416
1726
  historyChanged: false,
1417
1727
  visitCachedSnapshot: () => {},
1418
- willRender: true
1728
+ willRender: true,
1729
+ updateHistory: true,
1730
+ shouldCacheSnapshot: true,
1731
+ acceptsStreamResponse: false
1419
1732
  };
1420
1733
 
1421
1734
  var SystemStatusCode;
@@ -1433,21 +1746,27 @@ class Visit {
1433
1746
  this.followedRedirect = false;
1434
1747
  this.historyChanged = false;
1435
1748
  this.scrolled = false;
1749
+ this.shouldCacheSnapshot = true;
1750
+ this.acceptsStreamResponse = false;
1436
1751
  this.snapshotCached = false;
1437
1752
  this.state = VisitState.initialized;
1438
1753
  this.delegate = delegate;
1439
1754
  this.location = location;
1440
1755
  this.restorationIdentifier = restorationIdentifier || uuid();
1441
- const {action: action, historyChanged: historyChanged, referrer: referrer, snapshotHTML: snapshotHTML, response: response, visitCachedSnapshot: visitCachedSnapshot, willRender: willRender} = Object.assign(Object.assign({}, defaultOptions), options);
1756
+ const {action: action, historyChanged: historyChanged, referrer: referrer, snapshot: snapshot, snapshotHTML: snapshotHTML, response: response, visitCachedSnapshot: visitCachedSnapshot, willRender: willRender, updateHistory: updateHistory, shouldCacheSnapshot: shouldCacheSnapshot, acceptsStreamResponse: acceptsStreamResponse} = Object.assign(Object.assign({}, defaultOptions), options);
1442
1757
  this.action = action;
1443
1758
  this.historyChanged = historyChanged;
1444
1759
  this.referrer = referrer;
1760
+ this.snapshot = snapshot;
1445
1761
  this.snapshotHTML = snapshotHTML;
1446
1762
  this.response = response;
1447
1763
  this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
1448
1764
  this.visitCachedSnapshot = visitCachedSnapshot;
1449
1765
  this.willRender = willRender;
1766
+ this.updateHistory = updateHistory;
1450
1767
  this.scrolled = !willRender;
1768
+ this.shouldCacheSnapshot = shouldCacheSnapshot;
1769
+ this.acceptsStreamResponse = acceptsStreamResponse;
1451
1770
  }
1452
1771
  get adapter() {
1453
1772
  return this.delegate.adapter;
@@ -1485,9 +1804,11 @@ class Visit {
1485
1804
  if (this.state == VisitState.started) {
1486
1805
  this.recordTimingMetric(TimingMetric.visitEnd);
1487
1806
  this.state = VisitState.completed;
1488
- this.adapter.visitCompleted(this);
1489
- this.delegate.visitCompleted(this);
1490
1807
  this.followRedirect();
1808
+ if (!this.followedRedirect) {
1809
+ this.adapter.visitCompleted(this);
1810
+ this.delegate.visitCompleted(this);
1811
+ }
1491
1812
  }
1492
1813
  }
1493
1814
  fail() {
@@ -1498,9 +1819,9 @@ class Visit {
1498
1819
  }
1499
1820
  changeHistory() {
1500
1821
  var _a;
1501
- if (!this.historyChanged) {
1822
+ if (!this.historyChanged && this.updateHistory) {
1502
1823
  const actionForHistory = this.location.href === ((_a = this.referrer) === null || _a === void 0 ? void 0 : _a.href) ? "replace" : this.action;
1503
- const method = this.getHistoryMethodForAction(actionForHistory);
1824
+ const method = getHistoryMethodForAction(actionForHistory);
1504
1825
  this.history.update(method, this.location, this.restorationIdentifier);
1505
1826
  this.historyChanged = true;
1506
1827
  }
@@ -1543,14 +1864,15 @@ class Visit {
1543
1864
  if (this.response) {
1544
1865
  const {statusCode: statusCode, responseHTML: responseHTML} = this.response;
1545
1866
  this.render((async () => {
1546
- this.cacheSnapshot();
1867
+ if (this.shouldCacheSnapshot) this.cacheSnapshot();
1547
1868
  if (this.view.renderPromise) await this.view.renderPromise;
1548
1869
  if (isSuccessful(statusCode) && responseHTML != null) {
1549
- await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML), false, this.willRender);
1870
+ await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML), false, this.willRender, this);
1871
+ this.performScroll();
1550
1872
  this.adapter.visitRendered(this);
1551
1873
  this.complete();
1552
1874
  } else {
1553
- await this.view.renderError(PageSnapshot.fromHTMLString(responseHTML));
1875
+ await this.view.renderError(PageSnapshot.fromHTMLString(responseHTML), this);
1554
1876
  this.adapter.visitRendered(this);
1555
1877
  this.fail();
1556
1878
  }
@@ -1583,7 +1905,8 @@ class Visit {
1583
1905
  this.adapter.visitRendered(this);
1584
1906
  } else {
1585
1907
  if (this.view.renderPromise) await this.view.renderPromise;
1586
- await this.view.renderPage(snapshot, isPreview, this.willRender);
1908
+ await this.view.renderPage(snapshot, isPreview, this.willRender, this);
1909
+ this.performScroll();
1587
1910
  this.adapter.visitRendered(this);
1588
1911
  if (!isPreview) {
1589
1912
  this.complete();
@@ -1597,7 +1920,9 @@ class Visit {
1597
1920
  if (this.redirectedToLocation && !this.followedRedirect && ((_a = this.response) === null || _a === void 0 ? void 0 : _a.redirected)) {
1598
1921
  this.adapter.visitProposedToLocation(this.redirectedToLocation, {
1599
1922
  action: "replace",
1600
- response: this.response
1923
+ response: this.response,
1924
+ shouldCacheSnapshot: false,
1925
+ willRender: false
1601
1926
  });
1602
1927
  this.followedRedirect = true;
1603
1928
  }
@@ -1606,14 +1931,21 @@ class Visit {
1606
1931
  if (this.isSamePage) {
1607
1932
  this.render((async () => {
1608
1933
  this.cacheSnapshot();
1934
+ this.performScroll();
1935
+ this.changeHistory();
1609
1936
  this.adapter.visitRendered(this);
1610
1937
  }));
1611
1938
  }
1612
1939
  }
1940
+ prepareRequest(request) {
1941
+ if (this.acceptsStreamResponse) {
1942
+ request.acceptResponseType(StreamMessage.contentType);
1943
+ }
1944
+ }
1613
1945
  requestStarted() {
1614
1946
  this.startRequest();
1615
1947
  }
1616
- requestPreventedHandlingResponse(request, response) {}
1948
+ requestPreventedHandlingResponse(_request, _response) {}
1617
1949
  async requestSucceededWithResponse(request, response) {
1618
1950
  const responseHTML = await response.responseHTML;
1619
1951
  const {redirected: redirected, statusCode: statusCode} = response;
@@ -1647,7 +1979,7 @@ class Visit {
1647
1979
  });
1648
1980
  }
1649
1981
  }
1650
- requestErrored(request, error) {
1982
+ requestErrored(_request, _error) {
1651
1983
  this.recordResponse({
1652
1984
  statusCode: SystemStatusCode.networkFailure,
1653
1985
  redirected: false
@@ -1657,7 +1989,7 @@ class Visit {
1657
1989
  this.finishRequest();
1658
1990
  }
1659
1991
  performScroll() {
1660
- if (!this.scrolled) {
1992
+ if (!this.scrolled && !this.view.forceReloaded) {
1661
1993
  if (this.action == "restore") {
1662
1994
  this.scrollToRestoredPosition() || this.scrollToAnchor() || this.view.scrollToTop();
1663
1995
  } else {
@@ -1713,7 +2045,7 @@ class Visit {
1713
2045
  }
1714
2046
  cacheSnapshot() {
1715
2047
  if (!this.snapshotCached) {
1716
- this.view.cacheSnapshot().then((snapshot => snapshot && this.visitCachedSnapshot(snapshot)));
2048
+ this.view.cacheSnapshot(this.snapshot).then((snapshot => snapshot && this.visitCachedSnapshot(snapshot)));
1717
2049
  this.snapshotCached = true;
1718
2050
  }
1719
2051
  }
@@ -1724,7 +2056,6 @@ class Visit {
1724
2056
  }));
1725
2057
  await callback();
1726
2058
  delete this.frame;
1727
- this.performScroll();
1728
2059
  }
1729
2060
  cancelRender() {
1730
2061
  if (this.frame) {
@@ -1747,12 +2078,12 @@ class BrowserAdapter {
1747
2078
  this.session = session;
1748
2079
  }
1749
2080
  visitProposedToLocation(location, options) {
1750
- this.navigator.startVisit(location, uuid(), options);
2081
+ this.navigator.startVisit(location, (options === null || options === void 0 ? void 0 : options.restorationIdentifier) || uuid(), options);
1751
2082
  }
1752
2083
  visitStarted(visit) {
2084
+ this.location = visit.location;
1753
2085
  visit.loadCachedSnapshot();
1754
2086
  visit.issueRequest();
1755
- visit.changeHistory();
1756
2087
  visit.goToSamePageAnchor();
1757
2088
  }
1758
2089
  visitRequestStarted(visit) {
@@ -1771,27 +2102,32 @@ class BrowserAdapter {
1771
2102
  case SystemStatusCode.networkFailure:
1772
2103
  case SystemStatusCode.timeoutFailure:
1773
2104
  case SystemStatusCode.contentTypeMismatch:
1774
- return this.reload();
2105
+ return this.reload({
2106
+ reason: "request_failed",
2107
+ context: {
2108
+ statusCode: statusCode
2109
+ }
2110
+ });
1775
2111
 
1776
2112
  default:
1777
2113
  return visit.loadResponse();
1778
2114
  }
1779
2115
  }
1780
- visitRequestFinished(visit) {
2116
+ visitRequestFinished(_visit) {
1781
2117
  this.progressBar.setValue(1);
1782
2118
  this.hideVisitProgressBar();
1783
2119
  }
1784
- visitCompleted(visit) {}
1785
- pageInvalidated() {
1786
- this.reload();
2120
+ visitCompleted(_visit) {}
2121
+ pageInvalidated(reason) {
2122
+ this.reload(reason);
1787
2123
  }
1788
- visitFailed(visit) {}
1789
- visitRendered(visit) {}
1790
- formSubmissionStarted(formSubmission) {
2124
+ visitFailed(_visit) {}
2125
+ visitRendered(_visit) {}
2126
+ formSubmissionStarted(_formSubmission) {
1791
2127
  this.progressBar.setValue(0);
1792
2128
  this.showFormProgressBarAfterDelay();
1793
2129
  }
1794
- formSubmissionFinished(formSubmission) {
2130
+ formSubmissionFinished(_formSubmission) {
1795
2131
  this.progressBar.setValue(1);
1796
2132
  this.hideFormProgressBar();
1797
2133
  }
@@ -1817,8 +2153,12 @@ class BrowserAdapter {
1817
2153
  delete this.formProgressBarTimeout;
1818
2154
  }
1819
2155
  }
1820
- reload() {
1821
- window.location.reload();
2156
+ reload(reason) {
2157
+ var _a;
2158
+ dispatch("turbo:reload", {
2159
+ detail: reason
2160
+ });
2161
+ window.location.href = ((_a = this.location) === null || _a === void 0 ? void 0 : _a.toString()) || window.location.href;
1822
2162
  }
1823
2163
  get navigator() {
1824
2164
  return this.session.navigator;
@@ -1827,95 +2167,70 @@ class BrowserAdapter {
1827
2167
 
1828
2168
  class CacheObserver {
1829
2169
  constructor() {
2170
+ this.selector = "[data-turbo-temporary]";
2171
+ this.deprecatedSelector = "[data-turbo-cache=false]";
1830
2172
  this.started = false;
2173
+ this.removeTemporaryElements = _event => {
2174
+ for (const element of this.temporaryElements) {
2175
+ element.remove();
2176
+ }
2177
+ };
1831
2178
  }
1832
2179
  start() {
1833
2180
  if (!this.started) {
1834
2181
  this.started = true;
1835
- addEventListener("turbo:before-cache", this.removeStaleElements, false);
2182
+ addEventListener("turbo:before-cache", this.removeTemporaryElements, false);
1836
2183
  }
1837
2184
  }
1838
2185
  stop() {
1839
2186
  if (this.started) {
1840
2187
  this.started = false;
1841
- removeEventListener("turbo:before-cache", this.removeStaleElements, false);
1842
- }
1843
- }
1844
- removeStaleElements() {
1845
- const staleElements = [ ...document.querySelectorAll('[data-turbo-cache="false"]') ];
1846
- for (const element of staleElements) {
1847
- element.remove();
2188
+ removeEventListener("turbo:before-cache", this.removeTemporaryElements, false);
1848
2189
  }
1849
2190
  }
1850
- }
1851
-
1852
- class FormSubmitObserver {
1853
- constructor(delegate) {
1854
- this.started = false;
1855
- this.submitCaptured = () => {
1856
- removeEventListener("submit", this.submitBubbled, false);
1857
- addEventListener("submit", this.submitBubbled, false);
1858
- };
1859
- this.submitBubbled = event => {
1860
- if (!event.defaultPrevented) {
1861
- const form = event.target instanceof HTMLFormElement ? event.target : undefined;
1862
- const submitter = event.submitter || undefined;
1863
- if (form) {
1864
- const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.getAttribute("method");
1865
- if (method != "dialog" && this.delegate.willSubmitForm(form, submitter)) {
1866
- event.preventDefault();
1867
- this.delegate.formSubmitted(form, submitter);
1868
- }
1869
- }
1870
- }
1871
- };
1872
- this.delegate = delegate;
1873
- }
1874
- start() {
1875
- if (!this.started) {
1876
- addEventListener("submit", this.submitCaptured, true);
1877
- this.started = true;
1878
- }
2191
+ get temporaryElements() {
2192
+ return [ ...document.querySelectorAll(this.selector), ...this.temporaryElementsWithDeprecation ];
1879
2193
  }
1880
- stop() {
1881
- if (this.started) {
1882
- removeEventListener("submit", this.submitCaptured, true);
1883
- this.started = false;
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.`);
1884
2198
  }
2199
+ return [ ...elements ];
1885
2200
  }
1886
2201
  }
1887
2202
 
1888
2203
  class FrameRedirector {
1889
- constructor(element) {
2204
+ constructor(session, element) {
2205
+ this.session = session;
1890
2206
  this.element = element;
1891
2207
  this.linkInterceptor = new LinkInterceptor(this, element);
1892
- this.formInterceptor = new FormInterceptor(this, element);
2208
+ this.formSubmitObserver = new FormSubmitObserver(this, element);
1893
2209
  }
1894
2210
  start() {
1895
2211
  this.linkInterceptor.start();
1896
- this.formInterceptor.start();
2212
+ this.formSubmitObserver.start();
1897
2213
  }
1898
2214
  stop() {
1899
2215
  this.linkInterceptor.stop();
1900
- this.formInterceptor.stop();
2216
+ this.formSubmitObserver.stop();
1901
2217
  }
1902
- shouldInterceptLinkClick(element, url) {
2218
+ shouldInterceptLinkClick(element, _location, _event) {
1903
2219
  return this.shouldRedirect(element);
1904
2220
  }
1905
- linkClickIntercepted(element, url) {
2221
+ linkClickIntercepted(element, url, event) {
1906
2222
  const frame = this.findFrameElement(element);
1907
2223
  if (frame) {
1908
- frame.delegate.linkClickIntercepted(element, url);
2224
+ frame.delegate.linkClickIntercepted(element, url, event);
1909
2225
  }
1910
2226
  }
1911
- shouldInterceptFormSubmission(element, submitter) {
1912
- return this.shouldSubmit(element, submitter);
2227
+ willSubmitForm(element, submitter) {
2228
+ return element.closest("turbo-frame") == null && this.shouldSubmit(element, submitter) && this.shouldRedirect(element, submitter);
1913
2229
  }
1914
- formSubmissionIntercepted(element, submitter) {
2230
+ formSubmitted(element, submitter) {
1915
2231
  const frame = this.findFrameElement(element, submitter);
1916
2232
  if (frame) {
1917
- frame.removeAttribute("reloadable");
1918
- frame.delegate.formSubmissionIntercepted(element, submitter);
2233
+ frame.delegate.formSubmitted(element, submitter);
1919
2234
  }
1920
2235
  }
1921
2236
  shouldSubmit(form, submitter) {
@@ -1926,8 +2241,13 @@ class FrameRedirector {
1926
2241
  return this.shouldRedirect(form, submitter) && locationIsVisitable(action, rootLocation);
1927
2242
  }
1928
2243
  shouldRedirect(element, submitter) {
1929
- const frame = this.findFrameElement(element, submitter);
1930
- return frame ? frame != element.closest("turbo-frame") : false;
2244
+ const isNavigatable = element instanceof HTMLFormElement ? this.session.submissionIsNavigatable(element, submitter) : this.session.elementIsNavigatable(element);
2245
+ if (isNavigatable) {
2246
+ const frame = this.findFrameElement(element, submitter);
2247
+ return frame ? frame != element.closest("turbo-frame") : false;
2248
+ } else {
2249
+ return false;
2250
+ }
1931
2251
  }
1932
2252
  findFrameElement(element, submitter) {
1933
2253
  const id = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("data-turbo-frame")) || element.getAttribute("data-turbo-frame");
@@ -1957,7 +2277,7 @@ class History {
1957
2277
  }
1958
2278
  }
1959
2279
  };
1960
- this.onPageLoad = async event => {
2280
+ this.onPageLoad = async _event => {
1961
2281
  await nextMicrotask();
1962
2282
  this.pageLoaded = true;
1963
2283
  };
@@ -2023,57 +2343,6 @@ class History {
2023
2343
  }
2024
2344
  }
2025
2345
 
2026
- class LinkClickObserver {
2027
- constructor(delegate) {
2028
- this.started = false;
2029
- this.clickCaptured = () => {
2030
- removeEventListener("click", this.clickBubbled, false);
2031
- addEventListener("click", this.clickBubbled, false);
2032
- };
2033
- this.clickBubbled = event => {
2034
- if (this.clickEventIsSignificant(event)) {
2035
- const target = event.composedPath && event.composedPath()[0] || event.target;
2036
- const link = this.findLinkFromClickTarget(target);
2037
- if (link) {
2038
- const location = this.getLocationForLink(link);
2039
- if (this.delegate.willFollowLinkToLocation(link, location)) {
2040
- event.preventDefault();
2041
- this.delegate.followedLinkToLocation(link, location);
2042
- }
2043
- }
2044
- }
2045
- };
2046
- this.delegate = delegate;
2047
- }
2048
- start() {
2049
- if (!this.started) {
2050
- addEventListener("click", this.clickCaptured, true);
2051
- this.started = true;
2052
- }
2053
- }
2054
- stop() {
2055
- if (this.started) {
2056
- removeEventListener("click", this.clickCaptured, true);
2057
- this.started = false;
2058
- }
2059
- }
2060
- clickEventIsSignificant(event) {
2061
- return !(event.target && event.target.isContentEditable || event.defaultPrevented || event.which > 1 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey);
2062
- }
2063
- findLinkFromClickTarget(target) {
2064
- if (target instanceof Element) {
2065
- return target.closest("a[href]:not([target^=_]):not([download])");
2066
- }
2067
- }
2068
- getLocationForLink(link) {
2069
- return expandURL(link.getAttribute("href") || "");
2070
- }
2071
- }
2072
-
2073
- function isAction(action) {
2074
- return action == "advance" || action == "replace" || action == "restore";
2075
- }
2076
-
2077
2346
  class Navigator {
2078
2347
  constructor(delegate) {
2079
2348
  this.delegate = delegate;
@@ -2127,13 +2396,15 @@ class Navigator {
2127
2396
  if (formSubmission == this.formSubmission) {
2128
2397
  const responseHTML = await fetchResponse.responseHTML;
2129
2398
  if (responseHTML) {
2130
- if (formSubmission.method != FetchMethod.get) {
2399
+ const shouldCacheSnapshot = formSubmission.isSafe;
2400
+ if (!shouldCacheSnapshot) {
2131
2401
  this.view.clearSnapshotCache();
2132
2402
  }
2133
2403
  const {statusCode: statusCode, redirected: redirected} = fetchResponse;
2134
2404
  const action = this.getActionForFormSubmission(formSubmission);
2135
2405
  const visitOptions = {
2136
2406
  action: action,
2407
+ shouldCacheSnapshot: shouldCacheSnapshot,
2137
2408
  response: {
2138
2409
  statusCode: statusCode,
2139
2410
  responseHTML: responseHTML,
@@ -2149,9 +2420,9 @@ class Navigator {
2149
2420
  if (responseHTML) {
2150
2421
  const snapshot = PageSnapshot.fromHTMLString(responseHTML);
2151
2422
  if (fetchResponse.serverError) {
2152
- await this.view.renderError(snapshot);
2423
+ await this.view.renderError(snapshot, this.currentVisit);
2153
2424
  } else {
2154
- await this.view.renderPage(snapshot);
2425
+ await this.view.renderPage(snapshot, false, true, this.currentVisit);
2155
2426
  }
2156
2427
  this.view.scrollToTop();
2157
2428
  this.view.clearSnapshotCache();
@@ -2186,10 +2457,8 @@ class Navigator {
2186
2457
  get restorationIdentifier() {
2187
2458
  return this.history.restorationIdentifier;
2188
2459
  }
2189
- getActionForFormSubmission(formSubmission) {
2190
- const {formElement: formElement, submitter: submitter} = formSubmission;
2191
- const action = getAttribute("data-turbo-action", submitter, formElement);
2192
- return isAction(action) ? action : "advance";
2460
+ getActionForFormSubmission({submitter: submitter, formElement: formElement}) {
2461
+ return getVisitAction(submitter, formElement) || "advance";
2193
2462
  }
2194
2463
  }
2195
2464
 
@@ -2283,6 +2552,31 @@ class ScrollObserver {
2283
2552
  }
2284
2553
  }
2285
2554
 
2555
+ class StreamMessageRenderer {
2556
+ render({fragment: fragment}) {
2557
+ Bardo.preservingPermanentElements(this, getPermanentElementMapForFragment(fragment), (() => document.documentElement.appendChild(fragment)));
2558
+ }
2559
+ enteringBardo(currentPermanentElement, newPermanentElement) {
2560
+ newPermanentElement.replaceWith(currentPermanentElement.cloneNode(true));
2561
+ }
2562
+ leavingBardo() {}
2563
+ }
2564
+
2565
+ function getPermanentElementMapForFragment(fragment) {
2566
+ const permanentElementsInDocument = queryPermanentElementsAll(document.documentElement);
2567
+ const permanentElementMap = {};
2568
+ for (const permanentElementInDocument of permanentElementsInDocument) {
2569
+ const {id: id} = permanentElementInDocument;
2570
+ for (const streamElement of fragment.querySelectorAll("turbo-stream")) {
2571
+ const elementInStream = getPermanentElementById(streamElement.templateElement.content, id);
2572
+ if (elementInStream) {
2573
+ permanentElementMap[id] = [ permanentElementInDocument, elementInStream ];
2574
+ }
2575
+ }
2576
+ }
2577
+ return permanentElementMap;
2578
+ }
2579
+
2286
2580
  class StreamObserver {
2287
2581
  constructor(delegate) {
2288
2582
  this.sources = new Set;
@@ -2335,7 +2629,7 @@ class StreamObserver {
2335
2629
  }
2336
2630
  }
2337
2631
  receiveMessageHTML(html) {
2338
- this.delegate.receivedMessageFromStream(new StreamMessage(html));
2632
+ this.delegate.receivedMessageFromStream(StreamMessage.wrap(html));
2339
2633
  }
2340
2634
  }
2341
2635
 
@@ -2354,20 +2648,24 @@ function fetchResponseIsStream(response) {
2354
2648
  }
2355
2649
 
2356
2650
  class ErrorRenderer extends Renderer {
2651
+ static renderElement(currentElement, newElement) {
2652
+ const {documentElement: documentElement, body: body} = document;
2653
+ documentElement.replaceChild(newElement, body);
2654
+ }
2357
2655
  async render() {
2358
2656
  this.replaceHeadAndBody();
2359
2657
  this.activateScriptElements();
2360
2658
  }
2361
2659
  replaceHeadAndBody() {
2362
- const {documentElement: documentElement, head: head, body: body} = document;
2660
+ const {documentElement: documentElement, head: head} = document;
2363
2661
  documentElement.replaceChild(this.newHead, head);
2364
- documentElement.replaceChild(this.newElement, body);
2662
+ this.renderElement(this.currentElement, this.newElement);
2365
2663
  }
2366
2664
  activateScriptElements() {
2367
2665
  for (const replaceableElement of this.scriptElements) {
2368
2666
  const parentNode = replaceableElement.parentNode;
2369
2667
  if (parentNode) {
2370
- const element = this.createScriptElement(replaceableElement);
2668
+ const element = activateScriptElement(replaceableElement);
2371
2669
  parentNode.replaceChild(element, replaceableElement);
2372
2670
  }
2373
2671
  }
@@ -2376,20 +2674,39 @@ class ErrorRenderer extends Renderer {
2376
2674
  return this.newSnapshot.headSnapshot.element;
2377
2675
  }
2378
2676
  get scriptElements() {
2379
- return [ ...document.documentElement.querySelectorAll("script") ];
2677
+ return document.documentElement.querySelectorAll("script");
2380
2678
  }
2381
2679
  }
2382
2680
 
2383
2681
  class PageRenderer extends Renderer {
2682
+ static renderElement(currentElement, newElement) {
2683
+ if (document.body && newElement instanceof HTMLBodyElement) {
2684
+ document.body.replaceWith(newElement);
2685
+ } else {
2686
+ document.documentElement.appendChild(newElement);
2687
+ }
2688
+ }
2384
2689
  get shouldRender() {
2385
2690
  return this.newSnapshot.isVisitable && this.trackedElementsAreIdentical;
2386
2691
  }
2387
- prepareToRender() {
2388
- this.mergeHead();
2692
+ get reloadReason() {
2693
+ if (!this.newSnapshot.isVisitable) {
2694
+ return {
2695
+ reason: "turbo_visit_control_is_reload"
2696
+ };
2697
+ }
2698
+ if (!this.trackedElementsAreIdentical) {
2699
+ return {
2700
+ reason: "tracked_element_mismatch"
2701
+ };
2702
+ }
2703
+ }
2704
+ async prepareToRender() {
2705
+ await this.mergeHead();
2389
2706
  }
2390
2707
  async render() {
2391
2708
  if (this.willRender) {
2392
- this.replaceBody();
2709
+ await this.replaceBody();
2393
2710
  }
2394
2711
  }
2395
2712
  finishRendering() {
@@ -2407,31 +2724,64 @@ class PageRenderer extends Renderer {
2407
2724
  get newElement() {
2408
2725
  return this.newSnapshot.element;
2409
2726
  }
2410
- mergeHead() {
2411
- this.copyNewHeadStylesheetElements();
2727
+ async mergeHead() {
2728
+ const mergedHeadElements = this.mergeProvisionalElements();
2729
+ const newStylesheetElements = this.copyNewHeadStylesheetElements();
2412
2730
  this.copyNewHeadScriptElements();
2413
- this.removeCurrentHeadProvisionalElements();
2414
- this.copyNewHeadProvisionalElements();
2731
+ await mergedHeadElements;
2732
+ await newStylesheetElements;
2415
2733
  }
2416
- replaceBody() {
2417
- this.preservingPermanentElements((() => {
2734
+ async replaceBody() {
2735
+ await this.preservingPermanentElements((async () => {
2418
2736
  this.activateNewBody();
2419
- this.assignNewBody();
2737
+ await this.assignNewBody();
2420
2738
  }));
2421
2739
  }
2422
2740
  get trackedElementsAreIdentical() {
2423
2741
  return this.currentHeadSnapshot.trackedElementSignature == this.newHeadSnapshot.trackedElementSignature;
2424
2742
  }
2425
- copyNewHeadStylesheetElements() {
2743
+ async copyNewHeadStylesheetElements() {
2744
+ const loadingElements = [];
2426
2745
  for (const element of this.newHeadStylesheetElements) {
2746
+ loadingElements.push(waitForLoad(element));
2427
2747
  document.head.appendChild(element);
2428
2748
  }
2749
+ await Promise.all(loadingElements);
2429
2750
  }
2430
2751
  copyNewHeadScriptElements() {
2431
2752
  for (const element of this.newHeadScriptElements) {
2432
- document.head.appendChild(this.createScriptElement(element));
2753
+ document.head.appendChild(activateScriptElement(element));
2754
+ }
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);
2433
2765
  }
2434
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
+ }
2435
2785
  removeCurrentHeadProvisionalElements() {
2436
2786
  for (const element of this.currentHeadProvisionalElements) {
2437
2787
  document.head.removeChild(element);
@@ -2448,16 +2798,12 @@ class PageRenderer extends Renderer {
2448
2798
  }
2449
2799
  activateNewBodyScriptElements() {
2450
2800
  for (const inertScriptElement of this.newBodyScriptElements) {
2451
- const activatedScriptElement = this.createScriptElement(inertScriptElement);
2801
+ const activatedScriptElement = activateScriptElement(inertScriptElement);
2452
2802
  inertScriptElement.replaceWith(activatedScriptElement);
2453
2803
  }
2454
2804
  }
2455
- assignNewBody() {
2456
- if (document.body && this.newElement instanceof HTMLBodyElement) {
2457
- document.body.replaceWith(this.newElement);
2458
- } else {
2459
- document.documentElement.appendChild(this.newElement);
2460
- }
2805
+ async assignNewBody() {
2806
+ await this.renderElement(this.currentElement, this.newElement);
2461
2807
  }
2462
2808
  get newHeadStylesheetElements() {
2463
2809
  return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot);
@@ -2525,22 +2871,29 @@ class PageView extends View {
2525
2871
  super(...arguments);
2526
2872
  this.snapshotCache = new SnapshotCache(10);
2527
2873
  this.lastRenderedLocation = new URL(location.href);
2874
+ this.forceReloaded = false;
2528
2875
  }
2529
- renderPage(snapshot, isPreview = false, willRender = true) {
2530
- const renderer = new PageRenderer(this.snapshot, snapshot, isPreview, willRender);
2876
+ renderPage(snapshot, isPreview = false, willRender = true, visit) {
2877
+ const renderer = new PageRenderer(this.snapshot, snapshot, PageRenderer.renderElement, isPreview, willRender);
2878
+ if (!renderer.shouldRender) {
2879
+ this.forceReloaded = true;
2880
+ } else {
2881
+ visit === null || visit === void 0 ? void 0 : visit.changeHistory();
2882
+ }
2531
2883
  return this.render(renderer);
2532
2884
  }
2533
- renderError(snapshot) {
2534
- const renderer = new ErrorRenderer(this.snapshot, snapshot, false);
2885
+ renderError(snapshot, visit) {
2886
+ visit === null || visit === void 0 ? void 0 : visit.changeHistory();
2887
+ const renderer = new ErrorRenderer(this.snapshot, snapshot, ErrorRenderer.renderElement, false);
2535
2888
  return this.render(renderer);
2536
2889
  }
2537
2890
  clearSnapshotCache() {
2538
2891
  this.snapshotCache.clear();
2539
2892
  }
2540
- async cacheSnapshot() {
2541
- if (this.shouldCacheSnapshot) {
2893
+ async cacheSnapshot(snapshot = this.snapshot) {
2894
+ if (snapshot.isCacheable) {
2542
2895
  this.delegate.viewWillCacheSnapshot();
2543
- const {snapshot: snapshot, lastRenderedLocation: location} = this;
2896
+ const {lastRenderedLocation: location} = this;
2544
2897
  await nextEventLoopTick();
2545
2898
  const cachedSnapshot = snapshot.clone();
2546
2899
  this.snapshotCache.put(location, cachedSnapshot);
@@ -2553,8 +2906,46 @@ class PageView extends View {
2553
2906
  get snapshot() {
2554
2907
  return PageSnapshot.fromElement(this.element);
2555
2908
  }
2556
- get shouldCacheSnapshot() {
2557
- return this.snapshot.isCacheable;
2909
+ }
2910
+
2911
+ class Preloader {
2912
+ constructor(delegate) {
2913
+ this.selector = "a[data-turbo-preload]";
2914
+ this.delegate = delegate;
2915
+ }
2916
+ get snapshotCache() {
2917
+ return this.delegate.navigator.view.snapshotCache;
2918
+ }
2919
+ start() {
2920
+ if (document.readyState === "loading") {
2921
+ return document.addEventListener("DOMContentLoaded", (() => {
2922
+ this.preloadOnLoadLinksForView(document.body);
2923
+ }));
2924
+ } else {
2925
+ this.preloadOnLoadLinksForView(document.body);
2926
+ }
2927
+ }
2928
+ preloadOnLoadLinksForView(element) {
2929
+ for (const link of element.querySelectorAll(this.selector)) {
2930
+ this.preloadURL(link);
2931
+ }
2932
+ }
2933
+ async preloadURL(link) {
2934
+ const location = new URL(link.href);
2935
+ if (this.snapshotCache.has(location)) {
2936
+ return;
2937
+ }
2938
+ try {
2939
+ const response = await fetch(location.toString(), {
2940
+ headers: {
2941
+ "VND.PREFETCH": "true",
2942
+ Accept: "text/html"
2943
+ }
2944
+ });
2945
+ const responseText = await response.text();
2946
+ const snapshot = PageSnapshot.fromHTMLString(responseText);
2947
+ this.snapshotCache.put(location, snapshot);
2948
+ } catch (_) {}
2558
2949
  }
2559
2950
  }
2560
2951
 
@@ -2562,30 +2953,36 @@ class Session {
2562
2953
  constructor() {
2563
2954
  this.navigator = new Navigator(this);
2564
2955
  this.history = new History(this);
2956
+ this.preloader = new Preloader(this);
2565
2957
  this.view = new PageView(this, document.documentElement);
2566
2958
  this.adapter = new BrowserAdapter(this);
2567
2959
  this.pageObserver = new PageObserver(this);
2568
2960
  this.cacheObserver = new CacheObserver;
2569
- this.linkClickObserver = new LinkClickObserver(this);
2570
- this.formSubmitObserver = new FormSubmitObserver(this);
2961
+ this.linkClickObserver = new LinkClickObserver(this, window);
2962
+ this.formSubmitObserver = new FormSubmitObserver(this, document);
2571
2963
  this.scrollObserver = new ScrollObserver(this);
2572
2964
  this.streamObserver = new StreamObserver(this);
2573
- this.frameRedirector = new FrameRedirector(document.documentElement);
2965
+ this.formLinkClickObserver = new FormLinkClickObserver(this, document.documentElement);
2966
+ this.frameRedirector = new FrameRedirector(this, document.documentElement);
2967
+ this.streamMessageRenderer = new StreamMessageRenderer;
2574
2968
  this.drive = true;
2575
2969
  this.enabled = true;
2576
2970
  this.progressBarDelay = 500;
2577
2971
  this.started = false;
2972
+ this.formMode = "on";
2578
2973
  }
2579
2974
  start() {
2580
2975
  if (!this.started) {
2581
2976
  this.pageObserver.start();
2582
2977
  this.cacheObserver.start();
2978
+ this.formLinkClickObserver.start();
2583
2979
  this.linkClickObserver.start();
2584
2980
  this.formSubmitObserver.start();
2585
2981
  this.scrollObserver.start();
2586
2982
  this.streamObserver.start();
2587
2983
  this.frameRedirector.start();
2588
2984
  this.history.start();
2985
+ this.preloader.start();
2589
2986
  this.started = true;
2590
2987
  this.enabled = true;
2591
2988
  }
@@ -2597,6 +2994,7 @@ class Session {
2597
2994
  if (this.started) {
2598
2995
  this.pageObserver.stop();
2599
2996
  this.cacheObserver.stop();
2997
+ this.formLinkClickObserver.stop();
2600
2998
  this.linkClickObserver.stop();
2601
2999
  this.formSubmitObserver.stop();
2602
3000
  this.scrollObserver.stop();
@@ -2610,7 +3008,13 @@ class Session {
2610
3008
  this.adapter = adapter;
2611
3009
  }
2612
3010
  visit(location, options = {}) {
2613
- this.navigator.proposeVisit(expandURL(location), options);
3011
+ const frameElement = options.frame ? document.getElementById(options.frame) : null;
3012
+ if (frameElement instanceof FrameElement) {
3013
+ frameElement.src = location.toString();
3014
+ frameElement.loaded;
3015
+ } else {
3016
+ this.navigator.proposeVisit(expandURL(location), options);
3017
+ }
2614
3018
  }
2615
3019
  connectStreamSource(source) {
2616
3020
  this.streamObserver.connectStreamSource(source);
@@ -2619,7 +3023,7 @@ class Session {
2619
3023
  this.streamObserver.disconnectStreamSource(source);
2620
3024
  }
2621
3025
  renderStreamMessage(message) {
2622
- document.documentElement.appendChild(StreamMessage.wrap(message).fragment);
3026
+ this.streamMessageRenderer.render(StreamMessage.wrap(message));
2623
3027
  }
2624
3028
  clearCache() {
2625
3029
  this.view.clearSnapshotCache();
@@ -2627,6 +3031,9 @@ class Session {
2627
3031
  setProgressBarDelay(delay) {
2628
3032
  this.progressBarDelay = delay;
2629
3033
  }
3034
+ setFormMode(mode) {
3035
+ this.formMode = mode;
3036
+ }
2630
3037
  get location() {
2631
3038
  return this.history.location;
2632
3039
  }
@@ -2640,7 +3047,9 @@ class Session {
2640
3047
  historyChanged: true
2641
3048
  });
2642
3049
  } else {
2643
- this.adapter.pageInvalidated();
3050
+ this.adapter.pageInvalidated({
3051
+ reason: "turbo_disabled"
3052
+ });
2644
3053
  }
2645
3054
  }
2646
3055
  scrollPositionChanged(position) {
@@ -2648,41 +3057,21 @@ class Session {
2648
3057
  scrollPosition: position
2649
3058
  });
2650
3059
  }
2651
- willFollowLinkToLocation(link, location) {
2652
- return this.elementDriveEnabled(link) && locationIsVisitable(location, this.snapshot.rootLocation) && this.applicationAllowsFollowingLinkToLocation(link, location);
3060
+ willSubmitFormLinkToLocation(link, location) {
3061
+ return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation);
3062
+ }
3063
+ submittedFormLinkToLocation() {}
3064
+ willFollowLinkToLocation(link, location, event) {
3065
+ return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation) && this.applicationAllowsFollowingLinkToLocation(link, location, event);
2653
3066
  }
2654
3067
  followedLinkToLocation(link, location) {
2655
3068
  const action = this.getActionForLink(link);
2656
- this.convertLinkWithMethodClickToFormSubmission(link) || this.visit(location.href, {
2657
- action: action
3069
+ const acceptsStreamResponse = link.hasAttribute("data-turbo-stream");
3070
+ this.visit(location.href, {
3071
+ action: action,
3072
+ acceptsStreamResponse: acceptsStreamResponse
2658
3073
  });
2659
3074
  }
2660
- convertLinkWithMethodClickToFormSubmission(link) {
2661
- const linkMethod = link.getAttribute("data-turbo-method");
2662
- if (linkMethod) {
2663
- const form = document.createElement("form");
2664
- form.method = linkMethod;
2665
- form.action = link.getAttribute("href") || "undefined";
2666
- form.hidden = true;
2667
- if (link.hasAttribute("data-turbo-confirm")) {
2668
- form.setAttribute("data-turbo-confirm", link.getAttribute("data-turbo-confirm"));
2669
- }
2670
- const frame = this.getTargetFrameForLink(link);
2671
- if (frame) {
2672
- form.setAttribute("data-turbo-frame", frame);
2673
- form.addEventListener("turbo:submit-start", (() => form.remove()));
2674
- } else {
2675
- form.addEventListener("submit", (() => form.remove()));
2676
- }
2677
- document.body.appendChild(form);
2678
- return dispatch("submit", {
2679
- cancelable: true,
2680
- target: form
2681
- });
2682
- } else {
2683
- return false;
2684
- }
2685
- }
2686
3075
  allowsVisitingLocationWithAction(location, action) {
2687
3076
  return this.locationWithActionIsSamePage(location, action) || this.applicationAllowsVisitingLocation(location);
2688
3077
  }
@@ -2691,12 +3080,16 @@ class Session {
2691
3080
  this.adapter.visitProposedToLocation(location, options);
2692
3081
  }
2693
3082
  visitStarted(visit) {
3083
+ if (!visit.acceptsStreamResponse) {
3084
+ markAsBusy(document.documentElement);
3085
+ }
2694
3086
  extendURLWithDeprecatedProperties(visit.location);
2695
3087
  if (!visit.silent) {
2696
3088
  this.notifyApplicationAfterVisitingLocation(visit.location, visit.action);
2697
3089
  }
2698
3090
  }
2699
3091
  visitCompleted(visit) {
3092
+ clearBusyState(document.documentElement);
2700
3093
  this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
2701
3094
  }
2702
3095
  locationWithActionIsSamePage(location, action) {
@@ -2707,7 +3100,7 @@ class Session {
2707
3100
  }
2708
3101
  willSubmitForm(form, submitter) {
2709
3102
  const action = getAction(form, submitter);
2710
- return this.elementDriveEnabled(form) && (!submitter || this.elementDriveEnabled(submitter)) && locationIsVisitable(expandURL(action), this.snapshot.rootLocation);
3103
+ return this.submissionIsNavigatable(form, submitter) && locationIsVisitable(expandURL(action), this.snapshot.rootLocation);
2711
3104
  }
2712
3105
  formSubmitted(form, submitter) {
2713
3106
  this.navigator.submitForm(form, submitter);
@@ -2731,16 +3124,23 @@ class Session {
2731
3124
  this.notifyApplicationBeforeCachingSnapshot();
2732
3125
  }
2733
3126
  }
2734
- allowsImmediateRender({element: element}, resume) {
2735
- const event = this.notifyApplicationBeforeRender(element, resume);
2736
- return !event.defaultPrevented;
3127
+ allowsImmediateRender({element: element}, options) {
3128
+ const event = this.notifyApplicationBeforeRender(element, options);
3129
+ const {defaultPrevented: defaultPrevented, detail: {render: render}} = event;
3130
+ if (this.view.renderer && render) {
3131
+ this.view.renderer.renderElement = render;
3132
+ }
3133
+ return !defaultPrevented;
2737
3134
  }
2738
- viewRenderedSnapshot(snapshot, isPreview) {
3135
+ viewRenderedSnapshot(_snapshot, _isPreview) {
2739
3136
  this.view.lastRenderedLocation = this.history.location;
2740
3137
  this.notifyApplicationAfterRender();
2741
3138
  }
2742
- viewInvalidated() {
2743
- this.adapter.pageInvalidated();
3139
+ preloadOnLoadLinksForView(element) {
3140
+ this.preloader.preloadOnLoadLinksForView(element);
3141
+ }
3142
+ viewInvalidated(reason) {
3143
+ this.adapter.pageInvalidated(reason);
2744
3144
  }
2745
3145
  frameLoaded(frame) {
2746
3146
  this.notifyApplicationAfterFrameLoad(frame);
@@ -2748,19 +3148,20 @@ class Session {
2748
3148
  frameRendered(fetchResponse, frame) {
2749
3149
  this.notifyApplicationAfterFrameRender(fetchResponse, frame);
2750
3150
  }
2751
- applicationAllowsFollowingLinkToLocation(link, location) {
2752
- const event = this.notifyApplicationAfterClickingLinkToLocation(link, location);
3151
+ applicationAllowsFollowingLinkToLocation(link, location, ev) {
3152
+ const event = this.notifyApplicationAfterClickingLinkToLocation(link, location, ev);
2753
3153
  return !event.defaultPrevented;
2754
3154
  }
2755
3155
  applicationAllowsVisitingLocation(location) {
2756
3156
  const event = this.notifyApplicationBeforeVisitingLocation(location);
2757
3157
  return !event.defaultPrevented;
2758
3158
  }
2759
- notifyApplicationAfterClickingLinkToLocation(link, location) {
3159
+ notifyApplicationAfterClickingLinkToLocation(link, location, event) {
2760
3160
  return dispatch("turbo:click", {
2761
3161
  target: link,
2762
3162
  detail: {
2763
- url: location.href
3163
+ url: location.href,
3164
+ originalEvent: event
2764
3165
  },
2765
3166
  cancelable: true
2766
3167
  });
@@ -2774,7 +3175,6 @@ class Session {
2774
3175
  });
2775
3176
  }
2776
3177
  notifyApplicationAfterVisitingLocation(location, action) {
2777
- markAsBusy(document.documentElement);
2778
3178
  return dispatch("turbo:visit", {
2779
3179
  detail: {
2780
3180
  url: location.href,
@@ -2785,12 +3185,11 @@ class Session {
2785
3185
  notifyApplicationBeforeCachingSnapshot() {
2786
3186
  return dispatch("turbo:before-cache");
2787
3187
  }
2788
- notifyApplicationBeforeRender(newBody, resume) {
3188
+ notifyApplicationBeforeRender(newBody, options) {
2789
3189
  return dispatch("turbo:before-render", {
2790
- detail: {
2791
- newBody: newBody,
2792
- resume: resume
2793
- },
3190
+ detail: Object.assign({
3191
+ newBody: newBody
3192
+ }, options),
2794
3193
  cancelable: true
2795
3194
  });
2796
3195
  }
@@ -2798,7 +3197,6 @@ class Session {
2798
3197
  return dispatch("turbo:render");
2799
3198
  }
2800
3199
  notifyApplicationAfterPageLoad(timing = {}) {
2801
- clearBusyState(document.documentElement);
2802
3200
  return dispatch("turbo:load", {
2803
3201
  detail: {
2804
3202
  url: this.location.href,
@@ -2826,9 +3224,22 @@ class Session {
2826
3224
  cancelable: true
2827
3225
  });
2828
3226
  }
2829
- elementDriveEnabled(element) {
2830
- const container = element === null || element === void 0 ? void 0 : element.closest("[data-turbo]");
2831
- if (this.drive) {
3227
+ submissionIsNavigatable(form, submitter) {
3228
+ if (this.formMode == "off") {
3229
+ return false;
3230
+ } else {
3231
+ const submitterIsNavigatable = submitter ? this.elementIsNavigatable(submitter) : true;
3232
+ if (this.formMode == "optin") {
3233
+ return submitterIsNavigatable && form.closest('[data-turbo="true"]') != null;
3234
+ } else {
3235
+ return submitterIsNavigatable && this.elementIsNavigatable(form);
3236
+ }
3237
+ }
3238
+ }
3239
+ elementIsNavigatable(element) {
3240
+ const container = findClosestRecursively(element, "[data-turbo]");
3241
+ const withinFrame = findClosestRecursively(element, "turbo-frame");
3242
+ if (this.drive || withinFrame) {
2832
3243
  if (container) {
2833
3244
  return container.getAttribute("data-turbo") != "false";
2834
3245
  } else {
@@ -2843,19 +3254,7 @@ class Session {
2843
3254
  }
2844
3255
  }
2845
3256
  getActionForLink(link) {
2846
- const action = link.getAttribute("data-turbo-action");
2847
- return isAction(action) ? action : "advance";
2848
- }
2849
- getTargetFrameForLink(link) {
2850
- const frame = link.getAttribute("data-turbo-frame");
2851
- if (frame) {
2852
- return frame;
2853
- } else {
2854
- const container = link.closest("turbo-frame");
2855
- if (container) {
2856
- return container.id;
2857
- }
2858
- }
3257
+ return getVisitAction(link) || "advance";
2859
3258
  }
2860
3259
  get snapshot() {
2861
3260
  return this.view.snapshot;
@@ -2874,8 +3273,66 @@ const deprecatedLocationPropertyDescriptors = {
2874
3273
  }
2875
3274
  };
2876
3275
 
3276
+ class Cache {
3277
+ constructor(session) {
3278
+ this.session = session;
3279
+ }
3280
+ clear() {
3281
+ this.session.clearCache();
3282
+ }
3283
+ resetCacheControl() {
3284
+ this.setCacheControl("");
3285
+ }
3286
+ exemptPageFromCache() {
3287
+ this.setCacheControl("no-cache");
3288
+ }
3289
+ exemptPageFromPreview() {
3290
+ this.setCacheControl("no-preview");
3291
+ }
3292
+ setCacheControl(value) {
3293
+ setMetaContent("turbo-cache-control", value);
3294
+ }
3295
+ }
3296
+
3297
+ const StreamActions = {
3298
+ after() {
3299
+ this.targetElements.forEach((e => {
3300
+ var _a;
3301
+ return (_a = e.parentElement) === null || _a === void 0 ? void 0 : _a.insertBefore(this.templateContent, e.nextSibling);
3302
+ }));
3303
+ },
3304
+ append() {
3305
+ this.removeDuplicateTargetChildren();
3306
+ this.targetElements.forEach((e => e.append(this.templateContent)));
3307
+ },
3308
+ before() {
3309
+ this.targetElements.forEach((e => {
3310
+ var _a;
3311
+ return (_a = e.parentElement) === null || _a === void 0 ? void 0 : _a.insertBefore(this.templateContent, e);
3312
+ }));
3313
+ },
3314
+ prepend() {
3315
+ this.removeDuplicateTargetChildren();
3316
+ this.targetElements.forEach((e => e.prepend(this.templateContent)));
3317
+ },
3318
+ remove() {
3319
+ this.targetElements.forEach((e => e.remove()));
3320
+ },
3321
+ replace() {
3322
+ this.targetElements.forEach((e => e.replaceWith(this.templateContent)));
3323
+ },
3324
+ update() {
3325
+ this.targetElements.forEach((targetElement => {
3326
+ targetElement.innerHTML = "";
3327
+ targetElement.append(this.templateContent);
3328
+ }));
3329
+ }
3330
+ };
3331
+
2877
3332
  const session = new Session;
2878
3333
 
3334
+ const cache = new Cache(session);
3335
+
2879
3336
  const {navigator: navigator$1} = session;
2880
3337
 
2881
3338
  function start() {
@@ -2903,6 +3360,7 @@ function renderStreamMessage(message) {
2903
3360
  }
2904
3361
 
2905
3362
  function clearCache() {
3363
+ console.warn("Please replace `Turbo.clearCache()` with `Turbo.cache.clear()`. The top-level function is deprecated and will be removed in a future version of Turbo.`");
2906
3364
  session.clearCache();
2907
3365
  }
2908
3366
 
@@ -2914,12 +3372,18 @@ function setConfirmMethod(confirmMethod) {
2914
3372
  FormSubmission.confirmMethod = confirmMethod;
2915
3373
  }
2916
3374
 
3375
+ function setFormMode(mode) {
3376
+ session.setFormMode(mode);
3377
+ }
3378
+
2917
3379
  var Turbo = Object.freeze({
2918
3380
  __proto__: null,
2919
3381
  navigator: navigator$1,
2920
3382
  session: session,
3383
+ cache: cache,
2921
3384
  PageRenderer: PageRenderer,
2922
3385
  PageSnapshot: PageSnapshot,
3386
+ FrameRenderer: FrameRenderer,
2923
3387
  start: start,
2924
3388
  registerAdapter: registerAdapter,
2925
3389
  visit: visit,
@@ -2928,41 +3392,57 @@ var Turbo = Object.freeze({
2928
3392
  renderStreamMessage: renderStreamMessage,
2929
3393
  clearCache: clearCache,
2930
3394
  setProgressBarDelay: setProgressBarDelay,
2931
- setConfirmMethod: setConfirmMethod
3395
+ setConfirmMethod: setConfirmMethod,
3396
+ setFormMode: setFormMode,
3397
+ StreamActions: StreamActions
2932
3398
  });
2933
3399
 
3400
+ class TurboFrameMissingError extends Error {}
3401
+
2934
3402
  class FrameController {
2935
3403
  constructor(element) {
2936
- this.fetchResponseLoaded = fetchResponse => {};
3404
+ this.fetchResponseLoaded = _fetchResponse => {};
2937
3405
  this.currentFetchRequest = null;
2938
3406
  this.resolveVisitPromise = () => {};
2939
3407
  this.connected = false;
2940
3408
  this.hasBeenLoaded = false;
2941
- this.settingSourceURL = false;
3409
+ this.ignoredAttributes = new Set;
3410
+ this.action = null;
3411
+ this.visitCachedSnapshot = ({element: element}) => {
3412
+ const frame = element.querySelector("#" + this.element.id);
3413
+ if (frame && this.previousFrameElement) {
3414
+ frame.replaceChildren(...this.previousFrameElement.children);
3415
+ }
3416
+ delete this.previousFrameElement;
3417
+ };
2942
3418
  this.element = element;
2943
3419
  this.view = new FrameView(this, this.element);
2944
3420
  this.appearanceObserver = new AppearanceObserver(this, this.element);
3421
+ this.formLinkClickObserver = new FormLinkClickObserver(this, this.element);
2945
3422
  this.linkInterceptor = new LinkInterceptor(this, this.element);
2946
- this.formInterceptor = new FormInterceptor(this, this.element);
3423
+ this.restorationIdentifier = uuid();
3424
+ this.formSubmitObserver = new FormSubmitObserver(this, this.element);
2947
3425
  }
2948
3426
  connect() {
2949
3427
  if (!this.connected) {
2950
3428
  this.connected = true;
2951
- this.reloadable = false;
2952
3429
  if (this.loadingStyle == FrameLoadingStyle.lazy) {
2953
3430
  this.appearanceObserver.start();
3431
+ } else {
3432
+ this.loadSourceURL();
2954
3433
  }
3434
+ this.formLinkClickObserver.start();
2955
3435
  this.linkInterceptor.start();
2956
- this.formInterceptor.start();
2957
- this.sourceURLChanged();
3436
+ this.formSubmitObserver.start();
2958
3437
  }
2959
3438
  }
2960
3439
  disconnect() {
2961
3440
  if (this.connected) {
2962
3441
  this.connected = false;
2963
3442
  this.appearanceObserver.stop();
3443
+ this.formLinkClickObserver.stop();
2964
3444
  this.linkInterceptor.stop();
2965
- this.formInterceptor.stop();
3445
+ this.formSubmitObserver.stop();
2966
3446
  }
2967
3447
  }
2968
3448
  disabledChanged() {
@@ -2971,10 +3451,27 @@ class FrameController {
2971
3451
  }
2972
3452
  }
2973
3453
  sourceURLChanged() {
3454
+ if (this.isIgnoringChangesTo("src")) return;
3455
+ if (this.element.isConnected) {
3456
+ this.complete = false;
3457
+ }
2974
3458
  if (this.loadingStyle == FrameLoadingStyle.eager || this.hasBeenLoaded) {
2975
3459
  this.loadSourceURL();
2976
3460
  }
2977
3461
  }
3462
+ sourceURLReloaded() {
3463
+ const {src: src} = this.element;
3464
+ this.ignoringChangesToAttribute("complete", (() => {
3465
+ this.element.removeAttribute("complete");
3466
+ }));
3467
+ this.element.src = null;
3468
+ this.element.src = src;
3469
+ return this.element.loaded;
3470
+ }
3471
+ completeChanged() {
3472
+ if (this.isIgnoringChangesTo("complete")) return;
3473
+ this.loadSourceURL();
3474
+ }
2978
3475
  loadingStyleChanged() {
2979
3476
  if (this.loadingStyle == FrameLoadingStyle.lazy) {
2980
3477
  this.appearanceObserver.start();
@@ -2984,20 +3481,11 @@ class FrameController {
2984
3481
  }
2985
3482
  }
2986
3483
  async loadSourceURL() {
2987
- if (!this.settingSourceURL && this.enabled && this.isActive && (this.reloadable || this.sourceURL != this.currentURL)) {
2988
- const previousURL = this.currentURL;
2989
- this.currentURL = this.sourceURL;
2990
- if (this.sourceURL) {
2991
- try {
2992
- this.element.loaded = this.visit(expandURL(this.sourceURL));
2993
- this.appearanceObserver.stop();
2994
- await this.element.loaded;
2995
- this.hasBeenLoaded = true;
2996
- } catch (error) {
2997
- this.currentURL = previousURL;
2998
- throw error;
2999
- }
3000
- }
3484
+ if (this.enabled && this.isActive && !this.complete && this.sourceURL) {
3485
+ this.element.loaded = this.visit(expandURL(this.sourceURL));
3486
+ this.appearanceObserver.stop();
3487
+ await this.element.loaded;
3488
+ this.hasBeenLoaded = true;
3001
3489
  }
3002
3490
  }
3003
3491
  async loadResponse(fetchResponse) {
@@ -3007,71 +3495,73 @@ class FrameController {
3007
3495
  try {
3008
3496
  const html = await fetchResponse.responseHTML;
3009
3497
  if (html) {
3010
- const {body: body} = parseHTMLDocument(html);
3011
- const snapshot = new Snapshot(await this.extractForeignFrameElement(body));
3012
- const renderer = new FrameRenderer(this.view.snapshot, snapshot, false, false);
3013
- if (this.view.renderPromise) await this.view.renderPromise;
3014
- await this.view.render(renderer);
3015
- session.frameRendered(fetchResponse, this.element);
3016
- session.frameLoaded(this.element);
3017
- this.fetchResponseLoaded(fetchResponse);
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);
3504
+ }
3018
3505
  }
3019
- } catch (error) {
3020
- console.error(error);
3021
- this.view.invalidate();
3022
3506
  } finally {
3023
3507
  this.fetchResponseLoaded = () => {};
3024
3508
  }
3025
3509
  }
3026
3510
  elementAppearedInViewport(element) {
3511
+ this.proposeVisitIfNavigatedWithAction(element, element);
3027
3512
  this.loadSourceURL();
3028
3513
  }
3029
- shouldInterceptLinkClick(element, url) {
3030
- if (element.hasAttribute("data-turbo-method")) {
3031
- return false;
3032
- } else {
3033
- return this.shouldInterceptNavigation(element);
3034
- }
3514
+ willSubmitFormLinkToLocation(link) {
3515
+ return this.shouldInterceptNavigation(link);
3516
+ }
3517
+ submittedFormLinkToLocation(link, _location, form) {
3518
+ const frame = this.findFrameElement(link);
3519
+ if (frame) form.setAttribute("data-turbo-frame", frame.id);
3035
3520
  }
3036
- linkClickIntercepted(element, url) {
3037
- this.reloadable = true;
3038
- this.navigateFrame(element, url);
3521
+ shouldInterceptLinkClick(element, _location, _event) {
3522
+ return this.shouldInterceptNavigation(element);
3039
3523
  }
3040
- shouldInterceptFormSubmission(element, submitter) {
3041
- return this.shouldInterceptNavigation(element, submitter);
3524
+ linkClickIntercepted(element, location) {
3525
+ this.navigateFrame(element, location);
3042
3526
  }
3043
- formSubmissionIntercepted(element, submitter) {
3527
+ willSubmitForm(element, submitter) {
3528
+ return element.closest("turbo-frame") == this.element && this.shouldInterceptNavigation(element, submitter);
3529
+ }
3530
+ formSubmitted(element, submitter) {
3044
3531
  if (this.formSubmission) {
3045
3532
  this.formSubmission.stop();
3046
3533
  }
3047
- this.reloadable = false;
3048
3534
  this.formSubmission = new FormSubmission(this, element, submitter);
3049
3535
  const {fetchRequest: fetchRequest} = this.formSubmission;
3050
- this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
3536
+ this.prepareRequest(fetchRequest);
3051
3537
  this.formSubmission.start();
3052
3538
  }
3053
- prepareHeadersForRequest(headers, request) {
3054
- headers["Turbo-Frame"] = this.id;
3539
+ prepareRequest(request) {
3540
+ var _a;
3541
+ request.headers["Turbo-Frame"] = this.id;
3542
+ if ((_a = this.currentNavigationElement) === null || _a === void 0 ? void 0 : _a.hasAttribute("data-turbo-stream")) {
3543
+ request.acceptResponseType(StreamMessage.contentType);
3544
+ }
3055
3545
  }
3056
- requestStarted(request) {
3546
+ requestStarted(_request) {
3057
3547
  markAsBusy(this.element);
3058
3548
  }
3059
- requestPreventedHandlingResponse(request, response) {
3549
+ requestPreventedHandlingResponse(_request, _response) {
3060
3550
  this.resolveVisitPromise();
3061
3551
  }
3062
3552
  async requestSucceededWithResponse(request, response) {
3063
3553
  await this.loadResponse(response);
3064
3554
  this.resolveVisitPromise();
3065
3555
  }
3066
- requestFailedWithResponse(request, response) {
3067
- console.error(response);
3556
+ async requestFailedWithResponse(request, response) {
3557
+ await this.loadResponse(response);
3068
3558
  this.resolveVisitPromise();
3069
3559
  }
3070
3560
  requestErrored(request, error) {
3071
3561
  console.error(error);
3072
3562
  this.resolveVisitPromise();
3073
3563
  }
3074
- requestFinished(request) {
3564
+ requestFinished(_request) {
3075
3565
  clearBusyState(this.element);
3076
3566
  }
3077
3567
  formSubmissionStarted({formElement: formElement}) {
@@ -3079,11 +3569,15 @@ class FrameController {
3079
3569
  }
3080
3570
  formSubmissionSucceededWithResponse(formSubmission, response) {
3081
3571
  const frame = this.findFrameElement(formSubmission.formElement, formSubmission.submitter);
3082
- this.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
3572
+ frame.delegate.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
3083
3573
  frame.delegate.loadResponse(response);
3574
+ if (!formSubmission.isSafe) {
3575
+ session.clearCache();
3576
+ }
3084
3577
  }
3085
3578
  formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
3086
3579
  this.element.delegate.loadResponse(fetchResponse);
3580
+ session.clearCache();
3087
3581
  }
3088
3582
  formSubmissionErrored(formSubmission, error) {
3089
3583
  console.error(error);
@@ -3091,11 +3585,44 @@ class FrameController {
3091
3585
  formSubmissionFinished({formElement: formElement}) {
3092
3586
  clearBusyState(formElement, this.findFrameElement(formElement));
3093
3587
  }
3094
- allowsImmediateRender(snapshot, resume) {
3095
- return true;
3588
+ allowsImmediateRender({element: newFrame}, options) {
3589
+ const event = dispatch("turbo:before-frame-render", {
3590
+ target: this.element,
3591
+ detail: Object.assign({
3592
+ newFrame: newFrame
3593
+ }, options),
3594
+ cancelable: true
3595
+ });
3596
+ const {defaultPrevented: defaultPrevented, detail: {render: render}} = event;
3597
+ if (this.view.renderer && render) {
3598
+ this.view.renderer.renderElement = render;
3599
+ }
3600
+ return !defaultPrevented;
3601
+ }
3602
+ viewRenderedSnapshot(_snapshot, _isPreview) {}
3603
+ preloadOnLoadLinksForView(element) {
3604
+ session.preloadOnLoadLinksForView(element);
3096
3605
  }
3097
- viewRenderedSnapshot(snapshot, isPreview) {}
3098
3606
  viewInvalidated() {}
3607
+ willRenderFrame(currentElement, _newElement) {
3608
+ this.previousFrameElement = currentElement.cloneNode(true);
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
+ }
3099
3626
  async visit(url) {
3100
3627
  var _a;
3101
3628
  const request = new FetchRequest(this, FetchMethod.get, url, new URLSearchParams, this.element);
@@ -3112,14 +3639,16 @@ class FrameController {
3112
3639
  }
3113
3640
  navigateFrame(element, url, submitter) {
3114
3641
  const frame = this.findFrameElement(element, submitter);
3115
- this.proposeVisitIfNavigatedWithAction(frame, element, submitter);
3116
- frame.setAttribute("reloadable", "");
3117
- frame.src = url;
3642
+ frame.delegate.proposeVisitIfNavigatedWithAction(frame, element, submitter);
3643
+ this.withCurrentNavigationElement(element, (() => {
3644
+ frame.src = url;
3645
+ }));
3118
3646
  }
3119
3647
  proposeVisitIfNavigatedWithAction(frame, element, submitter) {
3120
- const action = getAttribute("data-turbo-action", submitter, element, frame);
3121
- if (isAction(action)) {
3122
- const {visitCachedSnapshot: visitCachedSnapshot} = new SnapshotSubstitution(frame);
3648
+ this.action = getVisitAction(submitter, element, frame);
3649
+ if (this.action) {
3650
+ const pageSnapshot = PageSnapshot.fromElement(frame).clone();
3651
+ const {visitCachedSnapshot: visitCachedSnapshot} = frame.delegate;
3123
3652
  frame.delegate.fetchResponseLoaded = fetchResponse => {
3124
3653
  if (frame.src) {
3125
3654
  const {statusCode: statusCode, redirected: redirected} = fetchResponse;
@@ -3129,16 +3658,70 @@ class FrameController {
3129
3658
  redirected: redirected,
3130
3659
  responseHTML: responseHTML
3131
3660
  };
3132
- session.visit(frame.src, {
3133
- action: action,
3661
+ const options = {
3134
3662
  response: response,
3135
3663
  visitCachedSnapshot: visitCachedSnapshot,
3136
- willRender: false
3137
- });
3664
+ willRender: false,
3665
+ updateHistory: false,
3666
+ restorationIdentifier: this.restorationIdentifier,
3667
+ snapshot: pageSnapshot
3668
+ };
3669
+ if (this.action) options.action = this.action;
3670
+ session.visit(frame.src, options);
3138
3671
  }
3139
3672
  };
3140
3673
  }
3141
3674
  }
3675
+ changeHistory() {
3676
+ if (this.action) {
3677
+ const method = getHistoryMethodForAction(this.action);
3678
+ session.history.update(method, expandURL(this.element.src || ""), this.restorationIdentifier);
3679
+ }
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
+ }
3685
+ willHandleFrameMissingFromResponse(fetchResponse) {
3686
+ this.element.setAttribute("complete", "");
3687
+ const response = fetchResponse.response;
3688
+ const visit = async (url, options = {}) => {
3689
+ if (url instanceof Response) {
3690
+ this.visitResponse(url);
3691
+ } else {
3692
+ session.visit(url, options);
3693
+ }
3694
+ };
3695
+ const event = dispatch("turbo:frame-missing", {
3696
+ target: this.element,
3697
+ detail: {
3698
+ response: response,
3699
+ visit: visit
3700
+ },
3701
+ cancelable: true
3702
+ });
3703
+ return !event.defaultPrevented;
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
+ }
3713
+ async visitResponse(response) {
3714
+ const wrapped = new FetchResponse(response);
3715
+ const responseHTML = await wrapped.responseHTML;
3716
+ const {location: location, redirected: redirected, statusCode: statusCode} = wrapped;
3717
+ return session.visit(location, {
3718
+ response: {
3719
+ redirected: redirected,
3720
+ statusCode: statusCode,
3721
+ responseHTML: responseHTML
3722
+ }
3723
+ });
3724
+ }
3142
3725
  findFrameElement(element, submitter) {
3143
3726
  var _a;
3144
3727
  const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
@@ -3148,18 +3731,20 @@ class FrameController {
3148
3731
  let element;
3149
3732
  const id = CSS.escape(this.id);
3150
3733
  try {
3151
- if (element = activateElement(container.querySelector(`turbo-frame#${id}`), this.currentURL)) {
3734
+ element = activateElement(container.querySelector(`turbo-frame#${id}`), this.sourceURL);
3735
+ if (element) {
3152
3736
  return element;
3153
3737
  }
3154
- if (element = activateElement(container.querySelector(`turbo-frame[src][recurse~=${id}]`), this.currentURL)) {
3738
+ element = activateElement(container.querySelector(`turbo-frame[src][recurse~=${id}]`), this.sourceURL);
3739
+ if (element) {
3155
3740
  await element.loaded;
3156
3741
  return await this.extractForeignFrameElement(element);
3157
3742
  }
3158
- console.error(`Response has no matching <turbo-frame id="${id}"> element`);
3159
3743
  } catch (error) {
3160
3744
  console.error(error);
3745
+ return new FrameElement;
3161
3746
  }
3162
- return new FrameElement;
3747
+ return null;
3163
3748
  }
3164
3749
  formActionIsVisitable(form, submitter) {
3165
3750
  const action = getAction(form, submitter);
@@ -3179,10 +3764,10 @@ class FrameController {
3179
3764
  return !frameElement.disabled;
3180
3765
  }
3181
3766
  }
3182
- if (!session.elementDriveEnabled(element)) {
3767
+ if (!session.elementIsNavigatable(element)) {
3183
3768
  return false;
3184
3769
  }
3185
- if (submitter && !session.elementDriveEnabled(submitter)) {
3770
+ if (submitter && !session.elementIsNavigatable(submitter)) {
3186
3771
  return false;
3187
3772
  }
3188
3773
  return true;
@@ -3198,23 +3783,10 @@ class FrameController {
3198
3783
  return this.element.src;
3199
3784
  }
3200
3785
  }
3201
- get reloadable() {
3202
- const frame = this.findFrameElement(this.element);
3203
- return frame.hasAttribute("reloadable");
3204
- }
3205
- set reloadable(value) {
3206
- const frame = this.findFrameElement(this.element);
3207
- if (value) {
3208
- frame.setAttribute("reloadable", "");
3209
- } else {
3210
- frame.removeAttribute("reloadable");
3211
- }
3212
- }
3213
3786
  set sourceURL(sourceURL) {
3214
- this.settingSourceURL = true;
3215
- this.element.src = sourceURL !== null && sourceURL !== void 0 ? sourceURL : null;
3216
- this.currentURL = this.element.src;
3217
- this.settingSourceURL = false;
3787
+ this.ignoringChangesToAttribute("src", (() => {
3788
+ this.element.src = sourceURL !== null && sourceURL !== void 0 ? sourceURL : null;
3789
+ }));
3218
3790
  }
3219
3791
  get loadingStyle() {
3220
3792
  return this.element.loading;
@@ -3222,6 +3794,18 @@ class FrameController {
3222
3794
  get isLoading() {
3223
3795
  return this.formSubmission !== undefined || this.resolveVisitPromise() !== undefined;
3224
3796
  }
3797
+ get complete() {
3798
+ return this.element.hasAttribute("complete");
3799
+ }
3800
+ set complete(value) {
3801
+ this.ignoringChangesToAttribute("complete", (() => {
3802
+ if (value) {
3803
+ this.element.setAttribute("complete", "");
3804
+ } else {
3805
+ this.element.removeAttribute("complete");
3806
+ }
3807
+ }));
3808
+ }
3225
3809
  get isActive() {
3226
3810
  return this.element.isActive && this.connected;
3227
3811
  }
@@ -3231,17 +3815,18 @@ class FrameController {
3231
3815
  const root = (_a = meta === null || meta === void 0 ? void 0 : meta.content) !== null && _a !== void 0 ? _a : "/";
3232
3816
  return expandURL(root);
3233
3817
  }
3234
- }
3235
-
3236
- class SnapshotSubstitution {
3237
- constructor(element) {
3238
- this.visitCachedSnapshot = ({element: element}) => {
3239
- var _a;
3240
- const {id: id, clone: clone} = this;
3241
- (_a = element.querySelector("#" + id)) === null || _a === void 0 ? void 0 : _a.replaceWith(clone);
3242
- };
3243
- this.clone = element.cloneNode(true);
3244
- this.id = element.id;
3818
+ isIgnoringChangesTo(attributeName) {
3819
+ return this.ignoredAttributes.has(attributeName);
3820
+ }
3821
+ ignoringChangesToAttribute(attributeName, callback) {
3822
+ this.ignoredAttributes.add(attributeName);
3823
+ callback();
3824
+ this.ignoredAttributes.delete(attributeName);
3825
+ }
3826
+ withCurrentNavigationElement(element, callback) {
3827
+ this.currentNavigationElement = element;
3828
+ callback();
3829
+ delete this.currentNavigationElement;
3245
3830
  }
3246
3831
  }
3247
3832
 
@@ -3271,42 +3856,10 @@ function activateElement(element, currentURL) {
3271
3856
  }
3272
3857
  }
3273
3858
 
3274
- const StreamActions = {
3275
- after() {
3276
- this.targetElements.forEach((e => {
3277
- var _a;
3278
- return (_a = e.parentElement) === null || _a === void 0 ? void 0 : _a.insertBefore(this.templateContent, e.nextSibling);
3279
- }));
3280
- },
3281
- append() {
3282
- this.removeDuplicateTargetChildren();
3283
- this.targetElements.forEach((e => e.append(this.templateContent)));
3284
- },
3285
- before() {
3286
- this.targetElements.forEach((e => {
3287
- var _a;
3288
- return (_a = e.parentElement) === null || _a === void 0 ? void 0 : _a.insertBefore(this.templateContent, e);
3289
- }));
3290
- },
3291
- prepend() {
3292
- this.removeDuplicateTargetChildren();
3293
- this.targetElements.forEach((e => e.prepend(this.templateContent)));
3294
- },
3295
- remove() {
3296
- this.targetElements.forEach((e => e.remove()));
3297
- },
3298
- replace() {
3299
- this.targetElements.forEach((e => e.replaceWith(this.templateContent)));
3300
- },
3301
- update() {
3302
- this.targetElements.forEach((e => {
3303
- e.innerHTML = "";
3304
- e.append(this.templateContent);
3305
- }));
3306
- }
3307
- };
3308
-
3309
3859
  class StreamElement extends HTMLElement {
3860
+ static async renderElement(newElement) {
3861
+ await newElement.performAction();
3862
+ }
3310
3863
  async connectedCallback() {
3311
3864
  try {
3312
3865
  await this.render();
@@ -3319,9 +3872,10 @@ class StreamElement extends HTMLElement {
3319
3872
  async render() {
3320
3873
  var _a;
3321
3874
  return (_a = this.renderPromise) !== null && _a !== void 0 ? _a : this.renderPromise = (async () => {
3322
- if (this.dispatchEvent(this.beforeRenderEvent)) {
3875
+ const event = this.beforeRenderEvent;
3876
+ if (this.dispatchEvent(event)) {
3323
3877
  await nextAnimationFrame();
3324
- this.performAction();
3878
+ await event.detail.render(this);
3325
3879
  }
3326
3880
  })();
3327
3881
  }
@@ -3336,7 +3890,7 @@ class StreamElement extends HTMLElement {
3336
3890
  get duplicateChildren() {
3337
3891
  var _a;
3338
3892
  const existingChildren = this.targetElements.flatMap((e => [ ...e.children ])).filter((c => !!c.id));
3339
- const newChildrenIds = [ ...(_a = this.templateContent) === null || _a === void 0 ? void 0 : _a.children ].filter((c => !!c.id)).map((c => c.id));
3893
+ const newChildrenIds = [ ...((_a = this.templateContent) === null || _a === void 0 ? void 0 : _a.children) || [] ].filter((c => !!c.id)).map((c => c.id));
3340
3894
  return existingChildren.filter((c => newChildrenIds.includes(c.id)));
3341
3895
  }
3342
3896
  get performAction() {
@@ -3362,7 +3916,11 @@ class StreamElement extends HTMLElement {
3362
3916
  return this.templateElement.content.cloneNode(true);
3363
3917
  }
3364
3918
  get templateElement() {
3365
- if (this.firstElementChild instanceof HTMLTemplateElement) {
3919
+ if (this.firstElementChild === null) {
3920
+ const template = this.ownerDocument.createElement("template");
3921
+ this.appendChild(template);
3922
+ return template;
3923
+ } else if (this.firstElementChild instanceof HTMLTemplateElement) {
3366
3924
  return this.firstElementChild;
3367
3925
  }
3368
3926
  this.raise("first child element must be a <template> element");
@@ -3386,7 +3944,11 @@ class StreamElement extends HTMLElement {
3386
3944
  get beforeRenderEvent() {
3387
3945
  return new CustomEvent("turbo:before-stream-render", {
3388
3946
  bubbles: true,
3389
- cancelable: true
3947
+ cancelable: true,
3948
+ detail: {
3949
+ newStream: this,
3950
+ render: StreamElement.renderElement
3951
+ }
3390
3952
  });
3391
3953
  }
3392
3954
  get targetElementsById() {
@@ -3409,17 +3971,45 @@ class StreamElement extends HTMLElement {
3409
3971
  }
3410
3972
  }
3411
3973
 
3974
+ class StreamSourceElement extends HTMLElement {
3975
+ constructor() {
3976
+ super(...arguments);
3977
+ this.streamSource = null;
3978
+ }
3979
+ connectedCallback() {
3980
+ this.streamSource = this.src.match(/^ws{1,2}:/) ? new WebSocket(this.src) : new EventSource(this.src);
3981
+ connectStreamSource(this.streamSource);
3982
+ }
3983
+ disconnectedCallback() {
3984
+ if (this.streamSource) {
3985
+ disconnectStreamSource(this.streamSource);
3986
+ }
3987
+ }
3988
+ get src() {
3989
+ return this.getAttribute("src") || "";
3990
+ }
3991
+ }
3992
+
3412
3993
  FrameElement.delegateConstructor = FrameController;
3413
3994
 
3414
- customElements.define("turbo-frame", FrameElement);
3995
+ if (customElements.get("turbo-frame") === undefined) {
3996
+ customElements.define("turbo-frame", FrameElement);
3997
+ }
3998
+
3999
+ if (customElements.get("turbo-stream") === undefined) {
4000
+ customElements.define("turbo-stream", StreamElement);
4001
+ }
3415
4002
 
3416
- customElements.define("turbo-stream", StreamElement);
4003
+ if (customElements.get("turbo-stream-source") === undefined) {
4004
+ customElements.define("turbo-stream-source", StreamSourceElement);
4005
+ }
3417
4006
 
3418
4007
  (() => {
3419
4008
  let element = document.currentScript;
3420
4009
  if (!element) return;
3421
4010
  if (element.hasAttribute("data-turbo-suppress-warning")) return;
3422
- while (element = element.parentElement) {
4011
+ element = element.parentElement;
4012
+ while (element) {
3423
4013
  if (element == document.body) {
3424
4014
  return console.warn(unindent`
3425
4015
  You are loading Turbo from a <script> element inside the <body> element. This is probably not what you meant to do!
@@ -3432,6 +4022,7 @@ customElements.define("turbo-stream", StreamElement);
3432
4022
  Suppress this warning by adding a "data-turbo-suppress-warning" attribute to: %s
3433
4023
  `, element.outerHTML);
3434
4024
  }
4025
+ element = element.parentElement;
3435
4026
  }
3436
4027
  })();
3437
4028
 
@@ -3441,8 +4032,17 @@ start();
3441
4032
 
3442
4033
  var turbo_es2017Esm = Object.freeze({
3443
4034
  __proto__: null,
4035
+ FrameElement: FrameElement,
4036
+ get FrameLoadingStyle() {
4037
+ return FrameLoadingStyle;
4038
+ },
4039
+ FrameRenderer: FrameRenderer,
3444
4040
  PageRenderer: PageRenderer,
3445
4041
  PageSnapshot: PageSnapshot,
4042
+ StreamActions: StreamActions,
4043
+ StreamElement: StreamElement,
4044
+ StreamSourceElement: StreamSourceElement,
4045
+ cache: cache,
3446
4046
  clearCache: clearCache,
3447
4047
  connectStreamSource: connectStreamSource,
3448
4048
  disconnectStreamSource: disconnectStreamSource,
@@ -3451,6 +4051,7 @@ var turbo_es2017Esm = Object.freeze({
3451
4051
  renderStreamMessage: renderStreamMessage,
3452
4052
  session: session,
3453
4053
  setConfirmMethod: setConfirmMethod,
4054
+ setFormMode: setFormMode,
3454
4055
  setProgressBarDelay: setProgressBarDelay,
3455
4056
  start: start,
3456
4057
  visit: visit
@@ -3503,7 +4104,9 @@ class TurboCableStreamSourceElement extends HTMLElement {
3503
4104
  async connectedCallback() {
3504
4105
  connectStreamSource(this);
3505
4106
  this.subscription = await subscribeTo(this.channel, {
3506
- received: this.dispatchMessageEvent.bind(this)
4107
+ received: this.dispatchMessageEvent.bind(this),
4108
+ connected: this.subscriptionConnected.bind(this),
4109
+ disconnected: this.subscriptionDisconnected.bind(this)
3507
4110
  });
3508
4111
  }
3509
4112
  disconnectedCallback() {
@@ -3516,6 +4119,12 @@ class TurboCableStreamSourceElement extends HTMLElement {
3516
4119
  });
3517
4120
  return this.dispatchEvent(event);
3518
4121
  }
4122
+ subscriptionConnected() {
4123
+ this.setAttribute("connected", "");
4124
+ }
4125
+ subscriptionDisconnected() {
4126
+ this.removeAttribute("connected");
4127
+ }
3519
4128
  get channel() {
3520
4129
  const channel = this.getAttribute("channel");
3521
4130
  const signed_stream_name = this.getAttribute("signed-stream-name");
@@ -3529,16 +4138,60 @@ class TurboCableStreamSourceElement extends HTMLElement {
3529
4138
  }
3530
4139
  }
3531
4140
 
3532
- 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
+ }
3533
4144
 
3534
- function overrideMethodWithFormmethod({detail: {formSubmission: {fetchRequest: fetchRequest, submitter: submitter}}}) {
3535
- const formMethod = submitter?.formMethod;
3536
- if (formMethod && fetchRequest.body.has("_method")) {
3537
- fetchRequest.body.set("_method", formMethod);
4145
+ function encodeMethodIntoRequestBody(event) {
4146
+ if (event.target instanceof HTMLFormElement) {
4147
+ const {target: form, detail: {fetchOptions: fetchOptions}} = event;
4148
+ form.addEventListener("turbo:submit-start", (({detail: {formSubmission: {submitter: submitter}}}) => {
4149
+ const body = isBodyInit(fetchOptions.body) ? fetchOptions.body : new URLSearchParams;
4150
+ const method = determineFetchMethod(submitter, body, form);
4151
+ if (!/get/i.test(method)) {
4152
+ if (/post/i.test(method)) {
4153
+ body.delete("_method");
4154
+ } else {
4155
+ body.set("_method", method);
4156
+ }
4157
+ fetchOptions.method = "post";
4158
+ }
4159
+ }), {
4160
+ once: true
4161
+ });
3538
4162
  }
3539
4163
  }
3540
4164
 
3541
- addEventListener("turbo:submit-start", overrideMethodWithFormmethod);
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.hasAttribute("formmethod")) {
4181
+ return submitter.formMethod;
4182
+ } else {
4183
+ return null;
4184
+ }
4185
+ } else {
4186
+ return null;
4187
+ }
4188
+ }
4189
+
4190
+ function isBodyInit(body) {
4191
+ return body instanceof FormData || body instanceof URLSearchParams;
4192
+ }
4193
+
4194
+ addEventListener("turbo:before-fetch-request", encodeMethodIntoRequestBody);
3542
4195
 
3543
4196
  var adapters = {
3544
4197
  logger: self.console,