turbo-rails 2.0.7 → 2.0.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -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", {