turbo-rails 0.5.9 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 025ba92afa27783a938ec698ff465685559d71698ab3023b091a54d14f0d6dd1
4
- data.tar.gz: ae377ccfdb57c9f9f2f862dc81e5863a5bc614002bbdfe39547cb1ede2aa797a
3
+ metadata.gz: 42d7a658511c698c83b4e41853f96f5b2534a9ee81b89500044a22d6695a5150
4
+ data.tar.gz: 46e9abb9a01e4c9b50d2c2b704ff0e522a7231496b58d7acbc78303cdef67486
5
5
  SHA512:
6
- metadata.gz: a7f03c2b76b07d65f64b3170b9f9ed6c1a9c4429c9f7eee3abf8bffbdbed8a320806f239df725c5aad37c826d2171ec2abc0c7787d42f929e2741584fc4d84e3
7
- data.tar.gz: b2566bd6203b982d5b3cad1c852517a41b3a11ca8c8f097cac3cf7ff45f8d83db3eab3337807a80299cf61fc21f58990275f2e5dc92af75b03b167bf04f05ecc
6
+ metadata.gz: be906021b12ba905c7ce316900cb28db1ec22cbec561c2e009758a58cb059c82984862bae1f045c2f60b1db9fd396ebb2c88ba07c08a2871d1f39556f67cd2b1
7
+ data.tar.gz: de3c17c9f07971421add4c6038f385b609d465eb939f1860900c635b35b72b65ebbbea030a2d45722bb6bda86b2b38e5d794c005e063698c3ec63478fd3dd083
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Turbo
2
2
 
