turbo-rails 0.7.11 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -14,6 +14,31 @@
14
14
  Object.setPrototypeOf(HTMLElement, BuiltInHTMLElement);
15
15
  })();
16
16
 
17
+ (function(prototype) {
18
+ if (typeof prototype.requestSubmit == "function") return;
19
+ prototype.requestSubmit = function(submitter) {
20
+ if (submitter) {
21
+ validateSubmitter(submitter, this);
22
+ submitter.click();
23
+ } else {
24
+ submitter = document.createElement("input");
25
+ submitter.type = "submit";
26
+ submitter.hidden = true;
27
+ this.appendChild(submitter);
28
+ submitter.click();
29
+ this.removeChild(submitter);
30
+ }
31
+ };
32
+ function validateSubmitter(submitter, form) {
33
+ submitter instanceof HTMLElement || raise(TypeError, "parameter 1 is not of type 'HTMLElement'");
34
+ submitter.type == "submit" || raise(TypeError, "The specified element is not a submit button");
35
+ submitter.form == form || raise(DOMException, "The specified element is not owned by this form element", "NotFoundError");
36
+ }
37
+ function raise(errorConstructor, message, name) {
38
+ throw new errorConstructor("Failed to execute 'requestSubmit' on 'HTMLFormElement': " + message + ".", name);
39
+ }
40
+ })(HTMLFormElement.prototype);
41
+
17
42
  const submittersByForm = new WeakMap;
18
43
 
