turbo-rails 0.5.7 → 0.5.12

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: bde12fe28a415f37f7000160bfe9577c066d0a450b2a8b3a072acb11a620f229
4
- data.tar.gz: 06e64c67ade4fa00e7ba13eabf31e3f78fef72e8e9faedd66861b0d370c9e94e
3
+ metadata.gz: 27a0cac6803a0e1e410fb4fce4aaa60a0998aee4dec7d1b4b4cbb674533ce5cf
4
+ data.tar.gz: a4a67e8cff6989445d669cd6aa3b64b39dfc7de4ca7afdfddfd41c1e9334558e
5
5
  SHA512:
6
- metadata.gz: d18cfb444d1511997d07d15e6696ef4a22dafef448ddbe21b0037c60c1b565ab4a24a623393dd9dbadb8685336f0fd6487d0350d4895d336b041b5d07bca7f66
7
- data.tar.gz: 1e094bc98631469c394117c65f8e73387c577c4d3e1fdfa329c9bf5ef3f5fd62ea7ba0a4efc394f8696e087f8f06c8870e7fe1ab4066bbe1049f659f22aa8163
6
+ metadata.gz: a2de3cde68a13beffda32366c8b209666b1bae8a8dc3423ed9f5645dcae00fe88ba9f8ff08582eb60c9212a209d24683adf14f220a670923eac6806268198583
7
+ data.tar.gz: be9f2251ca7bf7684d71ecfba9cbaaefbc64423d1b7f86428fc733facc9e1867ff6b54df1c756cc96ce0367ffe2cbb090af2bbf3319d432d88fbae005a3edd7b
data/README.md CHANGED
@@ -40,13 +40,12 @@ 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
@@ -56,11 +55,7 @@ You can watch [the video introduction to Hotwire](https://hotwire.dev/#screencas
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 ugprade 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
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();
@@ -68,6 +68,8 @@ class FrameElement extends HTMLElement {
68
68
  this.delegate.loadingStyleChanged();
69
69
  } else if (name == "src") {
70
70
  this.delegate.sourceURLChanged();
71
+ } else {
72
+ this.delegate.disabledChanged();
71
73
  }
72
74
  }
73
75
  get src() {
@@ -132,82 +134,61 @@ function frameLoadingStyleFromString(style) {
132
134
  }
133
135
  }
134
136
 
135
- class Location {
136
- constructor(url) {
137
- const linkWithAnchor = document.createElement("a");
138
- linkWithAnchor.href = url;
139
- this.absoluteURL = linkWithAnchor.href;
140
- const anchorLength = linkWithAnchor.hash.length;
141
- if (anchorLength < 2) {
142
- this.requestURL = this.absoluteURL;
143
- } else {
144
- this.requestURL = this.absoluteURL.slice(0, -anchorLength);
145
- this.anchor = linkWithAnchor.hash.slice(1);
146
- }
147
- }
148
- static get currentLocation() {
149
- return this.wrap(window.location.toString());
150
- }
151
- static wrap(locatable) {
152
- if (typeof locatable == "string") {
153
- return new this(locatable);
154
- } else if (locatable != null) {
155
- return locatable;
156
- }
157
- }
158
- getOrigin() {
159
- return this.absoluteURL.split("/", 3).join("/");
160
- }
161
- getPath() {
162
- return (this.requestURL.match(/\/\/[^/]*(\/[^?;]*)/) || [])[1] || "/";
163
- }
164
- getPathComponents() {
165
- return this.getPath().split("/").slice(1);
166
- }
167
- getLastPathComponent() {
168
- return this.getPathComponents().slice(-1)[0];
169
- }
170
- getExtension() {
171
- return (this.getLastPathComponent().match(/\.[^.]*$/) || [])[0] || "";
172
- }
173
- isHTML() {
174
- return !!this.getExtension().match(/^(?:|\.(?:htm|html|xhtml))$/);
175
- }
176
- isPrefixedBy(location) {
177
- const prefixURL = getPrefixURL(location);
178
- return this.isEqualTo(location) || stringStartsWith(this.absoluteURL, prefixURL);
179
- }
180
- isEqualTo(location) {
181
- return location && this.absoluteURL === location.absoluteURL;
182
- }
183
- toCacheKey() {
184
- return this.requestURL;
185
- }
186
- toJSON() {
187
- return this.absoluteURL;
188
- }
189
- toString() {
190
- return this.absoluteURL;
137
+ function expandURL(locatable) {
138
+ return new URL(locatable.toString(), document.baseURI);
139
+ }
140
+
141
+ function getAnchor(url) {
142
+ let anchorMatch;
143
+ if (url.hash) {
144
+ return url.hash.slice(1);
145
+ } else if (anchorMatch = url.href.match(/#(.*)$/)) {
146
+ return anchorMatch[1];
147
+ } else {
148
+ return "";
191
149
  }
192
- valueOf() {
193
- return this.absoluteURL;
150
+ }
151
+
152
+ function getExtension(url) {
153
+ return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || "";
154
+ }
155
+
156
+ function isHTML(url) {
157
+ return !!getExtension(url).match(/^(?:|\.(?:htm|html|xhtml))$/);
158
+ }
159
+
160
+ function isPrefixedBy(baseURL, url) {
161
+ const prefix = getPrefix(url);
162
+ return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix);
163
+ }
164
+
165
+ 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);
194
171
  }
195
172
  }
196
173
 
