turbo-rails 2.0.6 → 2.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- Turbo 8.0.5
2
+ Turbo 8.0.6
3
3
  Copyright © 2024 37signals LLC
4
4
  */
5
5
  (function(prototype) {
@@ -116,6 +116,9 @@ class FrameElement extends HTMLElement {
116
116
  this.removeAttribute("refresh");
117
117
  }
118
118
  }
119
+ get shouldReloadWithMorph() {
120
+ return this.src && this.refresh === "morph";
121
+ }
119
122
  get loading() {
120
123
  return frameLoadingStyleFromString(this.getAttribute("loading") || "");
121
124
  }
@@ -167,115 +170,11 @@ function frameLoadingStyleFromString(style) {
167
170
  }
168
171
  }
169
172
 
170
- function expandURL(locatable) {
171
- return new URL(locatable.toString(), document.baseURI);
172
- }
173
-
174
- function getAnchor(url) {
175
- let anchorMatch;
176
- if (url.hash) {
177
- return url.hash.slice(1);
178
- } else if (anchorMatch = url.href.match(/#(.*)$/)) {
179
- return anchorMatch[1];
180
- }
181
- }
182
-
183
- function getAction$1(form, submitter) {
184
- const action = submitter?.getAttribute("formaction") || form.getAttribute("action") || form.action;
185
- return expandURL(action);
186
- }
187
-
188
- function getExtension(url) {
189
- return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || "";
190
- }
191
-
192
- function isHTML(url) {
193
- return !!getExtension(url).match(/^(?:|\.(?:htm|html|xhtml|php))$/);
194
- }
195
-
196
- function isPrefixedBy(baseURL, url) {
197
- const prefix = getPrefix(url);
198
- return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix);
199
- }
200
-
201
- function locationIsVisitable(location, rootLocation) {
202
- return isPrefixedBy(location, rootLocation) && isHTML(location);
203
- }
204
-
205
- function getRequestURL(url) {
206
- const anchor = getAnchor(url);
207
- return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href;
208
- }
209
-
210
- function toCacheKey(url) {
211
- return getRequestURL(url);
212
- }
213
-
214
- function urlsAreEqual(left, right) {
215
- return expandURL(left).href == expandURL(right).href;
216
- }
217
-
218
- function getPathComponents(url) {
219
- return url.pathname.split("/").slice(1);
220
- }
221
-
222
- function getLastPathComponent(url) {
223
- return getPathComponents(url).slice(-1)[0];
224
- }
225
-
226
- function getPrefix(url) {
227
- return addTrailingSlash(url.origin + url.pathname);
228
- }
229
-
230
- function addTrailingSlash(value) {
231
- return value.endsWith("/") ? value : value + "/";
232
- }
233
-
234
- class FetchResponse {
235
- constructor(response) {
236
- this.response = response;
237
- }
238
- get succeeded() {
239
- return this.response.ok;
240
- }
241
- get failed() {
242
- return !this.succeeded;
243
- }
244
- get clientError() {
245
- return this.statusCode >= 400 && this.statusCode <= 499;
246
- }
247
- get serverError() {
248
- return this.statusCode >= 500 && this.statusCode <= 599;
249
- }
250
- get redirected() {
251
- return this.response.redirected;
252
- }
253
- get location() {
254
- return expandURL(this.response.url);
255
- }
256
- get isHTML() {
257
- return this.contentType && this.contentType.match(/^(?:text\/([^\s;,]+\b)?html|application\/xhtml\+xml)\b/);
258
- }
259
- get statusCode() {
260
- return this.response.status;
261
- }
262
- get contentType() {
263
- return this.header("Content-Type");
264
- }
265
- get responseText() {
266
- return this.response.clone().text();
267
- }
268
- get responseHTML() {
269
- if (this.isHTML) {
270
- return this.response.clone().text();
271
- } else {
272
- return Promise.resolve(undefined);
273
- }
274
- }
275
- header(name) {
276
- return this.response.headers.get(name);
277
- }
278
- }
173
+ const drive = {
174
+ enabled: true,
175
+ progressBarDelay: 500,
176
+ unvisitableExtensions: new Set([ ".7z", ".aac", ".apk", ".avi", ".bmp", ".bz2", ".css", ".csv", ".deb", ".dmg", ".doc", ".docx", ".exe", ".gif", ".gz", ".heic", ".heif", ".ico", ".iso", ".jpeg", ".jpg", ".js", ".json", ".m4a", ".mkv", ".mov", ".mp3", ".mp4", ".mpeg", ".mpg", ".msi", ".ogg", ".ogv", ".pdf", ".pkg", ".png", ".ppt", ".pptx", ".rar", ".rtf", ".svg", ".tar", ".tif", ".tiff", ".txt", ".wav", ".webm", ".webp", ".wma", ".wmv", ".xls", ".xlsx", ".xml", ".zip" ])
177
+ };
279
178
 
