turbo-rails 2.0.2 → 2.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  /*!
2
- Turbo 8.0.2
3
- Copyright © 2024 37signals LLC
2
+ Turbo 8.0.13
3
+ Copyright © 2025 37signals LLC
4
4
  */
5
5
  (function(prototype) {
6
6
  if (typeof prototype.requestSubmit == "function") return;
@@ -72,7 +72,7 @@ class FrameElement extends HTMLElement {
72
72
  static delegateConstructor=undefined;
73
73
  loaded=Promise.resolve();
74
74
  static get observedAttributes() {
75
- return [ "disabled", "complete", "loading", "src" ];
75
+ return [ "disabled", "loading", "src" ];
76
76
  }
77
77
  constructor() {
78
78
  super();
@@ -90,11 +90,9 @@ class FrameElement extends HTMLElement {
90
90
  attributeChangedCallback(name) {
91
91
  if (name == "loading") {
92
92
  this.delegate.loadingStyleChanged();
93
- } else if (name == "complete") {
94
- this.delegate.completeChanged();
95
93
  } else if (name == "src") {
96
94
  this.delegate.sourceURLChanged();
97
- } else {
95
+ } else if (name == "disabled") {
98
96
  this.delegate.disabledChanged();
99
97
  }
100
98
  }
@@ -118,6 +116,9 @@ class FrameElement extends HTMLElement {
118
116
  this.removeAttribute("refresh");
119
117
  }
120
118
  }
119
+ get shouldReloadWithMorph() {
120
+ return this.src && this.refresh === "morph";
121
+ }
121
122
  get loading() {
122
123
  return frameLoadingStyleFromString(this.getAttribute("loading") || "");
123
124
  }
@@ -169,122 +170,18 @@ function frameLoadingStyleFromString(style) {
169
170
  }
170
171
  }
171
172
 
172
- function expandURL(locatable) {
173
- return new URL(locatable.toString(), document.baseURI);
174
- }
175
-
176
- function getAnchor(url) {
177
- let anchorMatch;
178
- if (url.hash) {
179
- return url.hash.slice(1);
180
- } else if (anchorMatch = url.href.match(/#(.*)$/)) {
181
- return anchorMatch[1];
182
- }
183
- }
184
-
185
- function getAction$1(form, submitter) {
186
- const action = submitter?.getAttribute("formaction") || form.getAttribute("action") || form.action;
187
- return expandURL(action);
188
- }
189
-
190
- function getExtension(url) {
191
- return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || "";
192
- }
193
-
194
- function isHTML(url) {
195
- return !!getExtension(url).match(/^(?:|\.(?:htm|html|xhtml|php))$/);
196
- }
197
-
198
- function isPrefixedBy(baseURL, url) {
199
- const prefix = getPrefix(url);
200
- return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix);
201
- }
202
-
203
- function locationIsVisitable(location, rootLocation) {
204
- return isPrefixedBy(location, rootLocation) && isHTML(location);
205
- }
206
-
207
- function getRequestURL(url) {
208
- const anchor = getAnchor(url);
209
- return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href;
210
- }
211
-
212
- function toCacheKey(url) {
213
- return getRequestURL(url);
214
- }
215
-
216
- function urlsAreEqual(left, right) {
217
- return expandURL(left).href == expandURL(right).href;
218
- }
219
-
220
- function getPathComponents(url) {
221
- return url.pathname.split("/").slice(1);
222
- }
223
-
224
- function getLastPathComponent(url) {
225
- return getPathComponents(url).slice(-1)[0];
226
- }
227
-
228
- function getPrefix(url) {
229
- return addTrailingSlash(url.origin + url.pathname);
230
- }
231
-
232
- function addTrailingSlash(value) {
233
- return value.endsWith("/") ? value : value + "/";
234
- }
235
-
236
- class FetchResponse {
237
- constructor(response) {
238
- this.response = response;
239
- }
240
- get succeeded() {
241
- return this.response.ok;
242
- }
243
- get failed() {
244
- return !this.succeeded;
245
- }
246
- get clientError() {
247
- return this.statusCode >= 400 && this.statusCode <= 499;
248
- }
249
- get serverError() {
250
- return this.statusCode >= 500 && this.statusCode <= 599;
251
- }
252
- get redirected() {
253
- return this.response.redirected;
254
- }
255
- get location() {
256
- return expandURL(this.response.url);
257
- }
258
- get isHTML() {
259
- return this.contentType && this.contentType.match(/^(?:text\/([^\s;,]+\b)?html|application\/xhtml\+xml)\b/);
260
- }
261
- get statusCode() {
262
- return this.response.status;
263
- }
264
- get contentType() {
265
- return this.header("Content-Type");
266
- }
267
- get responseText() {
268
- return this.response.clone().text();
269
- }
270
- get responseHTML() {
271
- if (this.isHTML) {
272
- return this.response.clone().text();
273
- } else {
274
- return Promise.resolve(undefined);
275
- }
276
- }
277
- header(name) {
278
- return this.response.headers.get(name);
279
- }
280
- }
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
+ };
281
178
 
282
179
  function activateScriptElement(element) {
283
180
  if (element.getAttribute("data-turbo-eval") == "false") {
284
181
  return element;
285
182
  } else {
286
183
  const createdScriptElement = document.createElement("script");
287
- const cspNonce = getMetaContent("csp-nonce");
184
+ const cspNonce = getCspNonce();
288
185
  if (cspNonce) {
289
186
  createdScriptElement.nonce = cspNonce;
290
187
  }
@@ -322,6 +219,11 @@ function dispatch(eventName, {target: target, cancelable: cancelable, detail: de
322
219
  return event;
323
220
  }
324
221
 
222
+ function cancelEvent(event) {
223
+ event.preventDefault();
224
+ event.stopImmediatePropagation();
225
+ }
226
+
325
227
  function nextRepaint() {
326
228
  if (document.visibilityState === "hidden") {
327
229
  return nextEventLoopTick();
@@ -451,6 +353,14 @@ function getMetaContent(name) {
451
353
  return element && element.content;
452
354
  }
453
355
 
356
+ function getCspNonce() {
357
+ const element = getMetaElement("csp-nonce");
358
+ if (element) {
359
+ const {nonce: nonce, content: content} = element;
360
+ return nonce == "" ? content : nonce;
361
+ }
362
+ }
363
+
454
364
  function setMetaContent(name, content) {
455
365
  let element = getMetaElement(name);
456
366
  if (!element) {
@@ -485,13 +395,17 @@ async function around(callback, reader) {
485
395
  return [ before, after ];
486
396
  }
487
397
 
488
- function doesNotTargetIFrame(anchor) {
489
- if (anchor.hasAttribute("target")) {
490
- for (const element of document.getElementsByName(anchor.target)) {
398
+ function doesNotTargetIFrame(name) {
399
+ if (name === "_blank") {
400
+ return false;
401
+ } else if (name) {
402
+ for (const element of document.getElementsByName(name)) {
491
403
  if (element instanceof HTMLIFrameElement) return false;
492
404
  }
405
+ return true;
406
+ } else {
407
+ return true;
493
408
  }
494
- return true;
495
409
  }
496
410
 
497
411
  function findLinkFromClickTarget(target) {
@@ -511,6 +425,152 @@ function debounce(fn, delay) {
511
425
  };
512
426
  }
513
427
 
428
+ const submitter = {
429
+ "aria-disabled": {
430
+ beforeSubmit: submitter => {
431
+ submitter.setAttribute("aria-disabled", "true");
432
+ submitter.addEventListener("click", cancelEvent);
433
+ },
434
+ afterSubmit: submitter => {
435
+ submitter.removeAttribute("aria-disabled");
436
+ submitter.removeEventListener("click", cancelEvent);
437
+ }
438
+ },
439
+ disabled: {
440
+ beforeSubmit: submitter => submitter.disabled = true,
441
+ afterSubmit: submitter => submitter.disabled = false
442
+ }
443
+ };
444
+
445
+ class Config {
446
+ #submitter=null;
447
+ constructor(config) {
448
+ Object.assign(this, config);
449
+ }
450
+ get submitter() {
451
+ return this.#submitter;
452
+ }
453
+ set submitter(value) {
454
+ this.#submitter = submitter[value] || value;
455
+ }
456
+ }
457
+
458
+ const forms = new Config({
459
+ mode: "on",
460
+ submitter: "disabled"
461
+ });
462
+
463
+ const config = {
464
+ drive: drive,
465
+ forms: forms
466
+ };
467
+
468
+ function expandURL(locatable) {
469
+ return new URL(locatable.toString(), document.baseURI);
470
+ }
471
+
472
+ function getAnchor(url) {
473
+ let anchorMatch;
474
+ if (url.hash) {
475
+ return url.hash.slice(1);
476
+ } else if (anchorMatch = url.href.match(/#(.*)$/)) {
477
+ return anchorMatch[1];
478
+ }
479
+ }
480
+
481
+ function getAction$1(form, submitter) {
482
+ const action = submitter?.getAttribute("formaction") || form.getAttribute("action") || form.action;
483
+ return expandURL(action);
484
+ }
485
+
486
+ function getExtension(url) {
487
+ return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || "";
488
+ }
489
+
490
+ function isPrefixedBy(baseURL, url) {
491
+ const prefix = getPrefix(url);
492
+ return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix);
493
+ }
494
+
495
+ function locationIsVisitable(location, rootLocation) {
496
+ return isPrefixedBy(location, rootLocation) && !config.drive.unvisitableExtensions.has(getExtension(location));
497
+ }
498
+
499
+ function getRequestURL(url) {
500
+ const anchor = getAnchor(url);
501
+ return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href;
502
+ }
503
+
504
+ function toCacheKey(url) {
505
+ return getRequestURL(url);
506
+ }
507
+
508
+ function urlsAreEqual(left, right) {
509
+ return expandURL(left).href == expandURL(right).href;
510
+ }
511
+
512
+ function getPathComponents(url) {
513
+ return url.pathname.split("/").slice(1);
514
+ }
515
+
516
+ function getLastPathComponent(url) {
517
+ return getPathComponents(url).slice(-1)[0];
518
+ }
519
+
520
+ function getPrefix(url) {
521
+ return addTrailingSlash(url.origin + url.pathname);
522
+ }
523
+
524
+ function addTrailingSlash(value) {
525
+ return value.endsWith("/") ? value : value + "/";
526
+ }
527
+
528
+ class FetchResponse {
529
+ constructor(response) {
530
+ this.response = response;
531
+ }
532
+ get succeeded() {
533
+ return this.response.ok;
534
+ }
535
+ get failed() {
536
+ return !this.succeeded;
537
+ }
538
+ get clientError() {
539
+ return this.statusCode >= 400 && this.statusCode <= 499;
540
+ }
541
+ get serverError() {
542
+ return this.statusCode >= 500 && this.statusCode <= 599;
543
+ }
544
+ get redirected() {
545
+ return this.response.redirected;
546
+ }
547
+ get location() {
548
+ return expandURL(this.response.url);
549
+ }
550
+ get isHTML() {
551
+ return this.contentType && this.contentType.match(/^(?:text\/([^\s;,]+\b)?html|application\/xhtml\+xml)\b/);
552
+ }
553
+ get statusCode() {
554
+ return this.response.status;
555
+ }
556
+ get contentType() {
557
+ return this.header("Content-Type");
558
+ }
559
+ get responseText() {
560
+ return this.response.clone().text();
561
+ }
562
+ get responseHTML() {
563
+ if (this.isHTML) {
564
+ return this.response.clone().text();
565
+ } else {
566
+ return Promise.resolve(undefined);
567
+ }
568
+ }
569
+ header(name) {
570
+ return this.response.headers.get(name);
571
+ }
572
+ }
573
+
514
574
  class LimitedSet extends Set {
515
575
  constructor(maxSize) {
516
576
  super();
@@ -598,7 +658,7 @@ class FetchRequest {
598
658
  this.fetchOptions = {
599
659
  credentials: "same-origin",
600
660
  redirect: "follow",
601
- method: method,
661
+ method: method.toUpperCase(),
602
662
  headers: {
603
663
  ...this.defaultHeaders
604
664
  },
@@ -618,7 +678,7 @@ class FetchRequest {
618
678
  const [url, body] = buildResourceAndBody(this.url, fetchMethod, fetchBody, this.enctype);
619
679
  this.url = url;
620
680
  this.fetchOptions.body = body;
621
- this.fetchOptions.method = fetchMethod;
681
+ this.fetchOptions.method = fetchMethod.toUpperCase();
622
682
  }
623
683
  get headers() {
624
684
  return this.fetchOptions.headers;
@@ -859,7 +919,7 @@ const FormSubmissionState = {
859
919
 
860
920
  class FormSubmission {
861
921
  state=FormSubmissionState.initialized;
862
- static confirmMethod(message, _element, _submitter) {
922
+ static confirmMethod(message) {
863
923
  return Promise.resolve(confirm(message));
864
924
  }
865
925
  constructor(delegate, formElement, submitter, mustRedirect = false) {
@@ -901,7 +961,8 @@ class FormSubmission {
901
961
  const {initialized: initialized, requesting: requesting} = FormSubmissionState;
902
962
  const confirmationMessage = getAttribute("data-turbo-confirm", this.submitter, this.formElement);
903
963
  if (typeof confirmationMessage === "string") {
904
- const answer = await FormSubmission.confirmMethod(confirmationMessage, this.formElement, this.submitter);
964
+ const confirmMethod = typeof config.forms.confirm === "function" ? config.forms.confirm : FormSubmission.confirmMethod;
965
+ const answer = await confirmMethod(confirmationMessage, this.formElement, this.submitter);
905
966
  if (!answer) {
906
967
  return;
907
968
  }
@@ -932,7 +993,7 @@ class FormSubmission {
932
993
  }
933
994
  requestStarted(_request) {
934
995
  this.state = FormSubmissionState.waiting;
935
- this.submitter?.setAttribute("disabled", "");
996
+ if (this.submitter) config.forms.submitter.beforeSubmit(this.submitter);
936
997
  this.setSubmitsWith();
937
998
  markAsBusy(this.formElement);
938
999
  dispatch("turbo:submit-start", {
@@ -984,7 +1045,7 @@ class FormSubmission {
984
1045
  }
985
1046
  requestFinished(_request) {
986
1047
  this.state = FormSubmissionState.stopped;
987
- this.submitter?.removeAttribute("disabled");
1048
+ if (this.submitter) config.forms.submitter.afterSubmit(this.submitter);
988
1049
  this.resetSubmitterText();
989
1050
  clearBusyState(this.formElement);
990
1051
  dispatch("turbo:submit-end", {
@@ -1168,15 +1229,8 @@ function submissionDoesNotDismissDialog(form, submitter) {
1168
1229
  }
1169
1230
 
1170
1231
  function submissionDoesNotTargetIFrame(form, submitter) {
1171
- if (submitter?.hasAttribute("formtarget") || form.hasAttribute("target")) {
1172
- const target = submitter?.getAttribute("formtarget") || form.target;
1173
- for (const element of document.getElementsByName(target)) {
1174
- if (element instanceof HTMLIFrameElement) return false;
1175
- }
1176
- return true;
1177
- } else {
1178
- return true;
1179
- }
1232
+ const target = submitter?.getAttribute("formtarget") || form.getAttribute("target");
1233
+ return doesNotTargetIFrame(target);
1180
1234
  }
1181
1235
 
1182
1236
  class View {
@@ -1309,14 +1363,14 @@ class LinkInterceptor {
1309
1363
  document.removeEventListener("turbo:before-visit", this.willVisit);
1310
1364
  }
1311
1365
  clickBubbled=event => {
1312
- if (this.respondsToEventTarget(event.target)) {
1366
+ if (this.clickEventIsSignificant(event)) {
1313
1367
  this.clickEvent = event;
1314
1368
  } else {
1315
1369
  delete this.clickEvent;
1316
1370
  }
1317
1371
  };
1318
1372
  linkClicked=event => {
1319
- if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) {
1373
+ if (this.clickEvent && this.clickEventIsSignificant(event)) {
1320
1374
  if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url, event.detail.originalEvent)) {
1321
1375
  this.clickEvent.preventDefault();
1322
1376
  event.preventDefault();
@@ -1328,9 +1382,10 @@ class LinkInterceptor {
1328
1382
  willVisit=_event => {
1329
1383
  delete this.clickEvent;
1330
1384
  };
1331
- respondsToEventTarget(target) {
1332
- const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
1333
- return element && element.closest("turbo-frame, html") == this.element;
1385
+ clickEventIsSignificant(event) {
1386
+ const target = event.composed ? event.target?.parentElement : event.target;
1387
+ const element = findLinkFromClickTarget(target) || target;
1388
+ return element instanceof Element && element.closest("turbo-frame, html") == this.element;
1334
1389
  }
1335
1390
  }
1336
1391
 
@@ -1360,7 +1415,7 @@ class LinkClickObserver {
1360
1415
  if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {
1361
1416
  const target = event.composedPath && event.composedPath()[0] || event.target;
1362
1417
  const link = findLinkFromClickTarget(target);
1363
- if (link && doesNotTargetIFrame(link)) {
1418
+ if (link && doesNotTargetIFrame(link.target)) {
1364
1419
  const location = getLocationForLink(link);
1365
1420
  if (this.delegate.willFollowLinkToLocation(link, location, event)) {
1366
1421
  event.preventDefault();
@@ -1484,12 +1539,13 @@ function createPlaceholderForPermanentElement(permanentElement) {
1484
1539
 
1485
1540
  class Renderer {
1486
1541
  #activeElement=null;
1487
- constructor(currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) {
1542
+ static renderElement(currentElement, newElement) {}
1543
+ constructor(currentSnapshot, newSnapshot, isPreview, willRender = true) {
1488
1544
  this.currentSnapshot = currentSnapshot;
1489
1545
  this.newSnapshot = newSnapshot;
1490
1546
  this.isPreview = isPreview;
1491
1547
  this.willRender = willRender;
1492
- this.renderElement = renderElement;
1548
+ this.renderElement = this.constructor.renderElement;
1493
1549
  this.promise = new Promise(((resolve, reject) => this.resolvingFunctions = {
1494
1550
  resolve: resolve,
1495
1551
  reject: reject
@@ -1498,6 +1554,9 @@ class Renderer {
1498
1554
  get shouldRender() {
1499
1555
  return true;
1500
1556
  }
1557
+ get shouldAutofocus() {
1558
+ return true;
1559
+ }
1501
1560
  get reloadReason() {
1502
1561
  return;
1503
1562
  }
@@ -1515,9 +1574,11 @@ class Renderer {
1515
1574
  await Bardo.preservingPermanentElements(this, this.permanentElementMap, callback);
1516
1575
  }
1517
1576
  focusFirstAutofocusableElement() {
1518
- const element = this.connectedSnapshot.firstAutofocusableElement;
1519
- if (element) {
1520
- element.focus();
1577
+ if (this.shouldAutofocus) {
1578
+ const element = this.connectedSnapshot.firstAutofocusableElement;
1579
+ if (element) {
1580
+ element.focus();
1581
+ }
1521
1582
  }
1522
1583
  }
1523
1584
  enteringBardo(currentPermanentElement) {
@@ -1625,10 +1686,675 @@ function readScrollBehavior(value, defaultValue) {
1625
1686
  }
1626
1687
  }
1627
1688
 
1628
- class ProgressBar {
1629
- static animationDuration=300;
1630
- static get defaultCSS() {
1631
- return unindent`
1689
+ var Idiomorph = function() {
1690
+ const noOp = () => {};
1691
+ const defaults = {
1692
+ morphStyle: "outerHTML",
1693
+ callbacks: {
1694
+ beforeNodeAdded: noOp,
1695
+ afterNodeAdded: noOp,
1696
+ beforeNodeMorphed: noOp,
1697
+ afterNodeMorphed: noOp,
1698
+ beforeNodeRemoved: noOp,
1699
+ afterNodeRemoved: noOp,
1700
+ beforeAttributeUpdated: noOp
1701
+ },
1702
+ head: {
1703
+ style: "merge",
1704
+ shouldPreserve: elt => elt.getAttribute("im-preserve") === "true",
1705
+ shouldReAppend: elt => elt.getAttribute("im-re-append") === "true",
1706
+ shouldRemove: noOp,
1707
+ afterHeadMorphed: noOp
1708
+ },
1709
+ restoreFocus: true
1710
+ };
1711
+ function morph(oldNode, newContent, config = {}) {
1712
+ oldNode = normalizeElement(oldNode);
1713
+ const newNode = normalizeParent(newContent);
1714
+ const ctx = createMorphContext(oldNode, newNode, config);
1715
+ const morphedNodes = saveAndRestoreFocus(ctx, (() => withHeadBlocking(ctx, oldNode, newNode, (ctx => {
1716
+ if (ctx.morphStyle === "innerHTML") {
1717
+ morphChildren(ctx, oldNode, newNode);
1718
+ return Array.from(oldNode.childNodes);
1719
+ } else {
1720
+ return morphOuterHTML(ctx, oldNode, newNode);
1721
+ }
1722
+ }))));
1723
+ ctx.pantry.remove();
1724
+ return morphedNodes;
1725
+ }
1726
+ function morphOuterHTML(ctx, oldNode, newNode) {
1727
+ const oldParent = normalizeParent(oldNode);
1728
+ let childNodes = Array.from(oldParent.childNodes);
1729
+ const index = childNodes.indexOf(oldNode);
1730
+ const rightMargin = childNodes.length - (index + 1);
1731
+ morphChildren(ctx, oldParent, newNode, oldNode, oldNode.nextSibling);
1732
+ childNodes = Array.from(oldParent.childNodes);
1733
+ return childNodes.slice(index, childNodes.length - rightMargin);
1734
+ }
1735
+ function saveAndRestoreFocus(ctx, fn) {
1736
+ if (!ctx.config.restoreFocus) return fn();
1737
+ let activeElement = document.activeElement;
1738
+ if (!(activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement)) {
1739
+ return fn();
1740
+ }
1741
+ const {id: activeElementId, selectionStart: selectionStart, selectionEnd: selectionEnd} = activeElement;
1742
+ const results = fn();
1743
+ if (activeElementId && activeElementId !== document.activeElement?.id) {
1744
+ activeElement = ctx.target.querySelector(`#${activeElementId}`);
1745
+ activeElement?.focus();
1746
+ }
1747
+ if (activeElement && !activeElement.selectionEnd && selectionEnd) {
1748
+ activeElement.setSelectionRange(selectionStart, selectionEnd);
1749
+ }
1750
+ return results;
1751
+ }
1752
+ const morphChildren = function() {
1753
+ function morphChildren(ctx, oldParent, newParent, insertionPoint = null, endPoint = null) {
1754
+ if (oldParent instanceof HTMLTemplateElement && newParent instanceof HTMLTemplateElement) {
1755
+ oldParent = oldParent.content;
1756
+ newParent = newParent.content;
1757
+ }
1758
+ insertionPoint ||= oldParent.firstChild;
1759
+ for (const newChild of newParent.childNodes) {
1760
+ if (insertionPoint && insertionPoint != endPoint) {
1761
+ const bestMatch = findBestMatch(ctx, newChild, insertionPoint, endPoint);
1762
+ if (bestMatch) {
1763
+ if (bestMatch !== insertionPoint) {
1764
+ removeNodesBetween(ctx, insertionPoint, bestMatch);
1765
+ }
1766
+ morphNode(bestMatch, newChild, ctx);
1767
+ insertionPoint = bestMatch.nextSibling;
1768
+ continue;
1769
+ }
1770
+ }
1771
+ if (newChild instanceof Element && ctx.persistentIds.has(newChild.id)) {
1772
+ const movedChild = moveBeforeById(oldParent, newChild.id, insertionPoint, ctx);
1773
+ morphNode(movedChild, newChild, ctx);
1774
+ insertionPoint = movedChild.nextSibling;
1775
+ continue;
1776
+ }
1777
+ const insertedNode = createNode(oldParent, newChild, insertionPoint, ctx);
1778
+ if (insertedNode) {
1779
+ insertionPoint = insertedNode.nextSibling;
1780
+ }
1781
+ }
1782
+ while (insertionPoint && insertionPoint != endPoint) {
1783
+ const tempNode = insertionPoint;
1784
+ insertionPoint = insertionPoint.nextSibling;
1785
+ removeNode(ctx, tempNode);
1786
+ }
1787
+ }
1788
+ function createNode(oldParent, newChild, insertionPoint, ctx) {
1789
+ if (ctx.callbacks.beforeNodeAdded(newChild) === false) return null;
1790
+ if (ctx.idMap.has(newChild)) {
1791
+ const newEmptyChild = document.createElement(newChild.tagName);
1792
+ oldParent.insertBefore(newEmptyChild, insertionPoint);
1793
+ morphNode(newEmptyChild, newChild, ctx);
1794
+ ctx.callbacks.afterNodeAdded(newEmptyChild);
1795
+ return newEmptyChild;
1796
+ } else {
1797
+ const newClonedChild = document.importNode(newChild, true);
1798
+ oldParent.insertBefore(newClonedChild, insertionPoint);
1799
+ ctx.callbacks.afterNodeAdded(newClonedChild);
1800
+ return newClonedChild;
1801
+ }
1802
+ }
1803
+ const findBestMatch = function() {
1804
+ function findBestMatch(ctx, node, startPoint, endPoint) {
1805
+ let softMatch = null;
1806
+ let nextSibling = node.nextSibling;
1807
+ let siblingSoftMatchCount = 0;
1808
+ let cursor = startPoint;
1809
+ while (cursor && cursor != endPoint) {
1810
+ if (isSoftMatch(cursor, node)) {
1811
+ if (isIdSetMatch(ctx, cursor, node)) {
1812
+ return cursor;
1813
+ }
1814
+ if (softMatch === null) {
1815
+ if (!ctx.idMap.has(cursor)) {
1816
+ softMatch = cursor;
1817
+ }
1818
+ }
1819
+ }
1820
+ if (softMatch === null && nextSibling && isSoftMatch(cursor, nextSibling)) {
1821
+ siblingSoftMatchCount++;
1822
+ nextSibling = nextSibling.nextSibling;
1823
+ if (siblingSoftMatchCount >= 2) {
1824
+ softMatch = undefined;
1825
+ }
1826
+ }
1827
+ if (cursor.contains(document.activeElement)) break;
1828
+ cursor = cursor.nextSibling;
1829
+ }
1830
+ return softMatch || null;
1831
+ }
1832
+ function isIdSetMatch(ctx, oldNode, newNode) {
1833
+ let oldSet = ctx.idMap.get(oldNode);
1834
+ let newSet = ctx.idMap.get(newNode);
1835
+ if (!newSet || !oldSet) return false;
1836
+ for (const id of oldSet) {
1837
+ if (newSet.has(id)) {
1838
+ return true;
1839
+ }
1840
+ }
1841
+ return false;
1842
+ }
1843
+ function isSoftMatch(oldNode, newNode) {
1844
+ const oldElt = oldNode;
1845
+ const newElt = newNode;
1846
+ return oldElt.nodeType === newElt.nodeType && oldElt.tagName === newElt.tagName && (!oldElt.id || oldElt.id === newElt.id);
1847
+ }
1848
+ return findBestMatch;
1849
+ }();
1850
+ function removeNode(ctx, node) {
1851
+ if (ctx.idMap.has(node)) {
1852
+ moveBefore(ctx.pantry, node, null);
1853
+ } else {
1854
+ if (ctx.callbacks.beforeNodeRemoved(node) === false) return;
1855
+ node.parentNode?.removeChild(node);
1856
+ ctx.callbacks.afterNodeRemoved(node);
1857
+ }
1858
+ }
1859
+ function removeNodesBetween(ctx, startInclusive, endExclusive) {
1860
+ let cursor = startInclusive;
1861
+ while (cursor && cursor !== endExclusive) {
1862
+ let tempNode = cursor;
1863
+ cursor = cursor.nextSibling;
1864
+ removeNode(ctx, tempNode);
1865
+ }
1866
+ return cursor;
1867
+ }
1868
+ function moveBeforeById(parentNode, id, after, ctx) {
1869
+ const target = ctx.target.querySelector(`#${id}`) || ctx.pantry.querySelector(`#${id}`);
1870
+ removeElementFromAncestorsIdMaps(target, ctx);
1871
+ moveBefore(parentNode, target, after);
1872
+ return target;
1873
+ }
1874
+ function removeElementFromAncestorsIdMaps(element, ctx) {
1875
+ const id = element.id;
1876
+ while (element = element.parentNode) {
1877
+ let idSet = ctx.idMap.get(element);
1878
+ if (idSet) {
1879
+ idSet.delete(id);
1880
+ if (!idSet.size) {
1881
+ ctx.idMap.delete(element);
1882
+ }
1883
+ }
1884
+ }
1885
+ }
1886
+ function moveBefore(parentNode, element, after) {
1887
+ if (parentNode.moveBefore) {
1888
+ try {
1889
+ parentNode.moveBefore(element, after);
1890
+ } catch (e) {
1891
+ parentNode.insertBefore(element, after);
1892
+ }
1893
+ } else {
1894
+ parentNode.insertBefore(element, after);
1895
+ }
1896
+ }
1897
+ return morphChildren;
1898
+ }();
1899
+ const morphNode = function() {
1900
+ function morphNode(oldNode, newContent, ctx) {
1901
+ if (ctx.ignoreActive && oldNode === document.activeElement) {
1902
+ return null;
1903
+ }
1904
+ if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) {
1905
+ return oldNode;
1906
+ }
1907
+ if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) ; else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") {
1908
+ handleHeadElement(oldNode, newContent, ctx);
1909
+ } else {
1910
+ morphAttributes(oldNode, newContent, ctx);
1911
+ if (!ignoreValueOfActiveElement(oldNode, ctx)) {
1912
+ morphChildren(ctx, oldNode, newContent);
1913
+ }
1914
+ }
1915
+ ctx.callbacks.afterNodeMorphed(oldNode, newContent);
1916
+ return oldNode;
1917
+ }
1918
+ function morphAttributes(oldNode, newNode, ctx) {
1919
+ let type = newNode.nodeType;
1920
+ if (type === 1) {
1921
+ const oldElt = oldNode;
1922
+ const newElt = newNode;
1923
+ const oldAttributes = oldElt.attributes;
1924
+ const newAttributes = newElt.attributes;
1925
+ for (const newAttribute of newAttributes) {
1926
+ if (ignoreAttribute(newAttribute.name, oldElt, "update", ctx)) {
1927
+ continue;
1928
+ }
1929
+ if (oldElt.getAttribute(newAttribute.name) !== newAttribute.value) {
1930
+ oldElt.setAttribute(newAttribute.name, newAttribute.value);
1931
+ }
1932
+ }
1933
+ for (let i = oldAttributes.length - 1; 0 <= i; i--) {
1934
+ const oldAttribute = oldAttributes[i];
1935
+ if (!oldAttribute) continue;
1936
+ if (!newElt.hasAttribute(oldAttribute.name)) {
1937
+ if (ignoreAttribute(oldAttribute.name, oldElt, "remove", ctx)) {
1938
+ continue;
1939
+ }
1940
+ oldElt.removeAttribute(oldAttribute.name);
1941
+ }
1942
+ }
1943
+ if (!ignoreValueOfActiveElement(oldElt, ctx)) {
1944
+ syncInputValue(oldElt, newElt, ctx);
1945
+ }
1946
+ }
1947
+ if (type === 8 || type === 3) {
1948
+ if (oldNode.nodeValue !== newNode.nodeValue) {
1949
+ oldNode.nodeValue = newNode.nodeValue;
1950
+ }
1951
+ }
1952
+ }
1953
+ function syncInputValue(oldElement, newElement, ctx) {
1954
+ if (oldElement instanceof HTMLInputElement && newElement instanceof HTMLInputElement && newElement.type !== "file") {
1955
+ let newValue = newElement.value;
1956
+ let oldValue = oldElement.value;
1957
+ syncBooleanAttribute(oldElement, newElement, "checked", ctx);
1958
+ syncBooleanAttribute(oldElement, newElement, "disabled", ctx);
1959
+ if (!newElement.hasAttribute("value")) {
1960
+ if (!ignoreAttribute("value", oldElement, "remove", ctx)) {
1961
+ oldElement.value = "";
1962
+ oldElement.removeAttribute("value");
1963
+ }
1964
+ } else if (oldValue !== newValue) {
1965
+ if (!ignoreAttribute("value", oldElement, "update", ctx)) {
1966
+ oldElement.setAttribute("value", newValue);
1967
+ oldElement.value = newValue;
1968
+ }
1969
+ }
1970
+ } else if (oldElement instanceof HTMLOptionElement && newElement instanceof HTMLOptionElement) {
1971
+ syncBooleanAttribute(oldElement, newElement, "selected", ctx);
1972
+ } else if (oldElement instanceof HTMLTextAreaElement && newElement instanceof HTMLTextAreaElement) {
1973
+ let newValue = newElement.value;
1974
+ let oldValue = oldElement.value;
1975
+ if (ignoreAttribute("value", oldElement, "update", ctx)) {
1976
+ return;
1977
+ }
1978
+ if (newValue !== oldValue) {
1979
+ oldElement.value = newValue;
1980
+ }
1981
+ if (oldElement.firstChild && oldElement.firstChild.nodeValue !== newValue) {
1982
+ oldElement.firstChild.nodeValue = newValue;
1983
+ }
1984
+ }
1985
+ }
1986
+ function syncBooleanAttribute(oldElement, newElement, attributeName, ctx) {
1987
+ const newLiveValue = newElement[attributeName], oldLiveValue = oldElement[attributeName];
1988
+ if (newLiveValue !== oldLiveValue) {
1989
+ const ignoreUpdate = ignoreAttribute(attributeName, oldElement, "update", ctx);
1990
+ if (!ignoreUpdate) {
1991
+ oldElement[attributeName] = newElement[attributeName];
1992
+ }
1993
+ if (newLiveValue) {
1994
+ if (!ignoreUpdate) {
1995
+ oldElement.setAttribute(attributeName, "");
1996
+ }
1997
+ } else {
1998
+ if (!ignoreAttribute(attributeName, oldElement, "remove", ctx)) {
1999
+ oldElement.removeAttribute(attributeName);
2000
+ }
2001
+ }
2002
+ }
2003
+ }
2004
+ function ignoreAttribute(attr, element, updateType, ctx) {
2005
+ if (attr === "value" && ctx.ignoreActiveValue && element === document.activeElement) {
2006
+ return true;
2007
+ }
2008
+ return ctx.callbacks.beforeAttributeUpdated(attr, element, updateType) === false;
2009
+ }
2010
+ function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
2011
+ return !!ctx.ignoreActiveValue && possibleActiveElement === document.activeElement && possibleActiveElement !== document.body;
2012
+ }
2013
+ return morphNode;
2014
+ }();
2015
+ function withHeadBlocking(ctx, oldNode, newNode, callback) {
2016
+ if (ctx.head.block) {
2017
+ const oldHead = oldNode.querySelector("head");
2018
+ const newHead = newNode.querySelector("head");
2019
+ if (oldHead && newHead) {
2020
+ const promises = handleHeadElement(oldHead, newHead, ctx);
2021
+ return Promise.all(promises).then((() => {
2022
+ const newCtx = Object.assign(ctx, {
2023
+ head: {
2024
+ block: false,
2025
+ ignore: true
2026
+ }
2027
+ });
2028
+ return callback(newCtx);
2029
+ }));
2030
+ }
2031
+ }
2032
+ return callback(ctx);
2033
+ }
2034
+ function handleHeadElement(oldHead, newHead, ctx) {
2035
+ let added = [];
2036
+ let removed = [];
2037
+ let preserved = [];
2038
+ let nodesToAppend = [];
2039
+ let srcToNewHeadNodes = new Map;
2040
+ for (const newHeadChild of newHead.children) {
2041
+ srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
2042
+ }
2043
+ for (const currentHeadElt of oldHead.children) {
2044
+ let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
2045
+ let isReAppended = ctx.head.shouldReAppend(currentHeadElt);
2046
+ let isPreserved = ctx.head.shouldPreserve(currentHeadElt);
2047
+ if (inNewContent || isPreserved) {
2048
+ if (isReAppended) {
2049
+ removed.push(currentHeadElt);
2050
+ } else {
2051
+ srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
2052
+ preserved.push(currentHeadElt);
2053
+ }
2054
+ } else {
2055
+ if (ctx.head.style === "append") {
2056
+ if (isReAppended) {
2057
+ removed.push(currentHeadElt);
2058
+ nodesToAppend.push(currentHeadElt);
2059
+ }
2060
+ } else {
2061
+ if (ctx.head.shouldRemove(currentHeadElt) !== false) {
2062
+ removed.push(currentHeadElt);
2063
+ }
2064
+ }
2065
+ }
2066
+ }
2067
+ nodesToAppend.push(...srcToNewHeadNodes.values());
2068
+ let promises = [];
2069
+ for (const newNode of nodesToAppend) {
2070
+ let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild;
2071
+ if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
2072
+ if ("href" in newElt && newElt.href || "src" in newElt && newElt.src) {
2073
+ let resolve;
2074
+ let promise = new Promise((function(_resolve) {
2075
+ resolve = _resolve;
2076
+ }));
2077
+ newElt.addEventListener("load", (function() {
2078
+ resolve();
2079
+ }));
2080
+ promises.push(promise);
2081
+ }
2082
+ oldHead.appendChild(newElt);
2083
+ ctx.callbacks.afterNodeAdded(newElt);
2084
+ added.push(newElt);
2085
+ }
2086
+ }
2087
+ for (const removedElement of removed) {
2088
+ if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
2089
+ oldHead.removeChild(removedElement);
2090
+ ctx.callbacks.afterNodeRemoved(removedElement);
2091
+ }
2092
+ }
2093
+ ctx.head.afterHeadMorphed(oldHead, {
2094
+ added: added,
2095
+ kept: preserved,
2096
+ removed: removed
2097
+ });
2098
+ return promises;
2099
+ }
2100
+ const createMorphContext = function() {
2101
+ function createMorphContext(oldNode, newContent, config) {
2102
+ const {persistentIds: persistentIds, idMap: idMap} = createIdMaps(oldNode, newContent);
2103
+ const mergedConfig = mergeDefaults(config);
2104
+ const morphStyle = mergedConfig.morphStyle || "outerHTML";
2105
+ if (![ "innerHTML", "outerHTML" ].includes(morphStyle)) {
2106
+ throw `Do not understand how to morph style ${morphStyle}`;
2107
+ }
2108
+ return {
2109
+ target: oldNode,
2110
+ newContent: newContent,
2111
+ config: mergedConfig,
2112
+ morphStyle: morphStyle,
2113
+ ignoreActive: mergedConfig.ignoreActive,
2114
+ ignoreActiveValue: mergedConfig.ignoreActiveValue,
2115
+ restoreFocus: mergedConfig.restoreFocus,
2116
+ idMap: idMap,
2117
+ persistentIds: persistentIds,
2118
+ pantry: createPantry(),
2119
+ callbacks: mergedConfig.callbacks,
2120
+ head: mergedConfig.head
2121
+ };
2122
+ }
2123
+ function mergeDefaults(config) {
2124
+ let finalConfig = Object.assign({}, defaults);
2125
+ Object.assign(finalConfig, config);
2126
+ finalConfig.callbacks = Object.assign({}, defaults.callbacks, config.callbacks);
2127
+ finalConfig.head = Object.assign({}, defaults.head, config.head);
2128
+ return finalConfig;
2129
+ }
2130
+ function createPantry() {
2131
+ const pantry = document.createElement("div");
2132
+ pantry.hidden = true;
2133
+ document.body.insertAdjacentElement("afterend", pantry);
2134
+ return pantry;
2135
+ }
2136
+ function findIdElements(root) {
2137
+ let elements = Array.from(root.querySelectorAll("[id]"));
2138
+ if (root.id) {
2139
+ elements.push(root);
2140
+ }
2141
+ return elements;
2142
+ }
2143
+ function populateIdMapWithTree(idMap, persistentIds, root, elements) {
2144
+ for (const elt of elements) {
2145
+ if (persistentIds.has(elt.id)) {
2146
+ let current = elt;
2147
+ while (current) {
2148
+ let idSet = idMap.get(current);
2149
+ if (idSet == null) {
2150
+ idSet = new Set;
2151
+ idMap.set(current, idSet);
2152
+ }
2153
+ idSet.add(elt.id);
2154
+ if (current === root) break;
2155
+ current = current.parentElement;
2156
+ }
2157
+ }
2158
+ }
2159
+ }
2160
+ function createIdMaps(oldContent, newContent) {
2161
+ const oldIdElements = findIdElements(oldContent);
2162
+ const newIdElements = findIdElements(newContent);
2163
+ const persistentIds = createPersistentIds(oldIdElements, newIdElements);
2164
+ let idMap = new Map;
2165
+ populateIdMapWithTree(idMap, persistentIds, oldContent, oldIdElements);
2166
+ const newRoot = newContent.__idiomorphRoot || newContent;
2167
+ populateIdMapWithTree(idMap, persistentIds, newRoot, newIdElements);
2168
+ return {
2169
+ persistentIds: persistentIds,
2170
+ idMap: idMap
2171
+ };
2172
+ }
2173
+ function createPersistentIds(oldIdElements, newIdElements) {
2174
+ let duplicateIds = new Set;
2175
+ let oldIdTagNameMap = new Map;
2176
+ for (const {id: id, tagName: tagName} of oldIdElements) {
2177
+ if (oldIdTagNameMap.has(id)) {
2178
+ duplicateIds.add(id);
2179
+ } else {
2180
+ oldIdTagNameMap.set(id, tagName);
2181
+ }
2182
+ }
2183
+ let persistentIds = new Set;
2184
+ for (const {id: id, tagName: tagName} of newIdElements) {
2185
+ if (persistentIds.has(id)) {
2186
+ duplicateIds.add(id);
2187
+ } else if (oldIdTagNameMap.get(id) === tagName) {
2188
+ persistentIds.add(id);
2189
+ }
2190
+ }
2191
+ for (const id of duplicateIds) {
2192
+ persistentIds.delete(id);
2193
+ }
2194
+ return persistentIds;
2195
+ }
2196
+ return createMorphContext;
2197
+ }();
2198
+ const {normalizeElement: normalizeElement, normalizeParent: normalizeParent} = function() {
2199
+ const generatedByIdiomorph = new WeakSet;
2200
+ function normalizeElement(content) {
2201
+ if (content instanceof Document) {
2202
+ return content.documentElement;
2203
+ } else {
2204
+ return content;
2205
+ }
2206
+ }
2207
+ function normalizeParent(newContent) {
2208
+ if (newContent == null) {
2209
+ return document.createElement("div");
2210
+ } else if (typeof newContent === "string") {
2211
+ return normalizeParent(parseContent(newContent));
2212
+ } else if (generatedByIdiomorph.has(newContent)) {
2213
+ return newContent;
2214
+ } else if (newContent instanceof Node) {
2215
+ if (newContent.parentNode) {
2216
+ return createDuckTypedParent(newContent);
2217
+ } else {
2218
+ const dummyParent = document.createElement("div");
2219
+ dummyParent.append(newContent);
2220
+ return dummyParent;
2221
+ }
2222
+ } else {
2223
+ const dummyParent = document.createElement("div");
2224
+ for (const elt of [ ...newContent ]) {
2225
+ dummyParent.append(elt);
2226
+ }
2227
+ return dummyParent;
2228
+ }
2229
+ }
2230
+ function createDuckTypedParent(newContent) {
2231
+ return {
2232
+ childNodes: [ newContent ],
2233
+ querySelectorAll: s => {
2234
+ const elements = newContent.querySelectorAll(s);
2235
+ return newContent.matches(s) ? [ newContent, ...elements ] : elements;
2236
+ },
2237
+ insertBefore: (n, r) => newContent.parentNode.insertBefore(n, r),
2238
+ moveBefore: (n, r) => newContent.parentNode.moveBefore(n, r),
2239
+ get __idiomorphRoot() {
2240
+ return newContent;
2241
+ }
2242
+ };
2243
+ }
2244
+ function parseContent(newContent) {
2245
+ let parser = new DOMParser;
2246
+ let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, "");
2247
+ if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) {
2248
+ let content = parser.parseFromString(newContent, "text/html");
2249
+ if (contentWithSvgsRemoved.match(/<\/html>/)) {
2250
+ generatedByIdiomorph.add(content);
2251
+ return content;
2252
+ } else {
2253
+ let htmlElement = content.firstChild;
2254
+ if (htmlElement) {
2255
+ generatedByIdiomorph.add(htmlElement);
2256
+ }
2257
+ return htmlElement;
2258
+ }
2259
+ } else {
2260
+ let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html");
2261
+ let content = responseDoc.body.querySelector("template").content;
2262
+ generatedByIdiomorph.add(content);
2263
+ return content;
2264
+ }
2265
+ }
2266
+ return {
2267
+ normalizeElement: normalizeElement,
2268
+ normalizeParent: normalizeParent
2269
+ };
2270
+ }();
2271
+ return {
2272
+ morph: morph,
2273
+ defaults: defaults
2274
+ };
2275
+ }();
2276
+
2277
+ function morphElements(currentElement, newElement, {callbacks: callbacks, ...options} = {}) {
2278
+ Idiomorph.morph(currentElement, newElement, {
2279
+ ...options,
2280
+ callbacks: new DefaultIdiomorphCallbacks(callbacks)
2281
+ });
2282
+ }
2283
+
2284
+ function morphChildren(currentElement, newElement) {
2285
+ morphElements(currentElement, newElement.childNodes, {
2286
+ morphStyle: "innerHTML"
2287
+ });
2288
+ }
2289
+
2290
+ class DefaultIdiomorphCallbacks {
2291
+ #beforeNodeMorphed;
2292
+ constructor({beforeNodeMorphed: beforeNodeMorphed} = {}) {
2293
+ this.#beforeNodeMorphed = beforeNodeMorphed || (() => true);
2294
+ }
2295
+ beforeNodeAdded=node => !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id));
2296
+ beforeNodeMorphed=(currentElement, newElement) => {
2297
+ if (currentElement instanceof Element) {
2298
+ if (!currentElement.hasAttribute("data-turbo-permanent") && this.#beforeNodeMorphed(currentElement, newElement)) {
2299
+ const event = dispatch("turbo:before-morph-element", {
2300
+ cancelable: true,
2301
+ target: currentElement,
2302
+ detail: {
2303
+ currentElement: currentElement,
2304
+ newElement: newElement
2305
+ }
2306
+ });
2307
+ return !event.defaultPrevented;
2308
+ } else {
2309
+ return false;
2310
+ }
2311
+ }
2312
+ };
2313
+ beforeAttributeUpdated=(attributeName, target, mutationType) => {
2314
+ const event = dispatch("turbo:before-morph-attribute", {
2315
+ cancelable: true,
2316
+ target: target,
2317
+ detail: {
2318
+ attributeName: attributeName,
2319
+ mutationType: mutationType
2320
+ }
2321
+ });
2322
+ return !event.defaultPrevented;
2323
+ };
2324
+ beforeNodeRemoved=node => this.beforeNodeMorphed(node);
2325
+ afterNodeMorphed=(currentElement, newElement) => {
2326
+ if (currentElement instanceof Element) {
2327
+ dispatch("turbo:morph-element", {
2328
+ target: currentElement,
2329
+ detail: {
2330
+ currentElement: currentElement,
2331
+ newElement: newElement
2332
+ }
2333
+ });
2334
+ }
2335
+ };
2336
+ }
2337
+
2338
+ class MorphingFrameRenderer extends FrameRenderer {
2339
+ static renderElement(currentElement, newElement) {
2340
+ dispatch("turbo:before-frame-morph", {
2341
+ target: currentElement,
2342
+ detail: {
2343
+ currentElement: currentElement,
2344
+ newElement: newElement
2345
+ }
2346
+ });
2347
+ morphChildren(currentElement, newElement);
2348
+ }
2349
+ async preservingPermanentElements(callback) {
2350
+ return await callback();
2351
+ }
2352
+ }
2353
+
2354
+ class ProgressBar {
2355
+ static animationDuration=300;
2356
+ static get defaultCSS() {
2357
+ return unindent`
1632
2358
  .turbo-progress-bar {
1633
2359
  position: fixed;
1634
2360
  display: block;
@@ -1714,8 +2440,9 @@ class ProgressBar {
1714
2440
  const element = document.createElement("style");
1715
2441
  element.type = "text/css";
1716
2442
  element.textContent = ProgressBar.defaultCSS;
1717
- if (this.cspNonce) {
1718
- element.nonce = this.cspNonce;
2443
+ const cspNonce = getCspNonce();
2444
+ if (cspNonce) {
2445
+ element.nonce = cspNonce;
1719
2446
  }
1720
2447
  return element;
1721
2448
  }
@@ -1724,9 +2451,6 @@ class ProgressBar {
1724
2451
  element.className = "turbo-progress-bar";
1725
2452
  return element;
1726
2453
  }
1727
- get cspNonce() {
1728
- return getMetaContent("csp-nonce");
1729
- }
1730
2454
  }
1731
2455
 
1732
2456
  class HeadSnapshot extends Snapshot {
@@ -2229,16 +2953,6 @@ class Visit {
2229
2953
  ...this.timingMetrics
2230
2954
  };
2231
2955
  }
2232
- getHistoryMethodForAction(action) {
2233
- switch (action) {
2234
- case "replace":
2235
- return history.replaceState;
2236
-
2237
- case "advance":
2238
- case "restore":
2239
- return history.pushState;
2240
- }
2241
- }
2242
2956
  hasPreloadedResponse() {
2243
2957
  return typeof this.response == "object";
2244
2958
  }
@@ -2259,7 +2973,9 @@ class Visit {
2259
2973
  }
2260
2974
  async render(callback) {
2261
2975
  this.cancelRender();
2262
- this.frame = await nextRepaint();
2976
+ await new Promise((resolve => {
2977
+ this.frame = document.visibilityState === "hidden" ? setTimeout((() => resolve()), 0) : requestAnimationFrame((() => resolve()));
2978
+ }));
2263
2979
  await callback();
2264
2980
  delete this.frame;
2265
2981
  }
@@ -2339,6 +3055,9 @@ class BrowserAdapter {
2339
3055
  this.hideVisitProgressBar();
2340
3056
  }
2341
3057
  visitRendered(_visit) {}
3058
+ linkPrefetchingIsEnabledForLocation(location) {
3059
+ return true;
3060
+ }
2342
3061
  formSubmissionStarted(_formSubmission) {
2343
3062
  this.progressBar.setValue(0);
2344
3063
  this.showFormProgressBarAfterDelay();
@@ -2631,7 +3350,7 @@ class LinkPrefetchObserver {
2631
3350
  this.#prefetchedLink = null;
2632
3351
  };
2633
3352
  #tryToUsePrefetchedRequest=event => {
2634
- if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "get") {
3353
+ if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "GET") {
2635
3354
  const cached = prefetchCache.get(event.detail.url.toString());
2636
3355
  if (cached) {
2637
3356
  event.detail.fetchRequest = cached;
@@ -2795,11 +3514,18 @@ class Navigator {
2795
3514
  this.adapter.formSubmissionFinished(formSubmission);
2796
3515
  }
2797
3516
  }
3517
+ linkPrefetchingIsEnabledForLocation(location) {
3518
+ if (typeof this.adapter.linkPrefetchingIsEnabledForLocation === "function") {
3519
+ return this.adapter.linkPrefetchingIsEnabledForLocation(location);
3520
+ }
3521
+ return true;
3522
+ }
2798
3523
  visitStarted(visit) {
2799
3524
  this.delegate.visitStarted(visit);
2800
3525
  }
2801
3526
  visitCompleted(visit) {
2802
3527
  this.delegate.visitCompleted(visit);
3528
+ delete this.currentVisit;
2803
3529
  }
2804
3530
  locationWithActionIsSamePage(location, action) {
2805
3531
  const anchor = getAnchor(location);
@@ -2816,819 +3542,279 @@ class Navigator {
2816
3542
  get restorationIdentifier() {
2817
3543
  return this.history.restorationIdentifier;
2818
3544
  }
2819
- #getActionForFormSubmission(formSubmission, fetchResponse) {
2820
- const {submitter: submitter, formElement: formElement} = formSubmission;
2821
- return getVisitAction(submitter, formElement) || this.#getDefaultAction(fetchResponse);
2822
- }
2823
- #getDefaultAction(fetchResponse) {
2824
- const sameLocationRedirect = fetchResponse.redirected && fetchResponse.location.href === this.location?.href;
2825
- return sameLocationRedirect ? "replace" : "advance";
2826
- }
2827
- }
2828
-
2829
- const PageStage = {
2830
- initial: 0,
2831
- loading: 1,
2832
- interactive: 2,
2833
- complete: 3
2834
- };
2835
-
2836
- class PageObserver {
2837
- stage=PageStage.initial;
2838
- started=false;
2839
- constructor(delegate) {
2840
- this.delegate = delegate;
2841
- }
2842
- start() {
2843
- if (!this.started) {
2844
- if (this.stage == PageStage.initial) {
2845
- this.stage = PageStage.loading;
2846
- }
2847
- document.addEventListener("readystatechange", this.interpretReadyState, false);
2848
- addEventListener("pagehide", this.pageWillUnload, false);
2849
- this.started = true;
2850
- }
2851
- }
2852
- stop() {
2853
- if (this.started) {
2854
- document.removeEventListener("readystatechange", this.interpretReadyState, false);
2855
- removeEventListener("pagehide", this.pageWillUnload, false);
2856
- this.started = false;
2857
- }
2858
- }
2859
- interpretReadyState=() => {
2860
- const {readyState: readyState} = this;
2861
- if (readyState == "interactive") {
2862
- this.pageIsInteractive();
2863
- } else if (readyState == "complete") {
2864
- this.pageIsComplete();
2865
- }
2866
- };
2867
- pageIsInteractive() {
2868
- if (this.stage == PageStage.loading) {
2869
- this.stage = PageStage.interactive;
2870
- this.delegate.pageBecameInteractive();
2871
- }
2872
- }
2873
- pageIsComplete() {
2874
- this.pageIsInteractive();
2875
- if (this.stage == PageStage.interactive) {
2876
- this.stage = PageStage.complete;
2877
- this.delegate.pageLoaded();
2878
- }
2879
- }
2880
- pageWillUnload=() => {
2881
- this.delegate.pageWillUnload();
2882
- };
2883
- get readyState() {
2884
- return document.readyState;
2885
- }
2886
- }
2887
-
2888
- class ScrollObserver {
2889
- started=false;
2890
- constructor(delegate) {
2891
- this.delegate = delegate;
2892
- }
2893
- start() {
2894
- if (!this.started) {
2895
- addEventListener("scroll", this.onScroll, false);
2896
- this.onScroll();
2897
- this.started = true;
2898
- }
2899
- }
2900
- stop() {
2901
- if (this.started) {
2902
- removeEventListener("scroll", this.onScroll, false);
2903
- this.started = false;
2904
- }
2905
- }
2906
- onScroll=() => {
2907
- this.updatePosition({
2908
- x: window.pageXOffset,
2909
- y: window.pageYOffset
2910
- });
2911
- };
2912
- updatePosition(position) {
2913
- this.delegate.scrollPositionChanged(position);
2914
- }
2915
- }
2916
-
2917
- class StreamMessageRenderer {
2918
- render({fragment: fragment}) {
2919
- Bardo.preservingPermanentElements(this, getPermanentElementMapForFragment(fragment), (() => {
2920
- withAutofocusFromFragment(fragment, (() => {
2921
- withPreservedFocus((() => {
2922
- document.documentElement.appendChild(fragment);
2923
- }));
2924
- }));
2925
- }));
2926
- }
2927
- enteringBardo(currentPermanentElement, newPermanentElement) {
2928
- newPermanentElement.replaceWith(currentPermanentElement.cloneNode(true));
2929
- }
2930
- leavingBardo() {}
2931
- }
2932
-
2933
- function getPermanentElementMapForFragment(fragment) {
2934
- const permanentElementsInDocument = queryPermanentElementsAll(document.documentElement);
2935
- const permanentElementMap = {};
2936
- for (const permanentElementInDocument of permanentElementsInDocument) {
2937
- const {id: id} = permanentElementInDocument;
2938
- for (const streamElement of fragment.querySelectorAll("turbo-stream")) {
2939
- const elementInStream = getPermanentElementById(streamElement.templateElement.content, id);
2940
- if (elementInStream) {
2941
- permanentElementMap[id] = [ permanentElementInDocument, elementInStream ];
2942
- }
2943
- }
2944
- }
2945
- return permanentElementMap;
2946
- }
2947
-
2948
- async function withAutofocusFromFragment(fragment, callback) {
2949
- const generatedID = `turbo-stream-autofocus-${uuid()}`;
2950
- const turboStreams = fragment.querySelectorAll("turbo-stream");
2951
- const elementWithAutofocus = firstAutofocusableElementInStreams(turboStreams);
2952
- let willAutofocusId = null;
2953
- if (elementWithAutofocus) {
2954
- if (elementWithAutofocus.id) {
2955
- willAutofocusId = elementWithAutofocus.id;
2956
- } else {
2957
- willAutofocusId = generatedID;
2958
- }
2959
- elementWithAutofocus.id = willAutofocusId;
2960
- }
2961
- callback();
2962
- await nextRepaint();
2963
- const hasNoActiveElement = document.activeElement == null || document.activeElement == document.body;
2964
- if (hasNoActiveElement && willAutofocusId) {
2965
- const elementToAutofocus = document.getElementById(willAutofocusId);
2966
- if (elementIsFocusable(elementToAutofocus)) {
2967
- elementToAutofocus.focus();
2968
- }
2969
- if (elementToAutofocus && elementToAutofocus.id == generatedID) {
2970
- elementToAutofocus.removeAttribute("id");
2971
- }
2972
- }
2973
- }
2974
-
2975
- async function withPreservedFocus(callback) {
2976
- const [activeElementBeforeRender, activeElementAfterRender] = await around(callback, (() => document.activeElement));
2977
- const restoreFocusTo = activeElementBeforeRender && activeElementBeforeRender.id;
2978
- if (restoreFocusTo) {
2979
- const elementToFocus = document.getElementById(restoreFocusTo);
2980
- if (elementIsFocusable(elementToFocus) && elementToFocus != activeElementAfterRender) {
2981
- elementToFocus.focus();
2982
- }
2983
- }
2984
- }
2985
-
2986
- function firstAutofocusableElementInStreams(nodeListOfStreamElements) {
2987
- for (const streamElement of nodeListOfStreamElements) {
2988
- const elementWithAutofocus = queryAutofocusableElement(streamElement.templateElement.content);
2989
- if (elementWithAutofocus) return elementWithAutofocus;
2990
- }
2991
- return null;
2992
- }
2993
-
2994
- class StreamObserver {
2995
- sources=new Set;
2996
- #started=false;
2997
- constructor(delegate) {
2998
- this.delegate = delegate;
2999
- }
3000
- start() {
3001
- if (!this.#started) {
3002
- this.#started = true;
3003
- addEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
3004
- }
3005
- }
3006
- stop() {
3007
- if (this.#started) {
3008
- this.#started = false;
3009
- removeEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
3010
- }
3011
- }
3012
- connectStreamSource(source) {
3013
- if (!this.streamSourceIsConnected(source)) {
3014
- this.sources.add(source);
3015
- source.addEventListener("message", this.receiveMessageEvent, false);
3016
- }
3017
- }
3018
- disconnectStreamSource(source) {
3019
- if (this.streamSourceIsConnected(source)) {
3020
- this.sources.delete(source);
3021
- source.removeEventListener("message", this.receiveMessageEvent, false);
3022
- }
3023
- }
3024
- streamSourceIsConnected(source) {
3025
- return this.sources.has(source);
3026
- }
3027
- inspectFetchResponse=event => {
3028
- const response = fetchResponseFromEvent(event);
3029
- if (response && fetchResponseIsStream(response)) {
3030
- event.preventDefault();
3031
- this.receiveMessageResponse(response);
3032
- }
3033
- };
3034
- receiveMessageEvent=event => {
3035
- if (this.#started && typeof event.data == "string") {
3036
- this.receiveMessageHTML(event.data);
3037
- }
3038
- };
3039
- async receiveMessageResponse(response) {
3040
- const html = await response.responseHTML;
3041
- if (html) {
3042
- this.receiveMessageHTML(html);
3043
- }
3044
- }
3045
- receiveMessageHTML(html) {
3046
- this.delegate.receivedMessageFromStream(StreamMessage.wrap(html));
3545
+ #getActionForFormSubmission(formSubmission, fetchResponse) {
3546
+ const {submitter: submitter, formElement: formElement} = formSubmission;
3547
+ return getVisitAction(submitter, formElement) || this.#getDefaultAction(fetchResponse);
3047
3548
  }
3048
- }
3049
-
3050
- function fetchResponseFromEvent(event) {
3051
- const fetchResponse = event.detail?.fetchResponse;
3052
- if (fetchResponse instanceof FetchResponse) {
3053
- return fetchResponse;
3549
+ #getDefaultAction(fetchResponse) {
3550
+ const sameLocationRedirect = fetchResponse.redirected && fetchResponse.location.href === this.location?.href;
3551
+ return sameLocationRedirect ? "replace" : "advance";
3054
3552
  }
3055
3553
  }
3056
3554
 
3057
- function fetchResponseIsStream(response) {
3058
- const contentType = response.contentType ?? "";
3059
- return contentType.startsWith(StreamMessage.contentType);
3060
- }
3555
+ const PageStage = {
3556
+ initial: 0,
3557
+ loading: 1,
3558
+ interactive: 2,
3559
+ complete: 3
3560
+ };
3061
3561
 
3062
- class ErrorRenderer extends Renderer {
3063
- static renderElement(currentElement, newElement) {
3064
- const {documentElement: documentElement, body: body} = document;
3065
- documentElement.replaceChild(newElement, body);
3066
- }
3067
- async render() {
3068
- this.replaceHeadAndBody();
3069
- this.activateScriptElements();
3070
- }
3071
- replaceHeadAndBody() {
3072
- const {documentElement: documentElement, head: head} = document;
3073
- documentElement.replaceChild(this.newHead, head);
3074
- this.renderElement(this.currentElement, this.newElement);
3562
+ class PageObserver {
3563
+ stage=PageStage.initial;
3564
+ started=false;
3565
+ constructor(delegate) {
3566
+ this.delegate = delegate;
3075
3567
  }
3076
- activateScriptElements() {
3077
- for (const replaceableElement of this.scriptElements) {
3078
- const parentNode = replaceableElement.parentNode;
3079
- if (parentNode) {
3080
- const element = activateScriptElement(replaceableElement);
3081
- parentNode.replaceChild(element, replaceableElement);
3568
+ start() {
3569
+ if (!this.started) {
3570
+ if (this.stage == PageStage.initial) {
3571
+ this.stage = PageStage.loading;
3082
3572
  }
3573
+ document.addEventListener("readystatechange", this.interpretReadyState, false);
3574
+ addEventListener("pagehide", this.pageWillUnload, false);
3575
+ this.started = true;
3083
3576
  }
3084
3577
  }
3085
- get newHead() {
3086
- return this.newSnapshot.headSnapshot.element;
3087
- }
3088
- get scriptElements() {
3089
- return document.documentElement.querySelectorAll("script");
3578
+ stop() {
3579
+ if (this.started) {
3580
+ document.removeEventListener("readystatechange", this.interpretReadyState, false);
3581
+ removeEventListener("pagehide", this.pageWillUnload, false);
3582
+ this.started = false;
3583
+ }
3090
3584
  }
3091
- }
3092
-
3093
- var Idiomorph = function() {
3094
- let EMPTY_SET = new Set;
3095
- let defaults = {
3096
- morphStyle: "outerHTML",
3097
- callbacks: {
3098
- beforeNodeAdded: noOp,
3099
- afterNodeAdded: noOp,
3100
- beforeNodeMorphed: noOp,
3101
- afterNodeMorphed: noOp,
3102
- beforeNodeRemoved: noOp,
3103
- afterNodeRemoved: noOp,
3104
- beforeAttributeUpdated: noOp
3105
- },
3106
- head: {
3107
- style: "merge",
3108
- shouldPreserve: function(elt) {
3109
- return elt.getAttribute("im-preserve") === "true";
3110
- },
3111
- shouldReAppend: function(elt) {
3112
- return elt.getAttribute("im-re-append") === "true";
3113
- },
3114
- shouldRemove: noOp,
3115
- afterHeadMorphed: noOp
3585
+ interpretReadyState=() => {
3586
+ const {readyState: readyState} = this;
3587
+ if (readyState == "interactive") {
3588
+ this.pageIsInteractive();
3589
+ } else if (readyState == "complete") {
3590
+ this.pageIsComplete();
3116
3591
  }
3117
3592
  };
3118
- function morph(oldNode, newContent, config = {}) {
3119
- if (oldNode instanceof Document) {
3120
- oldNode = oldNode.documentElement;
3121
- }
3122
- if (typeof newContent === "string") {
3123
- newContent = parseContent(newContent);
3593
+ pageIsInteractive() {
3594
+ if (this.stage == PageStage.loading) {
3595
+ this.stage = PageStage.interactive;
3596
+ this.delegate.pageBecameInteractive();
3124
3597
  }
3125
- let normalizedContent = normalizeContent(newContent);
3126
- let ctx = createMorphContext(oldNode, normalizedContent, config);
3127
- return morphNormalizedContent(oldNode, normalizedContent, ctx);
3128
3598
  }
3129
- function morphNormalizedContent(oldNode, normalizedNewContent, ctx) {
3130
- if (ctx.head.block) {
3131
- let oldHead = oldNode.querySelector("head");
3132
- let newHead = normalizedNewContent.querySelector("head");
3133
- if (oldHead && newHead) {
3134
- let promises = handleHeadElement(newHead, oldHead, ctx);
3135
- Promise.all(promises).then((function() {
3136
- morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, {
3137
- head: {
3138
- block: false,
3139
- ignore: true
3140
- }
3141
- }));
3142
- }));
3143
- return;
3144
- }
3145
- }
3146
- if (ctx.morphStyle === "innerHTML") {
3147
- morphChildren(normalizedNewContent, oldNode, ctx);
3148
- return oldNode.children;
3149
- } else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) {
3150
- let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx);
3151
- let previousSibling = bestMatch?.previousSibling;
3152
- let nextSibling = bestMatch?.nextSibling;
3153
- let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx);
3154
- if (bestMatch) {
3155
- return insertSiblings(previousSibling, morphedNode, nextSibling);
3156
- } else {
3157
- return [];
3158
- }
3159
- } else {
3160
- throw "Do not understand how to morph style " + ctx.morphStyle;
3599
+ pageIsComplete() {
3600
+ this.pageIsInteractive();
3601
+ if (this.stage == PageStage.interactive) {
3602
+ this.stage = PageStage.complete;
3603
+ this.delegate.pageLoaded();
3161
3604
  }
3162
3605
  }
3163
- function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
3164
- return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement && possibleActiveElement !== document.body;
3606
+ pageWillUnload=() => {
3607
+ this.delegate.pageWillUnload();
3608
+ };
3609
+ get readyState() {
3610
+ return document.readyState;
3165
3611
  }
3166
- function morphOldNodeTo(oldNode, newContent, ctx) {
3167
- if (ctx.ignoreActive && oldNode === document.activeElement) ; else if (newContent == null) {
3168
- if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
3169
- oldNode.remove();
3170
- ctx.callbacks.afterNodeRemoved(oldNode);
3171
- return null;
3172
- } else if (!isSoftMatch(oldNode, newContent)) {
3173
- if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
3174
- if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode;
3175
- oldNode.parentElement.replaceChild(newContent, oldNode);
3176
- ctx.callbacks.afterNodeAdded(newContent);
3177
- ctx.callbacks.afterNodeRemoved(oldNode);
3178
- return newContent;
3179
- } else {
3180
- if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return oldNode;
3181
- if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) ; else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") {
3182
- handleHeadElement(newContent, oldNode, ctx);
3183
- } else {
3184
- syncNodeFrom(newContent, oldNode, ctx);
3185
- if (!ignoreValueOfActiveElement(oldNode, ctx)) {
3186
- morphChildren(newContent, oldNode, ctx);
3187
- }
3188
- }
3189
- ctx.callbacks.afterNodeMorphed(oldNode, newContent);
3190
- return oldNode;
3191
- }
3612
+ }
3613
+
3614
+ class ScrollObserver {
3615
+ started=false;
3616
+ constructor(delegate) {
3617
+ this.delegate = delegate;
3192
3618
  }
3193
- function morphChildren(newParent, oldParent, ctx) {
3194
- let nextNewChild = newParent.firstChild;
3195
- let insertionPoint = oldParent.firstChild;
3196
- let newChild;
3197
- while (nextNewChild) {
3198
- newChild = nextNewChild;
3199
- nextNewChild = newChild.nextSibling;
3200
- if (insertionPoint == null) {
3201
- if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
3202
- oldParent.appendChild(newChild);
3203
- ctx.callbacks.afterNodeAdded(newChild);
3204
- removeIdsFromConsideration(ctx, newChild);
3205
- continue;
3206
- }
3207
- if (isIdSetMatch(newChild, insertionPoint, ctx)) {
3208
- morphOldNodeTo(insertionPoint, newChild, ctx);
3209
- insertionPoint = insertionPoint.nextSibling;
3210
- removeIdsFromConsideration(ctx, newChild);
3211
- continue;
3212
- }
3213
- let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx);
3214
- if (idSetMatch) {
3215
- insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx);
3216
- morphOldNodeTo(idSetMatch, newChild, ctx);
3217
- removeIdsFromConsideration(ctx, newChild);
3218
- continue;
3219
- }
3220
- let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx);
3221
- if (softMatch) {
3222
- insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx);
3223
- morphOldNodeTo(softMatch, newChild, ctx);
3224
- removeIdsFromConsideration(ctx, newChild);
3225
- continue;
3226
- }
3227
- if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
3228
- oldParent.insertBefore(newChild, insertionPoint);
3229
- ctx.callbacks.afterNodeAdded(newChild);
3230
- removeIdsFromConsideration(ctx, newChild);
3231
- }
3232
- while (insertionPoint !== null) {
3233
- let tempNode = insertionPoint;
3234
- insertionPoint = insertionPoint.nextSibling;
3235
- removeNode(tempNode, ctx);
3619
+ start() {
3620
+ if (!this.started) {
3621
+ addEventListener("scroll", this.onScroll, false);
3622
+ this.onScroll();
3623
+ this.started = true;
3236
3624
  }
3237
3625
  }
3238
- function ignoreAttribute(attr, to, updateType, ctx) {
3239
- if (attr === "value" && ctx.ignoreActiveValue && to === document.activeElement) {
3240
- return true;
3626
+ stop() {
3627
+ if (this.started) {
3628
+ removeEventListener("scroll", this.onScroll, false);
3629
+ this.started = false;
3241
3630
  }
3242
- return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false;
3243
3631
  }
3244
- function syncNodeFrom(from, to, ctx) {
3245
- let type = from.nodeType;
3246
- if (type === 1) {
3247
- const fromAttributes = from.attributes;
3248
- const toAttributes = to.attributes;
3249
- for (const fromAttribute of fromAttributes) {
3250
- if (ignoreAttribute(fromAttribute.name, to, "update", ctx)) {
3251
- continue;
3252
- }
3253
- if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) {
3254
- to.setAttribute(fromAttribute.name, fromAttribute.value);
3255
- }
3256
- }
3257
- for (let i = toAttributes.length - 1; 0 <= i; i--) {
3258
- const toAttribute = toAttributes[i];
3259
- if (ignoreAttribute(toAttribute.name, to, "remove", ctx)) {
3260
- continue;
3261
- }
3262
- if (!from.hasAttribute(toAttribute.name)) {
3263
- to.removeAttribute(toAttribute.name);
3264
- }
3265
- }
3266
- }
3267
- if (type === 8 || type === 3) {
3268
- if (to.nodeValue !== from.nodeValue) {
3269
- to.nodeValue = from.nodeValue;
3270
- }
3271
- }
3272
- if (!ignoreValueOfActiveElement(to, ctx)) {
3273
- syncInputValue(from, to, ctx);
3274
- }
3632
+ onScroll=() => {
3633
+ this.updatePosition({
3634
+ x: window.pageXOffset,
3635
+ y: window.pageYOffset
3636
+ });
3637
+ };
3638
+ updatePosition(position) {
3639
+ this.delegate.scrollPositionChanged(position);
3275
3640
  }
3276
- function syncBooleanAttribute(from, to, attributeName, ctx) {
3277
- if (from[attributeName] !== to[attributeName]) {
3278
- let ignoreUpdate = ignoreAttribute(attributeName, to, "update", ctx);
3279
- if (!ignoreUpdate) {
3280
- to[attributeName] = from[attributeName];
3281
- }
3282
- if (from[attributeName]) {
3283
- if (!ignoreUpdate) {
3284
- to.setAttribute(attributeName, from[attributeName]);
3285
- }
3286
- } else {
3287
- if (!ignoreAttribute(attributeName, to, "remove", ctx)) {
3288
- to.removeAttribute(attributeName);
3289
- }
3290
- }
3291
- }
3641
+ }
3642
+
3643
+ class StreamMessageRenderer {
3644
+ render({fragment: fragment}) {
3645
+ Bardo.preservingPermanentElements(this, getPermanentElementMapForFragment(fragment), (() => {
3646
+ withAutofocusFromFragment(fragment, (() => {
3647
+ withPreservedFocus((() => {
3648
+ document.documentElement.appendChild(fragment);
3649
+ }));
3650
+ }));
3651
+ }));
3292
3652
  }
3293
- function syncInputValue(from, to, ctx) {
3294
- if (from instanceof HTMLInputElement && to instanceof HTMLInputElement && from.type !== "file") {
3295
- let fromValue = from.value;
3296
- let toValue = to.value;
3297
- syncBooleanAttribute(from, to, "checked", ctx);
3298
- syncBooleanAttribute(from, to, "disabled", ctx);
3299
- if (!from.hasAttribute("value")) {
3300
- if (!ignoreAttribute("value", to, "remove", ctx)) {
3301
- to.value = "";
3302
- to.removeAttribute("value");
3303
- }
3304
- } else if (fromValue !== toValue) {
3305
- if (!ignoreAttribute("value", to, "update", ctx)) {
3306
- to.setAttribute("value", fromValue);
3307
- to.value = fromValue;
3308
- }
3309
- }
3310
- } else if (from instanceof HTMLOptionElement) {
3311
- syncBooleanAttribute(from, to, "selected", ctx);
3312
- } else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) {
3313
- let fromValue = from.value;
3314
- let toValue = to.value;
3315
- if (ignoreAttribute("value", to, "update", ctx)) {
3316
- return;
3317
- }
3318
- if (fromValue !== toValue) {
3319
- to.value = fromValue;
3320
- }
3321
- if (to.firstChild && to.firstChild.nodeValue !== fromValue) {
3322
- to.firstChild.nodeValue = fromValue;
3323
- }
3324
- }
3653
+ enteringBardo(currentPermanentElement, newPermanentElement) {
3654
+ newPermanentElement.replaceWith(currentPermanentElement.cloneNode(true));
3325
3655
  }
3326
- function handleHeadElement(newHeadTag, currentHead, ctx) {
3327
- let added = [];
3328
- let removed = [];
3329
- let preserved = [];
3330
- let nodesToAppend = [];
3331
- let headMergeStyle = ctx.head.style;
3332
- let srcToNewHeadNodes = new Map;
3333
- for (const newHeadChild of newHeadTag.children) {
3334
- srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
3335
- }
3336
- for (const currentHeadElt of currentHead.children) {
3337
- let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
3338
- let isReAppended = ctx.head.shouldReAppend(currentHeadElt);
3339
- let isPreserved = ctx.head.shouldPreserve(currentHeadElt);
3340
- if (inNewContent || isPreserved) {
3341
- if (isReAppended) {
3342
- removed.push(currentHeadElt);
3343
- } else {
3344
- srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
3345
- preserved.push(currentHeadElt);
3346
- }
3347
- } else {
3348
- if (headMergeStyle === "append") {
3349
- if (isReAppended) {
3350
- removed.push(currentHeadElt);
3351
- nodesToAppend.push(currentHeadElt);
3352
- }
3353
- } else {
3354
- if (ctx.head.shouldRemove(currentHeadElt) !== false) {
3355
- removed.push(currentHeadElt);
3356
- }
3357
- }
3358
- }
3359
- }
3360
- nodesToAppend.push(...srcToNewHeadNodes.values());
3361
- let promises = [];
3362
- for (const newNode of nodesToAppend) {
3363
- let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild;
3364
- if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
3365
- if (newElt.href || newElt.src) {
3366
- let resolve = null;
3367
- let promise = new Promise((function(_resolve) {
3368
- resolve = _resolve;
3369
- }));
3370
- newElt.addEventListener("load", (function() {
3371
- resolve();
3372
- }));
3373
- promises.push(promise);
3374
- }
3375
- currentHead.appendChild(newElt);
3376
- ctx.callbacks.afterNodeAdded(newElt);
3377
- added.push(newElt);
3378
- }
3379
- }
3380
- for (const removedElement of removed) {
3381
- if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
3382
- currentHead.removeChild(removedElement);
3383
- ctx.callbacks.afterNodeRemoved(removedElement);
3656
+ leavingBardo() {}
3657
+ }
3658
+
3659
+ function getPermanentElementMapForFragment(fragment) {
3660
+ const permanentElementsInDocument = queryPermanentElementsAll(document.documentElement);
3661
+ const permanentElementMap = {};
3662
+ for (const permanentElementInDocument of permanentElementsInDocument) {
3663
+ const {id: id} = permanentElementInDocument;
3664
+ for (const streamElement of fragment.querySelectorAll("turbo-stream")) {
3665
+ const elementInStream = getPermanentElementById(streamElement.templateElement.content, id);
3666
+ if (elementInStream) {
3667
+ permanentElementMap[id] = [ permanentElementInDocument, elementInStream ];
3384
3668
  }
3385
3669
  }
3386
- ctx.head.afterHeadMorphed(currentHead, {
3387
- added: added,
3388
- kept: preserved,
3389
- removed: removed
3390
- });
3391
- return promises;
3392
3670
  }
3393
- function noOp() {}
3394
- function mergeDefaults(config) {
3395
- let finalConfig = {};
3396
- Object.assign(finalConfig, defaults);
3397
- Object.assign(finalConfig, config);
3398
- finalConfig.callbacks = {};
3399
- Object.assign(finalConfig.callbacks, defaults.callbacks);
3400
- Object.assign(finalConfig.callbacks, config.callbacks);
3401
- finalConfig.head = {};
3402
- Object.assign(finalConfig.head, defaults.head);
3403
- Object.assign(finalConfig.head, config.head);
3404
- return finalConfig;
3405
- }
3406
- function createMorphContext(oldNode, newContent, config) {
3407
- config = mergeDefaults(config);
3408
- return {
3409
- target: oldNode,
3410
- newContent: newContent,
3411
- config: config,
3412
- morphStyle: config.morphStyle,
3413
- ignoreActive: config.ignoreActive,
3414
- ignoreActiveValue: config.ignoreActiveValue,
3415
- idMap: createIdMap(oldNode, newContent),
3416
- deadIds: new Set,
3417
- callbacks: config.callbacks,
3418
- head: config.head
3419
- };
3671
+ return permanentElementMap;
3672
+ }
3673
+
3674
+ async function withAutofocusFromFragment(fragment, callback) {
3675
+ const generatedID = `turbo-stream-autofocus-${uuid()}`;
3676
+ const turboStreams = fragment.querySelectorAll("turbo-stream");
3677
+ const elementWithAutofocus = firstAutofocusableElementInStreams(turboStreams);
3678
+ let willAutofocusId = null;
3679
+ if (elementWithAutofocus) {
3680
+ if (elementWithAutofocus.id) {
3681
+ willAutofocusId = elementWithAutofocus.id;
3682
+ } else {
3683
+ willAutofocusId = generatedID;
3684
+ }
3685
+ elementWithAutofocus.id = willAutofocusId;
3420
3686
  }
3421
- function isIdSetMatch(node1, node2, ctx) {
3422
- if (node1 == null || node2 == null) {
3423
- return false;
3687
+ callback();
3688
+ await nextRepaint();
3689
+ const hasNoActiveElement = document.activeElement == null || document.activeElement == document.body;
3690
+ if (hasNoActiveElement && willAutofocusId) {
3691
+ const elementToAutofocus = document.getElementById(willAutofocusId);
3692
+ if (elementIsFocusable(elementToAutofocus)) {
3693
+ elementToAutofocus.focus();
3424
3694
  }
3425
- if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) {
3426
- if (node1.id !== "" && node1.id === node2.id) {
3427
- return true;
3428
- } else {
3429
- return getIdIntersectionCount(ctx, node1, node2) > 0;
3430
- }
3695
+ if (elementToAutofocus && elementToAutofocus.id == generatedID) {
3696
+ elementToAutofocus.removeAttribute("id");
3431
3697
  }
3432
- return false;
3433
3698
  }
3434
- function isSoftMatch(node1, node2) {
3435
- if (node1 == null || node2 == null) {
3436
- return false;
3699
+ }
3700
+
3701
+ async function withPreservedFocus(callback) {
3702
+ const [activeElementBeforeRender, activeElementAfterRender] = await around(callback, (() => document.activeElement));
3703
+ const restoreFocusTo = activeElementBeforeRender && activeElementBeforeRender.id;
3704
+ if (restoreFocusTo) {
3705
+ const elementToFocus = document.getElementById(restoreFocusTo);
3706
+ if (elementIsFocusable(elementToFocus) && elementToFocus != activeElementAfterRender) {
3707
+ elementToFocus.focus();
3437
3708
  }
3438
- return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName;
3439
- }
3440
- function removeNodesBetween(startInclusive, endExclusive, ctx) {
3441
- while (startInclusive !== endExclusive) {
3442
- let tempNode = startInclusive;
3443
- startInclusive = startInclusive.nextSibling;
3444
- removeNode(tempNode, ctx);
3445
- }
3446
- removeIdsFromConsideration(ctx, endExclusive);
3447
- return endExclusive.nextSibling;
3448
- }
3449
- function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
3450
- let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent);
3451
- let potentialMatch = null;
3452
- if (newChildPotentialIdCount > 0) {
3453
- let potentialMatch = insertionPoint;
3454
- let otherMatchCount = 0;
3455
- while (potentialMatch != null) {
3456
- if (isIdSetMatch(newChild, potentialMatch, ctx)) {
3457
- return potentialMatch;
3458
- }
3459
- otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent);
3460
- if (otherMatchCount > newChildPotentialIdCount) {
3461
- return null;
3462
- }
3463
- potentialMatch = potentialMatch.nextSibling;
3464
- }
3709
+ }
3710
+ }
3711
+
3712
+ function firstAutofocusableElementInStreams(nodeListOfStreamElements) {
3713
+ for (const streamElement of nodeListOfStreamElements) {
3714
+ const elementWithAutofocus = queryAutofocusableElement(streamElement.templateElement.content);
3715
+ if (elementWithAutofocus) return elementWithAutofocus;
3716
+ }
3717
+ return null;
3718
+ }
3719
+
3720
+ class StreamObserver {
3721
+ sources=new Set;
3722
+ #started=false;
3723
+ constructor(delegate) {
3724
+ this.delegate = delegate;
3725
+ }
3726
+ start() {
3727
+ if (!this.#started) {
3728
+ this.#started = true;
3729
+ addEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
3465
3730
  }
3466
- return potentialMatch;
3467
3731
  }
3468
- function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
3469
- let potentialSoftMatch = insertionPoint;
3470
- let nextSibling = newChild.nextSibling;
3471
- let siblingSoftMatchCount = 0;
3472
- while (potentialSoftMatch != null) {
3473
- if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) {
3474
- return null;
3475
- }
3476
- if (isSoftMatch(newChild, potentialSoftMatch)) {
3477
- return potentialSoftMatch;
3478
- }
3479
- if (isSoftMatch(nextSibling, potentialSoftMatch)) {
3480
- siblingSoftMatchCount++;
3481
- nextSibling = nextSibling.nextSibling;
3482
- if (siblingSoftMatchCount >= 2) {
3483
- return null;
3484
- }
3485
- }
3486
- potentialSoftMatch = potentialSoftMatch.nextSibling;
3732
+ stop() {
3733
+ if (this.#started) {
3734
+ this.#started = false;
3735
+ removeEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
3487
3736
  }
3488
- return potentialSoftMatch;
3489
3737
  }
3490
- function parseContent(newContent) {
3491
- let parser = new DOMParser;
3492
- let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, "");
3493
- if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) {
3494
- let content = parser.parseFromString(newContent, "text/html");
3495
- if (contentWithSvgsRemoved.match(/<\/html>/)) {
3496
- content.generatedByIdiomorph = true;
3497
- return content;
3498
- } else {
3499
- let htmlElement = content.firstChild;
3500
- if (htmlElement) {
3501
- htmlElement.generatedByIdiomorph = true;
3502
- return htmlElement;
3503
- } else {
3504
- return null;
3505
- }
3506
- }
3507
- } else {
3508
- let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html");
3509
- let content = responseDoc.body.querySelector("template").content;
3510
- content.generatedByIdiomorph = true;
3511
- return content;
3512
- }
3513
- }
3514
- function normalizeContent(newContent) {
3515
- if (newContent == null) {
3516
- const dummyParent = document.createElement("div");
3517
- return dummyParent;
3518
- } else if (newContent.generatedByIdiomorph) {
3519
- return newContent;
3520
- } else if (newContent instanceof Node) {
3521
- const dummyParent = document.createElement("div");
3522
- dummyParent.append(newContent);
3523
- return dummyParent;
3524
- } else {
3525
- const dummyParent = document.createElement("div");
3526
- for (const elt of [ ...newContent ]) {
3527
- dummyParent.append(elt);
3528
- }
3529
- return dummyParent;
3738
+ connectStreamSource(source) {
3739
+ if (!this.streamSourceIsConnected(source)) {
3740
+ this.sources.add(source);
3741
+ source.addEventListener("message", this.receiveMessageEvent, false);
3530
3742
  }
3531
3743
  }
3532
- function insertSiblings(previousSibling, morphedNode, nextSibling) {
3533
- let stack = [];
3534
- let added = [];
3535
- while (previousSibling != null) {
3536
- stack.push(previousSibling);
3537
- previousSibling = previousSibling.previousSibling;
3538
- }
3539
- while (stack.length > 0) {
3540
- let node = stack.pop();
3541
- added.push(node);
3542
- morphedNode.parentElement.insertBefore(node, morphedNode);
3543
- }
3544
- added.push(morphedNode);
3545
- while (nextSibling != null) {
3546
- stack.push(nextSibling);
3547
- added.push(nextSibling);
3548
- nextSibling = nextSibling.nextSibling;
3549
- }
3550
- while (stack.length > 0) {
3551
- morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling);
3552
- }
3553
- return added;
3554
- }
3555
- function findBestNodeMatch(newContent, oldNode, ctx) {
3556
- let currentElement;
3557
- currentElement = newContent.firstChild;
3558
- let bestElement = currentElement;
3559
- let score = 0;
3560
- while (currentElement) {
3561
- let newScore = scoreElement(currentElement, oldNode, ctx);
3562
- if (newScore > score) {
3563
- bestElement = currentElement;
3564
- score = newScore;
3565
- }
3566
- currentElement = currentElement.nextSibling;
3744
+ disconnectStreamSource(source) {
3745
+ if (this.streamSourceIsConnected(source)) {
3746
+ this.sources.delete(source);
3747
+ source.removeEventListener("message", this.receiveMessageEvent, false);
3567
3748
  }
3568
- return bestElement;
3569
3749
  }
3570
- function scoreElement(node1, node2, ctx) {
3571
- if (isSoftMatch(node1, node2)) {
3572
- return .5 + getIdIntersectionCount(ctx, node1, node2);
3750
+ streamSourceIsConnected(source) {
3751
+ return this.sources.has(source);
3752
+ }
3753
+ inspectFetchResponse=event => {
3754
+ const response = fetchResponseFromEvent(event);
3755
+ if (response && fetchResponseIsStream(response)) {
3756
+ event.preventDefault();
3757
+ this.receiveMessageResponse(response);
3573
3758
  }
3574
- return 0;
3759
+ };
3760
+ receiveMessageEvent=event => {
3761
+ if (this.#started && typeof event.data == "string") {
3762
+ this.receiveMessageHTML(event.data);
3763
+ }
3764
+ };
3765
+ async receiveMessageResponse(response) {
3766
+ const html = await response.responseHTML;
3767
+ if (html) {
3768
+ this.receiveMessageHTML(html);
3769
+ }
3770
+ }
3771
+ receiveMessageHTML(html) {
3772
+ this.delegate.receivedMessageFromStream(StreamMessage.wrap(html));
3575
3773
  }
3576
- function removeNode(tempNode, ctx) {
3577
- removeIdsFromConsideration(ctx, tempNode);
3578
- if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return;
3579
- tempNode.remove();
3580
- ctx.callbacks.afterNodeRemoved(tempNode);
3774
+ }
3775
+
3776
+ function fetchResponseFromEvent(event) {
3777
+ const fetchResponse = event.detail?.fetchResponse;
3778
+ if (fetchResponse instanceof FetchResponse) {
3779
+ return fetchResponse;
3581
3780
  }
3582
- function isIdInConsideration(ctx, id) {
3583
- return !ctx.deadIds.has(id);
3781
+ }
3782
+
3783
+ function fetchResponseIsStream(response) {
3784
+ const contentType = response.contentType ?? "";
3785
+ return contentType.startsWith(StreamMessage.contentType);
3786
+ }
3787
+
3788
+ class ErrorRenderer extends Renderer {
3789
+ static renderElement(currentElement, newElement) {
3790
+ const {documentElement: documentElement, body: body} = document;
3791
+ documentElement.replaceChild(newElement, body);
3584
3792
  }
3585
- function idIsWithinNode(ctx, id, targetNode) {
3586
- let idSet = ctx.idMap.get(targetNode) || EMPTY_SET;
3587
- return idSet.has(id);
3793
+ async render() {
3794
+ this.replaceHeadAndBody();
3795
+ this.activateScriptElements();
3588
3796
  }
3589
- function removeIdsFromConsideration(ctx, node) {
3590
- let idSet = ctx.idMap.get(node) || EMPTY_SET;
3591
- for (const id of idSet) {
3592
- ctx.deadIds.add(id);
3593
- }
3797
+ replaceHeadAndBody() {
3798
+ const {documentElement: documentElement, head: head} = document;
3799
+ documentElement.replaceChild(this.newHead, head);
3800
+ this.renderElement(this.currentElement, this.newElement);
3594
3801
  }
3595
- function getIdIntersectionCount(ctx, node1, node2) {
3596
- let sourceSet = ctx.idMap.get(node1) || EMPTY_SET;
3597
- let matchCount = 0;
3598
- for (const id of sourceSet) {
3599
- if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) {
3600
- ++matchCount;
3601
- }
3602
- }
3603
- return matchCount;
3604
- }
3605
- function populateIdMapForNode(node, idMap) {
3606
- let nodeParent = node.parentElement;
3607
- let idElements = node.querySelectorAll("[id]");
3608
- for (const elt of idElements) {
3609
- let current = elt;
3610
- while (current !== nodeParent && current != null) {
3611
- let idSet = idMap.get(current);
3612
- if (idSet == null) {
3613
- idSet = new Set;
3614
- idMap.set(current, idSet);
3615
- }
3616
- idSet.add(elt.id);
3617
- current = current.parentElement;
3802
+ activateScriptElements() {
3803
+ for (const replaceableElement of this.scriptElements) {
3804
+ const parentNode = replaceableElement.parentNode;
3805
+ if (parentNode) {
3806
+ const element = activateScriptElement(replaceableElement);
3807
+ parentNode.replaceChild(element, replaceableElement);
3618
3808
  }
3619
3809
  }
3620
3810
  }
3621
- function createIdMap(oldContent, newContent) {
3622
- let idMap = new Map;
3623
- populateIdMapForNode(oldContent, idMap);
3624
- populateIdMapForNode(newContent, idMap);
3625
- return idMap;
3811
+ get newHead() {
3812
+ return this.newSnapshot.headSnapshot.element;
3626
3813
  }
3627
- return {
3628
- morph: morph,
3629
- defaults: defaults
3630
- };
3631
- }();
3814
+ get scriptElements() {
3815
+ return document.documentElement.querySelectorAll("script");
3816
+ }
3817
+ }
3632
3818
 
3633
3819
  class PageRenderer extends Renderer {
3634
3820
  static renderElement(currentElement, newElement) {
@@ -3798,109 +3984,38 @@ class PageRenderer extends Renderer {
3798
3984
  }
3799
3985
  }
3800
3986
 
3801
- class MorphRenderer extends PageRenderer {
3802
- async render() {
3803
- if (this.willRender) await this.#morphBody();
3804
- }
3805
- get renderMethod() {
3806
- return "morph";
3807
- }
3808
- async #morphBody() {
3809
- this.#morphElements(this.currentElement, this.newElement);
3810
- this.#reloadRemoteFrames();
3811
- dispatch("turbo:morph", {
3812
- detail: {
3813
- currentElement: this.currentElement,
3814
- newElement: this.newElement
3815
- }
3816
- });
3817
- }
3818
- #morphElements(currentElement, newElement, morphStyle = "outerHTML") {
3819
- this.isMorphingTurboFrame = this.#isFrameReloadedWithMorph(currentElement);
3820
- Idiomorph.morph(currentElement, newElement, {
3821
- ignoreActiveValue: true,
3822
- morphStyle: morphStyle,
3987
+ class MorphingPageRenderer extends PageRenderer {
3988
+ static renderElement(currentElement, newElement) {
3989
+ morphElements(currentElement, newElement, {
3823
3990
  callbacks: {
3824
- beforeNodeAdded: this.#shouldAddElement,
3825
- beforeNodeMorphed: this.#shouldMorphElement,
3826
- beforeAttributeUpdated: this.#shouldUpdateAttribute,
3827
- beforeNodeRemoved: this.#shouldRemoveElement,
3828
- afterNodeMorphed: this.#didMorphElement
3829
- }
3830
- });
3831
- }
3832
- #shouldAddElement=node => !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id));
3833
- #shouldMorphElement=(oldNode, newNode) => {
3834
- if (oldNode instanceof HTMLElement) {
3835
- if (!oldNode.hasAttribute("data-turbo-permanent") && (this.isMorphingTurboFrame || !this.#isFrameReloadedWithMorph(oldNode))) {
3836
- const event = dispatch("turbo:before-morph-element", {
3837
- cancelable: true,
3838
- target: oldNode,
3839
- detail: {
3840
- newElement: newNode
3841
- }
3842
- });
3843
- return !event.defaultPrevented;
3844
- } else {
3845
- return false;
3846
- }
3847
- }
3848
- };
3849
- #shouldUpdateAttribute=(attributeName, target, mutationType) => {
3850
- const event = dispatch("turbo:before-morph-attribute", {
3851
- cancelable: true,
3852
- target: target,
3853
- detail: {
3854
- attributeName: attributeName,
3855
- mutationType: mutationType
3991
+ beforeNodeMorphed: element => !canRefreshFrame(element)
3856
3992
  }
3857
3993
  });
3858
- return !event.defaultPrevented;
3859
- };
3860
- #didMorphElement=(oldNode, newNode) => {
3861
- if (newNode instanceof HTMLElement) {
3862
- dispatch("turbo:morph-element", {
3863
- target: oldNode,
3864
- detail: {
3865
- newElement: newNode
3866
- }
3867
- });
3994
+ for (const frame of currentElement.querySelectorAll("turbo-frame")) {
3995
+ if (canRefreshFrame(frame)) frame.reload();
3868
3996
  }
3869
- };
3870
- #shouldRemoveElement=node => this.#shouldMorphElement(node);
3871
- #reloadRemoteFrames() {
3872
- this.#remoteFrames().forEach((frame => {
3873
- if (this.#isFrameReloadedWithMorph(frame)) {
3874
- this.#renderFrameWithMorph(frame);
3875
- frame.reload();
3876
- }
3877
- }));
3878
- }
3879
- #renderFrameWithMorph(frame) {
3880
- frame.addEventListener("turbo:before-frame-render", (event => {
3881
- event.detail.render = this.#morphFrameUpdate;
3882
- }), {
3883
- once: true
3884
- });
3885
- }
3886
- #morphFrameUpdate=(currentElement, newElement) => {
3887
- dispatch("turbo:before-frame-morph", {
3888
- target: currentElement,
3997
+ dispatch("turbo:morph", {
3889
3998
  detail: {
3890
3999
  currentElement: currentElement,
3891
4000
  newElement: newElement
3892
4001
  }
3893
4002
  });
3894
- this.#morphElements(currentElement, newElement.children, "innerHTML");
3895
- };
3896
- #isFrameReloadedWithMorph(element) {
3897
- return element.src && element.refresh === "morph";
3898
4003
  }
3899
- #remoteFrames() {
3900
- return Array.from(document.querySelectorAll("turbo-frame[src]")).filter((frame => !frame.closest("[data-turbo-permanent]")));
4004
+ async preservingPermanentElements(callback) {
4005
+ return await callback();
4006
+ }
4007
+ get renderMethod() {
4008
+ return "morph";
4009
+ }
4010
+ get shouldAutofocus() {
4011
+ return false;
3901
4012
  }
3902
4013
  }
3903
4014
 
4015
+ function canRefreshFrame(frame) {
4016
+ return frame instanceof FrameElement && frame.src && frame.refresh === "morph" && !frame.closest("[data-turbo-permanent]");
4017
+ }
4018
+
3904
4019
  class SnapshotCache {
3905
4020
  keys=[];
3906
4021
  snapshots={};
@@ -3954,8 +4069,8 @@ class PageView extends View {
3954
4069
  }
3955
4070
  renderPage(snapshot, isPreview = false, willRender = true, visit) {
3956
4071
  const shouldMorphPage = this.isPageRefresh(visit) && this.snapshot.shouldMorphPage;
3957
- const rendererClass = shouldMorphPage ? MorphRenderer : PageRenderer;
3958
- const renderer = new rendererClass(this.snapshot, snapshot, PageRenderer.renderElement, isPreview, willRender);
4072
+ const rendererClass = shouldMorphPage ? MorphingPageRenderer : PageRenderer;
4073
+ const renderer = new rendererClass(this.snapshot, snapshot, isPreview, willRender);
3959
4074
  if (!renderer.shouldRender) {
3960
4075
  this.forceReloaded = true;
3961
4076
  } else {
@@ -3965,7 +4080,7 @@ class PageView extends View {
3965
4080
  }
3966
4081
  renderError(snapshot, visit) {
3967
4082
  visit?.changeHistory();
3968
- const renderer = new ErrorRenderer(this.snapshot, snapshot, ErrorRenderer.renderElement, false);
4083
+ const renderer = new ErrorRenderer(this.snapshot, snapshot, false);
3969
4084
  return this.render(renderer);
3970
4085
  }
3971
4086
  clearSnapshotCache() {
@@ -4083,11 +4198,8 @@ class Session {
4083
4198
  frameRedirector=new FrameRedirector(this, document.documentElement);
4084
4199
  streamMessageRenderer=new StreamMessageRenderer;
4085
4200
  cache=new Cache(this);
4086
- drive=true;
4087
4201
  enabled=true;
4088
- progressBarDelay=500;
4089
4202
  started=false;
4090
- formMode="on";
4091
4203
  #pageRefreshDebouncePeriod=150;
4092
4204
  constructor(recentRequests) {
4093
4205
  this.recentRequests = recentRequests;
@@ -4146,10 +4258,11 @@ class Session {
4146
4258
  }
4147
4259
  refresh(url, requestId) {
4148
4260
  const isRecentRequest = requestId && this.recentRequests.has(requestId);
4149
- if (!isRecentRequest) {
4150
- this.cache.exemptPageFromPreview();
4261
+ const isCurrentUrl = url === document.baseURI;
4262
+ if (!isRecentRequest && !this.navigator.currentVisit && isCurrentUrl) {
4151
4263
  this.visit(url, {
4152
- action: "replace"
4264
+ action: "replace",
4265
+ shouldCacheSnapshot: false
4153
4266
  });
4154
4267
  }
4155
4268
  }
@@ -4166,10 +4279,26 @@ class Session {
4166
4279
  this.view.clearSnapshotCache();
4167
4280
  }
4168
4281
  setProgressBarDelay(delay) {
4282
+ 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.`");
4169
4283
  this.progressBarDelay = delay;
4170
4284
  }
4171
- setFormMode(mode) {
4172
- this.formMode = mode;
4285
+ set progressBarDelay(delay) {
4286
+ config.drive.progressBarDelay = delay;
4287
+ }
4288
+ get progressBarDelay() {
4289
+ return config.drive.progressBarDelay;
4290
+ }
4291
+ set drive(value) {
4292
+ config.drive.enabled = value;
4293
+ }
4294
+ get drive() {
4295
+ return config.drive.enabled;
4296
+ }
4297
+ set formMode(value) {
4298
+ config.forms.mode = value;
4299
+ }
4300
+ get formMode() {
4301
+ return config.forms.mode;
4173
4302
  }
4174
4303
  get location() {
4175
4304
  return this.history.location;
@@ -4219,7 +4348,7 @@ class Session {
4219
4348
  }
4220
4349
  submittedFormLinkToLocation() {}
4221
4350
  canPrefetchRequestToLocation(link, location) {
4222
- return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation);
4351
+ return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation) && this.navigator.linkPrefetchingIsEnabledForLocation(location);
4223
4352
  }
4224
4353
  willFollowLinkToLocation(link, location, event) {
4225
4354
  return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation) && this.applicationAllowsFollowingLinkToLocation(link, location, event);
@@ -4391,11 +4520,11 @@ class Session {
4391
4520
  });
4392
4521
  }
4393
4522
  submissionIsNavigatable(form, submitter) {
4394
- if (this.formMode == "off") {
4523
+ if (config.forms.mode == "off") {
4395
4524
  return false;
4396
4525
  } else {
4397
4526
  const submitterIsNavigatable = submitter ? this.elementIsNavigatable(submitter) : true;
4398
- if (this.formMode == "optin") {
4527
+ if (config.forms.mode == "optin") {
4399
4528
  return submitterIsNavigatable && form.closest('[data-turbo="true"]') != null;
4400
4529
  } else {
4401
4530
  return submitterIsNavigatable && this.elementIsNavigatable(form);
@@ -4405,7 +4534,7 @@ class Session {
4405
4534
  elementIsNavigatable(element) {
4406
4535
  const container = findClosestRecursively(element, "[data-turbo]");
4407
4536
  const withinFrame = findClosestRecursively(element, "turbo-frame");
4408
- if (this.drive || withinFrame) {
4537
+ if (config.drive.enabled || withinFrame) {
4409
4538
  if (container) {
4410
4539
  return container.getAttribute("data-turbo") != "false";
4411
4540
  } else {
@@ -4473,15 +4602,18 @@ function clearCache() {
4473
4602
  }
4474
4603
 
4475
4604
  function setProgressBarDelay(delay) {
4476
- session.setProgressBarDelay(delay);
4605
+ 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.`");
4606
+ config.drive.progressBarDelay = delay;
4477
4607
  }
4478
4608
 
4479
4609
  function setConfirmMethod(confirmMethod) {
4480
- FormSubmission.confirmMethod = confirmMethod;
4610
+ 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.`");
4611
+ config.forms.confirm = confirmMethod;
4481
4612
  }
4482
4613
 
4483
4614
  function setFormMode(mode) {
4484
- session.setFormMode(mode);
4615
+ 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.`");
4616
+ config.forms.mode = mode;
4485
4617
  }
4486
4618
 
4487
4619
  var Turbo = Object.freeze({
@@ -4493,6 +4625,7 @@ var Turbo = Object.freeze({
4493
4625
  PageSnapshot: PageSnapshot,
4494
4626
  FrameRenderer: FrameRenderer,
4495
4627
  fetch: fetchWithTurboHeaders,
4628
+ config: config,
4496
4629
  start: start,
4497
4630
  registerAdapter: registerAdapter,
4498
4631
  visit: visit,
@@ -4514,6 +4647,7 @@ class FrameController {
4514
4647
  #connected=false;
4515
4648
  #hasBeenLoaded=false;
4516
4649
  #ignoredAttributes=new Set;
4650
+ #shouldMorphFrame=false;
4517
4651
  action=null;
4518
4652
  constructor(element) {
4519
4653
  this.element = element;
@@ -4561,18 +4695,13 @@ class FrameController {
4561
4695
  }
4562
4696
  }
4563
4697
  sourceURLReloaded() {
4564
- const {src: src} = this.element;
4565
- this.#ignoringChangesToAttribute("complete", (() => {
4566
- this.element.removeAttribute("complete");
4567
- }));
4698
+ const {refresh: refresh, src: src} = this.element;
4699
+ this.#shouldMorphFrame = src && refresh === "morph";
4700
+ this.element.removeAttribute("complete");
4568
4701
  this.element.src = null;
4569
4702
  this.element.src = src;
4570
4703
  return this.element.loaded;
4571
4704
  }
4572
- completeChanged() {
4573
- if (this.#isIgnoringChangesTo("complete")) return;
4574
- this.#loadSourceURL();
4575
- }
4576
4705
  loadingStyleChanged() {
4577
4706
  if (this.loadingStyle == FrameLoadingStyle.lazy) {
4578
4707
  this.appearanceObserver.start();
@@ -4605,6 +4734,7 @@ class FrameController {
4605
4734
  }
4606
4735
  }
4607
4736
  } finally {
4737
+ this.#shouldMorphFrame = false;
4608
4738
  this.fetchResponseLoaded = () => Promise.resolve();
4609
4739
  }
4610
4740
  }
@@ -4717,9 +4847,10 @@ class FrameController {
4717
4847
  };
4718
4848
  async #loadFrameResponse(fetchResponse, document) {
4719
4849
  const newFrameElement = await this.extractForeignFrameElement(document.body);
4850
+ const rendererClass = this.#shouldMorphFrame ? MorphingFrameRenderer : FrameRenderer;
4720
4851
  if (newFrameElement) {
4721
4852
  const snapshot = new Snapshot(newFrameElement);
4722
- const renderer = new FrameRenderer(this, this.view.snapshot, snapshot, FrameRenderer.renderElement, false, false);
4853
+ const renderer = new rendererClass(this, this.view.snapshot, snapshot, false, false);
4723
4854
  if (this.view.renderPromise) await this.view.renderPromise;
4724
4855
  this.changeHistory();
4725
4856
  await this.view.render(renderer);
@@ -4904,13 +5035,11 @@ class FrameController {
4904
5035
  return this.element.hasAttribute("complete");
4905
5036
  }
4906
5037
  set complete(value) {
4907
- this.#ignoringChangesToAttribute("complete", (() => {
4908
- if (value) {
4909
- this.element.setAttribute("complete", "");
4910
- } else {
4911
- this.element.removeAttribute("complete");
4912
- }
4913
- }));
5038
+ if (value) {
5039
+ this.element.setAttribute("complete", "");
5040
+ } else {
5041
+ this.element.removeAttribute("complete");
5042
+ }
4914
5043
  }
4915
5044
  get isActive() {
4916
5045
  return this.element.isActive && this.#connected;
@@ -4980,12 +5109,24 @@ const StreamActions = {
4980
5109
  this.targetElements.forEach((e => e.remove()));
4981
5110
  },
4982
5111
  replace() {
4983
- this.targetElements.forEach((e => e.replaceWith(this.templateContent)));
5112
+ const method = this.getAttribute("method");
5113
+ this.targetElements.forEach((targetElement => {
5114
+ if (method === "morph") {
5115
+ morphElements(targetElement, this.templateContent);
5116
+ } else {
5117
+ targetElement.replaceWith(this.templateContent);
5118
+ }
5119
+ }));
4984
5120
  },
4985
5121
  update() {
5122
+ const method = this.getAttribute("method");
4986
5123
  this.targetElements.forEach((targetElement => {
4987
- targetElement.innerHTML = "";
4988
- targetElement.append(this.templateContent);
5124
+ if (method === "morph") {
5125
+ morphChildren(targetElement, this.templateContent);
5126
+ } else {
5127
+ targetElement.innerHTML = "";
5128
+ targetElement.append(this.templateContent);
5129
+ }
4989
5130
  }));
4990
5131
  },
4991
5132
  refresh() {
@@ -5024,9 +5165,9 @@ class StreamElement extends HTMLElement {
5024
5165
  this.duplicateChildren.forEach((c => c.remove()));
5025
5166
  }
5026
5167
  get duplicateChildren() {
5027
- const existingChildren = this.targetElements.flatMap((e => [ ...e.children ])).filter((c => !!c.id));
5028
- const newChildrenIds = [ ...this.templateContent?.children || [] ].filter((c => !!c.id)).map((c => c.id));
5029
- return existingChildren.filter((c => newChildrenIds.includes(c.id)));
5168
+ const existingChildren = this.targetElements.flatMap((e => [ ...e.children ])).filter((c => !!c.getAttribute("id")));
5169
+ const newChildrenIds = [ ...this.templateContent?.children || [] ].filter((c => !!c.getAttribute("id"))).map((c => c.getAttribute("id")));
5170
+ return existingChildren.filter((c => newChildrenIds.includes(c.getAttribute("id"))));
5030
5171
  }
5031
5172
  get performAction() {
5032
5173
  if (this.action) {
@@ -5182,6 +5323,7 @@ var Turbo$1 = Object.freeze({
5182
5323
  StreamSourceElement: StreamSourceElement,
5183
5324
  cache: cache,
5184
5325
  clearCache: clearCache,
5326
+ config: config,
5185
5327
  connectStreamSource: connectStreamSource,
5186
5328
  disconnectStreamSource: disconnectStreamSource,
5187
5329
  fetch: fetchWithTurboHeaders,
@@ -5243,6 +5385,7 @@ function walk(obj) {
5243
5385
  }
5244
5386
 
5245
5387
  class TurboCableStreamSourceElement extends HTMLElement {
5388
+ static observedAttributes=[ "channel", "signed-stream-name" ];
5246
5389
  async connectedCallback() {
5247
5390
  connectStreamSource(this);
5248
5391
  this.subscription = await subscribeTo(this.channel, {
@@ -5254,6 +5397,13 @@ class TurboCableStreamSourceElement extends HTMLElement {
5254
5397
  disconnectedCallback() {
5255
5398
  disconnectStreamSource(this);
5256
5399
  if (this.subscription) this.subscription.unsubscribe();
5400
+ this.subscriptionDisconnected();
5401
+ }
5402
+ attributeChangedCallback() {
5403
+ if (this.subscription) {
5404
+ this.disconnectedCallback();
5405
+ this.connectedCallback();
5406
+ }
5257
5407
  }
5258
5408
  dispatchMessageEvent(data) {
5259
5409
  const event = new MessageEvent("message", {
@@ -5340,8 +5490,8 @@ window.Turbo = Turbo$1;
5340
5490
  addEventListener("turbo:before-fetch-request", encodeMethodIntoRequestBody);
5341
5491
 
5342
5492
  var adapters = {
5343
- logger: self.console,
5344
- WebSocket: self.WebSocket
5493
+ logger: typeof console !== "undefined" ? console : undefined,
5494
+ WebSocket: typeof WebSocket !== "undefined" ? WebSocket : undefined
5345
5495
  };
5346
5496
 
5347
5497
  var logger = {
@@ -5383,12 +5533,11 @@ class ConnectionMonitor {
5383
5533
  isRunning() {
5384
5534
  return this.startedAt && !this.stoppedAt;
5385
5535
  }
5386
- recordPing() {
5536
+ recordMessage() {
5387
5537
  this.pingedAt = now();
5388
5538
  }
5389
5539
  recordConnect() {
5390
5540
  this.reconnectAttempts = 0;
5391
- this.recordPing();
5392
5541
  delete this.disconnectedAt;
5393
5542
  logger.log("ConnectionMonitor recorded connect");
5394
5543
  }
@@ -5587,6 +5736,7 @@ Connection.prototype.events = {
5587
5736
  return;
5588
5737
  }
5589
5738
  const {identifier: identifier, message: message, reason: reason, reconnect: reconnect, type: type} = JSON.parse(event.data);
5739
+ this.monitor.recordMessage();
5590
5740
  switch (type) {
5591
5741
  case message_types.welcome:
5592
5742
  if (this.triedToReconnect()) {
@@ -5602,7 +5752,7 @@ Connection.prototype.events = {
5602
5752
  });
5603
5753
 
5604
5754
  case message_types.ping:
5605
- return this.monitor.recordPing();
5755
+ return null;
5606
5756
 
5607
5757
  case message_types.confirmation:
5608
5758
  this.subscriptions.confirmSubscription(identifier);