3
- [Turbo](https://turbo.hotwire.dev) gives you the speed of a single-page web application without having to write any JavaScript. Turbo accelerates links and form submissions without requiring you to change your server-side generated HTML. It lets you carve up a page into independent frames, which can be lazy-loaded and operate as independent components. And finally, helps you make partial page updates using just HTML and a set of CRUD-like container tags. These three techniques reduce the amount of custom JavaScript that many web applications need to write by an order of magnitude. And for the few dynamic bits that are left, you're invited to finish the job with [Stimulus](https://github.com/hotwired/stimulus).
3
+ [Turbo](https://turbo.hotwired.dev) gives you the speed of a single-page web application without having to write any JavaScript. Turbo accelerates links and form submissions without requiring you to change your server-side generated HTML. It lets you carve up a page into independent frames, which can be lazy-loaded and operate as independent components. And finally, helps you make partial page updates using just HTML and a set of CRUD-like container tags. These three techniques reduce the amount of custom JavaScript that many web applications need to write by an order of magnitude. And for the few dynamic bits that are left, you're invited to finish the job with [Stimulus](https://github.com/hotwired/stimulus).
4
4
 
5
5
  On top of accelerating web applications, Turbo was built from the ground-up to form the foundation of hybrid native applications. Write the navigational shell of your Android or iOS app using the standard platform tooling, then seamlessly fill in features from the web, following native navigation patterns. Not every mobile screen needs to be written in Swift or Kotlin to feel native. With Turbo, you spend less time wrangling JSON, waiting on app stores to approve updates, or reimplementing features you've already created in HTML.
6
6
 
@@ -40,32 +40,29 @@ The JavaScript for Turbo can either be run through the asset pipeline, which is
40
40
 
41
41
  Running `turbo:install` will install through NPM if Webpacker is installed in the application. Otherwise the asset pipeline version is used.
42
42
 
43
- If you're using Webpack and need to use either the cable consumer or the Turbo instance, you can import [`Turbo`](https://turbo.hotwire.dev/reference/drive) and/or [`cable`](https://github.com/hotwired/turbo-rails/blob/main/app/javascript/turbo/cable.js) (`import { Turbo, cable } from "@hotwired/turbo-rails"`), but ensure that your application actually *uses* the members it `import`s when using this style (see [turbo-rails#48](https://github.com/hotwired/turbo-rails/issues/48)).
43
+ If you're using Webpack and need to use the cable consumer, you can import [`cable`](https://github.com/hotwired/turbo-rails/blob/main/app/javascript/turbo/cable.js) (`import { cable } from "@hotwired/turbo-rails"`), but ensure that your application actually *uses* the members it `import`s when using this style (see [turbo-rails#48](https://github.com/hotwired/turbo-rails/issues/48)).
44
44
 
45
- If you're using a [native adapter](https://turbo.hotwire.dev/handbook/native), you'll need to assign `window.Turbo`, even if it's not used for anything else:
45
+ The `Turbo` instance is automatically assigned to `window.Turbo` upon import:
46
46
 
47
47
  ```js
48
- import { Turbo } from "@hotwired/turbo-rails"
49
- window.Turbo = Turbo
48
+ import "@hotwired/turbo-rails"
50
49
  ```
51
50
 
52
51
  ## Usage
53
52
 
54
- You can watch [the video introduction to Hotwire](https://hotwire.dev/#screencast), which focuses extensively on demonstration Turbo in a Rails demo. Then you should familiarize yourself with [Turbo handbook](https://turbo.hotwire.dev/handbook/introduction) to understand Drive, Frames, and Streams in-depth. Finally, dive into the code documentation by starting with [`Turbo::FramesHelper`](https://github.com/hotwired/turbo-rails/blob/main/app/helpers/turbo/frames_helper.rb), [`Turbo::StreamsHelper`](https://github.com/hotwired/turbo-rails/blob/main/app/helpers/turbo/streams_helper.rb), [`Turbo::Streams::TagBuilder`](https://github.com/hotwired/turbo-rails/blob/main/app/models/turbo/streams/tag_builder.rb), and [`Turbo::Broadcastable`](https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb).
53
+ You can watch [the video introduction to Hotwire](https://hotwired.dev/#screencast), which focuses extensively on demonstration Turbo in a Rails demo. Then you should familiarize yourself with [Turbo handbook](https://turbo.hotwired.dev/handbook/introduction) to understand Drive, Frames, and Streams in-depth. Finally, dive into the code documentation by starting with [`Turbo::FramesHelper`](https://github.com/hotwired/turbo-rails/blob/main/app/helpers/turbo/frames_helper.rb), [`Turbo::StreamsHelper`](https://github.com/hotwired/turbo-rails/blob/main/app/helpers/turbo/streams_helper.rb), [`Turbo::Streams::TagBuilder`](https://github.com/hotwired/turbo-rails/blob/main/app/models/turbo/streams/tag_builder.rb), and [`Turbo::Broadcastable`](https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb).
55
54
 
56
55
 
57
56
  ## Compatibility with Rails UJS
58
57
 
59
- Rails UJS includes helpers for sending links and forms over XMLHttpRequest, so you can respond with Ajax. Turbo supersedes this functionality, so you should ensure that you're either running Rails 6.1 with the defaults that turn this off for forms, or that you add `config.action_view.form_with_generates_remote_forms = false` to your `config/application.rb`.
60
-
61
- Note that the helpers that turn `link_to` into remote invocations will _not_ currently work with Turbo. Links that have been made remote will not stick within frames nor will they allow you to respond with turbo stream actions. The recommendation is to replace these links with styled `button_to`, so you'll flow through a regular form, and you'll be better off with a11y compliance.
62
-
63
- You can still use the `data-confirm` and `data-disable-with`.
58
+ Turbo can coexist with Rails UJS, but you need to take a series of upgrade steps to make it happen. See [the upgrading guide](https://github.com/hotwired/turbo-rails/blob/main/UPGRADING.md).
64
59
 
65
60
 
66
61
  ## Development
67
62
 
68
63
  * To run the Rails tests: `bundle exec rake`.
64
+ * To install dependencies: `bundle install`
65
+ * To prepare the test database: `cd tests/dummy; RAILS_ENV=test ./bin/rails db:migrate`
69
66
  * To compile the JavaScript for the asset pipeline: `yarn build`
70
67
 
71
68
 
data/Rakefile CHANGED
@@ -2,6 +2,10 @@ require "bundler/setup"
2
2
  require "bundler/gem_tasks"
3
3
  require "rake/testtask"
4
4
 
5
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
6
+ load "rails/tasks/engine.rake"
7
+ load "rails/tasks/statistics.rake"
8
+
5
9
  Rake::TestTask.new do |test|
6
10
  test.libs << "test"
7
11
  test.test_files = FileList["test/**/*_test.rb"]
@@ -55,7 +55,7 @@ class FrameElement extends HTMLElement {
55
55
  this.delegate = new FrameElement.delegateConstructor(this);
56
56
  }
57
57
  static get observedAttributes() {
58
- return [ "loading", "src" ];
58
+ return [ "disabled", "loading", "src" ];
59
59
  }
60
60
  connectedCallback() {
61
61
  this.delegate.connect();
@@ -63,11 +63,18 @@ class FrameElement extends HTMLElement {
63
63
  disconnectedCallback() {
64
64
  this.delegate.disconnect();
65
65
  }
66
+ reload() {
67
+ const {src: src} = this;
68
+ this.src = null;
69
+ this.src = src;
70
+ }
66
71
  attributeChangedCallback(name) {
67
72
  if (name == "loading") {
68
73
  this.delegate.loadingStyleChanged();
69
74
  } else if (name == "src") {
70
75
  this.delegate.sourceURLChanged();
76
+ } else {
77
+ this.delegate.disabledChanged();
71
78
  }
72
79
  }
73
80
  get src() {
@@ -133,9 +140,7 @@ function frameLoadingStyleFromString(style) {
133
140
  }
134
141
 
135
142
  function expandURL(locatable) {
136
- const anchor = document.createElement("a");
137
- anchor.href = locatable.toString();
138
- return new URL(anchor.href);
143
+ return new URL(locatable.toString(), document.baseURI);
139
144
  }
140
145
 
141
146
  function getAnchor(url) {
@@ -144,8 +149,6 @@ function getAnchor(url) {
144
149
  return url.hash.slice(1);
145
150
  } else if (anchorMatch = url.href.match(/#(.*)$/)) {
146
151
  return anchorMatch[1];
147
- } else {
148
- return "";
149
152
  }
150
153
  }
151
154
 
@@ -162,13 +165,17 @@ function isPrefixedBy(baseURL, url) {
162
165
  return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix);
163
166
  }
164
167
 
168
+ function getRequestURL(url) {
169
+ const anchor = getAnchor(url);
170
+ return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href;
171
+ }
172
+
165
173
  function toCacheKey(url) {
166
- const anchorLength = url.hash.length;
167
- if (anchorLength < 2) {
168
- return url.href;
169
- } else {
170
- return url.href.slice(0, -anchorLength);
171
- }
174
+ return getRequestURL(url);
175
+ }
176
+
177
+ function urlsAreEqual(left, right) {
178
+ return expandURL(left).href == expandURL(right).href;
172
179
  }
173
180
 
174
181
  function getPathComponents(url) {
@@ -321,8 +328,10 @@ function fetchMethodFromString(method) {
321
328
  class FetchRequest {
322
329
  constructor(delegate, method, location, body = new URLSearchParams) {
323
330
  this.abortController = new AbortController;
331
+ this.resolveRequestPromise = value => {};
324
332
  this.delegate = delegate;
325
333
  this.method = method;
334
+ this.headers = this.defaultHeaders;
326
335
  if (this.isIdempotent) {
327
336
  this.url = mergeFormDataEntries(location, [ ...body.entries() ]);
328
337
  } else {
@@ -343,12 +352,10 @@ class FetchRequest {
343
352
  this.abortController.abort();
344
353
  }
345
354
  async perform() {
355
+ var _a, _b;
346
356
  const {fetchOptions: fetchOptions} = this;
347
- dispatch("turbo:before-fetch-request", {
348
- detail: {
349
- fetchOptions: fetchOptions
350
- }
351
- });
357
+ (_b = (_a = this.delegate).prepareHeadersForRequest) === null || _b === void 0 ? void 0 : _b.call(_a, this.headers, this);
358
+ await this.allowRequestToBeIntercepted(fetchOptions);
352
359
  try {
353
360
  this.delegate.requestStarted(this);
354
361
  const response = await fetch(this.url.href, fetchOptions);
@@ -387,23 +394,28 @@ class FetchRequest {
387
394
  signal: this.abortSignal
388
395
  };
389
396
  }
397
+ get defaultHeaders() {
398
+ return {
399
+ Accept: "text/html, application/xhtml+xml"
400
+ };
401
+ }
390
402
  get isIdempotent() {
391
403
  return this.method == FetchMethod.get;
392
404
  }
393
- get headers() {
394
- const headers = Object.assign({}, this.defaultHeaders);
395
- if (typeof this.delegate.prepareHeadersForRequest == "function") {
396
- this.delegate.prepareHeadersForRequest(headers, this);
397
- }
398
- return headers;
399
- }
400
405
  get abortSignal() {
401
406
  return this.abortController.signal;
402
407
  }
403
- get defaultHeaders() {
404
- return {
405
- Accept: "text/html, application/xhtml+xml"
406
- };
408
+ async allowRequestToBeIntercepted(fetchOptions) {
409
+ const requestInterception = new Promise((resolve => this.resolveRequestPromise = resolve));
410
+ const event = dispatch("turbo:before-fetch-request", {
411
+ cancelable: true,
412
+ detail: {
413
+ fetchOptions: fetchOptions,
414
+ url: this.url.href,
415
+ resume: this.resolveRequestPromise
416
+ }
417
+ });
418
+ if (event.defaultPrevented) await requestInterception;
407
419
  }
408
420
  }
409
421
 
@@ -548,6 +560,9 @@ class FormSubmission {
548
560
  var _a;
549
561
  return formEnctypeFromString(((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formenctype")) || this.formElement.enctype);
550
562
  }
563
+ get isIdempotent() {
564
+ return this.fetchRequest.isIdempotent;
565
+ }
551
566
  get stringFormData() {
552
567
  return [ ...this.formData ].reduce(((entries, [name, value]) => entries.concat(typeof value == "string" ? [ [ name, value ] ] : [])), []);
553
568
  }
@@ -639,8 +654,8 @@ function buildFormData(formElement, submitter) {
639
654
  const formData = new FormData(formElement);
640
655
  const name = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("name");
641
656
  const value = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("value");
642
- if (name && formData.get(name) != value) {
643
- formData.append(name, value || "");
657
+ if (name && value != null && formData.get(name) != value) {
658
+ formData.append(name, value);
644
659
  }
645
660
  return formData;
646
661
  }
@@ -682,6 +697,9 @@ class Snapshot {
682
697
  return null;
683
698
  }
684
699
  }
700
+ get isConnected() {
701
+ return this.element.isConnected;
702
+ }
685
703
  get firstAutofocusableElement() {
686
704
  return this.element.querySelector("[autofocus]");
687
705
  }
@@ -691,8 +709,16 @@ class Snapshot {
691
709
  getPermanentElementById(id) {
692
710
  return this.element.querySelector(`#${id}[data-turbo-permanent]`);
693
711
  }
694
- getPermanentElementsPresentInSnapshot(snapshot) {
695
- return this.permanentElements.filter((({id: id}) => snapshot.getPermanentElementById(id)));
712
+ getPermanentElementMapForSnapshot(snapshot) {
713
+ const permanentElementMap = {};
714
+ for (const currentPermanentElement of this.permanentElements) {
715
+ const {id: id} = currentPermanentElement;
716
+ const newPermanentElement = snapshot.getPermanentElementById(id);
717
+ if (newPermanentElement) {
718
+ permanentElementMap[id] = [ currentPermanentElement, newPermanentElement ];
719
+ }
720
+ }
721
+ return permanentElementMap;
696
722
  }
697
723
  }
698
724
 
@@ -722,6 +748,8 @@ class FormInterceptor {
722
748
 
723
749
  class View {
724
750
  constructor(delegate, element) {
751
+ this.resolveRenderPromise = value => {};
752
+ this.resolveInterceptionPromise = value => {};
725
753
  this.delegate = delegate;
726
754
  this.element = element;
727
755
  }
@@ -729,6 +757,7 @@ class View {
729
757
  const element = this.snapshot.getElementForAnchor(anchor);
730
758
  if (element) {
731
759
  this.scrollToElement(element);
760
+ this.focusElement(element);
732
761
  } else {
733
762
  this.scrollToPosition({
734
763
  x: 0,
@@ -736,9 +765,23 @@ class View {
736
765
  });
737
766
  }
738
767
  }
768
+ scrollToAnchorFromLocation(location) {
769
+ this.scrollToAnchor(getAnchor(location));
770
+ }
739
771
  scrollToElement(element) {
740
772
  element.scrollIntoView();
741
773
  }
774
+ focusElement(element) {
775
+ if (element instanceof HTMLElement) {
776
+ if (element.hasAttribute("tabindex")) {
777
+ element.focus();
778
+ } else {
779
+ element.setAttribute("tabindex", "-1");
780
+ element.focus();
781
+ element.removeAttribute("tabindex");
782
+ }
783
+ }
784
+ }
742
785
  scrollToPosition({x: x, y: y}) {
743
786
  this.scrollRoot.scrollTo(x, y);
744
787
  }
@@ -746,20 +789,22 @@ class View {
746
789
  return window;
747
790
  }
748
791
  async render(renderer) {
749
- if (this.renderer) {
750
- throw new Error("rendering is already in progress");
751
- }
752
792
  const {isPreview: isPreview, shouldRender: shouldRender, newSnapshot: snapshot} = renderer;
753
793
  if (shouldRender) {
754
794
  try {
795
+ this.renderPromise = new Promise((resolve => this.resolveRenderPromise = resolve));
755
796
  this.renderer = renderer;
756
797
  this.prepareToRenderSnapshot(renderer);
757
- this.delegate.viewWillRenderSnapshot(snapshot, isPreview);
798
+ const renderInterception = new Promise((resolve => this.resolveInterceptionPromise = resolve));
799
+ const immediateRender = this.delegate.allowsImmediateRender(snapshot, this.resolveInterceptionPromise);
800
+ if (!immediateRender) await renderInterception;
758
801
  await this.renderSnapshot(renderer);
759
802
  this.delegate.viewRenderedSnapshot(snapshot, isPreview);
760
803
  this.finishRenderingSnapshot(renderer);
761
804
  } finally {
762
805
  delete this.renderer;
806
+ this.resolveRenderPromise(undefined);
807
+ delete this.renderPromise;
763
808
  }
764
809
  } else {
765
810
  this.invalidate();
@@ -837,6 +882,56 @@ class LinkInterceptor {
837
882
  }
838
883
  }
839
884
 
885
+ class Bardo {
886
+ constructor(permanentElementMap) {
887
+ this.permanentElementMap = permanentElementMap;
888
+ }
889
+ static preservingPermanentElements(permanentElementMap, callback) {
890
+ const bardo = new this(permanentElementMap);
891
+ bardo.enter();
892
+ callback();
893
+ bardo.leave();
894
+ }
895
+ enter() {
896
+ for (const id in this.permanentElementMap) {
897
+ const [, newPermanentElement] = this.permanentElementMap[id];
898
+ this.replaceNewPermanentElementWithPlaceholder(newPermanentElement);
899
+ }
900
+ }
901
+ leave() {
902
+ for (const id in this.permanentElementMap) {
903
+ const [currentPermanentElement] = this.permanentElementMap[id];
904
+ this.replaceCurrentPermanentElementWithClone(currentPermanentElement);
905
+ this.replacePlaceholderWithPermanentElement(currentPermanentElement);
906
+ }
907
+ }
908
+ replaceNewPermanentElementWithPlaceholder(permanentElement) {
909
+ const placeholder = createPlaceholderForPermanentElement(permanentElement);
910
+ permanentElement.replaceWith(placeholder);
911
+ }
912
+ replaceCurrentPermanentElementWithClone(permanentElement) {
913
+ const clone = permanentElement.cloneNode(true);
914
+ permanentElement.replaceWith(clone);
915
+ }
916
+ replacePlaceholderWithPermanentElement(permanentElement) {
917
+ const placeholder = this.getPlaceholderById(permanentElement.id);
918
+ placeholder === null || placeholder === void 0 ? void 0 : placeholder.replaceWith(permanentElement);
919
+ }
920
+ getPlaceholderById(id) {
921
+ return this.placeholders.find((element => element.content == id));
922
+ }
923
+ get placeholders() {
924
+ return [ ...document.querySelectorAll("meta[name=turbo-permanent-placeholder][content]") ];
925
+ }
926
+ }
927
+
928
+ function createPlaceholderForPermanentElement(permanentElement) {
929
+ const element = document.createElement("meta");
930
+ element.setAttribute("name", "turbo-permanent-placeholder");
931
+ element.setAttribute("content", permanentElement.id);
932
+ return element;
933
+ }
934
+
840
935
  class Renderer {
841
936
  constructor(currentSnapshot, newSnapshot, isPreview) {
842
937
  this.currentSnapshot = currentSnapshot;
@@ -864,6 +959,9 @@ class Renderer {
864
959
  return element;
865
960
  } else {
866
961
  const createdScriptElement = document.createElement("script");
962
+ if (this.cspNonce) {
963
+ createdScriptElement.nonce = this.cspNonce;
964
+ }
867
965
  createdScriptElement.textContent = element.textContent;
868
966
  createdScriptElement.async = false;
869
967
  copyElementAttributes(createdScriptElement, element);
@@ -871,28 +969,29 @@ class Renderer {
871
969
  }
872
970
  }
873
971
  preservingPermanentElements(callback) {
874
- const placeholders = relocatePermanentElements(this.currentSnapshot, this.newSnapshot);
875
- callback();
876
- replacePlaceholderElementsWithClonedPermanentElements(placeholders);
972
+ Bardo.preservingPermanentElements(this.permanentElementMap, callback);
877
973
  }
878
974
  focusFirstAutofocusableElement() {
879
- const element = this.newSnapshot.firstAutofocusableElement;
975
+ const element = this.connectedSnapshot.firstAutofocusableElement;
880
976
  if (elementIsFocusable(element)) {
881
977
  element.focus();
882
978
  }
883
979
  }
980
+ get connectedSnapshot() {
981
+ return this.newSnapshot.isConnected ? this.newSnapshot : this.currentSnapshot;
982
+ }
884
983
  get currentElement() {
885
984
  return this.currentSnapshot.element;
886
985
  }
887
986
  get newElement() {
888
987
  return this.newSnapshot.element;
889
988
  }
890
- }
891
-
892
- function replaceElementWithElement(fromElement, toElement) {
893
- const parentElement = fromElement.parentElement;
894
- if (parentElement) {
895
- return parentElement.replaceChild(toElement, fromElement);
989
+ get permanentElementMap() {
990
+ return this.currentSnapshot.getPermanentElementMapForSnapshot(this.newSnapshot);
991
+ }
992
+ get cspNonce() {
993
+ var _a;
994
+ return (_a = document.head.querySelector('meta[name="csp-nonce"]')) === null || _a === void 0 ? void 0 : _a.getAttribute("content");
896
995
  }
897
996
  }
898
997
 
@@ -902,37 +1001,6 @@ function copyElementAttributes(destinationElement, sourceElement) {
902
1001
  }
903
1002
  }
904
1003
 
905
- function createPlaceholderForPermanentElement(permanentElement) {
906
- const element = document.createElement("meta");
907
- element.setAttribute("name", "turbo-permanent-placeholder");
908
- element.setAttribute("content", permanentElement.id);
909
- return {
910
- element: element,
911
- permanentElement: permanentElement
912
- };
913
- }
914
-
915
- function replacePlaceholderElementsWithClonedPermanentElements(placeholders) {
916
- for (const {element: element, permanentElement: permanentElement} of placeholders) {
917
- const clonedElement = permanentElement.cloneNode(true);
918
- replaceElementWithElement(element, clonedElement);
919
- }
920
- }
921
-
922
- function relocatePermanentElements(currentSnapshot, newSnapshot) {
923
- return currentSnapshot.getPermanentElementsPresentInSnapshot(newSnapshot).reduce(((placeholders, permanentElement) => {
924
- const newElement = newSnapshot.getPermanentElementById(permanentElement.id);
925
- if (newElement) {
926
- const placeholder = createPlaceholderForPermanentElement(permanentElement);
927
- replaceElementWithElement(permanentElement, placeholder.element);
928
- replaceElementWithElement(newElement, permanentElement);
929
- return [ ...placeholders, placeholder ];
930
- } else {
931
- return placeholders;
932
- }
933
- }), []);
934
- }
935
-
936
1004
  function elementIsFocusable(element) {
937
1005
  return element && typeof element.focus == "function";
938
1006
  }
@@ -949,6 +1017,8 @@ class FrameRenderer extends Renderer {
949
1017
  this.scrollFrameIntoView();
950
1018
  await nextAnimationFrame();
951
1019
  this.focusFirstAutofocusableElement();
1020
+ await nextAnimationFrame();
1021
+ this.activateScriptElements();
952
1022
  }
953
1023
  loadFrameElement() {
954
1024
  var _a;
@@ -975,6 +1045,15 @@ class FrameRenderer extends Renderer {
975
1045
  }
976
1046
  return false;
977
1047
  }
1048
+ activateScriptElements() {
1049
+ for (const inertScriptElement of this.newScriptElements) {
1050
+ const activatedScriptElement = this.createScriptElement(inertScriptElement);
1051
+ inertScriptElement.replaceWith(activatedScriptElement);
1052
+ }
1053
+ }
1054
+ get newScriptElements() {
1055
+ return this.currentElement.querySelectorAll("script");
1056
+ }
978
1057
  }
979
1058
 
980
1059
  function readScrollLogicalPosition(value, defaultValue) {
@@ -985,446 +1064,121 @@ function readScrollLogicalPosition(value, defaultValue) {
985
1064
  }
986
1065
  }
987
1066
 
988
- class FrameController {
989
- constructor(element) {
990
- this.resolveVisitPromise = () => {};
991
- this.element = element;
992
- this.view = new FrameView(this, this.element);
993
- this.appearanceObserver = new AppearanceObserver(this, this.element);
994
- this.linkInterceptor = new LinkInterceptor(this, this.element);
995
- this.formInterceptor = new FormInterceptor(this, this.element);
996
- }
997
- connect() {
998
- if (this.loadingStyle == FrameLoadingStyle.lazy) {
999
- this.appearanceObserver.start();
1000
- }
1001
- this.linkInterceptor.start();
1002
- this.formInterceptor.start();
1003
- }
1004
- disconnect() {
1005
- this.appearanceObserver.stop();
1006
- this.linkInterceptor.stop();
1007
- this.formInterceptor.stop();
1008
- }
1009
- sourceURLChanged() {
1010
- if (this.loadingStyle == FrameLoadingStyle.eager) {
1011
- this.loadSourceURL();
1012
- }
1013
- }
1014
- loadingStyleChanged() {
1015
- if (this.loadingStyle == FrameLoadingStyle.lazy) {
1016
- this.appearanceObserver.start();
1017
- } else {
1018
- this.appearanceObserver.stop();
1019
- this.loadSourceURL();
1020
- }
1067
+ class ProgressBar {
1068
+ constructor() {
1069
+ this.hiding = false;
1070
+ this.value = 0;
1071
+ this.visible = false;
1072
+ this.trickle = () => {
1073
+ this.setValue(this.value + Math.random() / 100);
1074
+ };
1075
+ this.stylesheetElement = this.createStylesheetElement();
1076
+ this.progressElement = this.createProgressElement();
1077
+ this.installStylesheetElement();
1078
+ this.setValue(0);
1021
1079
  }
1022
- async loadSourceURL() {
1023
- if (this.isActive && this.sourceURL && this.sourceURL != this.loadingURL) {
1024
- try {
1025
- this.loadingURL = this.sourceURL;
1026
- this.element.loaded = this.visit(this.sourceURL);
1027
- this.appearanceObserver.stop();
1028
- await this.element.loaded;
1029
- } finally {
1030
- delete this.loadingURL;
1080
+ static get defaultCSS() {
1081
+ return unindent`
1082
+ .turbo-progress-bar {
1083
+ position: fixed;
1084
+ display: block;
1085
+ top: 0;
1086
+ left: 0;
1087
+ height: 3px;
1088
+ background: #0076ff;
1089
+ z-index: 9999;
1090
+ transition:
1091
+ width ${ProgressBar.animationDuration}ms ease-out,
1092
+ opacity ${ProgressBar.animationDuration / 2}ms ${ProgressBar.animationDuration / 2}ms ease-in;
1093
+ transform: translate3d(0, 0, 0);
1031
1094
  }
1095
+ `;
1096
+ }
1097
+ show() {
1098
+ if (!this.visible) {
1099
+ this.visible = true;
1100
+ this.installProgressElement();
1101
+ this.startTrickling();
1032
1102
  }
1033
1103
  }
1034
- async loadResponse(response) {
1035
- try {
1036
- const html = await response.responseHTML;
1037
- if (html) {
1038
- const {body: body} = parseHTMLDocument(html);
1039
- const snapshot = new Snapshot(await this.extractForeignFrameElement(body));
1040
- const renderer = new FrameRenderer(this.view.snapshot, snapshot, false);
1041
- await this.view.render(renderer);
1042
- }
1043
- } catch (error) {
1044
- console.error(error);
1045
- this.view.invalidate();
1104
+ hide() {
1105
+ if (this.visible && !this.hiding) {
1106
+ this.hiding = true;
1107
+ this.fadeProgressElement((() => {
1108
+ this.uninstallProgressElement();
1109
+ this.stopTrickling();
1110
+ this.visible = false;
1111
+ this.hiding = false;
1112
+ }));
1046
1113
  }
1047
1114
  }
1048
- elementAppearedInViewport(element) {
1049
- this.loadSourceURL();
1115
+ setValue(value) {
1116
+ this.value = value;
1117
+ this.refresh();
1050
1118
  }
1051
- shouldInterceptLinkClick(element, url) {
1052
- return this.shouldInterceptNavigation(element);
1119
+ installStylesheetElement() {
1120
+ document.head.insertBefore(this.stylesheetElement, document.head.firstChild);
1053
1121
  }
1054
- linkClickIntercepted(element, url) {
1055
- this.navigateFrame(element, url);
1122
+ installProgressElement() {
1123
+ this.progressElement.style.width = "0";
1124
+ this.progressElement.style.opacity = "1";
1125
+ document.documentElement.insertBefore(this.progressElement, document.body);
1126
+ this.refresh();
1056
1127
  }
1057
- shouldInterceptFormSubmission(element) {
1058
- return this.shouldInterceptNavigation(element);
1128
+ fadeProgressElement(callback) {
1129
+ this.progressElement.style.opacity = "0";
1130
+ setTimeout(callback, ProgressBar.animationDuration * 1.5);
1059
1131
  }
1060
- formSubmissionIntercepted(element, submitter) {
1061
- if (this.formSubmission) {
1062
- this.formSubmission.stop();
1132
+ uninstallProgressElement() {
1133
+ if (this.progressElement.parentNode) {
1134
+ document.documentElement.removeChild(this.progressElement);
1063
1135
  }
1064
- this.formSubmission = new FormSubmission(this, element, submitter);
1065
- if (this.formSubmission.fetchRequest.isIdempotent) {
1066
- this.navigateFrame(element, this.formSubmission.fetchRequest.url.href);
1067
- } else {
1068
- this.formSubmission.start();
1136
+ }
1137
+ startTrickling() {
1138
+ if (!this.trickleInterval) {
1139
+ this.trickleInterval = window.setInterval(this.trickle, ProgressBar.animationDuration);
1069
1140
  }
1070
1141
  }
1071
- prepareHeadersForRequest(headers, request) {
1072
- headers["Turbo-Frame"] = this.id;
1142
+ stopTrickling() {
1143
+ window.clearInterval(this.trickleInterval);
1144
+ delete this.trickleInterval;
1073
1145
  }
1074
- requestStarted(request) {
1075
- this.element.setAttribute("busy", "");
1146
+ refresh() {
1147
+ requestAnimationFrame((() => {
1148
+ this.progressElement.style.width = `${10 + this.value * 90}%`;
1149
+ }));
1076
1150
  }
1077
- requestPreventedHandlingResponse(request, response) {
1078
- this.resolveVisitPromise();
1151
+ createStylesheetElement() {
1152
+ const element = document.createElement("style");
1153
+ element.type = "text/css";
1154
+ element.textContent = ProgressBar.defaultCSS;
1155
+ return element;
1079
1156
  }
1080
- async requestSucceededWithResponse(request, response) {
1081
- await this.loadResponse(response);
1082
- this.resolveVisitPromise();
1157
+ createProgressElement() {
1158
+ const element = document.createElement("div");
1159
+ element.className = "turbo-progress-bar";
1160
+ return element;
1083
1161
  }
1084
- requestFailedWithResponse(request, response) {
1085
- console.error(response);
1086
- this.resolveVisitPromise();
1087
- }
1088
- requestErrored(request, error) {
1089
- console.error(error);
1090
- this.resolveVisitPromise();
1091
- }
1092
- requestFinished(request) {
1093
- this.element.removeAttribute("busy");
1094
- }
1095
- formSubmissionStarted(formSubmission) {}
1096
- formSubmissionSucceededWithResponse(formSubmission, response) {
1097
- const frame = this.findFrameElement(formSubmission.formElement);
1098
- frame.delegate.loadResponse(response);
1099
- }
1100
- formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
1101
- this.element.delegate.loadResponse(fetchResponse);
1102
- }
1103
- formSubmissionErrored(formSubmission, error) {}
1104
- formSubmissionFinished(formSubmission) {}
1105
- viewWillRenderSnapshot(snapshot, isPreview) {}
1106
- viewRenderedSnapshot(snapshot, isPreview) {}
1107
- viewInvalidated() {}
1108
- async visit(url) {
1109
- const request = new FetchRequest(this, FetchMethod.get, expandURL(url));
1110
- return new Promise((resolve => {
1111
- this.resolveVisitPromise = () => {
1112
- this.resolveVisitPromise = () => {};
1113
- resolve();
1114
- };
1115
- request.perform();
1116
- }));
1117
- }
1118
- navigateFrame(element, url) {
1119
- const frame = this.findFrameElement(element);
1120
- frame.src = url;
1121
- }
1122
- findFrameElement(element) {
1123
- var _a;
1124
- const id = element.getAttribute("data-turbo-frame") || this.element.getAttribute("target");
1125
- return (_a = getFrameElementById(id)) !== null && _a !== void 0 ? _a : this.element;
1126
- }
1127
- async extractForeignFrameElement(container) {
1128
- let element;
1129
- const id = CSS.escape(this.id);
1130
- if (element = activateElement(container.querySelector(`turbo-frame#${id}`))) {
1131
- return element;
1132
- }
1133
- if (element = activateElement(container.querySelector(`turbo-frame[src][recurse~=${id}]`))) {
1134
- await element.loaded;
1135
- return await this.extractForeignFrameElement(element);
1136
- }
1137
- console.error(`Response has no matching <turbo-frame id="${id}"> element`);
1138
- return new FrameElement;
1139
- }
1140
- shouldInterceptNavigation(element) {
1141
- const id = element.getAttribute("data-turbo-frame") || this.element.getAttribute("target");
1142
- if (!this.enabled || id == "_top") {
1143
- return false;
1144
- }
1145
- if (id) {
1146
- const frameElement = getFrameElementById(id);
1147
- if (frameElement) {
1148
- return !frameElement.disabled;
1149
- }
1150
- }
1151
- return true;
1152
- }
1153
- get id() {
1154
- return this.element.id;
1155
- }
1156
- get enabled() {
1157
- return !this.element.disabled;
1158
- }
1159
- get sourceURL() {
1160
- return this.element.src;
1161
- }
1162
- get loadingStyle() {
1163
- return this.element.loading;
1164
- }
1165
- get isLoading() {
1166
- return this.formSubmission !== undefined || this.loadingURL !== undefined;
1167
- }
1168
- get isActive() {
1169
- return this.element.isActive;
1170
- }
1171
- }
1172
-
1173
- function getFrameElementById(id) {
1174
- if (id != null) {
1175
- const element = document.getElementById(id);
1176
- if (element instanceof FrameElement) {
1177
- return element;
1178
- }
1179
- }
1180
- }
1181
-
1182
- function activateElement(element) {
1183
- if (element && element.ownerDocument !== document) {
1184
- element = document.importNode(element, true);
1185
- }
1186
- if (element instanceof FrameElement) {
1187
- return element;
1188
- }
1189
- }
1190
-
1191
- const StreamActions = {
1192
- append() {
1193
- var _a;
1194
- (_a = this.targetElement) === null || _a === void 0 ? void 0 : _a.append(this.templateContent);
1195
- },
1196
- prepend() {
1197
- var _a;
1198
- (_a = this.targetElement) === null || _a === void 0 ? void 0 : _a.prepend(this.templateContent);
1199
- },
1200
- remove() {
1201
- var _a;
1202
- (_a = this.targetElement) === null || _a === void 0 ? void 0 : _a.remove();
1203
- },
1204
- replace() {
1205
- var _a;
1206
- (_a = this.targetElement) === null || _a === void 0 ? void 0 : _a.replaceWith(this.templateContent);
1207
- },
1208
- update() {
1209
- if (this.targetElement) {
1210
- this.targetElement.innerHTML = "";
1211
- this.targetElement.append(this.templateContent);
1212
- }
1213
- }
1214
- };
1215
-
1216
- class StreamElement extends HTMLElement {
1217
- async connectedCallback() {
1218
- try {
1219
- await this.render();
1220
- } catch (error) {
1221
- console.error(error);
1222
- } finally {
1223
- this.disconnect();
1224
- }
1225
- }
1226
- async render() {
1227
- var _a;
1228
- return (_a = this.renderPromise) !== null && _a !== void 0 ? _a : this.renderPromise = (async () => {
1229
- if (this.dispatchEvent(this.beforeRenderEvent)) {
1230
- await nextAnimationFrame();
1231
- this.performAction();
1232
- }
1233
- })();
1234
- }
1235
- disconnect() {
1236
- try {
1237
- this.remove();
1238
- } catch (_a) {}
1239
- }
1240
- get performAction() {
1241
- if (this.action) {
1242
- const actionFunction = StreamActions[this.action];
1243
- if (actionFunction) {
1244
- return actionFunction;
1245
- }
1246
- this.raise("unknown action");
1247
- }
1248
- this.raise("action attribute is missing");
1249
- }
1250
- get targetElement() {
1251
- var _a;
1252
- if (this.target) {
1253
- return (_a = this.ownerDocument) === null || _a === void 0 ? void 0 : _a.getElementById(this.target);
1254
- }
1255
- this.raise("target attribute is missing");
1256
- }
1257
- get templateContent() {
1258
- return this.templateElement.content;
1259
- }
1260
- get templateElement() {
1261
- if (this.firstElementChild instanceof HTMLTemplateElement) {
1262
- return this.firstElementChild;
1263
- }
1264
- this.raise("first child element must be a <template> element");
1265
- }
1266
- get action() {
1267
- return this.getAttribute("action");
1268
- }
1269
- get target() {
1270
- return this.getAttribute("target");
1271
- }
1272
- raise(message) {
1273
- throw new Error(`${this.description}: ${message}`);
1274
- }
1275
- get description() {
1276
- var _a, _b;
1277
- return (_b = ((_a = this.outerHTML.match(/<[^>]+>/)) !== null && _a !== void 0 ? _a : [])[0]) !== null && _b !== void 0 ? _b : "<turbo-stream>";
1278
- }
1279
- get beforeRenderEvent() {
1280
- return new CustomEvent("turbo:before-stream-render", {
1281
- bubbles: true,
1282
- cancelable: true
1283
- });
1284
- }
1285
- }
1286
-
1287
- FrameElement.delegateConstructor = FrameController;
1288
-
1289
- customElements.define("turbo-frame", FrameElement);
1290
-
1291
- customElements.define("turbo-stream", StreamElement);
1292
-
1293
- (() => {
1294
- let element = document.currentScript;
1295
- if (!element) return;
1296
- if (element.hasAttribute("data-turbo-suppress-warning")) return;
1297
- while (element = element.parentElement) {
1298
- if (element == document.body) {
1299
- return console.warn(unindent`
1300
- You are loading Turbo from a <script> element inside the <body> element. This is probably not what you meant to do!
1301
-
1302
- Load your application’s JavaScript bundle inside the <head> element instead. <script> elements in <body> are evaluated with each page change.
1303
-
1304
- For more information, see: https://turbo.hotwire.dev/handbook/building#working-with-script-elements
1305
-
1306
- ——
1307
- Suppress this warning by adding a "data-turbo-suppress-warning" attribute to: %s
1308
- `, element.outerHTML);
1309
- }
1310
- }
1311
- })();
1312
-
1313
- class ProgressBar {
1314
- constructor() {
1315
- this.hiding = false;
1316
- this.value = 0;
1317
- this.visible = false;
1318
- this.trickle = () => {
1319
- this.setValue(this.value + Math.random() / 100);
1320
- };
1321
- this.stylesheetElement = this.createStylesheetElement();
1322
- this.progressElement = this.createProgressElement();
1323
- this.installStylesheetElement();
1324
- this.setValue(0);
1325
- }
1326
- static get defaultCSS() {
1327
- return unindent`
1328
- .turbo-progress-bar {
1329
- position: fixed;
1330
- display: block;
1331
- top: 0;
1332
- left: 0;
1333
- height: 3px;
1334
- background: #0076ff;
1335
- z-index: 9999;
1336
- transition:
1337
- width ${ProgressBar.animationDuration}ms ease-out,
1338
- opacity ${ProgressBar.animationDuration / 2}ms ${ProgressBar.animationDuration / 2}ms ease-in;
1339
- transform: translate3d(0, 0, 0);
1340
- }
1341
- `;
1342
- }
1343
- show() {
1344
- if (!this.visible) {
1345
- this.visible = true;
1346
- this.installProgressElement();
1347
- this.startTrickling();
1348
- }
1349
- }
1350
- hide() {
1351
- if (this.visible && !this.hiding) {
1352
- this.hiding = true;
1353
- this.fadeProgressElement((() => {
1354
- this.uninstallProgressElement();
1355
- this.stopTrickling();
1356
- this.visible = false;
1357
- this.hiding = false;
1358
- }));
1359
- }
1360
- }
1361
- setValue(value) {
1362
- this.value = value;
1363
- this.refresh();
1364
- }
1365
- installStylesheetElement() {
1366
- document.head.insertBefore(this.stylesheetElement, document.head.firstChild);
1367
- }
1368
- installProgressElement() {
1369
- this.progressElement.style.width = "0";
1370
- this.progressElement.style.opacity = "1";
1371
- document.documentElement.insertBefore(this.progressElement, document.body);
1372
- this.refresh();
1373
- }
1374
- fadeProgressElement(callback) {
1375
- this.progressElement.style.opacity = "0";
1376
- setTimeout(callback, ProgressBar.animationDuration * 1.5);
1377
- }
1378
- uninstallProgressElement() {
1379
- if (this.progressElement.parentNode) {
1380
- document.documentElement.removeChild(this.progressElement);
1381
- }
1382
- }
1383
- startTrickling() {
1384
- if (!this.trickleInterval) {
1385
- this.trickleInterval = window.setInterval(this.trickle, ProgressBar.animationDuration);
1386
- }
1387
- }
1388
- stopTrickling() {
1389
- window.clearInterval(this.trickleInterval);
1390
- delete this.trickleInterval;
1391
- }
1392
- refresh() {
1393
- requestAnimationFrame((() => {
1394
- this.progressElement.style.width = `${10 + this.value * 90}%`;
1395
- }));
1396
- }
1397
- createStylesheetElement() {
1398
- const element = document.createElement("style");
1399
- element.type = "text/css";
1400
- element.textContent = ProgressBar.defaultCSS;
1401
- return element;
1402
- }
1403
- createProgressElement() {
1404
- const element = document.createElement("div");
1405
- element.className = "turbo-progress-bar";
1406
- return element;
1407
- }
1408
- }
1409
-
1410
- ProgressBar.animationDuration = 300;
1411
-
1412
- class HeadSnapshot extends Snapshot {
1413
- constructor() {
1414
- super(...arguments);
1415
- this.detailsByOuterHTML = this.children.reduce(((result, element) => {
1416
- const {outerHTML: outerHTML} = element;
1417
- const details = outerHTML in result ? result[outerHTML] : {
1418
- type: elementType(element),
1419
- tracked: elementIsTracked(element),
1420
- elements: []
1421
- };
1422
- return Object.assign(Object.assign({}, result), {
1423
- [outerHTML]: Object.assign(Object.assign({}, details), {
1424
- elements: [ ...details.elements, element ]
1425
- })
1426
- });
1427
- }), {});
1162
+ }
1163
+
1164
+ ProgressBar.animationDuration = 300;
1165
+
1166
+ class HeadSnapshot extends Snapshot {
1167
+ constructor() {
1168
+ super(...arguments);
1169
+ this.detailsByOuterHTML = this.children.filter((element => !elementIsNoscript(element))).reduce(((result, element) => {
1170
+ const {outerHTML: outerHTML} = element;
1171
+ const details = outerHTML in result ? result[outerHTML] : {
1172
+ type: elementType(element),
1173
+ tracked: elementIsTracked(element),
1174
+ elements: []
1175
+ };
1176
+ return Object.assign(Object.assign({}, result), {
1177
+ [outerHTML]: Object.assign(Object.assign({}, details), {
1178
+ elements: [ ...details.elements, element ]
1179
+ })
1180
+ });
1181
+ }), {});
1428
1182
  }
1429
1183
  get trackedElementSignature() {
1430
1184
  return Object.keys(this.detailsByOuterHTML).filter((outerHTML => this.detailsByOuterHTML[outerHTML].tracked)).join("");
@@ -1479,6 +1233,11 @@ function elementIsScript(element) {
1479
1233
  return tagName == "script";
1480
1234
  }
1481
1235
 
1236
+ function elementIsNoscript(element) {
1237
+ const tagName = element.tagName.toLowerCase();
1238
+ return tagName == "noscript";
1239
+ }
1240
+
1482
1241
  function elementIsStylesheet(element) {
1483
1242
  const tagName = element.tagName.toLowerCase();
1484
1243
  return tagName == "style" || tagName == "link" && element.getAttribute("rel") == "stylesheet";
@@ -1581,6 +1340,7 @@ class Visit {
1581
1340
  this.referrer = referrer;
1582
1341
  this.snapshotHTML = snapshotHTML;
1583
1342
  this.response = response;
1343
+ this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
1584
1344
  }
1585
1345
  get adapter() {
1586
1346
  return this.delegate.adapter;
@@ -1673,6 +1433,7 @@ class Visit {
1673
1433
  const {statusCode: statusCode, responseHTML: responseHTML} = this.response;
1674
1434
  this.render((async () => {
1675
1435
  this.cacheSnapshot();
1436
+ if (this.view.renderPromise) await this.view.renderPromise;
1676
1437
  if (isSuccessful(statusCode) && responseHTML != null) {
1677
1438
  await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML));
1678
1439
  this.adapter.visitRendered(this);
@@ -1707,10 +1468,15 @@ class Visit {
1707
1468
  const isPreview = this.shouldIssueRequest();
1708
1469
  this.render((async () => {
1709
1470
  this.cacheSnapshot();
1710
- await this.view.renderPage(snapshot);
1711
- this.adapter.visitRendered(this);
1712
- if (!isPreview) {
1713
- this.complete();
1471
+ if (this.isSamePage) {
1472
+ this.adapter.visitRendered(this);
1473
+ } else {
1474
+ if (this.view.renderPromise) await this.view.renderPromise;
1475
+ await this.view.renderPage(snapshot, isPreview);
1476
+ this.adapter.visitRendered(this);
1477
+ if (!isPreview) {
1478
+ this.complete();
1479
+ }
1714
1480
  }
1715
1481
  }));
1716
1482
  }
@@ -1722,6 +1488,14 @@ class Visit {
1722
1488
  this.followedRedirect = true;
1723
1489
  }
1724
1490
  }
1491
+ goToSamePageAnchor() {
1492
+ if (this.isSamePage) {
1493
+ this.render((async () => {
1494
+ this.cacheSnapshot();
1495
+ this.adapter.visitRendered(this);
1496
+ }));
1497
+ }
1498
+ }
1725
1499
  requestStarted() {
1726
1500
  this.startRequest();
1727
1501
  }
@@ -1764,10 +1538,13 @@ class Visit {
1764
1538
  performScroll() {
1765
1539
  if (!this.scrolled) {
1766
1540
  if (this.action == "restore") {
1767
- this.scrollToRestoredPosition() || this.scrollToTop();
1541
+ this.scrollToRestoredPosition() || this.scrollToAnchor() || this.scrollToTop();
1768
1542
  } else {
1769
1543
  this.scrollToAnchor() || this.scrollToTop();
1770
1544
  }
1545
+ if (this.isSamePage) {
1546
+ this.delegate.visitScrolledToSamePageLocation(this.view.lastRenderedLocation, this.location);
1547
+ }
1771
1548
  this.scrolled = true;
1772
1549
  }
1773
1550
  }
@@ -1779,8 +1556,9 @@ class Visit {
1779
1556
  }
1780
1557
  }
1781
1558
  scrollToAnchor() {
1782
- if (getAnchor(this.location) != null) {
1783
- this.view.scrollToAnchor(getAnchor(this.location));
1559
+ const anchor = getAnchor(this.location);
1560
+ if (anchor != null) {
1561
+ this.view.scrollToAnchor(anchor);
1784
1562
  return true;
1785
1563
  }
1786
1564
  }
@@ -1810,7 +1588,13 @@ class Visit {
1810
1588
  return typeof this.response == "object";
1811
1589
  }
1812
1590
  shouldIssueRequest() {
1813
- return this.action == "restore" ? !this.hasCachedSnapshot() : true;
1591
+ if (this.isSamePage) {
1592
+ return false;
1593
+ } else if (this.action == "restore") {
1594
+ return !this.hasCachedSnapshot();
1595
+ } else {
1596
+ return true;
1597
+ }
1814
1598
  }
1815
1599
  cacheSnapshot() {
1816
1600
  if (!this.snapshotCached) {
@@ -1823,7 +1607,7 @@ class Visit {
1823
1607
  await new Promise((resolve => {
1824
1608
  this.frame = requestAnimationFrame((() => resolve()));
1825
1609
  }));
1826
- callback();
1610
+ await callback();
1827
1611
  delete this.frame;
1828
1612
  this.performScroll();
1829
1613
  }
@@ -1853,6 +1637,7 @@ class BrowserAdapter {
1853
1637
  visitStarted(visit) {
1854
1638
  visit.issueRequest();
1855
1639
  visit.changeHistory();
1640
+ visit.goToSamePageAnchor();
1856
1641
  visit.loadCachedSnapshot();
1857
1642
  }
1858
1643
  visitRequestStarted(visit) {
@@ -1907,6 +1692,30 @@ class BrowserAdapter {
1907
1692
  }
1908
1693
  }
1909
1694
 
1695
+ class CacheObserver {
1696
+ constructor() {
1697
+ this.started = false;
1698
+ }
1699
+ start() {
1700
+ if (!this.started) {
1701
+ this.started = true;
1702
+ addEventListener("turbo:before-cache", this.removeStaleElements, false);
1703
+ }
1704
+ }
1705
+ stop() {
1706
+ if (this.started) {
1707
+ this.started = false;
1708
+ removeEventListener("turbo:before-cache", this.removeStaleElements, false);
1709
+ }
1710
+ }
1711
+ removeStaleElements() {
1712
+ const staleElements = [ ...document.querySelectorAll('[data-turbo-cache="false"]') ];
1713
+ for (const element of staleElements) {
1714
+ element.remove();
1715
+ }
1716
+ }
1717
+ }
1718
+
1910
1719
  class FormSubmitObserver {
1911
1720
  constructor(delegate) {
1912
1721
  this.started = false;
@@ -1990,812 +1799,1295 @@ class FrameRedirector {
1990
1799
  }
1991
1800
  }
1992
1801
 
1993
- class History {
1802
+ class History {
1803
+ constructor(delegate) {
1804
+ this.restorationIdentifier = uuid();
1805
+ this.restorationData = {};
1806
+ this.started = false;
1807
+ this.pageLoaded = false;
1808
+ this.onPopState = event => {
1809
+ if (this.shouldHandlePopState()) {
1810
+ const {turbo: turbo} = event.state || {};
1811
+ if (turbo) {
1812
+ this.location = new URL(window.location.href);
1813
+ const {restorationIdentifier: restorationIdentifier} = turbo;
1814
+ this.restorationIdentifier = restorationIdentifier;
1815
+ this.delegate.historyPoppedToLocationWithRestorationIdentifier(this.location, restorationIdentifier);
1816
+ }
1817
+ }
1818
+ };
1819
+ this.onPageLoad = async event => {
1820
+ await nextMicrotask();
1821
+ this.pageLoaded = true;
1822
+ };
1823
+ this.delegate = delegate;
1824
+ }
1825
+ start() {
1826
+ if (!this.started) {
1827
+ addEventListener("popstate", this.onPopState, false);
1828
+ addEventListener("load", this.onPageLoad, false);
1829
+ this.started = true;
1830
+ this.replace(new URL(window.location.href));
1831
+ }
1832
+ }
1833
+ stop() {
1834
+ if (this.started) {
1835
+ removeEventListener("popstate", this.onPopState, false);
1836
+ removeEventListener("load", this.onPageLoad, false);
1837
+ this.started = false;
1838
+ }
1839
+ }
1840
+ push(location, restorationIdentifier) {
1841
+ this.update(history.pushState, location, restorationIdentifier);
1842
+ }
1843
+ replace(location, restorationIdentifier) {
1844
+ this.update(history.replaceState, location, restorationIdentifier);
1845
+ }
1846
+ update(method, location, restorationIdentifier = uuid()) {
1847
+ const state = {
1848
+ turbo: {
1849
+ restorationIdentifier: restorationIdentifier
1850
+ }
1851
+ };
1852
+ method.call(history, state, "", location.href);
1853
+ this.location = location;
1854
+ this.restorationIdentifier = restorationIdentifier;
1855
+ }
1856
+ getRestorationDataForIdentifier(restorationIdentifier) {
1857
+ return this.restorationData[restorationIdentifier] || {};
1858
+ }
1859
+ updateRestorationData(additionalData) {
1860
+ const {restorationIdentifier: restorationIdentifier} = this;
1861
+ const restorationData = this.restorationData[restorationIdentifier];
1862
+ this.restorationData[restorationIdentifier] = Object.assign(Object.assign({}, restorationData), additionalData);
1863
+ }
1864
+ assumeControlOfScrollRestoration() {
1865
+ var _a;
1866
+ if (!this.previousScrollRestoration) {
1867
+ this.previousScrollRestoration = (_a = history.scrollRestoration) !== null && _a !== void 0 ? _a : "auto";
1868
+ history.scrollRestoration = "manual";
1869
+ }
1870
+ }
1871
+ relinquishControlOfScrollRestoration() {
1872
+ if (this.previousScrollRestoration) {
1873
+ history.scrollRestoration = this.previousScrollRestoration;
1874
+ delete this.previousScrollRestoration;
1875
+ }
1876
+ }
1877
+ shouldHandlePopState() {
1878
+ return this.pageIsLoaded();
1879
+ }
1880
+ pageIsLoaded() {
1881
+ return this.pageLoaded || document.readyState == "complete";
1882
+ }
1883
+ }
1884
+
1885
+ class LinkClickObserver {
1886
+ constructor(delegate) {
1887
+ this.started = false;
1888
+ this.clickCaptured = () => {
1889
+ removeEventListener("click", this.clickBubbled, false);
1890
+ addEventListener("click", this.clickBubbled, false);
1891
+ };
1892
+ this.clickBubbled = event => {
1893
+ if (this.clickEventIsSignificant(event)) {
1894
+ const link = this.findLinkFromClickTarget(event.target);
1895
+ if (link) {
1896
+ const location = this.getLocationForLink(link);
1897
+ if (this.delegate.willFollowLinkToLocation(link, location)) {
1898
+ event.preventDefault();
1899
+ this.delegate.followedLinkToLocation(link, location);
1900
+ }
1901
+ }
1902
+ }
1903
+ };
1904
+ this.delegate = delegate;
1905
+ }
1906
+ start() {
1907
+ if (!this.started) {
1908
+ addEventListener("click", this.clickCaptured, true);
1909
+ this.started = true;
1910
+ }
1911
+ }
1912
+ stop() {
1913
+ if (this.started) {
1914
+ removeEventListener("click", this.clickCaptured, true);
1915
+ this.started = false;
1916
+ }
1917
+ }
1918
+ clickEventIsSignificant(event) {
1919
+ return !(event.target && event.target.isContentEditable || event.defaultPrevented || event.which > 1 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey);
1920
+ }
1921
+ findLinkFromClickTarget(target) {
1922
+ if (target instanceof Element) {
1923
+ return target.closest("a[href]:not([target^=_]):not([download])");
1924
+ }
1925
+ }
1926
+ getLocationForLink(link) {
1927
+ return expandURL(link.getAttribute("href") || "");
1928
+ }
1929
+ }
1930
+
1931
+ function isAction(action) {
1932
+ return action == "advance" || action == "replace" || action == "restore";
1933
+ }
1934
+
1935
+ class Navigator {
1936
+ constructor(delegate) {
1937
+ this.delegate = delegate;
1938
+ }
1939
+ proposeVisit(location, options = {}) {
1940
+ if (this.delegate.allowsVisitingLocation(location)) {
1941
+ this.delegate.visitProposedToLocation(location, options);
1942
+ }
1943
+ }
1944
+ startVisit(locatable, restorationIdentifier, options = {}) {
1945
+ this.stop();
1946
+ this.currentVisit = new Visit(this, expandURL(locatable), restorationIdentifier, Object.assign({
1947
+ referrer: this.location
1948
+ }, options));
1949
+ this.currentVisit.start();
1950
+ }
1951
+ submitForm(form, submitter) {
1952
+ this.stop();
1953
+ this.formSubmission = new FormSubmission(this, form, submitter, true);
1954
+ if (this.formSubmission.isIdempotent) {
1955
+ this.proposeVisit(this.formSubmission.fetchRequest.url, {
1956
+ action: this.getActionForFormSubmission(this.formSubmission)
1957
+ });
1958
+ } else {
1959
+ this.formSubmission.start();
1960
+ }
1961
+ }
1962
+ stop() {
1963
+ if (this.formSubmission) {
1964
+ this.formSubmission.stop();
1965
+ delete this.formSubmission;
1966
+ }
1967
+ if (this.currentVisit) {
1968
+ this.currentVisit.cancel();
1969
+ delete this.currentVisit;
1970
+ }
1971
+ }
1972
+ get adapter() {
1973
+ return this.delegate.adapter;
1974
+ }
1975
+ get view() {
1976
+ return this.delegate.view;
1977
+ }
1978
+ get history() {
1979
+ return this.delegate.history;
1980
+ }
1981
+ formSubmissionStarted(formSubmission) {}
1982
+ async formSubmissionSucceededWithResponse(formSubmission, fetchResponse) {
1983
+ if (formSubmission == this.formSubmission) {
1984
+ const responseHTML = await fetchResponse.responseHTML;
1985
+ if (responseHTML) {
1986
+ if (formSubmission.method != FetchMethod.get) {
1987
+ this.view.clearSnapshotCache();
1988
+ }
1989
+ const {statusCode: statusCode} = fetchResponse;
1990
+ const visitOptions = {
1991
+ response: {
1992
+ statusCode: statusCode,
1993
+ responseHTML: responseHTML
1994
+ }
1995
+ };
1996
+ this.proposeVisit(fetchResponse.location, visitOptions);
1997
+ }
1998
+ }
1999
+ }
2000
+ async formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
2001
+ const responseHTML = await fetchResponse.responseHTML;
2002
+ if (responseHTML) {
2003
+ const snapshot = PageSnapshot.fromHTMLString(responseHTML);
2004
+ await this.view.renderPage(snapshot);
2005
+ this.view.clearSnapshotCache();
2006
+ }
2007
+ }
2008
+ formSubmissionErrored(formSubmission, error) {
2009
+ console.error(error);
2010
+ }
2011
+ formSubmissionFinished(formSubmission) {}
2012
+ visitStarted(visit) {
2013
+ this.delegate.visitStarted(visit);
2014
+ }
2015
+ visitCompleted(visit) {
2016
+ this.delegate.visitCompleted(visit);
2017
+ }
2018
+ locationWithActionIsSamePage(location, action) {
2019
+ return getRequestURL(location) === getRequestURL(this.view.lastRenderedLocation) && (getAnchor(location) != null || action == "restore");
2020
+ }
2021
+ visitScrolledToSamePageLocation(oldURL, newURL) {
2022
+ this.delegate.visitScrolledToSamePageLocation(oldURL, newURL);
2023
+ }
2024
+ get location() {
2025
+ return this.history.location;
2026
+ }
2027
+ get restorationIdentifier() {
2028
+ return this.history.restorationIdentifier;
2029
+ }
2030
+ getActionForFormSubmission(formSubmission) {
2031
+ const {formElement: formElement, submitter: submitter} = formSubmission;
2032
+ const action = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("data-turbo-action")) || formElement.getAttribute("data-turbo-action");
2033
+ return isAction(action) ? action : "advance";
2034
+ }
2035
+ }
2036
+
2037
+ var PageStage;
2038
+
2039
+ (function(PageStage) {
2040
+ PageStage[PageStage["initial"] = 0] = "initial";
2041
+ PageStage[PageStage["loading"] = 1] = "loading";
2042
+ PageStage[PageStage["interactive"] = 2] = "interactive";
2043
+ PageStage[PageStage["complete"] = 3] = "complete";
2044
+ })(PageStage || (PageStage = {}));
2045
+
2046
+ class PageObserver {
1994
2047
  constructor(delegate) {
1995
- this.restorationIdentifier = uuid();
1996
- this.restorationData = {};
2048
+ this.stage = PageStage.initial;
1997
2049
  this.started = false;
1998
- this.pageLoaded = false;
1999
- this.onPopState = event => {
2000
- if (this.shouldHandlePopState()) {
2001
- const {turbo: turbo} = event.state || {};
2002
- if (turbo) {
2003
- this.location = new URL(window.location.href);
2004
- const {restorationIdentifier: restorationIdentifier} = turbo;
2005
- this.restorationIdentifier = restorationIdentifier;
2006
- this.delegate.historyPoppedToLocationWithRestorationIdentifier(this.location, restorationIdentifier);
2007
- }
2050
+ this.interpretReadyState = () => {
2051
+ const {readyState: readyState} = this;
2052
+ if (readyState == "interactive") {
2053
+ this.pageIsInteractive();
2054
+ } else if (readyState == "complete") {
2055
+ this.pageIsComplete();
2008
2056
  }
2009
2057
  };
2010
- this.onPageLoad = async event => {
2011
- await nextMicrotask();
2012
- this.pageLoaded = true;
2058
+ this.pageWillUnload = () => {
2059
+ this.delegate.pageWillUnload();
2013
2060
  };
2014
2061
  this.delegate = delegate;
2015
2062
  }
2016
2063
  start() {
2017
2064
  if (!this.started) {
2018
- addEventListener("popstate", this.onPopState, false);
2019
- addEventListener("load", this.onPageLoad, false);
2065
+ if (this.stage == PageStage.initial) {
2066
+ this.stage = PageStage.loading;
2067
+ }
2068
+ document.addEventListener("readystatechange", this.interpretReadyState, false);
2069
+ addEventListener("pagehide", this.pageWillUnload, false);
2020
2070
  this.started = true;
2021
- this.replace(new URL(window.location.href));
2022
2071
  }
2023
2072
  }
2024
2073
  stop() {
2025
2074
  if (this.started) {
2026
- removeEventListener("popstate", this.onPopState, false);
2027
- removeEventListener("load", this.onPageLoad, false);
2075
+ document.removeEventListener("readystatechange", this.interpretReadyState, false);
2076
+ removeEventListener("pagehide", this.pageWillUnload, false);
2028
2077
  this.started = false;
2029
2078
  }
2030
2079
  }
2031
- push(location, restorationIdentifier) {
2032
- this.update(history.pushState, location, restorationIdentifier);
2033
- }
2034
- replace(location, restorationIdentifier) {
2035
- this.update(history.replaceState, location, restorationIdentifier);
2080
+ pageIsInteractive() {
2081
+ if (this.stage == PageStage.loading) {
2082
+ this.stage = PageStage.interactive;
2083
+ this.delegate.pageBecameInteractive();
2084
+ }
2036
2085
  }
2037
- update(method, location, restorationIdentifier = uuid()) {
2038
- const state = {
2039
- turbo: {
2040
- restorationIdentifier: restorationIdentifier
2041
- }
2042
- };
2043
- method.call(history, state, "", location.href);
2044
- this.location = location;
2045
- this.restorationIdentifier = restorationIdentifier;
2086
+ pageIsComplete() {
2087
+ this.pageIsInteractive();
2088
+ if (this.stage == PageStage.interactive) {
2089
+ this.stage = PageStage.complete;
2090
+ this.delegate.pageLoaded();
2091
+ }
2046
2092
  }
2047
- getRestorationDataForIdentifier(restorationIdentifier) {
2048
- return this.restorationData[restorationIdentifier] || {};
2093
+ get readyState() {
2094
+ return document.readyState;
2049
2095
  }
2050
- updateRestorationData(additionalData) {
2051
- const {restorationIdentifier: restorationIdentifier} = this;
2052
- const restorationData = this.restorationData[restorationIdentifier];
2053
- this.restorationData[restorationIdentifier] = Object.assign(Object.assign({}, restorationData), additionalData);
2096
+ }
2097
+
2098
+ class ScrollObserver {
2099
+ constructor(delegate) {
2100
+ this.started = false;
2101
+ this.onScroll = () => {
2102
+ this.updatePosition({
2103
+ x: window.pageXOffset,
2104
+ y: window.pageYOffset
2105
+ });
2106
+ };
2107
+ this.delegate = delegate;
2054
2108
  }
2055
- assumeControlOfScrollRestoration() {
2056
- var _a;
2057
- if (!this.previousScrollRestoration) {
2058
- this.previousScrollRestoration = (_a = history.scrollRestoration) !== null && _a !== void 0 ? _a : "auto";
2059
- history.scrollRestoration = "manual";
2109
+ start() {
2110
+ if (!this.started) {
2111
+ addEventListener("scroll", this.onScroll, false);
2112
+ this.onScroll();
2113
+ this.started = true;
2060
2114
  }
2061
2115
  }
2062
- relinquishControlOfScrollRestoration() {
2063
- if (this.previousScrollRestoration) {
2064
- history.scrollRestoration = this.previousScrollRestoration;
2065
- delete this.previousScrollRestoration;
2116
+ stop() {
2117
+ if (this.started) {
2118
+ removeEventListener("scroll", this.onScroll, false);
2119
+ this.started = false;
2066
2120
  }
2067
2121
  }
2068
- shouldHandlePopState() {
2069
- return this.pageIsLoaded();
2070
- }
2071
- pageIsLoaded() {
2072
- return this.pageLoaded || document.readyState == "complete";
2122
+ updatePosition(position) {
2123
+ this.delegate.scrollPositionChanged(position);
2073
2124
  }
2074
2125
  }
2075
2126
 
2076
- class LinkClickObserver {
2127
+ class StreamObserver {
2077
2128
  constructor(delegate) {
2129
+ this.sources = new Set;
2078
2130
  this.started = false;
2079
- this.clickCaptured = () => {
2080
- removeEventListener("click", this.clickBubbled, false);
2081
- addEventListener("click", this.clickBubbled, false);
2131
+ this.inspectFetchResponse = event => {
2132
+ const response = fetchResponseFromEvent(event);
2133
+ if (response && fetchResponseIsStream(response)) {
2134
+ event.preventDefault();
2135
+ this.receiveMessageResponse(response);
2136
+ }
2082
2137
  };
2083
- this.clickBubbled = event => {
2084
- if (this.clickEventIsSignificant(event)) {
2085
- const link = this.findLinkFromClickTarget(event.target);
2086
- if (link) {
2087
- const location = this.getLocationForLink(link);
2088
- if (this.delegate.willFollowLinkToLocation(link, location)) {
2089
- event.preventDefault();
2090
- this.delegate.followedLinkToLocation(link, location);
2091
- }
2092
- }
2138
+ this.receiveMessageEvent = event => {
2139
+ if (this.started && typeof event.data == "string") {
2140
+ this.receiveMessageHTML(event.data);
2093
2141
  }
2094
2142
  };
2095
2143
  this.delegate = delegate;
2096
2144
  }
2097
2145
  start() {
2098
2146
  if (!this.started) {
2099
- addEventListener("click", this.clickCaptured, true);
2100
2147
  this.started = true;
2148
+ addEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
2101
2149
  }
2102
2150
  }
2103
2151
  stop() {
2104
2152
  if (this.started) {
2105
- removeEventListener("click", this.clickCaptured, true);
2106
2153
  this.started = false;
2154
+ removeEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
2107
2155
  }
2108
2156
  }
2109
- clickEventIsSignificant(event) {
2110
- return !(event.target && event.target.isContentEditable || event.defaultPrevented || event.which > 1 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey);
2157
+ connectStreamSource(source) {
2158
+ if (!this.streamSourceIsConnected(source)) {
2159
+ this.sources.add(source);
2160
+ source.addEventListener("message", this.receiveMessageEvent, false);
2161
+ }
2111
2162
  }
2112
- findLinkFromClickTarget(target) {
2113
- if (target instanceof Element) {
2114
- return target.closest("a[href]:not([target^=_]):not([download])");
2163
+ disconnectStreamSource(source) {
2164
+ if (this.streamSourceIsConnected(source)) {
2165
+ this.sources.delete(source);
2166
+ source.removeEventListener("message", this.receiveMessageEvent, false);
2115
2167
  }
2116
2168
  }
2117
- getLocationForLink(link) {
2118
- return expandURL(link.getAttribute("href") || "");
2169
+ streamSourceIsConnected(source) {
2170
+ return this.sources.has(source);
2171
+ }
2172
+ async receiveMessageResponse(response) {
2173
+ const html = await response.responseHTML;
2174
+ if (html) {
2175
+ this.receiveMessageHTML(html);
2176
+ }
2177
+ }
2178
+ receiveMessageHTML(html) {
2179
+ this.delegate.receivedMessageFromStream(new StreamMessage(html));
2119
2180
  }
2120
2181
  }
2121
2182
 
2122
- class Navigator {
2123
- constructor(delegate) {
2124
- this.delegate = delegate;
2183
+ function fetchResponseFromEvent(event) {
2184
+ var _a;
2185
+ const fetchResponse = (_a = event.detail) === null || _a === void 0 ? void 0 : _a.fetchResponse;
2186
+ if (fetchResponse instanceof FetchResponse) {
2187
+ return fetchResponse;
2125
2188
  }
2126
- proposeVisit(location, options = {}) {
2127
- if (this.delegate.allowsVisitingLocation(location)) {
2128
- this.delegate.visitProposedToLocation(location, options);
2189
+ }
2190
+
2191
+ function fetchResponseIsStream(response) {
2192
+ var _a;
2193
+ const contentType = (_a = response.contentType) !== null && _a !== void 0 ? _a : "";
2194
+ return contentType.startsWith(StreamMessage.contentType);
2195
+ }
2196
+
2197
+ class ErrorRenderer extends Renderer {
2198
+ async render() {
2199
+ this.replaceHeadAndBody();
2200
+ this.activateScriptElements();
2201
+ }
2202
+ replaceHeadAndBody() {
2203
+ const {documentElement: documentElement, head: head, body: body} = document;
2204
+ documentElement.replaceChild(this.newHead, head);
2205
+ documentElement.replaceChild(this.newElement, body);
2206
+ }
2207
+ activateScriptElements() {
2208
+ for (const replaceableElement of this.scriptElements) {
2209
+ const parentNode = replaceableElement.parentNode;
2210
+ if (parentNode) {
2211
+ const element = this.createScriptElement(replaceableElement);
2212
+ parentNode.replaceChild(element, replaceableElement);
2213
+ }
2129
2214
  }
2130
2215
  }
2131
- startVisit(locatable, restorationIdentifier, options = {}) {
2132
- this.stop();
2133
- this.currentVisit = new Visit(this, expandURL(locatable), restorationIdentifier, Object.assign({
2134
- referrer: this.location
2135
- }, options));
2136
- this.currentVisit.start();
2216
+ get newHead() {
2217
+ return this.newSnapshot.headSnapshot.element;
2218
+ }
2219
+ get scriptElements() {
2220
+ return [ ...document.documentElement.querySelectorAll("script") ];
2221
+ }
2222
+ }
2223
+
2224
+ class PageRenderer extends Renderer {
2225
+ get shouldRender() {
2226
+ return this.newSnapshot.isVisitable && this.trackedElementsAreIdentical;
2227
+ }
2228
+ prepareToRender() {
2229
+ this.mergeHead();
2230
+ }
2231
+ async render() {
2232
+ this.replaceBody();
2233
+ }
2234
+ finishRendering() {
2235
+ super.finishRendering();
2236
+ if (!this.isPreview) {
2237
+ this.focusFirstAutofocusableElement();
2238
+ }
2239
+ }
2240
+ get currentHeadSnapshot() {
2241
+ return this.currentSnapshot.headSnapshot;
2242
+ }
2243
+ get newHeadSnapshot() {
2244
+ return this.newSnapshot.headSnapshot;
2245
+ }
2246
+ get newElement() {
2247
+ return this.newSnapshot.element;
2248
+ }
2249
+ mergeHead() {
2250
+ this.copyNewHeadStylesheetElements();
2251
+ this.copyNewHeadScriptElements();
2252
+ this.removeCurrentHeadProvisionalElements();
2253
+ this.copyNewHeadProvisionalElements();
2254
+ }
2255
+ replaceBody() {
2256
+ this.preservingPermanentElements((() => {
2257
+ this.activateNewBody();
2258
+ this.assignNewBody();
2259
+ }));
2260
+ }
2261
+ get trackedElementsAreIdentical() {
2262
+ return this.currentHeadSnapshot.trackedElementSignature == this.newHeadSnapshot.trackedElementSignature;
2137
2263
  }
2138
- submitForm(form, submitter) {
2139
- this.stop();
2140
- this.formSubmission = new FormSubmission(this, form, submitter, true);
2141
- if (this.formSubmission.fetchRequest.isIdempotent) {
2142
- this.proposeVisit(this.formSubmission.fetchRequest.url);
2143
- } else {
2144
- this.formSubmission.start();
2264
+ copyNewHeadStylesheetElements() {
2265
+ for (const element of this.newHeadStylesheetElements) {
2266
+ document.head.appendChild(element);
2145
2267
  }
2146
2268
  }
2147
- stop() {
2148
- if (this.formSubmission) {
2149
- this.formSubmission.stop();
2150
- delete this.formSubmission;
2151
- }
2152
- if (this.currentVisit) {
2153
- this.currentVisit.cancel();
2154
- delete this.currentVisit;
2269
+ copyNewHeadScriptElements() {
2270
+ for (const element of this.newHeadScriptElements) {
2271
+ document.head.appendChild(this.createScriptElement(element));
2155
2272
  }
2156
2273
  }
2157
- get adapter() {
2158
- return this.delegate.adapter;
2274
+ removeCurrentHeadProvisionalElements() {
2275
+ for (const element of this.currentHeadProvisionalElements) {
2276
+ document.head.removeChild(element);
2277
+ }
2159
2278
  }
2160
- get view() {
2161
- return this.delegate.view;
2279
+ copyNewHeadProvisionalElements() {
2280
+ for (const element of this.newHeadProvisionalElements) {
2281
+ document.head.appendChild(element);
2282
+ }
2162
2283
  }
2163
- get history() {
2164
- return this.delegate.history;
2284
+ activateNewBody() {
2285
+ document.adoptNode(this.newElement);
2286
+ this.activateNewBodyScriptElements();
2165
2287
  }
2166
- formSubmissionStarted(formSubmission) {}
2167
- async formSubmissionSucceededWithResponse(formSubmission, fetchResponse) {
2168
- if (formSubmission == this.formSubmission) {
2169
- const responseHTML = await fetchResponse.responseHTML;
2170
- if (responseHTML) {
2171
- if (formSubmission.method != FetchMethod.get) {
2172
- this.view.clearSnapshotCache();
2173
- }
2174
- const {statusCode: statusCode} = fetchResponse;
2175
- const visitOptions = {
2176
- response: {
2177
- statusCode: statusCode,
2178
- responseHTML: responseHTML
2179
- }
2180
- };
2181
- this.proposeVisit(fetchResponse.location, visitOptions);
2182
- }
2288
+ activateNewBodyScriptElements() {
2289
+ for (const inertScriptElement of this.newBodyScriptElements) {
2290
+ const activatedScriptElement = this.createScriptElement(inertScriptElement);
2291
+ inertScriptElement.replaceWith(activatedScriptElement);
2183
2292
  }
2184
2293
  }
2185
- async formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
2186
- const responseHTML = await fetchResponse.responseHTML;
2187
- if (responseHTML) {
2188
- const snapshot = PageSnapshot.fromHTMLString(responseHTML);
2189
- await this.view.renderPage(snapshot);
2190
- this.view.clearSnapshotCache();
2294
+ assignNewBody() {
2295
+ if (document.body && this.newElement instanceof HTMLBodyElement) {
2296
+ document.body.replaceWith(this.newElement);
2297
+ } else {
2298
+ document.documentElement.appendChild(this.newElement);
2191
2299
  }
2192
2300
  }
2193
- formSubmissionErrored(formSubmission, error) {}
2194
- formSubmissionFinished(formSubmission) {}
2195
- visitStarted(visit) {
2196
- this.delegate.visitStarted(visit);
2301
+ get newHeadStylesheetElements() {
2302
+ return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot);
2197
2303
  }
2198
- visitCompleted(visit) {
2199
- this.delegate.visitCompleted(visit);
2304
+ get newHeadScriptElements() {
2305
+ return this.newHeadSnapshot.getScriptElementsNotInSnapshot(this.currentHeadSnapshot);
2200
2306
  }
2201
- get location() {
2202
- return this.history.location;
2307
+ get currentHeadProvisionalElements() {
2308
+ return this.currentHeadSnapshot.provisionalElements;
2203
2309
  }
2204
- get restorationIdentifier() {
2205
- return this.history.restorationIdentifier;
2310
+ get newHeadProvisionalElements() {
2311
+ return this.newHeadSnapshot.provisionalElements;
2312
+ }
2313
+ get newBodyScriptElements() {
2314
+ return this.newElement.querySelectorAll("script");
2206
2315
  }
2207
2316
  }
2208
2317
 
2209
- var PageStage;
2210
-
2211
- (function(PageStage) {
2212
- PageStage[PageStage["initial"] = 0] = "initial";
2213
- PageStage[PageStage["loading"] = 1] = "loading";
2214
- PageStage[PageStage["interactive"] = 2] = "interactive";
2215
- PageStage[PageStage["complete"] = 3] = "complete";
2216
- })(PageStage || (PageStage = {}));
2217
-
2218
- class PageObserver {
2219
- constructor(delegate) {
2220
- this.stage = PageStage.initial;
2221
- this.started = false;
2222
- this.interpretReadyState = () => {
2223
- const {readyState: readyState} = this;
2224
- if (readyState == "interactive") {
2225
- this.pageIsInteractive();
2226
- } else if (readyState == "complete") {
2227
- this.pageIsComplete();
2228
- }
2229
- };
2230
- this.pageWillUnload = () => {
2231
- this.delegate.pageWillUnload();
2232
- };
2233
- this.delegate = delegate;
2318
+ class SnapshotCache {
2319
+ constructor(size) {
2320
+ this.keys = [];
2321
+ this.snapshots = {};
2322
+ this.size = size;
2234
2323
  }
2235
- start() {
2236
- if (!this.started) {
2237
- if (this.stage == PageStage.initial) {
2238
- this.stage = PageStage.loading;
2239
- }
2240
- document.addEventListener("readystatechange", this.interpretReadyState, false);
2241
- addEventListener("pagehide", this.pageWillUnload, false);
2242
- this.started = true;
2243
- }
2324
+ has(location) {
2325
+ return toCacheKey(location) in this.snapshots;
2244
2326
  }
2245
- stop() {
2246
- if (this.started) {
2247
- document.removeEventListener("readystatechange", this.interpretReadyState, false);
2248
- removeEventListener("pagehide", this.pageWillUnload, false);
2249
- this.started = false;
2327
+ get(location) {
2328
+ if (this.has(location)) {
2329
+ const snapshot = this.read(location);
2330
+ this.touch(location);
2331
+ return snapshot;
2250
2332
  }
2251
2333
  }
2252
- pageIsInteractive() {
2253
- if (this.stage == PageStage.loading) {
2254
- this.stage = PageStage.interactive;
2255
- this.delegate.pageBecameInteractive();
2256
- }
2334
+ put(location, snapshot) {
2335
+ this.write(location, snapshot);
2336
+ this.touch(location);
2337
+ return snapshot;
2257
2338
  }
2258
- pageIsComplete() {
2259
- this.pageIsInteractive();
2260
- if (this.stage == PageStage.interactive) {
2261
- this.stage = PageStage.complete;
2262
- this.delegate.pageLoaded();
2263
- }
2339
+ clear() {
2340
+ this.snapshots = {};
2264
2341
  }
2265
- get readyState() {
2266
- return document.readyState;
2342
+ read(location) {
2343
+ return this.snapshots[toCacheKey(location)];
2344
+ }
2345
+ write(location, snapshot) {
2346
+ this.snapshots[toCacheKey(location)] = snapshot;
2347
+ }
2348
+ touch(location) {
2349
+ const key = toCacheKey(location);
2350
+ const index = this.keys.indexOf(key);
2351
+ if (index > -1) this.keys.splice(index, 1);
2352
+ this.keys.unshift(key);
2353
+ this.trim();
2354
+ }
2355
+ trim() {
2356
+ for (const key of this.keys.splice(this.size)) {
2357
+ delete this.snapshots[key];
2358
+ }
2267
2359
  }
2268
2360
  }
2269
2361
 
2270
- class ScrollObserver {
2271
- constructor(delegate) {
2272
- this.started = false;
2273
- this.onScroll = () => {
2274
- this.updatePosition({
2275
- x: window.pageXOffset,
2276
- y: window.pageYOffset
2277
- });
2278
- };
2279
- this.delegate = delegate;
2362
+ class PageView extends View {
2363
+ constructor() {
2364
+ super(...arguments);
2365
+ this.snapshotCache = new SnapshotCache(10);
2366
+ this.lastRenderedLocation = new URL(location.href);
2280
2367
  }
2281
- start() {
2282
- if (!this.started) {
2283
- addEventListener("scroll", this.onScroll, false);
2284
- this.onScroll();
2285
- this.started = true;
2286
- }
2368
+ renderPage(snapshot, isPreview = false) {
2369
+ const renderer = new PageRenderer(this.snapshot, snapshot, isPreview);
2370
+ return this.render(renderer);
2287
2371
  }
2288
- stop() {
2289
- if (this.started) {
2290
- removeEventListener("scroll", this.onScroll, false);
2291
- this.started = false;
2372
+ renderError(snapshot) {
2373
+ const renderer = new ErrorRenderer(this.snapshot, snapshot, false);
2374
+ return this.render(renderer);
2375
+ }
2376
+ clearSnapshotCache() {
2377
+ this.snapshotCache.clear();
2378
+ }
2379
+ async cacheSnapshot() {
2380
+ if (this.shouldCacheSnapshot) {
2381
+ this.delegate.viewWillCacheSnapshot();
2382
+ const {snapshot: snapshot, lastRenderedLocation: location} = this;
2383
+ await nextEventLoopTick();
2384
+ this.snapshotCache.put(location, snapshot.clone());
2292
2385
  }
2293
2386
  }
2294
- updatePosition(position) {
2295
- this.delegate.scrollPositionChanged(position);
2387
+ getCachedSnapshotForLocation(location) {
2388
+ return this.snapshotCache.get(location);
2389
+ }
2390
+ get snapshot() {
2391
+ return PageSnapshot.fromElement(this.element);
2392
+ }
2393
+ get shouldCacheSnapshot() {
2394
+ return this.snapshot.isCacheable;
2296
2395
  }
2297
2396
  }
2298
2397
 
2299
- class StreamObserver {
2300
- constructor(delegate) {
2301
- this.sources = new Set;
2398
+ class Session {
2399
+ constructor() {
2400
+ this.navigator = new Navigator(this);
2401
+ this.history = new History(this);
2402
+ this.view = new PageView(this, document.documentElement);
2403
+ this.adapter = new BrowserAdapter(this);
2404
+ this.pageObserver = new PageObserver(this);
2405
+ this.cacheObserver = new CacheObserver;
2406
+ this.linkClickObserver = new LinkClickObserver(this);
2407
+ this.formSubmitObserver = new FormSubmitObserver(this);
2408
+ this.scrollObserver = new ScrollObserver(this);
2409
+ this.streamObserver = new StreamObserver(this);
2410
+ this.frameRedirector = new FrameRedirector(document.documentElement);
2411
+ this.enabled = true;
2412
+ this.progressBarDelay = 500;
2302
2413
  this.started = false;
2303
- this.inspectFetchResponse = event => {
2304
- const response = fetchResponseFromEvent(event);
2305
- if (response && fetchResponseIsStream(response)) {
2306
- event.preventDefault();
2307
- this.receiveMessageResponse(response);
2308
- }
2309
- };
2310
- this.receiveMessageEvent = event => {
2311
- if (this.started && typeof event.data == "string") {
2312
- this.receiveMessageHTML(event.data);
2313
- }
2314
- };
2315
- this.delegate = delegate;
2316
2414
  }
2317
2415
  start() {
2318
2416
  if (!this.started) {
2417
+ this.pageObserver.start();
2418
+ this.cacheObserver.start();
2419
+ this.linkClickObserver.start();
2420
+ this.formSubmitObserver.start();
2421
+ this.scrollObserver.start();
2422
+ this.streamObserver.start();
2423
+ this.frameRedirector.start();
2424
+ this.history.start();
2319
2425
  this.started = true;
2320
- addEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
2426
+ this.enabled = true;
2321
2427
  }
2322
2428
  }
2429
+ disable() {
2430
+ this.enabled = false;
2431
+ }
2323
2432
  stop() {
2324
2433
  if (this.started) {
2434
+ this.pageObserver.stop();
2435
+ this.cacheObserver.stop();
2436
+ this.linkClickObserver.stop();
2437
+ this.formSubmitObserver.stop();
2438
+ this.scrollObserver.stop();
2439
+ this.streamObserver.stop();
2440
+ this.frameRedirector.stop();
2441
+ this.history.stop();
2325
2442
  this.started = false;
2326
- removeEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
2327
2443
  }
2328
2444
  }
2445
+ registerAdapter(adapter) {
2446
+ this.adapter = adapter;
2447
+ }
2448
+ visit(location, options = {}) {
2449
+ this.navigator.proposeVisit(expandURL(location), options);
2450
+ }
2329
2451
  connectStreamSource(source) {
2330
- if (!this.streamSourceIsConnected(source)) {
2331
- this.sources.add(source);
2332
- source.addEventListener("message", this.receiveMessageEvent, false);
2333
- }
2452
+ this.streamObserver.connectStreamSource(source);
2334
2453
  }
2335
2454
  disconnectStreamSource(source) {
2336
- if (this.streamSourceIsConnected(source)) {
2337
- this.sources.delete(source);
2338
- source.removeEventListener("message", this.receiveMessageEvent, false);
2455
+ this.streamObserver.disconnectStreamSource(source);
2456
+ }
2457
+ renderStreamMessage(message) {
2458
+ document.documentElement.appendChild(StreamMessage.wrap(message).fragment);
2459
+ }
2460
+ clearCache() {
2461
+ this.view.clearSnapshotCache();
2462
+ }
2463
+ setProgressBarDelay(delay) {
2464
+ this.progressBarDelay = delay;
2465
+ }
2466
+ get location() {
2467
+ return this.history.location;
2468
+ }
2469
+ get restorationIdentifier() {
2470
+ return this.history.restorationIdentifier;
2471
+ }
2472
+ historyPoppedToLocationWithRestorationIdentifier(location, restorationIdentifier) {
2473
+ if (this.enabled) {
2474
+ this.navigator.startVisit(location, restorationIdentifier, {
2475
+ action: "restore",
2476
+ historyChanged: true
2477
+ });
2478
+ } else {
2479
+ this.adapter.pageInvalidated();
2339
2480
  }
2340
2481
  }
2341
- streamSourceIsConnected(source) {
2342
- return this.sources.has(source);
2482
+ scrollPositionChanged(position) {
2483
+ this.history.updateRestorationData({
2484
+ scrollPosition: position
2485
+ });
2343
2486
  }
2344
- async receiveMessageResponse(response) {
2345
- const html = await response.responseHTML;
2346
- if (html) {
2347
- this.receiveMessageHTML(html);
2487
+ willFollowLinkToLocation(link, location) {
2488
+ return elementIsNavigable(link) && this.locationIsVisitable(location) && this.applicationAllowsFollowingLinkToLocation(link, location);
2489
+ }
2490
+ followedLinkToLocation(link, location) {
2491
+ const action = this.getActionForLink(link);
2492
+ this.convertLinkWithMethodClickToFormSubmission(link) || this.visit(location.href, {
2493
+ action: action
2494
+ });
2495
+ }
2496
+ convertLinkWithMethodClickToFormSubmission(link) {
2497
+ var _a;
2498
+ const linkMethod = link.getAttribute("data-turbo-method");
2499
+ if (linkMethod) {
2500
+ const form = document.createElement("form");
2501
+ form.method = linkMethod;
2502
+ form.action = link.getAttribute("href") || "undefined";
2503
+ (_a = link.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(form, link);
2504
+ return dispatch("submit", {
2505
+ cancelable: true,
2506
+ target: form
2507
+ });
2508
+ } else {
2509
+ return false;
2348
2510
  }
2349
2511
  }
2350
- receiveMessageHTML(html) {
2351
- this.delegate.receivedMessageFromStream(new StreamMessage(html));
2512
+ allowsVisitingLocation(location) {
2513
+ return this.applicationAllowsVisitingLocation(location);
2352
2514
  }
2353
- }
2354
-
2355
- function fetchResponseFromEvent(event) {
2356
- var _a;
2357
- const fetchResponse = (_a = event.detail) === null || _a === void 0 ? void 0 : _a.fetchResponse;
2358
- if (fetchResponse instanceof FetchResponse) {
2359
- return fetchResponse;
2515
+ visitProposedToLocation(location, options) {
2516
+ extendURLWithDeprecatedProperties(location);
2517
+ this.adapter.visitProposedToLocation(location, options);
2360
2518
  }
2361
- }
2362
-
2363
- function fetchResponseIsStream(response) {
2364
- var _a;
2365
- const contentType = (_a = response.contentType) !== null && _a !== void 0 ? _a : "";
2366
- return contentType.startsWith(StreamMessage.contentType);
2367
- }
2368
-
2369
- function isAction(action) {
2370
- return action == "advance" || action == "replace" || action == "restore";
2371
- }
2372
-
2373
- class ErrorRenderer extends Renderer {
2374
- async render() {
2375
- this.replaceHeadAndBody();
2376
- this.activateScriptElements();
2519
+ visitStarted(visit) {
2520
+ extendURLWithDeprecatedProperties(visit.location);
2521
+ this.notifyApplicationAfterVisitingLocation(visit.location, visit.action);
2377
2522
  }
2378
- replaceHeadAndBody() {
2379
- const {documentElement: documentElement, head: head, body: body} = document;
2380
- documentElement.replaceChild(this.newHead, head);
2381
- documentElement.replaceChild(this.newElement, body);
2523
+ visitCompleted(visit) {
2524
+ this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
2382
2525
  }
2383
- activateScriptElements() {
2384
- for (const replaceableElement of this.scriptElements) {
2385
- const parentNode = replaceableElement.parentNode;
2386
- if (parentNode) {
2387
- const element = this.createScriptElement(replaceableElement);
2388
- parentNode.replaceChild(element, replaceableElement);
2389
- }
2390
- }
2526
+ locationWithActionIsSamePage(location, action) {
2527
+ return this.navigator.locationWithActionIsSamePage(location, action);
2391
2528
  }
2392
- get newHead() {
2393
- return this.newSnapshot.headSnapshot.element;
2529
+ visitScrolledToSamePageLocation(oldURL, newURL) {
2530
+ this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
2394
2531
  }
2395
- get scriptElements() {
2396
- return [ ...document.documentElement.querySelectorAll("script") ];
2532
+ willSubmitForm(form, submitter) {
2533
+ return elementIsNavigable(form) && elementIsNavigable(submitter);
2397
2534
  }
2398
- }
2399
-
2400
- class PageRenderer extends Renderer {
2401
- get shouldRender() {
2402
- return this.newSnapshot.isVisitable && this.trackedElementsAreIdentical;
2535
+ formSubmitted(form, submitter) {
2536
+ this.navigator.submitForm(form, submitter);
2403
2537
  }
2404
- prepareToRender() {
2405
- this.mergeHead();
2538
+ pageBecameInteractive() {
2539
+ this.view.lastRenderedLocation = this.location;
2540
+ this.notifyApplicationAfterPageLoad();
2406
2541
  }
2407
- async render() {
2408
- this.replaceBody();
2542
+ pageLoaded() {
2543
+ this.history.assumeControlOfScrollRestoration();
2409
2544
  }
2410
- finishRendering() {
2411
- super.finishRendering();
2412
- if (this.isPreview) {
2413
- this.focusFirstAutofocusableElement();
2414
- }
2545
+ pageWillUnload() {
2546
+ this.history.relinquishControlOfScrollRestoration();
2415
2547
  }
2416
- get currentHeadSnapshot() {
2417
- return this.currentSnapshot.headSnapshot;
2548
+ receivedMessageFromStream(message) {
2549
+ this.renderStreamMessage(message);
2418
2550
  }
2419
- get newHeadSnapshot() {
2420
- return this.newSnapshot.headSnapshot;
2551
+ viewWillCacheSnapshot() {
2552
+ this.notifyApplicationBeforeCachingSnapshot();
2421
2553
  }
2422
- get newElement() {
2423
- return this.newSnapshot.element;
2554
+ allowsImmediateRender({element: element}, resume) {
2555
+ const event = this.notifyApplicationBeforeRender(element, resume);
2556
+ return !event.defaultPrevented;
2424
2557
  }
2425
- mergeHead() {
2426
- this.copyNewHeadStylesheetElements();
2427
- this.copyNewHeadScriptElements();
2428
- this.removeCurrentHeadProvisionalElements();
2429
- this.copyNewHeadProvisionalElements();
2558
+ viewRenderedSnapshot(snapshot, isPreview) {
2559
+ this.view.lastRenderedLocation = this.history.location;
2560
+ this.notifyApplicationAfterRender();
2430
2561
  }
2431
- replaceBody() {
2432
- this.preservingPermanentElements((() => {
2433
- this.activateNewBody();
2434
- this.assignNewBody();
2435
- }));
2562
+ viewInvalidated() {
2563
+ this.adapter.pageInvalidated();
2436
2564
  }
2437
- get trackedElementsAreIdentical() {
2438
- return this.currentHeadSnapshot.trackedElementSignature == this.newHeadSnapshot.trackedElementSignature;
2565
+ applicationAllowsFollowingLinkToLocation(link, location) {
2566
+ const event = this.notifyApplicationAfterClickingLinkToLocation(link, location);
2567
+ return !event.defaultPrevented;
2439
2568
  }
2440
- copyNewHeadStylesheetElements() {
2441
- for (const element of this.newHeadStylesheetElements) {
2442
- document.head.appendChild(element);
2443
- }
2569
+ applicationAllowsVisitingLocation(location) {
2570
+ const event = this.notifyApplicationBeforeVisitingLocation(location);
2571
+ return !event.defaultPrevented;
2444
2572
  }
2445
- copyNewHeadScriptElements() {
2446
- for (const element of this.newHeadScriptElements) {
2447
- document.head.appendChild(this.createScriptElement(element));
2448
- }
2573
+ notifyApplicationAfterClickingLinkToLocation(link, location) {
2574
+ return dispatch("turbo:click", {
2575
+ target: link,
2576
+ detail: {
2577
+ url: location.href
2578
+ },
2579
+ cancelable: true
2580
+ });
2449
2581
  }
2450
- removeCurrentHeadProvisionalElements() {
2451
- for (const element of this.currentHeadProvisionalElements) {
2452
- document.head.removeChild(element);
2453
- }
2582
+ notifyApplicationBeforeVisitingLocation(location) {
2583
+ return dispatch("turbo:before-visit", {
2584
+ detail: {
2585
+ url: location.href
2586
+ },
2587
+ cancelable: true
2588
+ });
2454
2589
  }
2455
- copyNewHeadProvisionalElements() {
2456
- for (const element of this.newHeadProvisionalElements) {
2457
- document.head.appendChild(element);
2458
- }
2590
+ notifyApplicationAfterVisitingLocation(location, action) {
2591
+ return dispatch("turbo:visit", {
2592
+ detail: {
2593
+ url: location.href,
2594
+ action: action
2595
+ }
2596
+ });
2459
2597
  }
2460
- activateNewBody() {
2461
- document.adoptNode(this.newElement);
2462
- this.activateNewBodyScriptElements();
2598
+ notifyApplicationBeforeCachingSnapshot() {
2599
+ return dispatch("turbo:before-cache");
2463
2600
  }
2464
- activateNewBodyScriptElements() {
2465
- for (const inertScriptElement of this.newBodyScriptElements) {
2466
- const activatedScriptElement = this.createScriptElement(inertScriptElement);
2467
- replaceElementWithElement(inertScriptElement, activatedScriptElement);
2468
- }
2601
+ notifyApplicationBeforeRender(newBody, resume) {
2602
+ return dispatch("turbo:before-render", {
2603
+ detail: {
2604
+ newBody: newBody,
2605
+ resume: resume
2606
+ },
2607
+ cancelable: true
2608
+ });
2469
2609
  }
2470
- assignNewBody() {
2471
- if (document.body && this.newElement instanceof HTMLBodyElement) {
2472
- replaceElementWithElement(document.body, this.newElement);
2473
- } else {
2474
- document.documentElement.appendChild(this.newElement);
2475
- }
2610
+ notifyApplicationAfterRender() {
2611
+ return dispatch("turbo:render");
2476
2612
  }
2477
- get newHeadStylesheetElements() {
2478
- return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot);
2613
+ notifyApplicationAfterPageLoad(timing = {}) {
2614
+ return dispatch("turbo:load", {
2615
+ detail: {
2616
+ url: this.location.href,
2617
+ timing: timing
2618
+ }
2619
+ });
2479
2620
  }
2480
- get newHeadScriptElements() {
2481
- return this.newHeadSnapshot.getScriptElementsNotInSnapshot(this.currentHeadSnapshot);
2621
+ notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL) {
2622
+ dispatchEvent(new HashChangeEvent("hashchange", {
2623
+ oldURL: oldURL.toString(),
2624
+ newURL: newURL.toString()
2625
+ }));
2482
2626
  }
2483
- get currentHeadProvisionalElements() {
2484
- return this.currentHeadSnapshot.provisionalElements;
2627
+ getActionForLink(link) {
2628
+ const action = link.getAttribute("data-turbo-action");
2629
+ return isAction(action) ? action : "advance";
2485
2630
  }
2486
- get newHeadProvisionalElements() {
2487
- return this.newHeadSnapshot.provisionalElements;
2631
+ locationIsVisitable(location) {
2632
+ return isPrefixedBy(location, this.snapshot.rootLocation) && isHTML(location);
2488
2633
  }
2489
- get newBodyScriptElements() {
2490
- return [ ...this.newElement.querySelectorAll("script") ];
2634
+ get snapshot() {
2635
+ return this.view.snapshot;
2491
2636
  }
2492
2637
  }
2493
2638
 
2494
- class SnapshotCache {
2495
- constructor(size) {
2496
- this.keys = [];
2497
- this.snapshots = {};
2498
- this.size = size;
2639
+ function elementIsNavigable(element) {
2640
+ const container = element === null || element === void 0 ? void 0 : element.closest("[data-turbo]");
2641
+ if (container) {
2642
+ return container.getAttribute("data-turbo") != "false";
2643
+ } else {
2644
+ return true;
2499
2645
  }
2500
- has(location) {
2501
- return toCacheKey(location) in this.snapshots;
2646
+ }
2647
+
2648
+ function extendURLWithDeprecatedProperties(url) {
2649
+ Object.defineProperties(url, deprecatedLocationPropertyDescriptors);
2650
+ }
2651
+
2652
+ const deprecatedLocationPropertyDescriptors = {
2653
+ absoluteURL: {
2654
+ get() {
2655
+ return this.toString();
2656
+ }
2502
2657
  }
2503
- get(location) {
2504
- if (this.has(location)) {
2505
- const snapshot = this.read(location);
2506
- this.touch(location);
2507
- return snapshot;
2658
+ };
2659
+
2660
+ class FrameController {
2661
+ constructor(element) {
2662
+ this.resolveVisitPromise = () => {};
2663
+ this.connected = false;
2664
+ this.hasBeenLoaded = false;
2665
+ this.settingSourceURL = false;
2666
+ this.element = element;
2667
+ this.view = new FrameView(this, this.element);
2668
+ this.appearanceObserver = new AppearanceObserver(this, this.element);
2669
+ this.linkInterceptor = new LinkInterceptor(this, this.element);
2670
+ this.formInterceptor = new FormInterceptor(this, this.element);
2671
+ }
2672
+ connect() {
2673
+ if (!this.connected) {
2674
+ this.connected = true;
2675
+ if (this.loadingStyle == FrameLoadingStyle.lazy) {
2676
+ this.appearanceObserver.start();
2677
+ }
2678
+ this.linkInterceptor.start();
2679
+ this.formInterceptor.start();
2680
+ this.sourceURLChanged();
2508
2681
  }
2509
2682
  }
2510
- put(location, snapshot) {
2511
- this.write(location, snapshot);
2512
- this.touch(location);
2513
- return snapshot;
2683
+ disconnect() {
2684
+ if (this.connected) {
2685
+ this.connected = false;
2686
+ this.appearanceObserver.stop();
2687
+ this.linkInterceptor.stop();
2688
+ this.formInterceptor.stop();
2689
+ }
2514
2690
  }
2515
- clear() {
2516
- this.snapshots = {};
2691
+ disabledChanged() {
2692
+ if (this.loadingStyle == FrameLoadingStyle.eager) {
2693
+ this.loadSourceURL();
2694
+ }
2517
2695
  }
2518
- read(location) {
2519
- return this.snapshots[toCacheKey(location)];
2696
+ sourceURLChanged() {
2697
+ if (this.loadingStyle == FrameLoadingStyle.eager || this.hasBeenLoaded) {
2698
+ this.loadSourceURL();
2699
+ }
2520
2700
  }
2521
- write(location, snapshot) {
2522
- this.snapshots[toCacheKey(location)] = snapshot;
2701
+ loadingStyleChanged() {
2702
+ if (this.loadingStyle == FrameLoadingStyle.lazy) {
2703
+ this.appearanceObserver.start();
2704
+ } else {
2705
+ this.appearanceObserver.stop();
2706
+ this.loadSourceURL();
2707
+ }
2523
2708
  }
2524
- touch(location) {
2525
- const key = toCacheKey(location);
2526
- const index = this.keys.indexOf(key);
2527
- if (index > -1) this.keys.splice(index, 1);
2528
- this.keys.unshift(key);
2529
- this.trim();
2709
+ async loadSourceURL() {
2710
+ if (!this.settingSourceURL && this.enabled && this.isActive && this.sourceURL != this.currentURL) {
2711
+ const previousURL = this.currentURL;
2712
+ this.currentURL = this.sourceURL;
2713
+ if (this.sourceURL) {
2714
+ try {
2715
+ this.element.loaded = this.visit(this.sourceURL);
2716
+ this.appearanceObserver.stop();
2717
+ await this.element.loaded;
2718
+ this.hasBeenLoaded = true;
2719
+ } catch (error) {
2720
+ this.currentURL = previousURL;
2721
+ throw error;
2722
+ }
2723
+ }
2724
+ }
2530
2725
  }
2531
- trim() {
2532
- for (const key of this.keys.splice(this.size)) {
2533
- delete this.snapshots[key];
2726
+ async loadResponse(fetchResponse) {
2727
+ if (fetchResponse.redirected) {
2728
+ this.sourceURL = fetchResponse.response.url;
2729
+ }
2730
+ try {
2731
+ const html = await fetchResponse.responseHTML;
2732
+ if (html) {
2733
+ const {body: body} = parseHTMLDocument(html);
2734
+ const snapshot = new Snapshot(await this.extractForeignFrameElement(body));
2735
+ const renderer = new FrameRenderer(this.view.snapshot, snapshot, false);
2736
+ if (this.view.renderPromise) await this.view.renderPromise;
2737
+ await this.view.render(renderer);
2738
+ }
2739
+ } catch (error) {
2740
+ console.error(error);
2741
+ this.view.invalidate();
2534
2742
  }
2535
2743
  }
2536
- }
2537
-
2538
- class PageView extends View {
2539
- constructor() {
2540
- super(...arguments);
2541
- this.snapshotCache = new SnapshotCache(10);
2542
- this.lastRenderedLocation = new URL(location.href);
2744
+ elementAppearedInViewport(element) {
2745
+ this.loadSourceURL();
2543
2746
  }
2544
- renderPage(snapshot, isPreview = false) {
2545
- const renderer = new PageRenderer(this.snapshot, snapshot, isPreview);
2546
- return this.render(renderer);
2747
+ shouldInterceptLinkClick(element, url) {
2748
+ if (element.hasAttribute("data-turbo-method")) {
2749
+ return false;
2750
+ } else {
2751
+ return this.shouldInterceptNavigation(element);
2752
+ }
2547
2753
  }
2548
- renderError(snapshot) {
2549
- const renderer = new ErrorRenderer(this.snapshot, snapshot, false);
2550
- this.render(renderer);
2754
+ linkClickIntercepted(element, url) {
2755
+ this.navigateFrame(element, url);
2551
2756
  }
2552
- clearSnapshotCache() {
2553
- this.snapshotCache.clear();
2757
+ shouldInterceptFormSubmission(element, submitter) {
2758
+ return this.shouldInterceptNavigation(element, submitter);
2554
2759
  }
2555
- async cacheSnapshot() {
2556
- if (this.shouldCacheSnapshot) {
2557
- this.delegate.viewWillCacheSnapshot();
2558
- const {snapshot: snapshot, lastRenderedLocation: location} = this;
2559
- await nextEventLoopTick();
2560
- this.snapshotCache.put(location, snapshot.clone());
2760
+ formSubmissionIntercepted(element, submitter) {
2761
+ if (this.formSubmission) {
2762
+ this.formSubmission.stop();
2763
+ }
2764
+ this.formSubmission = new FormSubmission(this, element, submitter);
2765
+ if (this.formSubmission.fetchRequest.isIdempotent) {
2766
+ this.navigateFrame(element, this.formSubmission.fetchRequest.url.href);
2767
+ } else {
2768
+ const {fetchRequest: fetchRequest} = this.formSubmission;
2769
+ this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
2770
+ this.formSubmission.start();
2561
2771
  }
2562
2772
  }
2563
- getCachedSnapshotForLocation(location) {
2564
- return this.snapshotCache.get(location);
2773
+ prepareHeadersForRequest(headers, request) {
2774
+ headers["Turbo-Frame"] = this.id;
2565
2775
  }
2566
- get snapshot() {
2567
- return PageSnapshot.fromElement(this.element);
2776
+ requestStarted(request) {
2777
+ this.element.setAttribute("busy", "");
2568
2778
  }
2569
- get shouldCacheSnapshot() {
2570
- return this.snapshot.isCacheable;
2779
+ requestPreventedHandlingResponse(request, response) {
2780
+ this.resolveVisitPromise();
2571
2781
  }
2572
- }
2573
-
2574
- class Session {
2575
- constructor() {
2576
- this.navigator = new Navigator(this);
2577
- this.history = new History(this);
2578
- this.view = new PageView(this, document.documentElement);
2579
- this.adapter = new BrowserAdapter(this);
2580
- this.pageObserver = new PageObserver(this);
2581
- this.linkClickObserver = new LinkClickObserver(this);
2582
- this.formSubmitObserver = new FormSubmitObserver(this);
2583
- this.scrollObserver = new ScrollObserver(this);
2584
- this.streamObserver = new StreamObserver(this);
2585
- this.frameRedirector = new FrameRedirector(document.documentElement);
2586
- this.enabled = true;
2587
- this.progressBarDelay = 500;
2588
- this.started = false;
2782
+ async requestSucceededWithResponse(request, response) {
2783
+ await this.loadResponse(response);
2784
+ this.resolveVisitPromise();
2589
2785
  }
2590
- start() {
2591
- if (!this.started) {
2592
- this.pageObserver.start();
2593
- this.linkClickObserver.start();
2594
- this.formSubmitObserver.start();
2595
- this.scrollObserver.start();
2596
- this.streamObserver.start();
2597
- this.frameRedirector.start();
2598
- this.history.start();
2599
- this.started = true;
2600
- this.enabled = true;
2601
- }
2786
+ requestFailedWithResponse(request, response) {
2787
+ console.error(response);
2788
+ this.resolveVisitPromise();
2602
2789
  }
2603
- disable() {
2604
- this.enabled = false;
2790
+ requestErrored(request, error) {
2791
+ console.error(error);
2792
+ this.resolveVisitPromise();
2605
2793
  }
2606
- stop() {
2607
- if (this.started) {
2608
- this.pageObserver.stop();
2609
- this.linkClickObserver.stop();
2610
- this.formSubmitObserver.stop();
2611
- this.scrollObserver.stop();
2612
- this.streamObserver.stop();
2613
- this.frameRedirector.stop();
2614
- this.history.stop();
2615
- this.started = false;
2616
- }
2794
+ requestFinished(request) {
2795
+ this.element.removeAttribute("busy");
2617
2796
  }
2618
- registerAdapter(adapter) {
2619
- this.adapter = adapter;
2797
+ formSubmissionStarted(formSubmission) {
2798
+ const frame = this.findFrameElement(formSubmission.formElement);
2799
+ frame.setAttribute("busy", "");
2620
2800
  }
2621
- visit(location, options = {}) {
2622
- this.navigator.proposeVisit(expandURL(location), options);
2801
+ formSubmissionSucceededWithResponse(formSubmission, response) {
2802
+ const frame = this.findFrameElement(formSubmission.formElement);
2803
+ frame.delegate.loadResponse(response);
2623
2804
  }
2624
- connectStreamSource(source) {
2625
- this.streamObserver.connectStreamSource(source);
2805
+ formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
2806
+ this.element.delegate.loadResponse(fetchResponse);
2626
2807
  }
2627
- disconnectStreamSource(source) {
2628
- this.streamObserver.disconnectStreamSource(source);
2808
+ formSubmissionErrored(formSubmission, error) {
2809
+ console.error(error);
2629
2810
  }
2630
- renderStreamMessage(message) {
2631
- document.documentElement.appendChild(StreamMessage.wrap(message).fragment);
2811
+ formSubmissionFinished(formSubmission) {
2812
+ const frame = this.findFrameElement(formSubmission.formElement);
2813
+ frame.removeAttribute("busy");
2632
2814
  }
2633
- clearCache() {
2634
- this.view.clearSnapshotCache();
2815
+ allowsImmediateRender(snapshot, resume) {
2816
+ return true;
2635
2817
  }
2636
- setProgressBarDelay(delay) {
2637
- this.progressBarDelay = delay;
2818
+ viewRenderedSnapshot(snapshot, isPreview) {}
2819
+ viewInvalidated() {}
2820
+ async visit(url) {
2821
+ const request = new FetchRequest(this, FetchMethod.get, expandURL(url));
2822
+ return new Promise((resolve => {
2823
+ this.resolveVisitPromise = () => {
2824
+ this.resolveVisitPromise = () => {};
2825
+ resolve();
2826
+ };
2827
+ request.perform();
2828
+ }));
2638
2829
  }
2639
- get location() {
2640
- return this.history.location;
2830
+ navigateFrame(element, url) {
2831
+ const frame = this.findFrameElement(element);
2832
+ frame.src = url;
2641
2833
  }
2642
- get restorationIdentifier() {
2643
- return this.history.restorationIdentifier;
2834
+ findFrameElement(element) {
2835
+ var _a;
2836
+ const id = element.getAttribute("data-turbo-frame") || this.element.getAttribute("target");
2837
+ return (_a = getFrameElementById(id)) !== null && _a !== void 0 ? _a : this.element;
2644
2838
  }
2645
- historyPoppedToLocationWithRestorationIdentifier(location) {
2646
- if (this.enabled) {
2647
- this.navigator.proposeVisit(location, {
2648
- action: "restore",
2649
- historyChanged: true
2650
- });
2651
- } else {
2652
- this.adapter.pageInvalidated();
2839
+ async extractForeignFrameElement(container) {
2840
+ let element;
2841
+ const id = CSS.escape(this.id);
2842
+ try {
2843
+ if (element = activateElement(container.querySelector(`turbo-frame#${id}`), this.currentURL)) {
2844
+ return element;
2845
+ }
2846
+ if (element = activateElement(container.querySelector(`turbo-frame[src][recurse~=${id}]`), this.currentURL)) {
2847
+ await element.loaded;
2848
+ return await this.extractForeignFrameElement(element);
2849
+ }
2850
+ console.error(`Response has no matching <turbo-frame id="${id}"> element`);
2851
+ } catch (error) {
2852
+ console.error(error);
2653
2853
  }
2854
+ return new FrameElement;
2654
2855
  }
2655
- scrollPositionChanged(position) {
2656
- this.history.updateRestorationData({
2657
- scrollPosition: position
2658
- });
2856
+ shouldInterceptNavigation(element, submitter) {
2857
+ const id = element.getAttribute("data-turbo-frame") || this.element.getAttribute("target");
2858
+ if (!this.enabled || id == "_top") {
2859
+ return false;
2860
+ }
2861
+ if (id) {
2862
+ const frameElement = getFrameElementById(id);
2863
+ if (frameElement) {
2864
+ return !frameElement.disabled;
2865
+ }
2866
+ }
2867
+ if (!elementIsNavigable(element)) {
2868
+ return false;
2869
+ }
2870
+ if (submitter && !elementIsNavigable(submitter)) {
2871
+ return false;
2872
+ }
2873
+ return true;
2659
2874
  }
2660
- willFollowLinkToLocation(link, location) {
2661
- return this.elementIsNavigable(link) && this.locationIsVisitable(location) && this.applicationAllowsFollowingLinkToLocation(link, location);
2875
+ get id() {
2876
+ return this.element.id;
2662
2877
  }
2663
- followedLinkToLocation(link, location) {
2664
- const action = this.getActionForLink(link);
2665
- this.visit(location.href, {
2666
- action: action
2667
- });
2878
+ get enabled() {
2879
+ return !this.element.disabled;
2668
2880
  }
2669
- allowsVisitingLocation(location) {
2670
- return this.applicationAllowsVisitingLocation(location);
2881
+ get sourceURL() {
2882
+ if (this.element.src) {
2883
+ return this.element.src;
2884
+ }
2671
2885
  }
2672
- visitProposedToLocation(location, options) {
2673
- extendURLWithDeprecatedProperties(location);
2674
- this.adapter.visitProposedToLocation(location, options);
2886
+ set sourceURL(sourceURL) {
2887
+ this.settingSourceURL = true;
2888
+ this.element.src = sourceURL !== null && sourceURL !== void 0 ? sourceURL : null;
2889
+ this.currentURL = this.element.src;
2890
+ this.settingSourceURL = false;
2675
2891
  }
2676
- visitStarted(visit) {
2677
- extendURLWithDeprecatedProperties(visit.location);
2678
- this.notifyApplicationAfterVisitingLocation(visit.location);
2892
+ get loadingStyle() {
2893
+ return this.element.loading;
2679
2894
  }
2680
- visitCompleted(visit) {
2681
- this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
2895
+ get isLoading() {
2896
+ return this.formSubmission !== undefined || this.resolveVisitPromise() !== undefined;
2682
2897
  }
2683
- willSubmitForm(form, submitter) {
2684
- return this.elementIsNavigable(form) && this.elementIsNavigable(submitter);
2898
+ get isActive() {
2899
+ return this.element.isActive && this.connected;
2685
2900
  }
2686
- formSubmitted(form, submitter) {
2687
- this.navigator.submitForm(form, submitter);
2901
+ }
2902
+
2903
+ function getFrameElementById(id) {
2904
+ if (id != null) {
2905
+ const element = document.getElementById(id);
2906
+ if (element instanceof FrameElement) {
2907
+ return element;
2908
+ }
2688
2909
  }
2689
- pageBecameInteractive() {
2690
- this.view.lastRenderedLocation = this.location;
2691
- this.notifyApplicationAfterPageLoad();
2910
+ }
2911
+
2912
+ function activateElement(element, currentURL) {
2913
+ if (element) {
2914
+ const src = element.getAttribute("src");
2915
+ if (src != null && currentURL != null && urlsAreEqual(src, currentURL)) {
2916
+ throw new Error(`Matching <turbo-frame id="${element.id}"> element has a source URL which references itself`);
2917
+ }
2918
+ if (element.ownerDocument !== document) {
2919
+ element = document.importNode(element, true);
2920
+ }
2921
+ if (element instanceof FrameElement) {
2922
+ element.connectedCallback();
2923
+ return element;
2924
+ }
2692
2925
  }
2693
- pageLoaded() {
2694
- this.history.assumeControlOfScrollRestoration();
2926
+ }
2927
+
2928
+ const StreamActions = {
2929
+ after() {
2930
+ this.targetElements.forEach((e => {
2931
+ var _a;
2932
+ return (_a = e.parentElement) === null || _a === void 0 ? void 0 : _a.insertBefore(this.templateContent, e.nextSibling);
2933
+ }));
2934
+ },
2935
+ append() {
2936
+ this.removeDuplicateTargetChildren();
2937
+ this.targetElements.forEach((e => e.append(this.templateContent)));
2938
+ },
2939
+ before() {
2940
+ this.targetElements.forEach((e => {
2941
+ var _a;
2942
+ return (_a = e.parentElement) === null || _a === void 0 ? void 0 : _a.insertBefore(this.templateContent, e);
2943
+ }));
2944
+ },
2945
+ prepend() {
2946
+ this.removeDuplicateTargetChildren();
2947
+ this.targetElements.forEach((e => e.prepend(this.templateContent)));
2948
+ },
2949
+ remove() {
2950
+ this.targetElements.forEach((e => e.remove()));
2951
+ },
2952
+ replace() {
2953
+ this.targetElements.forEach((e => e.replaceWith(this.templateContent)));
2954
+ },
2955
+ update() {
2956
+ this.targetElements.forEach((e => {
2957
+ e.innerHTML = "";
2958
+ e.append(this.templateContent);
2959
+ }));
2695
2960
  }
2696
- pageWillUnload() {
2697
- this.history.relinquishControlOfScrollRestoration();
2961
+ };
2962
+
2963
+ class StreamElement extends HTMLElement {
2964
+ async connectedCallback() {
2965
+ try {
2966
+ await this.render();
2967
+ } catch (error) {
2968
+ console.error(error);
2969
+ } finally {
2970
+ this.disconnect();
2971
+ }
2698
2972
  }
2699
- receivedMessageFromStream(message) {
2700
- this.renderStreamMessage(message);
2973
+ async render() {
2974
+ var _a;
2975
+ return (_a = this.renderPromise) !== null && _a !== void 0 ? _a : this.renderPromise = (async () => {
2976
+ if (this.dispatchEvent(this.beforeRenderEvent)) {
2977
+ await nextAnimationFrame();
2978
+ this.performAction();
2979
+ }
2980
+ })();
2701
2981
  }
2702
- viewWillCacheSnapshot() {
2703
- this.notifyApplicationBeforeCachingSnapshot();
2982
+ disconnect() {
2983
+ try {
2984
+ this.remove();
2985
+ } catch (_a) {}
2704
2986
  }
2705
- viewWillRenderSnapshot({element: element}, isPreview) {
2706
- this.notifyApplicationBeforeRender(element);
2987
+ removeDuplicateTargetChildren() {
2988
+ this.duplicateChildren.forEach((c => c.remove()));
2707
2989
  }
2708
- viewRenderedSnapshot(snapshot, isPreview) {
2709
- this.view.lastRenderedLocation = this.history.location;
2710
- this.notifyApplicationAfterRender();
2990
+ get duplicateChildren() {
2991
+ var _a;
2992
+ const existingChildren = this.targetElements.flatMap((e => [ ...e.children ])).filter((c => !!c.id));
2993
+ const newChildrenIds = [ ...(_a = this.templateContent) === null || _a === void 0 ? void 0 : _a.children ].filter((c => !!c.id)).map((c => c.id));
2994
+ return existingChildren.filter((c => newChildrenIds.includes(c.id)));
2711
2995
  }
2712
- viewInvalidated() {
2713
- this.adapter.pageInvalidated();
2996
+ get performAction() {
2997
+ if (this.action) {
2998
+ const actionFunction = StreamActions[this.action];
2999
+ if (actionFunction) {
3000
+ return actionFunction;
3001
+ }
3002
+ this.raise("unknown action");
3003
+ }
3004
+ this.raise("action attribute is missing");
2714
3005
  }
2715
- applicationAllowsFollowingLinkToLocation(link, location) {
2716
- const event = this.notifyApplicationAfterClickingLinkToLocation(link, location);
2717
- return !event.defaultPrevented;
3006
+ get targetElements() {
3007
+ if (this.target) {
3008
+ return this.targetElementsById;
3009
+ } else if (this.targets) {
3010
+ return this.targetElementsByQuery;
3011
+ } else {
3012
+ this.raise("target or targets attribute is missing");
3013
+ }
2718
3014
  }
2719
- applicationAllowsVisitingLocation(location) {
2720
- const event = this.notifyApplicationBeforeVisitingLocation(location);
2721
- return !event.defaultPrevented;
3015
+ get templateContent() {
3016
+ return this.templateElement.content.cloneNode(true);
2722
3017
  }
2723
- notifyApplicationAfterClickingLinkToLocation(link, location) {
2724
- return dispatch("turbo:click", {
2725
- target: link,
2726
- detail: {
2727
- url: location.href
2728
- },
2729
- cancelable: true
2730
- });
3018
+ get templateElement() {
3019
+ if (this.firstElementChild instanceof HTMLTemplateElement) {
3020
+ return this.firstElementChild;
3021
+ }
3022
+ this.raise("first child element must be a <template> element");
2731
3023
  }
2732
- notifyApplicationBeforeVisitingLocation(location) {
2733
- return dispatch("turbo:before-visit", {
2734
- detail: {
2735
- url: location.href
2736
- },
2737
- cancelable: true
2738
- });
3024
+ get action() {
3025
+ return this.getAttribute("action");
2739
3026
  }
2740
- notifyApplicationAfterVisitingLocation(location) {
2741
- return dispatch("turbo:visit", {
2742
- detail: {
2743
- url: location.href
2744
- }
2745
- });
3027
+ get target() {
3028
+ return this.getAttribute("target");
2746
3029
  }
2747
- notifyApplicationBeforeCachingSnapshot() {
2748
- return dispatch("turbo:before-cache");
3030
+ get targets() {
3031
+ return this.getAttribute("targets");
2749
3032
  }
2750
- notifyApplicationBeforeRender(newBody) {
2751
- return dispatch("turbo:before-render", {
2752
- detail: {
2753
- newBody: newBody
2754
- }
2755
- });
3033
+ raise(message) {
3034
+ throw new Error(`${this.description}: ${message}`);
2756
3035
  }
2757
- notifyApplicationAfterRender() {
2758
- return dispatch("turbo:render");
3036
+ get description() {
3037
+ var _a, _b;
3038
+ return (_b = ((_a = this.outerHTML.match(/<[^>]+>/)) !== null && _a !== void 0 ? _a : [])[0]) !== null && _b !== void 0 ? _b : "<turbo-stream>";
2759
3039
  }
2760
- notifyApplicationAfterPageLoad(timing = {}) {
2761
- return dispatch("turbo:load", {
2762
- detail: {
2763
- url: this.location.href,
2764
- timing: timing
2765
- }
3040
+ get beforeRenderEvent() {
3041
+ return new CustomEvent("turbo:before-stream-render", {
3042
+ bubbles: true,
3043
+ cancelable: true
2766
3044
  });
2767
3045
  }
2768
- getActionForLink(link) {
2769
- const action = link.getAttribute("data-turbo-action");
2770
- return isAction(action) ? action : "advance";
2771
- }
2772
- elementIsNavigable(element) {
2773
- const container = element === null || element === void 0 ? void 0 : element.closest("[data-turbo]");
2774
- if (container) {
2775
- return container.getAttribute("data-turbo") != "false";
3046
+ get targetElementsById() {
3047
+ var _a;
3048
+ const element = (_a = this.ownerDocument) === null || _a === void 0 ? void 0 : _a.getElementById(this.target);
3049
+ if (element !== null) {
3050
+ return [ element ];
2776
3051
  } else {
2777
- return true;
3052
+ return [];
2778
3053
  }
2779
3054
  }
2780
- locationIsVisitable(location) {
2781
- return isPrefixedBy(location, this.snapshot.rootLocation) && isHTML(location);
2782
- }
2783
- get snapshot() {
2784
- return this.view.snapshot;
3055
+ get targetElementsByQuery() {
3056
+ var _a;
3057
+ const elements = (_a = this.ownerDocument) === null || _a === void 0 ? void 0 : _a.querySelectorAll(this.targets);
3058
+ if (elements.length !== 0) {
3059
+ return Array.prototype.slice.call(elements);
3060
+ } else {
3061
+ return [];
3062
+ }
2785
3063
  }
2786
3064
  }
2787
3065
 
2788
- function extendURLWithDeprecatedProperties(url) {
2789
- Object.defineProperties(url, deprecatedLocationPropertyDescriptors);
2790
- }
3066
+ FrameElement.delegateConstructor = FrameController;
2791
3067
 
2792
- const deprecatedLocationPropertyDescriptors = {
2793
- absoluteURL: {
2794
- get() {
2795
- return this.toString();
3068
+ customElements.define("turbo-frame", FrameElement);
3069
+
3070
+ customElements.define("turbo-stream", StreamElement);
3071
+
3072
+ (() => {
3073
+ let element = document.currentScript;
3074
+ if (!element) return;
3075
+ if (element.hasAttribute("data-turbo-suppress-warning")) return;
3076
+ while (element = element.parentElement) {
3077
+ if (element == document.body) {
3078
+ return console.warn(unindent`
3079
+ You are loading Turbo from a <script> element inside the <body> element. This is probably not what you meant to do!
3080
+
3081
+ Load your application’s JavaScript bundle inside the <head> element instead. <script> elements in <body> are evaluated with each page change.
3082
+
3083
+ For more information, see: https://turbo.hotwired.dev/handbook/building#working-with-script-elements
3084
+
3085
+ ——
3086
+ Suppress this warning by adding a "data-turbo-suppress-warning" attribute to: %s
3087
+ `, element.outerHTML);
2796
3088
  }
2797
3089
  }
2798
- };
3090
+ })();
2799
3091
 
2800
3092
  const session = new Session;
2801
3093
 
@@ -2833,10 +3125,29 @@ function setProgressBarDelay(delay) {
2833
3125
  session.setProgressBarDelay(delay);
2834
3126
  }
2835
3127
 
3128
+ var Turbo = Object.freeze({
3129
+ __proto__: null,
3130
+ navigator: navigator,
3131
+ PageRenderer: PageRenderer,
3132
+ PageSnapshot: PageSnapshot,
3133
+ start: start,
3134
+ registerAdapter: registerAdapter,
3135
+ visit: visit,
3136
+ connectStreamSource: connectStreamSource,
3137
+ disconnectStreamSource: disconnectStreamSource,
3138
+ renderStreamMessage: renderStreamMessage,
3139
+ clearCache: clearCache,
3140
+ setProgressBarDelay: setProgressBarDelay
3141
+ });
3142
+
3143
+ window.Turbo = Turbo;
3144
+
2836
3145
  start();
2837
3146
 
2838
3147
  var turbo_es2017Esm = Object.freeze({
2839
3148
  __proto__: null,
3149
+ PageRenderer: PageRenderer,
3150
+ PageSnapshot: PageSnapshot,
2840
3151
  clearCache: clearCache,
2841
3152
  connectStreamSource: connectStreamSource,
2842
3153
  disconnectStreamSource: disconnectStreamSource,
@@ -2851,17 +3162,20 @@ var turbo_es2017Esm = Object.freeze({
2851
3162
  let consumer;
2852
3163
 
2853
3164
  async function getConsumer() {
2854
- if (consumer) return consumer;
2855
- const {createConsumer: createConsumer} = await Promise.resolve().then((function() {
2856
- return index;
2857
- }));
2858
- return setConsumer(createConsumer());
3165
+ return consumer || setConsumer(createConsumer().then(setConsumer));
2859
3166
  }
2860
3167
 
2861
3168
  function setConsumer(newConsumer) {
2862
3169
  return consumer = newConsumer;
2863
3170
  }
2864
3171
 
3172
+ async function createConsumer() {
3173
+ const {createConsumer: createConsumer} = await Promise.resolve().then((function() {
3174
+ return index;
3175
+ }));
3176
+ return createConsumer();
3177
+ }
3178
+
2865
3179
  async function subscribeTo(channel, mixin) {
2866
3180
  const {subscriptions: subscriptions} = await getConsumer();
2867
3181
  return subscriptions.create(channel, mixin);
@@ -2871,6 +3185,7 @@ var cable = Object.freeze({
2871
3185
  __proto__: null,
2872
3186
  getConsumer: getConsumer,
2873
3187
  setConsumer: setConsumer,
3188
+ createConsumer: createConsumer,
2874
3189
  subscribeTo: subscribeTo
2875
3190
  });
2876
3191
 
@@ -3334,7 +3649,7 @@ function createWebSocketURL(url) {
3334
3649
  }
3335
3650
  }
3336
3651
 
3337
- function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) {
3652
+ function createConsumer$1(url = getConfig("url") || INTERNAL.default_mount_path) {
3338
3653
  return new Consumer(url);
3339
3654
  }
3340
3655
 
@@ -3356,7 +3671,7 @@ var index = Object.freeze({
3356
3671
  adapters: adapters,
3357
3672
  createWebSocketURL: createWebSocketURL,
3358
3673
  logger: logger,
3359
- createConsumer: createConsumer,
3674
+ createConsumer: createConsumer$1,
3360
3675
  getConfig: getConfig
3361
3676
  });
3362
3677