280
179
  function activateScriptElement(element) {
281
180
  if (element.getAttribute("data-turbo-eval") == "false") {
@@ -320,6 +219,11 @@ function dispatch(eventName, {target: target, cancelable: cancelable, detail: de
320
219
  return event;
321
220
  }
322
221
 
222
+ function cancelEvent(event) {
223
+ event.preventDefault();
224
+ event.stopImmediatePropagation();
225
+ }
226
+
323
227
  function nextRepaint() {
324
228
  if (document.visibilityState === "hidden") {
325
229
  return nextEventLoopTick();
@@ -513,6 +417,152 @@ function debounce(fn, delay) {
513
417
  };
514
418
  }
515
419
 
420
+ const submitter = {
421
+ "aria-disabled": {
422
+ beforeSubmit: submitter => {
423
+ submitter.setAttribute("aria-disabled", "true");
424
+ submitter.addEventListener("click", cancelEvent);
425
+ },
426
+ afterSubmit: submitter => {
427
+ submitter.removeAttribute("aria-disabled");
428
+ submitter.removeEventListener("click", cancelEvent);
429
+ }
430
+ },
431
+ disabled: {
432
+ beforeSubmit: submitter => submitter.disabled = true,
433
+ afterSubmit: submitter => submitter.disabled = false
434
+ }
435
+ };
436
+
437
+ class Config {
438
+ #submitter=null;
439
+ constructor(config) {
440
+ Object.assign(this, config);
441
+ }
442
+ get submitter() {
443
+ return this.#submitter;
444
+ }
445
+ set submitter(value) {
446
+ this.#submitter = submitter[value] || value;
447
+ }
448
+ }
449
+
450
+ const forms = new Config({
451
+ mode: "on",
452
+ submitter: "disabled"
453
+ });
454
+
455
+ const config = {
456
+ drive: drive,
457
+ forms: forms
458
+ };
459
+
460
+ function expandURL(locatable) {
461
+ return new URL(locatable.toString(), document.baseURI);
462
+ }
463
+
464
+ function getAnchor(url) {
465
+ let anchorMatch;
466
+ if (url.hash) {
467
+ return url.hash.slice(1);
468
+ } else if (anchorMatch = url.href.match(/#(.*)$/)) {
469
+ return anchorMatch[1];
470
+ }
471
+ }
472
+
473
+ function getAction$1(form, submitter) {
474
+ const action = submitter?.getAttribute("formaction") || form.getAttribute("action") || form.action;
475
+ return expandURL(action);
476
+ }
477
+
478
+ function getExtension(url) {
479
+ return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || "";
480
+ }
481
+
482
+ function isPrefixedBy(baseURL, url) {
483
+ const prefix = getPrefix(url);
484
+ return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix);
485
+ }
486
+
487
+ function locationIsVisitable(location, rootLocation) {
488
+ return isPrefixedBy(location, rootLocation) && !config.drive.unvisitableExtensions.has(getExtension(location));
489
+ }
490
+
491
+ function getRequestURL(url) {
492
+ const anchor = getAnchor(url);
493
+ return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href;
494
+ }
495
+
496
+ function toCacheKey(url) {
497
+ return getRequestURL(url);
498
+ }
499
+
500
+ function urlsAreEqual(left, right) {
501
+ return expandURL(left).href == expandURL(right).href;
502
+ }
503
+
504
+ function getPathComponents(url) {
505
+ return url.pathname.split("/").slice(1);
506
+ }
507
+
508
+ function getLastPathComponent(url) {
509
+ return getPathComponents(url).slice(-1)[0];
510
+ }
511
+
512
+ function getPrefix(url) {
513
+ return addTrailingSlash(url.origin + url.pathname);
514
+ }
515
+
516
+ function addTrailingSlash(value) {
517
+ return value.endsWith("/") ? value : value + "/";
518
+ }
519
+
520
+ class FetchResponse {
521
+ constructor(response) {
522
+ this.response = response;
523
+ }
524
+ get succeeded() {
525
+ return this.response.ok;
526
+ }
527
+ get failed() {
528
+ return !this.succeeded;
529
+ }
530
+ get clientError() {
531
+ return this.statusCode >= 400 && this.statusCode <= 499;
532
+ }
533
+ get serverError() {
534
+ return this.statusCode >= 500 && this.statusCode <= 599;
535
+ }
536
+ get redirected() {
537
+ return this.response.redirected;
538
+ }
539
+ get location() {
540
+ return expandURL(this.response.url);
541
+ }
542
+ get isHTML() {
543
+ return this.contentType && this.contentType.match(/^(?:text\/([^\s;,]+\b)?html|application\/xhtml\+xml)\b/);
544
+ }
545
+ get statusCode() {
546
+ return this.response.status;
547
+ }
548
+ get contentType() {
549
+ return this.header("Content-Type");
550
+ }
551
+ get responseText() {
552
+ return this.response.clone().text();
553
+ }
554
+ get responseHTML() {
555
+ if (this.isHTML) {
556
+ return this.response.clone().text();
557
+ } else {
558
+ return Promise.resolve(undefined);
559
+ }
560
+ }
561
+ header(name) {
562
+ return this.response.headers.get(name);
563
+ }
564
+ }
565
+
516
566
  class LimitedSet extends Set {
517
567
  constructor(maxSize) {
518
568
  super();
@@ -861,7 +911,7 @@ const FormSubmissionState = {
861
911
 
862
912
  class FormSubmission {
863
913
  state=FormSubmissionState.initialized;
864
- static confirmMethod(message, _element, _submitter) {
914
+ static confirmMethod(message) {
865
915
  return Promise.resolve(confirm(message));
866
916
  }
867
917
  constructor(delegate, formElement, submitter, mustRedirect = false) {
@@ -903,7 +953,8 @@ class FormSubmission {
903
953
  const {initialized: initialized, requesting: requesting} = FormSubmissionState;
904
954
  const confirmationMessage = getAttribute("data-turbo-confirm", this.submitter, this.formElement);
905
955
  if (typeof confirmationMessage === "string") {
906
- const answer = await FormSubmission.confirmMethod(confirmationMessage, this.formElement, this.submitter);
956
+ const confirmMethod = typeof config.forms.confirm === "function" ? config.forms.confirm : FormSubmission.confirmMethod;
957
+ const answer = await confirmMethod(confirmationMessage, this.formElement, this.submitter);
907
958
  if (!answer) {
908
959
  return;
909
960
  }
@@ -934,7 +985,7 @@ class FormSubmission {
934
985
  }
935
986
  requestStarted(_request) {
936
987
  this.state = FormSubmissionState.waiting;
937
- this.submitter?.setAttribute("disabled", "");
988
+ if (this.submitter) config.forms.submitter.beforeSubmit(this.submitter);
938
989
  this.setSubmitsWith();
939
990
  markAsBusy(this.formElement);
940
991
  dispatch("turbo:submit-start", {
@@ -986,7 +1037,7 @@ class FormSubmission {
986
1037
  }
987
1038
  requestFinished(_request) {
988
1039
  this.state = FormSubmissionState.stopped;
989
- this.submitter?.removeAttribute("disabled");
1040
+ if (this.submitter) config.forms.submitter.afterSubmit(this.submitter);
990
1041
  this.resetSubmitterText();
991
1042
  clearBusyState(this.formElement);
992
1043
  dispatch("turbo:submit-end", {
@@ -1626,2083 +1677,2085 @@ function readScrollBehavior(value, defaultValue) {
1626
1677
  }
1627
1678
  }
1628
1679
 
1629
- class ProgressBar {
1630
- static animationDuration=300;
1631
- static get defaultCSS() {
1632
- return unindent`
1633
- .turbo-progress-bar {
1634
- position: fixed;
1635
- display: block;
1636
- top: 0;
1637
- left: 0;
1638
- height: 3px;
1639
- background: #0076ff;
1640
- z-index: 2147483647;
1641
- transition:
1642
- width ${ProgressBar.animationDuration}ms ease-out,
1643
- opacity ${ProgressBar.animationDuration / 2}ms ${ProgressBar.animationDuration / 2}ms ease-in;
1644
- transform: translate3d(0, 0, 0);
1645
- }
1646
- `;
1647
- }
1648
- hiding=false;
1649
- value=0;
1650
- visible=false;
1651
- constructor() {
1652
- this.stylesheetElement = this.createStylesheetElement();
1653
- this.progressElement = this.createProgressElement();
1654
- this.installStylesheetElement();
1655
- this.setValue(0);
1656
- }
1657
- show() {
1658
- if (!this.visible) {
1659
- this.visible = true;
1660
- this.installProgressElement();
1661
- this.startTrickling();
1680
+ var Idiomorph = function() {
1681
+ let EMPTY_SET = new Set;
1682
+ let defaults = {
1683
+ morphStyle: "outerHTML",
1684
+ callbacks: {
1685
+ beforeNodeAdded: noOp,
1686
+ afterNodeAdded: noOp,
1687
+ beforeNodeMorphed: noOp,
1688
+ afterNodeMorphed: noOp,
1689
+ beforeNodeRemoved: noOp,
1690
+ afterNodeRemoved: noOp,
1691
+ beforeAttributeUpdated: noOp
1692
+ },
1693
+ head: {
1694
+ style: "merge",
1695
+ shouldPreserve: function(elt) {
1696
+ return elt.getAttribute("im-preserve") === "true";
1697
+ },
1698
+ shouldReAppend: function(elt) {
1699
+ return elt.getAttribute("im-re-append") === "true";
1700
+ },
1701
+ shouldRemove: noOp,
1702
+ afterHeadMorphed: noOp
1662
1703
  }
1663
- }
1664
- hide() {
1665
- if (this.visible && !this.hiding) {
1666
- this.hiding = true;
1667
- this.fadeProgressElement((() => {
1668
- this.uninstallProgressElement();
1669
- this.stopTrickling();
1670
- this.visible = false;
1671
- this.hiding = false;
1672
- }));
1704
+ };
1705
+ function morph(oldNode, newContent, config = {}) {
1706
+ if (oldNode instanceof Document) {
1707
+ oldNode = oldNode.documentElement;
1673
1708
  }
1674
- }
1675
- setValue(value) {
1676
- this.value = value;
1677
- this.refresh();
1678
- }
1679
- installStylesheetElement() {
1680
- document.head.insertBefore(this.stylesheetElement, document.head.firstChild);
1681
- }
1682
- installProgressElement() {
1683
- this.progressElement.style.width = "0";
1684
- this.progressElement.style.opacity = "1";
1685
- document.documentElement.insertBefore(this.progressElement, document.body);
1686
- this.refresh();
1687
- }
1688
- fadeProgressElement(callback) {
1689
- this.progressElement.style.opacity = "0";
1690
- setTimeout(callback, ProgressBar.animationDuration * 1.5);
1691
- }
1692
- uninstallProgressElement() {
1693
- if (this.progressElement.parentNode) {
1694
- document.documentElement.removeChild(this.progressElement);
1709
+ if (typeof newContent === "string") {
1710
+ newContent = parseContent(newContent);
1695
1711
  }
1712
+ let normalizedContent = normalizeContent(newContent);
1713
+ let ctx = createMorphContext(oldNode, normalizedContent, config);
1714
+ return morphNormalizedContent(oldNode, normalizedContent, ctx);
1696
1715
  }
1697
- startTrickling() {
1698
- if (!this.trickleInterval) {
1699
- this.trickleInterval = window.setInterval(this.trickle, ProgressBar.animationDuration);
1716
+ function morphNormalizedContent(oldNode, normalizedNewContent, ctx) {
1717
+ if (ctx.head.block) {
1718
+ let oldHead = oldNode.querySelector("head");
1719
+ let newHead = normalizedNewContent.querySelector("head");
1720
+ if (oldHead && newHead) {
1721
+ let promises = handleHeadElement(newHead, oldHead, ctx);
1722
+ Promise.all(promises).then((function() {
1723
+ morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, {
1724
+ head: {
1725
+ block: false,
1726
+ ignore: true
1727
+ }
1728
+ }));
1729
+ }));
1730
+ return;
1731
+ }
1700
1732
  }
1701
- }
1702
- stopTrickling() {
1703
- window.clearInterval(this.trickleInterval);
1704
- delete this.trickleInterval;
1705
- }
1706
- trickle=() => {
1707
- this.setValue(this.value + Math.random() / 100);
1708
- };
1709
- refresh() {
1710
- requestAnimationFrame((() => {
1711
- this.progressElement.style.width = `${10 + this.value * 90}%`;
1712
- }));
1713
- }
1714
- createStylesheetElement() {
1715
- const element = document.createElement("style");
1716
- element.type = "text/css";
1717
- element.textContent = ProgressBar.defaultCSS;
1718
- if (this.cspNonce) {
1719
- element.nonce = this.cspNonce;
1733
+ if (ctx.morphStyle === "innerHTML") {
1734
+ morphChildren(normalizedNewContent, oldNode, ctx);
1735
+ return oldNode.children;
1736
+ } else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) {
1737
+ let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx);
1738
+ let previousSibling = bestMatch?.previousSibling;
1739
+ let nextSibling = bestMatch?.nextSibling;
1740
+ let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx);
1741
+ if (bestMatch) {
1742
+ return insertSiblings(previousSibling, morphedNode, nextSibling);
1743
+ } else {
1744
+ return [];
1745
+ }
1746
+ } else {
1747
+ throw "Do not understand how to morph style " + ctx.morphStyle;
1720
1748
  }
1721
- return element;
1722
1749
  }
1723
- createProgressElement() {
1724
- const element = document.createElement("div");
1725
- element.className = "turbo-progress-bar";
1726
- return element;
1727
- }
1728
- get cspNonce() {
1729
- return getMetaContent("csp-nonce");
1750
+ function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
1751
+ return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement && possibleActiveElement !== document.body;
1730
1752
  }
1731
- }
1732
-
1733
- class HeadSnapshot extends Snapshot {
1734
- detailsByOuterHTML=this.children.filter((element => !elementIsNoscript(element))).map((element => elementWithoutNonce(element))).reduce(((result, element) => {
1735
- const {outerHTML: outerHTML} = element;
1736
- const details = outerHTML in result ? result[outerHTML] : {
1737
- type: elementType(element),
1738
- tracked: elementIsTracked(element),
1739
- elements: []
1740
- };
1741
- return {
1742
- ...result,
1743
- [outerHTML]: {
1744
- ...details,
1745
- elements: [ ...details.elements, element ]
1753
+ function morphOldNodeTo(oldNode, newContent, ctx) {
1754
+ if (ctx.ignoreActive && oldNode === document.activeElement) ; else if (newContent == null) {
1755
+ if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
1756
+ oldNode.remove();
1757
+ ctx.callbacks.afterNodeRemoved(oldNode);
1758
+ return null;
1759
+ } else if (!isSoftMatch(oldNode, newContent)) {
1760
+ if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
1761
+ if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode;
1762
+ oldNode.parentElement.replaceChild(newContent, oldNode);
1763
+ ctx.callbacks.afterNodeAdded(newContent);
1764
+ ctx.callbacks.afterNodeRemoved(oldNode);
1765
+ return newContent;
1766
+ } else {
1767
+ if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return oldNode;
1768
+ if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) ; else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") {
1769
+ handleHeadElement(newContent, oldNode, ctx);
1770
+ } else {
1771
+ syncNodeFrom(newContent, oldNode, ctx);
1772
+ if (!ignoreValueOfActiveElement(oldNode, ctx)) {
1773
+ morphChildren(newContent, oldNode, ctx);
1774
+ }
1746
1775
  }
1747
- };
1748
- }), {});
1749
- get trackedElementSignature() {
1750
- return Object.keys(this.detailsByOuterHTML).filter((outerHTML => this.detailsByOuterHTML[outerHTML].tracked)).join("");
1776
+ ctx.callbacks.afterNodeMorphed(oldNode, newContent);
1777
+ return oldNode;
1778
+ }
1751
1779
  }
1752
- getScriptElementsNotInSnapshot(snapshot) {
1753
- return this.getElementsMatchingTypeNotInSnapshot("script", snapshot);
1780
+ function morphChildren(newParent, oldParent, ctx) {
1781
+ let nextNewChild = newParent.firstChild;
1782
+ let insertionPoint = oldParent.firstChild;
1783
+ let newChild;
1784
+ while (nextNewChild) {
1785
+ newChild = nextNewChild;
1786
+ nextNewChild = newChild.nextSibling;
1787
+ if (insertionPoint == null) {
1788
+ if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
1789
+ oldParent.appendChild(newChild);
1790
+ ctx.callbacks.afterNodeAdded(newChild);
1791
+ removeIdsFromConsideration(ctx, newChild);
1792
+ continue;
1793
+ }
1794
+ if (isIdSetMatch(newChild, insertionPoint, ctx)) {
1795
+ morphOldNodeTo(insertionPoint, newChild, ctx);
1796
+ insertionPoint = insertionPoint.nextSibling;
1797
+ removeIdsFromConsideration(ctx, newChild);
1798
+ continue;
1799
+ }
1800
+ let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx);
1801
+ if (idSetMatch) {
1802
+ insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx);
1803
+ morphOldNodeTo(idSetMatch, newChild, ctx);
1804
+ removeIdsFromConsideration(ctx, newChild);
1805
+ continue;
1806
+ }
1807
+ let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx);
1808
+ if (softMatch) {
1809
+ insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx);
1810
+ morphOldNodeTo(softMatch, newChild, ctx);
1811
+ removeIdsFromConsideration(ctx, newChild);
1812
+ continue;
1813
+ }
1814
+ if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
1815
+ oldParent.insertBefore(newChild, insertionPoint);
1816
+ ctx.callbacks.afterNodeAdded(newChild);
1817
+ removeIdsFromConsideration(ctx, newChild);
1818
+ }
1819
+ while (insertionPoint !== null) {
1820
+ let tempNode = insertionPoint;
1821
+ insertionPoint = insertionPoint.nextSibling;
1822
+ removeNode(tempNode, ctx);
1823
+ }
1754
1824
  }
1755
- getStylesheetElementsNotInSnapshot(snapshot) {
1756
- return this.getElementsMatchingTypeNotInSnapshot("stylesheet", snapshot);
1825
+ function ignoreAttribute(attr, to, updateType, ctx) {
1826
+ if (attr === "value" && ctx.ignoreActiveValue && to === document.activeElement) {
1827
+ return true;
1828
+ }
1829
+ return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false;
1757
1830
  }
1758
- getElementsMatchingTypeNotInSnapshot(matchedType, snapshot) {
1759
- return Object.keys(this.detailsByOuterHTML).filter((outerHTML => !(outerHTML in snapshot.detailsByOuterHTML))).map((outerHTML => this.detailsByOuterHTML[outerHTML])).filter((({type: type}) => type == matchedType)).map((({elements: [element]}) => element));
1831
+ function syncNodeFrom(from, to, ctx) {
1832
+ let type = from.nodeType;
1833
+ if (type === 1) {
1834
+ const fromAttributes = from.attributes;
1835
+ const toAttributes = to.attributes;
1836
+ for (const fromAttribute of fromAttributes) {
1837
+ if (ignoreAttribute(fromAttribute.name, to, "update", ctx)) {
1838
+ continue;
1839
+ }
1840
+ if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) {
1841
+ to.setAttribute(fromAttribute.name, fromAttribute.value);
1842
+ }
1843
+ }
1844
+ for (let i = toAttributes.length - 1; 0 <= i; i--) {
1845
+ const toAttribute = toAttributes[i];
1846
+ if (ignoreAttribute(toAttribute.name, to, "remove", ctx)) {
1847
+ continue;
1848
+ }
1849
+ if (!from.hasAttribute(toAttribute.name)) {
1850
+ to.removeAttribute(toAttribute.name);
1851
+ }
1852
+ }
1853
+ }
1854
+ if (type === 8 || type === 3) {
1855
+ if (to.nodeValue !== from.nodeValue) {
1856
+ to.nodeValue = from.nodeValue;
1857
+ }
1858
+ }
1859
+ if (!ignoreValueOfActiveElement(to, ctx)) {
1860
+ syncInputValue(from, to, ctx);
1861
+ }
1760
1862
  }
1761
- get provisionalElements() {
1762
- return Object.keys(this.detailsByOuterHTML).reduce(((result, outerHTML) => {
1763
- const {type: type, tracked: tracked, elements: elements} = this.detailsByOuterHTML[outerHTML];
1764
- if (type == null && !tracked) {
1765
- return [ ...result, ...elements ];
1766
- } else if (elements.length > 1) {
1767
- return [ ...result, ...elements.slice(1) ];
1863
+ function syncBooleanAttribute(from, to, attributeName, ctx) {
1864
+ if (from[attributeName] !== to[attributeName]) {
1865
+ let ignoreUpdate = ignoreAttribute(attributeName, to, "update", ctx);
1866
+ if (!ignoreUpdate) {
1867
+ to[attributeName] = from[attributeName];
1868
+ }
1869
+ if (from[attributeName]) {
1870
+ if (!ignoreUpdate) {
1871
+ to.setAttribute(attributeName, from[attributeName]);
1872
+ }
1768
1873
  } else {
1769
- return result;
1874
+ if (!ignoreAttribute(attributeName, to, "remove", ctx)) {
1875
+ to.removeAttribute(attributeName);
1876
+ }
1770
1877
  }
1771
- }), []);
1772
- }
1773
- getMetaValue(name) {
1774
- const element = this.findMetaElementByName(name);
1775
- return element ? element.getAttribute("content") : null;
1776
- }
1777
- findMetaElementByName(name) {
1778
- return Object.keys(this.detailsByOuterHTML).reduce(((result, outerHTML) => {
1779
- const {elements: [element]} = this.detailsByOuterHTML[outerHTML];
1780
- return elementIsMetaElementWithName(element, name) ? element : result;
1781
- }), undefined | undefined);
1782
- }
1783
- }
1784
-
1785
- function elementType(element) {
1786
- if (elementIsScript(element)) {
1787
- return "script";
1788
- } else if (elementIsStylesheet(element)) {
1789
- return "stylesheet";
1790
- }
1791
- }
1792
-
1793
- function elementIsTracked(element) {
1794
- return element.getAttribute("data-turbo-track") == "reload";
1795
- }
1796
-
1797
- function elementIsScript(element) {
1798
- const tagName = element.localName;
1799
- return tagName == "script";
1800
- }
1801
-
1802
- function elementIsNoscript(element) {
1803
- const tagName = element.localName;
1804
- return tagName == "noscript";
1805
- }
1806
-
1807
- function elementIsStylesheet(element) {
1808
- const tagName = element.localName;
1809
- return tagName == "style" || tagName == "link" && element.getAttribute("rel") == "stylesheet";
1810
- }
1811
-
1812
- function elementIsMetaElementWithName(element, name) {
1813
- const tagName = element.localName;
1814
- return tagName == "meta" && element.getAttribute("name") == name;
1815
- }
1816
-
1817
- function elementWithoutNonce(element) {
1818
- if (element.hasAttribute("nonce")) {
1819
- element.setAttribute("nonce", "");
1878
+ }
1820
1879
  }
1821
- return element;
1822
- }
1823
-
1824
- class PageSnapshot extends Snapshot {
1825
- static fromHTMLString(html = "") {
1826
- return this.fromDocument(parseHTMLDocument(html));
1880
+ function syncInputValue(from, to, ctx) {
1881
+ if (from instanceof HTMLInputElement && to instanceof HTMLInputElement && from.type !== "file") {
1882
+ let fromValue = from.value;
1883
+ let toValue = to.value;
1884
+ syncBooleanAttribute(from, to, "checked", ctx);
1885
+ syncBooleanAttribute(from, to, "disabled", ctx);
1886
+ if (!from.hasAttribute("value")) {
1887
+ if (!ignoreAttribute("value", to, "remove", ctx)) {
1888
+ to.value = "";
1889
+ to.removeAttribute("value");
1890
+ }
1891
+ } else if (fromValue !== toValue) {
1892
+ if (!ignoreAttribute("value", to, "update", ctx)) {
1893
+ to.setAttribute("value", fromValue);
1894
+ to.value = fromValue;
1895
+ }
1896
+ }
1897
+ } else if (from instanceof HTMLOptionElement) {
1898
+ syncBooleanAttribute(from, to, "selected", ctx);
1899
+ } else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) {
1900
+ let fromValue = from.value;
1901
+ let toValue = to.value;
1902
+ if (ignoreAttribute("value", to, "update", ctx)) {
1903
+ return;
1904
+ }
1905
+ if (fromValue !== toValue) {
1906
+ to.value = fromValue;
1907
+ }
1908
+ if (to.firstChild && to.firstChild.nodeValue !== fromValue) {
1909
+ to.firstChild.nodeValue = fromValue;
1910
+ }
1911
+ }
1827
1912
  }
1828
- static fromElement(element) {
1829
- return this.fromDocument(element.ownerDocument);
1913
+ function handleHeadElement(newHeadTag, currentHead, ctx) {
1914
+ let added = [];
1915
+ let removed = [];
1916
+ let preserved = [];
1917
+ let nodesToAppend = [];
1918
+ let headMergeStyle = ctx.head.style;
1919
+ let srcToNewHeadNodes = new Map;
1920
+ for (const newHeadChild of newHeadTag.children) {
1921
+ srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
1922
+ }
1923
+ for (const currentHeadElt of currentHead.children) {
1924
+ let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
1925
+ let isReAppended = ctx.head.shouldReAppend(currentHeadElt);
1926
+ let isPreserved = ctx.head.shouldPreserve(currentHeadElt);
1927
+ if (inNewContent || isPreserved) {
1928
+ if (isReAppended) {
1929
+ removed.push(currentHeadElt);
1930
+ } else {
1931
+ srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
1932
+ preserved.push(currentHeadElt);
1933
+ }
1934
+ } else {
1935
+ if (headMergeStyle === "append") {
1936
+ if (isReAppended) {
1937
+ removed.push(currentHeadElt);
1938
+ nodesToAppend.push(currentHeadElt);
1939
+ }
1940
+ } else {
1941
+ if (ctx.head.shouldRemove(currentHeadElt) !== false) {
1942
+ removed.push(currentHeadElt);
1943
+ }
1944
+ }
1945
+ }
1946
+ }
1947
+ nodesToAppend.push(...srcToNewHeadNodes.values());
1948
+ let promises = [];
1949
+ for (const newNode of nodesToAppend) {
1950
+ let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild;
1951
+ if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
1952
+ if (newElt.href || newElt.src) {
1953
+ let resolve = null;
1954
+ let promise = new Promise((function(_resolve) {
1955
+ resolve = _resolve;
1956
+ }));
1957
+ newElt.addEventListener("load", (function() {
1958
+ resolve();
1959
+ }));
1960
+ promises.push(promise);
1961
+ }
1962
+ currentHead.appendChild(newElt);
1963
+ ctx.callbacks.afterNodeAdded(newElt);
1964
+ added.push(newElt);
1965
+ }
1966
+ }
1967
+ for (const removedElement of removed) {
1968
+ if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
1969
+ currentHead.removeChild(removedElement);
1970
+ ctx.callbacks.afterNodeRemoved(removedElement);
1971
+ }
1972
+ }
1973
+ ctx.head.afterHeadMorphed(currentHead, {
1974
+ added: added,
1975
+ kept: preserved,
1976
+ removed: removed
1977
+ });
1978
+ return promises;
1830
1979
  }
1831
- static fromDocument({documentElement: documentElement, body: body, head: head}) {
1832
- return new this(documentElement, body, new HeadSnapshot(head));
1980
+ function noOp() {}
1981
+ function mergeDefaults(config) {
1982
+ let finalConfig = {};
1983
+ Object.assign(finalConfig, defaults);
1984
+ Object.assign(finalConfig, config);
1985
+ finalConfig.callbacks = {};
1986
+ Object.assign(finalConfig.callbacks, defaults.callbacks);
1987
+ Object.assign(finalConfig.callbacks, config.callbacks);
1988
+ finalConfig.head = {};
1989
+ Object.assign(finalConfig.head, defaults.head);
1990
+ Object.assign(finalConfig.head, config.head);
1991
+ return finalConfig;
1833
1992
  }
1834
- constructor(documentElement, body, headSnapshot) {
1835
- super(body);
1836
- this.documentElement = documentElement;
1837
- this.headSnapshot = headSnapshot;
1993
+ function createMorphContext(oldNode, newContent, config) {
1994
+ config = mergeDefaults(config);
1995
+ return {
1996
+ target: oldNode,
1997
+ newContent: newContent,
1998
+ config: config,
1999
+ morphStyle: config.morphStyle,
2000
+ ignoreActive: config.ignoreActive,
2001
+ ignoreActiveValue: config.ignoreActiveValue,
2002
+ idMap: createIdMap(oldNode, newContent),
2003
+ deadIds: new Set,
2004
+ callbacks: config.callbacks,
2005
+ head: config.head
2006
+ };
1838
2007
  }
1839
- clone() {
1840
- const clonedElement = this.element.cloneNode(true);
1841
- const selectElements = this.element.querySelectorAll("select");
1842
- const clonedSelectElements = clonedElement.querySelectorAll("select");
1843
- for (const [index, source] of selectElements.entries()) {
1844
- const clone = clonedSelectElements[index];
1845
- for (const option of clone.selectedOptions) option.selected = false;
1846
- for (const option of source.selectedOptions) clone.options[option.index].selected = true;
2008
+ function isIdSetMatch(node1, node2, ctx) {
2009
+ if (node1 == null || node2 == null) {
2010
+ return false;
1847
2011
  }
1848
- for (const clonedPasswordInput of clonedElement.querySelectorAll('input[type="password"]')) {
1849
- clonedPasswordInput.value = "";
2012
+ if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) {
2013
+ if (node1.id !== "" && node1.id === node2.id) {
2014
+ return true;
2015
+ } else {
2016
+ return getIdIntersectionCount(ctx, node1, node2) > 0;
2017
+ }
1850
2018
  }
1851
- return new PageSnapshot(this.documentElement, clonedElement, this.headSnapshot);
1852
- }
1853
- get lang() {
1854
- return this.documentElement.getAttribute("lang");
1855
- }
1856
- get headElement() {
1857
- return this.headSnapshot.element;
1858
- }
1859
- get rootLocation() {
1860
- const root = this.getSetting("root") ?? "/";
1861
- return expandURL(root);
1862
- }
1863
- get cacheControlValue() {
1864
- return this.getSetting("cache-control");
1865
- }
1866
- get isPreviewable() {
1867
- return this.cacheControlValue != "no-preview";
1868
- }
1869
- get isCacheable() {
1870
- return this.cacheControlValue != "no-cache";
1871
- }
1872
- get isVisitable() {
1873
- return this.getSetting("visit-control") != "reload";
2019
+ return false;
1874
2020
  }
1875
- get prefersViewTransitions() {
1876
- return this.headSnapshot.getMetaValue("view-transition") === "same-origin";
2021
+ function isSoftMatch(node1, node2) {
2022
+ if (node1 == null || node2 == null) {
2023
+ return false;
2024
+ }
2025
+ return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName;
1877
2026
  }
1878
- get shouldMorphPage() {
1879
- return this.getSetting("refresh-method") === "morph";
2027
+ function removeNodesBetween(startInclusive, endExclusive, ctx) {
2028
+ while (startInclusive !== endExclusive) {
2029
+ let tempNode = startInclusive;
2030
+ startInclusive = startInclusive.nextSibling;
2031
+ removeNode(tempNode, ctx);
2032
+ }
2033
+ removeIdsFromConsideration(ctx, endExclusive);
2034
+ return endExclusive.nextSibling;
1880
2035
  }
1881
- get shouldPreserveScrollPosition() {
1882
- return this.getSetting("refresh-scroll") === "preserve";
2036
+ function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
2037
+ let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent);
2038
+ let potentialMatch = null;
2039
+ if (newChildPotentialIdCount > 0) {
2040
+ let potentialMatch = insertionPoint;
2041
+ let otherMatchCount = 0;
2042
+ while (potentialMatch != null) {
2043
+ if (isIdSetMatch(newChild, potentialMatch, ctx)) {
2044
+ return potentialMatch;
2045
+ }
2046
+ otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent);
2047
+ if (otherMatchCount > newChildPotentialIdCount) {
2048
+ return null;
2049
+ }
2050
+ potentialMatch = potentialMatch.nextSibling;
2051
+ }
2052
+ }
2053
+ return potentialMatch;
1883
2054
  }
1884
- getSetting(name) {
1885
- return this.headSnapshot.getMetaValue(`turbo-${name}`);
2055
+ function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
2056
+ let potentialSoftMatch = insertionPoint;
2057
+ let nextSibling = newChild.nextSibling;
2058
+ let siblingSoftMatchCount = 0;
2059
+ while (potentialSoftMatch != null) {
2060
+ if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) {
2061
+ return null;
2062
+ }
2063
+ if (isSoftMatch(newChild, potentialSoftMatch)) {
2064
+ return potentialSoftMatch;
2065
+ }
2066
+ if (isSoftMatch(nextSibling, potentialSoftMatch)) {
2067
+ siblingSoftMatchCount++;
2068
+ nextSibling = nextSibling.nextSibling;
2069
+ if (siblingSoftMatchCount >= 2) {
2070
+ return null;
2071
+ }
2072
+ }
2073
+ potentialSoftMatch = potentialSoftMatch.nextSibling;
2074
+ }
2075
+ return potentialSoftMatch;
1886
2076
  }
1887
- }
1888
-
1889
- class ViewTransitioner {
1890
- #viewTransitionStarted=false;
1891
- #lastOperation=Promise.resolve();
1892
- renderChange(useViewTransition, render) {
1893
- if (useViewTransition && this.viewTransitionsAvailable && !this.#viewTransitionStarted) {
1894
- this.#viewTransitionStarted = true;
1895
- this.#lastOperation = this.#lastOperation.then((async () => {
1896
- await document.startViewTransition(render).finished;
1897
- }));
2077
+ function parseContent(newContent) {
2078
+ let parser = new DOMParser;
2079
+ let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, "");
2080
+ if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) {
2081
+ let content = parser.parseFromString(newContent, "text/html");
2082
+ if (contentWithSvgsRemoved.match(/<\/html>/)) {
2083
+ content.generatedByIdiomorph = true;
2084
+ return content;
2085
+ } else {
2086
+ let htmlElement = content.firstChild;
2087
+ if (htmlElement) {
2088
+ htmlElement.generatedByIdiomorph = true;
2089
+ return htmlElement;
2090
+ } else {
2091
+ return null;
2092
+ }
2093
+ }
1898
2094
  } else {
1899
- this.#lastOperation = this.#lastOperation.then(render);
2095
+ let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html");
2096
+ let content = responseDoc.body.querySelector("template").content;
2097
+ content.generatedByIdiomorph = true;
2098
+ return content;
1900
2099
  }
1901
- return this.#lastOperation;
1902
2100
  }
1903
- get viewTransitionsAvailable() {
1904
- return document.startViewTransition;
2101
+ function normalizeContent(newContent) {
2102
+ if (newContent == null) {
2103
+ const dummyParent = document.createElement("div");
2104
+ return dummyParent;
2105
+ } else if (newContent.generatedByIdiomorph) {
2106
+ return newContent;
2107
+ } else if (newContent instanceof Node) {
2108
+ const dummyParent = document.createElement("div");
2109
+ dummyParent.append(newContent);
2110
+ return dummyParent;
2111
+ } else {
2112
+ const dummyParent = document.createElement("div");
2113
+ for (const elt of [ ...newContent ]) {
2114
+ dummyParent.append(elt);
2115
+ }
2116
+ return dummyParent;
2117
+ }
1905
2118
  }
1906
- }
1907
-
1908
- const defaultOptions = {
1909
- action: "advance",
1910
- historyChanged: false,
1911
- visitCachedSnapshot: () => {},
1912
- willRender: true,
1913
- updateHistory: true,
1914
- shouldCacheSnapshot: true,
1915
- acceptsStreamResponse: false
1916
- };
1917
-
1918
- const TimingMetric = {
1919
- visitStart: "visitStart",
1920
- requestStart: "requestStart",
1921
- requestEnd: "requestEnd",
1922
- visitEnd: "visitEnd"
1923
- };
1924
-
1925
- const VisitState = {
1926
- initialized: "initialized",
1927
- started: "started",
1928
- canceled: "canceled",
1929
- failed: "failed",
1930
- completed: "completed"
1931
- };
1932
-
1933
- const SystemStatusCode = {
1934
- networkFailure: 0,
1935
- timeoutFailure: -1,
1936
- contentTypeMismatch: -2
1937
- };
1938
-
1939
- const Direction = {
1940
- advance: "forward",
1941
- restore: "back",
1942
- replace: "none"
1943
- };
1944
-
1945
- class Visit {
1946
- identifier=uuid();
1947
- timingMetrics={};
1948
- followedRedirect=false;
1949
- historyChanged=false;
1950
- scrolled=false;
1951
- shouldCacheSnapshot=true;
1952
- acceptsStreamResponse=false;
1953
- snapshotCached=false;
1954
- state=VisitState.initialized;
1955
- viewTransitioner=new ViewTransitioner;
1956
- constructor(delegate, location, restorationIdentifier, options = {}) {
1957
- this.delegate = delegate;
1958
- this.location = location;
1959
- this.restorationIdentifier = restorationIdentifier || uuid();
1960
- const {action: action, historyChanged: historyChanged, referrer: referrer, snapshot: snapshot, snapshotHTML: snapshotHTML, response: response, visitCachedSnapshot: visitCachedSnapshot, willRender: willRender, updateHistory: updateHistory, shouldCacheSnapshot: shouldCacheSnapshot, acceptsStreamResponse: acceptsStreamResponse, direction: direction} = {
1961
- ...defaultOptions,
1962
- ...options
1963
- };
1964
- this.action = action;
1965
- this.historyChanged = historyChanged;
1966
- this.referrer = referrer;
1967
- this.snapshot = snapshot;
1968
- this.snapshotHTML = snapshotHTML;
1969
- this.response = response;
1970
- this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
1971
- this.isPageRefresh = this.view.isPageRefresh(this);
1972
- this.visitCachedSnapshot = visitCachedSnapshot;
1973
- this.willRender = willRender;
1974
- this.updateHistory = updateHistory;
1975
- this.scrolled = !willRender;
1976
- this.shouldCacheSnapshot = shouldCacheSnapshot;
1977
- this.acceptsStreamResponse = acceptsStreamResponse;
1978
- this.direction = direction || Direction[action];
2119
+ function insertSiblings(previousSibling, morphedNode, nextSibling) {
2120
+ let stack = [];
2121
+ let added = [];
2122
+ while (previousSibling != null) {
2123
+ stack.push(previousSibling);
2124
+ previousSibling = previousSibling.previousSibling;
2125
+ }
2126
+ while (stack.length > 0) {
2127
+ let node = stack.pop();
2128
+ added.push(node);
2129
+ morphedNode.parentElement.insertBefore(node, morphedNode);
2130
+ }
2131
+ added.push(morphedNode);
2132
+ while (nextSibling != null) {
2133
+ stack.push(nextSibling);
2134
+ added.push(nextSibling);
2135
+ nextSibling = nextSibling.nextSibling;
2136
+ }
2137
+ while (stack.length > 0) {
2138
+ morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling);
2139
+ }
2140
+ return added;
1979
2141
  }
1980
- get adapter() {
1981
- return this.delegate.adapter;
2142
+ function findBestNodeMatch(newContent, oldNode, ctx) {
2143
+ let currentElement;
2144
+ currentElement = newContent.firstChild;
2145
+ let bestElement = currentElement;
2146
+ let score = 0;
2147
+ while (currentElement) {
2148
+ let newScore = scoreElement(currentElement, oldNode, ctx);
2149
+ if (newScore > score) {
2150
+ bestElement = currentElement;
2151
+ score = newScore;
2152
+ }
2153
+ currentElement = currentElement.nextSibling;
2154
+ }
2155
+ return bestElement;
1982
2156
  }
1983
- get view() {
1984
- return this.delegate.view;
2157
+ function scoreElement(node1, node2, ctx) {
2158
+ if (isSoftMatch(node1, node2)) {
2159
+ return .5 + getIdIntersectionCount(ctx, node1, node2);
2160
+ }
2161
+ return 0;
1985
2162
  }
1986
- get history() {
1987
- return this.delegate.history;
2163
+ function removeNode(tempNode, ctx) {
2164
+ removeIdsFromConsideration(ctx, tempNode);
2165
+ if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return;
2166
+ tempNode.remove();
2167
+ ctx.callbacks.afterNodeRemoved(tempNode);
1988
2168
  }
1989
- get restorationData() {
1990
- return this.history.getRestorationDataForIdentifier(this.restorationIdentifier);
2169
+ function isIdInConsideration(ctx, id) {
2170
+ return !ctx.deadIds.has(id);
1991
2171
  }
1992
- get silent() {
1993
- return this.isSamePage;
2172
+ function idIsWithinNode(ctx, id, targetNode) {
2173
+ let idSet = ctx.idMap.get(targetNode) || EMPTY_SET;
2174
+ return idSet.has(id);
1994
2175
  }
1995
- start() {
1996
- if (this.state == VisitState.initialized) {
1997
- this.recordTimingMetric(TimingMetric.visitStart);
1998
- this.state = VisitState.started;
1999
- this.adapter.visitStarted(this);
2000
- this.delegate.visitStarted(this);
2176
+ function removeIdsFromConsideration(ctx, node) {
2177
+ let idSet = ctx.idMap.get(node) || EMPTY_SET;
2178
+ for (const id of idSet) {
2179
+ ctx.deadIds.add(id);
2001
2180
  }
2002
2181
  }
2003
- cancel() {
2004
- if (this.state == VisitState.started) {
2005
- if (this.request) {
2006
- this.request.cancel();
2182
+ function getIdIntersectionCount(ctx, node1, node2) {
2183
+ let sourceSet = ctx.idMap.get(node1) || EMPTY_SET;
2184
+ let matchCount = 0;
2185
+ for (const id of sourceSet) {
2186
+ if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) {
2187
+ ++matchCount;
2007
2188
  }
2008
- this.cancelRender();
2009
- this.state = VisitState.canceled;
2010
2189
  }
2190
+ return matchCount;
2011
2191
  }
2012
- complete() {
2013
- if (this.state == VisitState.started) {
2014
- this.recordTimingMetric(TimingMetric.visitEnd);
2015
- this.adapter.visitCompleted(this);
2016
- this.state = VisitState.completed;
2017
- this.followRedirect();
2018
- if (!this.followedRedirect) {
2019
- this.delegate.visitCompleted(this);
2192
+ function populateIdMapForNode(node, idMap) {
2193
+ let nodeParent = node.parentElement;
2194
+ let idElements = node.querySelectorAll("[id]");
2195
+ for (const elt of idElements) {
2196
+ let current = elt;
2197
+ while (current !== nodeParent && current != null) {
2198
+ let idSet = idMap.get(current);
2199
+ if (idSet == null) {
2200
+ idSet = new Set;
2201
+ idMap.set(current, idSet);
2202
+ }
2203
+ idSet.add(elt.id);
2204
+ current = current.parentElement;
2020
2205
  }
2021
2206
  }
2022
2207
  }
2023
- fail() {
2024
- if (this.state == VisitState.started) {
2025
- this.state = VisitState.failed;
2026
- this.adapter.visitFailed(this);
2027
- this.delegate.visitCompleted(this);
2028
- }
2208
+ function createIdMap(oldContent, newContent) {
2209
+ let idMap = new Map;
2210
+ populateIdMapForNode(oldContent, idMap);
2211
+ populateIdMapForNode(newContent, idMap);
2212
+ return idMap;
2029
2213
  }
2030
- changeHistory() {
2031
- if (!this.historyChanged && this.updateHistory) {
2032
- const actionForHistory = this.location.href === this.referrer?.href ? "replace" : this.action;
2033
- const method = getHistoryMethodForAction(actionForHistory);
2034
- this.history.update(method, this.location, this.restorationIdentifier);
2035
- this.historyChanged = true;
2036
- }
2214
+ return {
2215
+ morph: morph,
2216
+ defaults: defaults
2217
+ };
2218
+ }();
2219
+
2220
+ function morphElements(currentElement, newElement, {callbacks: callbacks, ...options} = {}) {
2221
+ Idiomorph.morph(currentElement, newElement, {
2222
+ ...options,
2223
+ callbacks: new DefaultIdiomorphCallbacks(callbacks)
2224
+ });
2225
+ }
2226
+
2227
+ function morphChildren(currentElement, newElement) {
2228
+ morphElements(currentElement, newElement.children, {
2229
+ morphStyle: "innerHTML"
2230
+ });
2231
+ }
2232
+
2233
+ class DefaultIdiomorphCallbacks {
2234
+ #beforeNodeMorphed;
2235
+ constructor({beforeNodeMorphed: beforeNodeMorphed} = {}) {
2236
+ this.#beforeNodeMorphed = beforeNodeMorphed || (() => true);
2037
2237
  }
2038
- issueRequest() {
2039
- if (this.hasPreloadedResponse()) {
2040
- this.simulateRequest();
2041
- } else if (this.shouldIssueRequest() && !this.request) {
2042
- this.request = new FetchRequest(this, FetchMethod.get, this.location);
2043
- this.request.perform();
2238
+ beforeNodeAdded=node => !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id));
2239
+ beforeNodeMorphed=(currentElement, newElement) => {
2240
+ if (currentElement instanceof Element) {
2241
+ if (!currentElement.hasAttribute("data-turbo-permanent") && this.#beforeNodeMorphed(currentElement, newElement)) {
2242
+ const event = dispatch("turbo:before-morph-element", {
2243
+ cancelable: true,
2244
+ target: currentElement,
2245
+ detail: {
2246
+ currentElement: currentElement,
2247
+ newElement: newElement
2248
+ }
2249
+ });
2250
+ return !event.defaultPrevented;
2251
+ } else {
2252
+ return false;
2253
+ }
2044
2254
  }
2045
- }
2046
- simulateRequest() {
2047
- if (this.response) {
2048
- this.startRequest();
2049
- this.recordResponse();
2050
- this.finishRequest();
2255
+ };
2256
+ beforeAttributeUpdated=(attributeName, target, mutationType) => {
2257
+ const event = dispatch("turbo:before-morph-attribute", {
2258
+ cancelable: true,
2259
+ target: target,
2260
+ detail: {
2261
+ attributeName: attributeName,
2262
+ mutationType: mutationType
2263
+ }
2264
+ });
2265
+ return !event.defaultPrevented;
2266
+ };
2267
+ beforeNodeRemoved=node => this.beforeNodeMorphed(node);
2268
+ afterNodeMorphed=(currentElement, newElement) => {
2269
+ if (currentElement instanceof Element) {
2270
+ dispatch("turbo:morph-element", {
2271
+ target: currentElement,
2272
+ detail: {
2273
+ currentElement: currentElement,
2274
+ newElement: newElement
2275
+ }
2276
+ });
2051
2277
  }
2278
+ };
2279
+ }
2280
+
2281
+ class MorphingFrameRenderer extends FrameRenderer {
2282
+ static renderElement(currentElement, newElement) {
2283
+ dispatch("turbo:before-frame-morph", {
2284
+ target: currentElement,
2285
+ detail: {
2286
+ currentElement: currentElement,
2287
+ newElement: newElement
2288
+ }
2289
+ });
2290
+ morphChildren(currentElement, newElement);
2052
2291
  }
2053
- startRequest() {
2054
- this.recordTimingMetric(TimingMetric.requestStart);
2055
- this.adapter.visitRequestStarted(this);
2056
- }
2057
- recordResponse(response = this.response) {
2058
- this.response = response;
2059
- if (response) {
2060
- const {statusCode: statusCode} = response;
2061
- if (isSuccessful(statusCode)) {
2062
- this.adapter.visitRequestCompleted(this);
2063
- } else {
2064
- this.adapter.visitRequestFailedWithStatusCode(this, statusCode);
2292
+ }
2293
+
2294
+ class ProgressBar {
2295
+ static animationDuration=300;
2296
+ static get defaultCSS() {
2297
+ return unindent`
2298
+ .turbo-progress-bar {
2299
+ position: fixed;
2300
+ display: block;
2301
+ top: 0;
2302
+ left: 0;
2303
+ height: 3px;
2304
+ background: #0076ff;
2305
+ z-index: 2147483647;
2306
+ transition:
2307
+ width ${ProgressBar.animationDuration}ms ease-out,
2308
+ opacity ${ProgressBar.animationDuration / 2}ms ${ProgressBar.animationDuration / 2}ms ease-in;
2309
+ transform: translate3d(0, 0, 0);
2065
2310
  }
2066
- }
2311
+ `;
2067
2312
  }
2068
- finishRequest() {
2069
- this.recordTimingMetric(TimingMetric.requestEnd);
2070
- this.adapter.visitRequestFinished(this);
2313
+ hiding=false;
2314
+ value=0;
2315
+ visible=false;
2316
+ constructor() {
2317
+ this.stylesheetElement = this.createStylesheetElement();
2318
+ this.progressElement = this.createProgressElement();
2319
+ this.installStylesheetElement();
2320
+ this.setValue(0);
2071
2321
  }
2072
- loadResponse() {
2073
- if (this.response) {
2074
- const {statusCode: statusCode, responseHTML: responseHTML} = this.response;
2075
- this.render((async () => {
2076
- if (this.shouldCacheSnapshot) this.cacheSnapshot();
2077
- if (this.view.renderPromise) await this.view.renderPromise;
2078
- if (isSuccessful(statusCode) && responseHTML != null) {
2079
- const snapshot = PageSnapshot.fromHTMLString(responseHTML);
2080
- await this.renderPageSnapshot(snapshot, false);
2081
- this.adapter.visitRendered(this);
2082
- this.complete();
2083
- } else {
2084
- await this.view.renderError(PageSnapshot.fromHTMLString(responseHTML), this);
2085
- this.adapter.visitRendered(this);
2086
- this.fail();
2087
- }
2322
+ show() {
2323
+ if (!this.visible) {
2324
+ this.visible = true;
2325
+ this.installProgressElement();
2326
+ this.startTrickling();
2327
+ }
2328
+ }
2329
+ hide() {
2330
+ if (this.visible && !this.hiding) {
2331
+ this.hiding = true;
2332
+ this.fadeProgressElement((() => {
2333
+ this.uninstallProgressElement();
2334
+ this.stopTrickling();
2335
+ this.visible = false;
2336
+ this.hiding = false;
2088
2337
  }));
2089
2338
  }
2090
2339
  }
2091
- getCachedSnapshot() {
2092
- const snapshot = this.view.getCachedSnapshotForLocation(this.location) || this.getPreloadedSnapshot();
2093
- if (snapshot && (!getAnchor(this.location) || snapshot.hasAnchor(getAnchor(this.location)))) {
2094
- if (this.action == "restore" || snapshot.isPreviewable) {
2095
- return snapshot;
2096
- }
2097
- }
2340
+ setValue(value) {
2341
+ this.value = value;
2342
+ this.refresh();
2098
2343
  }
2099
- getPreloadedSnapshot() {
2100
- if (this.snapshotHTML) {
2101
- return PageSnapshot.fromHTMLString(this.snapshotHTML);
2102
- }
2344
+ installStylesheetElement() {
2345
+ document.head.insertBefore(this.stylesheetElement, document.head.firstChild);
2103
2346
  }
2104
- hasCachedSnapshot() {
2105
- return this.getCachedSnapshot() != null;
2347
+ installProgressElement() {
2348
+ this.progressElement.style.width = "0";
2349
+ this.progressElement.style.opacity = "1";
2350
+ document.documentElement.insertBefore(this.progressElement, document.body);
2351
+ this.refresh();
2106
2352
  }
2107
- loadCachedSnapshot() {
2108
- const snapshot = this.getCachedSnapshot();
2109
- if (snapshot) {
2110
- const isPreview = this.shouldIssueRequest();
2111
- this.render((async () => {
2112
- this.cacheSnapshot();
2113
- if (this.isSamePage || this.isPageRefresh) {
2114
- this.adapter.visitRendered(this);
2115
- } else {
2116
- if (this.view.renderPromise) await this.view.renderPromise;
2117
- await this.renderPageSnapshot(snapshot, isPreview);
2118
- this.adapter.visitRendered(this);
2119
- if (!isPreview) {
2120
- this.complete();
2121
- }
2122
- }
2123
- }));
2124
- }
2353
+ fadeProgressElement(callback) {
2354
+ this.progressElement.style.opacity = "0";
2355
+ setTimeout(callback, ProgressBar.animationDuration * 1.5);
2125
2356
  }
2126
- followRedirect() {
2127
- if (this.redirectedToLocation && !this.followedRedirect && this.response?.redirected) {
2128
- this.adapter.visitProposedToLocation(this.redirectedToLocation, {
2129
- action: "replace",
2130
- response: this.response,
2131
- shouldCacheSnapshot: false,
2132
- willRender: false
2133
- });
2134
- this.followedRedirect = true;
2357
+ uninstallProgressElement() {
2358
+ if (this.progressElement.parentNode) {
2359
+ document.documentElement.removeChild(this.progressElement);
2135
2360
  }
2136
2361
  }
2137
- goToSamePageAnchor() {
2138
- if (this.isSamePage) {
2139
- this.render((async () => {
2140
- this.cacheSnapshot();
2141
- this.performScroll();
2142
- this.changeHistory();
2143
- this.adapter.visitRendered(this);
2144
- }));
2362
+ startTrickling() {
2363
+ if (!this.trickleInterval) {
2364
+ this.trickleInterval = window.setInterval(this.trickle, ProgressBar.animationDuration);
2145
2365
  }
2146
2366
  }
2147
- prepareRequest(request) {
2148
- if (this.acceptsStreamResponse) {
2149
- request.acceptResponseType(StreamMessage.contentType);
2150
- }
2367
+ stopTrickling() {
2368
+ window.clearInterval(this.trickleInterval);
2369
+ delete this.trickleInterval;
2151
2370
  }
2152
- requestStarted() {
2153
- this.startRequest();
2371
+ trickle=() => {
2372
+ this.setValue(this.value + Math.random() / 100);
2373
+ };
2374
+ refresh() {
2375
+ requestAnimationFrame((() => {
2376
+ this.progressElement.style.width = `${10 + this.value * 90}%`;
2377
+ }));
2154
2378
  }
2155
- requestPreventedHandlingResponse(_request, _response) {}
2156
- async requestSucceededWithResponse(request, response) {
2157
- const responseHTML = await response.responseHTML;
2158
- const {redirected: redirected, statusCode: statusCode} = response;
2159
- if (responseHTML == undefined) {
2160
- this.recordResponse({
2161
- statusCode: SystemStatusCode.contentTypeMismatch,
2162
- redirected: redirected
2163
- });
2164
- } else {
2165
- this.redirectedToLocation = response.redirected ? response.location : undefined;
2166
- this.recordResponse({
2167
- statusCode: statusCode,
2168
- responseHTML: responseHTML,
2169
- redirected: redirected
2170
- });
2379
+ createStylesheetElement() {
2380
+ const element = document.createElement("style");
2381
+ element.type = "text/css";
2382
+ element.textContent = ProgressBar.defaultCSS;
2383
+ if (this.cspNonce) {
2384
+ element.nonce = this.cspNonce;
2171
2385
  }
2386
+ return element;
2172
2387
  }
2173
- async requestFailedWithResponse(request, response) {
2174
- const responseHTML = await response.responseHTML;
2175
- const {redirected: redirected, statusCode: statusCode} = response;
2176
- if (responseHTML == undefined) {
2177
- this.recordResponse({
2178
- statusCode: SystemStatusCode.contentTypeMismatch,
2179
- redirected: redirected
2180
- });
2181
- } else {
2182
- this.recordResponse({
2183
- statusCode: statusCode,
2184
- responseHTML: responseHTML,
2185
- redirected: redirected
2186
- });
2187
- }
2388
+ createProgressElement() {
2389
+ const element = document.createElement("div");
2390
+ element.className = "turbo-progress-bar";
2391
+ return element;
2188
2392
  }
2189
- requestErrored(_request, _error) {
2190
- this.recordResponse({
2191
- statusCode: SystemStatusCode.networkFailure,
2192
- redirected: false
2193
- });
2393
+ get cspNonce() {
2394
+ return getMetaContent("csp-nonce");
2194
2395
  }
2195
- requestFinished() {
2196
- this.finishRequest();
2396
+ }
2397
+
2398
+ class HeadSnapshot extends Snapshot {
2399
+ detailsByOuterHTML=this.children.filter((element => !elementIsNoscript(element))).map((element => elementWithoutNonce(element))).reduce(((result, element) => {
2400
+ const {outerHTML: outerHTML} = element;
2401
+ const details = outerHTML in result ? result[outerHTML] : {
2402
+ type: elementType(element),
2403
+ tracked: elementIsTracked(element),
2404
+ elements: []
2405
+ };
2406
+ return {
2407
+ ...result,
2408
+ [outerHTML]: {
2409
+ ...details,
2410
+ elements: [ ...details.elements, element ]
2411
+ }
2412
+ };
2413
+ }), {});
2414
+ get trackedElementSignature() {
2415
+ return Object.keys(this.detailsByOuterHTML).filter((outerHTML => this.detailsByOuterHTML[outerHTML].tracked)).join("");
2197
2416
  }
2198
- performScroll() {
2199
- if (!this.scrolled && !this.view.forceReloaded && !this.view.shouldPreserveScrollPosition(this)) {
2200
- if (this.action == "restore") {
2201
- this.scrollToRestoredPosition() || this.scrollToAnchor() || this.view.scrollToTop();
2417
+ getScriptElementsNotInSnapshot(snapshot) {
2418
+ return this.getElementsMatchingTypeNotInSnapshot("script", snapshot);
2419
+ }
2420
+ getStylesheetElementsNotInSnapshot(snapshot) {
2421
+ return this.getElementsMatchingTypeNotInSnapshot("stylesheet", snapshot);
2422
+ }
2423
+ getElementsMatchingTypeNotInSnapshot(matchedType, snapshot) {
2424
+ return Object.keys(this.detailsByOuterHTML).filter((outerHTML => !(outerHTML in snapshot.detailsByOuterHTML))).map((outerHTML => this.detailsByOuterHTML[outerHTML])).filter((({type: type}) => type == matchedType)).map((({elements: [element]}) => element));
2425
+ }
2426
+ get provisionalElements() {
2427
+ return Object.keys(this.detailsByOuterHTML).reduce(((result, outerHTML) => {
2428
+ const {type: type, tracked: tracked, elements: elements} = this.detailsByOuterHTML[outerHTML];
2429
+ if (type == null && !tracked) {
2430
+ return [ ...result, ...elements ];
2431
+ } else if (elements.length > 1) {
2432
+ return [ ...result, ...elements.slice(1) ];
2202
2433
  } else {
2203
- this.scrollToAnchor() || this.view.scrollToTop();
2204
- }
2205
- if (this.isSamePage) {
2206
- this.delegate.visitScrolledToSamePageLocation(this.view.lastRenderedLocation, this.location);
2434
+ return result;
2207
2435
  }
2208
- this.scrolled = true;
2436
+ }), []);
2437
+ }
2438
+ getMetaValue(name) {
2439
+ const element = this.findMetaElementByName(name);
2440
+ return element ? element.getAttribute("content") : null;
2441
+ }
2442
+ findMetaElementByName(name) {
2443
+ return Object.keys(this.detailsByOuterHTML).reduce(((result, outerHTML) => {
2444
+ const {elements: [element]} = this.detailsByOuterHTML[outerHTML];
2445
+ return elementIsMetaElementWithName(element, name) ? element : result;
2446
+ }), undefined | undefined);
2447
+ }
2448
+ }
2449
+
2450
+ function elementType(element) {
2451
+ if (elementIsScript(element)) {
2452
+ return "script";
2453
+ } else if (elementIsStylesheet(element)) {
2454
+ return "stylesheet";
2455
+ }
2456
+ }
2457
+
2458
+ function elementIsTracked(element) {
2459
+ return element.getAttribute("data-turbo-track") == "reload";
2460
+ }
2461
+
2462
+ function elementIsScript(element) {
2463
+ const tagName = element.localName;
2464
+ return tagName == "script";
2465
+ }
2466
+
2467
+ function elementIsNoscript(element) {
2468
+ const tagName = element.localName;
2469
+ return tagName == "noscript";
2470
+ }
2471
+
2472
+ function elementIsStylesheet(element) {
2473
+ const tagName = element.localName;
2474
+ return tagName == "style" || tagName == "link" && element.getAttribute("rel") == "stylesheet";
2475
+ }
2476
+
2477
+ function elementIsMetaElementWithName(element, name) {
2478
+ const tagName = element.localName;
2479
+ return tagName == "meta" && element.getAttribute("name") == name;
2480
+ }
2481
+
2482
+ function elementWithoutNonce(element) {
2483
+ if (element.hasAttribute("nonce")) {
2484
+ element.setAttribute("nonce", "");
2485
+ }
2486
+ return element;
2487
+ }
2488
+
2489
+ class PageSnapshot extends Snapshot {
2490
+ static fromHTMLString(html = "") {
2491
+ return this.fromDocument(parseHTMLDocument(html));
2492
+ }
2493
+ static fromElement(element) {
2494
+ return this.fromDocument(element.ownerDocument);
2495
+ }
2496
+ static fromDocument({documentElement: documentElement, body: body, head: head}) {
2497
+ return new this(documentElement, body, new HeadSnapshot(head));
2498
+ }
2499
+ constructor(documentElement, body, headSnapshot) {
2500
+ super(body);
2501
+ this.documentElement = documentElement;
2502
+ this.headSnapshot = headSnapshot;
2503
+ }
2504
+ clone() {
2505
+ const clonedElement = this.element.cloneNode(true);
2506
+ const selectElements = this.element.querySelectorAll("select");
2507
+ const clonedSelectElements = clonedElement.querySelectorAll("select");
2508
+ for (const [index, source] of selectElements.entries()) {
2509
+ const clone = clonedSelectElements[index];
2510
+ for (const option of clone.selectedOptions) option.selected = false;
2511
+ for (const option of source.selectedOptions) clone.options[option.index].selected = true;
2209
2512
  }
2210
- }
2211
- scrollToRestoredPosition() {
2212
- const {scrollPosition: scrollPosition} = this.restorationData;
2213
- if (scrollPosition) {
2214
- this.view.scrollToPosition(scrollPosition);
2215
- return true;
2513
+ for (const clonedPasswordInput of clonedElement.querySelectorAll('input[type="password"]')) {
2514
+ clonedPasswordInput.value = "";
2216
2515
  }
2516
+ return new PageSnapshot(this.documentElement, clonedElement, this.headSnapshot);
2217
2517
  }
2218
- scrollToAnchor() {
2219
- const anchor = getAnchor(this.location);
2220
- if (anchor != null) {
2221
- this.view.scrollToAnchor(anchor);
2222
- return true;
2223
- }
2518
+ get lang() {
2519
+ return this.documentElement.getAttribute("lang");
2224
2520
  }
2225
- recordTimingMetric(metric) {
2226
- this.timingMetrics[metric] = (new Date).getTime();
2521
+ get headElement() {
2522
+ return this.headSnapshot.element;
2227
2523
  }
2228
- getTimingMetrics() {
2229
- return {
2230
- ...this.timingMetrics
2231
- };
2524
+ get rootLocation() {
2525
+ const root = this.getSetting("root") ?? "/";
2526
+ return expandURL(root);
2232
2527
  }
2233
- getHistoryMethodForAction(action) {
2234
- switch (action) {
2235
- case "replace":
2236
- return history.replaceState;
2237
-
2238
- case "advance":
2239
- case "restore":
2240
- return history.pushState;
2241
- }
2528
+ get cacheControlValue() {
2529
+ return this.getSetting("cache-control");
2242
2530
  }
2243
- hasPreloadedResponse() {
2244
- return typeof this.response == "object";
2531
+ get isPreviewable() {
2532
+ return this.cacheControlValue != "no-preview";
2245
2533
  }
2246
- shouldIssueRequest() {
2247
- if (this.isSamePage) {
2248
- return false;
2249
- } else if (this.action == "restore") {
2250
- return !this.hasCachedSnapshot();
2251
- } else {
2252
- return this.willRender;
2253
- }
2534
+ get isCacheable() {
2535
+ return this.cacheControlValue != "no-cache";
2254
2536
  }
2255
- cacheSnapshot() {
2256
- if (!this.snapshotCached) {
2257
- this.view.cacheSnapshot(this.snapshot).then((snapshot => snapshot && this.visitCachedSnapshot(snapshot)));
2258
- this.snapshotCached = true;
2259
- }
2537
+ get isVisitable() {
2538
+ return this.getSetting("visit-control") != "reload";
2260
2539
  }
2261
- async render(callback) {
2262
- this.cancelRender();
2263
- this.frame = await nextRepaint();
2264
- await callback();
2265
- delete this.frame;
2540
+ get prefersViewTransitions() {
2541
+ return this.headSnapshot.getMetaValue("view-transition") === "same-origin";
2266
2542
  }
2267
- async renderPageSnapshot(snapshot, isPreview) {
2268
- await this.viewTransitioner.renderChange(this.view.shouldTransitionTo(snapshot), (async () => {
2269
- await this.view.renderPage(snapshot, isPreview, this.willRender, this);
2270
- this.performScroll();
2271
- }));
2543
+ get shouldMorphPage() {
2544
+ return this.getSetting("refresh-method") === "morph";
2272
2545
  }
2273
- cancelRender() {
2274
- if (this.frame) {
2275
- cancelAnimationFrame(this.frame);
2276
- delete this.frame;
2277
- }
2546
+ get shouldPreserveScrollPosition() {
2547
+ return this.getSetting("refresh-scroll") === "preserve";
2548
+ }
2549
+ getSetting(name) {
2550
+ return this.headSnapshot.getMetaValue(`turbo-${name}`);
2278
2551
  }
2279
2552
  }
2280
2553
 
2281
- function isSuccessful(statusCode) {
2282
- return statusCode >= 200 && statusCode < 300;
2283
- }
2284
-
2285
- class BrowserAdapter {
2286
- progressBar=new ProgressBar;
2287
- constructor(session) {
2288
- this.session = session;
2289
- }
2290
- visitProposedToLocation(location, options) {
2291
- if (locationIsVisitable(location, this.navigator.rootLocation)) {
2292
- this.navigator.startVisit(location, options?.restorationIdentifier || uuid(), options);
2293
- } else {
2294
- window.location.href = location.toString();
2295
- }
2296
- }
2297
- visitStarted(visit) {
2298
- this.location = visit.location;
2299
- visit.loadCachedSnapshot();
2300
- visit.issueRequest();
2301
- visit.goToSamePageAnchor();
2302
- }
2303
- visitRequestStarted(visit) {
2304
- this.progressBar.setValue(0);
2305
- if (visit.hasCachedSnapshot() || visit.action != "restore") {
2306
- this.showVisitProgressBarAfterDelay();
2554
+ class ViewTransitioner {
2555
+ #viewTransitionStarted=false;
2556
+ #lastOperation=Promise.resolve();
2557
+ renderChange(useViewTransition, render) {
2558
+ if (useViewTransition && this.viewTransitionsAvailable && !this.#viewTransitionStarted) {
2559
+ this.#viewTransitionStarted = true;
2560
+ this.#lastOperation = this.#lastOperation.then((async () => {
2561
+ await document.startViewTransition(render).finished;
2562
+ }));
2307
2563
  } else {
2308
- this.showProgressBar();
2564
+ this.#lastOperation = this.#lastOperation.then(render);
2309
2565
  }
2566
+ return this.#lastOperation;
2310
2567
  }
2311
- visitRequestCompleted(visit) {
2312
- visit.loadResponse();
2568
+ get viewTransitionsAvailable() {
2569
+ return document.startViewTransition;
2313
2570
  }
2314
- visitRequestFailedWithStatusCode(visit, statusCode) {
2315
- switch (statusCode) {
2316
- case SystemStatusCode.networkFailure:
2317
- case SystemStatusCode.timeoutFailure:
2318
- case SystemStatusCode.contentTypeMismatch:
2319
- return this.reload({
2320
- reason: "request_failed",
2321
- context: {
2322
- statusCode: statusCode
2323
- }
2324
- });
2571
+ }
2325
2572
 
2326
- default:
2327
- return visit.loadResponse();
2328
- }
2329
- }
2330
- visitRequestFinished(_visit) {}
2331
- visitCompleted(_visit) {
2332
- this.progressBar.setValue(1);
2333
- this.hideVisitProgressBar();
2334
- }
2335
- pageInvalidated(reason) {
2336
- this.reload(reason);
2337
- }
2338
- visitFailed(_visit) {
2339
- this.progressBar.setValue(1);
2340
- this.hideVisitProgressBar();
2341
- }
2342
- visitRendered(_visit) {}
2343
- formSubmissionStarted(_formSubmission) {
2344
- this.progressBar.setValue(0);
2345
- this.showFormProgressBarAfterDelay();
2346
- }
2347
- formSubmissionFinished(_formSubmission) {
2348
- this.progressBar.setValue(1);
2349
- this.hideFormProgressBar();
2350
- }
2351
- showVisitProgressBarAfterDelay() {
2352
- this.visitProgressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay);
2573
+ const defaultOptions = {
2574
+ action: "advance",
2575
+ historyChanged: false,
2576
+ visitCachedSnapshot: () => {},
2577
+ willRender: true,
2578
+ updateHistory: true,
2579
+ shouldCacheSnapshot: true,
2580
+ acceptsStreamResponse: false
2581
+ };
2582
+
2583
+ const TimingMetric = {
2584
+ visitStart: "visitStart",
2585
+ requestStart: "requestStart",
2586
+ requestEnd: "requestEnd",
2587
+ visitEnd: "visitEnd"
2588
+ };
2589
+
2590
+ const VisitState = {
2591
+ initialized: "initialized",
2592
+ started: "started",
2593
+ canceled: "canceled",
2594
+ failed: "failed",
2595
+ completed: "completed"
2596
+ };
2597
+
2598
+ const SystemStatusCode = {
2599
+ networkFailure: 0,
2600
+ timeoutFailure: -1,
2601
+ contentTypeMismatch: -2
2602
+ };
2603
+
2604
+ const Direction = {
2605
+ advance: "forward",
2606
+ restore: "back",
2607
+ replace: "none"
2608
+ };
2609
+
2610
+ class Visit {
2611
+ identifier=uuid();
2612
+ timingMetrics={};
2613
+ followedRedirect=false;
2614
+ historyChanged=false;
2615
+ scrolled=false;
2616
+ shouldCacheSnapshot=true;
2617
+ acceptsStreamResponse=false;
2618
+ snapshotCached=false;
2619
+ state=VisitState.initialized;
2620
+ viewTransitioner=new ViewTransitioner;
2621
+ constructor(delegate, location, restorationIdentifier, options = {}) {
2622
+ this.delegate = delegate;
2623
+ this.location = location;
2624
+ this.restorationIdentifier = restorationIdentifier || uuid();
2625
+ const {action: action, historyChanged: historyChanged, referrer: referrer, snapshot: snapshot, snapshotHTML: snapshotHTML, response: response, visitCachedSnapshot: visitCachedSnapshot, willRender: willRender, updateHistory: updateHistory, shouldCacheSnapshot: shouldCacheSnapshot, acceptsStreamResponse: acceptsStreamResponse, direction: direction} = {
2626
+ ...defaultOptions,
2627
+ ...options
2628
+ };
2629
+ this.action = action;
2630
+ this.historyChanged = historyChanged;
2631
+ this.referrer = referrer;
2632
+ this.snapshot = snapshot;
2633
+ this.snapshotHTML = snapshotHTML;
2634
+ this.response = response;
2635
+ this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
2636
+ this.isPageRefresh = this.view.isPageRefresh(this);
2637
+ this.visitCachedSnapshot = visitCachedSnapshot;
2638
+ this.willRender = willRender;
2639
+ this.updateHistory = updateHistory;
2640
+ this.scrolled = !willRender;
2641
+ this.shouldCacheSnapshot = shouldCacheSnapshot;
2642
+ this.acceptsStreamResponse = acceptsStreamResponse;
2643
+ this.direction = direction || Direction[action];
2353
2644
  }
2354
- hideVisitProgressBar() {
2355
- this.progressBar.hide();
2356
- if (this.visitProgressBarTimeout != null) {
2357
- window.clearTimeout(this.visitProgressBarTimeout);
2358
- delete this.visitProgressBarTimeout;
2359
- }
2645
+ get adapter() {
2646
+ return this.delegate.adapter;
2360
2647
  }
2361
- showFormProgressBarAfterDelay() {
2362
- if (this.formProgressBarTimeout == null) {
2363
- this.formProgressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay);
2364
- }
2648
+ get view() {
2649
+ return this.delegate.view;
2365
2650
  }
2366
- hideFormProgressBar() {
2367
- this.progressBar.hide();
2368
- if (this.formProgressBarTimeout != null) {
2369
- window.clearTimeout(this.formProgressBarTimeout);
2370
- delete this.formProgressBarTimeout;
2371
- }
2651
+ get history() {
2652
+ return this.delegate.history;
2372
2653
  }
2373
- showProgressBar=() => {
2374
- this.progressBar.show();
2375
- };
2376
- reload(reason) {
2377
- dispatch("turbo:reload", {
2378
- detail: reason
2379
- });
2380
- window.location.href = this.location?.toString() || window.location.href;
2654
+ get restorationData() {
2655
+ return this.history.getRestorationDataForIdentifier(this.restorationIdentifier);
2381
2656
  }
2382
- get navigator() {
2383
- return this.session.navigator;
2657
+ get silent() {
2658
+ return this.isSamePage;
2384
2659
  }
2385
- }
2386
-
2387
- class CacheObserver {
2388
- selector="[data-turbo-temporary]";
2389
- deprecatedSelector="[data-turbo-cache=false]";
2390
- started=false;
2391
2660
  start() {
2392
- if (!this.started) {
2393
- this.started = true;
2394
- addEventListener("turbo:before-cache", this.removeTemporaryElements, false);
2661
+ if (this.state == VisitState.initialized) {
2662
+ this.recordTimingMetric(TimingMetric.visitStart);
2663
+ this.state = VisitState.started;
2664
+ this.adapter.visitStarted(this);
2665
+ this.delegate.visitStarted(this);
2395
2666
  }
2396
2667
  }
2397
- stop() {
2398
- if (this.started) {
2399
- this.started = false;
2400
- removeEventListener("turbo:before-cache", this.removeTemporaryElements, false);
2668
+ cancel() {
2669
+ if (this.state == VisitState.started) {
2670
+ if (this.request) {
2671
+ this.request.cancel();
2672
+ }
2673
+ this.cancelRender();
2674
+ this.state = VisitState.canceled;
2401
2675
  }
2402
2676
  }
2403
- removeTemporaryElements=_event => {
2404
- for (const element of this.temporaryElements) {
2405
- element.remove();
2677
+ complete() {
2678
+ if (this.state == VisitState.started) {
2679
+ this.recordTimingMetric(TimingMetric.visitEnd);
2680
+ this.adapter.visitCompleted(this);
2681
+ this.state = VisitState.completed;
2682
+ this.followRedirect();
2683
+ if (!this.followedRedirect) {
2684
+ this.delegate.visitCompleted(this);
2685
+ }
2406
2686
  }
2407
- };
2408
- get temporaryElements() {
2409
- return [ ...document.querySelectorAll(this.selector), ...this.temporaryElementsWithDeprecation ];
2410
2687
  }
2411
- get temporaryElementsWithDeprecation() {
2412
- const elements = document.querySelectorAll(this.deprecatedSelector);
2413
- if (elements.length) {
2414
- console.warn(`The ${this.deprecatedSelector} selector is deprecated and will be removed in a future version. Use ${this.selector} instead.`);
2688
+ fail() {
2689
+ if (this.state == VisitState.started) {
2690
+ this.state = VisitState.failed;
2691
+ this.adapter.visitFailed(this);
2692
+ this.delegate.visitCompleted(this);
2415
2693
  }
2416
- return [ ...elements ];
2417
- }
2418
- }
2419
-
2420
- class FrameRedirector {
2421
- constructor(session, element) {
2422
- this.session = session;
2423
- this.element = element;
2424
- this.linkInterceptor = new LinkInterceptor(this, element);
2425
- this.formSubmitObserver = new FormSubmitObserver(this, element);
2426
- }
2427
- start() {
2428
- this.linkInterceptor.start();
2429
- this.formSubmitObserver.start();
2430
- }
2431
- stop() {
2432
- this.linkInterceptor.stop();
2433
- this.formSubmitObserver.stop();
2434
- }
2435
- shouldInterceptLinkClick(element, _location, _event) {
2436
- return this.#shouldRedirect(element);
2437
2694
  }
2438
- linkClickIntercepted(element, url, event) {
2439
- const frame = this.#findFrameElement(element);
2440
- if (frame) {
2441
- frame.delegate.linkClickIntercepted(element, url, event);
2695
+ changeHistory() {
2696
+ if (!this.historyChanged && this.updateHistory) {
2697
+ const actionForHistory = this.location.href === this.referrer?.href ? "replace" : this.action;
2698
+ const method = getHistoryMethodForAction(actionForHistory);
2699
+ this.history.update(method, this.location, this.restorationIdentifier);
2700
+ this.historyChanged = true;
2442
2701
  }
2443
2702
  }
2444
- willSubmitForm(element, submitter) {
2445
- return element.closest("turbo-frame") == null && this.#shouldSubmit(element, submitter) && this.#shouldRedirect(element, submitter);
2446
- }
2447
- formSubmitted(element, submitter) {
2448
- const frame = this.#findFrameElement(element, submitter);
2449
- if (frame) {
2450
- frame.delegate.formSubmitted(element, submitter);
2703
+ issueRequest() {
2704
+ if (this.hasPreloadedResponse()) {
2705
+ this.simulateRequest();
2706
+ } else if (this.shouldIssueRequest() && !this.request) {
2707
+ this.request = new FetchRequest(this, FetchMethod.get, this.location);
2708
+ this.request.perform();
2451
2709
  }
2452
2710
  }
2453
- #shouldSubmit(form, submitter) {
2454
- const action = getAction$1(form, submitter);
2455
- const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
2456
- const rootLocation = expandURL(meta?.content ?? "/");
2457
- return this.#shouldRedirect(form, submitter) && locationIsVisitable(action, rootLocation);
2458
- }
2459
- #shouldRedirect(element, submitter) {
2460
- const isNavigatable = element instanceof HTMLFormElement ? this.session.submissionIsNavigatable(element, submitter) : this.session.elementIsNavigatable(element);
2461
- if (isNavigatable) {
2462
- const frame = this.#findFrameElement(element, submitter);
2463
- return frame ? frame != element.closest("turbo-frame") : false;
2464
- } else {
2465
- return false;
2711
+ simulateRequest() {
2712
+ if (this.response) {
2713
+ this.startRequest();
2714
+ this.recordResponse();
2715
+ this.finishRequest();
2466
2716
  }
2467
2717
  }
2468
- #findFrameElement(element, submitter) {
2469
- const id = submitter?.getAttribute("data-turbo-frame") || element.getAttribute("data-turbo-frame");
2470
- if (id && id != "_top") {
2471
- const frame = this.element.querySelector(`#${id}:not([disabled])`);
2472
- if (frame instanceof FrameElement) {
2473
- return frame;
2718
+ startRequest() {
2719
+ this.recordTimingMetric(TimingMetric.requestStart);
2720
+ this.adapter.visitRequestStarted(this);
2721
+ }
2722
+ recordResponse(response = this.response) {
2723
+ this.response = response;
2724
+ if (response) {
2725
+ const {statusCode: statusCode} = response;
2726
+ if (isSuccessful(statusCode)) {
2727
+ this.adapter.visitRequestCompleted(this);
2728
+ } else {
2729
+ this.adapter.visitRequestFailedWithStatusCode(this, statusCode);
2474
2730
  }
2475
2731
  }
2476
2732
  }
2477
- }
2478
-
2479
- class History {
2480
- location;
2481
- restorationIdentifier=uuid();
2482
- restorationData={};
2483
- started=false;
2484
- pageLoaded=false;
2485
- currentIndex=0;
2486
- constructor(delegate) {
2487
- this.delegate = delegate;
2733
+ finishRequest() {
2734
+ this.recordTimingMetric(TimingMetric.requestEnd);
2735
+ this.adapter.visitRequestFinished(this);
2488
2736
  }
2489
- start() {
2490
- if (!this.started) {
2491
- addEventListener("popstate", this.onPopState, false);
2492
- addEventListener("load", this.onPageLoad, false);
2493
- this.currentIndex = history.state?.turbo?.restorationIndex || 0;
2494
- this.started = true;
2495
- this.replace(new URL(window.location.href));
2737
+ loadResponse() {
2738
+ if (this.response) {
2739
+ const {statusCode: statusCode, responseHTML: responseHTML} = this.response;
2740
+ this.render((async () => {
2741
+ if (this.shouldCacheSnapshot) this.cacheSnapshot();
2742
+ if (this.view.renderPromise) await this.view.renderPromise;
2743
+ if (isSuccessful(statusCode) && responseHTML != null) {
2744
+ const snapshot = PageSnapshot.fromHTMLString(responseHTML);
2745
+ await this.renderPageSnapshot(snapshot, false);
2746
+ this.adapter.visitRendered(this);
2747
+ this.complete();
2748
+ } else {
2749
+ await this.view.renderError(PageSnapshot.fromHTMLString(responseHTML), this);
2750
+ this.adapter.visitRendered(this);
2751
+ this.fail();
2752
+ }
2753
+ }));
2496
2754
  }
2497
2755
  }
2498
- stop() {
2499
- if (this.started) {
2500
- removeEventListener("popstate", this.onPopState, false);
2501
- removeEventListener("load", this.onPageLoad, false);
2502
- this.started = false;
2756
+ getCachedSnapshot() {
2757
+ const snapshot = this.view.getCachedSnapshotForLocation(this.location) || this.getPreloadedSnapshot();
2758
+ if (snapshot && (!getAnchor(this.location) || snapshot.hasAnchor(getAnchor(this.location)))) {
2759
+ if (this.action == "restore" || snapshot.isPreviewable) {
2760
+ return snapshot;
2761
+ }
2503
2762
  }
2504
2763
  }
2505
- push(location, restorationIdentifier) {
2506
- this.update(history.pushState, location, restorationIdentifier);
2507
- }
2508
- replace(location, restorationIdentifier) {
2509
- this.update(history.replaceState, location, restorationIdentifier);
2510
- }
2511
- update(method, location, restorationIdentifier = uuid()) {
2512
- if (method === history.pushState) ++this.currentIndex;
2513
- const state = {
2514
- turbo: {
2515
- restorationIdentifier: restorationIdentifier,
2516
- restorationIndex: this.currentIndex
2517
- }
2518
- };
2519
- method.call(history, state, "", location.href);
2520
- this.location = location;
2521
- this.restorationIdentifier = restorationIdentifier;
2764
+ getPreloadedSnapshot() {
2765
+ if (this.snapshotHTML) {
2766
+ return PageSnapshot.fromHTMLString(this.snapshotHTML);
2767
+ }
2522
2768
  }
2523
- getRestorationDataForIdentifier(restorationIdentifier) {
2524
- return this.restorationData[restorationIdentifier] || {};
2769
+ hasCachedSnapshot() {
2770
+ return this.getCachedSnapshot() != null;
2525
2771
  }
2526
- updateRestorationData(additionalData) {
2527
- const {restorationIdentifier: restorationIdentifier} = this;
2528
- const restorationData = this.restorationData[restorationIdentifier];
2529
- this.restorationData[restorationIdentifier] = {
2530
- ...restorationData,
2531
- ...additionalData
2532
- };
2772
+ loadCachedSnapshot() {
2773
+ const snapshot = this.getCachedSnapshot();
2774
+ if (snapshot) {
2775
+ const isPreview = this.shouldIssueRequest();
2776
+ this.render((async () => {
2777
+ this.cacheSnapshot();
2778
+ if (this.isSamePage || this.isPageRefresh) {
2779
+ this.adapter.visitRendered(this);
2780
+ } else {
2781
+ if (this.view.renderPromise) await this.view.renderPromise;
2782
+ await this.renderPageSnapshot(snapshot, isPreview);
2783
+ this.adapter.visitRendered(this);
2784
+ if (!isPreview) {
2785
+ this.complete();
2786
+ }
2787
+ }
2788
+ }));
2789
+ }
2533
2790
  }
2534
- assumeControlOfScrollRestoration() {
2535
- if (!this.previousScrollRestoration) {
2536
- this.previousScrollRestoration = history.scrollRestoration ?? "auto";
2537
- history.scrollRestoration = "manual";
2791
+ followRedirect() {
2792
+ if (this.redirectedToLocation && !this.followedRedirect && this.response?.redirected) {
2793
+ this.adapter.visitProposedToLocation(this.redirectedToLocation, {
2794
+ action: "replace",
2795
+ response: this.response,
2796
+ shouldCacheSnapshot: false,
2797
+ willRender: false
2798
+ });
2799
+ this.followedRedirect = true;
2538
2800
  }
2539
2801
  }
2540
- relinquishControlOfScrollRestoration() {
2541
- if (this.previousScrollRestoration) {
2542
- history.scrollRestoration = this.previousScrollRestoration;
2543
- delete this.previousScrollRestoration;
2802
+ goToSamePageAnchor() {
2803
+ if (this.isSamePage) {
2804
+ this.render((async () => {
2805
+ this.cacheSnapshot();
2806
+ this.performScroll();
2807
+ this.changeHistory();
2808
+ this.adapter.visitRendered(this);
2809
+ }));
2544
2810
  }
2545
2811
  }
2546
- onPopState=event => {
2547
- if (this.shouldHandlePopState()) {
2548
- const {turbo: turbo} = event.state || {};
2549
- if (turbo) {
2550
- this.location = new URL(window.location.href);
2551
- const {restorationIdentifier: restorationIdentifier, restorationIndex: restorationIndex} = turbo;
2552
- this.restorationIdentifier = restorationIdentifier;
2553
- const direction = restorationIndex > this.currentIndex ? "forward" : "back";
2554
- this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(this.location, restorationIdentifier, direction);
2555
- this.currentIndex = restorationIndex;
2556
- }
2812
+ prepareRequest(request) {
2813
+ if (this.acceptsStreamResponse) {
2814
+ request.acceptResponseType(StreamMessage.contentType);
2557
2815
  }
2558
- };
2559
- onPageLoad=async _event => {
2560
- await nextMicrotask();
2561
- this.pageLoaded = true;
2562
- };
2563
- shouldHandlePopState() {
2564
- return this.pageIsLoaded();
2565
2816
  }
2566
- pageIsLoaded() {
2567
- return this.pageLoaded || document.readyState == "complete";
2817
+ requestStarted() {
2818
+ this.startRequest();
2568
2819
  }
2569
- }
2570
-
2571
- class LinkPrefetchObserver {
2572
- started=false;
2573
- #prefetchedLink=null;
2574
- constructor(delegate, eventTarget) {
2575
- this.delegate = delegate;
2576
- this.eventTarget = eventTarget;
2820
+ requestPreventedHandlingResponse(_request, _response) {}
2821
+ async requestSucceededWithResponse(request, response) {
2822
+ const responseHTML = await response.responseHTML;
2823
+ const {redirected: redirected, statusCode: statusCode} = response;
2824
+ if (responseHTML == undefined) {
2825
+ this.recordResponse({
2826
+ statusCode: SystemStatusCode.contentTypeMismatch,
2827
+ redirected: redirected
2828
+ });
2829
+ } else {
2830
+ this.redirectedToLocation = response.redirected ? response.location : undefined;
2831
+ this.recordResponse({
2832
+ statusCode: statusCode,
2833
+ responseHTML: responseHTML,
2834
+ redirected: redirected
2835
+ });
2836
+ }
2577
2837
  }
2578
- start() {
2579
- if (this.started) return;
2580
- if (this.eventTarget.readyState === "loading") {
2581
- this.eventTarget.addEventListener("DOMContentLoaded", this.#enable, {
2582
- once: true
2838
+ async requestFailedWithResponse(request, response) {
2839
+ const responseHTML = await response.responseHTML;
2840
+ const {redirected: redirected, statusCode: statusCode} = response;
2841
+ if (responseHTML == undefined) {
2842
+ this.recordResponse({
2843
+ statusCode: SystemStatusCode.contentTypeMismatch,
2844
+ redirected: redirected
2583
2845
  });
2584
2846
  } else {
2585
- this.#enable();
2847
+ this.recordResponse({
2848
+ statusCode: statusCode,
2849
+ responseHTML: responseHTML,
2850
+ redirected: redirected
2851
+ });
2586
2852
  }
2587
2853
  }
2588
- stop() {
2589
- if (!this.started) return;
2590
- this.eventTarget.removeEventListener("mouseenter", this.#tryToPrefetchRequest, {
2591
- capture: true,
2592
- passive: true
2593
- });
2594
- this.eventTarget.removeEventListener("mouseleave", this.#cancelRequestIfObsolete, {
2595
- capture: true,
2596
- passive: true
2854
+ requestErrored(_request, _error) {
2855
+ this.recordResponse({
2856
+ statusCode: SystemStatusCode.networkFailure,
2857
+ redirected: false
2597
2858
  });
2598
- this.eventTarget.removeEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true);
2599
- this.started = false;
2600
2859
  }
2601
- #enable=() => {
2602
- this.eventTarget.addEventListener("mouseenter", this.#tryToPrefetchRequest, {
2603
- capture: true,
2604
- passive: true
2605
- });
2606
- this.eventTarget.addEventListener("mouseleave", this.#cancelRequestIfObsolete, {
2607
- capture: true,
2608
- passive: true
2609
- });
2610
- this.eventTarget.addEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true);
2611
- this.started = true;
2612
- };
2613
- #tryToPrefetchRequest=event => {
2614
- if (getMetaContent("turbo-prefetch") === "false") return;
2615
- const target = event.target;
2616
- const isLink = target.matches && target.matches("a[href]:not([target^=_]):not([download])");
2617
- if (isLink && this.#isPrefetchable(target)) {
2618
- const link = target;
2619
- const location = getLocationForLink(link);
2620
- if (this.delegate.canPrefetchRequestToLocation(link, location)) {
2621
- this.#prefetchedLink = link;
2622
- const fetchRequest = new FetchRequest(this, FetchMethod.get, location, new URLSearchParams, target);
2623
- prefetchCache.setLater(location.toString(), fetchRequest, this.#cacheTtl);
2860
+ requestFinished() {
2861
+ this.finishRequest();
2862
+ }
2863
+ performScroll() {
2864
+ if (!this.scrolled && !this.view.forceReloaded && !this.view.shouldPreserveScrollPosition(this)) {
2865
+ if (this.action == "restore") {
2866
+ this.scrollToRestoredPosition() || this.scrollToAnchor() || this.view.scrollToTop();
2867
+ } else {
2868
+ this.scrollToAnchor() || this.view.scrollToTop();
2624
2869
  }
2625
- }
2626
- };
2627
- #cancelRequestIfObsolete=event => {
2628
- if (event.target === this.#prefetchedLink) this.#cancelPrefetchRequest();
2629
- };
2630
- #cancelPrefetchRequest=() => {
2631
- prefetchCache.clear();
2632
- this.#prefetchedLink = null;
2633
- };
2634
- #tryToUsePrefetchedRequest=event => {
2635
- if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "GET") {
2636
- const cached = prefetchCache.get(event.detail.url.toString());
2637
- if (cached) {
2638
- event.detail.fetchRequest = cached;
2870
+ if (this.isSamePage) {
2871
+ this.delegate.visitScrolledToSamePageLocation(this.view.lastRenderedLocation, this.location);
2639
2872
  }
2640
- prefetchCache.clear();
2641
- }
2642
- };
2643
- prepareRequest(request) {
2644
- const link = request.target;
2645
- request.headers["X-Sec-Purpose"] = "prefetch";
2646
- const turboFrame = link.closest("turbo-frame");
2647
- const turboFrameTarget = link.getAttribute("data-turbo-frame") || turboFrame?.getAttribute("target") || turboFrame?.id;
2648
- if (turboFrameTarget && turboFrameTarget !== "_top") {
2649
- request.headers["Turbo-Frame"] = turboFrameTarget;
2873
+ this.scrolled = true;
2650
2874
  }
2651
2875
  }
2652
- requestSucceededWithResponse() {}
2653
- requestStarted(fetchRequest) {}
2654
- requestErrored(fetchRequest) {}
2655
- requestFinished(fetchRequest) {}
2656
- requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}
2657
- requestFailedWithResponse(fetchRequest, fetchResponse) {}
2658
- get #cacheTtl() {
2659
- return Number(getMetaContent("turbo-prefetch-cache-time")) || cacheTtl;
2660
- }
2661
- #isPrefetchable(link) {
2662
- const href = link.getAttribute("href");
2663
- if (!href) return false;
2664
- if (unfetchableLink(link)) return false;
2665
- if (linkToTheSamePage(link)) return false;
2666
- if (linkOptsOut(link)) return false;
2667
- if (nonSafeLink(link)) return false;
2668
- if (eventPrevented(link)) return false;
2669
- return true;
2670
- }
2671
- }
2672
-
2673
- const unfetchableLink = link => link.origin !== document.location.origin || ![ "http:", "https:" ].includes(link.protocol) || link.hasAttribute("target");
2674
-
2675
- const linkToTheSamePage = link => link.pathname + link.search === document.location.pathname + document.location.search || link.href.startsWith("#");
2676
-
2677
- const linkOptsOut = link => {
2678
- if (link.getAttribute("data-turbo-prefetch") === "false") return true;
2679
- if (link.getAttribute("data-turbo") === "false") return true;
2680
- const turboPrefetchParent = findClosestRecursively(link, "[data-turbo-prefetch]");
2681
- if (turboPrefetchParent && turboPrefetchParent.getAttribute("data-turbo-prefetch") === "false") return true;
2682
- return false;
2683
- };
2684
-
2685
- const nonSafeLink = link => {
2686
- const turboMethod = link.getAttribute("data-turbo-method");
2687
- if (turboMethod && turboMethod.toLowerCase() !== "get") return true;
2688
- if (isUJS(link)) return true;
2689
- if (link.hasAttribute("data-turbo-confirm")) return true;
2690
- if (link.hasAttribute("data-turbo-stream")) return true;
2691
- return false;
2692
- };
2693
-
2694
- const isUJS = link => link.hasAttribute("data-remote") || link.hasAttribute("data-behavior") || link.hasAttribute("data-confirm") || link.hasAttribute("data-method");
2695
-
2696
- const eventPrevented = link => {
2697
- const event = dispatch("turbo:before-prefetch", {
2698
- target: link,
2699
- cancelable: true
2700
- });
2701
- return event.defaultPrevented;
2702
- };
2703
-
2704
- class Navigator {
2705
- constructor(delegate) {
2706
- this.delegate = delegate;
2876
+ scrollToRestoredPosition() {
2877
+ const {scrollPosition: scrollPosition} = this.restorationData;
2878
+ if (scrollPosition) {
2879
+ this.view.scrollToPosition(scrollPosition);
2880
+ return true;
2881
+ }
2707
2882
  }
2708
- proposeVisit(location, options = {}) {
2709
- if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
2710
- this.delegate.visitProposedToLocation(location, options);
2883
+ scrollToAnchor() {
2884
+ const anchor = getAnchor(this.location);
2885
+ if (anchor != null) {
2886
+ this.view.scrollToAnchor(anchor);
2887
+ return true;
2711
2888
  }
2712
2889
  }
2713
- startVisit(locatable, restorationIdentifier, options = {}) {
2714
- this.stop();
2715
- this.currentVisit = new Visit(this, expandURL(locatable), restorationIdentifier, {
2716
- referrer: this.location,
2717
- ...options
2718
- });
2719
- this.currentVisit.start();
2890
+ recordTimingMetric(metric) {
2891
+ this.timingMetrics[metric] = (new Date).getTime();
2720
2892
  }
2721
- submitForm(form, submitter) {
2722
- this.stop();
2723
- this.formSubmission = new FormSubmission(this, form, submitter, true);
2724
- this.formSubmission.start();
2893
+ getTimingMetrics() {
2894
+ return {
2895
+ ...this.timingMetrics
2896
+ };
2725
2897
  }
2726
- stop() {
2727
- if (this.formSubmission) {
2728
- this.formSubmission.stop();
2729
- delete this.formSubmission;
2730
- }
2731
- if (this.currentVisit) {
2732
- this.currentVisit.cancel();
2733
- delete this.currentVisit;
2898
+ getHistoryMethodForAction(action) {
2899
+ switch (action) {
2900
+ case "replace":
2901
+ return history.replaceState;
2902
+
2903
+ case "advance":
2904
+ case "restore":
2905
+ return history.pushState;
2734
2906
  }
2735
2907
  }
2736
- get adapter() {
2737
- return this.delegate.adapter;
2908
+ hasPreloadedResponse() {
2909
+ return typeof this.response == "object";
2738
2910
  }
2739
- get view() {
2740
- return this.delegate.view;
2911
+ shouldIssueRequest() {
2912
+ if (this.isSamePage) {
2913
+ return false;
2914
+ } else if (this.action == "restore") {
2915
+ return !this.hasCachedSnapshot();
2916
+ } else {
2917
+ return this.willRender;
2918
+ }
2741
2919
  }
2742
- get rootLocation() {
2743
- return this.view.snapshot.rootLocation;
2920
+ cacheSnapshot() {
2921
+ if (!this.snapshotCached) {
2922
+ this.view.cacheSnapshot(this.snapshot).then((snapshot => snapshot && this.visitCachedSnapshot(snapshot)));
2923
+ this.snapshotCached = true;
2924
+ }
2744
2925
  }
2745
- get history() {
2746
- return this.delegate.history;
2926
+ async render(callback) {
2927
+ this.cancelRender();
2928
+ await new Promise((resolve => {
2929
+ this.frame = document.visibilityState === "hidden" ? setTimeout((() => resolve()), 0) : requestAnimationFrame((() => resolve()));
2930
+ }));
2931
+ await callback();
2932
+ delete this.frame;
2747
2933
  }
2748
- formSubmissionStarted(formSubmission) {
2749
- if (typeof this.adapter.formSubmissionStarted === "function") {
2750
- this.adapter.formSubmissionStarted(formSubmission);
2934
+ async renderPageSnapshot(snapshot, isPreview) {
2935
+ await this.viewTransitioner.renderChange(this.view.shouldTransitionTo(snapshot), (async () => {
2936
+ await this.view.renderPage(snapshot, isPreview, this.willRender, this);
2937
+ this.performScroll();
2938
+ }));
2939
+ }
2940
+ cancelRender() {
2941
+ if (this.frame) {
2942
+ cancelAnimationFrame(this.frame);
2943
+ delete this.frame;
2751
2944
  }
2752
2945
  }
2753
- async formSubmissionSucceededWithResponse(formSubmission, fetchResponse) {
2754
- if (formSubmission == this.formSubmission) {
2755
- const responseHTML = await fetchResponse.responseHTML;
2756
- if (responseHTML) {
2757
- const shouldCacheSnapshot = formSubmission.isSafe;
2758
- if (!shouldCacheSnapshot) {
2759
- this.view.clearSnapshotCache();
2760
- }
2761
- const {statusCode: statusCode, redirected: redirected} = fetchResponse;
2762
- const action = this.#getActionForFormSubmission(formSubmission, fetchResponse);
2763
- const visitOptions = {
2764
- action: action,
2765
- shouldCacheSnapshot: shouldCacheSnapshot,
2766
- response: {
2767
- statusCode: statusCode,
2768
- responseHTML: responseHTML,
2769
- redirected: redirected
2770
- }
2771
- };
2772
- this.proposeVisit(fetchResponse.location, visitOptions);
2773
- }
2946
+ }
2947
+
2948
+ function isSuccessful(statusCode) {
2949
+ return statusCode >= 200 && statusCode < 300;
2950
+ }
2951
+
2952
+ class BrowserAdapter {
2953
+ progressBar=new ProgressBar;
2954
+ constructor(session) {
2955
+ this.session = session;
2956
+ }
2957
+ visitProposedToLocation(location, options) {
2958
+ if (locationIsVisitable(location, this.navigator.rootLocation)) {
2959
+ this.navigator.startVisit(location, options?.restorationIdentifier || uuid(), options);
2960
+ } else {
2961
+ window.location.href = location.toString();
2774
2962
  }
2775
2963
  }
2776
- async formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
2777
- const responseHTML = await fetchResponse.responseHTML;
2778
- if (responseHTML) {
2779
- const snapshot = PageSnapshot.fromHTMLString(responseHTML);
2780
- if (fetchResponse.serverError) {
2781
- await this.view.renderError(snapshot, this.currentVisit);
2782
- } else {
2783
- await this.view.renderPage(snapshot, false, true, this.currentVisit);
2784
- }
2785
- if (!snapshot.shouldPreserveScrollPosition) {
2786
- this.view.scrollToTop();
2787
- }
2788
- this.view.clearSnapshotCache();
2964
+ visitStarted(visit) {
2965
+ this.location = visit.location;
2966
+ visit.loadCachedSnapshot();
2967
+ visit.issueRequest();
2968
+ visit.goToSamePageAnchor();
2969
+ }
2970
+ visitRequestStarted(visit) {
2971
+ this.progressBar.setValue(0);
2972
+ if (visit.hasCachedSnapshot() || visit.action != "restore") {
2973
+ this.showVisitProgressBarAfterDelay();
2974
+ } else {
2975
+ this.showProgressBar();
2789
2976
  }
2790
2977
  }
2791
- formSubmissionErrored(formSubmission, error) {
2792
- console.error(error);
2978
+ visitRequestCompleted(visit) {
2979
+ visit.loadResponse();
2793
2980
  }
2794
- formSubmissionFinished(formSubmission) {
2795
- if (typeof this.adapter.formSubmissionFinished === "function") {
2796
- this.adapter.formSubmissionFinished(formSubmission);
2981
+ visitRequestFailedWithStatusCode(visit, statusCode) {
2982
+ switch (statusCode) {
2983
+ case SystemStatusCode.networkFailure:
2984
+ case SystemStatusCode.timeoutFailure:
2985
+ case SystemStatusCode.contentTypeMismatch:
2986
+ return this.reload({
2987
+ reason: "request_failed",
2988
+ context: {
2989
+ statusCode: statusCode
2990
+ }
2991
+ });
2992
+
2993
+ default:
2994
+ return visit.loadResponse();
2797
2995
  }
2798
2996
  }
2799
- visitStarted(visit) {
2800
- this.delegate.visitStarted(visit);
2997
+ visitRequestFinished(_visit) {}
2998
+ visitCompleted(_visit) {
2999
+ this.progressBar.setValue(1);
3000
+ this.hideVisitProgressBar();
2801
3001
  }
2802
- visitCompleted(visit) {
2803
- this.delegate.visitCompleted(visit);
2804
- delete this.currentVisit;
3002
+ pageInvalidated(reason) {
3003
+ this.reload(reason);
2805
3004
  }
2806
- locationWithActionIsSamePage(location, action) {
2807
- const anchor = getAnchor(location);
2808
- const currentAnchor = getAnchor(this.view.lastRenderedLocation);
2809
- const isRestorationToTop = action === "restore" && typeof anchor === "undefined";
2810
- return action !== "replace" && getRequestURL(location) === getRequestURL(this.view.lastRenderedLocation) && (isRestorationToTop || anchor != null && anchor !== currentAnchor);
3005
+ visitFailed(_visit) {
3006
+ this.progressBar.setValue(1);
3007
+ this.hideVisitProgressBar();
2811
3008
  }
2812
- visitScrolledToSamePageLocation(oldURL, newURL) {
2813
- this.delegate.visitScrolledToSamePageLocation(oldURL, newURL);
3009
+ visitRendered(_visit) {}
3010
+ formSubmissionStarted(_formSubmission) {
3011
+ this.progressBar.setValue(0);
3012
+ this.showFormProgressBarAfterDelay();
2814
3013
  }
2815
- get location() {
2816
- return this.history.location;
3014
+ formSubmissionFinished(_formSubmission) {
3015
+ this.progressBar.setValue(1);
3016
+ this.hideFormProgressBar();
2817
3017
  }
2818
- get restorationIdentifier() {
2819
- return this.history.restorationIdentifier;
3018
+ showVisitProgressBarAfterDelay() {
3019
+ this.visitProgressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay);
2820
3020
  }
2821
- #getActionForFormSubmission(formSubmission, fetchResponse) {
2822
- const {submitter: submitter, formElement: formElement} = formSubmission;
2823
- return getVisitAction(submitter, formElement) || this.#getDefaultAction(fetchResponse);
3021
+ hideVisitProgressBar() {
3022
+ this.progressBar.hide();
3023
+ if (this.visitProgressBarTimeout != null) {
3024
+ window.clearTimeout(this.visitProgressBarTimeout);
3025
+ delete this.visitProgressBarTimeout;
3026
+ }
2824
3027
  }
2825
- #getDefaultAction(fetchResponse) {
2826
- const sameLocationRedirect = fetchResponse.redirected && fetchResponse.location.href === this.location?.href;
2827
- return sameLocationRedirect ? "replace" : "advance";
3028
+ showFormProgressBarAfterDelay() {
3029
+ if (this.formProgressBarTimeout == null) {
3030
+ this.formProgressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay);
3031
+ }
3032
+ }
3033
+ hideFormProgressBar() {
3034
+ this.progressBar.hide();
3035
+ if (this.formProgressBarTimeout != null) {
3036
+ window.clearTimeout(this.formProgressBarTimeout);
3037
+ delete this.formProgressBarTimeout;
3038
+ }
3039
+ }
3040
+ showProgressBar=() => {
3041
+ this.progressBar.show();
3042
+ };
3043
+ reload(reason) {
3044
+ dispatch("turbo:reload", {
3045
+ detail: reason
3046
+ });
3047
+ window.location.href = this.location?.toString() || window.location.href;
3048
+ }
3049
+ get navigator() {
3050
+ return this.session.navigator;
2828
3051
  }
2829
3052
  }
2830
3053
 
2831
- const PageStage = {
2832
- initial: 0,
2833
- loading: 1,
2834
- interactive: 2,
2835
- complete: 3
2836
- };
2837
-
2838
- class PageObserver {
2839
- stage=PageStage.initial;
3054
+ class CacheObserver {
3055
+ selector="[data-turbo-temporary]";
3056
+ deprecatedSelector="[data-turbo-cache=false]";
2840
3057
  started=false;
2841
- constructor(delegate) {
2842
- this.delegate = delegate;
2843
- }
2844
3058
  start() {
2845
3059
  if (!this.started) {
2846
- if (this.stage == PageStage.initial) {
2847
- this.stage = PageStage.loading;
2848
- }
2849
- document.addEventListener("readystatechange", this.interpretReadyState, false);
2850
- addEventListener("pagehide", this.pageWillUnload, false);
2851
3060
  this.started = true;
3061
+ addEventListener("turbo:before-cache", this.removeTemporaryElements, false);
2852
3062
  }
2853
3063
  }
2854
3064
  stop() {
2855
3065
  if (this.started) {
2856
- document.removeEventListener("readystatechange", this.interpretReadyState, false);
2857
- removeEventListener("pagehide", this.pageWillUnload, false);
2858
3066
  this.started = false;
3067
+ removeEventListener("turbo:before-cache", this.removeTemporaryElements, false);
2859
3068
  }
2860
3069
  }
2861
- interpretReadyState=() => {
2862
- const {readyState: readyState} = this;
2863
- if (readyState == "interactive") {
2864
- this.pageIsInteractive();
2865
- } else if (readyState == "complete") {
2866
- this.pageIsComplete();
3070
+ removeTemporaryElements=_event => {
3071
+ for (const element of this.temporaryElements) {
3072
+ element.remove();
2867
3073
  }
2868
3074
  };
2869
- pageIsInteractive() {
2870
- if (this.stage == PageStage.loading) {
2871
- this.stage = PageStage.interactive;
2872
- this.delegate.pageBecameInteractive();
2873
- }
3075
+ get temporaryElements() {
3076
+ return [ ...document.querySelectorAll(this.selector), ...this.temporaryElementsWithDeprecation ];
2874
3077
  }
2875
- pageIsComplete() {
2876
- this.pageIsInteractive();
2877
- if (this.stage == PageStage.interactive) {
2878
- this.stage = PageStage.complete;
2879
- this.delegate.pageLoaded();
3078
+ get temporaryElementsWithDeprecation() {
3079
+ const elements = document.querySelectorAll(this.deprecatedSelector);
3080
+ if (elements.length) {
3081
+ console.warn(`The ${this.deprecatedSelector} selector is deprecated and will be removed in a future version. Use ${this.selector} instead.`);
2880
3082
  }
2881
- }
2882
- pageWillUnload=() => {
2883
- this.delegate.pageWillUnload();
2884
- };
2885
- get readyState() {
2886
- return document.readyState;
3083
+ return [ ...elements ];
2887
3084
  }
2888
3085
  }
2889
3086
 
2890
- class ScrollObserver {
2891
- started=false;
2892
- constructor(delegate) {
2893
- this.delegate = delegate;
3087
+ class FrameRedirector {
3088
+ constructor(session, element) {
3089
+ this.session = session;
3090
+ this.element = element;
3091
+ this.linkInterceptor = new LinkInterceptor(this, element);
3092
+ this.formSubmitObserver = new FormSubmitObserver(this, element);
2894
3093
  }
2895
3094
  start() {
2896
- if (!this.started) {
2897
- addEventListener("scroll", this.onScroll, false);
2898
- this.onScroll();
2899
- this.started = true;
2900
- }
3095
+ this.linkInterceptor.start();
3096
+ this.formSubmitObserver.start();
2901
3097
  }
2902
3098
  stop() {
2903
- if (this.started) {
2904
- removeEventListener("scroll", this.onScroll, false);
2905
- this.started = false;
2906
- }
3099
+ this.linkInterceptor.stop();
3100
+ this.formSubmitObserver.stop();
2907
3101
  }
2908
- onScroll=() => {
2909
- this.updatePosition({
2910
- x: window.pageXOffset,
2911
- y: window.pageYOffset
2912
- });
2913
- };
2914
- updatePosition(position) {
2915
- this.delegate.scrollPositionChanged(position);
3102
+ shouldInterceptLinkClick(element, _location, _event) {
3103
+ return this.#shouldRedirect(element);
2916
3104
  }
2917
- }
2918
-
2919
- class StreamMessageRenderer {
2920
- render({fragment: fragment}) {
2921
- Bardo.preservingPermanentElements(this, getPermanentElementMapForFragment(fragment), (() => {
2922
- withAutofocusFromFragment(fragment, (() => {
2923
- withPreservedFocus((() => {
2924
- document.documentElement.appendChild(fragment);
2925
- }));
2926
- }));
2927
- }));
3105
+ linkClickIntercepted(element, url, event) {
3106
+ const frame = this.#findFrameElement(element);
3107
+ if (frame) {
3108
+ frame.delegate.linkClickIntercepted(element, url, event);
3109
+ }
2928
3110
  }
2929
- enteringBardo(currentPermanentElement, newPermanentElement) {
2930
- newPermanentElement.replaceWith(currentPermanentElement.cloneNode(true));
3111
+ willSubmitForm(element, submitter) {
3112
+ return element.closest("turbo-frame") == null && this.#shouldSubmit(element, submitter) && this.#shouldRedirect(element, submitter);
2931
3113
  }
2932
- leavingBardo() {}
2933
- }
2934
-
2935
- function getPermanentElementMapForFragment(fragment) {
2936
- const permanentElementsInDocument = queryPermanentElementsAll(document.documentElement);
2937
- const permanentElementMap = {};
2938
- for (const permanentElementInDocument of permanentElementsInDocument) {
2939
- const {id: id} = permanentElementInDocument;
2940
- for (const streamElement of fragment.querySelectorAll("turbo-stream")) {
2941
- const elementInStream = getPermanentElementById(streamElement.templateElement.content, id);
2942
- if (elementInStream) {
2943
- permanentElementMap[id] = [ permanentElementInDocument, elementInStream ];
2944
- }
3114
+ formSubmitted(element, submitter) {
3115
+ const frame = this.#findFrameElement(element, submitter);
3116
+ if (frame) {
3117
+ frame.delegate.formSubmitted(element, submitter);
2945
3118
  }
2946
3119
  }
2947
- return permanentElementMap;
2948
- }
2949
-
2950
- async function withAutofocusFromFragment(fragment, callback) {
2951
- const generatedID = `turbo-stream-autofocus-${uuid()}`;
2952
- const turboStreams = fragment.querySelectorAll("turbo-stream");
2953
- const elementWithAutofocus = firstAutofocusableElementInStreams(turboStreams);
2954
- let willAutofocusId = null;
2955
- if (elementWithAutofocus) {
2956
- if (elementWithAutofocus.id) {
2957
- willAutofocusId = elementWithAutofocus.id;
2958
- } else {
2959
- willAutofocusId = generatedID;
2960
- }
2961
- elementWithAutofocus.id = willAutofocusId;
3120
+ #shouldSubmit(form, submitter) {
3121
+ const action = getAction$1(form, submitter);
3122
+ const meta = this.element.ownerDocument.querySelector(`meta[name="turbo-root"]`);
3123
+ const rootLocation = expandURL(meta?.content ?? "/");
3124
+ return this.#shouldRedirect(form, submitter) && locationIsVisitable(action, rootLocation);
2962
3125
  }
2963
- callback();
2964
- await nextRepaint();
2965
- const hasNoActiveElement = document.activeElement == null || document.activeElement == document.body;
2966
- if (hasNoActiveElement && willAutofocusId) {
2967
- const elementToAutofocus = document.getElementById(willAutofocusId);
2968
- if (elementIsFocusable(elementToAutofocus)) {
2969
- elementToAutofocus.focus();
2970
- }
2971
- if (elementToAutofocus && elementToAutofocus.id == generatedID) {
2972
- elementToAutofocus.removeAttribute("id");
3126
+ #shouldRedirect(element, submitter) {
3127
+ const isNavigatable = element instanceof HTMLFormElement ? this.session.submissionIsNavigatable(element, submitter) : this.session.elementIsNavigatable(element);
3128
+ if (isNavigatable) {
3129
+ const frame = this.#findFrameElement(element, submitter);
3130
+ return frame ? frame != element.closest("turbo-frame") : false;
3131
+ } else {
3132
+ return false;
2973
3133
  }
2974
3134
  }
2975
- }
2976
-
2977
- async function withPreservedFocus(callback) {
2978
- const [activeElementBeforeRender, activeElementAfterRender] = await around(callback, (() => document.activeElement));
2979
- const restoreFocusTo = activeElementBeforeRender && activeElementBeforeRender.id;
2980
- if (restoreFocusTo) {
2981
- const elementToFocus = document.getElementById(restoreFocusTo);
2982
- if (elementIsFocusable(elementToFocus) && elementToFocus != activeElementAfterRender) {
2983
- elementToFocus.focus();
3135
+ #findFrameElement(element, submitter) {
3136
+ const id = submitter?.getAttribute("data-turbo-frame") || element.getAttribute("data-turbo-frame");
3137
+ if (id && id != "_top") {
3138
+ const frame = this.element.querySelector(`#${id}:not([disabled])`);
3139
+ if (frame instanceof FrameElement) {
3140
+ return frame;
3141
+ }
2984
3142
  }
2985
3143
  }
2986
3144
  }
2987
3145
 
2988
- function firstAutofocusableElementInStreams(nodeListOfStreamElements) {
2989
- for (const streamElement of nodeListOfStreamElements) {
2990
- const elementWithAutofocus = queryAutofocusableElement(streamElement.templateElement.content);
2991
- if (elementWithAutofocus) return elementWithAutofocus;
2992
- }
2993
- return null;
2994
- }
2995
-
2996
- class StreamObserver {
2997
- sources=new Set;
2998
- #started=false;
3146
+ class History {
3147
+ location;
3148
+ restorationIdentifier=uuid();
3149
+ restorationData={};
3150
+ started=false;
3151
+ pageLoaded=false;
3152
+ currentIndex=0;
2999
3153
  constructor(delegate) {
3000
3154
  this.delegate = delegate;
3001
3155
  }
3002
3156
  start() {
3003
- if (!this.#started) {
3004
- this.#started = true;
3005
- addEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
3157
+ if (!this.started) {
3158
+ addEventListener("popstate", this.onPopState, false);
3159
+ addEventListener("load", this.onPageLoad, false);
3160
+ this.currentIndex = history.state?.turbo?.restorationIndex || 0;
3161
+ this.started = true;
3162
+ this.replace(new URL(window.location.href));
3006
3163
  }
3007
3164
  }
3008
3165
  stop() {
3009
- if (this.#started) {
3010
- this.#started = false;
3011
- removeEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
3012
- }
3013
- }
3014
- connectStreamSource(source) {
3015
- if (!this.streamSourceIsConnected(source)) {
3016
- this.sources.add(source);
3017
- source.addEventListener("message", this.receiveMessageEvent, false);
3018
- }
3019
- }
3020
- disconnectStreamSource(source) {
3021
- if (this.streamSourceIsConnected(source)) {
3022
- this.sources.delete(source);
3023
- source.removeEventListener("message", this.receiveMessageEvent, false);
3166
+ if (this.started) {
3167
+ removeEventListener("popstate", this.onPopState, false);
3168
+ removeEventListener("load", this.onPageLoad, false);
3169
+ this.started = false;
3024
3170
  }
3025
3171
  }
3026
- streamSourceIsConnected(source) {
3027
- return this.sources.has(source);
3172
+ push(location, restorationIdentifier) {
3173
+ this.update(history.pushState, location, restorationIdentifier);
3028
3174
  }
3029
- inspectFetchResponse=event => {
3030
- const response = fetchResponseFromEvent(event);
3031
- if (response && fetchResponseIsStream(response)) {
3032
- event.preventDefault();
3033
- this.receiveMessageResponse(response);
3034
- }
3035
- };
3036
- receiveMessageEvent=event => {
3037
- if (this.#started && typeof event.data == "string") {
3038
- this.receiveMessageHTML(event.data);
3039
- }
3040
- };
3041
- async receiveMessageResponse(response) {
3042
- const html = await response.responseHTML;
3043
- if (html) {
3044
- this.receiveMessageHTML(html);
3045
- }
3175
+ replace(location, restorationIdentifier) {
3176
+ this.update(history.replaceState, location, restorationIdentifier);
3046
3177
  }
3047
- receiveMessageHTML(html) {
3048
- this.delegate.receivedMessageFromStream(StreamMessage.wrap(html));
3178
+ update(method, location, restorationIdentifier = uuid()) {
3179
+ if (method === history.pushState) ++this.currentIndex;
3180
+ const state = {
3181
+ turbo: {
3182
+ restorationIdentifier: restorationIdentifier,
3183
+ restorationIndex: this.currentIndex
3184
+ }
3185
+ };
3186
+ method.call(history, state, "", location.href);
3187
+ this.location = location;
3188
+ this.restorationIdentifier = restorationIdentifier;
3049
3189
  }
3050
- }
3051
-
3052
- function fetchResponseFromEvent(event) {
3053
- const fetchResponse = event.detail?.fetchResponse;
3054
- if (fetchResponse instanceof FetchResponse) {
3055
- return fetchResponse;
3190
+ getRestorationDataForIdentifier(restorationIdentifier) {
3191
+ return this.restorationData[restorationIdentifier] || {};
3056
3192
  }
3057
- }
3058
-
3059
- function fetchResponseIsStream(response) {
3060
- const contentType = response.contentType ?? "";
3061
- return contentType.startsWith(StreamMessage.contentType);
3062
- }
3063
-
3064
- class ErrorRenderer extends Renderer {
3065
- static renderElement(currentElement, newElement) {
3066
- const {documentElement: documentElement, body: body} = document;
3067
- documentElement.replaceChild(newElement, body);
3193
+ updateRestorationData(additionalData) {
3194
+ const {restorationIdentifier: restorationIdentifier} = this;
3195
+ const restorationData = this.restorationData[restorationIdentifier];
3196
+ this.restorationData[restorationIdentifier] = {
3197
+ ...restorationData,
3198
+ ...additionalData
3199
+ };
3068
3200
  }
3069
- async render() {
3070
- this.replaceHeadAndBody();
3071
- this.activateScriptElements();
3201
+ assumeControlOfScrollRestoration() {
3202
+ if (!this.previousScrollRestoration) {
3203
+ this.previousScrollRestoration = history.scrollRestoration ?? "auto";
3204
+ history.scrollRestoration = "manual";
3205
+ }
3072
3206
  }
3073
- replaceHeadAndBody() {
3074
- const {documentElement: documentElement, head: head} = document;
3075
- documentElement.replaceChild(this.newHead, head);
3076
- this.renderElement(this.currentElement, this.newElement);
3207
+ relinquishControlOfScrollRestoration() {
3208
+ if (this.previousScrollRestoration) {
3209
+ history.scrollRestoration = this.previousScrollRestoration;
3210
+ delete this.previousScrollRestoration;
3211
+ }
3077
3212
  }
3078
- activateScriptElements() {
3079
- for (const replaceableElement of this.scriptElements) {
3080
- const parentNode = replaceableElement.parentNode;
3081
- if (parentNode) {
3082
- const element = activateScriptElement(replaceableElement);
3083
- parentNode.replaceChild(element, replaceableElement);
3213
+ onPopState=event => {
3214
+ if (this.shouldHandlePopState()) {
3215
+ const {turbo: turbo} = event.state || {};
3216
+ if (turbo) {
3217
+ this.location = new URL(window.location.href);
3218
+ const {restorationIdentifier: restorationIdentifier, restorationIndex: restorationIndex} = turbo;
3219
+ this.restorationIdentifier = restorationIdentifier;
3220
+ const direction = restorationIndex > this.currentIndex ? "forward" : "back";
3221
+ this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(this.location, restorationIdentifier, direction);
3222
+ this.currentIndex = restorationIndex;
3084
3223
  }
3085
3224
  }
3225
+ };
3226
+ onPageLoad=async _event => {
3227
+ await nextMicrotask();
3228
+ this.pageLoaded = true;
3229
+ };
3230
+ shouldHandlePopState() {
3231
+ return this.pageIsLoaded();
3086
3232
  }
3087
- get newHead() {
3088
- return this.newSnapshot.headSnapshot.element;
3089
- }
3090
- get scriptElements() {
3091
- return document.documentElement.querySelectorAll("script");
3233
+ pageIsLoaded() {
3234
+ return this.pageLoaded || document.readyState == "complete";
3092
3235
  }
3093
3236
  }
3094
3237
 
3095
- var Idiomorph = function() {
3096
- let EMPTY_SET = new Set;
3097
- let defaults = {
3098
- morphStyle: "outerHTML",
3099
- callbacks: {
3100
- beforeNodeAdded: noOp,
3101
- afterNodeAdded: noOp,
3102
- beforeNodeMorphed: noOp,
3103
- afterNodeMorphed: noOp,
3104
- beforeNodeRemoved: noOp,
3105
- afterNodeRemoved: noOp,
3106
- beforeAttributeUpdated: noOp
3107
- },
3108
- head: {
3109
- style: "merge",
3110
- shouldPreserve: function(elt) {
3111
- return elt.getAttribute("im-preserve") === "true";
3112
- },
3113
- shouldReAppend: function(elt) {
3114
- return elt.getAttribute("im-re-append") === "true";
3115
- },
3116
- shouldRemove: noOp,
3117
- afterHeadMorphed: noOp
3118
- }
3119
- };
3120
- function morph(oldNode, newContent, config = {}) {
3121
- if (oldNode instanceof Document) {
3122
- oldNode = oldNode.documentElement;
3123
- }
3124
- if (typeof newContent === "string") {
3125
- newContent = parseContent(newContent);
3126
- }
3127
- let normalizedContent = normalizeContent(newContent);
3128
- let ctx = createMorphContext(oldNode, normalizedContent, config);
3129
- return morphNormalizedContent(oldNode, normalizedContent, ctx);
3238
+ class LinkPrefetchObserver {
3239
+ started=false;
3240
+ #prefetchedLink=null;
3241
+ constructor(delegate, eventTarget) {
3242
+ this.delegate = delegate;
3243
+ this.eventTarget = eventTarget;
3130
3244
  }
3131
- function morphNormalizedContent(oldNode, normalizedNewContent, ctx) {
3132
- if (ctx.head.block) {
3133
- let oldHead = oldNode.querySelector("head");
3134
- let newHead = normalizedNewContent.querySelector("head");
3135
- if (oldHead && newHead) {
3136
- let promises = handleHeadElement(newHead, oldHead, ctx);
3137
- Promise.all(promises).then((function() {
3138
- morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, {
3139
- head: {
3140
- block: false,
3141
- ignore: true
3142
- }
3143
- }));
3144
- }));
3145
- return;
3146
- }
3147
- }
3148
- if (ctx.morphStyle === "innerHTML") {
3149
- morphChildren(normalizedNewContent, oldNode, ctx);
3150
- return oldNode.children;
3151
- } else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) {
3152
- let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx);
3153
- let previousSibling = bestMatch?.previousSibling;
3154
- let nextSibling = bestMatch?.nextSibling;
3155
- let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx);
3156
- if (bestMatch) {
3157
- return insertSiblings(previousSibling, morphedNode, nextSibling);
3158
- } else {
3159
- return [];
3160
- }
3245
+ start() {
3246
+ if (this.started) return;
3247
+ if (this.eventTarget.readyState === "loading") {
3248
+ this.eventTarget.addEventListener("DOMContentLoaded", this.#enable, {
3249
+ once: true
3250
+ });
3161
3251
  } else {
3162
- throw "Do not understand how to morph style " + ctx.morphStyle;
3252
+ this.#enable();
3163
3253
  }
3164
3254
  }
3165
- function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
3166
- return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement && possibleActiveElement !== document.body;
3255
+ stop() {
3256
+ if (!this.started) return;
3257
+ this.eventTarget.removeEventListener("mouseenter", this.#tryToPrefetchRequest, {
3258
+ capture: true,
3259
+ passive: true
3260
+ });
3261
+ this.eventTarget.removeEventListener("mouseleave", this.#cancelRequestIfObsolete, {
3262
+ capture: true,
3263
+ passive: true
3264
+ });
3265
+ this.eventTarget.removeEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true);
3266
+ this.started = false;
3167
3267
  }
3168
- function morphOldNodeTo(oldNode, newContent, ctx) {
3169
- if (ctx.ignoreActive && oldNode === document.activeElement) ; else if (newContent == null) {
3170
- if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
3171
- oldNode.remove();
3172
- ctx.callbacks.afterNodeRemoved(oldNode);
3173
- return null;
3174
- } else if (!isSoftMatch(oldNode, newContent)) {
3175
- if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
3176
- if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode;
3177
- oldNode.parentElement.replaceChild(newContent, oldNode);
3178
- ctx.callbacks.afterNodeAdded(newContent);
3179
- ctx.callbacks.afterNodeRemoved(oldNode);
3180
- return newContent;
3181
- } else {
3182
- if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return oldNode;
3183
- if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) ; else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") {
3184
- handleHeadElement(newContent, oldNode, ctx);
3185
- } else {
3186
- syncNodeFrom(newContent, oldNode, ctx);
3187
- if (!ignoreValueOfActiveElement(oldNode, ctx)) {
3188
- morphChildren(newContent, oldNode, ctx);
3189
- }
3268
+ #enable=() => {
3269
+ this.eventTarget.addEventListener("mouseenter", this.#tryToPrefetchRequest, {
3270
+ capture: true,
3271
+ passive: true
3272
+ });
3273
+ this.eventTarget.addEventListener("mouseleave", this.#cancelRequestIfObsolete, {
3274
+ capture: true,
3275
+ passive: true
3276
+ });
3277
+ this.eventTarget.addEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true);
3278
+ this.started = true;
3279
+ };
3280
+ #tryToPrefetchRequest=event => {
3281
+ if (getMetaContent("turbo-prefetch") === "false") return;
3282
+ const target = event.target;
3283
+ const isLink = target.matches && target.matches("a[href]:not([target^=_]):not([download])");
3284
+ if (isLink && this.#isPrefetchable(target)) {
3285
+ const link = target;
3286
+ const location = getLocationForLink(link);
3287
+ if (this.delegate.canPrefetchRequestToLocation(link, location)) {
3288
+ this.#prefetchedLink = link;
3289
+ const fetchRequest = new FetchRequest(this, FetchMethod.get, location, new URLSearchParams, target);
3290
+ prefetchCache.setLater(location.toString(), fetchRequest, this.#cacheTtl);
3190
3291
  }
3191
- ctx.callbacks.afterNodeMorphed(oldNode, newContent);
3192
- return oldNode;
3193
3292
  }
3194
- }
3195
- function morphChildren(newParent, oldParent, ctx) {
3196
- let nextNewChild = newParent.firstChild;
3197
- let insertionPoint = oldParent.firstChild;
3198
- let newChild;
3199
- while (nextNewChild) {
3200
- newChild = nextNewChild;
3201
- nextNewChild = newChild.nextSibling;
3202
- if (insertionPoint == null) {
3203
- if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
3204
- oldParent.appendChild(newChild);
3205
- ctx.callbacks.afterNodeAdded(newChild);
3206
- removeIdsFromConsideration(ctx, newChild);
3207
- continue;
3208
- }
3209
- if (isIdSetMatch(newChild, insertionPoint, ctx)) {
3210
- morphOldNodeTo(insertionPoint, newChild, ctx);
3211
- insertionPoint = insertionPoint.nextSibling;
3212
- removeIdsFromConsideration(ctx, newChild);
3213
- continue;
3214
- }
3215
- let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx);
3216
- if (idSetMatch) {
3217
- insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx);
3218
- morphOldNodeTo(idSetMatch, newChild, ctx);
3219
- removeIdsFromConsideration(ctx, newChild);
3220
- continue;
3221
- }
3222
- let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx);
3223
- if (softMatch) {
3224
- insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx);
3225
- morphOldNodeTo(softMatch, newChild, ctx);
3226
- removeIdsFromConsideration(ctx, newChild);
3227
- continue;
3293
+ };
3294
+ #cancelRequestIfObsolete=event => {
3295
+ if (event.target === this.#prefetchedLink) this.#cancelPrefetchRequest();
3296
+ };
3297
+ #cancelPrefetchRequest=() => {
3298
+ prefetchCache.clear();
3299
+ this.#prefetchedLink = null;
3300
+ };
3301
+ #tryToUsePrefetchedRequest=event => {
3302
+ if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "GET") {
3303
+ const cached = prefetchCache.get(event.detail.url.toString());
3304
+ if (cached) {
3305
+ event.detail.fetchRequest = cached;
3228
3306
  }
3229
- if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
3230
- oldParent.insertBefore(newChild, insertionPoint);
3231
- ctx.callbacks.afterNodeAdded(newChild);
3232
- removeIdsFromConsideration(ctx, newChild);
3307
+ prefetchCache.clear();
3233
3308
  }
3234
- while (insertionPoint !== null) {
3235
- let tempNode = insertionPoint;
3236
- insertionPoint = insertionPoint.nextSibling;
3237
- removeNode(tempNode, ctx);
3309
+ };
3310
+ prepareRequest(request) {
3311
+ const link = request.target;
3312
+ request.headers["X-Sec-Purpose"] = "prefetch";
3313
+ const turboFrame = link.closest("turbo-frame");
3314
+ const turboFrameTarget = link.getAttribute("data-turbo-frame") || turboFrame?.getAttribute("target") || turboFrame?.id;
3315
+ if (turboFrameTarget && turboFrameTarget !== "_top") {
3316
+ request.headers["Turbo-Frame"] = turboFrameTarget;
3238
3317
  }
3239
3318
  }
3240
- function ignoreAttribute(attr, to, updateType, ctx) {
3241
- if (attr === "value" && ctx.ignoreActiveValue && to === document.activeElement) {
3242
- return true;
3319
+ requestSucceededWithResponse() {}
3320
+ requestStarted(fetchRequest) {}
3321
+ requestErrored(fetchRequest) {}
3322
+ requestFinished(fetchRequest) {}
3323
+ requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}
3324
+ requestFailedWithResponse(fetchRequest, fetchResponse) {}
3325
+ get #cacheTtl() {
3326
+ return Number(getMetaContent("turbo-prefetch-cache-time")) || cacheTtl;
3327
+ }
3328
+ #isPrefetchable(link) {
3329
+ const href = link.getAttribute("href");
3330
+ if (!href) return false;
3331
+ if (unfetchableLink(link)) return false;
3332
+ if (linkToTheSamePage(link)) return false;
3333
+ if (linkOptsOut(link)) return false;
3334
+ if (nonSafeLink(link)) return false;
3335
+ if (eventPrevented(link)) return false;
3336
+ return true;
3337
+ }
3338
+ }
3339
+
3340
+ const unfetchableLink = link => link.origin !== document.location.origin || ![ "http:", "https:" ].includes(link.protocol) || link.hasAttribute("target");
3341
+
3342
+ const linkToTheSamePage = link => link.pathname + link.search === document.location.pathname + document.location.search || link.href.startsWith("#");
3343
+
3344
+ const linkOptsOut = link => {
3345
+ if (link.getAttribute("data-turbo-prefetch") === "false") return true;
3346
+ if (link.getAttribute("data-turbo") === "false") return true;
3347
+ const turboPrefetchParent = findClosestRecursively(link, "[data-turbo-prefetch]");
3348
+ if (turboPrefetchParent && turboPrefetchParent.getAttribute("data-turbo-prefetch") === "false") return true;
3349
+ return false;
3350
+ };
3351
+
3352
+ const nonSafeLink = link => {
3353
+ const turboMethod = link.getAttribute("data-turbo-method");
3354
+ if (turboMethod && turboMethod.toLowerCase() !== "get") return true;
3355
+ if (isUJS(link)) return true;
3356
+ if (link.hasAttribute("data-turbo-confirm")) return true;
3357
+ if (link.hasAttribute("data-turbo-stream")) return true;
3358
+ return false;
3359
+ };
3360
+
3361
+ const isUJS = link => link.hasAttribute("data-remote") || link.hasAttribute("data-behavior") || link.hasAttribute("data-confirm") || link.hasAttribute("data-method");
3362
+
3363
+ const eventPrevented = link => {
3364
+ const event = dispatch("turbo:before-prefetch", {
3365
+ target: link,
3366
+ cancelable: true
3367
+ });
3368
+ return event.defaultPrevented;
3369
+ };
3370
+
3371
+ class Navigator {
3372
+ constructor(delegate) {
3373
+ this.delegate = delegate;
3374
+ }
3375
+ proposeVisit(location, options = {}) {
3376
+ if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
3377
+ this.delegate.visitProposedToLocation(location, options);
3243
3378
  }
3244
- return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false;
3245
3379
  }
3246
- function syncNodeFrom(from, to, ctx) {
3247
- let type = from.nodeType;
3248
- if (type === 1) {
3249
- const fromAttributes = from.attributes;
3250
- const toAttributes = to.attributes;
3251
- for (const fromAttribute of fromAttributes) {
3252
- if (ignoreAttribute(fromAttribute.name, to, "update", ctx)) {
3253
- continue;
3254
- }
3255
- if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) {
3256
- to.setAttribute(fromAttribute.name, fromAttribute.value);
3257
- }
3258
- }
3259
- for (let i = toAttributes.length - 1; 0 <= i; i--) {
3260
- const toAttribute = toAttributes[i];
3261
- if (ignoreAttribute(toAttribute.name, to, "remove", ctx)) {
3262
- continue;
3263
- }
3264
- if (!from.hasAttribute(toAttribute.name)) {
3265
- to.removeAttribute(toAttribute.name);
3266
- }
3267
- }
3268
- }
3269
- if (type === 8 || type === 3) {
3270
- if (to.nodeValue !== from.nodeValue) {
3271
- to.nodeValue = from.nodeValue;
3272
- }
3380
+ startVisit(locatable, restorationIdentifier, options = {}) {
3381
+ this.stop();
3382
+ this.currentVisit = new Visit(this, expandURL(locatable), restorationIdentifier, {
3383
+ referrer: this.location,
3384
+ ...options
3385
+ });
3386
+ this.currentVisit.start();
3387
+ }
3388
+ submitForm(form, submitter) {
3389
+ this.stop();
3390
+ this.formSubmission = new FormSubmission(this, form, submitter, true);
3391
+ this.formSubmission.start();
3392
+ }
3393
+ stop() {
3394
+ if (this.formSubmission) {
3395
+ this.formSubmission.stop();
3396
+ delete this.formSubmission;
3273
3397
  }
3274
- if (!ignoreValueOfActiveElement(to, ctx)) {
3275
- syncInputValue(from, to, ctx);
3398
+ if (this.currentVisit) {
3399
+ this.currentVisit.cancel();
3400
+ delete this.currentVisit;
3276
3401
  }
3277
3402
  }
3278
- function syncBooleanAttribute(from, to, attributeName, ctx) {
3279
- if (from[attributeName] !== to[attributeName]) {
3280
- let ignoreUpdate = ignoreAttribute(attributeName, to, "update", ctx);
3281
- if (!ignoreUpdate) {
3282
- to[attributeName] = from[attributeName];
3283
- }
3284
- if (from[attributeName]) {
3285
- if (!ignoreUpdate) {
3286
- to.setAttribute(attributeName, from[attributeName]);
3287
- }
3288
- } else {
3289
- if (!ignoreAttribute(attributeName, to, "remove", ctx)) {
3290
- to.removeAttribute(attributeName);
3291
- }
3292
- }
3293
- }
3403
+ get adapter() {
3404
+ return this.delegate.adapter;
3294
3405
  }
3295
- function syncInputValue(from, to, ctx) {
3296
- if (from instanceof HTMLInputElement && to instanceof HTMLInputElement && from.type !== "file") {
3297
- let fromValue = from.value;
3298
- let toValue = to.value;
3299
- syncBooleanAttribute(from, to, "checked", ctx);
3300
- syncBooleanAttribute(from, to, "disabled", ctx);
3301
- if (!from.hasAttribute("value")) {
3302
- if (!ignoreAttribute("value", to, "remove", ctx)) {
3303
- to.value = "";
3304
- to.removeAttribute("value");
3305
- }
3306
- } else if (fromValue !== toValue) {
3307
- if (!ignoreAttribute("value", to, "update", ctx)) {
3308
- to.setAttribute("value", fromValue);
3309
- to.value = fromValue;
3310
- }
3311
- }
3312
- } else if (from instanceof HTMLOptionElement) {
3313
- syncBooleanAttribute(from, to, "selected", ctx);
3314
- } else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) {
3315
- let fromValue = from.value;
3316
- let toValue = to.value;
3317
- if (ignoreAttribute("value", to, "update", ctx)) {
3318
- return;
3319
- }
3320
- if (fromValue !== toValue) {
3321
- to.value = fromValue;
3322
- }
3323
- if (to.firstChild && to.firstChild.nodeValue !== fromValue) {
3324
- to.firstChild.nodeValue = fromValue;
3325
- }
3326
- }
3406
+ get view() {
3407
+ return this.delegate.view;
3327
3408
  }
3328
- function handleHeadElement(newHeadTag, currentHead, ctx) {
3329
- let added = [];
3330
- let removed = [];
3331
- let preserved = [];
3332
- let nodesToAppend = [];
3333
- let headMergeStyle = ctx.head.style;
3334
- let srcToNewHeadNodes = new Map;
3335
- for (const newHeadChild of newHeadTag.children) {
3336
- srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
3409
+ get rootLocation() {
3410
+ return this.view.snapshot.rootLocation;
3411
+ }
3412
+ get history() {
3413
+ return this.delegate.history;
3414
+ }
3415
+ formSubmissionStarted(formSubmission) {
3416
+ if (typeof this.adapter.formSubmissionStarted === "function") {
3417
+ this.adapter.formSubmissionStarted(formSubmission);
3337
3418
  }
3338
- for (const currentHeadElt of currentHead.children) {
3339
- let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
3340
- let isReAppended = ctx.head.shouldReAppend(currentHeadElt);
3341
- let isPreserved = ctx.head.shouldPreserve(currentHeadElt);
3342
- if (inNewContent || isPreserved) {
3343
- if (isReAppended) {
3344
- removed.push(currentHeadElt);
3345
- } else {
3346
- srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
3347
- preserved.push(currentHeadElt);
3419
+ }
3420
+ async formSubmissionSucceededWithResponse(formSubmission, fetchResponse) {
3421
+ if (formSubmission == this.formSubmission) {
3422
+ const responseHTML = await fetchResponse.responseHTML;
3423
+ if (responseHTML) {
3424
+ const shouldCacheSnapshot = formSubmission.isSafe;
3425
+ if (!shouldCacheSnapshot) {
3426
+ this.view.clearSnapshotCache();
3348
3427
  }
3349
- } else {
3350
- if (headMergeStyle === "append") {
3351
- if (isReAppended) {
3352
- removed.push(currentHeadElt);
3353
- nodesToAppend.push(currentHeadElt);
3354
- }
3355
- } else {
3356
- if (ctx.head.shouldRemove(currentHeadElt) !== false) {
3357
- removed.push(currentHeadElt);
3428
+ const {statusCode: statusCode, redirected: redirected} = fetchResponse;
3429
+ const action = this.#getActionForFormSubmission(formSubmission, fetchResponse);
3430
+ const visitOptions = {
3431
+ action: action,
3432
+ shouldCacheSnapshot: shouldCacheSnapshot,
3433
+ response: {
3434
+ statusCode: statusCode,
3435
+ responseHTML: responseHTML,
3436
+ redirected: redirected
3358
3437
  }
3359
- }
3360
- }
3361
- }
3362
- nodesToAppend.push(...srcToNewHeadNodes.values());
3363
- let promises = [];
3364
- for (const newNode of nodesToAppend) {
3365
- let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild;
3366
- if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
3367
- if (newElt.href || newElt.src) {
3368
- let resolve = null;
3369
- let promise = new Promise((function(_resolve) {
3370
- resolve = _resolve;
3371
- }));
3372
- newElt.addEventListener("load", (function() {
3373
- resolve();
3374
- }));
3375
- promises.push(promise);
3376
- }
3377
- currentHead.appendChild(newElt);
3378
- ctx.callbacks.afterNodeAdded(newElt);
3379
- added.push(newElt);
3380
- }
3381
- }
3382
- for (const removedElement of removed) {
3383
- if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
3384
- currentHead.removeChild(removedElement);
3385
- ctx.callbacks.afterNodeRemoved(removedElement);
3438
+ };
3439
+ this.proposeVisit(fetchResponse.location, visitOptions);
3386
3440
  }
3387
3441
  }
3388
- ctx.head.afterHeadMorphed(currentHead, {
3389
- added: added,
3390
- kept: preserved,
3391
- removed: removed
3392
- });
3393
- return promises;
3394
- }
3395
- function noOp() {}
3396
- function mergeDefaults(config) {
3397
- let finalConfig = {};
3398
- Object.assign(finalConfig, defaults);
3399
- Object.assign(finalConfig, config);
3400
- finalConfig.callbacks = {};
3401
- Object.assign(finalConfig.callbacks, defaults.callbacks);
3402
- Object.assign(finalConfig.callbacks, config.callbacks);
3403
- finalConfig.head = {};
3404
- Object.assign(finalConfig.head, defaults.head);
3405
- Object.assign(finalConfig.head, config.head);
3406
- return finalConfig;
3407
- }
3408
- function createMorphContext(oldNode, newContent, config) {
3409
- config = mergeDefaults(config);
3410
- return {
3411
- target: oldNode,
3412
- newContent: newContent,
3413
- config: config,
3414
- morphStyle: config.morphStyle,
3415
- ignoreActive: config.ignoreActive,
3416
- ignoreActiveValue: config.ignoreActiveValue,
3417
- idMap: createIdMap(oldNode, newContent),
3418
- deadIds: new Set,
3419
- callbacks: config.callbacks,
3420
- head: config.head
3421
- };
3422
3442
  }
3423
- function isIdSetMatch(node1, node2, ctx) {
3424
- if (node1 == null || node2 == null) {
3425
- return false;
3426
- }
3427
- if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) {
3428
- if (node1.id !== "" && node1.id === node2.id) {
3429
- return true;
3443
+ async formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
3444
+ const responseHTML = await fetchResponse.responseHTML;
3445
+ if (responseHTML) {
3446
+ const snapshot = PageSnapshot.fromHTMLString(responseHTML);
3447
+ if (fetchResponse.serverError) {
3448
+ await this.view.renderError(snapshot, this.currentVisit);
3430
3449
  } else {
3431
- return getIdIntersectionCount(ctx, node1, node2) > 0;
3450
+ await this.view.renderPage(snapshot, false, true, this.currentVisit);
3451
+ }
3452
+ if (!snapshot.shouldPreserveScrollPosition) {
3453
+ this.view.scrollToTop();
3432
3454
  }
3455
+ this.view.clearSnapshotCache();
3433
3456
  }
3434
- return false;
3435
3457
  }
3436
- function isSoftMatch(node1, node2) {
3437
- if (node1 == null || node2 == null) {
3438
- return false;
3439
- }
3440
- return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName;
3458
+ formSubmissionErrored(formSubmission, error) {
3459
+ console.error(error);
3441
3460
  }
3442
- function removeNodesBetween(startInclusive, endExclusive, ctx) {
3443
- while (startInclusive !== endExclusive) {
3444
- let tempNode = startInclusive;
3445
- startInclusive = startInclusive.nextSibling;
3446
- removeNode(tempNode, ctx);
3461
+ formSubmissionFinished(formSubmission) {
3462
+ if (typeof this.adapter.formSubmissionFinished === "function") {
3463
+ this.adapter.formSubmissionFinished(formSubmission);
3447
3464
  }
3448
- removeIdsFromConsideration(ctx, endExclusive);
3449
- return endExclusive.nextSibling;
3450
3465
  }
3451
- function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
3452
- let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent);
3453
- let potentialMatch = null;
3454
- if (newChildPotentialIdCount > 0) {
3455
- let potentialMatch = insertionPoint;
3456
- let otherMatchCount = 0;
3457
- while (potentialMatch != null) {
3458
- if (isIdSetMatch(newChild, potentialMatch, ctx)) {
3459
- return potentialMatch;
3460
- }
3461
- otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent);
3462
- if (otherMatchCount > newChildPotentialIdCount) {
3463
- return null;
3464
- }
3465
- potentialMatch = potentialMatch.nextSibling;
3466
+ visitStarted(visit) {
3467
+ this.delegate.visitStarted(visit);
3468
+ }
3469
+ visitCompleted(visit) {
3470
+ this.delegate.visitCompleted(visit);
3471
+ delete this.currentVisit;
3472
+ }
3473
+ locationWithActionIsSamePage(location, action) {
3474
+ const anchor = getAnchor(location);
3475
+ const currentAnchor = getAnchor(this.view.lastRenderedLocation);
3476
+ const isRestorationToTop = action === "restore" && typeof anchor === "undefined";
3477
+ return action !== "replace" && getRequestURL(location) === getRequestURL(this.view.lastRenderedLocation) && (isRestorationToTop || anchor != null && anchor !== currentAnchor);
3478
+ }
3479
+ visitScrolledToSamePageLocation(oldURL, newURL) {
3480
+ this.delegate.visitScrolledToSamePageLocation(oldURL, newURL);
3481
+ }
3482
+ get location() {
3483
+ return this.history.location;
3484
+ }
3485
+ get restorationIdentifier() {
3486
+ return this.history.restorationIdentifier;
3487
+ }
3488
+ #getActionForFormSubmission(formSubmission, fetchResponse) {
3489
+ const {submitter: submitter, formElement: formElement} = formSubmission;
3490
+ return getVisitAction(submitter, formElement) || this.#getDefaultAction(fetchResponse);
3491
+ }
3492
+ #getDefaultAction(fetchResponse) {
3493
+ const sameLocationRedirect = fetchResponse.redirected && fetchResponse.location.href === this.location?.href;
3494
+ return sameLocationRedirect ? "replace" : "advance";
3495
+ }
3496
+ }
3497
+
3498
+ const PageStage = {
3499
+ initial: 0,
3500
+ loading: 1,
3501
+ interactive: 2,
3502
+ complete: 3
3503
+ };
3504
+
3505
+ class PageObserver {
3506
+ stage=PageStage.initial;
3507
+ started=false;
3508
+ constructor(delegate) {
3509
+ this.delegate = delegate;
3510
+ }
3511
+ start() {
3512
+ if (!this.started) {
3513
+ if (this.stage == PageStage.initial) {
3514
+ this.stage = PageStage.loading;
3466
3515
  }
3516
+ document.addEventListener("readystatechange", this.interpretReadyState, false);
3517
+ addEventListener("pagehide", this.pageWillUnload, false);
3518
+ this.started = true;
3467
3519
  }
3468
- return potentialMatch;
3469
3520
  }
3470
- function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
3471
- let potentialSoftMatch = insertionPoint;
3472
- let nextSibling = newChild.nextSibling;
3473
- let siblingSoftMatchCount = 0;
3474
- while (potentialSoftMatch != null) {
3475
- if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) {
3476
- return null;
3477
- }
3478
- if (isSoftMatch(newChild, potentialSoftMatch)) {
3479
- return potentialSoftMatch;
3480
- }
3481
- if (isSoftMatch(nextSibling, potentialSoftMatch)) {
3482
- siblingSoftMatchCount++;
3483
- nextSibling = nextSibling.nextSibling;
3484
- if (siblingSoftMatchCount >= 2) {
3485
- return null;
3486
- }
3487
- }
3488
- potentialSoftMatch = potentialSoftMatch.nextSibling;
3521
+ stop() {
3522
+ if (this.started) {
3523
+ document.removeEventListener("readystatechange", this.interpretReadyState, false);
3524
+ removeEventListener("pagehide", this.pageWillUnload, false);
3525
+ this.started = false;
3489
3526
  }
3490
- return potentialSoftMatch;
3491
3527
  }
3492
- function parseContent(newContent) {
3493
- let parser = new DOMParser;
3494
- let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, "");
3495
- if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) {
3496
- let content = parser.parseFromString(newContent, "text/html");
3497
- if (contentWithSvgsRemoved.match(/<\/html>/)) {
3498
- content.generatedByIdiomorph = true;
3499
- return content;
3500
- } else {
3501
- let htmlElement = content.firstChild;
3502
- if (htmlElement) {
3503
- htmlElement.generatedByIdiomorph = true;
3504
- return htmlElement;
3505
- } else {
3506
- return null;
3507
- }
3508
- }
3509
- } else {
3510
- let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html");
3511
- let content = responseDoc.body.querySelector("template").content;
3512
- content.generatedByIdiomorph = true;
3513
- return content;
3528
+ interpretReadyState=() => {
3529
+ const {readyState: readyState} = this;
3530
+ if (readyState == "interactive") {
3531
+ this.pageIsInteractive();
3532
+ } else if (readyState == "complete") {
3533
+ this.pageIsComplete();
3534
+ }
3535
+ };
3536
+ pageIsInteractive() {
3537
+ if (this.stage == PageStage.loading) {
3538
+ this.stage = PageStage.interactive;
3539
+ this.delegate.pageBecameInteractive();
3514
3540
  }
3515
3541
  }
3516
- function normalizeContent(newContent) {
3517
- if (newContent == null) {
3518
- const dummyParent = document.createElement("div");
3519
- return dummyParent;
3520
- } else if (newContent.generatedByIdiomorph) {
3521
- return newContent;
3522
- } else if (newContent instanceof Node) {
3523
- const dummyParent = document.createElement("div");
3524
- dummyParent.append(newContent);
3525
- return dummyParent;
3526
- } else {
3527
- const dummyParent = document.createElement("div");
3528
- for (const elt of [ ...newContent ]) {
3529
- dummyParent.append(elt);
3530
- }
3531
- return dummyParent;
3542
+ pageIsComplete() {
3543
+ this.pageIsInteractive();
3544
+ if (this.stage == PageStage.interactive) {
3545
+ this.stage = PageStage.complete;
3546
+ this.delegate.pageLoaded();
3532
3547
  }
3533
3548
  }
3534
- function insertSiblings(previousSibling, morphedNode, nextSibling) {
3535
- let stack = [];
3536
- let added = [];
3537
- while (previousSibling != null) {
3538
- stack.push(previousSibling);
3539
- previousSibling = previousSibling.previousSibling;
3549
+ pageWillUnload=() => {
3550
+ this.delegate.pageWillUnload();
3551
+ };
3552
+ get readyState() {
3553
+ return document.readyState;
3554
+ }
3555
+ }
3556
+
3557
+ class ScrollObserver {
3558
+ started=false;
3559
+ constructor(delegate) {
3560
+ this.delegate = delegate;
3561
+ }
3562
+ start() {
3563
+ if (!this.started) {
3564
+ addEventListener("scroll", this.onScroll, false);
3565
+ this.onScroll();
3566
+ this.started = true;
3540
3567
  }
3541
- while (stack.length > 0) {
3542
- let node = stack.pop();
3543
- added.push(node);
3544
- morphedNode.parentElement.insertBefore(node, morphedNode);
3568
+ }
3569
+ stop() {
3570
+ if (this.started) {
3571
+ removeEventListener("scroll", this.onScroll, false);
3572
+ this.started = false;
3545
3573
  }
3546
- added.push(morphedNode);
3547
- while (nextSibling != null) {
3548
- stack.push(nextSibling);
3549
- added.push(nextSibling);
3550
- nextSibling = nextSibling.nextSibling;
3574
+ }
3575
+ onScroll=() => {
3576
+ this.updatePosition({
3577
+ x: window.pageXOffset,
3578
+ y: window.pageYOffset
3579
+ });
3580
+ };
3581
+ updatePosition(position) {
3582
+ this.delegate.scrollPositionChanged(position);
3583
+ }
3584
+ }
3585
+
3586
+ class StreamMessageRenderer {
3587
+ render({fragment: fragment}) {
3588
+ Bardo.preservingPermanentElements(this, getPermanentElementMapForFragment(fragment), (() => {
3589
+ withAutofocusFromFragment(fragment, (() => {
3590
+ withPreservedFocus((() => {
3591
+ document.documentElement.appendChild(fragment);
3592
+ }));
3593
+ }));
3594
+ }));
3595
+ }
3596
+ enteringBardo(currentPermanentElement, newPermanentElement) {
3597
+ newPermanentElement.replaceWith(currentPermanentElement.cloneNode(true));
3598
+ }
3599
+ leavingBardo() {}
3600
+ }
3601
+
3602
+ function getPermanentElementMapForFragment(fragment) {
3603
+ const permanentElementsInDocument = queryPermanentElementsAll(document.documentElement);
3604
+ const permanentElementMap = {};
3605
+ for (const permanentElementInDocument of permanentElementsInDocument) {
3606
+ const {id: id} = permanentElementInDocument;
3607
+ for (const streamElement of fragment.querySelectorAll("turbo-stream")) {
3608
+ const elementInStream = getPermanentElementById(streamElement.templateElement.content, id);
3609
+ if (elementInStream) {
3610
+ permanentElementMap[id] = [ permanentElementInDocument, elementInStream ];
3611
+ }
3551
3612
  }
3552
- while (stack.length > 0) {
3553
- morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling);
3613
+ }
3614
+ return permanentElementMap;
3615
+ }
3616
+
3617
+ async function withAutofocusFromFragment(fragment, callback) {
3618
+ const generatedID = `turbo-stream-autofocus-${uuid()}`;
3619
+ const turboStreams = fragment.querySelectorAll("turbo-stream");
3620
+ const elementWithAutofocus = firstAutofocusableElementInStreams(turboStreams);
3621
+ let willAutofocusId = null;
3622
+ if (elementWithAutofocus) {
3623
+ if (elementWithAutofocus.id) {
3624
+ willAutofocusId = elementWithAutofocus.id;
3625
+ } else {
3626
+ willAutofocusId = generatedID;
3554
3627
  }
3555
- return added;
3628
+ elementWithAutofocus.id = willAutofocusId;
3556
3629
  }
3557
- function findBestNodeMatch(newContent, oldNode, ctx) {
3558
- let currentElement;
3559
- currentElement = newContent.firstChild;
3560
- let bestElement = currentElement;
3561
- let score = 0;
3562
- while (currentElement) {
3563
- let newScore = scoreElement(currentElement, oldNode, ctx);
3564
- if (newScore > score) {
3565
- bestElement = currentElement;
3566
- score = newScore;
3567
- }
3568
- currentElement = currentElement.nextSibling;
3630
+ callback();
3631
+ await nextRepaint();
3632
+ const hasNoActiveElement = document.activeElement == null || document.activeElement == document.body;
3633
+ if (hasNoActiveElement && willAutofocusId) {
3634
+ const elementToAutofocus = document.getElementById(willAutofocusId);
3635
+ if (elementIsFocusable(elementToAutofocus)) {
3636
+ elementToAutofocus.focus();
3637
+ }
3638
+ if (elementToAutofocus && elementToAutofocus.id == generatedID) {
3639
+ elementToAutofocus.removeAttribute("id");
3569
3640
  }
3570
- return bestElement;
3571
3641
  }
3572
- function scoreElement(node1, node2, ctx) {
3573
- if (isSoftMatch(node1, node2)) {
3574
- return .5 + getIdIntersectionCount(ctx, node1, node2);
3642
+ }
3643
+
3644
+ async function withPreservedFocus(callback) {
3645
+ const [activeElementBeforeRender, activeElementAfterRender] = await around(callback, (() => document.activeElement));
3646
+ const restoreFocusTo = activeElementBeforeRender && activeElementBeforeRender.id;
3647
+ if (restoreFocusTo) {
3648
+ const elementToFocus = document.getElementById(restoreFocusTo);
3649
+ if (elementIsFocusable(elementToFocus) && elementToFocus != activeElementAfterRender) {
3650
+ elementToFocus.focus();
3575
3651
  }
3576
- return 0;
3577
3652
  }
3578
- function removeNode(tempNode, ctx) {
3579
- removeIdsFromConsideration(ctx, tempNode);
3580
- if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return;
3581
- tempNode.remove();
3582
- ctx.callbacks.afterNodeRemoved(tempNode);
3653
+ }
3654
+
3655
+ function firstAutofocusableElementInStreams(nodeListOfStreamElements) {
3656
+ for (const streamElement of nodeListOfStreamElements) {
3657
+ const elementWithAutofocus = queryAutofocusableElement(streamElement.templateElement.content);
3658
+ if (elementWithAutofocus) return elementWithAutofocus;
3583
3659
  }
3584
- function isIdInConsideration(ctx, id) {
3585
- return !ctx.deadIds.has(id);
3660
+ return null;
3661
+ }
3662
+
3663
+ class StreamObserver {
3664
+ sources=new Set;
3665
+ #started=false;
3666
+ constructor(delegate) {
3667
+ this.delegate = delegate;
3586
3668
  }
3587
- function idIsWithinNode(ctx, id, targetNode) {
3588
- let idSet = ctx.idMap.get(targetNode) || EMPTY_SET;
3589
- return idSet.has(id);
3669
+ start() {
3670
+ if (!this.#started) {
3671
+ this.#started = true;
3672
+ addEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
3673
+ }
3590
3674
  }
3591
- function removeIdsFromConsideration(ctx, node) {
3592
- let idSet = ctx.idMap.get(node) || EMPTY_SET;
3593
- for (const id of idSet) {
3594
- ctx.deadIds.add(id);
3675
+ stop() {
3676
+ if (this.#started) {
3677
+ this.#started = false;
3678
+ removeEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
3595
3679
  }
3596
3680
  }
3597
- function getIdIntersectionCount(ctx, node1, node2) {
3598
- let sourceSet = ctx.idMap.get(node1) || EMPTY_SET;
3599
- let matchCount = 0;
3600
- for (const id of sourceSet) {
3601
- if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) {
3602
- ++matchCount;
3603
- }
3681
+ connectStreamSource(source) {
3682
+ if (!this.streamSourceIsConnected(source)) {
3683
+ this.sources.add(source);
3684
+ source.addEventListener("message", this.receiveMessageEvent, false);
3604
3685
  }
3605
- return matchCount;
3606
3686
  }
3607
- function populateIdMapForNode(node, idMap) {
3608
- let nodeParent = node.parentElement;
3609
- let idElements = node.querySelectorAll("[id]");
3610
- for (const elt of idElements) {
3611
- let current = elt;
3612
- while (current !== nodeParent && current != null) {
3613
- let idSet = idMap.get(current);
3614
- if (idSet == null) {
3615
- idSet = new Set;
3616
- idMap.set(current, idSet);
3617
- }
3618
- idSet.add(elt.id);
3619
- current = current.parentElement;
3620
- }
3687
+ disconnectStreamSource(source) {
3688
+ if (this.streamSourceIsConnected(source)) {
3689
+ this.sources.delete(source);
3690
+ source.removeEventListener("message", this.receiveMessageEvent, false);
3621
3691
  }
3622
3692
  }
3623
- function createIdMap(oldContent, newContent) {
3624
- let idMap = new Map;
3625
- populateIdMapForNode(oldContent, idMap);
3626
- populateIdMapForNode(newContent, idMap);
3627
- return idMap;
3693
+ streamSourceIsConnected(source) {
3694
+ return this.sources.has(source);
3628
3695
  }
3629
- return {
3630
- morph: morph,
3631
- defaults: defaults
3696
+ inspectFetchResponse=event => {
3697
+ const response = fetchResponseFromEvent(event);
3698
+ if (response && fetchResponseIsStream(response)) {
3699
+ event.preventDefault();
3700
+ this.receiveMessageResponse(response);
3701
+ }
3632
3702
  };
3633
- }();
3634
-
3635
- function morphElements(currentElement, newElement, {callbacks: callbacks, ...options} = {}) {
3636
- Idiomorph.morph(currentElement, newElement, {
3637
- ...options,
3638
- callbacks: new DefaultIdiomorphCallbacks(callbacks)
3639
- });
3703
+ receiveMessageEvent=event => {
3704
+ if (this.#started && typeof event.data == "string") {
3705
+ this.receiveMessageHTML(event.data);
3706
+ }
3707
+ };
3708
+ async receiveMessageResponse(response) {
3709
+ const html = await response.responseHTML;
3710
+ if (html) {
3711
+ this.receiveMessageHTML(html);
3712
+ }
3713
+ }
3714
+ receiveMessageHTML(html) {
3715
+ this.delegate.receivedMessageFromStream(StreamMessage.wrap(html));
3716
+ }
3640
3717
  }
3641
3718
 
3642
- function morphChildren(currentElement, newElement) {
3643
- morphElements(currentElement, newElement.children, {
3644
- morphStyle: "innerHTML"
3645
- });
3719
+ function fetchResponseFromEvent(event) {
3720
+ const fetchResponse = event.detail?.fetchResponse;
3721
+ if (fetchResponse instanceof FetchResponse) {
3722
+ return fetchResponse;
3723
+ }
3646
3724
  }
3647
3725
 
3648
- class DefaultIdiomorphCallbacks {
3649
- #beforeNodeMorphed;
3650
- constructor({beforeNodeMorphed: beforeNodeMorphed} = {}) {
3651
- this.#beforeNodeMorphed = beforeNodeMorphed || (() => true);
3652
- }
3653
- beforeNodeAdded=node => !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id));
3654
- beforeNodeMorphed=(currentElement, newElement) => {
3655
- if (currentElement instanceof Element) {
3656
- if (!currentElement.hasAttribute("data-turbo-permanent") && this.#beforeNodeMorphed(currentElement, newElement)) {
3657
- const event = dispatch("turbo:before-morph-element", {
3658
- cancelable: true,
3659
- target: currentElement,
3660
- detail: {
3661
- currentElement: currentElement,
3662
- newElement: newElement
3663
- }
3664
- });
3665
- return !event.defaultPrevented;
3666
- } else {
3667
- return false;
3668
- }
3669
- }
3670
- };
3671
- beforeAttributeUpdated=(attributeName, target, mutationType) => {
3672
- const event = dispatch("turbo:before-morph-attribute", {
3673
- cancelable: true,
3674
- target: target,
3675
- detail: {
3676
- attributeName: attributeName,
3677
- mutationType: mutationType
3678
- }
3679
- });
3680
- return !event.defaultPrevented;
3681
- };
3682
- beforeNodeRemoved=node => this.beforeNodeMorphed(node);
3683
- afterNodeMorphed=(currentElement, newElement) => {
3684
- if (currentElement instanceof Element) {
3685
- dispatch("turbo:morph-element", {
3686
- target: currentElement,
3687
- detail: {
3688
- currentElement: currentElement,
3689
- newElement: newElement
3690
- }
3691
- });
3692
- }
3693
- };
3726
+ function fetchResponseIsStream(response) {
3727
+ const contentType = response.contentType ?? "";
3728
+ return contentType.startsWith(StreamMessage.contentType);
3694
3729
  }
3695
3730
 
3696
- class MorphingFrameRenderer extends FrameRenderer {
3731
+ class ErrorRenderer extends Renderer {
3697
3732
  static renderElement(currentElement, newElement) {
3698
- dispatch("turbo:before-frame-morph", {
3699
- target: currentElement,
3700
- detail: {
3701
- currentElement: currentElement,
3702
- newElement: newElement
3733
+ const {documentElement: documentElement, body: body} = document;
3734
+ documentElement.replaceChild(newElement, body);
3735
+ }
3736
+ async render() {
3737
+ this.replaceHeadAndBody();
3738
+ this.activateScriptElements();
3739
+ }
3740
+ replaceHeadAndBody() {
3741
+ const {documentElement: documentElement, head: head} = document;
3742
+ documentElement.replaceChild(this.newHead, head);
3743
+ this.renderElement(this.currentElement, this.newElement);
3744
+ }
3745
+ activateScriptElements() {
3746
+ for (const replaceableElement of this.scriptElements) {
3747
+ const parentNode = replaceableElement.parentNode;
3748
+ if (parentNode) {
3749
+ const element = activateScriptElement(replaceableElement);
3750
+ parentNode.replaceChild(element, replaceableElement);
3703
3751
  }
3704
- });
3705
- morphChildren(currentElement, newElement);
3752
+ }
3753
+ }
3754
+ get newHead() {
3755
+ return this.newSnapshot.headSnapshot.element;
3756
+ }
3757
+ get scriptElements() {
3758
+ return document.documentElement.querySelectorAll("script");
3706
3759
  }
3707
3760
  }
3708
3761
 
@@ -3882,7 +3935,7 @@ class MorphingPageRenderer extends PageRenderer {
3882
3935
  }
3883
3936
  });
3884
3937
  for (const frame of currentElement.querySelectorAll("turbo-frame")) {
3885
- if (canRefreshFrame(frame)) refreshFrame(frame);
3938
+ if (canRefreshFrame(frame)) frame.reload();
3886
3939
  }
3887
3940
  dispatch("turbo:morph", {
3888
3941
  detail: {
@@ -3906,15 +3959,6 @@ function canRefreshFrame(frame) {
3906
3959
  return frame instanceof FrameElement && frame.src && frame.refresh === "morph" && !frame.closest("[data-turbo-permanent]");
3907
3960
  }
3908
3961
 
3909
- function refreshFrame(frame) {
3910
- frame.addEventListener("turbo:before-frame-render", (({detail: detail}) => {
3911
- detail.render = MorphingFrameRenderer.renderElement;
3912
- }), {
3913
- once: true
3914
- });
3915
- frame.reload();
3916
- }
3917
-
3918
3962
  class SnapshotCache {
3919
3963
  keys=[];
3920
3964
  snapshots={};
@@ -4097,11 +4141,8 @@ class Session {
4097
4141
  frameRedirector=new FrameRedirector(this, document.documentElement);
4098
4142
  streamMessageRenderer=new StreamMessageRenderer;
4099
4143
  cache=new Cache(this);
4100
- drive=true;
4101
4144
  enabled=true;
4102
- progressBarDelay=500;
4103
4145
  started=false;
4104
- formMode="on";
4105
4146
  #pageRefreshDebouncePeriod=150;
4106
4147
  constructor(recentRequests) {
4107
4148
  this.recentRequests = recentRequests;
@@ -4180,10 +4221,26 @@ class Session {
4180
4221
  this.view.clearSnapshotCache();
4181
4222
  }
4182
4223
  setProgressBarDelay(delay) {
4224
+ console.warn("Please replace `session.setProgressBarDelay(delay)` with `session.progressBarDelay = delay`. The function is deprecated and will be removed in a future version of Turbo.`");
4183
4225
  this.progressBarDelay = delay;
4184
4226
  }
4185
- setFormMode(mode) {
4186
- this.formMode = mode;
4227
+ set progressBarDelay(delay) {
4228
+ config.drive.progressBarDelay = delay;
4229
+ }
4230
+ get progressBarDelay() {
4231
+ return config.drive.progressBarDelay;
4232
+ }
4233
+ set drive(value) {
4234
+ config.drive.enabled = value;
4235
+ }
4236
+ get drive() {
4237
+ return config.drive.enabled;
4238
+ }
4239
+ set formMode(value) {
4240
+ config.forms.mode = value;
4241
+ }
4242
+ get formMode() {
4243
+ return config.forms.mode;
4187
4244
  }
4188
4245
  get location() {
4189
4246
  return this.history.location;
@@ -4405,11 +4462,11 @@ class Session {
4405
4462
  });
4406
4463
  }
4407
4464
  submissionIsNavigatable(form, submitter) {
4408
- if (this.formMode == "off") {
4465
+ if (config.forms.mode == "off") {
4409
4466
  return false;
4410
4467
  } else {
4411
4468
  const submitterIsNavigatable = submitter ? this.elementIsNavigatable(submitter) : true;
4412
- if (this.formMode == "optin") {
4469
+ if (config.forms.mode == "optin") {
4413
4470
  return submitterIsNavigatable && form.closest('[data-turbo="true"]') != null;
4414
4471
  } else {
4415
4472
  return submitterIsNavigatable && this.elementIsNavigatable(form);
@@ -4419,7 +4476,7 @@ class Session {
4419
4476
  elementIsNavigatable(element) {
4420
4477
  const container = findClosestRecursively(element, "[data-turbo]");
4421
4478
  const withinFrame = findClosestRecursively(element, "turbo-frame");
4422
- if (this.drive || withinFrame) {
4479
+ if (config.drive.enabled || withinFrame) {
4423
4480
  if (container) {
4424
4481
  return container.getAttribute("data-turbo") != "false";
4425
4482
  } else {
@@ -4487,15 +4544,18 @@ function clearCache() {
4487
4544
  }
4488
4545
 
4489
4546
  function setProgressBarDelay(delay) {
4490
- session.setProgressBarDelay(delay);
4547
+ console.warn("Please replace `Turbo.setProgressBarDelay(delay)` with `Turbo.config.drive.progressBarDelay = delay`. The top-level function is deprecated and will be removed in a future version of Turbo.`");
4548
+ config.drive.progressBarDelay = delay;
4491
4549
  }
4492
4550
 
4493
4551
  function setConfirmMethod(confirmMethod) {
4494
- FormSubmission.confirmMethod = confirmMethod;
4552
+ console.warn("Please replace `Turbo.setConfirmMethod(confirmMethod)` with `Turbo.config.forms.confirm = confirmMethod`. The top-level function is deprecated and will be removed in a future version of Turbo.`");
4553
+ config.forms.confirm = confirmMethod;
4495
4554
  }
4496
4555
 
4497
4556
  function setFormMode(mode) {
4498
- session.setFormMode(mode);
4557
+ console.warn("Please replace `Turbo.setFormMode(mode)` with `Turbo.config.forms.mode = mode`. The top-level function is deprecated and will be removed in a future version of Turbo.`");
4558
+ config.forms.mode = mode;
4499
4559
  }
4500
4560
 
4501
4561
  var Turbo = Object.freeze({
@@ -4507,6 +4567,7 @@ var Turbo = Object.freeze({
4507
4567
  PageSnapshot: PageSnapshot,
4508
4568
  FrameRenderer: FrameRenderer,
4509
4569
  fetch: fetchWithTurboHeaders,
4570
+ config: config,
4510
4571
  start: start,
4511
4572
  registerAdapter: registerAdapter,
4512
4573
  visit: visit,
@@ -4575,6 +4636,13 @@ class FrameController {
4575
4636
  }
4576
4637
  }
4577
4638
  sourceURLReloaded() {
4639
+ if (this.element.shouldReloadWithMorph) {
4640
+ this.element.addEventListener("turbo:before-frame-render", (({detail: detail}) => {
4641
+ detail.render = MorphingFrameRenderer.renderElement;
4642
+ }), {
4643
+ once: true
4644
+ });
4645
+ }
4578
4646
  const {src: src} = this.element;
4579
4647
  this.element.removeAttribute("complete");
4580
4648
  this.element.src = null;
@@ -5200,6 +5268,7 @@ var Turbo$1 = Object.freeze({
5200
5268
  StreamSourceElement: StreamSourceElement,
5201
5269
  cache: cache,
5202
5270
  clearCache: clearCache,
5271
+ config: config,
5203
5272
  connectStreamSource: connectStreamSource,
5204
5273
  disconnectStreamSource: disconnectStreamSource,
5205
5274
  fetch: fetchWithTurboHeaders,
@@ -5261,6 +5330,7 @@ function walk(obj) {
5261
5330
  }
5262
5331
 
5263
5332
  class TurboCableStreamSourceElement extends HTMLElement {
5333
+ static observedAttributes=[ "channel", "signed-stream-name" ];
5264
5334
  async connectedCallback() {
5265
5335
  connectStreamSource(this);
5266
5336
  this.subscription = await subscribeTo(this.channel, {
@@ -5272,6 +5342,13 @@ class TurboCableStreamSourceElement extends HTMLElement {
5272
5342
  disconnectedCallback() {
5273
5343
  disconnectStreamSource(this);
5274
5344
  if (this.subscription) this.subscription.unsubscribe();
5345
+ this.subscriptionDisconnected();
5346
+ }
5347
+ attributeChangedCallback() {
5348
+ if (this.subscription) {
5349
+ this.disconnectedCallback();
5350
+ this.connectedCallback();
5351
+ }
5275
5352
  }
5276
5353
  dispatchMessageEvent(data) {
5277
5354
  const event = new MessageEvent("message", {