19
44
  function findSubmitterFromClickTarget(target) {
@@ -30,9 +55,17 @@ function clickCaptured(event) {
30
55
  }
31
56
 
32
57
  (function() {
33
- if ("SubmitEvent" in window) return;
58
+ if ("submitter" in Event.prototype) return;
59
+ let prototype;
60
+ if ("SubmitEvent" in window && /Apple Computer/.test(navigator.vendor)) {
61
+ prototype = window.SubmitEvent.prototype;
62
+ } else if ("SubmitEvent" in window) {
63
+ return;
64
+ } else {
65
+ prototype = window.Event.prototype;
66
+ }
34
67
  addEventListener("click", clickCaptured, true);
35
- Object.defineProperty(Event.prototype, "submitter", {
68
+ Object.defineProperty(prototype, "submitter", {
36
69
  get() {
37
70
  if (this.type == "submit" && this.target instanceof HTMLFormElement) {
38
71
  return submittersByForm.get(this.target);
@@ -55,7 +88,7 @@ class FrameElement extends HTMLElement {
55
88
  this.delegate = new FrameElement.delegateConstructor(this);
56
89
  }
57
90
  static get observedAttributes() {
58
- return [ "disabled", "loading", "src" ];
91
+ return [ "disabled", "complete", "loading", "src" ];
59
92
  }
60
93
  connectedCallback() {
61
94
  this.delegate.connect();
@@ -65,12 +98,16 @@ class FrameElement extends HTMLElement {
65
98
  }
66
99
  reload() {
67
100
  const {src: src} = this;
101
+ this.removeAttribute("complete");
68
102
  this.src = null;
69
103
  this.src = src;
104
+ return this.loaded;
70
105
  }
71
106
  attributeChangedCallback(name) {
72
107
  if (name == "loading") {
73
108
  this.delegate.loadingStyleChanged();
109
+ } else if (name == "complete") {
110
+ this.delegate.completeChanged();
74
111
  } else if (name == "src") {
75
112
  this.delegate.sourceURLChanged();
76
113
  } else {
@@ -152,12 +189,17 @@ function getAnchor(url) {
152
189
  }
153
190
  }
154
191
 
192
+ function getAction(form, submitter) {
193
+ const action = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formaction")) || form.getAttribute("action") || form.action;
194
+ return expandURL(action);
195
+ }
196
+
155
197
  function getExtension(url) {
156
198
  return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || "";
157
199
  }
158
200
 
159
201
  function isHTML(url) {
160
- return !!getExtension(url).match(/^(?:|\.(?:htm|html|xhtml))$/);
202
+ return !!getExtension(url).match(/^(?:|\.(?:htm|html|xhtml|php))$/);
161
203
  }
162
204
 
163
205
  function isPrefixedBy(baseURL, url) {
@@ -165,6 +207,10 @@ function isPrefixedBy(baseURL, url) {
165
207
  return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix);
166
208
  }
167
209
 
210
+ function locationIsVisitable(location, rootLocation) {
211
+ return isPrefixedBy(location, rootLocation) && isHTML(location);
212
+ }
213
+
168
214
  function getRequestURL(url) {
169
215
  const anchor = getAnchor(url);
170
216
  return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href;
@@ -226,11 +272,11 @@ class FetchResponse {
226
272
  return this.header("Content-Type");
227
273
  }
228
274
  get responseText() {
229
- return this.response.text();
275
+ return this.response.clone().text();
230
276
  }
231
277
  get responseHTML() {
232
278
  if (this.isHTML) {
233
- return this.response.text();
279
+ return this.response.clone().text();
234
280
  } else {
235
281
  return Promise.resolve(undefined);
236
282
  }
@@ -240,13 +286,49 @@ class FetchResponse {
240
286
  }
241
287
  }
242
288
 
289
+ function isAction(action) {
290
+ return action == "advance" || action == "replace" || action == "restore";
291
+ }
292
+
293
+ function activateScriptElement(element) {
294
+ if (element.getAttribute("data-turbo-eval") == "false") {
295
+ return element;
296
+ } else {
297
+ const createdScriptElement = document.createElement("script");
298
+ const cspNonce = getMetaContent("csp-nonce");
299
+ if (cspNonce) {
300
+ createdScriptElement.nonce = cspNonce;
301
+ }
302
+ createdScriptElement.textContent = element.textContent;
303
+ createdScriptElement.async = false;
304
+ copyElementAttributes(createdScriptElement, element);
305
+ return createdScriptElement;
306
+ }
307
+ }
308
+
309
+ function copyElementAttributes(destinationElement, sourceElement) {
310
+ for (const {name: name, value: value} of sourceElement.attributes) {
311
+ destinationElement.setAttribute(name, value);
312
+ }
313
+ }
314
+
315
+ function createDocumentFragment(html) {
316
+ const template = document.createElement("template");
317
+ template.innerHTML = html;
318
+ return template.content;
319
+ }
320
+
243
321
  function dispatch(eventName, {target: target, cancelable: cancelable, detail: detail} = {}) {
244
322
  const event = new CustomEvent(eventName, {
245
323
  cancelable: cancelable,
246
324
  bubbles: true,
247
325
  detail: detail
248
326
  });
249
- void (target || document.documentElement).dispatchEvent(event);
327
+ if (target && target.isConnected) {
328
+ target.dispatchEvent(event);
329
+ } else {
330
+ document.documentElement.dispatchEvent(event);
331
+ }
250
332
  return event;
251
333
  }
252
334
 
@@ -281,7 +363,7 @@ function interpolate(strings, values) {
281
363
  }
282
364
 
283
365
  function uuid() {
284
- return Array.apply(null, {
366
+ return Array.from({
285
367
  length: 36
286
368
  }).map(((_, i) => {
287
369
  if (i == 8 || i == 13 || i == 18 || i == 23) {
@@ -296,6 +378,88 @@ function uuid() {
296
378
  })).join("");
297
379
  }
298
380
 
381
+ function getAttribute(attributeName, ...elements) {
382
+ for (const value of elements.map((element => element === null || element === void 0 ? void 0 : element.getAttribute(attributeName)))) {
383
+ if (typeof value == "string") return value;
384
+ }
385
+ return null;
386
+ }
387
+
388
+ function hasAttribute(attributeName, ...elements) {
389
+ return elements.some((element => element && element.hasAttribute(attributeName)));
390
+ }
391
+
392
+ function markAsBusy(...elements) {
393
+ for (const element of elements) {
394
+ if (element.localName == "turbo-frame") {
395
+ element.setAttribute("busy", "");
396
+ }
397
+ element.setAttribute("aria-busy", "true");
398
+ }
399
+ }
400
+
401
+ function clearBusyState(...elements) {
402
+ for (const element of elements) {
403
+ if (element.localName == "turbo-frame") {
404
+ element.removeAttribute("busy");
405
+ }
406
+ element.removeAttribute("aria-busy");
407
+ }
408
+ }
409
+
410
+ function waitForLoad(element, timeoutInMilliseconds = 2e3) {
411
+ return new Promise((resolve => {
412
+ const onComplete = () => {
413
+ element.removeEventListener("error", onComplete);
414
+ element.removeEventListener("load", onComplete);
415
+ resolve();
416
+ };
417
+ element.addEventListener("load", onComplete, {
418
+ once: true
419
+ });
420
+ element.addEventListener("error", onComplete, {
421
+ once: true
422
+ });
423
+ setTimeout(resolve, timeoutInMilliseconds);
424
+ }));
425
+ }
426
+
427
+ function getHistoryMethodForAction(action) {
428
+ switch (action) {
429
+ case "replace":
430
+ return history.replaceState;
431
+
432
+ case "advance":
433
+ case "restore":
434
+ return history.pushState;
435
+ }
436
+ }
437
+
438
+ function getVisitAction(...elements) {
439
+ const action = getAttribute("data-turbo-action", ...elements);
440
+ return isAction(action) ? action : null;
441
+ }
442
+
443
+ function getMetaElement(name) {
444
+ return document.querySelector(`meta[name="${name}"]`);
445
+ }
446
+
447
+ function getMetaContent(name) {
448
+ const element = getMetaElement(name);
449
+ return element && element.content;
450
+ }
451
+
452
+ function setMetaContent(name, content) {
453
+ let element = getMetaElement(name);
454
+ if (!element) {
455
+ element = document.createElement("meta");
456
+ element.setAttribute("name", name);
457
+ document.head.appendChild(element);
458
+ }
459
+ element.setAttribute("content", content);
460
+ return element;
461
+ }
462
+
299
463
  var FetchMethod;
300
464
 
301
465
  (function(FetchMethod) {
@@ -326,18 +490,15 @@ function fetchMethodFromString(method) {
326
490
  }
327
491
 
328
492
  class FetchRequest {
329
- constructor(delegate, method, location, body = new URLSearchParams) {
493
+ constructor(delegate, method, location, body = new URLSearchParams, target = null) {
330
494
  this.abortController = new AbortController;
331
- this.resolveRequestPromise = value => {};
495
+ this.resolveRequestPromise = _value => {};
332
496
  this.delegate = delegate;
333
497
  this.method = method;
334
498
  this.headers = this.defaultHeaders;
335
- if (this.isIdempotent) {
336
- this.url = mergeFormDataEntries(location, [ ...body.entries() ]);
337
- } else {
338
- this.body = body;
339
- this.url = location;
340
- }
499
+ this.body = body;
500
+ this.url = location;
501
+ this.target = target;
341
502
  }
342
503
  get location() {
343
504
  return this.url;
@@ -362,7 +523,9 @@ class FetchRequest {
362
523
  return await this.receive(response);
363
524
  } catch (error) {
364
525
  if (error.name !== "AbortError") {
365
- this.delegate.requestErrored(this, error);
526
+ if (this.willDelegateErrorHandling(error)) {
527
+ this.delegate.requestErrored(this, error);
528
+ }
366
529
  throw error;
367
530
  }
368
531
  } finally {
@@ -375,7 +538,8 @@ class FetchRequest {
375
538
  cancelable: true,
376
539
  detail: {
377
540
  fetchResponse: fetchResponse
378
- }
541
+ },
542
+ target: this.target
379
543
  });
380
544
  if (event.defaultPrevented) {
381
545
  this.delegate.requestPreventedHandlingResponse(this, fetchResponse);
@@ -393,7 +557,7 @@ class FetchRequest {
393
557
  credentials: "same-origin",
394
558
  headers: this.headers,
395
559
  redirect: "follow",
396
- body: this.body,
560
+ body: this.isIdempotent ? null : this.body,
397
561
  signal: this.abortSignal,
398
562
  referrer: (_a = this.delegate.referrer) === null || _a === void 0 ? void 0 : _a.href
399
563
  };
@@ -409,32 +573,33 @@ class FetchRequest {
409
573
  get abortSignal() {
410
574
  return this.abortController.signal;
411
575
  }
576
+ acceptResponseType(mimeType) {
577
+ this.headers["Accept"] = [ mimeType, this.headers["Accept"] ].join(", ");
578
+ }
412
579
  async allowRequestToBeIntercepted(fetchOptions) {
413
580
  const requestInterception = new Promise((resolve => this.resolveRequestPromise = resolve));
414
581
  const event = dispatch("turbo:before-fetch-request", {
415
582
  cancelable: true,
416
583
  detail: {
417
584
  fetchOptions: fetchOptions,
418
- url: this.url.href,
585
+ url: this.url,
419
586
  resume: this.resolveRequestPromise
420
- }
587
+ },
588
+ target: this.target
421
589
  });
422
590
  if (event.defaultPrevented) await requestInterception;
423
591
  }
424
- }
425
-
426
- function mergeFormDataEntries(url, entries) {
427
- const currentSearchParams = new URLSearchParams(url.search);
428
- for (const [name, value] of entries) {
429
- if (value instanceof File) continue;
430
- if (currentSearchParams.has(name)) {
431
- currentSearchParams.delete(name);
432
- url.searchParams.set(name, value);
433
- } else {
434
- url.searchParams.append(name, value);
435
- }
592
+ willDelegateErrorHandling(error) {
593
+ const event = dispatch("turbo:fetch-request-error", {
594
+ target: this.target,
595
+ cancelable: true,
596
+ detail: {
597
+ request: this,
598
+ error: error
599
+ }
600
+ });
601
+ return !event.defaultPrevented;
436
602
  }
437
- return url;
438
603
  }
439
604
 
440
605
  class AppearanceObserver {
@@ -465,40 +630,31 @@ class AppearanceObserver {
465
630
  }
466
631
 
467
632
  class StreamMessage {
468
- constructor(html) {
469
- this.templateElement = document.createElement("template");
470
- this.templateElement.innerHTML = html;
633
+ constructor(fragment) {
634
+ this.fragment = importStreamElements(fragment);
471
635
  }
472
636
  static wrap(message) {
473
637
  if (typeof message == "string") {
474
- return new this(message);
638
+ return new this(createDocumentFragment(message));
475
639
  } else {
476
640
  return message;
477
641
  }
478
642
  }
479
- get fragment() {
480
- const fragment = document.createDocumentFragment();
481
- for (const element of this.foreignElements) {
482
- fragment.appendChild(document.importNode(element, true));
483
- }
484
- return fragment;
485
- }
486
- get foreignElements() {
487
- return this.templateChildren.reduce(((streamElements, child) => {
488
- if (child.tagName.toLowerCase() == "turbo-stream") {
489
- return [ ...streamElements, child ];
490
- } else {
491
- return streamElements;
492
- }
493
- }), []);
494
- }
495
- get templateChildren() {
496
- return Array.from(this.templateElement.content.children);
497
- }
498
643
  }
499
644
 
500
645
  StreamMessage.contentType = "text/vnd.turbo-stream.html";
501
646
 
647
+ function importStreamElements(fragment) {
648
+ for (const element of fragment.querySelectorAll("turbo-stream")) {
649
+ const streamElement = document.importNode(element, true);
650
+ for (const inertScriptElement of streamElement.templateElement.content.querySelectorAll("script")) {
651
+ inertScriptElement.replaceWith(activateScriptElement(inertScriptElement));
652
+ }
653
+ element.replaceWith(streamElement);
654
+ }
655
+ return fragment;
656
+ }
657
+
502
658
  var FormSubmissionState;
503
659
 
504
660
  (function(FormSubmissionState) {
@@ -538,9 +694,16 @@ class FormSubmission {
538
694
  this.formElement = formElement;
539
695
  this.submitter = submitter;
540
696
  this.formData = buildFormData(formElement, submitter);
541
- this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body);
697
+ this.location = expandURL(this.action);
698
+ if (this.method == FetchMethod.get) {
699
+ mergeFormDataEntries(this.location, [ ...this.body.entries() ]);
700
+ }
701
+ this.fetchRequest = new FetchRequest(this, this.method, this.location, this.body, this.formElement);
542
702
  this.mustRedirect = mustRedirect;
543
703
  }
704
+ static confirmMethod(message, _element, _submitter) {
705
+ return Promise.resolve(confirm(message));
706
+ }
544
707
  get method() {
545
708
  var _a;
546
709
  const method = ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formmethod")) || this.formElement.getAttribute("method") || "";
@@ -549,10 +712,11 @@ class FormSubmission {
549
712
  get action() {
550
713
  var _a;
551
714
  const formElementAction = typeof this.formElement.action === "string" ? this.formElement.action : null;
552
- return ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formaction")) || this.formElement.getAttribute("action") || formElementAction || "";
553
- }
554
- get location() {
555
- return expandURL(this.action);
715
+ if ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.hasAttribute("formaction")) {
716
+ return this.submitter.getAttribute("formaction") || "";
717
+ } else {
718
+ return this.formElement.getAttribute("action") || formElementAction || "";
719
+ }
556
720
  }
557
721
  get body() {
558
722
  if (this.enctype == FormEnctype.urlEncoded || this.method == FetchMethod.get) {
@@ -573,6 +737,13 @@ class FormSubmission {
573
737
  }
574
738
  async start() {
575
739
  const {initialized: initialized, requesting: requesting} = FormSubmissionState;
740
+ const confirmationMessage = getAttribute("data-turbo-confirm", this.submitter, this.formElement);
741
+ if (typeof confirmationMessage === "string") {
742
+ const answer = await FormSubmission.confirmMethod(confirmationMessage, this.formElement, this.submitter);
743
+ if (!answer) {
744
+ return;
745
+ }
746
+ }
576
747
  if (this.state == initialized) {
577
748
  this.state = requesting;
578
749
  return this.fetchRequest.perform();
@@ -592,11 +763,15 @@ class FormSubmission {
592
763
  if (token) {
593
764
  headers["X-CSRF-Token"] = token;
594
765
  }
595
- headers["Accept"] = [ StreamMessage.contentType, headers["Accept"] ].join(", ");
766
+ }
767
+ if (this.requestAcceptsTurboStreamResponse(request)) {
768
+ request.acceptResponseType(StreamMessage.contentType);
596
769
  }
597
770
  }
598
- requestStarted(request) {
771
+ requestStarted(_request) {
772
+ var _a;
599
773
  this.state = FormSubmissionState.waiting;
774
+ (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.setAttribute("disabled", "");
600
775
  dispatch("turbo:submit-start", {
601
776
  target: this.formElement,
602
777
  detail: {
@@ -640,8 +815,10 @@ class FormSubmission {
640
815
  };
641
816
  this.delegate.formSubmissionErrored(this, error);
642
817
  }
643
- requestFinished(request) {
818
+ requestFinished(_request) {
819
+ var _a;
644
820
  this.state = FormSubmissionState.stopped;
821
+ (_a = this.submitter) === null || _a === void 0 ? void 0 : _a.removeAttribute("disabled");
645
822
  dispatch("turbo:submit-end", {
646
823
  target: this.formElement,
647
824
  detail: Object.assign({
@@ -653,14 +830,17 @@ class FormSubmission {
653
830
  requestMustRedirect(request) {
654
831
  return !request.isIdempotent && this.mustRedirect;
655
832
  }
833
+ requestAcceptsTurboStreamResponse(request) {
834
+ return !request.isIdempotent || hasAttribute("data-turbo-stream", this.submitter, this.formElement);
835
+ }
656
836
  }
657
837
 
658
838
  function buildFormData(formElement, submitter) {
659
839
  const formData = new FormData(formElement);
660
840
  const name = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("name");
661
841
  const value = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("value");
662
- if (name && value != null && formData.get(name) != value) {
663
- formData.append(name, value);
842
+ if (name) {
843
+ formData.append(name, value || "");
664
844
  }
665
845
  return formData;
666
846
  }
@@ -676,19 +856,27 @@ function getCookieValue(cookieName) {
676
856
  }
677
857
  }
678
858
 
679
- function getMetaContent(name) {
680
- const element = document.querySelector(`meta[name="${name}"]`);
681
- return element && element.content;
682
- }
683
-
684
859
  function responseSucceededWithoutRedirect(response) {
685
860
  return response.statusCode == 200 && !response.redirected;
686
861
  }
687
862
 
863
+ function mergeFormDataEntries(url, entries) {
864
+ const searchParams = new URLSearchParams;
865
+ for (const [name, value] of entries) {
866
+ if (value instanceof File) continue;
867
+ searchParams.append(name, value);
868
+ }
869
+ url.search = searchParams.toString();
870
+ return url;
871
+ }
872
+
688
873
  class Snapshot {
689
874
  constructor(element) {
690
875
  this.element = element;
691
876
  }
877
+ get activeElement() {
878
+ return this.element.ownerDocument.activeElement;
879
+ }
692
880
  get children() {
693
881
  return [ ...this.element.children ];
694
882
  }
@@ -702,13 +890,17 @@ class Snapshot {
702
890
  return this.element.isConnected;
703
891
  }
704
892
  get firstAutofocusableElement() {
705
- return this.element.querySelector("[autofocus]");
893
+ const inertDisabledOrHidden = "[inert], :disabled, [hidden], details:not([open]), dialog:not([open])";
894
+ for (const element of this.element.querySelectorAll("[autofocus]")) {
895
+ if (element.closest(inertDisabledOrHidden) == null) return element; else continue;
896
+ }
897
+ return null;
706
898
  }
707
899
  get permanentElements() {
708
- return [ ...this.element.querySelectorAll("[id][data-turbo-permanent]") ];
900
+ return queryPermanentElementsAll(this.element);
709
901
  }
710
902
  getPermanentElementById(id) {
711
- return this.element.querySelector(`#${id}[data-turbo-permanent]`);
903
+ return getPermanentElementById(this.element, id);
712
904
  }
713
905
  getPermanentElementMapForSnapshot(snapshot) {
714
906
  const permanentElementMap = {};
@@ -723,34 +915,65 @@ class Snapshot {
723
915
  }
724
916
  }
725
917
 
726
- class FormInterceptor {
727
- constructor(delegate, element) {
918
+ function getPermanentElementById(node, id) {
919
+ return node.querySelector(`#${id}[data-turbo-permanent]`);
920
+ }
921
+
922
+ function queryPermanentElementsAll(node) {
923
+ return node.querySelectorAll("[id][data-turbo-permanent]");
924
+ }
925
+
926
+ class FormSubmitObserver {
927
+ constructor(delegate, eventTarget) {
928
+ this.started = false;
929
+ this.submitCaptured = () => {
930
+ this.eventTarget.removeEventListener("submit", this.submitBubbled, false);
931
+ this.eventTarget.addEventListener("submit", this.submitBubbled, false);
932
+ };
728
933
  this.submitBubbled = event => {
729
- if (event.target instanceof HTMLFormElement) {
730
- const form = event.target;
934
+ if (!event.defaultPrevented) {
935
+ const form = event.target instanceof HTMLFormElement ? event.target : undefined;
731
936
  const submitter = event.submitter || undefined;
732
- if (this.delegate.shouldInterceptFormSubmission(form, submitter)) {
937
+ if (form && submissionDoesNotDismissDialog(form, submitter) && submissionDoesNotTargetIFrame(form, submitter) && this.delegate.willSubmitForm(form, submitter)) {
733
938
  event.preventDefault();
734
- event.stopImmediatePropagation();
735
- this.delegate.formSubmissionIntercepted(form, submitter);
939
+ this.delegate.formSubmitted(form, submitter);
736
940
  }
737
941
  }
738
942
  };
739
943
  this.delegate = delegate;
740
- this.element = element;
944
+ this.eventTarget = eventTarget;
741
945
  }
742
946
  start() {
743
- this.element.addEventListener("submit", this.submitBubbled);
947
+ if (!this.started) {
948
+ this.eventTarget.addEventListener("submit", this.submitCaptured, true);
949
+ this.started = true;
950
+ }
744
951
  }
745
952
  stop() {
746
- this.element.removeEventListener("submit", this.submitBubbled);
953
+ if (this.started) {
954
+ this.eventTarget.removeEventListener("submit", this.submitCaptured, true);
955
+ this.started = false;
956
+ }
957
+ }
958
+ }
959
+
960
+ function submissionDoesNotDismissDialog(form, submitter) {
961
+ const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.getAttribute("method");
962
+ return method != "dialog";
963
+ }
964
+
965
+ function submissionDoesNotTargetIFrame(form, submitter) {
966
+ const target = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formtarget")) || form.target;
967
+ for (const element of document.getElementsByName(target)) {
968
+ if (element instanceof HTMLIFrameElement) return false;
747
969
  }
970
+ return true;
748
971
  }
749
972
 
750
973
  class View {
751
974
  constructor(delegate, element) {
752
- this.resolveRenderPromise = value => {};
753
- this.resolveInterceptionPromise = value => {};
975
+ this.resolveRenderPromise = _value => {};
976
+ this.resolveInterceptionPromise = _value => {};
754
977
  this.delegate = delegate;
755
978
  this.element = element;
756
979
  }
@@ -801,12 +1024,17 @@ class View {
801
1024
  try {
802
1025
  this.renderPromise = new Promise((resolve => this.resolveRenderPromise = resolve));
803
1026
  this.renderer = renderer;
804
- this.prepareToRenderSnapshot(renderer);
1027
+ await this.prepareToRenderSnapshot(renderer);
805
1028
  const renderInterception = new Promise((resolve => this.resolveInterceptionPromise = resolve));
806
- const immediateRender = this.delegate.allowsImmediateRender(snapshot, this.resolveInterceptionPromise);
1029
+ const options = {
1030
+ resume: this.resolveInterceptionPromise,
1031
+ render: this.renderer.renderElement
1032
+ };
1033
+ const immediateRender = this.delegate.allowsImmediateRender(snapshot, options);
807
1034
  if (!immediateRender) await renderInterception;
808
1035
  await this.renderSnapshot(renderer);
809
1036
  this.delegate.viewRenderedSnapshot(snapshot, isPreview);
1037
+ this.delegate.preloadOnLoadLinksForView(this.element);
810
1038
  this.finishRenderingSnapshot(renderer);
811
1039
  } finally {
812
1040
  delete this.renderer;
@@ -814,15 +1042,15 @@ class View {
814
1042
  delete this.renderPromise;
815
1043
  }
816
1044
  } else {
817
- this.invalidate();
1045
+ this.invalidate(renderer.reloadReason);
818
1046
  }
819
1047
  }
820
- invalidate() {
821
- this.delegate.viewInvalidated();
1048
+ invalidate(reason) {
1049
+ this.delegate.viewInvalidated(reason);
822
1050
  }
823
- prepareToRenderSnapshot(renderer) {
1051
+ async prepareToRenderSnapshot(renderer) {
824
1052
  this.markAsPreview(renderer.isPreview);
825
- renderer.prepareToRender();
1053
+ await renderer.prepareToRender();
826
1054
  }
827
1055
  markAsPreview(isPreview) {
828
1056
  if (isPreview) {
@@ -848,60 +1076,115 @@ class FrameView extends View {
848
1076
  }
849
1077
  }
850
1078
 
851
- class LinkInterceptor {
852
- constructor(delegate, element) {
853
- this.clickBubbled = event => {
854
- if (this.respondsToEventTarget(event.target)) {
855
- this.clickEvent = event;
856
- } else {
857
- delete this.clickEvent;
858
- }
1079
+ class LinkClickObserver {
1080
+ constructor(delegate, eventTarget) {
1081
+ this.started = false;
1082
+ this.clickCaptured = () => {
1083
+ this.eventTarget.removeEventListener("click", this.clickBubbled, false);
1084
+ this.eventTarget.addEventListener("click", this.clickBubbled, false);
859
1085
  };
860
- this.linkClicked = event => {
861
- if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) {
862
- if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url)) {
863
- this.clickEvent.preventDefault();
864
- event.preventDefault();
865
- this.delegate.linkClickIntercepted(event.target, event.detail.url);
1086
+ this.clickBubbled = event => {
1087
+ if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {
1088
+ const target = event.composedPath && event.composedPath()[0] || event.target;
1089
+ const link = this.findLinkFromClickTarget(target);
1090
+ if (link && doesNotTargetIFrame(link)) {
1091
+ const location = this.getLocationForLink(link);
1092
+ if (this.delegate.willFollowLinkToLocation(link, location, event)) {
1093
+ event.preventDefault();
1094
+ this.delegate.followedLinkToLocation(link, location);
1095
+ }
866
1096
  }
867
1097
  }
868
- delete this.clickEvent;
869
- };
870
- this.willVisit = () => {
871
- delete this.clickEvent;
872
1098
  };
873
1099
  this.delegate = delegate;
874
- this.element = element;
1100
+ this.eventTarget = eventTarget;
1101
+ }
1102
+ start() {
1103
+ if (!this.started) {
1104
+ this.eventTarget.addEventListener("click", this.clickCaptured, true);
1105
+ this.started = true;
1106
+ }
1107
+ }
1108
+ stop() {
1109
+ if (this.started) {
1110
+ this.eventTarget.removeEventListener("click", this.clickCaptured, true);
1111
+ this.started = false;
1112
+ }
1113
+ }
1114
+ clickEventIsSignificant(event) {
1115
+ return !(event.target && event.target.isContentEditable || event.defaultPrevented || event.which > 1 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey);
1116
+ }
1117
+ findLinkFromClickTarget(target) {
1118
+ if (target instanceof Element) {
1119
+ return target.closest("a[href]:not([target^=_]):not([download])");
1120
+ }
1121
+ }
1122
+ getLocationForLink(link) {
1123
+ return expandURL(link.getAttribute("href") || "");
1124
+ }
1125
+ }
1126
+
1127
+ function doesNotTargetIFrame(anchor) {
1128
+ for (const element of document.getElementsByName(anchor.target)) {
1129
+ if (element instanceof HTMLIFrameElement) return false;
1130
+ }
1131
+ return true;
1132
+ }
1133
+
1134
+ class FormLinkClickObserver {
1135
+ constructor(delegate, element) {
1136
+ this.delegate = delegate;
1137
+ this.linkClickObserver = new LinkClickObserver(this, element);
875
1138
  }
876
1139
  start() {
877
- this.element.addEventListener("click", this.clickBubbled);
878
- document.addEventListener("turbo:click", this.linkClicked);
879
- document.addEventListener("turbo:before-visit", this.willVisit);
1140
+ this.linkClickObserver.start();
880
1141
  }
881
1142
  stop() {
882
- this.element.removeEventListener("click", this.clickBubbled);
883
- document.removeEventListener("turbo:click", this.linkClicked);
884
- document.removeEventListener("turbo:before-visit", this.willVisit);
1143
+ this.linkClickObserver.stop();
885
1144
  }
886
- respondsToEventTarget(target) {
887
- const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
888
- return element && element.closest("turbo-frame, html") == this.element;
1145
+ willFollowLinkToLocation(link, location, originalEvent) {
1146
+ return this.delegate.willSubmitFormLinkToLocation(link, location, originalEvent) && link.hasAttribute("data-turbo-method");
1147
+ }
1148
+ followedLinkToLocation(link, location) {
1149
+ const action = location.href;
1150
+ const form = document.createElement("form");
1151
+ form.setAttribute("data-turbo", "true");
1152
+ form.setAttribute("action", action);
1153
+ form.setAttribute("hidden", "");
1154
+ const method = link.getAttribute("data-turbo-method");
1155
+ if (method) form.setAttribute("method", method);
1156
+ const turboFrame = link.getAttribute("data-turbo-frame");
1157
+ if (turboFrame) form.setAttribute("data-turbo-frame", turboFrame);
1158
+ const turboAction = link.getAttribute("data-turbo-action");
1159
+ if (turboAction) form.setAttribute("data-turbo-action", turboAction);
1160
+ const turboConfirm = link.getAttribute("data-turbo-confirm");
1161
+ if (turboConfirm) form.setAttribute("data-turbo-confirm", turboConfirm);
1162
+ const turboStream = link.hasAttribute("data-turbo-stream");
1163
+ if (turboStream) form.setAttribute("data-turbo-stream", "");
1164
+ this.delegate.submittedFormLinkToLocation(link, location, form);
1165
+ document.body.appendChild(form);
1166
+ form.addEventListener("turbo:submit-end", (() => form.remove()), {
1167
+ once: true
1168
+ });
1169
+ requestAnimationFrame((() => form.requestSubmit()));
889
1170
  }
890
1171
  }
891
1172
 
892
1173
  class Bardo {
893
- constructor(permanentElementMap) {
1174
+ constructor(delegate, permanentElementMap) {
1175
+ this.delegate = delegate;
894
1176
  this.permanentElementMap = permanentElementMap;
895
1177
  }
896
- static preservingPermanentElements(permanentElementMap, callback) {
897
- const bardo = new this(permanentElementMap);
1178
+ static preservingPermanentElements(delegate, permanentElementMap, callback) {
1179
+ const bardo = new this(delegate, permanentElementMap);
898
1180
  bardo.enter();
899
1181
  callback();
900
1182
  bardo.leave();
901
1183
  }
902
1184
  enter() {
903
1185
  for (const id in this.permanentElementMap) {
904
- const [, newPermanentElement] = this.permanentElementMap[id];
1186
+ const [currentPermanentElement, newPermanentElement] = this.permanentElementMap[id];
1187
+ this.delegate.enteringBardo(currentPermanentElement, newPermanentElement);
905
1188
  this.replaceNewPermanentElementWithPlaceholder(newPermanentElement);
906
1189
  }
907
1190
  }
@@ -910,6 +1193,7 @@ class Bardo {
910
1193
  const [currentPermanentElement] = this.permanentElementMap[id];
911
1194
  this.replaceCurrentPermanentElementWithClone(currentPermanentElement);
912
1195
  this.replacePlaceholderWithPermanentElement(currentPermanentElement);
1196
+ this.delegate.leavingBardo(currentPermanentElement);
913
1197
  }
914
1198
  }
915
1199
  replaceNewPermanentElementWithPlaceholder(permanentElement) {
@@ -940,10 +1224,13 @@ function createPlaceholderForPermanentElement(permanentElement) {
940
1224
  }
941
1225
 
942
1226
  class Renderer {
943
- constructor(currentSnapshot, newSnapshot, isPreview) {
1227
+ constructor(currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) {
1228
+ this.activeElement = null;
944
1229
  this.currentSnapshot = currentSnapshot;
945
1230
  this.newSnapshot = newSnapshot;
946
1231
  this.isPreview = isPreview;
1232
+ this.willRender = willRender;
1233
+ this.renderElement = renderElement;
947
1234
  this.promise = new Promise(((resolve, reject) => this.resolvingFunctions = {
948
1235
  resolve: resolve,
949
1236
  reject: reject
@@ -952,6 +1239,9 @@ class Renderer {
952
1239
  get shouldRender() {
953
1240
  return true;
954
1241
  }
1242
+ get reloadReason() {
1243
+ return;
1244
+ }
955
1245
  prepareToRender() {
956
1246
  return;
957
1247
  }
@@ -961,22 +1251,8 @@ class Renderer {
961
1251
  delete this.resolvingFunctions;
962
1252
  }
963
1253
  }
964
- createScriptElement(element) {
965
- if (element.getAttribute("data-turbo-eval") == "false") {
966
- return element;
967
- } else {
968
- const createdScriptElement = document.createElement("script");
969
- if (this.cspNonce) {
970
- createdScriptElement.nonce = this.cspNonce;
971
- }
972
- createdScriptElement.textContent = element.textContent;
973
- createdScriptElement.async = false;
974
- copyElementAttributes(createdScriptElement, element);
975
- return createdScriptElement;
976
- }
977
- }
978
1254
  preservingPermanentElements(callback) {
979
- Bardo.preservingPermanentElements(this.permanentElementMap, callback);
1255
+ Bardo.preservingPermanentElements(this, this.permanentElementMap, callback);
980
1256
  }
981
1257
  focusFirstAutofocusableElement() {
982
1258
  const element = this.connectedSnapshot.firstAutofocusableElement;
@@ -984,6 +1260,18 @@ class Renderer {
984
1260
  element.focus();
985
1261
  }
986
1262
  }
1263
+ enteringBardo(currentPermanentElement) {
1264
+ if (this.activeElement) return;
1265
+ if (currentPermanentElement.contains(this.currentSnapshot.activeElement)) {
1266
+ this.activeElement = this.currentSnapshot.activeElement;
1267
+ }
1268
+ }
1269
+ leavingBardo(currentPermanentElement) {
1270
+ if (currentPermanentElement.contains(this.activeElement) && this.activeElement instanceof HTMLElement) {
1271
+ this.activeElement.focus();
1272
+ this.activeElement = null;
1273
+ }
1274
+ }
987
1275
  get connectedSnapshot() {
988
1276
  return this.newSnapshot.isConnected ? this.newSnapshot : this.currentSnapshot;
989
1277
  }
@@ -996,16 +1284,6 @@ class Renderer {
996
1284
  get permanentElementMap() {
997
1285
  return this.currentSnapshot.getPermanentElementMapForSnapshot(this.newSnapshot);
998
1286
  }
999
- get cspNonce() {
1000
- var _a;
1001
- return (_a = document.head.querySelector('meta[name="csp-nonce"]')) === null || _a === void 0 ? void 0 : _a.getAttribute("content");
1002
- }
1003
- }
1004
-
1005
- function copyElementAttributes(destinationElement, sourceElement) {
1006
- for (const {name: name, value: value} of [ ...sourceElement.attributes ]) {
1007
- destinationElement.setAttribute(name, value);
1008
- }
1009
1287
  }
1010
1288
 
1011
1289
  function elementIsFocusable(element) {
@@ -1013,6 +1291,22 @@ function elementIsFocusable(element) {
1013
1291
  }
1014
1292
 
1015
1293
  class FrameRenderer extends Renderer {
1294
+ constructor(delegate, currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) {
1295
+ super(currentSnapshot, newSnapshot, renderElement, isPreview, willRender);
1296
+ this.delegate = delegate;
1297
+ }
1298
+ static renderElement(currentElement, newElement) {
1299
+ var _a;
1300
+ const destinationRange = document.createRange();
1301
+ destinationRange.selectNodeContents(currentElement);
1302
+ destinationRange.deleteContents();
1303
+ const frameElement = newElement;
1304
+ const sourceRange = (_a = frameElement.ownerDocument) === null || _a === void 0 ? void 0 : _a.createRange();
1305
+ if (sourceRange) {
1306
+ sourceRange.selectNodeContents(frameElement);
1307
+ currentElement.appendChild(sourceRange.extractContents());
1308
+ }
1309
+ }
1016
1310
  get shouldRender() {
1017
1311
  return true;
1018
1312
  }
@@ -1028,24 +1322,18 @@ class FrameRenderer extends Renderer {
1028
1322
  this.activateScriptElements();
1029
1323
  }
1030
1324
  loadFrameElement() {
1031
- var _a;
1032
- const destinationRange = document.createRange();
1033
- destinationRange.selectNodeContents(this.currentElement);
1034
- destinationRange.deleteContents();
1035
- const frameElement = this.newElement;
1036
- const sourceRange = (_a = frameElement.ownerDocument) === null || _a === void 0 ? void 0 : _a.createRange();
1037
- if (sourceRange) {
1038
- sourceRange.selectNodeContents(frameElement);
1039
- this.currentElement.appendChild(sourceRange.extractContents());
1040
- }
1325
+ this.delegate.willRenderFrame(this.currentElement, this.newElement);
1326
+ this.renderElement(this.currentElement, this.newElement);
1041
1327
  }
1042
1328
  scrollFrameIntoView() {
1043
1329
  if (this.currentElement.autoscroll || this.newElement.autoscroll) {
1044
1330
  const element = this.currentElement.firstElementChild;
1045
1331
  const block = readScrollLogicalPosition(this.currentElement.getAttribute("data-autoscroll-block"), "end");
1332
+ const behavior = readScrollBehavior(this.currentElement.getAttribute("data-autoscroll-behavior"), "auto");
1046
1333
  if (element) {
1047
1334
  element.scrollIntoView({
1048
- block: block
1335
+ block: block,
1336
+ behavior: behavior
1049
1337
  });
1050
1338
  return true;
1051
1339
  }
@@ -1054,7 +1342,7 @@ class FrameRenderer extends Renderer {
1054
1342
  }
1055
1343
  activateScriptElements() {
1056
1344
  for (const inertScriptElement of this.newScriptElements) {
1057
- const activatedScriptElement = this.createScriptElement(inertScriptElement);
1345
+ const activatedScriptElement = activateScriptElement(inertScriptElement);
1058
1346
  inertScriptElement.replaceWith(activatedScriptElement);
1059
1347
  }
1060
1348
  }
@@ -1071,6 +1359,14 @@ function readScrollLogicalPosition(value, defaultValue) {
1071
1359
  }
1072
1360
  }
1073
1361
 
1362
+ function readScrollBehavior(value, defaultValue) {
1363
+ if (value == "auto" || value == "smooth") {
1364
+ return value;
1365
+ } else {
1366
+ return defaultValue;
1367
+ }
1368
+ }
1369
+
1074
1370
  class ProgressBar {
1075
1371
  constructor() {
1076
1372
  this.hiding = false;
@@ -1093,7 +1389,7 @@ class ProgressBar {
1093
1389
  left: 0;
1094
1390
  height: 3px;
1095
1391
  background: #0076ff;
1096
- z-index: 9999;
1392
+ z-index: 2147483647;
1097
1393
  transition:
1098
1394
  width ${ProgressBar.animationDuration}ms ease-out,
1099
1395
  opacity ${ProgressBar.animationDuration / 2}ms ${ProgressBar.animationDuration / 2}ms ease-in;
@@ -1159,6 +1455,9 @@ class ProgressBar {
1159
1455
  const element = document.createElement("style");
1160
1456
  element.type = "text/css";
1161
1457
  element.textContent = ProgressBar.defaultCSS;
1458
+ if (this.cspNonce) {
1459
+ element.nonce = this.cspNonce;
1460
+ }
1162
1461
  return element;
1163
1462
  }
1164
1463
  createProgressElement() {
@@ -1166,6 +1465,9 @@ class ProgressBar {
1166
1465
  element.className = "turbo-progress-bar";
1167
1466
  return element;
1168
1467
  }
1468
+ get cspNonce() {
1469
+ return getMetaContent("csp-nonce");
1470
+ }
1169
1471
  }
1170
1472
 
1171
1473
  ProgressBar.animationDuration = 300;
@@ -1173,7 +1475,7 @@ ProgressBar.animationDuration = 300;
1173
1475
  class HeadSnapshot extends Snapshot {
1174
1476
  constructor() {
1175
1477
  super(...arguments);
1176
- this.detailsByOuterHTML = this.children.filter((element => !elementIsNoscript(element))).reduce(((result, element) => {
1478
+ this.detailsByOuterHTML = this.children.filter((element => !elementIsNoscript(element))).map((element => elementWithoutNonce(element))).reduce(((result, element) => {
1177
1479
  const {outerHTML: outerHTML} = element;
1178
1480
  const details = outerHTML in result ? result[outerHTML] : {
1179
1481
  type: elementType(element),
@@ -1236,25 +1538,32 @@ function elementIsTracked(element) {
1236
1538
  }
1237
1539
 
1238
1540
  function elementIsScript(element) {
1239
- const tagName = element.tagName.toLowerCase();
1541
+ const tagName = element.localName;
1240
1542
  return tagName == "script";
1241
1543
  }
1242
1544
 
1243
1545
  function elementIsNoscript(element) {
1244
- const tagName = element.tagName.toLowerCase();
1546
+ const tagName = element.localName;
1245
1547
  return tagName == "noscript";
1246
1548
  }
1247
1549
 
1248
1550
  function elementIsStylesheet(element) {
1249
- const tagName = element.tagName.toLowerCase();
1551
+ const tagName = element.localName;
1250
1552
  return tagName == "style" || tagName == "link" && element.getAttribute("rel") == "stylesheet";
1251
1553
  }
1252
1554
 
1253
1555
  function elementIsMetaElementWithName(element, name) {
1254
- const tagName = element.tagName.toLowerCase();
1556
+ const tagName = element.localName;
1255
1557
  return tagName == "meta" && element.getAttribute("name") == name;
1256
1558
  }
1257
1559
 
1560
+ function elementWithoutNonce(element) {
1561
+ if (element.hasAttribute("nonce")) {
1562
+ element.setAttribute("nonce", "");
1563
+ }
1564
+ return element;
1565
+ }
1566
+
1258
1567
  class PageSnapshot extends Snapshot {
1259
1568
  constructor(element, headSnapshot) {
1260
1569
  super(element);
@@ -1270,7 +1579,18 @@ class PageSnapshot extends Snapshot {
1270
1579
  return new this(body, new HeadSnapshot(head));
1271
1580
  }
1272
1581
  clone() {
1273
- return new PageSnapshot(this.element.cloneNode(true), this.headSnapshot);
1582
+ const clonedElement = this.element.cloneNode(true);
1583
+ const selectElements = this.element.querySelectorAll("select");
1584
+ const clonedSelectElements = clonedElement.querySelectorAll("select");
1585
+ for (const [index, source] of selectElements.entries()) {
1586
+ const clone = clonedSelectElements[index];
1587
+ for (const option of clone.selectedOptions) option.selected = false;
1588
+ for (const option of source.selectedOptions) clone.options[option.index].selected = true;
1589
+ }
1590
+ for (const clonedPasswordInput of clonedElement.querySelectorAll('input[type="password"]')) {
1591
+ clonedPasswordInput.value = "";
1592
+ }
1593
+ return new PageSnapshot(clonedElement, this.headSnapshot);
1274
1594
  }
1275
1595
  get headElement() {
1276
1596
  return this.headSnapshot.element;
@@ -1318,7 +1638,12 @@ var VisitState;
1318
1638
 
1319
1639
  const defaultOptions = {
1320
1640
  action: "advance",
1321
- historyChanged: false
1641
+ historyChanged: false,
1642
+ visitCachedSnapshot: () => {},
1643
+ willRender: true,
1644
+ updateHistory: true,
1645
+ shouldCacheSnapshot: true,
1646
+ acceptsStreamResponse: false
1322
1647
  };
1323
1648
 
1324
1649
  var SystemStatusCode;
@@ -1336,18 +1661,26 @@ class Visit {
1336
1661
  this.followedRedirect = false;
1337
1662
  this.historyChanged = false;
1338
1663
  this.scrolled = false;
1664
+ this.shouldCacheSnapshot = true;
1665
+ this.acceptsStreamResponse = false;
1339
1666
  this.snapshotCached = false;
1340
1667
  this.state = VisitState.initialized;
1341
1668
  this.delegate = delegate;
1342
1669
  this.location = location;
1343
1670
  this.restorationIdentifier = restorationIdentifier || uuid();
1344
- const {action: action, historyChanged: historyChanged, referrer: referrer, snapshotHTML: snapshotHTML, response: response} = Object.assign(Object.assign({}, defaultOptions), options);
1671
+ const {action: action, historyChanged: historyChanged, referrer: referrer, snapshotHTML: snapshotHTML, response: response, visitCachedSnapshot: visitCachedSnapshot, willRender: willRender, updateHistory: updateHistory, shouldCacheSnapshot: shouldCacheSnapshot, acceptsStreamResponse: acceptsStreamResponse} = Object.assign(Object.assign({}, defaultOptions), options);
1345
1672
  this.action = action;
1346
1673
  this.historyChanged = historyChanged;
1347
1674
  this.referrer = referrer;
1348
1675
  this.snapshotHTML = snapshotHTML;
1349
1676
  this.response = response;
1350
1677
  this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
1678
+ this.visitCachedSnapshot = visitCachedSnapshot;
1679
+ this.willRender = willRender;
1680
+ this.updateHistory = updateHistory;
1681
+ this.scrolled = !willRender;
1682
+ this.shouldCacheSnapshot = shouldCacheSnapshot;
1683
+ this.acceptsStreamResponse = acceptsStreamResponse;
1351
1684
  }
1352
1685
  get adapter() {
1353
1686
  return this.delegate.adapter;
@@ -1385,9 +1718,11 @@ class Visit {
1385
1718
  if (this.state == VisitState.started) {
1386
1719
  this.recordTimingMetric(TimingMetric.visitEnd);
1387
1720
  this.state = VisitState.completed;
1388
- this.adapter.visitCompleted(this);
1389
- this.delegate.visitCompleted(this);
1390
1721
  this.followRedirect();
1722
+ if (!this.followedRedirect) {
1723
+ this.adapter.visitCompleted(this);
1724
+ this.delegate.visitCompleted(this);
1725
+ }
1391
1726
  }
1392
1727
  }
1393
1728
  fail() {
@@ -1398,9 +1733,9 @@ class Visit {
1398
1733
  }
1399
1734
  changeHistory() {
1400
1735
  var _a;
1401
- if (!this.historyChanged) {
1736
+ if (!this.historyChanged && this.updateHistory) {
1402
1737
  const actionForHistory = this.location.href === ((_a = this.referrer) === null || _a === void 0 ? void 0 : _a.href) ? "replace" : this.action;
1403
- const method = this.getHistoryMethodForAction(actionForHistory);
1738
+ const method = getHistoryMethodForAction(actionForHistory);
1404
1739
  this.history.update(method, this.location, this.restorationIdentifier);
1405
1740
  this.historyChanged = true;
1406
1741
  }
@@ -1443,14 +1778,15 @@ class Visit {
1443
1778
  if (this.response) {
1444
1779
  const {statusCode: statusCode, responseHTML: responseHTML} = this.response;
1445
1780
  this.render((async () => {
1446
- this.cacheSnapshot();
1781
+ if (this.shouldCacheSnapshot) this.cacheSnapshot();
1447
1782
  if (this.view.renderPromise) await this.view.renderPromise;
1448
1783
  if (isSuccessful(statusCode) && responseHTML != null) {
1449
- await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML));
1784
+ await this.view.renderPage(PageSnapshot.fromHTMLString(responseHTML), false, this.willRender, this);
1785
+ this.performScroll();
1450
1786
  this.adapter.visitRendered(this);
1451
1787
  this.complete();
1452
1788
  } else {
1453
- await this.view.renderError(PageSnapshot.fromHTMLString(responseHTML));
1789
+ await this.view.renderError(PageSnapshot.fromHTMLString(responseHTML), this);
1454
1790
  this.adapter.visitRendered(this);
1455
1791
  this.fail();
1456
1792
  }
@@ -1483,7 +1819,8 @@ class Visit {
1483
1819
  this.adapter.visitRendered(this);
1484
1820
  } else {
1485
1821
  if (this.view.renderPromise) await this.view.renderPromise;
1486
- await this.view.renderPage(snapshot, isPreview);
1822
+ await this.view.renderPage(snapshot, isPreview, this.willRender, this);
1823
+ this.performScroll();
1487
1824
  this.adapter.visitRendered(this);
1488
1825
  if (!isPreview) {
1489
1826
  this.complete();
@@ -1493,7 +1830,8 @@ class Visit {
1493
1830
  }
1494
1831
  }
1495
1832
  followRedirect() {
1496
- if (this.redirectedToLocation && !this.followedRedirect) {
1833
+ var _a;
1834
+ if (this.redirectedToLocation && !this.followedRedirect && ((_a = this.response) === null || _a === void 0 ? void 0 : _a.redirected)) {
1497
1835
  this.adapter.visitProposedToLocation(this.redirectedToLocation, {
1498
1836
  action: "replace",
1499
1837
  response: this.response
@@ -1505,51 +1843,64 @@ class Visit {
1505
1843
  if (this.isSamePage) {
1506
1844
  this.render((async () => {
1507
1845
  this.cacheSnapshot();
1846
+ this.performScroll();
1508
1847
  this.adapter.visitRendered(this);
1509
1848
  }));
1510
1849
  }
1511
1850
  }
1512
- requestStarted() {
1513
- this.startRequest();
1851
+ prepareHeadersForRequest(headers, request) {
1852
+ if (this.acceptsStreamResponse) {
1853
+ request.acceptResponseType(StreamMessage.contentType);
1854
+ }
1514
1855
  }
1515
- requestPreventedHandlingResponse(request, response) {}
1856
+ requestStarted() {
1857
+ this.startRequest();
1858
+ }
1859
+ requestPreventedHandlingResponse(_request, _response) {}
1516
1860
  async requestSucceededWithResponse(request, response) {
1517
1861
  const responseHTML = await response.responseHTML;
1862
+ const {redirected: redirected, statusCode: statusCode} = response;
1518
1863
  if (responseHTML == undefined) {
1519
1864
  this.recordResponse({
1520
- statusCode: SystemStatusCode.contentTypeMismatch
1865
+ statusCode: SystemStatusCode.contentTypeMismatch,
1866
+ redirected: redirected
1521
1867
  });
1522
1868
  } else {
1523
1869
  this.redirectedToLocation = response.redirected ? response.location : undefined;
1524
1870
  this.recordResponse({
1525
- statusCode: response.statusCode,
1526
- responseHTML: responseHTML
1871
+ statusCode: statusCode,
1872
+ responseHTML: responseHTML,
1873
+ redirected: redirected
1527
1874
  });
1528
1875
  }
1529
1876
  }
1530
1877
  async requestFailedWithResponse(request, response) {
1531
1878
  const responseHTML = await response.responseHTML;
1879
+ const {redirected: redirected, statusCode: statusCode} = response;
1532
1880
  if (responseHTML == undefined) {
1533
1881
  this.recordResponse({
1534
- statusCode: SystemStatusCode.contentTypeMismatch
1882
+ statusCode: SystemStatusCode.contentTypeMismatch,
1883
+ redirected: redirected
1535
1884
  });
1536
1885
  } else {
1537
1886
  this.recordResponse({
1538
- statusCode: response.statusCode,
1539
- responseHTML: responseHTML
1887
+ statusCode: statusCode,
1888
+ responseHTML: responseHTML,
1889
+ redirected: redirected
1540
1890
  });
1541
1891
  }
1542
1892
  }
1543
- requestErrored(request, error) {
1893
+ requestErrored(_request, _error) {
1544
1894
  this.recordResponse({
1545
- statusCode: SystemStatusCode.networkFailure
1895
+ statusCode: SystemStatusCode.networkFailure,
1896
+ redirected: false
1546
1897
  });
1547
1898
  }
1548
1899
  requestFinished() {
1549
1900
  this.finishRequest();
1550
1901
  }
1551
1902
  performScroll() {
1552
- if (!this.scrolled) {
1903
+ if (!this.scrolled && !this.view.forceReloaded) {
1553
1904
  if (this.action == "restore") {
1554
1905
  this.scrollToRestoredPosition() || this.scrollToAnchor() || this.view.scrollToTop();
1555
1906
  } else {
@@ -1600,12 +1951,12 @@ class Visit {
1600
1951
  } else if (this.action == "restore") {
1601
1952
  return !this.hasCachedSnapshot();
1602
1953
  } else {
1603
- return true;
1954
+ return this.willRender;
1604
1955
  }
1605
1956
  }
1606
1957
  cacheSnapshot() {
1607
1958
  if (!this.snapshotCached) {
1608
- this.view.cacheSnapshot();
1959
+ this.view.cacheSnapshot().then((snapshot => snapshot && this.visitCachedSnapshot(snapshot)));
1609
1960
  this.snapshotCached = true;
1610
1961
  }
1611
1962
  }
@@ -1616,7 +1967,6 @@ class Visit {
1616
1967
  }));
1617
1968
  await callback();
1618
1969
  delete this.frame;
1619
- this.performScroll();
1620
1970
  }
1621
1971
  cancelRender() {
1622
1972
  if (this.frame) {
@@ -1639,13 +1989,13 @@ class BrowserAdapter {
1639
1989
  this.session = session;
1640
1990
  }
1641
1991
  visitProposedToLocation(location, options) {
1642
- this.navigator.startVisit(location, uuid(), options);
1992
+ this.navigator.startVisit(location, (options === null || options === void 0 ? void 0 : options.restorationIdentifier) || uuid(), options);
1643
1993
  }
1644
1994
  visitStarted(visit) {
1995
+ this.location = visit.location;
1996
+ visit.loadCachedSnapshot();
1645
1997
  visit.issueRequest();
1646
- visit.changeHistory();
1647
1998
  visit.goToSamePageAnchor();
1648
- visit.loadCachedSnapshot();
1649
1999
  }
1650
2000
  visitRequestStarted(visit) {
1651
2001
  this.progressBar.setValue(0);
@@ -1663,27 +2013,32 @@ class BrowserAdapter {
1663
2013
  case SystemStatusCode.networkFailure:
1664
2014
  case SystemStatusCode.timeoutFailure:
1665
2015
  case SystemStatusCode.contentTypeMismatch:
1666
- return this.reload();
2016
+ return this.reload({
2017
+ reason: "request_failed",
2018
+ context: {
2019
+ statusCode: statusCode
2020
+ }
2021
+ });
1667
2022
 
1668
2023
  default:
1669
2024
  return visit.loadResponse();
1670
2025
  }
1671
2026
  }
1672
- visitRequestFinished(visit) {
2027
+ visitRequestFinished(_visit) {
1673
2028
  this.progressBar.setValue(1);
1674
2029
  this.hideVisitProgressBar();
1675
2030
  }
1676
- visitCompleted(visit) {}
1677
- pageInvalidated() {
1678
- this.reload();
2031
+ visitCompleted(_visit) {}
2032
+ pageInvalidated(reason) {
2033
+ this.reload(reason);
1679
2034
  }
1680
- visitFailed(visit) {}
1681
- visitRendered(visit) {}
1682
- formSubmissionStarted(formSubmission) {
2035
+ visitFailed(_visit) {}
2036
+ visitRendered(_visit) {}
2037
+ formSubmissionStarted(_formSubmission) {
1683
2038
  this.progressBar.setValue(0);
1684
2039
  this.showFormProgressBarAfterDelay();
1685
2040
  }
1686
- formSubmissionFinished(formSubmission) {
2041
+ formSubmissionFinished(_formSubmission) {
1687
2042
  this.progressBar.setValue(1);
1688
2043
  this.hideFormProgressBar();
1689
2044
  }
@@ -1709,8 +2064,12 @@ class BrowserAdapter {
1709
2064
  delete this.formProgressBarTimeout;
1710
2065
  }
1711
2066
  }
1712
- reload() {
1713
- window.location.reload();
2067
+ reload(reason) {
2068
+ dispatch("turbo:reload", {
2069
+ detail: reason
2070
+ });
2071
+ if (!this.location) return;
2072
+ window.location.href = this.location.toString();
1714
2073
  }
1715
2074
  get navigator() {
1716
2075
  return this.session.navigator;
@@ -1720,103 +2079,89 @@ class BrowserAdapter {
1720
2079
  class CacheObserver {
1721
2080
  constructor() {
1722
2081
  this.started = false;
1723
- }
1724
- start() {
1725
- if (!this.started) {
1726
- this.started = true;
1727
- addEventListener("turbo:before-cache", this.removeStaleElements, false);
1728
- }
1729
- }
1730
- stop() {
1731
- if (this.started) {
1732
- this.started = false;
1733
- removeEventListener("turbo:before-cache", this.removeStaleElements, false);
1734
- }
1735
- }
1736
- removeStaleElements() {
1737
- const staleElements = [ ...document.querySelectorAll('[data-turbo-cache="false"]') ];
1738
- for (const element of staleElements) {
1739
- element.remove();
1740
- }
1741
- }
1742
- }
1743
-
1744
- class FormSubmitObserver {
1745
- constructor(delegate) {
1746
- this.started = false;
1747
- this.submitCaptured = () => {
1748
- removeEventListener("submit", this.submitBubbled, false);
1749
- addEventListener("submit", this.submitBubbled, false);
1750
- };
1751
- this.submitBubbled = event => {
1752
- if (!event.defaultPrevented) {
1753
- const form = event.target instanceof HTMLFormElement ? event.target : undefined;
1754
- const submitter = event.submitter || undefined;
1755
- if (form) {
1756
- const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.method;
1757
- if (method != "dialog" && this.delegate.willSubmitForm(form, submitter)) {
1758
- event.preventDefault();
1759
- this.delegate.formSubmitted(form, submitter);
1760
- }
1761
- }
2082
+ this.removeStaleElements = _event => {
2083
+ const staleElements = [ ...document.querySelectorAll('[data-turbo-cache="false"]') ];
2084
+ for (const element of staleElements) {
2085
+ element.remove();
1762
2086
  }
1763
2087
  };
1764
- this.delegate = delegate;
1765
2088
  }
1766
2089
  start() {
1767
2090
  if (!this.started) {
1768
- addEventListener("submit", this.submitCaptured, true);
1769
2091
  this.started = true;
2092
+ addEventListener("turbo:before-cache", this.removeStaleElements, false);
1770
2093
  }
1771
2094
  }
1772
2095
  stop() {
1773
2096
  if (this.started) {
1774
- removeEventListener("submit", this.submitCaptured, true);
1775
2097
  this.started = false;
2098
+ removeEventListener("turbo:before-cache", this.removeStaleElements, false);
1776
2099
  }
1777
2100
  }
1778
2101
  }
1779
2102
 
1780
2103
  class FrameRedirector {
1781
- constructor(element) {
2104
+ constructor(session, element) {
2105
+ this.session = session;
1782
2106
  this.element = element;
1783
- this.linkInterceptor = new LinkInterceptor(this, element);
1784
- this.formInterceptor = new FormInterceptor(this, element);
2107
+ this.linkClickObserver = new LinkClickObserver(this, element);
2108
+ this.formSubmitObserver = new FormSubmitObserver(this, element);
1785
2109
  }
1786
2110
  start() {
1787
- this.linkInterceptor.start();
1788
- this.formInterceptor.start();
2111
+ this.linkClickObserver.start();
2112
+ this.formSubmitObserver.start();
1789
2113
  }
1790
2114
  stop() {
1791
- this.linkInterceptor.stop();
1792
- this.formInterceptor.stop();
2115
+ this.linkClickObserver.stop();
2116
+ this.formSubmitObserver.stop();
1793
2117
  }
1794
- shouldInterceptLinkClick(element, url) {
1795
- return this.shouldRedirect(element);
2118
+ willFollowLinkToLocation(element, location, event) {
2119
+ return this.shouldRedirect(element) && this.frameAllowsVisitingLocation(element, location, event);
1796
2120
  }
1797
- linkClickIntercepted(element, url) {
2121
+ followedLinkToLocation(element, url) {
1798
2122
  const frame = this.findFrameElement(element);
1799
2123
  if (frame) {
1800
- frame.setAttribute("reloadable", "");
1801
- frame.src = url;
2124
+ frame.delegate.followedLinkToLocation(element, url);
1802
2125
  }
1803
2126
  }
1804
- shouldInterceptFormSubmission(element, submitter) {
1805
- return this.shouldRedirect(element, submitter);
2127
+ willSubmitForm(element, submitter) {
2128
+ return element.closest("turbo-frame") == null && this.shouldSubmit(element, submitter) && this.shouldRedirect(element, submitter);
1806
2129
  }
1807
- formSubmissionIntercepted(element, submitter) {
1808
- const frame = this.findFrameElement(element);
2130
+ formSubmitted(element, submitter) {
2131
+ const frame = this.findFrameElement(element, submitter);
1809
2132
  if (frame) {
1810
- frame.removeAttribute("reloadable");
1811
- frame.delegate.formSubmissionIntercepted(element, submitter);
2133
+ frame.delegate.formSubmitted(element, submitter);
1812
2134
  }
1813
2135
  }
2136
+ frameAllowsVisitingLocation(target, {href: url}, originalEvent) {
2137
+ const event = dispatch("turbo:click", {
2138
+ target: target,
2139
+ detail: {
2140
+ url: url,
2141
+ originalEvent: originalEvent
2142
+ },
2143
+ cancelable: true
2144
+ });
2145
+ return !event.defaultPrevented;
2146
+ }
2147
+ shouldSubmit(form, submitter) {
2148
+ var _a;
2149
+ const action = getAction(form, submitter);
2150
+ const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
2151
+ const rootLocation = expandURL((_a = meta === null || meta === void 0 ? void 0 : meta.content) !== null && _a !== void 0 ? _a : "/");
2152
+ return this.shouldRedirect(form, submitter) && locationIsVisitable(action, rootLocation);
2153
+ }
1814
2154
  shouldRedirect(element, submitter) {
1815
- const frame = this.findFrameElement(element);
1816
- return frame ? frame != element.closest("turbo-frame") : false;
2155
+ const isNavigatable = element instanceof HTMLFormElement ? this.session.submissionIsNavigatable(element, submitter) : this.session.elementIsNavigatable(element);
2156
+ if (isNavigatable) {
2157
+ const frame = this.findFrameElement(element, submitter);
2158
+ return frame ? frame != element.closest("turbo-frame") : false;
2159
+ } else {
2160
+ return false;
2161
+ }
1817
2162
  }
1818
- findFrameElement(element) {
1819
- const id = element.getAttribute("data-turbo-frame");
2163
+ findFrameElement(element, submitter) {
2164
+ const id = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("data-turbo-frame")) || element.getAttribute("data-turbo-frame");
1820
2165
  if (id && id != "_top") {
1821
2166
  const frame = this.element.querySelector(`#${id}:not([disabled])`);
1822
2167
  if (frame instanceof FrameElement) {
@@ -1843,7 +2188,7 @@ class History {
1843
2188
  }
1844
2189
  }
1845
2190
  };
1846
- this.onPageLoad = async event => {
2191
+ this.onPageLoad = async _event => {
1847
2192
  await nextMicrotask();
1848
2193
  this.pageLoaded = true;
1849
2194
  };
@@ -1909,67 +2254,21 @@ class History {
1909
2254
  }
1910
2255
  }
1911
2256
 
1912
- class LinkClickObserver {
1913
- constructor(delegate) {
1914
- this.started = false;
1915
- this.clickCaptured = () => {
1916
- removeEventListener("click", this.clickBubbled, false);
1917
- addEventListener("click", this.clickBubbled, false);
1918
- };
1919
- this.clickBubbled = event => {
1920
- if (this.clickEventIsSignificant(event)) {
1921
- const target = event.composedPath && event.composedPath()[0] || event.target;
1922
- const link = this.findLinkFromClickTarget(target);
1923
- if (link) {
1924
- const location = this.getLocationForLink(link);
1925
- if (this.delegate.willFollowLinkToLocation(link, location)) {
1926
- event.preventDefault();
1927
- this.delegate.followedLinkToLocation(link, location);
1928
- }
1929
- }
1930
- }
1931
- };
1932
- this.delegate = delegate;
1933
- }
1934
- start() {
1935
- if (!this.started) {
1936
- addEventListener("click", this.clickCaptured, true);
1937
- this.started = true;
1938
- }
1939
- }
1940
- stop() {
1941
- if (this.started) {
1942
- removeEventListener("click", this.clickCaptured, true);
1943
- this.started = false;
1944
- }
1945
- }
1946
- clickEventIsSignificant(event) {
1947
- return !(event.target && event.target.isContentEditable || event.defaultPrevented || event.which > 1 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey);
1948
- }
1949
- findLinkFromClickTarget(target) {
1950
- if (target instanceof Element) {
1951
- return target.closest("a[href]:not([target^=_]):not([download])");
1952
- }
1953
- }
1954
- getLocationForLink(link) {
1955
- return expandURL(link.getAttribute("href") || "");
1956
- }
1957
- }
1958
-
1959
- function isAction(action) {
1960
- return action == "advance" || action == "replace" || action == "restore";
1961
- }
1962
-
1963
2257
  class Navigator {
1964
2258
  constructor(delegate) {
1965
2259
  this.delegate = delegate;
1966
2260
  }
1967
2261
  proposeVisit(location, options = {}) {
1968
2262
  if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
1969
- this.delegate.visitProposedToLocation(location, options);
2263
+ if (locationIsVisitable(location, this.view.snapshot.rootLocation)) {
2264
+ this.delegate.visitProposedToLocation(location, options);
2265
+ } else {
2266
+ window.location.href = location.toString();
2267
+ }
1970
2268
  }
1971
2269
  }
1972
2270
  startVisit(locatable, restorationIdentifier, options = {}) {
2271
+ this.lastVisit = this.currentVisit;
1973
2272
  this.stop();
1974
2273
  this.currentVisit = new Visit(this, expandURL(locatable), restorationIdentifier, Object.assign({
1975
2274
  referrer: this.location
@@ -1979,13 +2278,7 @@ class Navigator {
1979
2278
  submitForm(form, submitter) {
1980
2279
  this.stop();
1981
2280
  this.formSubmission = new FormSubmission(this, form, submitter, true);
1982
- if (this.formSubmission.isIdempotent) {
1983
- this.proposeVisit(this.formSubmission.fetchRequest.url, {
1984
- action: this.getActionForFormSubmission(this.formSubmission)
1985
- });
1986
- } else {
1987
- this.formSubmission.start();
1988
- }
2281
+ this.formSubmission.start();
1989
2282
  }
1990
2283
  stop() {
1991
2284
  if (this.formSubmission) {
@@ -2015,14 +2308,19 @@ class Navigator {
2015
2308
  if (formSubmission == this.formSubmission) {
2016
2309
  const responseHTML = await fetchResponse.responseHTML;
2017
2310
  if (responseHTML) {
2018
- if (formSubmission.method != FetchMethod.get) {
2311
+ const shouldCacheSnapshot = formSubmission.method == FetchMethod.get;
2312
+ if (!shouldCacheSnapshot) {
2019
2313
  this.view.clearSnapshotCache();
2020
2314
  }
2021
- const {statusCode: statusCode} = fetchResponse;
2315
+ const {statusCode: statusCode, redirected: redirected} = fetchResponse;
2316
+ const action = this.getActionForFormSubmission(formSubmission);
2022
2317
  const visitOptions = {
2318
+ action: action,
2319
+ shouldCacheSnapshot: shouldCacheSnapshot,
2023
2320
  response: {
2024
2321
  statusCode: statusCode,
2025
- responseHTML: responseHTML
2322
+ responseHTML: responseHTML,
2323
+ redirected: redirected
2026
2324
  }
2027
2325
  };
2028
2326
  this.proposeVisit(fetchResponse.location, visitOptions);
@@ -2034,9 +2332,9 @@ class Navigator {
2034
2332
  if (responseHTML) {
2035
2333
  const snapshot = PageSnapshot.fromHTMLString(responseHTML);
2036
2334
  if (fetchResponse.serverError) {
2037
- await this.view.renderError(snapshot);
2335
+ await this.view.renderError(snapshot, this.currentVisit);
2038
2336
  } else {
2039
- await this.view.renderPage(snapshot);
2337
+ await this.view.renderPage(snapshot, false, true, this.currentVisit);
2040
2338
  }
2041
2339
  this.view.scrollToTop();
2042
2340
  this.view.clearSnapshotCache();
@@ -2057,10 +2355,12 @@ class Navigator {
2057
2355
  this.delegate.visitCompleted(visit);
2058
2356
  }
2059
2357
  locationWithActionIsSamePage(location, action) {
2358
+ var _a;
2060
2359
  const anchor = getAnchor(location);
2061
- const currentAnchor = getAnchor(this.view.lastRenderedLocation);
2360
+ const lastLocation = ((_a = this.lastVisit) === null || _a === void 0 ? void 0 : _a.location) || this.view.lastRenderedLocation;
2361
+ const currentAnchor = getAnchor(lastLocation);
2062
2362
  const isRestorationToTop = action === "restore" && typeof anchor === "undefined";
2063
- return action !== "replace" && getRequestURL(location) === getRequestURL(this.view.lastRenderedLocation) && (isRestorationToTop || anchor != null && anchor !== currentAnchor);
2363
+ return action !== "replace" && getRequestURL(location) === getRequestURL(lastLocation) && (isRestorationToTop || anchor != null && anchor !== currentAnchor);
2064
2364
  }
2065
2365
  visitScrolledToSamePageLocation(oldURL, newURL) {
2066
2366
  this.delegate.visitScrolledToSamePageLocation(oldURL, newURL);
@@ -2073,7 +2373,7 @@ class Navigator {
2073
2373
  }
2074
2374
  getActionForFormSubmission(formSubmission) {
2075
2375
  const {formElement: formElement, submitter: submitter} = formSubmission;
2076
- const action = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("data-turbo-action")) || formElement.getAttribute("data-turbo-action");
2376
+ const action = getAttribute("data-turbo-action", submitter, formElement);
2077
2377
  return isAction(action) ? action : "advance";
2078
2378
  }
2079
2379
  }
@@ -2168,6 +2468,31 @@ class ScrollObserver {
2168
2468
  }
2169
2469
  }
2170
2470
 
2471
+ class StreamMessageRenderer {
2472
+ render({fragment: fragment}) {
2473
+ Bardo.preservingPermanentElements(this, getPermanentElementMapForFragment(fragment), (() => document.documentElement.appendChild(fragment)));
2474
+ }
2475
+ enteringBardo(currentPermanentElement, newPermanentElement) {
2476
+ newPermanentElement.replaceWith(currentPermanentElement.cloneNode(true));
2477
+ }
2478
+ leavingBardo() {}
2479
+ }
2480
+
2481
+ function getPermanentElementMapForFragment(fragment) {
2482
+ const permanentElementsInDocument = queryPermanentElementsAll(document.documentElement);
2483
+ const permanentElementMap = {};
2484
+ for (const permanentElementInDocument of permanentElementsInDocument) {
2485
+ const {id: id} = permanentElementInDocument;
2486
+ for (const streamElement of fragment.querySelectorAll("turbo-stream")) {
2487
+ const elementInStream = getPermanentElementById(streamElement.templateElement.content, id);
2488
+ if (elementInStream) {
2489
+ permanentElementMap[id] = [ permanentElementInDocument, elementInStream ];
2490
+ }
2491
+ }
2492
+ }
2493
+ return permanentElementMap;
2494
+ }
2495
+
2171
2496
  class StreamObserver {
2172
2497
  constructor(delegate) {
2173
2498
  this.sources = new Set;
@@ -2220,7 +2545,7 @@ class StreamObserver {
2220
2545
  }
2221
2546
  }
2222
2547
  receiveMessageHTML(html) {
2223
- this.delegate.receivedMessageFromStream(new StreamMessage(html));
2548
+ this.delegate.receivedMessageFromStream(StreamMessage.wrap(html));
2224
2549
  }
2225
2550
  }
2226
2551
 
@@ -2239,20 +2564,24 @@ function fetchResponseIsStream(response) {
2239
2564
  }
2240
2565
 
2241
2566
  class ErrorRenderer extends Renderer {
2567
+ static renderElement(currentElement, newElement) {
2568
+ const {documentElement: documentElement, body: body} = document;
2569
+ documentElement.replaceChild(newElement, body);
2570
+ }
2242
2571
  async render() {
2243
2572
  this.replaceHeadAndBody();
2244
2573
  this.activateScriptElements();
2245
2574
  }
2246
2575
  replaceHeadAndBody() {
2247
- const {documentElement: documentElement, head: head, body: body} = document;
2576
+ const {documentElement: documentElement, head: head} = document;
2248
2577
  documentElement.replaceChild(this.newHead, head);
2249
- documentElement.replaceChild(this.newElement, body);
2578
+ this.renderElement(this.currentElement, this.newElement);
2250
2579
  }
2251
2580
  activateScriptElements() {
2252
2581
  for (const replaceableElement of this.scriptElements) {
2253
2582
  const parentNode = replaceableElement.parentNode;
2254
2583
  if (parentNode) {
2255
- const element = this.createScriptElement(replaceableElement);
2584
+ const element = activateScriptElement(replaceableElement);
2256
2585
  parentNode.replaceChild(element, replaceableElement);
2257
2586
  }
2258
2587
  }
@@ -2261,19 +2590,40 @@ class ErrorRenderer extends Renderer {
2261
2590
  return this.newSnapshot.headSnapshot.element;
2262
2591
  }
2263
2592
  get scriptElements() {
2264
- return [ ...document.documentElement.querySelectorAll("script") ];
2593
+ return document.documentElement.querySelectorAll("script");
2265
2594
  }
2266
2595
  }
2267
2596
 
2268
2597
  class PageRenderer extends Renderer {
2598
+ static renderElement(currentElement, newElement) {
2599
+ if (document.body && newElement instanceof HTMLBodyElement) {
2600
+ document.body.replaceWith(newElement);
2601
+ } else {
2602
+ document.documentElement.appendChild(newElement);
2603
+ }
2604
+ }
2269
2605
  get shouldRender() {
2270
2606
  return this.newSnapshot.isVisitable && this.trackedElementsAreIdentical;
2271
2607
  }
2272
- prepareToRender() {
2273
- this.mergeHead();
2608
+ get reloadReason() {
2609
+ if (!this.newSnapshot.isVisitable) {
2610
+ return {
2611
+ reason: "turbo_visit_control_is_reload"
2612
+ };
2613
+ }
2614
+ if (!this.trackedElementsAreIdentical) {
2615
+ return {
2616
+ reason: "tracked_element_mismatch"
2617
+ };
2618
+ }
2619
+ }
2620
+ async prepareToRender() {
2621
+ await this.mergeHead();
2274
2622
  }
2275
2623
  async render() {
2276
- this.replaceBody();
2624
+ if (this.willRender) {
2625
+ this.replaceBody();
2626
+ }
2277
2627
  }
2278
2628
  finishRendering() {
2279
2629
  super.finishRendering();
@@ -2290,11 +2640,12 @@ class PageRenderer extends Renderer {
2290
2640
  get newElement() {
2291
2641
  return this.newSnapshot.element;
2292
2642
  }
2293
- mergeHead() {
2294
- this.copyNewHeadStylesheetElements();
2643
+ async mergeHead() {
2644
+ const newStylesheetElements = this.copyNewHeadStylesheetElements();
2295
2645
  this.copyNewHeadScriptElements();
2296
2646
  this.removeCurrentHeadProvisionalElements();
2297
2647
  this.copyNewHeadProvisionalElements();
2648
+ await newStylesheetElements;
2298
2649
  }
2299
2650
  replaceBody() {
2300
2651
  this.preservingPermanentElements((() => {
@@ -2305,14 +2656,17 @@ class PageRenderer extends Renderer {
2305
2656
  get trackedElementsAreIdentical() {
2306
2657
  return this.currentHeadSnapshot.trackedElementSignature == this.newHeadSnapshot.trackedElementSignature;
2307
2658
  }
2308
- copyNewHeadStylesheetElements() {
2659
+ async copyNewHeadStylesheetElements() {
2660
+ const loadingElements = [];
2309
2661
  for (const element of this.newHeadStylesheetElements) {
2662
+ loadingElements.push(waitForLoad(element));
2310
2663
  document.head.appendChild(element);
2311
2664
  }
2665
+ await Promise.all(loadingElements);
2312
2666
  }
2313
2667
  copyNewHeadScriptElements() {
2314
2668
  for (const element of this.newHeadScriptElements) {
2315
- document.head.appendChild(this.createScriptElement(element));
2669
+ document.head.appendChild(activateScriptElement(element));
2316
2670
  }
2317
2671
  }
2318
2672
  removeCurrentHeadProvisionalElements() {
@@ -2331,16 +2685,12 @@ class PageRenderer extends Renderer {
2331
2685
  }
2332
2686
  activateNewBodyScriptElements() {
2333
2687
  for (const inertScriptElement of this.newBodyScriptElements) {
2334
- const activatedScriptElement = this.createScriptElement(inertScriptElement);
2688
+ const activatedScriptElement = activateScriptElement(inertScriptElement);
2335
2689
  inertScriptElement.replaceWith(activatedScriptElement);
2336
2690
  }
2337
2691
  }
2338
2692
  assignNewBody() {
2339
- if (document.body && this.newElement instanceof HTMLBodyElement) {
2340
- document.body.replaceWith(this.newElement);
2341
- } else {
2342
- document.documentElement.appendChild(this.newElement);
2343
- }
2693
+ this.renderElement(this.currentElement, this.newElement);
2344
2694
  }
2345
2695
  get newHeadStylesheetElements() {
2346
2696
  return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot);
@@ -2408,13 +2758,20 @@ class PageView extends View {
2408
2758
  super(...arguments);
2409
2759
  this.snapshotCache = new SnapshotCache(10);
2410
2760
  this.lastRenderedLocation = new URL(location.href);
2761
+ this.forceReloaded = false;
2411
2762
  }
2412
- renderPage(snapshot, isPreview = false) {
2413
- const renderer = new PageRenderer(this.snapshot, snapshot, isPreview);
2763
+ renderPage(snapshot, isPreview = false, willRender = true, visit) {
2764
+ const renderer = new PageRenderer(this.snapshot, snapshot, PageRenderer.renderElement, isPreview, willRender);
2765
+ if (!renderer.shouldRender) {
2766
+ this.forceReloaded = true;
2767
+ } else {
2768
+ visit === null || visit === void 0 ? void 0 : visit.changeHistory();
2769
+ }
2414
2770
  return this.render(renderer);
2415
2771
  }
2416
- renderError(snapshot) {
2417
- const renderer = new ErrorRenderer(this.snapshot, snapshot, false);
2772
+ renderError(snapshot, visit) {
2773
+ visit === null || visit === void 0 ? void 0 : visit.changeHistory();
2774
+ const renderer = new ErrorRenderer(this.snapshot, snapshot, ErrorRenderer.renderElement, false);
2418
2775
  return this.render(renderer);
2419
2776
  }
2420
2777
  clearSnapshotCache() {
@@ -2425,7 +2782,9 @@ class PageView extends View {
2425
2782
  this.delegate.viewWillCacheSnapshot();
2426
2783
  const {snapshot: snapshot, lastRenderedLocation: location} = this;
2427
2784
  await nextEventLoopTick();
2428
- this.snapshotCache.put(location, snapshot.clone());
2785
+ const cachedSnapshot = snapshot.clone();
2786
+ this.snapshotCache.put(location, cachedSnapshot);
2787
+ return cachedSnapshot;
2429
2788
  }
2430
2789
  }
2431
2790
  getCachedSnapshotForLocation(location) {
@@ -2439,34 +2798,81 @@ class PageView extends View {
2439
2798
  }
2440
2799
  }
2441
2800
 
2801
+ class Preloader {
2802
+ constructor(delegate) {
2803
+ this.selector = "a[data-turbo-preload]";
2804
+ this.delegate = delegate;
2805
+ }
2806
+ get snapshotCache() {
2807
+ return this.delegate.navigator.view.snapshotCache;
2808
+ }
2809
+ start() {
2810
+ if (document.readyState === "loading") {
2811
+ return document.addEventListener("DOMContentLoaded", (() => {
2812
+ this.preloadOnLoadLinksForView(document.body);
2813
+ }));
2814
+ } else {
2815
+ this.preloadOnLoadLinksForView(document.body);
2816
+ }
2817
+ }
2818
+ preloadOnLoadLinksForView(element) {
2819
+ for (const link of element.querySelectorAll(this.selector)) {
2820
+ this.preloadURL(link);
2821
+ }
2822
+ }
2823
+ async preloadURL(link) {
2824
+ const location = new URL(link.href);
2825
+ if (this.snapshotCache.has(location)) {
2826
+ return;
2827
+ }
2828
+ try {
2829
+ const response = await fetch(location.toString(), {
2830
+ headers: {
2831
+ "VND.PREFETCH": "true",
2832
+ Accept: "text/html"
2833
+ }
2834
+ });
2835
+ const responseText = await response.text();
2836
+ const snapshot = PageSnapshot.fromHTMLString(responseText);
2837
+ this.snapshotCache.put(location, snapshot);
2838
+ } catch (_) {}
2839
+ }
2840
+ }
2841
+
2442
2842
  class Session {
2443
2843
  constructor() {
2444
2844
  this.navigator = new Navigator(this);
2445
2845
  this.history = new History(this);
2846
+ this.preloader = new Preloader(this);
2446
2847
  this.view = new PageView(this, document.documentElement);
2447
2848
  this.adapter = new BrowserAdapter(this);
2448
2849
  this.pageObserver = new PageObserver(this);
2449
2850
  this.cacheObserver = new CacheObserver;
2450
- this.linkClickObserver = new LinkClickObserver(this);
2451
- this.formSubmitObserver = new FormSubmitObserver(this);
2851
+ this.linkClickObserver = new LinkClickObserver(this, window);
2852
+ this.formSubmitObserver = new FormSubmitObserver(this, document);
2452
2853
  this.scrollObserver = new ScrollObserver(this);
2453
2854
  this.streamObserver = new StreamObserver(this);
2454
- this.frameRedirector = new FrameRedirector(document.documentElement);
2855
+ this.formLinkClickObserver = new FormLinkClickObserver(this, document.documentElement);
2856
+ this.frameRedirector = new FrameRedirector(this, document.documentElement);
2857
+ this.streamMessageRenderer = new StreamMessageRenderer;
2455
2858
  this.drive = true;
2456
2859
  this.enabled = true;
2457
2860
  this.progressBarDelay = 500;
2458
2861
  this.started = false;
2862
+ this.formMode = "on";
2459
2863
  }
2460
2864
  start() {
2461
2865
  if (!this.started) {
2462
2866
  this.pageObserver.start();
2463
2867
  this.cacheObserver.start();
2868
+ this.formLinkClickObserver.start();
2464
2869
  this.linkClickObserver.start();
2465
2870
  this.formSubmitObserver.start();
2466
2871
  this.scrollObserver.start();
2467
2872
  this.streamObserver.start();
2468
2873
  this.frameRedirector.start();
2469
2874
  this.history.start();
2875
+ this.preloader.start();
2470
2876
  this.started = true;
2471
2877
  this.enabled = true;
2472
2878
  }
@@ -2478,6 +2884,7 @@ class Session {
2478
2884
  if (this.started) {
2479
2885
  this.pageObserver.stop();
2480
2886
  this.cacheObserver.stop();
2887
+ this.formLinkClickObserver.stop();
2481
2888
  this.linkClickObserver.stop();
2482
2889
  this.formSubmitObserver.stop();
2483
2890
  this.scrollObserver.stop();
@@ -2491,7 +2898,13 @@ class Session {
2491
2898
  this.adapter = adapter;
2492
2899
  }
2493
2900
  visit(location, options = {}) {
2494
- this.navigator.proposeVisit(expandURL(location), options);
2901
+ const frameElement = options.frame ? document.getElementById(options.frame) : null;
2902
+ if (frameElement instanceof FrameElement) {
2903
+ frameElement.src = location.toString();
2904
+ frameElement.loaded;
2905
+ } else {
2906
+ this.navigator.proposeVisit(expandURL(location), options);
2907
+ }
2495
2908
  }
2496
2909
  connectStreamSource(source) {
2497
2910
  this.streamObserver.connectStreamSource(source);
@@ -2500,7 +2913,7 @@ class Session {
2500
2913
  this.streamObserver.disconnectStreamSource(source);
2501
2914
  }
2502
2915
  renderStreamMessage(message) {
2503
- document.documentElement.appendChild(StreamMessage.wrap(message).fragment);
2916
+ this.streamMessageRenderer.render(StreamMessage.wrap(message));
2504
2917
  }
2505
2918
  clearCache() {
2506
2919
  this.view.clearSnapshotCache();
@@ -2508,6 +2921,9 @@ class Session {
2508
2921
  setProgressBarDelay(delay) {
2509
2922
  this.progressBarDelay = delay;
2510
2923
  }
2924
+ setFormMode(mode) {
2925
+ this.formMode = mode;
2926
+ }
2511
2927
  get location() {
2512
2928
  return this.history.location;
2513
2929
  }
@@ -2521,7 +2937,9 @@ class Session {
2521
2937
  historyChanged: true
2522
2938
  });
2523
2939
  } else {
2524
- this.adapter.pageInvalidated();
2940
+ this.adapter.pageInvalidated({
2941
+ reason: "turbo_disabled"
2942
+ });
2525
2943
  }
2526
2944
  }
2527
2945
  scrollPositionChanged(position) {
@@ -2529,30 +2947,21 @@ class Session {
2529
2947
  scrollPosition: position
2530
2948
  });
2531
2949
  }
2532
- willFollowLinkToLocation(link, location) {
2533
- return this.elementDriveEnabled(link) && this.locationIsVisitable(location) && this.applicationAllowsFollowingLinkToLocation(link, location);
2950
+ willSubmitFormLinkToLocation(link, location) {
2951
+ return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation);
2952
+ }
2953
+ submittedFormLinkToLocation() {}
2954
+ willFollowLinkToLocation(link, location, event) {
2955
+ return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation) && this.applicationAllowsFollowingLinkToLocation(link, location, event);
2534
2956
  }
2535
2957
  followedLinkToLocation(link, location) {
2536
2958
  const action = this.getActionForLink(link);
2537
- this.convertLinkWithMethodClickToFormSubmission(link) || this.visit(location.href, {
2538
- action: action
2959
+ const acceptsStreamResponse = link.hasAttribute("data-turbo-stream");
2960
+ this.visit(location.href, {
2961
+ action: action,
2962
+ acceptsStreamResponse: acceptsStreamResponse
2539
2963
  });
2540
2964
  }
2541
- convertLinkWithMethodClickToFormSubmission(link) {
2542
- const linkMethod = link.getAttribute("data-turbo-method");
2543
- if (linkMethod) {
2544
- const form = document.createElement("form");
2545
- form.method = linkMethod;
2546
- form.action = link.getAttribute("href") || "undefined";
2547
- document.body.appendChild(form);
2548
- return dispatch("submit", {
2549
- cancelable: true,
2550
- target: form
2551
- });
2552
- } else {
2553
- return false;
2554
- }
2555
- }
2556
2965
  allowsVisitingLocationWithAction(location, action) {
2557
2966
  return this.locationWithActionIsSamePage(location, action) || this.applicationAllowsVisitingLocation(location);
2558
2967
  }
@@ -2561,12 +2970,16 @@ class Session {
2561
2970
  this.adapter.visitProposedToLocation(location, options);
2562
2971
  }
2563
2972
  visitStarted(visit) {
2973
+ if (!visit.acceptsStreamResponse) {
2974
+ markAsBusy(document.documentElement);
2975
+ }
2564
2976
  extendURLWithDeprecatedProperties(visit.location);
2565
2977
  if (!visit.silent) {
2566
2978
  this.notifyApplicationAfterVisitingLocation(visit.location, visit.action);
2567
2979
  }
2568
2980
  }
2569
2981
  visitCompleted(visit) {
2982
+ clearBusyState(document.documentElement);
2570
2983
  this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
2571
2984
  }
2572
2985
  locationWithActionIsSamePage(location, action) {
@@ -2576,7 +2989,8 @@ class Session {
2576
2989
  this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
2577
2990
  }
2578
2991
  willSubmitForm(form, submitter) {
2579
- return this.elementDriveEnabled(form) && this.elementDriveEnabled(submitter);
2992
+ const action = getAction(form, submitter);
2993
+ return this.submissionIsNavigatable(form, submitter) && locationIsVisitable(expandURL(action), this.snapshot.rootLocation);
2580
2994
  }
2581
2995
  formSubmitted(form, submitter) {
2582
2996
  this.navigator.submitForm(form, submitter);
@@ -2600,16 +3014,23 @@ class Session {
2600
3014
  this.notifyApplicationBeforeCachingSnapshot();
2601
3015
  }
2602
3016
  }
2603
- allowsImmediateRender({element: element}, resume) {
2604
- const event = this.notifyApplicationBeforeRender(element, resume);
2605
- return !event.defaultPrevented;
3017
+ allowsImmediateRender({element: element}, options) {
3018
+ const event = this.notifyApplicationBeforeRender(element, options);
3019
+ const {defaultPrevented: defaultPrevented, detail: {render: render}} = event;
3020
+ if (this.view.renderer && render) {
3021
+ this.view.renderer.renderElement = render;
3022
+ }
3023
+ return !defaultPrevented;
2606
3024
  }
2607
- viewRenderedSnapshot(snapshot, isPreview) {
3025
+ viewRenderedSnapshot(_snapshot, _isPreview) {
2608
3026
  this.view.lastRenderedLocation = this.history.location;
2609
3027
  this.notifyApplicationAfterRender();
2610
3028
  }
2611
- viewInvalidated() {
2612
- this.adapter.pageInvalidated();
3029
+ preloadOnLoadLinksForView(element) {
3030
+ this.preloader.preloadOnLoadLinksForView(element);
3031
+ }
3032
+ viewInvalidated(reason) {
3033
+ this.adapter.pageInvalidated(reason);
2613
3034
  }
2614
3035
  frameLoaded(frame) {
2615
3036
  this.notifyApplicationAfterFrameLoad(frame);
@@ -2617,19 +3038,20 @@ class Session {
2617
3038
  frameRendered(fetchResponse, frame) {
2618
3039
  this.notifyApplicationAfterFrameRender(fetchResponse, frame);
2619
3040
  }
2620
- applicationAllowsFollowingLinkToLocation(link, location) {
2621
- const event = this.notifyApplicationAfterClickingLinkToLocation(link, location);
3041
+ applicationAllowsFollowingLinkToLocation(link, location, ev) {
3042
+ const event = this.notifyApplicationAfterClickingLinkToLocation(link, location, ev);
2622
3043
  return !event.defaultPrevented;
2623
3044
  }
2624
3045
  applicationAllowsVisitingLocation(location) {
2625
3046
  const event = this.notifyApplicationBeforeVisitingLocation(location);
2626
3047
  return !event.defaultPrevented;
2627
3048
  }
2628
- notifyApplicationAfterClickingLinkToLocation(link, location) {
3049
+ notifyApplicationAfterClickingLinkToLocation(link, location, event) {
2629
3050
  return dispatch("turbo:click", {
2630
3051
  target: link,
2631
3052
  detail: {
2632
- url: location.href
3053
+ url: location.href,
3054
+ originalEvent: event
2633
3055
  },
2634
3056
  cancelable: true
2635
3057
  });
@@ -2653,12 +3075,11 @@ class Session {
2653
3075
  notifyApplicationBeforeCachingSnapshot() {
2654
3076
  return dispatch("turbo:before-cache");
2655
3077
  }
2656
- notifyApplicationBeforeRender(newBody, resume) {
3078
+ notifyApplicationBeforeRender(newBody, options) {
2657
3079
  return dispatch("turbo:before-render", {
2658
- detail: {
2659
- newBody: newBody,
2660
- resume: resume
2661
- },
3080
+ detail: Object.assign({
3081
+ newBody: newBody
3082
+ }, options),
2662
3083
  cancelable: true
2663
3084
  });
2664
3085
  }
@@ -2693,9 +3114,22 @@ class Session {
2693
3114
  cancelable: true
2694
3115
  });
2695
3116
  }
2696
- elementDriveEnabled(element) {
2697
- const container = element === null || element === void 0 ? void 0 : element.closest("[data-turbo]");
2698
- if (this.drive) {
3117
+ submissionIsNavigatable(form, submitter) {
3118
+ if (this.formMode == "off") {
3119
+ return false;
3120
+ } else {
3121
+ const submitterIsNavigatable = submitter ? this.elementIsNavigatable(submitter) : true;
3122
+ if (this.formMode == "optin") {
3123
+ return submitterIsNavigatable && form.closest('[data-turbo="true"]') != null;
3124
+ } else {
3125
+ return submitterIsNavigatable && this.elementIsNavigatable(form);
3126
+ }
3127
+ }
3128
+ }
3129
+ elementIsNavigatable(element) {
3130
+ const container = element.closest("[data-turbo]");
3131
+ const withinFrame = element.closest("turbo-frame");
3132
+ if (this.drive || withinFrame) {
2699
3133
  if (container) {
2700
3134
  return container.getAttribute("data-turbo") != "false";
2701
3135
  } else {
@@ -2713,9 +3147,6 @@ class Session {
2713
3147
  const action = link.getAttribute("data-turbo-action");
2714
3148
  return isAction(action) ? action : "advance";
2715
3149
  }
2716
- locationIsVisitable(location) {
2717
- return isPrefixedBy(location, this.snapshot.rootLocation) && isHTML(location);
2718
- }
2719
3150
  get snapshot() {
2720
3151
  return this.view.snapshot;
2721
3152
  }
@@ -2733,9 +3164,64 @@ const deprecatedLocationPropertyDescriptors = {
2733
3164
  }
2734
3165
  };
2735
3166
 
3167
+ class Cache {
3168
+ constructor(session) {
3169
+ this.session = session;
3170
+ }
3171
+ clear() {
3172
+ this.session.clearCache();
3173
+ }
3174
+ resetCacheControl() {
3175
+ this.setCacheControl("");
3176
+ }
3177
+ exemptPageFromCache() {
3178
+ this.setCacheControl("no-cache");
3179
+ }
3180
+ exemptPageFromPreview() {
3181
+ this.setCacheControl("no-preview");
3182
+ }
3183
+ setCacheControl(value) {
3184
+ setMetaContent("turbo-cache-control", value);
3185
+ }
3186
+ }
3187
+
3188
+ const StreamActions = {
3189
+ after() {
3190
+ this.targetElements.forEach((e => {
3191
+ var _a;
3192
+ return (_a = e.parentElement) === null || _a === void 0 ? void 0 : _a.insertBefore(this.templateContent, e.nextSibling);
3193
+ }));
3194
+ },
3195
+ append() {
3196
+ this.removeDuplicateTargetChildren();
3197
+ this.targetElements.forEach((e => e.append(this.templateContent)));
3198
+ },
3199
+ before() {
3200
+ this.targetElements.forEach((e => {
3201
+ var _a;
3202
+ return (_a = e.parentElement) === null || _a === void 0 ? void 0 : _a.insertBefore(this.templateContent, e);
3203
+ }));
3204
+ },
3205
+ prepend() {
3206
+ this.removeDuplicateTargetChildren();
3207
+ this.targetElements.forEach((e => e.prepend(this.templateContent)));
3208
+ },
3209
+ remove() {
3210
+ this.targetElements.forEach((e => e.remove()));
3211
+ },
3212
+ replace() {
3213
+ this.targetElements.forEach((e => e.replaceWith(this.templateContent)));
3214
+ },
3215
+ update() {
3216
+ this.targetElements.forEach((e => e.replaceChildren(this.templateContent)));
3217
+ }
3218
+ };
3219
+
2736
3220
  const session = new Session;
2737
3221
 
2738
- const {navigator: navigator} = session;
3222
+ const cache = new Cache(session);
3223
+
3224
+ const {navigator: navigator$1} = session;
2739
3225
 
2740
3226
  function start() {
2741
3227
  session.start();
@@ -2762,6 +3248,7 @@ function renderStreamMessage(message) {
2762
3248
  }
2763
3249
 
2764
3250
  function clearCache() {
3251
+ console.warn("Please replace `Turbo.clearCache()` with `Turbo.cache.clear()`. The top-level function is deprecated and will be removed in a future version of Turbo.`");
2765
3252
  session.clearCache();
2766
3253
  }
2767
3254
 
@@ -2769,12 +3256,22 @@ function setProgressBarDelay(delay) {
2769
3256
  session.setProgressBarDelay(delay);
2770
3257
  }
2771
3258
 
3259
+ function setConfirmMethod(confirmMethod) {
3260
+ FormSubmission.confirmMethod = confirmMethod;
3261
+ }
3262
+
3263
+ function setFormMode(mode) {
3264
+ session.setFormMode(mode);
3265
+ }
3266
+
2772
3267
  var Turbo = Object.freeze({
2773
3268
  __proto__: null,
2774
- navigator: navigator,
3269
+ navigator: navigator$1,
2775
3270
  session: session,
3271
+ cache: cache,
2776
3272
  PageRenderer: PageRenderer,
2777
3273
  PageSnapshot: PageSnapshot,
3274
+ FrameRenderer: FrameRenderer,
2778
3275
  start: start,
2779
3276
  registerAdapter: registerAdapter,
2780
3277
  visit: visit,
@@ -2782,39 +3279,56 @@ var Turbo = Object.freeze({
2782
3279
  disconnectStreamSource: disconnectStreamSource,
2783
3280
  renderStreamMessage: renderStreamMessage,
2784
3281
  clearCache: clearCache,
2785
- setProgressBarDelay: setProgressBarDelay
3282
+ setProgressBarDelay: setProgressBarDelay,
3283
+ setConfirmMethod: setConfirmMethod,
3284
+ setFormMode: setFormMode,
3285
+ StreamActions: StreamActions
2786
3286
  });
2787
3287
 
2788
3288
  class FrameController {
2789
3289
  constructor(element) {
3290
+ this.fetchResponseLoaded = _fetchResponse => {};
3291
+ this.currentFetchRequest = null;
2790
3292
  this.resolveVisitPromise = () => {};
2791
3293
  this.connected = false;
2792
3294
  this.hasBeenLoaded = false;
2793
- this.settingSourceURL = false;
3295
+ this.ignoredAttributes = new Set;
3296
+ this.action = null;
3297
+ this.visitCachedSnapshot = ({element: element}) => {
3298
+ const frame = element.querySelector("#" + this.element.id);
3299
+ if (frame && this.previousFrameElement) {
3300
+ frame.replaceChildren(...this.previousFrameElement.children);
3301
+ }
3302
+ delete this.previousFrameElement;
3303
+ };
2794
3304
  this.element = element;
2795
3305
  this.view = new FrameView(this, this.element);
2796
3306
  this.appearanceObserver = new AppearanceObserver(this, this.element);
2797
- this.linkInterceptor = new LinkInterceptor(this, this.element);
2798
- this.formInterceptor = new FormInterceptor(this, this.element);
3307
+ this.formLinkClickObserver = new FormLinkClickObserver(this, this.element);
3308
+ this.linkClickObserver = new LinkClickObserver(this, this.element);
3309
+ this.restorationIdentifier = uuid();
3310
+ this.formSubmitObserver = new FormSubmitObserver(this, this.element);
2799
3311
  }
2800
3312
  connect() {
2801
3313
  if (!this.connected) {
2802
3314
  this.connected = true;
2803
- this.reloadable = false;
2804
3315
  if (this.loadingStyle == FrameLoadingStyle.lazy) {
2805
3316
  this.appearanceObserver.start();
3317
+ } else {
3318
+ this.loadSourceURL();
2806
3319
  }
2807
- this.linkInterceptor.start();
2808
- this.formInterceptor.start();
2809
- this.sourceURLChanged();
3320
+ this.formLinkClickObserver.start();
3321
+ this.linkClickObserver.start();
3322
+ this.formSubmitObserver.start();
2810
3323
  }
2811
3324
  }
2812
3325
  disconnect() {
2813
3326
  if (this.connected) {
2814
3327
  this.connected = false;
2815
3328
  this.appearanceObserver.stop();
2816
- this.linkInterceptor.stop();
2817
- this.formInterceptor.stop();
3329
+ this.formLinkClickObserver.stop();
3330
+ this.linkClickObserver.stop();
3331
+ this.formSubmitObserver.stop();
2818
3332
  }
2819
3333
  }
2820
3334
  disabledChanged() {
@@ -2823,10 +3337,18 @@ class FrameController {
2823
3337
  }
2824
3338
  }
2825
3339
  sourceURLChanged() {
3340
+ if (this.isIgnoringChangesTo("src")) return;
3341
+ if (this.element.isConnected) {
3342
+ this.complete = false;
3343
+ }
2826
3344
  if (this.loadingStyle == FrameLoadingStyle.eager || this.hasBeenLoaded) {
2827
3345
  this.loadSourceURL();
2828
3346
  }
2829
3347
  }
3348
+ completeChanged() {
3349
+ if (this.isIgnoringChangesTo("complete")) return;
3350
+ this.loadSourceURL();
3351
+ }
2830
3352
  loadingStyleChanged() {
2831
3353
  if (this.loadingStyle == FrameLoadingStyle.lazy) {
2832
3354
  this.appearanceObserver.start();
@@ -2836,103 +3358,107 @@ class FrameController {
2836
3358
  }
2837
3359
  }
2838
3360
  async loadSourceURL() {
2839
- if (!this.settingSourceURL && this.enabled && this.isActive && (this.reloadable || this.sourceURL != this.currentURL)) {
2840
- const previousURL = this.currentURL;
2841
- this.currentURL = this.sourceURL;
2842
- if (this.sourceURL) {
2843
- try {
2844
- this.element.loaded = this.visit(this.sourceURL);
2845
- this.appearanceObserver.stop();
2846
- await this.element.loaded;
2847
- this.hasBeenLoaded = true;
2848
- session.frameLoaded(this.element);
2849
- } catch (error) {
2850
- this.currentURL = previousURL;
2851
- throw error;
2852
- }
2853
- }
3361
+ if (this.enabled && this.isActive && !this.complete && this.sourceURL) {
3362
+ this.element.loaded = this.visit(expandURL(this.sourceURL));
3363
+ this.appearanceObserver.stop();
3364
+ await this.element.loaded;
3365
+ this.hasBeenLoaded = true;
2854
3366
  }
2855
3367
  }
2856
3368
  async loadResponse(fetchResponse) {
2857
- if (fetchResponse.redirected) {
3369
+ if (fetchResponse.redirected || fetchResponse.succeeded && fetchResponse.isHTML) {
2858
3370
  this.sourceURL = fetchResponse.response.url;
2859
3371
  }
2860
3372
  try {
2861
3373
  const html = await fetchResponse.responseHTML;
2862
3374
  if (html) {
2863
3375
  const {body: body} = parseHTMLDocument(html);
2864
- const snapshot = new Snapshot(await this.extractForeignFrameElement(body));
2865
- const renderer = new FrameRenderer(this.view.snapshot, snapshot, false);
2866
- if (this.view.renderPromise) await this.view.renderPromise;
2867
- await this.view.render(renderer);
2868
- session.frameRendered(fetchResponse, this.element);
3376
+ const newFrameElement = await this.extractForeignFrameElement(body);
3377
+ if (newFrameElement) {
3378
+ const snapshot = new Snapshot(newFrameElement);
3379
+ const renderer = new FrameRenderer(this, this.view.snapshot, snapshot, FrameRenderer.renderElement, false, false);
3380
+ if (this.view.renderPromise) await this.view.renderPromise;
3381
+ this.changeHistory();
3382
+ await this.view.render(renderer);
3383
+ this.complete = true;
3384
+ session.frameRendered(fetchResponse, this.element);
3385
+ session.frameLoaded(this.element);
3386
+ this.fetchResponseLoaded(fetchResponse);
3387
+ } else if (this.willHandleFrameMissingFromResponse(fetchResponse)) {
3388
+ console.warn(`A matching frame for #${this.element.id} was missing from the response, transforming into full-page Visit.`);
3389
+ this.visitResponse(fetchResponse.response);
3390
+ }
2869
3391
  }
2870
3392
  } catch (error) {
2871
3393
  console.error(error);
2872
3394
  this.view.invalidate();
3395
+ } finally {
3396
+ this.fetchResponseLoaded = () => {};
2873
3397
  }
2874
3398
  }
2875
- elementAppearedInViewport(element) {
3399
+ elementAppearedInViewport(_element) {
2876
3400
  this.loadSourceURL();
2877
3401
  }
2878
- shouldInterceptLinkClick(element, url) {
2879
- if (element.hasAttribute("data-turbo-method")) {
2880
- return false;
2881
- } else {
2882
- return this.shouldInterceptNavigation(element);
2883
- }
3402
+ willSubmitFormLinkToLocation(link) {
3403
+ return link.closest("turbo-frame") == this.element && this.shouldInterceptNavigation(link);
3404
+ }
3405
+ submittedFormLinkToLocation(link, _location, form) {
3406
+ const frame = this.findFrameElement(link);
3407
+ if (frame) form.setAttribute("data-turbo-frame", frame.id);
3408
+ }
3409
+ willFollowLinkToLocation(element, location, event) {
3410
+ return this.shouldInterceptNavigation(element) && this.frameAllowsVisitingLocation(element, location, event);
2884
3411
  }
2885
- linkClickIntercepted(element, url) {
2886
- this.reloadable = true;
2887
- this.navigateFrame(element, url);
3412
+ followedLinkToLocation(element, location) {
3413
+ this.navigateFrame(element, location.href);
2888
3414
  }
2889
- shouldInterceptFormSubmission(element, submitter) {
2890
- return this.shouldInterceptNavigation(element, submitter);
3415
+ willSubmitForm(element, submitter) {
3416
+ return element.closest("turbo-frame") == this.element && this.shouldInterceptNavigation(element, submitter);
2891
3417
  }
2892
- formSubmissionIntercepted(element, submitter) {
3418
+ formSubmitted(element, submitter) {
2893
3419
  if (this.formSubmission) {
2894
3420
  this.formSubmission.stop();
2895
3421
  }
2896
- this.reloadable = false;
2897
3422
  this.formSubmission = new FormSubmission(this, element, submitter);
2898
- if (this.formSubmission.fetchRequest.isIdempotent) {
2899
- this.navigateFrame(element, this.formSubmission.fetchRequest.url.href);
2900
- } else {
2901
- const {fetchRequest: fetchRequest} = this.formSubmission;
2902
- this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
2903
- this.formSubmission.start();
2904
- }
3423
+ const {fetchRequest: fetchRequest} = this.formSubmission;
3424
+ this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest);
3425
+ this.formSubmission.start();
2905
3426
  }
2906
3427
  prepareHeadersForRequest(headers, request) {
3428
+ var _a;
2907
3429
  headers["Turbo-Frame"] = this.id;
3430
+ if ((_a = this.currentNavigationElement) === null || _a === void 0 ? void 0 : _a.hasAttribute("data-turbo-stream")) {
3431
+ request.acceptResponseType(StreamMessage.contentType);
3432
+ }
2908
3433
  }
2909
- requestStarted(request) {
2910
- this.element.setAttribute("busy", "");
3434
+ requestStarted(_request) {
3435
+ markAsBusy(this.element);
2911
3436
  }
2912
- requestPreventedHandlingResponse(request, response) {
3437
+ requestPreventedHandlingResponse(_request, _response) {
2913
3438
  this.resolveVisitPromise();
2914
3439
  }
2915
3440
  async requestSucceededWithResponse(request, response) {
2916
3441
  await this.loadResponse(response);
2917
3442
  this.resolveVisitPromise();
2918
3443
  }
2919
- requestFailedWithResponse(request, response) {
3444
+ async requestFailedWithResponse(request, response) {
2920
3445
  console.error(response);
3446
+ await this.loadResponse(response);
2921
3447
  this.resolveVisitPromise();
2922
3448
  }
2923
3449
  requestErrored(request, error) {
2924
3450
  console.error(error);
2925
3451
  this.resolveVisitPromise();
2926
3452
  }
2927
- requestFinished(request) {
2928
- this.element.removeAttribute("busy");
3453
+ requestFinished(_request) {
3454
+ clearBusyState(this.element);
2929
3455
  }
2930
- formSubmissionStarted(formSubmission) {
2931
- const frame = this.findFrameElement(formSubmission.formElement);
2932
- frame.setAttribute("busy", "");
3456
+ formSubmissionStarted({formElement: formElement}) {
3457
+ markAsBusy(formElement, this.findFrameElement(formElement));
2933
3458
  }
2934
3459
  formSubmissionSucceededWithResponse(formSubmission, response) {
2935
- const frame = this.findFrameElement(formSubmission.formElement);
3460
+ const frame = this.findFrameElement(formSubmission.formElement, formSubmission.submitter);
3461
+ this.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
2936
3462
  frame.delegate.loadResponse(response);
2937
3463
  }
2938
3464
  formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
@@ -2941,53 +3467,150 @@ class FrameController {
2941
3467
  formSubmissionErrored(formSubmission, error) {
2942
3468
  console.error(error);
2943
3469
  }
2944
- formSubmissionFinished(formSubmission) {
2945
- const frame = this.findFrameElement(formSubmission.formElement);
2946
- frame.removeAttribute("busy");
3470
+ formSubmissionFinished({formElement: formElement}) {
3471
+ clearBusyState(formElement, this.findFrameElement(formElement));
2947
3472
  }
2948
- allowsImmediateRender(snapshot, resume) {
2949
- return true;
3473
+ allowsImmediateRender({element: newFrame}, options) {
3474
+ const event = dispatch("turbo:before-frame-render", {
3475
+ target: this.element,
3476
+ detail: Object.assign({
3477
+ newFrame: newFrame
3478
+ }, options),
3479
+ cancelable: true
3480
+ });
3481
+ const {defaultPrevented: defaultPrevented, detail: {render: render}} = event;
3482
+ if (this.view.renderer && render) {
3483
+ this.view.renderer.renderElement = render;
3484
+ }
3485
+ return !defaultPrevented;
3486
+ }
3487
+ viewRenderedSnapshot(_snapshot, _isPreview) {}
3488
+ preloadOnLoadLinksForView(element) {
3489
+ session.preloadOnLoadLinksForView(element);
2950
3490
  }
2951
- viewRenderedSnapshot(snapshot, isPreview) {}
2952
3491
  viewInvalidated() {}
3492
+ willRenderFrame(currentElement, _newElement) {
3493
+ this.previousFrameElement = currentElement.cloneNode(true);
3494
+ }
2953
3495
  async visit(url) {
2954
- const request = new FetchRequest(this, FetchMethod.get, expandURL(url));
3496
+ var _a;
3497
+ const request = new FetchRequest(this, FetchMethod.get, url, new URLSearchParams, this.element);
3498
+ (_a = this.currentFetchRequest) === null || _a === void 0 ? void 0 : _a.cancel();
3499
+ this.currentFetchRequest = request;
2955
3500
  return new Promise((resolve => {
2956
3501
  this.resolveVisitPromise = () => {
2957
3502
  this.resolveVisitPromise = () => {};
3503
+ this.currentFetchRequest = null;
2958
3504
  resolve();
2959
3505
  };
2960
3506
  request.perform();
2961
3507
  }));
2962
3508
  }
2963
- navigateFrame(element, url) {
2964
- const frame = this.findFrameElement(element);
2965
- frame.src = url;
3509
+ navigateFrame(element, url, submitter) {
3510
+ const frame = this.findFrameElement(element, submitter);
3511
+ this.proposeVisitIfNavigatedWithAction(frame, element, submitter);
3512
+ this.withCurrentNavigationElement(element, (() => {
3513
+ frame.src = url;
3514
+ }));
3515
+ }
3516
+ proposeVisitIfNavigatedWithAction(frame, element, submitter) {
3517
+ this.action = getVisitAction(submitter, element, frame);
3518
+ this.frame = frame;
3519
+ if (isAction(this.action)) {
3520
+ const {visitCachedSnapshot: visitCachedSnapshot} = frame.delegate;
3521
+ frame.delegate.fetchResponseLoaded = fetchResponse => {
3522
+ if (frame.src) {
3523
+ const {statusCode: statusCode, redirected: redirected} = fetchResponse;
3524
+ const responseHTML = frame.ownerDocument.documentElement.outerHTML;
3525
+ const response = {
3526
+ statusCode: statusCode,
3527
+ redirected: redirected,
3528
+ responseHTML: responseHTML
3529
+ };
3530
+ const options = {
3531
+ response: response,
3532
+ visitCachedSnapshot: visitCachedSnapshot,
3533
+ willRender: false,
3534
+ updateHistory: false,
3535
+ restorationIdentifier: this.restorationIdentifier
3536
+ };
3537
+ if (this.action) options.action = this.action;
3538
+ session.visit(frame.src, options);
3539
+ }
3540
+ };
3541
+ }
3542
+ }
3543
+ changeHistory() {
3544
+ if (this.action && this.frame) {
3545
+ const method = getHistoryMethodForAction(this.action);
3546
+ session.history.update(method, expandURL(this.frame.src || ""), this.restorationIdentifier);
3547
+ }
3548
+ }
3549
+ willHandleFrameMissingFromResponse(fetchResponse) {
3550
+ this.element.setAttribute("complete", "");
3551
+ const response = fetchResponse.response;
3552
+ const visit = async (url, options = {}) => {
3553
+ if (url instanceof Response) {
3554
+ this.visitResponse(url);
3555
+ } else {
3556
+ session.visit(url, options);
3557
+ }
3558
+ };
3559
+ const event = dispatch("turbo:frame-missing", {
3560
+ target: this.element,
3561
+ detail: {
3562
+ response: response,
3563
+ visit: visit
3564
+ },
3565
+ cancelable: true
3566
+ });
3567
+ return !event.defaultPrevented;
3568
+ }
3569
+ async visitResponse(response) {
3570
+ const wrapped = new FetchResponse(response);
3571
+ const responseHTML = await wrapped.responseHTML;
3572
+ const {location: location, redirected: redirected, statusCode: statusCode} = wrapped;
3573
+ return session.visit(location, {
3574
+ response: {
3575
+ redirected: redirected,
3576
+ statusCode: statusCode,
3577
+ responseHTML: responseHTML
3578
+ }
3579
+ });
2966
3580
  }
2967
- findFrameElement(element) {
3581
+ findFrameElement(element, submitter) {
2968
3582
  var _a;
2969
- const id = element.getAttribute("data-turbo-frame") || this.element.getAttribute("target");
3583
+ const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
2970
3584
  return (_a = getFrameElementById(id)) !== null && _a !== void 0 ? _a : this.element;
2971
3585
  }
2972
3586
  async extractForeignFrameElement(container) {
2973
3587
  let element;
2974
3588
  const id = CSS.escape(this.id);
2975
3589
  try {
2976
- if (element = activateElement(container.querySelector(`turbo-frame#${id}`), this.currentURL)) {
3590
+ element = activateElement(container.querySelector(`turbo-frame#${id}`), this.sourceURL);
3591
+ if (element) {
2977
3592
  return element;
2978
3593
  }
2979
- if (element = activateElement(container.querySelector(`turbo-frame[src][recurse~=${id}]`), this.currentURL)) {
3594
+ element = activateElement(container.querySelector(`turbo-frame[src][recurse~=${id}]`), this.sourceURL);
3595
+ if (element) {
2980
3596
  await element.loaded;
2981
3597
  return await this.extractForeignFrameElement(element);
2982
3598
  }
2983
- console.error(`Response has no matching <turbo-frame id="${id}"> element`);
2984
3599
  } catch (error) {
2985
3600
  console.error(error);
3601
+ return new FrameElement;
2986
3602
  }
2987
- return new FrameElement;
3603
+ return null;
3604
+ }
3605
+ formActionIsVisitable(form, submitter) {
3606
+ const action = getAction(form, submitter);
3607
+ return locationIsVisitable(expandURL(action), this.rootLocation);
2988
3608
  }
2989
3609
  shouldInterceptNavigation(element, submitter) {
2990
- const id = element.getAttribute("data-turbo-frame") || this.element.getAttribute("target");
3610
+ const id = getAttribute("data-turbo-frame", submitter, element) || this.element.getAttribute("target");
3611
+ if (element instanceof HTMLFormElement && !this.formActionIsVisitable(element, submitter)) {
3612
+ return false;
3613
+ }
2991
3614
  if (!this.enabled || id == "_top") {
2992
3615
  return false;
2993
3616
  }
@@ -2997,10 +3620,10 @@ class FrameController {
2997
3620
  return !frameElement.disabled;
2998
3621
  }
2999
3622
  }
3000
- if (!session.elementDriveEnabled(element)) {
3623
+ if (!session.elementIsNavigatable(element)) {
3001
3624
  return false;
3002
3625
  }
3003
- if (submitter && !session.elementDriveEnabled(submitter)) {
3626
+ if (submitter && !session.elementIsNavigatable(submitter)) {
3004
3627
  return false;
3005
3628
  }
3006
3629
  return true;
@@ -3016,23 +3639,10 @@ class FrameController {
3016
3639
  return this.element.src;
3017
3640
  }
3018
3641
  }
3019
- get reloadable() {
3020
- const frame = this.findFrameElement(this.element);
3021
- return frame.hasAttribute("reloadable");
3022
- }
3023
- set reloadable(value) {
3024
- const frame = this.findFrameElement(this.element);
3025
- if (value) {
3026
- frame.setAttribute("reloadable", "");
3027
- } else {
3028
- frame.removeAttribute("reloadable");
3029
- }
3030
- }
3031
3642
  set sourceURL(sourceURL) {
3032
- this.settingSourceURL = true;
3033
- this.element.src = sourceURL !== null && sourceURL !== void 0 ? sourceURL : null;
3034
- this.currentURL = this.element.src;
3035
- this.settingSourceURL = false;
3643
+ this.ignoringChangesToAttribute("src", (() => {
3644
+ this.element.src = sourceURL !== null && sourceURL !== void 0 ? sourceURL : null;
3645
+ }));
3036
3646
  }
3037
3647
  get loadingStyle() {
3038
3648
  return this.element.loading;
@@ -3040,9 +3650,51 @@ class FrameController {
3040
3650
  get isLoading() {
3041
3651
  return this.formSubmission !== undefined || this.resolveVisitPromise() !== undefined;
3042
3652
  }
3653
+ get complete() {
3654
+ return this.element.hasAttribute("complete");
3655
+ }
3656
+ set complete(value) {
3657
+ this.ignoringChangesToAttribute("complete", (() => {
3658
+ if (value) {
3659
+ this.element.setAttribute("complete", "");
3660
+ } else {
3661
+ this.element.removeAttribute("complete");
3662
+ }
3663
+ }));
3664
+ }
3043
3665
  get isActive() {
3044
3666
  return this.element.isActive && this.connected;
3045
3667
  }
3668
+ get rootLocation() {
3669
+ var _a;
3670
+ const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
3671
+ const root = (_a = meta === null || meta === void 0 ? void 0 : meta.content) !== null && _a !== void 0 ? _a : "/";
3672
+ return expandURL(root);
3673
+ }
3674
+ frameAllowsVisitingLocation(target, {href: url}, originalEvent) {
3675
+ const event = dispatch("turbo:click", {
3676
+ target: target,
3677
+ detail: {
3678
+ url: url,
3679
+ originalEvent: originalEvent
3680
+ },
3681
+ cancelable: true
3682
+ });
3683
+ return !event.defaultPrevented;
3684
+ }
3685
+ isIgnoringChangesTo(attributeName) {
3686
+ return this.ignoredAttributes.has(attributeName);
3687
+ }
3688
+ ignoringChangesToAttribute(attributeName, callback) {
3689
+ this.ignoredAttributes.add(attributeName);
3690
+ callback();
3691
+ this.ignoredAttributes.delete(attributeName);
3692
+ }
3693
+ withCurrentNavigationElement(element, callback) {
3694
+ this.currentNavigationElement = element;
3695
+ callback();
3696
+ delete this.currentNavigationElement;
3697
+ }
3046
3698
  }
3047
3699
 
3048
3700
  function getFrameElementById(id) {
@@ -3065,47 +3717,16 @@ function activateElement(element, currentURL) {
3065
3717
  }
3066
3718
  if (element instanceof FrameElement) {
3067
3719
  element.connectedCallback();
3720
+ element.disconnectedCallback();
3068
3721
  return element;
3069
3722
  }
3070
3723
  }
3071
3724
  }
3072
3725
 
3073
- const StreamActions = {
3074
- after() {
3075
- this.targetElements.forEach((e => {
3076
- var _a;
3077
- return (_a = e.parentElement) === null || _a === void 0 ? void 0 : _a.insertBefore(this.templateContent, e.nextSibling);
3078
- }));
3079
- },
3080
- append() {
3081
- this.removeDuplicateTargetChildren();
3082
- this.targetElements.forEach((e => e.append(this.templateContent)));
3083
- },
3084
- before() {
3085
- this.targetElements.forEach((e => {
3086
- var _a;
3087
- return (_a = e.parentElement) === null || _a === void 0 ? void 0 : _a.insertBefore(this.templateContent, e);
3088
- }));
3089
- },
3090
- prepend() {
3091
- this.removeDuplicateTargetChildren();
3092
- this.targetElements.forEach((e => e.prepend(this.templateContent)));
3093
- },
3094
- remove() {
3095
- this.targetElements.forEach((e => e.remove()));
3096
- },
3097
- replace() {
3098
- this.targetElements.forEach((e => e.replaceWith(this.templateContent)));
3099
- },
3100
- update() {
3101
- this.targetElements.forEach((e => {
3102
- e.innerHTML = "";
3103
- e.append(this.templateContent);
3104
- }));
3105
- }
3106
- };
3107
-
3108
3726
  class StreamElement extends HTMLElement {
3727
+ static async renderElement(newElement) {
3728
+ await newElement.performAction();
3729
+ }
3109
3730
  async connectedCallback() {
3110
3731
  try {
3111
3732
  await this.render();
@@ -3118,9 +3739,10 @@ class StreamElement extends HTMLElement {
3118
3739
  async render() {
3119
3740
  var _a;
3120
3741
  return (_a = this.renderPromise) !== null && _a !== void 0 ? _a : this.renderPromise = (async () => {
3121
- if (this.dispatchEvent(this.beforeRenderEvent)) {
3742
+ const event = this.beforeRenderEvent;
3743
+ if (this.dispatchEvent(event)) {
3122
3744
  await nextAnimationFrame();
3123
- this.performAction();
3745
+ await event.detail.render(this);
3124
3746
  }
3125
3747
  })();
3126
3748
  }
@@ -3135,7 +3757,7 @@ class StreamElement extends HTMLElement {
3135
3757
  get duplicateChildren() {
3136
3758
  var _a;
3137
3759
  const existingChildren = this.targetElements.flatMap((e => [ ...e.children ])).filter((c => !!c.id));
3138
- const newChildrenIds = [ ...(_a = this.templateContent) === null || _a === void 0 ? void 0 : _a.children ].filter((c => !!c.id)).map((c => c.id));
3760
+ const newChildrenIds = [ ...((_a = this.templateContent) === null || _a === void 0 ? void 0 : _a.children) || [] ].filter((c => !!c.id)).map((c => c.id));
3139
3761
  return existingChildren.filter((c => newChildrenIds.includes(c.id)));
3140
3762
  }
3141
3763
  get performAction() {
@@ -3161,7 +3783,11 @@ class StreamElement extends HTMLElement {
3161
3783
  return this.templateElement.content.cloneNode(true);
3162
3784
  }
3163
3785
  get templateElement() {
3164
- if (this.firstElementChild instanceof HTMLTemplateElement) {
3786
+ if (this.firstElementChild === null) {
3787
+ const template = this.ownerDocument.createElement("template");
3788
+ this.appendChild(template);
3789
+ return template;
3790
+ } else if (this.firstElementChild instanceof HTMLTemplateElement) {
3165
3791
  return this.firstElementChild;
3166
3792
  }
3167
3793
  this.raise("first child element must be a <template> element");
@@ -3185,7 +3811,11 @@ class StreamElement extends HTMLElement {
3185
3811
  get beforeRenderEvent() {
3186
3812
  return new CustomEvent("turbo:before-stream-render", {
3187
3813
  bubbles: true,
3188
- cancelable: true
3814
+ cancelable: true,
3815
+ detail: {
3816
+ newStream: this,
3817
+ render: StreamElement.renderElement
3818
+ }
3189
3819
  });
3190
3820
  }
3191
3821
  get targetElementsById() {
@@ -3208,17 +3838,45 @@ class StreamElement extends HTMLElement {
3208
3838
  }
3209
3839
  }
3210
3840
 
3841
+ class StreamSourceElement extends HTMLElement {
3842
+ constructor() {
3843
+ super(...arguments);
3844
+ this.streamSource = null;
3845
+ }
3846
+ connectedCallback() {
3847
+ this.streamSource = this.src.match(/^ws{1,2}:/) ? new WebSocket(this.src) : new EventSource(this.src);
3848
+ connectStreamSource(this.streamSource);
3849
+ }
3850
+ disconnectedCallback() {
3851
+ if (this.streamSource) {
3852
+ disconnectStreamSource(this.streamSource);
3853
+ }
3854
+ }
3855
+ get src() {
3856
+ return this.getAttribute("src") || "";
3857
+ }
3858
+ }
3859
+
3211
3860
  FrameElement.delegateConstructor = FrameController;
3212
3861
 
3213
- customElements.define("turbo-frame", FrameElement);
3862
+ if (customElements.get("turbo-frame") === undefined) {
3863
+ customElements.define("turbo-frame", FrameElement);
3864
+ }
3214
3865
 
3215
- customElements.define("turbo-stream", StreamElement);
3866
+ if (customElements.get("turbo-stream") === undefined) {
3867
+ customElements.define("turbo-stream", StreamElement);
3868
+ }
3869
+
3870
+ if (customElements.get("turbo-stream-source") === undefined) {
3871
+ customElements.define("turbo-stream-source", StreamSourceElement);
3872
+ }
3216
3873
 
3217
3874
  (() => {
3218
3875
  let element = document.currentScript;
3219
3876
  if (!element) return;
3220
3877
  if (element.hasAttribute("data-turbo-suppress-warning")) return;
3221
- while (element = element.parentElement) {
3878
+ element = element.parentElement;
3879
+ while (element) {
3222
3880
  if (element == document.body) {
3223
3881
  return console.warn(unindent`
3224
3882
  You are loading Turbo from a <script> element inside the <body> element. This is probably not what you meant to do!
@@ -3231,6 +3889,7 @@ customElements.define("turbo-stream", StreamElement);
3231
3889
  Suppress this warning by adding a "data-turbo-suppress-warning" attribute to: %s
3232
3890
  `, element.outerHTML);
3233
3891
  }
3892
+ element = element.parentElement;
3234
3893
  }
3235
3894
  })();
3236
3895
 
@@ -3240,15 +3899,26 @@ start();
3240
3899
 
3241
3900
  var turbo_es2017Esm = Object.freeze({
3242
3901
  __proto__: null,
3902
+ FrameElement: FrameElement,
3903
+ get FrameLoadingStyle() {
3904
+ return FrameLoadingStyle;
3905
+ },
3906
+ FrameRenderer: FrameRenderer,
3243
3907
  PageRenderer: PageRenderer,
3244
3908
  PageSnapshot: PageSnapshot,
3909
+ StreamActions: StreamActions,
3910
+ StreamElement: StreamElement,
3911
+ StreamSourceElement: StreamSourceElement,
3912
+ cache: cache,
3245
3913
  clearCache: clearCache,
3246
3914
  connectStreamSource: connectStreamSource,
3247
3915
  disconnectStreamSource: disconnectStreamSource,
3248
- navigator: navigator,
3916
+ navigator: navigator$1,
3249
3917
  registerAdapter: registerAdapter,
3250
3918
  renderStreamMessage: renderStreamMessage,
3251
3919
  session: session,
3920
+ setConfirmMethod: setConfirmMethod,
3921
+ setFormMode: setFormMode,
3252
3922
  setProgressBarDelay: setProgressBarDelay,
3253
3923
  start: start,
3254
3924
  visit: visit
@@ -3284,6 +3954,19 @@ var cable = Object.freeze({
3284
3954
  subscribeTo: subscribeTo
3285
3955
  });
3286
3956
 
3957
+ function walk(obj) {
3958
+ if (!obj || typeof obj !== "object") return obj;
3959
+ if (obj instanceof Date || obj instanceof RegExp) return obj;
3960
+ if (Array.isArray(obj)) return obj.map(walk);
3961
+ return Object.keys(obj).reduce((function(acc, key) {
3962
+ var camel = key[0].toLowerCase() + key.slice(1).replace(/([A-Z]+)/g, (function(m, x) {
3963
+ return "_" + x.toLowerCase();
3964
+ }));
3965
+ acc[camel] = walk(obj[key]);
3966
+ return acc;
3967
+ }), {});
3968
+ }
3969
+
3287
3970
  class TurboCableStreamSourceElement extends HTMLElement {
3288
3971
  async connectedCallback() {
3289
3972
  connectStreamSource(this);
@@ -3306,13 +3989,37 @@ class TurboCableStreamSourceElement extends HTMLElement {
3306
3989
  const signed_stream_name = this.getAttribute("signed-stream-name");
3307
3990
  return {
3308
3991
  channel: channel,
3309
- signed_stream_name: signed_stream_name
3992
+ signed_stream_name: signed_stream_name,
3993
+ ...walk({
3994
+ ...this.dataset
3995
+ })
3310
3996
  };
3311
3997
  }
3312
3998
  }
3313
3999
 
3314
4000
  customElements.define("turbo-cable-stream-source", TurboCableStreamSourceElement);
3315
4001
 
4002
+ function encodeMethodIntoRequestBody(event) {
4003
+ if (event.target instanceof HTMLFormElement) {
4004
+ const {target: form, detail: {fetchOptions: fetchOptions}} = event;
4005
+ form.addEventListener("turbo:submit-start", (({detail: {formSubmission: {submitter: submitter}}}) => {
4006
+ const method = submitter && submitter.formMethod || fetchOptions.body && fetchOptions.body.get("_method") || form.getAttribute("method");
4007
+ if (!/get/i.test(method)) {
4008
+ if (/post/i.test(method)) {
4009
+ fetchOptions.body.delete("_method");
4010
+ } else {
4011
+ fetchOptions.body.set("_method", method);
4012
+ }
4013
+ fetchOptions.method = "post";
4014
+ }
4015
+ }), {
4016
+ once: true
4017
+ });
4018
+ }
4019
+ }
4020
+
4021
+ addEventListener("turbo:before-fetch-request", encodeMethodIntoRequestBody);
4022
+
3316
4023
  var adapters = {
3317
4024
  logger: self.console,
3318
4025
  WebSocket: self.WebSocket
@@ -3331,8 +4038,6 @@ const now = () => (new Date).getTime();
3331
4038
 
3332
4039
  const secondsSince = time => (now() - time) / 1e3;
3333
4040
 
3334
- const clamp = (number, min, max) => Math.max(min, Math.min(max, number));
3335
-
3336
4041
  class ConnectionMonitor {
3337
4042
  constructor(connection) {
3338
4043
  this.visibilityDidChange = this.visibilityDidChange.bind(this);
@@ -3345,7 +4050,7 @@ class ConnectionMonitor {
3345
4050
  delete this.stoppedAt;
3346
4051
  this.startPolling();
3347
4052
  addEventListener("visibilitychange", this.visibilityDidChange);
3348
- logger.log(`ConnectionMonitor started. pollInterval = ${this.getPollInterval()} ms`);
4053
+ logger.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`);
3349
4054
  }
3350
4055
  }
3351
4056
  stop() {
@@ -3386,24 +4091,29 @@ class ConnectionMonitor {
3386
4091
  }), this.getPollInterval());
3387
4092
  }
3388
4093
  getPollInterval() {
3389
- const {min: min, max: max, multiplier: multiplier} = this.constructor.pollInterval;
3390
- const interval = multiplier * Math.log(this.reconnectAttempts + 1);
3391
- return Math.round(clamp(interval, min, max) * 1e3);
4094
+ const {staleThreshold: staleThreshold, reconnectionBackoffRate: reconnectionBackoffRate} = this.constructor;
4095
+ const backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10));
4096
+ const jitterMax = this.reconnectAttempts === 0 ? 1 : reconnectionBackoffRate;
4097
+ const jitter = jitterMax * Math.random();
4098
+ return staleThreshold * 1e3 * backoff * (1 + jitter);
3392
4099
  }
3393
4100
  reconnectIfStale() {
3394
4101
  if (this.connectionIsStale()) {
3395
- logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, pollInterval = ${this.getPollInterval()} ms, time disconnected = ${secondsSince(this.disconnectedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`);
4102
+ logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`);
3396
4103
  this.reconnectAttempts++;
3397
4104
  if (this.disconnectedRecently()) {
3398
- logger.log("ConnectionMonitor skipping reopening recent disconnect");
4105
+ logger.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(this.disconnectedAt)} s`);
3399
4106
  } else {
3400
4107
  logger.log("ConnectionMonitor reopening");
3401
4108
  this.connection.reopen();
3402
4109
  }
3403
4110
  }
3404
4111
  }
4112
+ get refreshedAt() {
4113
+ return this.pingedAt ? this.pingedAt : this.startedAt;
4114
+ }
3405
4115
  connectionIsStale() {
3406
- return secondsSince(this.pingedAt ? this.pingedAt : this.startedAt) > this.constructor.staleThreshold;
4116
+ return secondsSince(this.refreshedAt) > this.constructor.staleThreshold;
3407
4117
  }
3408
4118
  disconnectedRecently() {
3409
4119
  return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold;
@@ -3420,14 +4130,10 @@ class ConnectionMonitor {
3420
4130
  }
3421
4131
  }
3422
4132
 
3423
- ConnectionMonitor.pollInterval = {
3424
- min: 3,
3425
- max: 30,
3426
- multiplier: 5
3427
- };
3428
-
3429
4133
  ConnectionMonitor.staleThreshold = 6;
3430
4134
 
4135
+ ConnectionMonitor.reconnectionBackoffRate = .15;
4136
+
3431
4137
  var INTERNAL = {
3432
4138
  message_types: {
3433
4139
  welcome: "welcome",
@@ -3570,6 +4276,7 @@ Connection.prototype.events = {
3570
4276
  return this.monitor.recordPing();
3571
4277
 
3572
4278
  case message_types.confirmation:
4279
+ this.subscriptions.confirmSubscription(identifier);
3573
4280
  return this.subscriptions.notify(identifier, "connected");
3574
4281
 
3575
4282
  case message_types.rejection:
@@ -3637,9 +4344,47 @@ class Subscription {
3637
4344
  }
3638
4345
  }
3639
4346
 
4347
+ class SubscriptionGuarantor {
4348
+ constructor(subscriptions) {
4349
+ this.subscriptions = subscriptions;
4350
+ this.pendingSubscriptions = [];
4351
+ }
4352
+ guarantee(subscription) {
4353
+ if (this.pendingSubscriptions.indexOf(subscription) == -1) {
4354
+ logger.log(`SubscriptionGuarantor guaranteeing ${subscription.identifier}`);
4355
+ this.pendingSubscriptions.push(subscription);
4356
+ } else {
4357
+ logger.log(`SubscriptionGuarantor already guaranteeing ${subscription.identifier}`);
4358
+ }
4359
+ this.startGuaranteeing();
4360
+ }
4361
+ forget(subscription) {
4362
+ logger.log(`SubscriptionGuarantor forgetting ${subscription.identifier}`);
4363
+ this.pendingSubscriptions = this.pendingSubscriptions.filter((s => s !== subscription));
4364
+ }
4365
+ startGuaranteeing() {
4366
+ this.stopGuaranteeing();
4367
+ this.retrySubscribing();
4368
+ }
4369
+ stopGuaranteeing() {
4370
+ clearTimeout(this.retryTimeout);
4371
+ }
4372
+ retrySubscribing() {
4373
+ this.retryTimeout = setTimeout((() => {
4374
+ if (this.subscriptions && typeof this.subscriptions.subscribe === "function") {
4375
+ this.pendingSubscriptions.map((subscription => {
4376
+ logger.log(`SubscriptionGuarantor resubscribing ${subscription.identifier}`);
4377
+ this.subscriptions.subscribe(subscription);
4378
+ }));
4379
+ }
4380
+ }), 500);
4381
+ }
4382
+ }
4383
+
3640
4384
  class Subscriptions {
3641
4385
  constructor(consumer) {
3642
4386
  this.consumer = consumer;
4387
+ this.guarantor = new SubscriptionGuarantor(this);
3643
4388
  this.subscriptions = [];
3644
4389
  }
3645
4390
  create(channelName, mixin) {
@@ -3654,7 +4399,7 @@ class Subscriptions {
3654
4399
  this.subscriptions.push(subscription);
3655
4400
  this.consumer.ensureActiveConnection();
3656
4401
  this.notify(subscription, "initialized");
3657
- this.sendCommand(subscription, "subscribe");
4402
+ this.subscribe(subscription);
3658
4403
  return subscription;
3659
4404
  }
3660
4405
  remove(subscription) {
@@ -3672,6 +4417,7 @@ class Subscriptions {
3672
4417
  }));
3673
4418
  }
3674
4419
  forget(subscription) {
4420
+ this.guarantor.forget(subscription);
3675
4421
  this.subscriptions = this.subscriptions.filter((s => s !== subscription));
3676
4422
  return subscription;
3677
4423
  }
@@ -3679,7 +4425,7 @@ class Subscriptions {
3679
4425
  return this.subscriptions.filter((s => s.identifier === identifier));
3680
4426
  }
3681
4427
  reload() {
3682
- return this.subscriptions.map((subscription => this.sendCommand(subscription, "subscribe")));
4428
+ return this.subscriptions.map((subscription => this.subscribe(subscription)));
3683
4429
  }
3684
4430
  notifyAll(callbackName, ...args) {
3685
4431
  return this.subscriptions.map((subscription => this.notify(subscription, callbackName, ...args)));
@@ -3693,6 +4439,15 @@ class Subscriptions {
3693
4439
  }
3694
4440
  return subscriptions.map((subscription => typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined));
3695
4441
  }
4442
+ subscribe(subscription) {
4443
+ if (this.sendCommand(subscription, "subscribe")) {
4444
+ this.guarantor.guarantee(subscription);
4445
+ }
4446
+ }
4447
+ confirmSubscription(identifier) {
4448
+ logger.log(`Subscription confirmed ${identifier}`);
4449
+ this.findAll(identifier).map((subscription => this.guarantor.forget(subscription)));
4450
+ }
3696
4451
  sendCommand(subscription, command) {
3697
4452
  const {identifier: identifier} = subscription;
3698
4453
  return this.consumer.send({
@@ -3763,6 +4518,7 @@ var index = Object.freeze({
3763
4518
  INTERNAL: INTERNAL,
3764
4519
  Subscription: Subscription,
3765
4520
  Subscriptions: Subscriptions,
4521
+ SubscriptionGuarantor: SubscriptionGuarantor,
3766
4522
  adapters: adapters,
3767
4523
  createWebSocketURL: createWebSocketURL,
3768
4524
  logger: logger,