197
- function getPrefixURL(location) {
198
- return addTrailingSlash(location.getOrigin() + location.getPath());
174
+ function urlsAreEqual(left, right) {
175
+ return expandURL(left).href == expandURL(right).href;
176
+ }
177
+
178
+ function getPathComponents(url) {
179
+ return url.pathname.split("/").slice(1);
199
180
  }
200
181
 
201
- function addTrailingSlash(url) {
202
- return stringEndsWith(url, "/") ? url : url + "/";
182
+ function getLastPathComponent(url) {
183
+ return getPathComponents(url).slice(-1)[0];
203
184
  }
204
185
 
205
- function stringStartsWith(string, prefix) {
206
- return string.slice(0, prefix.length) === prefix;
186
+ function getPrefix(url) {
187
+ return addTrailingSlash(url.origin + url.pathname);
207
188
  }
208
189
 
209
- function stringEndsWith(string, suffix) {
210
- return string.slice(-suffix.length) === suffix;
190
+ function addTrailingSlash(value) {
191
+ return value.endsWith("/") ? value : value + "/";
211
192
  }
212
193
 
213
194
  class FetchResponse {
@@ -230,7 +211,7 @@ class FetchResponse {
230
211
  return this.response.redirected;
231
212
  }
232
213
  get location() {
233
- return Location.wrap(this.response.url);
214
+ return expandURL(this.response.url);
234
215
  }
235
216
  get isHTML() {
236
217
  return this.contentType && this.contentType.match(/^(?:text\/([^\s;,]+\b)?html|application\/xhtml\+xml)\b/);
@@ -270,10 +251,18 @@ function nextAnimationFrame() {
270
251
  return new Promise((resolve => requestAnimationFrame((() => resolve()))));
271
252
  }
272
253
 
254
+ function nextEventLoopTick() {
255
+ return new Promise((resolve => setTimeout((() => resolve()), 0)));
256
+ }
257
+
273
258
  function nextMicrotask() {
274
259
  return Promise.resolve();
275
260
  }
276
261
 
262
+ function parseHTMLDocument(html = "") {
263
+ return (new DOMParser).parseFromString(html, "text/html");
264
+ }
265
+
277
266
  function unindent(strings, ...values) {
278
267
  const lines = interpolate(strings, values).replace(/^\n/, "").split("\n");
279
268
  const match = lines[0].match(/^\s+/);
@@ -334,27 +323,23 @@ function fetchMethodFromString(method) {
334
323
  }
335
324
 
336
325
  class FetchRequest {
337
- constructor(delegate, method, location, body) {
326
+ constructor(delegate, method, location, body = new URLSearchParams) {
338
327
  this.abortController = new AbortController;
339
328
  this.delegate = delegate;
340
329
  this.method = method;
341
- this.location = location;
342
- this.body = body;
343
- }
344
- get url() {
345
- const url = this.location.absoluteURL;
346
- const query = this.params.toString();
347
- if (this.isIdempotent && query.length) {
348
- return [ url, query ].join(url.includes("?") ? "&" : "?");
330
+ this.headers = this.defaultHeaders;
331
+ if (this.isIdempotent) {
332
+ this.url = mergeFormDataEntries(location, [ ...body.entries() ]);
349
333
  } else {
350
- return url;
334
+ this.body = body;
335
+ this.url = location;
351
336
  }
352
337
  }
338
+ get location() {
339
+ return this.url;
340
+ }
353
341
  get params() {
354
- return this.entries.reduce(((params, [name, value]) => {
355
- params.append(name, value.toString());
356
- return params;
357
- }), new URLSearchParams);
342
+ return this.url.searchParams;
358
343
  }
359
344
  get entries() {
360
345
  return this.body ? Array.from(this.body.entries()) : [];
@@ -363,7 +348,9 @@ class FetchRequest {
363
348
  this.abortController.abort();
364
349
  }
365
350
  async perform() {
351
+ var _a, _b;
366
352
  const {fetchOptions: fetchOptions} = this;
353
+ (_b = (_a = this.delegate).prepareHeadersForRequest) === null || _b === void 0 ? void 0 : _b.call(_a, this.headers, this);
367
354
  dispatch("turbo:before-fetch-request", {
368
355
  detail: {
369
356
  fetchOptions: fetchOptions
@@ -371,7 +358,7 @@ class FetchRequest {
371
358
  });
372
359
  try {
373
360
  this.delegate.requestStarted(this);
374
- const response = await fetch(this.url, fetchOptions);
361
+ const response = await fetch(this.url.href, fetchOptions);
375
362
  return await this.receive(response);
376
363
  } catch (error) {
377
364
  this.delegate.requestErrored(this, error);
@@ -403,28 +390,35 @@ class FetchRequest {
403
390
  credentials: "same-origin",
404
391
  headers: this.headers,
405
392
  redirect: "follow",
406
- body: this.isIdempotent ? undefined : this.body,
393
+ body: this.body,
407
394
  signal: this.abortSignal
408
395
  };
409
396
  }
397
+ get defaultHeaders() {
398
+ return {
399
+ Accept: "text/html, application/xhtml+xml"
400
+ };
401
+ }
410
402
  get isIdempotent() {
411
403
  return this.method == FetchMethod.get;
412
404
  }
413
- get headers() {
414
- return Object.assign({
415
- Accept: "text/html, application/xhtml+xml"
416
- }, this.additionalHeaders);
405
+ get abortSignal() {
406
+ return this.abortController.signal;
417
407
  }
418
- get additionalHeaders() {
419
- if (typeof this.delegate.additionalHeadersForRequest == "function") {
420
- return this.delegate.additionalHeadersForRequest(this);
408
+ }
409
+
410
+ function mergeFormDataEntries(url, entries) {
411
+ const currentSearchParams = new URLSearchParams(url.search);
412
+ for (const [name, value] of entries) {
413
+ if (value instanceof File) continue;
414
+ if (currentSearchParams.has(name)) {
415
+ currentSearchParams.delete(name);
416
+ url.searchParams.set(name, value);
421
417
  } else {
422
- return {};
418
+ url.searchParams.append(name, value);
423
419
  }
424
420
  }
425
- get abortSignal() {
426
- return this.abortController.signal;
427
- }
421
+ return url;
428
422
  }
429
423
 
430
424
  class AppearanceObserver {
@@ -454,6 +448,41 @@ class AppearanceObserver {
454
448
  }
455
449
  }
456
450
 
451
+ class StreamMessage {
452
+ constructor(html) {
453
+ this.templateElement = document.createElement("template");
454
+ this.templateElement.innerHTML = html;
455
+ }
456
+ static wrap(message) {
457
+ if (typeof message == "string") {
458
+ return new this(message);
459
+ } else {
460
+ return message;
461
+ }
462
+ }
463
+ get fragment() {
464
+ const fragment = document.createDocumentFragment();
465
+ for (const element of this.foreignElements) {
466
+ fragment.appendChild(document.importNode(element, true));
467
+ }
468
+ return fragment;
469
+ }
470
+ get foreignElements() {
471
+ return this.templateChildren.reduce(((streamElements, child) => {
472
+ if (child.tagName.toLowerCase() == "turbo-stream") {
473
+ return [ ...streamElements, child ];
474
+ } else {
475
+ return streamElements;
476
+ }
477
+ }), []);
478
+ }
479
+ get templateChildren() {
480
+ return Array.from(this.templateElement.content.children);
481
+ }
482
+ }
483
+
484
+ StreamMessage.contentType = "text/vnd.turbo-stream.html";
485
+
457
486
  var FormSubmissionState;
458
487
 
459
488
  (function(FormSubmissionState) {
@@ -465,14 +494,35 @@ var FormSubmissionState;
465
494
  FormSubmissionState[FormSubmissionState["stopped"] = 5] = "stopped";
466
495
  })(FormSubmissionState || (FormSubmissionState = {}));
467
496
 
497
+ var FormEnctype;
498
+
499
+ (function(FormEnctype) {
500
+ FormEnctype["urlEncoded"] = "application/x-www-form-urlencoded";
501
+ FormEnctype["multipart"] = "multipart/form-data";
502
+ FormEnctype["plain"] = "text/plain";
503
+ })(FormEnctype || (FormEnctype = {}));
504
+
505
+ function formEnctypeFromString(encoding) {
506
+ switch (encoding.toLowerCase()) {
507
+ case FormEnctype.multipart:
508
+ return FormEnctype.multipart;
509
+
510
+ case FormEnctype.plain:
511
+ return FormEnctype.plain;
512
+
513
+ default:
514
+ return FormEnctype.urlEncoded;
515
+ }
516
+ }
517
+
468
518
  class FormSubmission {
469
519
  constructor(delegate, formElement, submitter, mustRedirect = false) {
470
520
  this.state = FormSubmissionState.initialized;
471
521
  this.delegate = delegate;
472
522
  this.formElement = formElement;
473
- this.formData = buildFormData(formElement, submitter);
474
523
  this.submitter = submitter;
475
- this.fetchRequest = new FetchRequest(this, this.method, this.location, this.formData);
524
+ this.formData = buildFormData(formElement, submitter);
525
+ this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body);
476
526
  this.mustRedirect = mustRedirect;
477
527
  }
478
528
  get method() {
@@ -485,7 +535,24 @@ class FormSubmission {
485
535
  return ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formaction")) || this.formElement.action;
486
536
  }
487
537
  get location() {
488
- return Location.wrap(this.action);
538
+ return expandURL(this.action);
539
+ }
540
+ get body() {
541
+ if (this.enctype == FormEnctype.urlEncoded || this.method == FetchMethod.get) {
542
+ return new URLSearchParams(this.stringFormData);
543
+ } else {
544
+ return this.formData;
545
+ }
546
+ }
547
+ get enctype() {
548
+ var _a;
549
+ return formEnctypeFromString(((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formenctype")) || this.formElement.enctype);
550
+ }
551
+ get isIdempotent() {
552
+ return this.fetchRequest.isIdempotent;
553
+ }
554
+ get stringFormData() {
555
+ return [ ...this.formData ].reduce(((entries, [name, value]) => entries.concat(typeof value == "string" ? [ [ name, value ] ] : [])), []);
489
556
  }
490
557
  async start() {
491
558
  const {initialized: initialized, requesting: requesting} = FormSubmissionState;
@@ -502,15 +569,14 @@ class FormSubmission {
502
569
  return true;
503
570
  }
504
571
  }
505
- additionalHeadersForRequest(request) {
506
- const headers = {};
507
- if (this.method != FetchMethod.get) {
572
+ prepareHeadersForRequest(headers, request) {
573
+ if (!request.isIdempotent) {
508
574
  const token = getCookieValue(getMetaContent("csrf-param")) || getMetaContent("csrf-token");
509
575
  if (token) {
510
576
  headers["X-CSRF-Token"] = token;
511
577
  }
578
+ headers["Accept"] = [ StreamMessage.contentType, headers["Accept"] ].join(", ");
512
579
  }
513
- return headers;
514
580
  }
515
581
  requestStarted(request) {
516
582
  this.state = FormSubmissionState.waiting;
@@ -576,8 +642,8 @@ function buildFormData(formElement, submitter) {
576
642
  const formData = new FormData(formElement);
577
643
  const name = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("name");
578
644
  const value = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("value");
579
- if (name && formData.get(name) != value) {
580
- formData.append(name, value || "");
645
+ if (name && value != null && formData.get(name) != value) {
646
+ formData.append(name, value);
581
647
  }
582
648
  return formData;
583
649
  }
@@ -602,6 +668,48 @@ function responseSucceededWithoutRedirect(response) {
602
668
  return response.statusCode == 200 && !response.redirected;
603
669
  }
604
670
 
671
+ class Snapshot {
672
+ constructor(element) {
673
+ this.element = element;
674
+ }
675
+ get children() {
676
+ return [ ...this.element.children ];
677
+ }
678
+ hasAnchor(anchor) {
679
+ return this.getElementForAnchor(anchor) != null;
680
+ }
681
+ getElementForAnchor(anchor) {
682
+ try {
683
+ return this.element.querySelector(`[id='${anchor}'], a[name='${anchor}']`);
684
+ } catch (_a) {
685
+ return null;
686
+ }
687
+ }
688
+ get isConnected() {
689
+ return this.element.isConnected;
690
+ }
691
+ get firstAutofocusableElement() {
692
+ return this.element.querySelector("[autofocus]");
693
+ }
694
+ get permanentElements() {
695
+ return [ ...this.element.querySelectorAll("[id][data-turbo-permanent]") ];
696
+ }
697
+ getPermanentElementById(id) {
698
+ return this.element.querySelector(`#${id}[data-turbo-permanent]`);
699
+ }
700
+ getPermanentElementMapForSnapshot(snapshot) {
701
+ const permanentElementMap = {};
702
+ for (const currentPermanentElement of this.permanentElements) {
703
+ const {id: id} = currentPermanentElement;
704
+ const newPermanentElement = snapshot.getPermanentElementById(id);
705
+ if (newPermanentElement) {
706
+ permanentElementMap[id] = [ currentPermanentElement, newPermanentElement ];
707
+ }
708
+ }
709
+ return permanentElementMap;
710
+ }
711
+ }
712
+
605
713
  class FormInterceptor {
606
714
  constructor(delegate, element) {
607
715
  this.submitBubbled = event => {
@@ -626,6 +734,82 @@ class FormInterceptor {
626
734
  }
627
735
  }
628
736
 
737
+ class View {
738
+ constructor(delegate, element) {
739
+ this.delegate = delegate;
740
+ this.element = element;
741
+ }
742
+ scrollToAnchor(anchor) {
743
+ const element = this.snapshot.getElementForAnchor(anchor);
744
+ if (element) {
745
+ this.scrollToElement(element);
746
+ } else {
747
+ this.scrollToPosition({
748
+ x: 0,
749
+ y: 0
750
+ });
751
+ }
752
+ }
753
+ scrollToElement(element) {
754
+ element.scrollIntoView();
755
+ }
756
+ scrollToPosition({x: x, y: y}) {
757
+ this.scrollRoot.scrollTo(x, y);
758
+ }
759
+ get scrollRoot() {
760
+ return window;
761
+ }
762
+ async render(renderer) {
763
+ if (this.renderer) {
764
+ throw new Error("rendering is already in progress");
765
+ }
766
+ const {isPreview: isPreview, shouldRender: shouldRender, newSnapshot: snapshot} = renderer;
767
+ if (shouldRender) {
768
+ try {
769
+ this.renderer = renderer;
770
+ this.prepareToRenderSnapshot(renderer);
771
+ this.delegate.viewWillRenderSnapshot(snapshot, isPreview);
772
+ await this.renderSnapshot(renderer);
773
+ this.delegate.viewRenderedSnapshot(snapshot, isPreview);
774
+ this.finishRenderingSnapshot(renderer);
775
+ } finally {
776
+ delete this.renderer;
777
+ }
778
+ } else {
779
+ this.invalidate();
780
+ }
781
+ }
782
+ invalidate() {
783
+ this.delegate.viewInvalidated();
784
+ }
785
+ prepareToRenderSnapshot(renderer) {
786
+ this.markAsPreview(renderer.isPreview);
787
+ renderer.prepareToRender();
788
+ }
789
+ markAsPreview(isPreview) {
790
+ if (isPreview) {
791
+ this.element.setAttribute("data-turbo-preview", "");
792
+ } else {
793
+ this.element.removeAttribute("data-turbo-preview");
794
+ }
795
+ }
796
+ async renderSnapshot(renderer) {
797
+ await renderer.render();
798
+ }
799
+ finishRenderingSnapshot(renderer) {
800
+ renderer.finishRendering();
801
+ }
802
+ }
803
+
804
+ class FrameView extends View {
805
+ invalidate() {
806
+ this.element.innerHTML = "";
807
+ }
808
+ get snapshot() {
809
+ return new Snapshot(this.element);
810
+ }
811
+ }
812
+
629
813
  class LinkInterceptor {
630
814
  constructor(delegate, element) {
631
815
  this.clickBubbled = event => {
@@ -640,7 +824,7 @@ class LinkInterceptor {
640
824
  if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url)) {
641
825
  this.clickEvent.preventDefault();
642
826
  event.preventDefault();
643
- this.delegate.linkClickIntercepted(event.target, event.detail.url);
827
+ this.convertLinkWithMethodClickToFormSubmission(event.target) || this.delegate.linkClickIntercepted(event.target, event.detail.url);
644
828
  }
645
829
  }
646
830
  delete this.clickEvent;
@@ -661,183 +845,181 @@ class LinkInterceptor {
661
845
  document.removeEventListener("turbo:click", this.linkClicked);
662
846
  document.removeEventListener("turbo:before-visit", this.willVisit);
663
847
  }
848
+ convertLinkWithMethodClickToFormSubmission(link) {
849
+ var _a;
850
+ const linkMethod = link.getAttribute("data-turbo-method") || link.getAttribute("data-method");
851
+ if (linkMethod) {
852
+ const form = document.createElement("form");
853
+ form.method = linkMethod;
854
+ form.action = link.getAttribute("href") || "undefined";
855
+ (_a = link.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(form, link);
856
+ return dispatch("submit", {
857
+ target: form
858
+ });
859
+ } else {
860
+ return false;
861
+ }
862
+ }
664
863
  respondsToEventTarget(target) {
665
864
  const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
666
865
  return element && element.closest("turbo-frame, html") == this.element;
667
866
  }
668
867
  }
669
868
 
670
- class FrameController {
671
- constructor(element) {
672
- this.resolveVisitPromise = () => {};
673
- this.element = element;
674
- this.appearanceObserver = new AppearanceObserver(this, this.element);
675
- this.linkInterceptor = new LinkInterceptor(this, this.element);
676
- this.formInterceptor = new FormInterceptor(this, this.element);
677
- }
678
- connect() {
679
- if (this.loadingStyle == FrameLoadingStyle.lazy) {
680
- this.appearanceObserver.start();
681
- }
682
- this.linkInterceptor.start();
683
- this.formInterceptor.start();
869
+ class Bardo {
870
+ constructor(permanentElementMap) {
871
+ this.permanentElementMap = permanentElementMap;
684
872
  }
685
- disconnect() {
686
- this.appearanceObserver.stop();
687
- this.linkInterceptor.stop();
688
- this.formInterceptor.stop();
873
+ static preservingPermanentElements(permanentElementMap, callback) {
874
+ const bardo = new this(permanentElementMap);
875
+ bardo.enter();
876
+ callback();
877
+ bardo.leave();
689
878
  }
690
- sourceURLChanged() {
691
- if (this.loadingStyle == FrameLoadingStyle.eager) {
692
- this.loadSourceURL();
879
+ enter() {
880
+ for (const id in this.permanentElementMap) {
881
+ const [, newPermanentElement] = this.permanentElementMap[id];
882
+ this.replaceNewPermanentElementWithPlaceholder(newPermanentElement);
693
883
  }
694
884
  }
695
- loadingStyleChanged() {
696
- if (this.loadingStyle == FrameLoadingStyle.lazy) {
697
- this.appearanceObserver.start();
698
- } else {
699
- this.appearanceObserver.stop();
700
- this.loadSourceURL();
885
+ leave() {
886
+ for (const id in this.permanentElementMap) {
887
+ const [currentPermanentElement] = this.permanentElementMap[id];
888
+ this.replaceCurrentPermanentElementWithClone(currentPermanentElement);
889
+ this.replacePlaceholderWithPermanentElement(currentPermanentElement);
701
890
  }
702
891
  }
703
- async loadSourceURL() {
704
- if (this.isActive && this.sourceURL && this.sourceURL != this.loadingURL) {
705
- try {
706
- this.loadingURL = this.sourceURL;
707
- this.element.loaded = this.visit(this.sourceURL);
708
- this.appearanceObserver.stop();
709
- await this.element.loaded;
710
- } finally {
711
- delete this.loadingURL;
712
- }
713
- }
892
+ replaceNewPermanentElementWithPlaceholder(permanentElement) {
893
+ const placeholder = createPlaceholderForPermanentElement(permanentElement);
894
+ permanentElement.replaceWith(placeholder);
714
895
  }
715
- async loadResponse(response) {
716
- const fragment = fragmentFromHTML(await response.responseHTML);
717
- if (fragment) {
718
- const element = await this.extractForeignFrameElement(fragment);
719
- await nextAnimationFrame();
720
- this.loadFrameElement(element);
721
- this.scrollFrameIntoView(element);
722
- await nextAnimationFrame();
723
- this.focusFirstAutofocusableElement();
724
- }
896
+ replaceCurrentPermanentElementWithClone(permanentElement) {
897
+ const clone = permanentElement.cloneNode(true);
898
+ permanentElement.replaceWith(clone);
725
899
  }
726
- elementAppearedInViewport(element) {
727
- this.loadSourceURL();
900
+ replacePlaceholderWithPermanentElement(permanentElement) {
901
+ const placeholder = this.getPlaceholderById(permanentElement.id);
902
+ placeholder === null || placeholder === void 0 ? void 0 : placeholder.replaceWith(permanentElement);
728
903
  }
729
- shouldInterceptLinkClick(element, url) {
730
- return this.shouldInterceptNavigation(element);
904
+ getPlaceholderById(id) {
905
+ return this.placeholders.find((element => element.content == id));
731
906
  }
732
- linkClickIntercepted(element, url) {
733
- this.navigateFrame(element, url);
907
+ get placeholders() {
908
+ return [ ...document.querySelectorAll("meta[name=turbo-permanent-placeholder][content]") ];
734
909
  }
735
- shouldInterceptFormSubmission(element) {
736
- return this.shouldInterceptNavigation(element);
910
+ }
911
+
912
+ function createPlaceholderForPermanentElement(permanentElement) {
913
+ const element = document.createElement("meta");
914
+ element.setAttribute("name", "turbo-permanent-placeholder");
915
+ element.setAttribute("content", permanentElement.id);
916
+ return element;
917
+ }
918
+
919
+ class Renderer {
920
+ constructor(currentSnapshot, newSnapshot, isPreview) {
921
+ this.currentSnapshot = currentSnapshot;
922
+ this.newSnapshot = newSnapshot;
923
+ this.isPreview = isPreview;
924
+ this.promise = new Promise(((resolve, reject) => this.resolvingFunctions = {
925
+ resolve: resolve,
926
+ reject: reject
927
+ }));
737
928
  }
738
- formSubmissionIntercepted(element, submitter) {
739
- if (this.formSubmission) {
740
- this.formSubmission.stop();
929
+ get shouldRender() {
930
+ return true;
931
+ }
932
+ prepareToRender() {
933
+ return;
934
+ }
935
+ finishRendering() {
936
+ if (this.resolvingFunctions) {
937
+ this.resolvingFunctions.resolve();
938
+ delete this.resolvingFunctions;
741
939
  }
742
- this.formSubmission = new FormSubmission(this, element, submitter);
743
- if (this.formSubmission.fetchRequest.isIdempotent) {
744
- this.navigateFrame(element, this.formSubmission.fetchRequest.url);
940
+ }
941
+ createScriptElement(element) {
942
+ if (element.getAttribute("data-turbo-eval") == "false") {
943
+ return element;
745
944
  } else {
746
- this.formSubmission.start();
945
+ const createdScriptElement = document.createElement("script");
946
+ if (this.cspNonce) {
947
+ createdScriptElement.nonce = this.cspNonce;
948
+ }
949
+ createdScriptElement.textContent = element.textContent;
950
+ createdScriptElement.async = false;
951
+ copyElementAttributes(createdScriptElement, element);
952
+ return createdScriptElement;
747
953
  }
748
954
  }
749
- additionalHeadersForRequest(request) {
750
- return {
751
- "Turbo-Frame": this.id
752
- };
955
+ preservingPermanentElements(callback) {
956
+ Bardo.preservingPermanentElements(this.permanentElementMap, callback);
753
957
  }
754
- requestStarted(request) {
755
- this.element.setAttribute("busy", "");
958
+ focusFirstAutofocusableElement() {
959
+ const element = this.connectedSnapshot.firstAutofocusableElement;
960
+ if (elementIsFocusable(element)) {
961
+ element.focus();
962
+ }
756
963
  }
757
- requestPreventedHandlingResponse(request, response) {
758
- this.resolveVisitPromise();
964
+ get connectedSnapshot() {
965
+ return this.newSnapshot.isConnected ? this.newSnapshot : this.currentSnapshot;
759
966
  }
760
- async requestSucceededWithResponse(request, response) {
761
- await this.loadResponse(response);
762
- this.resolveVisitPromise();
967
+ get currentElement() {
968
+ return this.currentSnapshot.element;
763
969
  }
764
- requestFailedWithResponse(request, response) {
765
- console.error(response);
766
- this.resolveVisitPromise();
970
+ get newElement() {
971
+ return this.newSnapshot.element;
767
972
  }
768
- requestErrored(request, error) {
769
- console.error(error);
770
- this.resolveVisitPromise();
973
+ get permanentElementMap() {
974
+ return this.currentSnapshot.getPermanentElementMapForSnapshot(this.newSnapshot);
771
975
  }
772
- requestFinished(request) {
773
- this.element.removeAttribute("busy");
976
+ get cspNonce() {
977
+ var _a;
978
+ return (_a = document.head.querySelector('meta[name="csp-nonce"]')) === null || _a === void 0 ? void 0 : _a.getAttribute("content");
774
979
  }
775
- formSubmissionStarted(formSubmission) {}
776
- formSubmissionSucceededWithResponse(formSubmission, response) {
777
- const frame = this.findFrameElement(formSubmission.formElement);
778
- frame.delegate.loadResponse(response);
980
+ }
981
+
982
+ function copyElementAttributes(destinationElement, sourceElement) {
983
+ for (const {name: name, value: value} of [ ...sourceElement.attributes ]) {
984
+ destinationElement.setAttribute(name, value);
779
985
  }
780
- formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
781
- this.element.delegate.loadResponse(fetchResponse);
986
+ }
987
+
988
+ function elementIsFocusable(element) {
989
+ return element && typeof element.focus == "function";
990
+ }
991
+
992
+ class FrameRenderer extends Renderer {
993
+ get shouldRender() {
994
+ return true;
782
995
  }
783
- formSubmissionErrored(formSubmission, error) {}
784
- formSubmissionFinished(formSubmission) {}
785
- async visit(url) {
786
- const location = Location.wrap(url);
787
- const request = new FetchRequest(this, FetchMethod.get, location);
788
- return new Promise((resolve => {
789
- this.resolveVisitPromise = () => {
790
- this.resolveVisitPromise = () => {};
791
- resolve();
792
- };
793
- request.perform();
996
+ async render() {
997
+ await nextAnimationFrame();
998
+ this.preservingPermanentElements((() => {
999
+ this.loadFrameElement();
794
1000
  }));
1001
+ this.scrollFrameIntoView();
1002
+ await nextAnimationFrame();
1003
+ this.focusFirstAutofocusableElement();
1004
+ await nextAnimationFrame();
1005
+ this.activateScriptElements();
795
1006
  }
796
- navigateFrame(element, url) {
797
- const frame = this.findFrameElement(element);
798
- frame.src = url;
799
- }
800
- findFrameElement(element) {
801
- var _a;
802
- const id = element.getAttribute("data-turbo-frame");
803
- return (_a = getFrameElementById(id)) !== null && _a !== void 0 ? _a : this.element;
804
- }
805
- async extractForeignFrameElement(container) {
806
- let element;
807
- const id = CSS.escape(this.id);
808
- if (element = activateElement(container.querySelector(`turbo-frame#${id}`))) {
809
- return element;
810
- }
811
- if (element = activateElement(container.querySelector(`turbo-frame[src][recurse~=${id}]`))) {
812
- await element.loaded;
813
- return await this.extractForeignFrameElement(element);
814
- }
815
- console.error(`Response has no matching <turbo-frame id="${id}"> element`);
816
- return new FrameElement;
817
- }
818
- loadFrameElement(frameElement) {
1007
+ loadFrameElement() {
819
1008
  var _a;
820
1009
  const destinationRange = document.createRange();
821
- destinationRange.selectNodeContents(this.element);
1010
+ destinationRange.selectNodeContents(this.currentElement);
822
1011
  destinationRange.deleteContents();
1012
+ const frameElement = this.newElement;
823
1013
  const sourceRange = (_a = frameElement.ownerDocument) === null || _a === void 0 ? void 0 : _a.createRange();
824
1014
  if (sourceRange) {
825
1015
  sourceRange.selectNodeContents(frameElement);
826
- this.element.appendChild(sourceRange.extractContents());
1016
+ this.currentElement.appendChild(sourceRange.extractContents());
827
1017
  }
828
1018
  }
829
- focusFirstAutofocusableElement() {
830
- const element = this.firstAutofocusableElement;
831
- if (element) {
832
- element.focus();
833
- return true;
834
- }
835
- return false;
836
- }
837
- scrollFrameIntoView(frame) {
838
- if (this.element.autoscroll || frame.autoscroll) {
839
- const element = this.element.firstElementChild;
840
- const block = readScrollLogicalPosition(this.element.getAttribute("data-autoscroll-block"), "end");
1019
+ scrollFrameIntoView() {
1020
+ if (this.currentElement.autoscroll || this.newElement.autoscroll) {
1021
+ const element = this.currentElement.firstElementChild;
1022
+ const block = readScrollLogicalPosition(this.currentElement.getAttribute("data-autoscroll-block"), "end");
841
1023
  if (element) {
842
1024
  element.scrollIntoView({
843
1025
  block: block
@@ -847,49 +1029,14 @@ class FrameController {
847
1029
  }
848
1030
  return false;
849
1031
  }
850
- shouldInterceptNavigation(element) {
851
- const id = element.getAttribute("data-turbo-frame") || this.element.getAttribute("target");
852
- if (!this.enabled || id == "_top") {
853
- return false;
854
- }
855
- if (id) {
856
- const frameElement = getFrameElementById(id);
857
- if (frameElement) {
858
- return !frameElement.disabled;
859
- }
1032
+ activateScriptElements() {
1033
+ for (const inertScriptElement of this.newScriptElements) {
1034
+ const activatedScriptElement = this.createScriptElement(inertScriptElement);
1035
+ inertScriptElement.replaceWith(activatedScriptElement);
860
1036
  }
861
- return true;
862
- }
863
- get firstAutofocusableElement() {
864
- const element = this.element.querySelector("[autofocus]");
865
- return element instanceof HTMLElement ? element : null;
866
- }
867
- get id() {
868
- return this.element.id;
869
- }
870
- get enabled() {
871
- return !this.element.disabled;
872
- }
873
- get sourceURL() {
874
- return this.element.src;
875
- }
876
- get loadingStyle() {
877
- return this.element.loading;
878
- }
879
- get isLoading() {
880
- return this.formSubmission !== undefined || this.loadingURL !== undefined;
881
- }
882
- get isActive() {
883
- return this.element.isActive;
884
1037
  }
885
- }
886
-
887
- function getFrameElementById(id) {
888
- if (id != null) {
889
- const element = document.getElementById(id);
890
- if (element instanceof FrameElement) {
891
- return element;
892
- }
1038
+ get newScriptElements() {
1039
+ return this.currentElement.querySelectorAll("script");
893
1040
  }
894
1041
  }
895
1042
 
@@ -901,144 +1048,6 @@ function readScrollLogicalPosition(value, defaultValue) {
901
1048
  }
902
1049
  }
903
1050
 
904
- function fragmentFromHTML(html) {
905
- if (html) {
906
- const foreignDocument = document.implementation.createHTMLDocument();
907
- return foreignDocument.createRange().createContextualFragment(html);
908
- }
909
- }
910
-
911
- function activateElement(element) {
912
- if (element && element.ownerDocument !== document) {
913
- element = document.importNode(element, true);
914
- }
915
- if (element instanceof FrameElement) {
916
- return element;
917
- }
918
- }
919
-
920
- const StreamActions = {
921
- append() {
922
- var _a;
923
- (_a = this.targetElement) === null || _a === void 0 ? void 0 : _a.append(this.templateContent);
924
- },
925
- prepend() {
926
- var _a;
927
- (_a = this.targetElement) === null || _a === void 0 ? void 0 : _a.prepend(this.templateContent);
928
- },
929
- remove() {
930
- var _a;
931
- (_a = this.targetElement) === null || _a === void 0 ? void 0 : _a.remove();
932
- },
933
- replace() {
934
- var _a;
935
- (_a = this.targetElement) === null || _a === void 0 ? void 0 : _a.replaceWith(this.templateContent);
936
- },
937
- update() {
938
- if (this.targetElement) {
939
- this.targetElement.innerHTML = "";
940
- this.targetElement.append(this.templateContent);
941
- }
942
- }
943
- };
944
-
945
- class StreamElement extends HTMLElement {
946
- async connectedCallback() {
947
- try {
948
- await this.render();
949
- } catch (error) {
950
- console.error(error);
951
- } finally {
952
- this.disconnect();
953
- }
954
- }
955
- async render() {
956
- var _a;
957
- return (_a = this.renderPromise) !== null && _a !== void 0 ? _a : this.renderPromise = (async () => {
958
- if (this.dispatchEvent(this.beforeRenderEvent)) {
959
- await nextAnimationFrame();
960
- this.performAction();
961
- }
962
- })();
963
- }
964
- disconnect() {
965
- try {
966
- this.remove();
967
- } catch (_a) {}
968
- }
969
- get performAction() {
970
- if (this.action) {
971
- const actionFunction = StreamActions[this.action];
972
- if (actionFunction) {
973
- return actionFunction;
974
- }
975
- this.raise("unknown action");
976
- }
977
- this.raise("action attribute is missing");
978
- }
979
- get targetElement() {
980
- var _a;
981
- if (this.target) {
982
- return (_a = this.ownerDocument) === null || _a === void 0 ? void 0 : _a.getElementById(this.target);
983
- }
984
- this.raise("target attribute is missing");
985
- }
986
- get templateContent() {
987
- return this.templateElement.content;
988
- }
989
- get templateElement() {
990
- if (this.firstElementChild instanceof HTMLTemplateElement) {
991
- return this.firstElementChild;
992
- }
993
- this.raise("first child element must be a <template> element");
994
- }
995
- get action() {
996
- return this.getAttribute("action");
997
- }
998
- get target() {
999
- return this.getAttribute("target");
1000
- }
1001
- raise(message) {
1002
- throw new Error(`${this.description}: ${message}`);
1003
- }
1004
- get description() {
1005
- var _a, _b;
1006
- return (_b = ((_a = this.outerHTML.match(/<[^>]+>/)) !== null && _a !== void 0 ? _a : [])[0]) !== null && _b !== void 0 ? _b : "<turbo-stream>";
1007
- }
1008
- get beforeRenderEvent() {
1009
- return new CustomEvent("turbo:before-stream-render", {
1010
- bubbles: true,
1011
- cancelable: true
1012
- });
1013
- }
1014
- }
1015
-
1016
- FrameElement.delegateConstructor = FrameController;
1017
-
1018
- customElements.define("turbo-frame", FrameElement);
1019
-
1020
- customElements.define("turbo-stream", StreamElement);
1021
-
1022
- (() => {
1023
- let element = document.currentScript;
1024
- if (!element) return;
1025
- if (element.hasAttribute("data-turbo-suppress-warning")) return;
1026
- while (element = element.parentElement) {
1027
- if (element == document.body) {
1028
- return console.warn(unindent`
1029
- You are loading Turbo from a <script> element inside the <body> element. This is probably not what you meant to do!
1030
-
1031
- Load your application’s JavaScript bundle inside the <head> element instead. <script> elements in <body> are evaluated with each page change.
1032
-
1033
- For more information, see: https://turbo.hotwire.dev/handbook/building#working-with-script-elements
1034
-
1035
- ——
1036
- Suppress this warning by adding a "data-turbo-suppress-warning" attribute to: %s
1037
- `, element.outerHTML);
1038
- }
1039
- }
1040
- })();
1041
-
1042
1051
  class ProgressBar {
1043
1052
  constructor() {
1044
1053
  this.hiding = false;
@@ -1138,9 +1147,10 @@ class ProgressBar {
1138
1147
 
1139
1148
  ProgressBar.animationDuration = 300;
1140
1149
 
1141
- class HeadDetails {
1142
- constructor(children) {
1143
- this.detailsByOuterHTML = children.reduce(((result, element) => {
1150
+ class HeadSnapshot extends Snapshot {
1151
+ constructor() {
1152
+ super(...arguments);
1153
+ this.detailsByOuterHTML = this.children.reduce(((result, element) => {
1144
1154
  const {outerHTML: outerHTML} = element;
1145
1155
  const details = outerHTML in result ? result[outerHTML] : {
1146
1156
  type: elementType(element),
@@ -1154,23 +1164,19 @@ class HeadDetails {
1154
1164
  });
1155
1165
  }), {});
1156
1166
  }
1157
- static fromHeadElement(headElement) {
1158
- const children = headElement ? [ ...headElement.children ] : [];
1159
- return new this(children);
1160
- }
1161
- getTrackedElementSignature() {
1167
+ get trackedElementSignature() {
1162
1168
  return Object.keys(this.detailsByOuterHTML).filter((outerHTML => this.detailsByOuterHTML[outerHTML].tracked)).join("");
1163
1169
  }
1164
- getScriptElementsNotInDetails(headDetails) {
1165
- return this.getElementsMatchingTypeNotInDetails("script", headDetails);
1170
+ getScriptElementsNotInSnapshot(snapshot) {
1171
+ return this.getElementsMatchingTypeNotInSnapshot("script", snapshot);
1166
1172
  }
1167
- getStylesheetElementsNotInDetails(headDetails) {
1168
- return this.getElementsMatchingTypeNotInDetails("stylesheet", headDetails);
1173
+ getStylesheetElementsNotInSnapshot(snapshot) {
1174
+ return this.getElementsMatchingTypeNotInSnapshot("stylesheet", snapshot);
1169
1175
  }
1170
- getElementsMatchingTypeNotInDetails(matchedType, headDetails) {
1171
- return Object.keys(this.detailsByOuterHTML).filter((outerHTML => !(outerHTML in headDetails.detailsByOuterHTML))).map((outerHTML => this.detailsByOuterHTML[outerHTML])).filter((({type: type}) => type == matchedType)).map((({elements: [element]}) => element));
1176
+ getElementsMatchingTypeNotInSnapshot(matchedType, snapshot) {
1177
+ return Object.keys(this.detailsByOuterHTML).filter((outerHTML => !(outerHTML in snapshot.detailsByOuterHTML))).map((outerHTML => this.detailsByOuterHTML[outerHTML])).filter((({type: type}) => type == matchedType)).map((({elements: [element]}) => element));
1172
1178
  }
1173
- getProvisionalElements() {
1179
+ get provisionalElements() {
1174
1180
  return Object.keys(this.detailsByOuterHTML).reduce(((result, outerHTML) => {
1175
1181
  const {type: type, tracked: tracked, elements: elements} = this.detailsByOuterHTML[outerHTML];
1176
1182
  if (type == null && !tracked) {
@@ -1221,75 +1227,45 @@ function elementIsMetaElementWithName(element, name) {
1221
1227
  return tagName == "meta" && element.getAttribute("name") == name;
1222
1228
  }
1223
1229
 
1224
- class Snapshot {
1225
- constructor(headDetails, bodyElement) {
1226
- this.headDetails = headDetails;
1227
- this.bodyElement = bodyElement;
1228
- }
1229
- static wrap(value) {
1230
- if (value instanceof this) {
1231
- return value;
1232
- } else if (typeof value == "string") {
1233
- return this.fromHTMLString(value);
1234
- } else {
1235
- return this.fromHTMLElement(value);
1236
- }
1237
- }
1238
- static fromHTMLString(html) {
1239
- const {documentElement: documentElement} = (new DOMParser).parseFromString(html, "text/html");
1240
- return this.fromHTMLElement(documentElement);
1230
+ class PageSnapshot extends Snapshot {
1231
+ constructor(element, headSnapshot) {
1232
+ super(element);
1233
+ this.headSnapshot = headSnapshot;
1241
1234
  }
1242
- static fromHTMLElement(htmlElement) {
1243
- const headElement = htmlElement.querySelector("head");
1244
- const bodyElement = htmlElement.querySelector("body") || document.createElement("body");
1245
- const headDetails = HeadDetails.fromHeadElement(headElement);
1246
- return new this(headDetails, bodyElement);
1235
+ static fromHTMLString(html = "") {
1236
+ return this.fromDocument(parseHTMLDocument(html));
1247
1237
  }
1248
- clone() {
1249
- const {bodyElement: bodyElement} = Snapshot.fromHTMLString(this.bodyElement.outerHTML);
1250
- return new Snapshot(this.headDetails, bodyElement);
1251
- }
1252
- getRootLocation() {
1253
- const root = this.getSetting("root", "/");
1254
- return new Location(root);
1255
- }
1256
- getCacheControlValue() {
1257
- return this.getSetting("cache-control");
1258
- }
1259
- getElementForAnchor(anchor) {
1260
- try {
1261
- return this.bodyElement.querySelector(`[id='${anchor}'], a[name='${anchor}']`);
1262
- } catch (_a) {
1263
- return null;
1264
- }
1238
+ static fromElement(element) {
1239
+ return this.fromDocument(element.ownerDocument);
1265
1240
  }
1266
- getPermanentElements() {
1267
- return [ ...this.bodyElement.querySelectorAll("[id][data-turbo-permanent]") ];
1241
+ static fromDocument({head: head, body: body}) {
1242
+ return new this(body, new HeadSnapshot(head));
1268
1243
  }
1269
- getPermanentElementById(id) {
1270
- return this.bodyElement.querySelector(`#${id}[data-turbo-permanent]`);
1244
+ clone() {
1245
+ return new PageSnapshot(this.element.cloneNode(true), this.headSnapshot);
1271
1246
  }
1272
- getPermanentElementsPresentInSnapshot(snapshot) {
1273
- return this.getPermanentElements().filter((({id: id}) => snapshot.getPermanentElementById(id)));
1247
+ get headElement() {
1248
+ return this.headSnapshot.element;
1274
1249
  }
1275
- findFirstAutofocusableElement() {
1276
- return this.bodyElement.querySelector("[autofocus]");
1250
+ get rootLocation() {
1251
+ var _a;
1252
+ const root = (_a = this.getSetting("root")) !== null && _a !== void 0 ? _a : "/";
1253
+ return expandURL(root);
1277
1254
  }
1278
- hasAnchor(anchor) {
1279
- return this.getElementForAnchor(anchor) != null;
1255
+ get cacheControlValue() {
1256
+ return this.getSetting("cache-control");
1280
1257
  }
1281
- isPreviewable() {
1282
- return this.getCacheControlValue() != "no-preview";
1258
+ get isPreviewable() {
1259
+ return this.cacheControlValue != "no-preview";
1283
1260
  }
1284
- isCacheable() {
1285
- return this.getCacheControlValue() != "no-cache";
1261
+ get isCacheable() {
1262
+ return this.cacheControlValue != "no-cache";
1286
1263
  }
1287
- isVisitable() {
1264
+ get isVisitable() {
1288
1265
  return this.getSetting("visit-control") != "reload";
1289
1266
  }
1290
- getSetting(name, defaultValue) {
1291
- const value = this.headDetails.getMetaValue(`turbo-${name}`);
1292
- return value == null ? defaultValue : value;
1267
+ getSetting(name) {
1268
+ return this.headSnapshot.getMetaValue(`turbo-${name}`);
1293
1269
  }
1294
1270
  }
1295
1271
 
@@ -1334,16 +1310,6 @@ class Visit {
1334
1310
  this.scrolled = false;
1335
1311
  this.snapshotCached = false;
1336
1312
  this.state = VisitState.initialized;
1337
- this.performScroll = () => {
1338
- if (!this.scrolled) {
1339
- if (this.action == "restore") {
1340
- this.scrollToRestoredPosition() || this.scrollToTop();
1341
- } else {
1342
- this.scrollToAnchor() || this.scrollToTop();
1343
- }
1344
- this.scrolled = true;
1345
- }
1346
- };
1347
1313
  this.delegate = delegate;
1348
1314
  this.location = location;
1349
1315
  this.restorationIdentifier = restorationIdentifier || uuid();
@@ -1398,8 +1364,9 @@ class Visit {
1398
1364
  }
1399
1365
  }
1400
1366
  changeHistory() {
1367
+ var _a;
1401
1368
  if (!this.historyChanged) {
1402
- const actionForHistory = this.location.isEqualTo(this.referrer) ? "replace" : this.action;
1369
+ const actionForHistory = this.location.href === ((_a = this.referrer) === null || _a === void 0 ? void 0 : _a.href) ? "replace" : this.action;
1403
1370
  const method = this.getHistoryMethodForAction(actionForHistory);
1404
1371
  this.history.update(method, this.location, this.restorationIdentifier);
1405
1372
  this.historyChanged = true;
@@ -1442,18 +1409,14 @@ class Visit {
1442
1409
  loadResponse() {
1443
1410
  if (this.response) {
1444
1411
  const {statusCode: statusCode, responseHTML: responseHTML} = this.response;
1445
- this.render((() => {
1412
+ this.render((async () => {
1446
1413
  this.cacheSnapshot();
1447
1414
  if (isSuccessful(statusCode) && responseHTML != null) {
1448
- this.view.render({
1449
- snapshot: Snapshot.fromHTMLString(responseHTML)
1450
- }, this.performScroll);
1415
+ await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML));
1451
1416
  this.adapter.visitRendered(this);
1452
1417
  this.complete();
1453
1418
  } else {
1454
- this.view.render({
1455
- error: responseHTML
1456
- }, this.performScroll);
1419
+ await this.view.renderError(PageSnapshot.fromHTMLString(responseHTML));
1457
1420
  this.adapter.visitRendered(this);
1458
1421
  this.fail();
1459
1422
  }
@@ -1462,15 +1425,15 @@ class Visit {
1462
1425
  }
1463
1426
  getCachedSnapshot() {
1464
1427
  const snapshot = this.view.getCachedSnapshotForLocation(this.location) || this.getPreloadedSnapshot();
1465
- if (snapshot && (!this.location.anchor || snapshot.hasAnchor(this.location.anchor))) {
1466
- if (this.action == "restore" || snapshot.isPreviewable()) {
1428
+ if (snapshot && (!getAnchor(this.location) || snapshot.hasAnchor(getAnchor(this.location)))) {
1429
+ if (this.action == "restore" || snapshot.isPreviewable) {
1467
1430
  return snapshot;
1468
1431
  }
1469
1432
  }
1470
1433
  }
1471
1434
  getPreloadedSnapshot() {
1472
1435
  if (this.snapshotHTML) {
1473
- return Snapshot.wrap(this.snapshotHTML);
1436
+ return PageSnapshot.fromHTMLString(this.snapshotHTML);
1474
1437
  }
1475
1438
  }
1476
1439
  hasCachedSnapshot() {
@@ -1480,12 +1443,9 @@ class Visit {
1480
1443
  const snapshot = this.getCachedSnapshot();
1481
1444
  if (snapshot) {
1482
1445
  const isPreview = this.shouldIssueRequest();
1483
- this.render((() => {
1446
+ this.render((async () => {
1484
1447
  this.cacheSnapshot();
1485
- this.view.render({
1486
- snapshot: snapshot,
1487
- isPreview: isPreview
1488
- }, this.performScroll);
1448
+ await this.view.renderPage(snapshot, isPreview);
1489
1449
  this.adapter.visitRendered(this);
1490
1450
  if (!isPreview) {
1491
1451
  this.complete();
@@ -1539,16 +1499,26 @@ class Visit {
1539
1499
  requestFinished() {
1540
1500
  this.finishRequest();
1541
1501
  }
1542
- scrollToRestoredPosition() {
1543
- const {scrollPosition: scrollPosition} = this.restorationData;
1544
- if (scrollPosition) {
1502
+ performScroll() {
1503
+ if (!this.scrolled) {
1504
+ if (this.action == "restore") {
1505
+ this.scrollToRestoredPosition() || this.scrollToTop();
1506
+ } else {
1507
+ this.scrollToAnchor() || this.scrollToTop();
1508
+ }
1509
+ this.scrolled = true;
1510
+ }
1511
+ }
1512
+ scrollToRestoredPosition() {
1513
+ const {scrollPosition: scrollPosition} = this.restorationData;
1514
+ if (scrollPosition) {
1545
1515
  this.view.scrollToPosition(scrollPosition);
1546
1516
  return true;
1547
1517
  }
1548
1518
  }
1549
1519
  scrollToAnchor() {
1550
- if (this.location.anchor != null) {
1551
- this.view.scrollToAnchor(this.location.anchor);
1520
+ if (getAnchor(this.location)) {
1521
+ this.view.scrollToAnchor(getAnchor(this.location));
1552
1522
  return true;
1553
1523
  }
1554
1524
  }
@@ -1586,12 +1556,14 @@ class Visit {
1586
1556
  this.snapshotCached = true;
1587
1557
  }
1588
1558
  }
1589
- render(callback) {
1559
+ async render(callback) {
1590
1560
  this.cancelRender();
1591
- this.frame = requestAnimationFrame((() => {
1592
- delete this.frame;
1593
- callback.call(this);
1561
+ await new Promise((resolve => {
1562
+ this.frame = requestAnimationFrame((() => resolve()));
1594
1563
  }));
1564
+ callback();
1565
+ delete this.frame;
1566
+ this.performScroll();
1595
1567
  }
1596
1568
  cancelRender() {
1597
1569
  if (this.frame) {
@@ -1673,6 +1645,30 @@ class BrowserAdapter {
1673
1645
  }
1674
1646
  }
1675
1647
 
1648
+ class CacheObserver {
1649
+ constructor() {
1650
+ this.started = false;
1651
+ }
1652
+ start() {
1653
+ if (!this.started) {
1654
+ this.started = true;
1655
+ addEventListener("turbo:before-cache", this.removeStaleElements, false);
1656
+ }
1657
+ }
1658
+ stop() {
1659
+ if (this.started) {
1660
+ this.started = false;
1661
+ removeEventListener("turbo:before-cache", this.removeStaleElements, false);
1662
+ }
1663
+ }
1664
+ removeStaleElements() {
1665
+ const staleElements = [ ...document.querySelectorAll('[data-turbo-cache="false"]') ];
1666
+ for (const element of staleElements) {
1667
+ element.remove();
1668
+ }
1669
+ }
1670
+ }
1671
+
1676
1672
  class FormSubmitObserver {
1677
1673
  constructor(delegate) {
1678
1674
  this.started = false;
@@ -1766,11 +1762,10 @@ class History {
1766
1762
  if (this.shouldHandlePopState()) {
1767
1763
  const {turbo: turbo} = event.state || {};
1768
1764
  if (turbo) {
1769
- const location = Location.currentLocation;
1770
- this.location = location;
1765
+ this.location = new URL(window.location.href);
1771
1766
  const {restorationIdentifier: restorationIdentifier} = turbo;
1772
1767
  this.restorationIdentifier = restorationIdentifier;
1773
- this.delegate.historyPoppedToLocationWithRestorationIdentifier(location, restorationIdentifier);
1768
+ this.delegate.historyPoppedToLocationWithRestorationIdentifier(this.location, restorationIdentifier);
1774
1769
  }
1775
1770
  }
1776
1771
  };
@@ -1785,7 +1780,7 @@ class History {
1785
1780
  addEventListener("popstate", this.onPopState, false);
1786
1781
  addEventListener("load", this.onPageLoad, false);
1787
1782
  this.started = true;
1788
- this.replace(Location.currentLocation);
1783
+ this.replace(new URL(window.location.href));
1789
1784
  }
1790
1785
  }
1791
1786
  stop() {
@@ -1807,7 +1802,7 @@ class History {
1807
1802
  restorationIdentifier: restorationIdentifier
1808
1803
  }
1809
1804
  };
1810
- method.call(history, state, "", location.absoluteURL);
1805
+ method.call(history, state, "", location.href);
1811
1806
  this.location = location;
1812
1807
  this.restorationIdentifier = restorationIdentifier;
1813
1808
  }
@@ -1882,10 +1877,14 @@ class LinkClickObserver {
1882
1877
  }
1883
1878
  }
1884
1879
  getLocationForLink(link) {
1885
- return new Location(link.getAttribute("href") || "");
1880
+ return expandURL(link.getAttribute("href") || "");
1886
1881
  }
1887
1882
  }
1888
1883
 
1884
+ function isAction(action) {
1885
+ return action == "advance" || action == "replace" || action == "restore";
1886
+ }
1887
+
1889
1888
  class Navigator {
1890
1889
  constructor(delegate) {
1891
1890
  this.delegate = delegate;
@@ -1895,9 +1894,9 @@ class Navigator {
1895
1894
  this.delegate.visitProposedToLocation(location, options);
1896
1895
  }
1897
1896
  }
1898
- startVisit(location, restorationIdentifier, options = {}) {
1897
+ startVisit(locatable, restorationIdentifier, options = {}) {
1899
1898
  this.stop();
1900
- this.currentVisit = new Visit(this, Location.wrap(location), restorationIdentifier, Object.assign({
1899
+ this.currentVisit = new Visit(this, expandURL(locatable), restorationIdentifier, Object.assign({
1901
1900
  referrer: this.location
1902
1901
  }, options));
1903
1902
  this.currentVisit.start();
@@ -1905,7 +1904,13 @@ class Navigator {
1905
1904
  submitForm(form, submitter) {
1906
1905
  this.stop();
1907
1906
  this.formSubmission = new FormSubmission(this, form, submitter, true);
1908
- this.formSubmission.start();
1907
+ if (this.formSubmission.isIdempotent) {
1908
+ this.proposeVisit(this.formSubmission.fetchRequest.url, {
1909
+ action: this.getActionForFormSubmission(this.formSubmission)
1910
+ });
1911
+ } else {
1912
+ this.formSubmission.start();
1913
+ }
1909
1914
  }
1910
1915
  stop() {
1911
1916
  if (this.formSubmission) {
@@ -1948,14 +1953,14 @@ class Navigator {
1948
1953
  async formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
1949
1954
  const responseHTML = await fetchResponse.responseHTML;
1950
1955
  if (responseHTML) {
1951
- const snapshot = Snapshot.fromHTMLString(responseHTML);
1952
- this.view.render({
1953
- snapshot: snapshot
1954
- }, (() => {}));
1956
+ const snapshot = PageSnapshot.fromHTMLString(responseHTML);
1957
+ await this.view.renderPage(snapshot);
1955
1958
  this.view.clearSnapshotCache();
1956
1959
  }
1957
1960
  }
1958
- formSubmissionErrored(formSubmission, error) {}
1961
+ formSubmissionErrored(formSubmission, error) {
1962
+ console.error(error);
1963
+ }
1959
1964
  formSubmissionFinished(formSubmission) {}
1960
1965
  visitStarted(visit) {
1961
1966
  this.delegate.visitStarted(visit);
@@ -1969,6 +1974,11 @@ class Navigator {
1969
1974
  get restorationIdentifier() {
1970
1975
  return this.history.restorationIdentifier;
1971
1976
  }
1977
+ getActionForFormSubmission(formSubmission) {
1978
+ const {formElement: formElement, submitter: submitter} = formSubmission;
1979
+ const action = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("data-turbo-action")) || formElement.getAttribute("data-turbo-action");
1980
+ return isAction(action) ? action : "advance";
1981
+ }
1972
1982
  }
1973
1983
 
1974
1984
  var PageStage;
@@ -2061,51 +2071,10 @@ class ScrollObserver {
2061
2071
  }
2062
2072
  }
2063
2073
 
2064
- class StreamMessage {
2065
- constructor(html) {
2066
- this.templateElement = document.createElement("template");
2067
- this.templateElement.innerHTML = html;
2068
- }
2069
- static wrap(message) {
2070
- if (typeof message == "string") {
2071
- return new this(message);
2072
- } else {
2073
- return message;
2074
- }
2075
- }
2076
- get fragment() {
2077
- const fragment = document.createDocumentFragment();
2078
- for (const element of this.foreignElements) {
2079
- fragment.appendChild(document.importNode(element, true));
2080
- }
2081
- return fragment;
2082
- }
2083
- get foreignElements() {
2084
- return this.templateChildren.reduce(((streamElements, child) => {
2085
- if (child.tagName.toLowerCase() == "turbo-stream") {
2086
- return [ ...streamElements, child ];
2087
- } else {
2088
- return streamElements;
2089
- }
2090
- }), []);
2091
- }
2092
- get templateChildren() {
2093
- return Array.from(this.templateElement.content.children);
2094
- }
2095
- }
2096
-
2097
2074
  class StreamObserver {
2098
2075
  constructor(delegate) {
2099
2076
  this.sources = new Set;
2100
2077
  this.started = false;
2101
- this.prepareFetchRequest = event => {
2102
- var _a;
2103
- const fetchOptions = (_a = event.detail) === null || _a === void 0 ? void 0 : _a.fetchOptions;
2104
- if (fetchOptions) {
2105
- const {headers: headers} = fetchOptions;
2106
- headers.Accept = [ "text/vnd.turbo-stream.html", headers.Accept ].join(", ");
2107
- }
2108
- };
2109
2078
  this.inspectFetchResponse = event => {
2110
2079
  const response = fetchResponseFromEvent(event);
2111
2080
  if (response && fetchResponseIsStream(response)) {
@@ -2123,14 +2092,12 @@ class StreamObserver {
2123
2092
  start() {
2124
2093
  if (!this.started) {
2125
2094
  this.started = true;
2126
- addEventListener("turbo:before-fetch-request", this.prepareFetchRequest, true);
2127
2095
  addEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
2128
2096
  }
2129
2097
  }
2130
2098
  stop() {
2131
2099
  if (this.started) {
2132
2100
  this.started = false;
2133
- removeEventListener("turbo:before-fetch-request", this.prepareFetchRequest, true);
2134
2101
  removeEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
2135
2102
  }
2136
2103
  }
@@ -2171,70 +2138,21 @@ function fetchResponseFromEvent(event) {
2171
2138
  function fetchResponseIsStream(response) {
2172
2139
  var _a;
2173
2140
  const contentType = (_a = response.contentType) !== null && _a !== void 0 ? _a : "";
2174
- return /^text\/vnd\.turbo-stream\.html\b/.test(contentType);
2175
- }
2176
-
2177
- function isAction(action) {
2178
- return action == "advance" || action == "replace" || action == "restore";
2179
- }
2180
-
2181
- class Renderer {
2182
- renderView(callback) {
2183
- this.delegate.viewWillRender(this.newBody);
2184
- callback();
2185
- this.delegate.viewRendered(this.newBody);
2186
- }
2187
- invalidateView() {
2188
- this.delegate.viewInvalidated();
2189
- }
2190
- createScriptElement(element) {
2191
- if (element.getAttribute("data-turbo-eval") == "false") {
2192
- return element;
2193
- } else {
2194
- const createdScriptElement = document.createElement("script");
2195
- createdScriptElement.textContent = element.textContent;
2196
- createdScriptElement.async = false;
2197
- copyElementAttributes(createdScriptElement, element);
2198
- return createdScriptElement;
2199
- }
2200
- }
2201
- }
2202
-
2203
- function copyElementAttributes(destinationElement, sourceElement) {
2204
- for (const {name: name, value: value} of [ ...sourceElement.attributes ]) {
2205
- destinationElement.setAttribute(name, value);
2206
- }
2141
+ return contentType.startsWith(StreamMessage.contentType);
2207
2142
  }
2208
2143
 
2209
2144
  class ErrorRenderer extends Renderer {
2210
- constructor(delegate, html) {
2211
- super();
2212
- this.delegate = delegate;
2213
- this.htmlElement = (() => {
2214
- const htmlElement = document.createElement("html");
2215
- htmlElement.innerHTML = html;
2216
- return htmlElement;
2217
- })();
2218
- this.newHead = this.htmlElement.querySelector("head") || document.createElement("head");
2219
- this.newBody = this.htmlElement.querySelector("body") || document.createElement("body");
2220
- }
2221
- static render(delegate, callback, html) {
2222
- return new this(delegate, html).render(callback);
2223
- }
2224
- render(callback) {
2225
- this.renderView((() => {
2226
- this.replaceHeadAndBody();
2227
- this.activateBodyScriptElements();
2228
- callback();
2229
- }));
2145
+ async render() {
2146
+ this.replaceHeadAndBody();
2147
+ this.activateScriptElements();
2230
2148
  }
2231
2149
  replaceHeadAndBody() {
2232
2150
  const {documentElement: documentElement, head: head, body: body} = document;
2233
2151
  documentElement.replaceChild(this.newHead, head);
2234
- documentElement.replaceChild(this.newBody, body);
2152
+ documentElement.replaceChild(this.newElement, body);
2235
2153
  }
2236
- activateBodyScriptElements() {
2237
- for (const replaceableElement of this.getScriptElements()) {
2154
+ activateScriptElements() {
2155
+ for (const replaceableElement of this.scriptElements) {
2238
2156
  const parentNode = replaceableElement.parentNode;
2239
2157
  if (parentNode) {
2240
2158
  const element = this.createScriptElement(replaceableElement);
@@ -2242,82 +2160,38 @@ class ErrorRenderer extends Renderer {
2242
2160
  }
2243
2161
  }
2244
2162
  }
2245
- getScriptElements() {
2163
+ get newHead() {
2164
+ return this.newSnapshot.headSnapshot.element;
2165
+ }
2166
+ get scriptElements() {
2246
2167
  return [ ...document.documentElement.querySelectorAll("script") ];
2247
2168
  }
2248
2169
  }
2249
2170
 
2250
- class SnapshotCache {
2251
- constructor(size) {
2252
- this.keys = [];
2253
- this.snapshots = {};
2254
- this.size = size;
2255
- }
2256
- has(location) {
2257
- return location.toCacheKey() in this.snapshots;
2258
- }
2259
- get(location) {
2260
- if (this.has(location)) {
2261
- const snapshot = this.read(location);
2262
- this.touch(location);
2263
- return snapshot;
2264
- }
2265
- }
2266
- put(location, snapshot) {
2267
- this.write(location, snapshot);
2268
- this.touch(location);
2269
- return snapshot;
2270
- }
2271
- clear() {
2272
- this.snapshots = {};
2273
- }
2274
- read(location) {
2275
- return this.snapshots[location.toCacheKey()];
2171
+ class PageRenderer extends Renderer {
2172
+ get shouldRender() {
2173
+ return this.newSnapshot.isVisitable && this.trackedElementsAreIdentical;
2276
2174
  }
2277
- write(location, snapshot) {
2278
- this.snapshots[location.toCacheKey()] = snapshot;
2175
+ prepareToRender() {
2176
+ this.mergeHead();
2279
2177
  }
2280
- touch(location) {
2281
- const key = location.toCacheKey();
2282
- const index = this.keys.indexOf(key);
2283
- if (index > -1) this.keys.splice(index, 1);
2284
- this.keys.unshift(key);
2285
- this.trim();
2178
+ async render() {
2179
+ this.replaceBody();
2286
2180
  }
2287
- trim() {
2288
- for (const key of this.keys.splice(this.size)) {
2289
- delete this.snapshots[key];
2181
+ finishRendering() {
2182
+ super.finishRendering();
2183
+ if (!this.isPreview) {
2184
+ this.focusFirstAutofocusableElement();
2290
2185
  }
2291
2186
  }
2292
- }
2293
-
2294
- class SnapshotRenderer extends Renderer {
2295
- constructor(delegate, currentSnapshot, newSnapshot, isPreview) {
2296
- super();
2297
- this.delegate = delegate;
2298
- this.currentSnapshot = currentSnapshot;
2299
- this.currentHeadDetails = currentSnapshot.headDetails;
2300
- this.newSnapshot = newSnapshot;
2301
- this.newHeadDetails = newSnapshot.headDetails;
2302
- this.newBody = newSnapshot.bodyElement;
2303
- this.isPreview = isPreview;
2187
+ get currentHeadSnapshot() {
2188
+ return this.currentSnapshot.headSnapshot;
2304
2189
  }
2305
- static render(delegate, callback, currentSnapshot, newSnapshot, isPreview) {
2306
- return new this(delegate, currentSnapshot, newSnapshot, isPreview).render(callback);
2190
+ get newHeadSnapshot() {
2191
+ return this.newSnapshot.headSnapshot;
2307
2192
  }
2308
- render(callback) {
2309
- if (this.shouldRender()) {
2310
- this.mergeHead();
2311
- this.renderView((() => {
2312
- this.replaceBody();
2313
- if (!this.isPreview) {
2314
- this.focusFirstAutofocusableElement();
2315
- }
2316
- callback();
2317
- }));
2318
- } else {
2319
- this.invalidateView();
2320
- }
2193
+ get newElement() {
2194
+ return this.newSnapshot.element;
2321
2195
  }
2322
2196
  mergeHead() {
2323
2197
  this.copyNewHeadStylesheetElements();
@@ -2326,190 +2200,145 @@ class SnapshotRenderer extends Renderer {
2326
2200
  this.copyNewHeadProvisionalElements();
2327
2201
  }
2328
2202
  replaceBody() {
2329
- const placeholders = this.relocateCurrentBodyPermanentElements();
2330
- this.activateNewBody();
2331
- this.assignNewBody();
2332
- this.replacePlaceholderElementsWithClonedPermanentElements(placeholders);
2333
- }
2334
- shouldRender() {
2335
- return this.newSnapshot.isVisitable() && this.trackedElementsAreIdentical();
2203
+ this.preservingPermanentElements((() => {
2204
+ this.activateNewBody();
2205
+ this.assignNewBody();
2206
+ }));
2336
2207
  }
2337
- trackedElementsAreIdentical() {
2338
- return this.currentHeadDetails.getTrackedElementSignature() == this.newHeadDetails.getTrackedElementSignature();
2208
+ get trackedElementsAreIdentical() {
2209
+ return this.currentHeadSnapshot.trackedElementSignature == this.newHeadSnapshot.trackedElementSignature;
2339
2210
  }
2340
2211
  copyNewHeadStylesheetElements() {
2341
- for (const element of this.getNewHeadStylesheetElements()) {
2212
+ for (const element of this.newHeadStylesheetElements) {
2342
2213
  document.head.appendChild(element);
2343
2214
  }
2344
2215
  }
2345
2216
  copyNewHeadScriptElements() {
2346
- for (const element of this.getNewHeadScriptElements()) {
2217
+ for (const element of this.newHeadScriptElements) {
2347
2218
  document.head.appendChild(this.createScriptElement(element));
2348
2219
  }
2349
2220
  }
2350
2221
  removeCurrentHeadProvisionalElements() {
2351
- for (const element of this.getCurrentHeadProvisionalElements()) {
2222
+ for (const element of this.currentHeadProvisionalElements) {
2352
2223
  document.head.removeChild(element);
2353
2224
  }
2354
2225
  }
2355
2226
  copyNewHeadProvisionalElements() {
2356
- for (const element of this.getNewHeadProvisionalElements()) {
2227
+ for (const element of this.newHeadProvisionalElements) {
2357
2228
  document.head.appendChild(element);
2358
2229
  }
2359
2230
  }
2360
- relocateCurrentBodyPermanentElements() {
2361
- return this.getCurrentBodyPermanentElements().reduce(((placeholders, permanentElement) => {
2362
- const newElement = this.newSnapshot.getPermanentElementById(permanentElement.id);
2363
- if (newElement) {
2364
- const placeholder = createPlaceholderForPermanentElement(permanentElement);
2365
- replaceElementWithElement(permanentElement, placeholder.element);
2366
- replaceElementWithElement(newElement, permanentElement);
2367
- return [ ...placeholders, placeholder ];
2368
- } else {
2369
- return placeholders;
2370
- }
2371
- }), []);
2372
- }
2373
- replacePlaceholderElementsWithClonedPermanentElements(placeholders) {
2374
- for (const {element: element, permanentElement: permanentElement} of placeholders) {
2375
- const clonedElement = permanentElement.cloneNode(true);
2376
- replaceElementWithElement(element, clonedElement);
2377
- }
2378
- }
2379
2231
  activateNewBody() {
2380
- document.adoptNode(this.newBody);
2232
+ document.adoptNode(this.newElement);
2381
2233
  this.activateNewBodyScriptElements();
2382
2234
  }
2383
2235
  activateNewBodyScriptElements() {
2384
- for (const inertScriptElement of this.getNewBodyScriptElements()) {
2236
+ for (const inertScriptElement of this.newBodyScriptElements) {
2385
2237
  const activatedScriptElement = this.createScriptElement(inertScriptElement);
2386
- replaceElementWithElement(inertScriptElement, activatedScriptElement);
2238
+ inertScriptElement.replaceWith(activatedScriptElement);
2387
2239
  }
2388
2240
  }
2389
2241
  assignNewBody() {
2390
- if (document.body) {
2391
- replaceElementWithElement(document.body, this.newBody);
2242
+ if (document.body && this.newElement instanceof HTMLBodyElement) {
2243
+ document.body.replaceWith(this.newElement);
2392
2244
  } else {
2393
- document.documentElement.appendChild(this.newBody);
2394
- }
2395
- }
2396
- focusFirstAutofocusableElement() {
2397
- const element = this.newSnapshot.findFirstAutofocusableElement();
2398
- if (elementIsFocusable(element)) {
2399
- element.focus();
2245
+ document.documentElement.appendChild(this.newElement);
2400
2246
  }
2401
2247
  }
2402
- getNewHeadStylesheetElements() {
2403
- return this.newHeadDetails.getStylesheetElementsNotInDetails(this.currentHeadDetails);
2404
- }
2405
- getNewHeadScriptElements() {
2406
- return this.newHeadDetails.getScriptElementsNotInDetails(this.currentHeadDetails);
2248
+ get newHeadStylesheetElements() {
2249
+ return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot);
2407
2250
  }
2408
- getCurrentHeadProvisionalElements() {
2409
- return this.currentHeadDetails.getProvisionalElements();
2251
+ get newHeadScriptElements() {
2252
+ return this.newHeadSnapshot.getScriptElementsNotInSnapshot(this.currentHeadSnapshot);
2410
2253
  }
2411
- getNewHeadProvisionalElements() {
2412
- return this.newHeadDetails.getProvisionalElements();
2254
+ get currentHeadProvisionalElements() {
2255
+ return this.currentHeadSnapshot.provisionalElements;
2413
2256
  }
2414
- getCurrentBodyPermanentElements() {
2415
- return this.currentSnapshot.getPermanentElementsPresentInSnapshot(this.newSnapshot);
2257
+ get newHeadProvisionalElements() {
2258
+ return this.newHeadSnapshot.provisionalElements;
2416
2259
  }
2417
- getNewBodyScriptElements() {
2418
- return [ ...this.newBody.querySelectorAll("script") ];
2260
+ get newBodyScriptElements() {
2261
+ return this.newElement.querySelectorAll("script");
2419
2262
  }
2420
2263
  }
2421
2264
 
2422
- function createPlaceholderForPermanentElement(permanentElement) {
2423
- const element = document.createElement("meta");
2424
- element.setAttribute("name", "turbo-permanent-placeholder");
2425
- element.setAttribute("content", permanentElement.id);
2426
- return {
2427
- element: element,
2428
- permanentElement: permanentElement
2429
- };
2430
- }
2431
-
2432
- function replaceElementWithElement(fromElement, toElement) {
2433
- const parentElement = fromElement.parentElement;
2434
- if (parentElement) {
2435
- return parentElement.replaceChild(toElement, fromElement);
2265
+ class SnapshotCache {
2266
+ constructor(size) {
2267
+ this.keys = [];
2268
+ this.snapshots = {};
2269
+ this.size = size;
2270
+ }
2271
+ has(location) {
2272
+ return toCacheKey(location) in this.snapshots;
2273
+ }
2274
+ get(location) {
2275
+ if (this.has(location)) {
2276
+ const snapshot = this.read(location);
2277
+ this.touch(location);
2278
+ return snapshot;
2279
+ }
2280
+ }
2281
+ put(location, snapshot) {
2282
+ this.write(location, snapshot);
2283
+ this.touch(location);
2284
+ return snapshot;
2285
+ }
2286
+ clear() {
2287
+ this.snapshots = {};
2288
+ }
2289
+ read(location) {
2290
+ return this.snapshots[toCacheKey(location)];
2291
+ }
2292
+ write(location, snapshot) {
2293
+ this.snapshots[toCacheKey(location)] = snapshot;
2294
+ }
2295
+ touch(location) {
2296
+ const key = toCacheKey(location);
2297
+ const index = this.keys.indexOf(key);
2298
+ if (index > -1) this.keys.splice(index, 1);
2299
+ this.keys.unshift(key);
2300
+ this.trim();
2301
+ }
2302
+ trim() {
2303
+ for (const key of this.keys.splice(this.size)) {
2304
+ delete this.snapshots[key];
2305
+ }
2436
2306
  }
2437
2307
  }
2438
2308
 
2439
- function elementIsFocusable(element) {
2440
- return element && typeof element.focus == "function";
2441
- }
2442
-
2443
- class View {
2444
- constructor(delegate) {
2445
- this.htmlElement = document.documentElement;
2309
+ class PageView extends View {
2310
+ constructor() {
2311
+ super(...arguments);
2446
2312
  this.snapshotCache = new SnapshotCache(10);
2447
- this.delegate = delegate;
2313
+ this.lastRenderedLocation = new URL(location.href);
2448
2314
  }
2449
- getRootLocation() {
2450
- return this.getSnapshot().getRootLocation();
2451
- }
2452
- getElementForAnchor(anchor) {
2453
- return this.getSnapshot().getElementForAnchor(anchor);
2315
+ renderPage(snapshot, isPreview = false) {
2316
+ const renderer = new PageRenderer(this.snapshot, snapshot, isPreview);
2317
+ return this.render(renderer);
2454
2318
  }
2455
- getSnapshot() {
2456
- return Snapshot.fromHTMLElement(this.htmlElement);
2319
+ renderError(snapshot) {
2320
+ const renderer = new ErrorRenderer(this.snapshot, snapshot, false);
2321
+ this.render(renderer);
2457
2322
  }
2458
2323
  clearSnapshotCache() {
2459
2324
  this.snapshotCache.clear();
2460
2325
  }
2461
- shouldCacheSnapshot() {
2462
- return this.getSnapshot().isCacheable();
2463
- }
2464
2326
  async cacheSnapshot() {
2465
- if (this.shouldCacheSnapshot()) {
2327
+ if (this.shouldCacheSnapshot) {
2466
2328
  this.delegate.viewWillCacheSnapshot();
2467
- const snapshot = this.getSnapshot();
2468
- const location = this.lastRenderedLocation || Location.currentLocation;
2469
- await nextMicrotask();
2329
+ const {snapshot: snapshot, lastRenderedLocation: location} = this;
2330
+ await nextEventLoopTick();
2470
2331
  this.snapshotCache.put(location, snapshot.clone());
2471
2332
  }
2472
2333
  }
2473
2334
  getCachedSnapshotForLocation(location) {
2474
2335
  return this.snapshotCache.get(location);
2475
2336
  }
2476
- render({snapshot: snapshot, error: error, isPreview: isPreview}, callback) {
2477
- this.markAsPreview(isPreview);
2478
- if (snapshot) {
2479
- this.renderSnapshot(snapshot, isPreview, callback);
2480
- } else {
2481
- this.renderError(error, callback);
2482
- }
2483
- }
2484
- scrollToAnchor(anchor) {
2485
- const element = this.getElementForAnchor(anchor);
2486
- if (element) {
2487
- this.scrollToElement(element);
2488
- } else {
2489
- this.scrollToPosition({
2490
- x: 0,
2491
- y: 0
2492
- });
2493
- }
2494
- }
2495
- scrollToElement(element) {
2496
- element.scrollIntoView();
2337
+ get snapshot() {
2338
+ return PageSnapshot.fromElement(this.element);
2497
2339
  }
2498
- scrollToPosition({x: x, y: y}) {
2499
- window.scrollTo(x, y);
2500
- }
2501
- markAsPreview(isPreview) {
2502
- if (isPreview) {
2503
- this.htmlElement.setAttribute("data-turbo-preview", "");
2504
- } else {
2505
- this.htmlElement.removeAttribute("data-turbo-preview");
2506
- }
2507
- }
2508
- renderSnapshot(snapshot, isPreview, callback) {
2509
- SnapshotRenderer.render(this.delegate, callback, this.getSnapshot(), snapshot, isPreview || false);
2510
- }
2511
- renderError(error, callback) {
2512
- ErrorRenderer.render(this.delegate, callback, error || "");
2340
+ get shouldCacheSnapshot() {
2341
+ return this.snapshot.isCacheable;
2513
2342
  }
2514
2343
  }
2515
2344
 
@@ -2517,9 +2346,10 @@ class Session {
2517
2346
  constructor() {
2518
2347
  this.navigator = new Navigator(this);
2519
2348
  this.history = new History(this);
2520
- this.view = new View(this);
2349
+ this.view = new PageView(this, document.documentElement);
2521
2350
  this.adapter = new BrowserAdapter(this);
2522
2351
  this.pageObserver = new PageObserver(this);
2352
+ this.cacheObserver = new CacheObserver;
2523
2353
  this.linkClickObserver = new LinkClickObserver(this);
2524
2354
  this.formSubmitObserver = new FormSubmitObserver(this);
2525
2355
  this.scrollObserver = new ScrollObserver(this);
@@ -2532,6 +2362,7 @@ class Session {
2532
2362
  start() {
2533
2363
  if (!this.started) {
2534
2364
  this.pageObserver.start();
2365
+ this.cacheObserver.start();
2535
2366
  this.linkClickObserver.start();
2536
2367
  this.formSubmitObserver.start();
2537
2368
  this.scrollObserver.start();
@@ -2548,6 +2379,7 @@ class Session {
2548
2379
  stop() {
2549
2380
  if (this.started) {
2550
2381
  this.pageObserver.stop();
2382
+ this.cacheObserver.stop();
2551
2383
  this.linkClickObserver.stop();
2552
2384
  this.formSubmitObserver.stop();
2553
2385
  this.scrollObserver.stop();
@@ -2561,7 +2393,7 @@ class Session {
2561
2393
  this.adapter = adapter;
2562
2394
  }
2563
2395
  visit(location, options = {}) {
2564
- this.navigator.proposeVisit(Location.wrap(location), options);
2396
+ this.navigator.proposeVisit(expandURL(location), options);
2565
2397
  }
2566
2398
  connectStreamSource(source) {
2567
2399
  this.streamObserver.connectStreamSource(source);
@@ -2584,9 +2416,9 @@ class Session {
2584
2416
  get restorationIdentifier() {
2585
2417
  return this.history.restorationIdentifier;
2586
2418
  }
2587
- historyPoppedToLocationWithRestorationIdentifier(location) {
2419
+ historyPoppedToLocationWithRestorationIdentifier(location, restorationIdentifier) {
2588
2420
  if (this.enabled) {
2589
- this.navigator.proposeVisit(location, {
2421
+ this.navigator.startVisit(location, restorationIdentifier, {
2590
2422
  action: "restore",
2591
2423
  historyChanged: true
2592
2424
  });
@@ -2594,134 +2426,577 @@ class Session {
2594
2426
  this.adapter.pageInvalidated();
2595
2427
  }
2596
2428
  }
2597
- scrollPositionChanged(position) {
2598
- this.history.updateRestorationData({
2599
- scrollPosition: position
2600
- });
2429
+ scrollPositionChanged(position) {
2430
+ this.history.updateRestorationData({
2431
+ scrollPosition: position
2432
+ });
2433
+ }
2434
+ willFollowLinkToLocation(link, location) {
2435
+ return elementIsNavigable(link) && this.locationIsVisitable(location) && this.applicationAllowsFollowingLinkToLocation(link, location);
2436
+ }
2437
+ followedLinkToLocation(link, location) {
2438
+ const action = this.getActionForLink(link);
2439
+ this.visit(location.href, {
2440
+ action: action
2441
+ });
2442
+ }
2443
+ allowsVisitingLocation(location) {
2444
+ return this.applicationAllowsVisitingLocation(location);
2445
+ }
2446
+ visitProposedToLocation(location, options) {
2447
+ extendURLWithDeprecatedProperties(location);
2448
+ this.adapter.visitProposedToLocation(location, options);
2449
+ }
2450
+ visitStarted(visit) {
2451
+ extendURLWithDeprecatedProperties(visit.location);
2452
+ this.notifyApplicationAfterVisitingLocation(visit.location);
2453
+ }
2454
+ visitCompleted(visit) {
2455
+ this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
2456
+ }
2457
+ willSubmitForm(form, submitter) {
2458
+ return elementIsNavigable(form) && elementIsNavigable(submitter);
2459
+ }
2460
+ formSubmitted(form, submitter) {
2461
+ this.navigator.submitForm(form, submitter);
2462
+ }
2463
+ pageBecameInteractive() {
2464
+ this.view.lastRenderedLocation = this.location;
2465
+ this.notifyApplicationAfterPageLoad();
2466
+ }
2467
+ pageLoaded() {
2468
+ this.history.assumeControlOfScrollRestoration();
2469
+ }
2470
+ pageWillUnload() {
2471
+ this.history.relinquishControlOfScrollRestoration();
2472
+ }
2473
+ receivedMessageFromStream(message) {
2474
+ this.renderStreamMessage(message);
2475
+ }
2476
+ viewWillCacheSnapshot() {
2477
+ this.notifyApplicationBeforeCachingSnapshot();
2478
+ }
2479
+ viewWillRenderSnapshot({element: element}, isPreview) {
2480
+ this.notifyApplicationBeforeRender(element);
2481
+ }
2482
+ viewRenderedSnapshot(snapshot, isPreview) {
2483
+ this.view.lastRenderedLocation = this.history.location;
2484
+ this.notifyApplicationAfterRender();
2485
+ }
2486
+ viewInvalidated() {
2487
+ this.adapter.pageInvalidated();
2488
+ }
2489
+ applicationAllowsFollowingLinkToLocation(link, location) {
2490
+ const event = this.notifyApplicationAfterClickingLinkToLocation(link, location);
2491
+ return !event.defaultPrevented;
2492
+ }
2493
+ applicationAllowsVisitingLocation(location) {
2494
+ const event = this.notifyApplicationBeforeVisitingLocation(location);
2495
+ return !event.defaultPrevented;
2496
+ }
2497
+ notifyApplicationAfterClickingLinkToLocation(link, location) {
2498
+ return dispatch("turbo:click", {
2499
+ target: link,
2500
+ detail: {
2501
+ url: location.href
2502
+ },
2503
+ cancelable: true
2504
+ });
2505
+ }
2506
+ notifyApplicationBeforeVisitingLocation(location) {
2507
+ return dispatch("turbo:before-visit", {
2508
+ detail: {
2509
+ url: location.href
2510
+ },
2511
+ cancelable: true
2512
+ });
2513
+ }
2514
+ notifyApplicationAfterVisitingLocation(location) {
2515
+ return dispatch("turbo:visit", {
2516
+ detail: {
2517
+ url: location.href
2518
+ }
2519
+ });
2520
+ }
2521
+ notifyApplicationBeforeCachingSnapshot() {
2522
+ return dispatch("turbo:before-cache");
2523
+ }
2524
+ notifyApplicationBeforeRender(newBody) {
2525
+ return dispatch("turbo:before-render", {
2526
+ detail: {
2527
+ newBody: newBody
2528
+ }
2529
+ });
2530
+ }
2531
+ notifyApplicationAfterRender() {
2532
+ return dispatch("turbo:render");
2533
+ }
2534
+ notifyApplicationAfterPageLoad(timing = {}) {
2535
+ return dispatch("turbo:load", {
2536
+ detail: {
2537
+ url: this.location.href,
2538
+ timing: timing
2539
+ }
2540
+ });
2541
+ }
2542
+ getActionForLink(link) {
2543
+ const action = link.getAttribute("data-turbo-action");
2544
+ return isAction(action) ? action : "advance";
2545
+ }
2546
+ locationIsVisitable(location) {
2547
+ return isPrefixedBy(location, this.snapshot.rootLocation) && isHTML(location);
2548
+ }
2549
+ get snapshot() {
2550
+ return this.view.snapshot;
2551
+ }
2552
+ }
2553
+
2554
+ function elementIsNavigable(element) {
2555
+ const container = element === null || element === void 0 ? void 0 : element.closest("[data-turbo]");
2556
+ if (container) {
2557
+ return container.getAttribute("data-turbo") != "false";
2558
+ } else {
2559
+ return true;
2560
+ }
2561
+ }
2562
+
2563
+ function extendURLWithDeprecatedProperties(url) {
2564
+ Object.defineProperties(url, deprecatedLocationPropertyDescriptors);
2565
+ }
2566
+
2567
+ const deprecatedLocationPropertyDescriptors = {
2568
+ absoluteURL: {
2569
+ get() {
2570
+ return this.toString();
2571
+ }
2572
+ }
2573
+ };
2574
+
2575
+ class FrameController {
2576
+ constructor(element) {
2577
+ this.resolveVisitPromise = () => {};
2578
+ this.connected = false;
2579
+ this.hasBeenLoaded = false;
2580
+ this.settingSourceURL = false;
2581
+ this.element = element;
2582
+ this.view = new FrameView(this, this.element);
2583
+ this.appearanceObserver = new AppearanceObserver(this, this.element);
2584
+ this.linkInterceptor = new LinkInterceptor(this, this.element);
2585
+ this.formInterceptor = new FormInterceptor(this, this.element);
2586
+ }
2587
+ connect() {
2588
+ if (!this.connected) {
2589
+ this.connected = true;
2590
+ if (this.loadingStyle == FrameLoadingStyle.lazy) {
2591
+ this.appearanceObserver.start();
2592
+ }
2593
+ this.linkInterceptor.start();
2594
+ this.formInterceptor.start();
2595
+ this.sourceURLChanged();
2596
+ }
2597
+ }
2598
+ disconnect() {
2599
+ if (this.connected) {
2600
+ this.connected = false;
2601
+ this.appearanceObserver.stop();
2602
+ this.linkInterceptor.stop();
2603
+ this.formInterceptor.stop();
2604
+ }
2605
+ }
2606
+ disabledChanged() {
2607
+ if (this.loadingStyle == FrameLoadingStyle.eager) {
2608
+ this.loadSourceURL();
2609
+ }
2610
+ }
2611
+ sourceURLChanged() {
2612
+ if (this.loadingStyle == FrameLoadingStyle.eager || this.hasBeenLoaded) {
2613
+ this.loadSourceURL();
2614
+ }
2615
+ }
2616
+ loadingStyleChanged() {
2617
+ if (this.loadingStyle == FrameLoadingStyle.lazy) {
2618
+ this.appearanceObserver.start();
2619
+ } else {
2620
+ this.appearanceObserver.stop();
2621
+ this.loadSourceURL();
2622
+ }
2623
+ }
2624
+ async loadSourceURL() {
2625
+ if (!this.settingSourceURL && this.enabled && this.isActive && this.sourceURL != this.currentURL) {
2626
+ const previousURL = this.currentURL;
2627
+ this.currentURL = this.sourceURL;
2628
+ if (this.sourceURL) {
2629
+ try {
2630
+ this.element.loaded = this.visit(this.sourceURL);
2631
+ this.appearanceObserver.stop();
2632
+ await this.element.loaded;
2633
+ this.hasBeenLoaded = true;
2634
+ } catch (error) {
2635
+ this.currentURL = previousURL;
2636
+ throw error;
2637
+ }
2638
+ }
2639
+ }
2640
+ }
2641
+ async loadResponse(fetchResponse) {
2642
+ if (fetchResponse.redirected) {
2643
+ this.sourceURL = fetchResponse.response.url;
2644
+ }
2645
+ try {
2646
+ const html = await fetchResponse.responseHTML;
2647
+ if (html) {
2648
+ const {body: body} = parseHTMLDocument(html);
2649
+ const snapshot = new Snapshot(await this.extractForeignFrameElement(body));
2650
+ const renderer = new FrameRenderer(this.view.snapshot, snapshot, false);
2651
+ await this.view.render(renderer);
2652
+ }
2653
+ } catch (error) {
2654
+ console.error(error);
2655
+ this.view.invalidate();
2656
+ }
2657
+ }
2658
+ elementAppearedInViewport(element) {
2659
+ this.loadSourceURL();
2660
+ }
2661
+ shouldInterceptLinkClick(element, url) {
2662
+ return this.shouldInterceptNavigation(element);
2663
+ }
2664
+ linkClickIntercepted(element, url) {
2665
+ this.navigateFrame(element, url);
2666
+ }
2667
+ shouldInterceptFormSubmission(element, submitter) {
2668
+ return this.shouldInterceptNavigation(element, submitter);
2669
+ }
2670
+ formSubmissionIntercepted(element, submitter) {
2671
+ if (this.formSubmission) {
2672
+ this.formSubmission.stop();
2673
+ }
2674
+ this.formSubmission = new FormSubmission(this, element, submitter);
2675
+ if (this.formSubmission.fetchRequest.isIdempotent) {
2676
+ this.navigateFrame(element, this.formSubmission.fetchRequest.url.href);
2677
+ } else {
2678
+ const {fetchRequest: fetchRequest} = this.formSubmission;
2679
+ this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
2680
+ this.formSubmission.start();
2681
+ }
2682
+ }
2683
+ prepareHeadersForRequest(headers, request) {
2684
+ headers["Turbo-Frame"] = this.id;
2685
+ }
2686
+ requestStarted(request) {
2687
+ this.element.setAttribute("busy", "");
2688
+ }
2689
+ requestPreventedHandlingResponse(request, response) {
2690
+ this.resolveVisitPromise();
2691
+ }
2692
+ async requestSucceededWithResponse(request, response) {
2693
+ await this.loadResponse(response);
2694
+ this.resolveVisitPromise();
2695
+ }
2696
+ requestFailedWithResponse(request, response) {
2697
+ console.error(response);
2698
+ this.resolveVisitPromise();
2699
+ }
2700
+ requestErrored(request, error) {
2701
+ console.error(error);
2702
+ this.resolveVisitPromise();
2703
+ }
2704
+ requestFinished(request) {
2705
+ this.element.removeAttribute("busy");
2706
+ }
2707
+ formSubmissionStarted(formSubmission) {
2708
+ const frame = this.findFrameElement(formSubmission.formElement);
2709
+ frame.setAttribute("busy", "");
2710
+ }
2711
+ formSubmissionSucceededWithResponse(formSubmission, response) {
2712
+ const frame = this.findFrameElement(formSubmission.formElement);
2713
+ frame.delegate.loadResponse(response);
2714
+ }
2715
+ formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
2716
+ this.element.delegate.loadResponse(fetchResponse);
2717
+ }
2718
+ formSubmissionErrored(formSubmission, error) {
2719
+ console.error(error);
2720
+ }
2721
+ formSubmissionFinished(formSubmission) {
2722
+ const frame = this.findFrameElement(formSubmission.formElement);
2723
+ frame.removeAttribute("busy");
2724
+ }
2725
+ viewWillRenderSnapshot(snapshot, isPreview) {}
2726
+ viewRenderedSnapshot(snapshot, isPreview) {}
2727
+ viewInvalidated() {}
2728
+ async visit(url) {
2729
+ const request = new FetchRequest(this, FetchMethod.get, expandURL(url));
2730
+ return new Promise((resolve => {
2731
+ this.resolveVisitPromise = () => {
2732
+ this.resolveVisitPromise = () => {};
2733
+ resolve();
2734
+ };
2735
+ request.perform();
2736
+ }));
2737
+ }
2738
+ navigateFrame(element, url) {
2739
+ const frame = this.findFrameElement(element);
2740
+ frame.src = url;
2741
+ }
2742
+ findFrameElement(element) {
2743
+ var _a;
2744
+ const id = element.getAttribute("data-turbo-frame") || this.element.getAttribute("target");
2745
+ return (_a = getFrameElementById(id)) !== null && _a !== void 0 ? _a : this.element;
2746
+ }
2747
+ async extractForeignFrameElement(container) {
2748
+ let element;
2749
+ const id = CSS.escape(this.id);
2750
+ try {
2751
+ if (element = activateElement(container.querySelector(`turbo-frame#${id}`), this.currentURL)) {
2752
+ return element;
2753
+ }
2754
+ if (element = activateElement(container.querySelector(`turbo-frame[src][recurse~=${id}]`), this.currentURL)) {
2755
+ await element.loaded;
2756
+ return await this.extractForeignFrameElement(element);
2757
+ }
2758
+ console.error(`Response has no matching <turbo-frame id="${id}"> element`);
2759
+ } catch (error) {
2760
+ console.error(error);
2761
+ }
2762
+ return new FrameElement;
2763
+ }
2764
+ shouldInterceptNavigation(element, submitter) {
2765
+ const id = element.getAttribute("data-turbo-frame") || this.element.getAttribute("target");
2766
+ if (!this.enabled || id == "_top") {
2767
+ return false;
2768
+ }
2769
+ if (id) {
2770
+ const frameElement = getFrameElementById(id);
2771
+ if (frameElement) {
2772
+ return !frameElement.disabled;
2773
+ }
2774
+ }
2775
+ if (!elementIsNavigable(element)) {
2776
+ return false;
2777
+ }
2778
+ if (submitter && !elementIsNavigable(submitter)) {
2779
+ return false;
2780
+ }
2781
+ return true;
2601
2782
  }
2602
- willFollowLinkToLocation(link, location) {
2603
- return this.elementIsNavigable(link) && this.locationIsVisitable(location) && this.applicationAllowsFollowingLinkToLocation(link, location);
2783
+ get id() {
2784
+ return this.element.id;
2604
2785
  }
2605
- followedLinkToLocation(link, location) {
2606
- const action = this.getActionForLink(link);
2607
- this.visit(location, {
2608
- action: action
2609
- });
2786
+ get enabled() {
2787
+ return !this.element.disabled;
2610
2788
  }
2611
- allowsVisitingLocation(location) {
2612
- return this.applicationAllowsVisitingLocation(location);
2789
+ get sourceURL() {
2790
+ if (this.element.src) {
2791
+ return this.element.src;
2792
+ }
2613
2793
  }
2614
- visitProposedToLocation(location, options) {
2615
- this.adapter.visitProposedToLocation(location, options);
2794
+ set sourceURL(sourceURL) {
2795
+ this.settingSourceURL = true;
2796
+ this.element.src = sourceURL !== null && sourceURL !== void 0 ? sourceURL : null;
2797
+ this.currentURL = this.element.src;
2798
+ this.settingSourceURL = false;
2616
2799
  }
2617
- visitStarted(visit) {
2618
- this.notifyApplicationAfterVisitingLocation(visit.location);
2800
+ get loadingStyle() {
2801
+ return this.element.loading;
2619
2802
  }
2620
- visitCompleted(visit) {
2621
- this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
2803
+ get isLoading() {
2804
+ return this.formSubmission !== undefined || this.resolveVisitPromise() !== undefined;
2622
2805
  }
2623
- willSubmitForm(form, submitter) {
2624
- return this.elementIsNavigable(form) && this.elementIsNavigable(submitter);
2806
+ get isActive() {
2807
+ return this.element.isActive && this.connected;
2625
2808
  }
2626
- formSubmitted(form, submitter) {
2627
- this.navigator.submitForm(form, submitter);
2809
+ }
2810
+
2811
+ function getFrameElementById(id) {
2812
+ if (id != null) {
2813
+ const element = document.getElementById(id);
2814
+ if (element instanceof FrameElement) {
2815
+ return element;
2816
+ }
2628
2817
  }
2629
- pageBecameInteractive() {
2630
- this.view.lastRenderedLocation = this.location;
2631
- this.notifyApplicationAfterPageLoad();
2818
+ }
2819
+
2820
+ function activateElement(element, currentURL) {
2821
+ if (element) {
2822
+ const src = element.getAttribute("src");
2823
+ if (src != null && currentURL != null && urlsAreEqual(src, currentURL)) {
2824
+ throw new Error(`Matching <turbo-frame id="${element.id}"> element has a source URL which references itself`);
2825
+ }
2826
+ if (element.ownerDocument !== document) {
2827
+ element = document.importNode(element, true);
2828
+ }
2829
+ if (element instanceof FrameElement) {
2830
+ element.connectedCallback();
2831
+ return element;
2832
+ }
2632
2833
  }
2633
- pageLoaded() {
2634
- this.history.assumeControlOfScrollRestoration();
2834
+ }
2835
+
2836
+ const StreamActions = {
2837
+ after() {
2838
+ this.targetElements.forEach((e => {
2839
+ var _a;
2840
+ return (_a = e.parentElement) === null || _a === void 0 ? void 0 : _a.insertBefore(this.templateContent, e.nextSibling);
2841
+ }));
2842
+ },
2843
+ append() {
2844
+ this.removeDuplicateTargetChildren();
2845
+ this.targetElements.forEach((e => e.append(this.templateContent)));
2846
+ },
2847
+ before() {
2848
+ this.targetElements.forEach((e => {
2849
+ var _a;
2850
+ return (_a = e.parentElement) === null || _a === void 0 ? void 0 : _a.insertBefore(this.templateContent, e);
2851
+ }));
2852
+ },
2853
+ prepend() {
2854
+ this.removeDuplicateTargetChildren();
2855
+ this.targetElements.forEach((e => e.prepend(this.templateContent)));
2856
+ },
2857
+ remove() {
2858
+ this.targetElements.forEach((e => e.remove()));
2859
+ },
2860
+ replace() {
2861
+ this.targetElements.forEach((e => e.replaceWith(this.templateContent)));
2862
+ },
2863
+ update() {
2864
+ this.targetElements.forEach((e => {
2865
+ e.innerHTML = "";
2866
+ e.append(this.templateContent);
2867
+ }));
2635
2868
  }
2636
- pageWillUnload() {
2637
- this.history.relinquishControlOfScrollRestoration();
2869
+ };
2870
+
2871
+ class StreamElement extends HTMLElement {
2872
+ async connectedCallback() {
2873
+ try {
2874
+ await this.render();
2875
+ } catch (error) {
2876
+ console.error(error);
2877
+ } finally {
2878
+ this.disconnect();
2879
+ }
2638
2880
  }
2639
- receivedMessageFromStream(message) {
2640
- this.renderStreamMessage(message);
2881
+ async render() {
2882
+ var _a;
2883
+ return (_a = this.renderPromise) !== null && _a !== void 0 ? _a : this.renderPromise = (async () => {
2884
+ if (this.dispatchEvent(this.beforeRenderEvent)) {
2885
+ await nextAnimationFrame();
2886
+ this.performAction();
2887
+ }
2888
+ })();
2641
2889
  }
2642
- viewWillRender(newBody) {
2643
- this.notifyApplicationBeforeRender(newBody);
2890
+ disconnect() {
2891
+ try {
2892
+ this.remove();
2893
+ } catch (_a) {}
2644
2894
  }
2645
- viewRendered() {
2646
- this.view.lastRenderedLocation = this.history.location;
2647
- this.notifyApplicationAfterRender();
2895
+ removeDuplicateTargetChildren() {
2896
+ this.duplicateChildren.forEach((c => c.remove()));
2648
2897
  }
2649
- viewInvalidated() {
2650
- this.adapter.pageInvalidated();
2898
+ get duplicateChildren() {
2899
+ var _a;
2900
+ const existingChildren = this.targetElements.flatMap((e => [ ...e.children ])).filter((c => !!c.id));
2901
+ const newChildrenIds = [ ...(_a = this.templateContent) === null || _a === void 0 ? void 0 : _a.children ].filter((c => !!c.id)).map((c => c.id));
2902
+ return existingChildren.filter((c => newChildrenIds.includes(c.id)));
2651
2903
  }
2652
- viewWillCacheSnapshot() {
2653
- this.notifyApplicationBeforeCachingSnapshot();
2904
+ get performAction() {
2905
+ if (this.action) {
2906
+ const actionFunction = StreamActions[this.action];
2907
+ if (actionFunction) {
2908
+ return actionFunction;
2909
+ }
2910
+ this.raise("unknown action");
2911
+ }
2912
+ this.raise("action attribute is missing");
2654
2913
  }
2655
- applicationAllowsFollowingLinkToLocation(link, location) {
2656
- const event = this.notifyApplicationAfterClickingLinkToLocation(link, location);
2657
- return !event.defaultPrevented;
2914
+ get targetElements() {
2915
+ if (this.target) {
2916
+ return this.targetElementsById;
2917
+ } else if (this.targets) {
2918
+ return this.targetElementsByQuery;
2919
+ } else {
2920
+ this.raise("target or targets attribute is missing");
2921
+ }
2658
2922
  }
2659
- applicationAllowsVisitingLocation(location) {
2660
- const event = this.notifyApplicationBeforeVisitingLocation(location);
2661
- return !event.defaultPrevented;
2923
+ get templateContent() {
2924
+ return this.templateElement.content.cloneNode(true);
2662
2925
  }
2663
- notifyApplicationAfterClickingLinkToLocation(link, location) {
2664
- return dispatch("turbo:click", {
2665
- target: link,
2666
- detail: {
2667
- url: location.absoluteURL
2668
- },
2669
- cancelable: true
2670
- });
2926
+ get templateElement() {
2927
+ if (this.firstElementChild instanceof HTMLTemplateElement) {
2928
+ return this.firstElementChild;
2929
+ }
2930
+ this.raise("first child element must be a <template> element");
2671
2931
  }
2672
- notifyApplicationBeforeVisitingLocation(location) {
2673
- return dispatch("turbo:before-visit", {
2674
- detail: {
2675
- url: location.absoluteURL
2676
- },
2677
- cancelable: true
2678
- });
2932
+ get action() {
2933
+ return this.getAttribute("action");
2679
2934
  }
2680
- notifyApplicationAfterVisitingLocation(location) {
2681
- return dispatch("turbo:visit", {
2682
- detail: {
2683
- url: location.absoluteURL
2684
- }
2685
- });
2935
+ get target() {
2936
+ return this.getAttribute("target");
2686
2937
  }
2687
- notifyApplicationBeforeCachingSnapshot() {
2688
- return dispatch("turbo:before-cache");
2938
+ get targets() {
2939
+ return this.getAttribute("targets");
2689
2940
  }
2690
- notifyApplicationBeforeRender(newBody) {
2691
- return dispatch("turbo:before-render", {
2692
- detail: {
2693
- newBody: newBody
2694
- }
2695
- });
2941
+ raise(message) {
2942
+ throw new Error(`${this.description}: ${message}`);
2696
2943
  }
2697
- notifyApplicationAfterRender() {
2698
- return dispatch("turbo:render");
2944
+ get description() {
2945
+ var _a, _b;
2946
+ return (_b = ((_a = this.outerHTML.match(/<[^>]+>/)) !== null && _a !== void 0 ? _a : [])[0]) !== null && _b !== void 0 ? _b : "<turbo-stream>";
2699
2947
  }
2700
- notifyApplicationAfterPageLoad(timing = {}) {
2701
- return dispatch("turbo:load", {
2702
- detail: {
2703
- url: this.location.absoluteURL,
2704
- timing: timing
2705
- }
2948
+ get beforeRenderEvent() {
2949
+ return new CustomEvent("turbo:before-stream-render", {
2950
+ bubbles: true,
2951
+ cancelable: true
2706
2952
  });
2707
2953
  }
2708
- getActionForLink(link) {
2709
- const action = link.getAttribute("data-turbo-action");
2710
- return isAction(action) ? action : "advance";
2711
- }
2712
- elementIsNavigable(element) {
2713
- const container = element === null || element === void 0 ? void 0 : element.closest("[data-turbo]");
2714
- if (container) {
2715
- return container.getAttribute("data-turbo") != "false";
2954
+ get targetElementsById() {
2955
+ var _a;
2956
+ const element = (_a = this.ownerDocument) === null || _a === void 0 ? void 0 : _a.getElementById(this.target);
2957
+ if (element !== null) {
2958
+ return [ element ];
2716
2959
  } else {
2717
- return true;
2960
+ return [];
2718
2961
  }
2719
2962
  }
2720
- locationIsVisitable(location) {
2721
- return location.isPrefixedBy(this.view.getRootLocation()) && location.isHTML();
2963
+ get targetElementsByQuery() {
2964
+ var _a;
2965
+ const elements = (_a = this.ownerDocument) === null || _a === void 0 ? void 0 : _a.querySelectorAll(this.targets);
2966
+ if (elements.length !== 0) {
2967
+ return Array.prototype.slice.call(elements);
2968
+ } else {
2969
+ return [];
2970
+ }
2722
2971
  }
2723
2972
  }
2724
2973
 
2974
+ FrameElement.delegateConstructor = FrameController;
2975
+
2976
+ customElements.define("turbo-frame", FrameElement);
2977
+
2978
+ customElements.define("turbo-stream", StreamElement);
2979
+
2980
+ (() => {
2981
+ let element = document.currentScript;
2982
+ if (!element) return;
2983
+ if (element.hasAttribute("data-turbo-suppress-warning")) return;
2984
+ while (element = element.parentElement) {
2985
+ if (element == document.body) {
2986
+ return console.warn(unindent`
2987
+ You are loading Turbo from a <script> element inside the <body> element. This is probably not what you meant to do!
2988
+
2989
+ Load your application’s JavaScript bundle inside the <head> element instead. <script> elements in <body> are evaluated with each page change.
2990
+
2991
+ For more information, see: https://turbo.hotwire.dev/handbook/building#working-with-script-elements
2992
+
2993
+ ——
2994
+ Suppress this warning by adding a "data-turbo-suppress-warning" attribute to: %s
2995
+ `, element.outerHTML);
2996
+ }
2997
+ }
2998
+ })();
2999
+
2725
3000
  const session = new Session;
2726
3001
 
2727
3002
  const {navigator: navigator} = session;
@@ -2758,6 +3033,21 @@ function setProgressBarDelay(delay) {
2758
3033
  session.setProgressBarDelay(delay);
2759
3034
  }
2760
3035
 
3036
+ var Turbo = Object.freeze({
3037
+ __proto__: null,
3038
+ navigator: navigator,
3039
+ start: start,
3040
+ registerAdapter: registerAdapter,
3041
+ visit: visit,
3042
+ connectStreamSource: connectStreamSource,
3043
+ disconnectStreamSource: disconnectStreamSource,
3044
+ renderStreamMessage: renderStreamMessage,
3045
+ clearCache: clearCache,
3046
+ setProgressBarDelay: setProgressBarDelay
3047
+ });
3048
+
3049
+ window.Turbo = Turbo;
3050
+
2761
3051
  start();
2762
3052
 
2763
3053
  var turbo_es2017Esm = Object.freeze({
@@ -2776,17 +3066,20 @@ var turbo_es2017Esm = Object.freeze({
2776
3066
  let consumer;
2777
3067
 
2778
3068
  async function getConsumer() {
2779
- if (consumer) return consumer;
2780
- const {createConsumer: createConsumer} = await Promise.resolve().then((function() {
2781
- return index;
2782
- }));
2783
- return setConsumer(createConsumer());
3069
+ return consumer || setConsumer(createConsumer().then(setConsumer));
2784
3070
  }
2785
3071
 
2786
3072
  function setConsumer(newConsumer) {
2787
3073
  return consumer = newConsumer;
2788
3074
  }
2789
3075
 
3076
+ async function createConsumer() {
3077
+ const {createConsumer: createConsumer} = await Promise.resolve().then((function() {
3078
+ return index;
3079
+ }));
3080
+ return createConsumer();
3081
+ }
3082
+
2790
3083
  async function subscribeTo(channel, mixin) {
2791
3084
  const {subscriptions: subscriptions} = await getConsumer();
2792
3085
  return subscriptions.create(channel, mixin);
@@ -2796,6 +3089,7 @@ var cable = Object.freeze({
2796
3089
  __proto__: null,
2797
3090
  getConsumer: getConsumer,
2798
3091
  setConsumer: setConsumer,
3092
+ createConsumer: createConsumer,
2799
3093
  subscribeTo: subscribeTo
2800
3094
  });
2801
3095
 
@@ -3259,7 +3553,7 @@ function createWebSocketURL(url) {
3259
3553
  }
3260
3554
  }
3261
3555
 
3262
- function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) {
3556
+ function createConsumer$1(url = getConfig("url") || INTERNAL.default_mount_path) {
3263
3557
  return new Consumer(url);
3264
3558
  }
3265
3559
 
@@ -3281,7 +3575,7 @@ var index = Object.freeze({
3281
3575
  adapters: adapters,
3282
3576
  createWebSocketURL: createWebSocketURL,
3283
3577
  logger: logger,
3284
- createConsumer: createConsumer,
3578
+ createConsumer: createConsumer$1,
3285
3579
  getConfig: getConfig
3286
3580
  });
3287
3581