uchi 0.1.8 → 0.2.0

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/uchi/application.js +384 -317
  3. data/app/assets/stylesheets/uchi/application.css +8 -0
  4. data/app/assets/tailwind/uchi.css +5 -0
  5. data/app/components/uchi/field/base/show.html.erb +1 -0
  6. data/app/components/uchi/field/base.rb +24 -27
  7. data/app/components/uchi/field/boolean/index.html.erb +9 -7
  8. data/app/components/uchi/field/boolean/show.html.erb +9 -7
  9. data/app/components/uchi/field/file/show.html.erb +1 -1
  10. data/app/components/uchi/field/file.rb +1 -1
  11. data/app/components/uchi/field/has_and_belongs_to_many.rb +19 -1
  12. data/app/components/uchi/field/has_many.rb +20 -0
  13. data/app/components/uchi/field/id/show.html.erb +1 -1
  14. data/app/components/uchi/field/image/index.html.erb +1 -1
  15. data/app/components/uchi/field/image/show.html.erb +2 -2
  16. data/app/components/uchi/field/image.rb +1 -1
  17. data/app/components/uchi/field/select/edit.html.erb +1 -0
  18. data/app/components/uchi/field/select/index.html.erb +1 -0
  19. data/app/components/uchi/field/select/show.html.erb +1 -0
  20. data/app/components/uchi/field/select.rb +144 -0
  21. data/app/components/uchi/field/text.rb +1 -1
  22. data/app/components/uchi/ui/index/records_table/records_table.html.erb +1 -1
  23. data/app/controllers/uchi/repository_controller.rb +2 -2
  24. data/app/views/layouts/uchi/_javascript.html.erb +1 -1
  25. data/app/views/layouts/uchi/application.html.erb +4 -6
  26. data/app/views/uchi/has_many/associated_records/index.html.erb +6 -4
  27. data/app/views/uchi/repository/edit.html.erb +1 -1
  28. data/app/views/uchi/repository/new.html.erb +1 -1
  29. data/app/views/uchi/repository/show.html.erb +4 -4
  30. data/lib/uchi/field/configuration.rb +42 -5
  31. data/lib/uchi/repository.rb +19 -7
  32. data/lib/uchi/sort_order.rb +9 -4
  33. data/lib/uchi/version.rb +1 -1
  34. metadata +6 -5
  35. data/app/components/uchi/field/date/show.html.erb +0 -1
  36. data/app/components/uchi/field/number/show.html.erb +0 -1
  37. data/app/components/uchi/field/string/show.html.erb +0 -1
  38. data/app/components/uchi/field/text/show.html.erb +0 -1
@@ -611,7 +611,6 @@
611
611
  StreamElement: () => StreamElement,
612
612
  StreamSourceElement: () => StreamSourceElement,
613
613
  cache: () => cache,
614
- clearCache: () => clearCache,
615
614
  config: () => config,
616
615
  connectStreamSource: () => connectStreamSource,
617
616
  disconnectStreamSource: () => disconnectStreamSource,
@@ -619,7 +618,11 @@
619
618
  fetchEnctypeFromString: () => fetchEnctypeFromString,
620
619
  fetchMethodFromString: () => fetchMethodFromString,
621
620
  isSafe: () => isSafe,
622
- navigator: () => navigator$1,
621
+ morphBodyElements: () => morphBodyElements,
622
+ morphChildren: () => morphChildren,
623
+ morphElements: () => morphElements,
624
+ morphTurboFrameElements: () => morphTurboFrameElements,
625
+ navigator: () => sessionNavigator,
623
626
  registerAdapter: () => registerAdapter,
624
627
  renderStreamMessage: () => renderStreamMessage,
625
628
  session: () => session,
@@ -629,62 +632,6 @@
629
632
  start: () => start,
630
633
  visit: () => visit
631
634
  });
632
- (function(prototype) {
633
- if (typeof prototype.requestSubmit == "function") return;
634
- prototype.requestSubmit = function(submitter2) {
635
- if (submitter2) {
636
- validateSubmitter(submitter2, this);
637
- submitter2.click();
638
- } else {
639
- submitter2 = document.createElement("input");
640
- submitter2.type = "submit";
641
- submitter2.hidden = true;
642
- this.appendChild(submitter2);
643
- submitter2.click();
644
- this.removeChild(submitter2);
645
- }
646
- };
647
- function validateSubmitter(submitter2, form) {
648
- submitter2 instanceof HTMLElement || raise(TypeError, "parameter 1 is not of type 'HTMLElement'");
649
- submitter2.type == "submit" || raise(TypeError, "The specified element is not a submit button");
650
- submitter2.form == form || raise(DOMException, "The specified element is not owned by this form element", "NotFoundError");
651
- }
652
- function raise(errorConstructor, message, name) {
653
- throw new errorConstructor("Failed to execute 'requestSubmit' on 'HTMLFormElement': " + message + ".", name);
654
- }
655
- })(HTMLFormElement.prototype);
656
- var submittersByForm = /* @__PURE__ */ new WeakMap();
657
- function findSubmitterFromClickTarget(target) {
658
- const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
659
- const candidate = element ? element.closest("input, button") : null;
660
- return candidate?.type == "submit" ? candidate : null;
661
- }
662
- function clickCaptured(event) {
663
- const submitter2 = findSubmitterFromClickTarget(event.target);
664
- if (submitter2 && submitter2.form) {
665
- submittersByForm.set(submitter2.form, submitter2);
666
- }
667
- }
668
- (function() {
669
- if ("submitter" in Event.prototype) return;
670
- let prototype = window.Event.prototype;
671
- if ("SubmitEvent" in window) {
672
- const prototypeOfSubmitEvent = window.SubmitEvent.prototype;
673
- if (/Apple Computer/.test(navigator.vendor) && !("submitter" in prototypeOfSubmitEvent)) {
674
- prototype = prototypeOfSubmitEvent;
675
- } else {
676
- return;
677
- }
678
- }
679
- addEventListener("click", clickCaptured, true);
680
- Object.defineProperty(prototype, "submitter", {
681
- get() {
682
- if (this.type == "submit" && this.target instanceof HTMLFormElement) {
683
- return submittersByForm.get(this.target);
684
- }
685
- }
686
- });
687
- })();
688
635
  var FrameLoadingStyle = {
689
636
  eager: "eager",
690
637
  lazy: "lazy"
@@ -957,9 +904,6 @@
957
904
  function nextEventLoopTick() {
958
905
  return new Promise((resolve) => setTimeout(() => resolve(), 0));
959
906
  }
960
- function nextMicrotask() {
961
- return Promise.resolve();
962
- }
963
907
  function parseHTMLDocument(html = "") {
964
908
  return new DOMParser().parseFromString(html, "text/html");
965
909
  }
@@ -984,7 +928,7 @@
984
928
  } else if (i == 19) {
985
929
  return (Math.floor(Math.random() * 4) + 8).toString(16);
986
930
  } else {
987
- return Math.floor(Math.random() * 15).toString(16);
931
+ return Math.floor(Math.random() * 16).toString(16);
988
932
  }
989
933
  }).join("");
990
934
  }
@@ -1097,10 +1041,13 @@
1097
1041
  }
1098
1042
  }
1099
1043
  function findLinkFromClickTarget(target) {
1100
- return findClosestRecursively(target, "a[href]:not([target^=_]):not([download])");
1101
- }
1102
- function getLocationForLink(link) {
1103
- return expandURL(link.getAttribute("href") || "");
1044
+ const link = findClosestRecursively(target, "a[href], a[xlink\\:href]");
1045
+ if (!link) return null;
1046
+ if (link.href.startsWith("#")) return null;
1047
+ if (link.hasAttribute("download")) return null;
1048
+ const linkTarget = link.getAttribute("target");
1049
+ if (linkTarget && linkTarget !== "_self") return null;
1050
+ return link;
1104
1051
  }
1105
1052
  function debounce(fn, delay) {
1106
1053
  let timeoutId = null;
@@ -1165,12 +1112,15 @@
1165
1112
  return (getLastPathComponent(url).match(/\.[^.]*$/) || [])[0] || "";
1166
1113
  }
1167
1114
  function isPrefixedBy(baseURL, url) {
1168
- const prefix = getPrefix(url);
1169
- return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix);
1115
+ const prefix = addTrailingSlash(url.origin + url.pathname);
1116
+ return addTrailingSlash(baseURL.href) === prefix || baseURL.href.startsWith(prefix);
1170
1117
  }
1171
1118
  function locationIsVisitable(location2, rootLocation) {
1172
1119
  return isPrefixedBy(location2, rootLocation) && !config.drive.unvisitableExtensions.has(getExtension(location2));
1173
1120
  }
1121
+ function getLocationForLink(link) {
1122
+ return expandURL(link.getAttribute("href") || "");
1123
+ }
1174
1124
  function getRequestURL(url) {
1175
1125
  const anchor = getAnchor(url);
1176
1126
  return anchor != null ? url.href.slice(0, -(anchor.length + 1)) : url.href;
@@ -1187,9 +1137,6 @@
1187
1137
  function getLastPathComponent(url) {
1188
1138
  return getPathComponents(url).slice(-1)[0];
1189
1139
  }
1190
- function getPrefix(url) {
1191
- return addTrailingSlash(url.origin + url.pathname);
1192
- }
1193
1140
  function addTrailingSlash(value) {
1194
1141
  return value.endsWith("/") ? value : value + "/";
1195
1142
  }
@@ -1253,13 +1200,12 @@
1253
1200
  }
1254
1201
  };
1255
1202
  var recentRequests = new LimitedSet(20);
1256
- var nativeFetch = window.fetch;
1257
1203
  function fetchWithTurboHeaders(url, options = {}) {
1258
1204
  const modifiedHeaders = new Headers(options.headers || {});
1259
1205
  const requestUID = uuid();
1260
1206
  recentRequests.add(requestUID);
1261
1207
  modifiedHeaders.append("X-Turbo-Request-Id", requestUID);
1262
- return nativeFetch(url, {
1208
+ return window.fetch(url, {
1263
1209
  ...options,
1264
1210
  headers: modifiedHeaders
1265
1211
  });
@@ -1513,29 +1459,92 @@
1513
1459
  }
1514
1460
  return fragment;
1515
1461
  }
1462
+ var identity = (key) => key;
1463
+ var LRUCache = class {
1464
+ keys = [];
1465
+ entries = {};
1466
+ #toCacheKey;
1467
+ constructor(size, toCacheKey2 = identity) {
1468
+ this.size = size;
1469
+ this.#toCacheKey = toCacheKey2;
1470
+ }
1471
+ has(key) {
1472
+ return this.#toCacheKey(key) in this.entries;
1473
+ }
1474
+ get(key) {
1475
+ if (this.has(key)) {
1476
+ const entry = this.read(key);
1477
+ this.touch(key);
1478
+ return entry;
1479
+ }
1480
+ }
1481
+ put(key, entry) {
1482
+ this.write(key, entry);
1483
+ this.touch(key);
1484
+ return entry;
1485
+ }
1486
+ clear() {
1487
+ for (const key of Object.keys(this.entries)) {
1488
+ this.evict(key);
1489
+ }
1490
+ }
1491
+ // Private
1492
+ read(key) {
1493
+ return this.entries[this.#toCacheKey(key)];
1494
+ }
1495
+ write(key, entry) {
1496
+ this.entries[this.#toCacheKey(key)] = entry;
1497
+ }
1498
+ touch(key) {
1499
+ key = this.#toCacheKey(key);
1500
+ const index = this.keys.indexOf(key);
1501
+ if (index > -1) this.keys.splice(index, 1);
1502
+ this.keys.unshift(key);
1503
+ this.trim();
1504
+ }
1505
+ trim() {
1506
+ for (const key of this.keys.splice(this.size)) {
1507
+ this.evict(key);
1508
+ }
1509
+ }
1510
+ evict(key) {
1511
+ delete this.entries[key];
1512
+ }
1513
+ };
1516
1514
  var PREFETCH_DELAY = 100;
1517
- var PrefetchCache = class {
1515
+ var PrefetchCache = class extends LRUCache {
1518
1516
  #prefetchTimeout = null;
1519
- #prefetched = null;
1520
- get(url) {
1521
- if (this.#prefetched && this.#prefetched.url === url && this.#prefetched.expire > Date.now()) {
1522
- return this.#prefetched.request;
1523
- }
1517
+ #maxAges = {};
1518
+ constructor(size = 1, prefetchDelay = PREFETCH_DELAY) {
1519
+ super(size, toCacheKey);
1520
+ this.prefetchDelay = prefetchDelay;
1524
1521
  }
1525
- setLater(url, request, ttl) {
1526
- this.clear();
1522
+ putLater(url, request, ttl) {
1527
1523
  this.#prefetchTimeout = setTimeout(() => {
1528
1524
  request.perform();
1529
- this.set(url, request, ttl);
1525
+ this.put(url, request, ttl);
1530
1526
  this.#prefetchTimeout = null;
1531
- }, PREFETCH_DELAY);
1527
+ }, this.prefetchDelay);
1532
1528
  }
1533
- set(url, request, ttl) {
1534
- this.#prefetched = { url, request, expire: new Date((/* @__PURE__ */ new Date()).getTime() + ttl) };
1529
+ put(url, request, ttl = cacheTtl) {
1530
+ super.put(url, request);
1531
+ this.#maxAges[toCacheKey(url)] = new Date((/* @__PURE__ */ new Date()).getTime() + ttl);
1535
1532
  }
1536
1533
  clear() {
1534
+ super.clear();
1537
1535
  if (this.#prefetchTimeout) clearTimeout(this.#prefetchTimeout);
1538
- this.#prefetched = null;
1536
+ }
1537
+ evict(key) {
1538
+ super.evict(key);
1539
+ delete this.#maxAges[key];
1540
+ }
1541
+ has(key) {
1542
+ if (super.has(key)) {
1543
+ const maxAge = this.#maxAges[toCacheKey(key)];
1544
+ return maxAge && maxAge > Date.now();
1545
+ } else {
1546
+ return false;
1547
+ }
1539
1548
  }
1540
1549
  };
1541
1550
  var cacheTtl = 10 * 1e3;
@@ -1849,8 +1858,8 @@
1849
1858
  scrollToAnchor(anchor) {
1850
1859
  const element = this.snapshot.getElementForAnchor(anchor);
1851
1860
  if (element) {
1852
- this.scrollToElement(element);
1853
1861
  this.focusElement(element);
1862
+ this.scrollToElement(element);
1854
1863
  } else {
1855
1864
  this.scrollToPosition({ x: 0, y: 0 });
1856
1865
  }
@@ -2311,9 +2320,6 @@
2311
2320
  }
2312
2321
  function morphOuterHTML(ctx, oldNode, newNode) {
2313
2322
  const oldParent = normalizeParent(oldNode);
2314
- let childNodes = Array.from(oldParent.childNodes);
2315
- const index = childNodes.indexOf(oldNode);
2316
- const rightMargin = childNodes.length - (index + 1);
2317
2323
  morphChildren2(
2318
2324
  ctx,
2319
2325
  oldParent,
@@ -2324,8 +2330,7 @@
2324
2330
  oldNode.nextSibling
2325
2331
  // end point for iteration
2326
2332
  );
2327
- childNodes = Array.from(oldParent.childNodes);
2328
- return childNodes.slice(index, childNodes.length - rightMargin);
2333
+ return Array.from(oldParent.childNodes);
2329
2334
  }
2330
2335
  function saveAndRestoreFocus(ctx, fn) {
2331
2336
  if (!ctx.config.restoreFocus) return fn();
@@ -2338,8 +2343,8 @@
2338
2343
  }
2339
2344
  const { id: activeElementId, selectionStart, selectionEnd } = activeElement;
2340
2345
  const results = fn();
2341
- if (activeElementId && activeElementId !== document.activeElement?.id) {
2342
- activeElement = ctx.target.querySelector(`#${activeElementId}`);
2346
+ if (activeElementId && activeElementId !== document.activeElement?.getAttribute("id")) {
2347
+ activeElement = ctx.target.querySelector(`[id="${activeElementId}"]`);
2343
2348
  activeElement?.focus();
2344
2349
  }
2345
2350
  if (activeElement && !activeElement.selectionEnd && selectionEnd) {
@@ -2371,16 +2376,22 @@
2371
2376
  continue;
2372
2377
  }
2373
2378
  }
2374
- if (newChild instanceof Element && ctx.persistentIds.has(newChild.id)) {
2375
- const movedChild = moveBeforeById(
2376
- oldParent,
2377
- newChild.id,
2378
- insertionPoint,
2379
- ctx
2379
+ if (newChild instanceof Element) {
2380
+ const newChildId = (
2381
+ /** @type {String} */
2382
+ newChild.getAttribute("id")
2380
2383
  );
2381
- morphNode(movedChild, newChild, ctx);
2382
- insertionPoint = movedChild.nextSibling;
2383
- continue;
2384
+ if (ctx.persistentIds.has(newChildId)) {
2385
+ const movedChild = moveBeforeById(
2386
+ oldParent,
2387
+ newChildId,
2388
+ insertionPoint,
2389
+ ctx
2390
+ );
2391
+ morphNode(movedChild, newChild, ctx);
2392
+ insertionPoint = movedChild.nextSibling;
2393
+ continue;
2394
+ }
2384
2395
  }
2385
2396
  const insertedNode = createNode(
2386
2397
  oldParent,
@@ -2440,7 +2451,7 @@
2440
2451
  softMatch = void 0;
2441
2452
  }
2442
2453
  }
2443
- if (cursor.contains(document.activeElement)) break;
2454
+ if (ctx.activeElementAndParents.includes(cursor)) break;
2444
2455
  cursor = cursor.nextSibling;
2445
2456
  }
2446
2457
  return softMatch || null;
@@ -2468,7 +2479,8 @@
2468
2479
  return oldElt.nodeType === newElt.nodeType && oldElt.tagName === newElt.tagName && // If oldElt has an `id` with possible state and it doesn't match newElt.id then avoid morphing.
2469
2480
  // We'll still match an anonymous node with an IDed newElt, though, because if it got this far,
2470
2481
  // its not persistent, and new nodes can't have any hidden state.
2471
- (!oldElt.id || oldElt.id === newElt.id);
2482
+ // We can't use .id because of form input shadowing, and we can't count on .getAttribute's presence because it could be a document-fragment
2483
+ (!oldElt.getAttribute?.("id") || oldElt.getAttribute?.("id") === newElt.getAttribute?.("id"));
2472
2484
  }
2473
2485
  return findBestMatch2;
2474
2486
  })();
@@ -2496,14 +2508,19 @@
2496
2508
  function moveBeforeById(parentNode, id, after, ctx) {
2497
2509
  const target = (
2498
2510
  /** @type {Element} - will always be found */
2499
- ctx.target.querySelector(`#${id}`) || ctx.pantry.querySelector(`#${id}`)
2511
+ // ctx.target.id unsafe because of form input shadowing
2512
+ // ctx.target could be a document fragment which doesn't have `getAttribute`
2513
+ ctx.target.getAttribute?.("id") === id && ctx.target || ctx.target.querySelector(`[id="${id}"]`) || ctx.pantry.querySelector(`[id="${id}"]`)
2500
2514
  );
2501
2515
  removeElementFromAncestorsIdMaps(target, ctx);
2502
2516
  moveBefore(parentNode, target, after);
2503
2517
  return target;
2504
2518
  }
2505
2519
  function removeElementFromAncestorsIdMaps(element, ctx) {
2506
- const id = element.id;
2520
+ const id = (
2521
+ /** @type {String} */
2522
+ element.getAttribute("id")
2523
+ );
2507
2524
  while (element = element.parentNode) {
2508
2525
  let idSet = ctx.idMap.get(element);
2509
2526
  if (idSet) {
@@ -2767,6 +2784,7 @@
2767
2784
  idMap,
2768
2785
  persistentIds,
2769
2786
  pantry: createPantry(),
2787
+ activeElementAndParents: createActiveElementAndParents(oldNode),
2770
2788
  callbacks: mergedConfig.callbacks,
2771
2789
  head: mergedConfig.head
2772
2790
  };
@@ -2788,16 +2806,32 @@
2788
2806
  document.body.insertAdjacentElement("afterend", pantry);
2789
2807
  return pantry;
2790
2808
  }
2809
+ function createActiveElementAndParents(oldNode) {
2810
+ let activeElementAndParents = [];
2811
+ let elt = document.activeElement;
2812
+ if (elt?.tagName !== "BODY" && oldNode.contains(elt)) {
2813
+ while (elt) {
2814
+ activeElementAndParents.push(elt);
2815
+ if (elt === oldNode) break;
2816
+ elt = elt.parentElement;
2817
+ }
2818
+ }
2819
+ return activeElementAndParents;
2820
+ }
2791
2821
  function findIdElements(root) {
2792
2822
  let elements = Array.from(root.querySelectorAll("[id]"));
2793
- if (root.id) {
2823
+ if (root.getAttribute?.("id")) {
2794
2824
  elements.push(root);
2795
2825
  }
2796
2826
  return elements;
2797
2827
  }
2798
2828
  function populateIdMapWithTree(idMap, persistentIds, root, elements) {
2799
2829
  for (const elt of elements) {
2800
- if (persistentIds.has(elt.id)) {
2830
+ const id = (
2831
+ /** @type {String} */
2832
+ elt.getAttribute("id")
2833
+ );
2834
+ if (persistentIds.has(id)) {
2801
2835
  let current = elt;
2802
2836
  while (current) {
2803
2837
  let idSet = idMap.get(current);
@@ -2805,7 +2839,7 @@
2805
2839
  idSet = /* @__PURE__ */ new Set();
2806
2840
  idMap.set(current, idSet);
2807
2841
  }
2808
- idSet.add(elt.id);
2842
+ idSet.add(id);
2809
2843
  if (current === root) break;
2810
2844
  current = current.parentElement;
2811
2845
  }
@@ -2871,7 +2905,10 @@
2871
2905
  );
2872
2906
  } else if (newContent instanceof Node) {
2873
2907
  if (newContent.parentNode) {
2874
- return createDuckTypedParent(newContent);
2908
+ return (
2909
+ /** @type {any} */
2910
+ new SlicedParentNode(newContent)
2911
+ );
2875
2912
  } else {
2876
2913
  const dummyParent = document.createElement("div");
2877
2914
  dummyParent.append(newContent);
@@ -2885,27 +2922,68 @@
2885
2922
  return dummyParent;
2886
2923
  }
2887
2924
  }
2888
- function createDuckTypedParent(newContent) {
2889
- return (
2890
- /** @type {Element} */
2891
- /** @type {unknown} */
2892
- {
2893
- childNodes: [newContent],
2894
- /** @ts-ignore - cover your eyes for a minute, tsc */
2895
- querySelectorAll: (s) => {
2896
- const elements = newContent.querySelectorAll(s);
2897
- return newContent.matches(s) ? [newContent, ...elements] : elements;
2898
- },
2899
- /** @ts-ignore */
2900
- insertBefore: (n, r) => newContent.parentNode.insertBefore(n, r),
2901
- /** @ts-ignore */
2902
- moveBefore: (n, r) => newContent.parentNode.moveBefore(n, r),
2903
- // for later use with populateIdMapWithTree to halt upwards iteration
2904
- get __idiomorphRoot() {
2905
- return newContent;
2906
- }
2925
+ class SlicedParentNode {
2926
+ /** @param {Node} node */
2927
+ constructor(node) {
2928
+ this.originalNode = node;
2929
+ this.realParentNode = /** @type {Element} */
2930
+ node.parentNode;
2931
+ this.previousSibling = node.previousSibling;
2932
+ this.nextSibling = node.nextSibling;
2933
+ }
2934
+ /** @returns {Node[]} */
2935
+ get childNodes() {
2936
+ const nodes = [];
2937
+ let cursor = this.previousSibling ? this.previousSibling.nextSibling : this.realParentNode.firstChild;
2938
+ while (cursor && cursor != this.nextSibling) {
2939
+ nodes.push(cursor);
2940
+ cursor = cursor.nextSibling;
2907
2941
  }
2908
- );
2942
+ return nodes;
2943
+ }
2944
+ /**
2945
+ * @param {string} selector
2946
+ * @returns {Element[]}
2947
+ */
2948
+ querySelectorAll(selector) {
2949
+ return this.childNodes.reduce(
2950
+ (results, node) => {
2951
+ if (node instanceof Element) {
2952
+ if (node.matches(selector)) results.push(node);
2953
+ const nodeList = node.querySelectorAll(selector);
2954
+ for (let i = 0; i < nodeList.length; i++) {
2955
+ results.push(nodeList[i]);
2956
+ }
2957
+ }
2958
+ return results;
2959
+ },
2960
+ /** @type {Element[]} */
2961
+ []
2962
+ );
2963
+ }
2964
+ /**
2965
+ * @param {Node} node
2966
+ * @param {Node} referenceNode
2967
+ * @returns {Node}
2968
+ */
2969
+ insertBefore(node, referenceNode) {
2970
+ return this.realParentNode.insertBefore(node, referenceNode);
2971
+ }
2972
+ /**
2973
+ * @param {Node} node
2974
+ * @param {Node} referenceNode
2975
+ * @returns {Node}
2976
+ */
2977
+ moveBefore(node, referenceNode) {
2978
+ return this.realParentNode.moveBefore(node, referenceNode);
2979
+ }
2980
+ /**
2981
+ * for later use with populateIdMapWithTree to halt upwards iteration
2982
+ * @returns {Node}
2983
+ */
2984
+ get __idiomorphRoot() {
2985
+ return this.originalNode;
2986
+ }
2909
2987
  }
2910
2988
  function parseContent(newContent) {
2911
2989
  let parser = new DOMParser();
@@ -2951,11 +3029,21 @@
2951
3029
  callbacks: new DefaultIdiomorphCallbacks(callbacks)
2952
3030
  });
2953
3031
  }
2954
- function morphChildren(currentElement, newElement) {
3032
+ function morphChildren(currentElement, newElement, options = {}) {
2955
3033
  morphElements(currentElement, newElement.childNodes, {
3034
+ ...options,
2956
3035
  morphStyle: "innerHTML"
2957
3036
  });
2958
3037
  }
3038
+ function shouldRefreshFrameWithMorphing(currentFrame, newFrame) {
3039
+ return currentFrame instanceof FrameElement && currentFrame.shouldReloadWithMorph && (!newFrame || areFramesCompatibleForRefreshing(currentFrame, newFrame)) && !currentFrame.closest("[data-turbo-permanent]");
3040
+ }
3041
+ function areFramesCompatibleForRefreshing(currentFrame, newFrame) {
3042
+ return newFrame instanceof Element && newFrame.nodeName === "TURBO-FRAME" && currentFrame.id === newFrame.id && (!newFrame.getAttribute("src") || urlsAreEqual(currentFrame.src, newFrame.getAttribute("src")));
3043
+ }
3044
+ function closestFrameReloadableWithMorphing(node) {
3045
+ return node.parentElement.closest("turbo-frame[src][refresh=morph]");
3046
+ }
2959
3047
  var DefaultIdiomorphCallbacks = class {
2960
3048
  #beforeNodeMorphed;
2961
3049
  constructor({ beforeNodeMorphed } = {}) {
@@ -3004,7 +3092,17 @@
3004
3092
  target: currentElement,
3005
3093
  detail: { currentElement, newElement }
3006
3094
  });
3007
- morphChildren(currentElement, newElement);
3095
+ morphChildren(currentElement, newElement, {
3096
+ callbacks: {
3097
+ beforeNodeMorphed: (node, newNode) => {
3098
+ if (shouldRefreshFrameWithMorphing(node, newNode) && closestFrameReloadableWithMorphing(node) === currentElement) {
3099
+ node.reload();
3100
+ return false;
3101
+ }
3102
+ return true;
3103
+ }
3104
+ }
3105
+ });
3008
3106
  }
3009
3107
  async preservingPermanentElements(callback) {
3010
3108
  return await callback();
@@ -3225,11 +3323,17 @@
3225
3323
  for (const clonedPasswordInput of clonedElement.querySelectorAll('input[type="password"]')) {
3226
3324
  clonedPasswordInput.value = "";
3227
3325
  }
3326
+ for (const clonedNoscriptElement of clonedElement.querySelectorAll("noscript")) {
3327
+ clonedNoscriptElement.remove();
3328
+ }
3228
3329
  return new _PageSnapshot(this.documentElement, clonedElement, this.headSnapshot);
3229
3330
  }
3230
3331
  get lang() {
3231
3332
  return this.documentElement.getAttribute("lang");
3232
3333
  }
3334
+ get dir() {
3335
+ return this.documentElement.getAttribute("dir");
3336
+ }
3233
3337
  get headElement() {
3234
3338
  return this.headSnapshot.element;
3235
3339
  }
@@ -3250,13 +3354,14 @@
3250
3354
  return this.getSetting("visit-control") != "reload";
3251
3355
  }
3252
3356
  get prefersViewTransitions() {
3253
- return this.headSnapshot.getMetaValue("view-transition") === "same-origin";
3357
+ const viewTransitionEnabled = this.getSetting("view-transition") === "true" || this.headSnapshot.getMetaValue("view-transition") === "same-origin";
3358
+ return viewTransitionEnabled && !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
3254
3359
  }
3255
- get shouldMorphPage() {
3256
- return this.getSetting("refresh-method") === "morph";
3360
+ get refreshMethod() {
3361
+ return this.getSetting("refresh-method");
3257
3362
  }
3258
- get shouldPreserveScrollPosition() {
3259
- return this.getSetting("refresh-scroll") === "preserve";
3363
+ get refreshScroll() {
3364
+ return this.getSetting("refresh-scroll");
3260
3365
  }
3261
3366
  // Private
3262
3367
  getSetting(name) {
@@ -3289,7 +3394,8 @@
3289
3394
  willRender: true,
3290
3395
  updateHistory: true,
3291
3396
  shouldCacheSnapshot: true,
3292
- acceptsStreamResponse: false
3397
+ acceptsStreamResponse: false,
3398
+ refresh: {}
3293
3399
  };
3294
3400
  var TimingMetric = {
3295
3401
  visitStart: "visitStart",
@@ -3342,7 +3448,8 @@
3342
3448
  updateHistory,
3343
3449
  shouldCacheSnapshot,
3344
3450
  acceptsStreamResponse,
3345
- direction
3451
+ direction,
3452
+ refresh
3346
3453
  } = {
3347
3454
  ...defaultOptions,
3348
3455
  ...options
@@ -3353,7 +3460,6 @@
3353
3460
  this.snapshot = snapshot;
3354
3461
  this.snapshotHTML = snapshotHTML;
3355
3462
  this.response = response;
3356
- this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
3357
3463
  this.isPageRefresh = this.view.isPageRefresh(this);
3358
3464
  this.visitCachedSnapshot = visitCachedSnapshot;
3359
3465
  this.willRender = willRender;
@@ -3362,6 +3468,7 @@
3362
3468
  this.shouldCacheSnapshot = shouldCacheSnapshot;
3363
3469
  this.acceptsStreamResponse = acceptsStreamResponse;
3364
3470
  this.direction = direction || Direction[action];
3471
+ this.refresh = refresh;
3365
3472
  }
3366
3473
  get adapter() {
3367
3474
  return this.delegate.adapter;
@@ -3375,9 +3482,6 @@
3375
3482
  get restorationData() {
3376
3483
  return this.history.getRestorationDataForIdentifier(this.restorationIdentifier);
3377
3484
  }
3378
- get silent() {
3379
- return this.isSamePage;
3380
- }
3381
3485
  start() {
3382
3486
  if (this.state == VisitState.initialized) {
3383
3487
  this.recordTimingMetric(TimingMetric.visitStart);
@@ -3496,7 +3600,7 @@
3496
3600
  const isPreview = this.shouldIssueRequest();
3497
3601
  this.render(async () => {
3498
3602
  this.cacheSnapshot();
3499
- if (this.isSamePage || this.isPageRefresh) {
3603
+ if (this.isPageRefresh) {
3500
3604
  this.adapter.visitRendered(this);
3501
3605
  } else {
3502
3606
  if (this.view.renderPromise) await this.view.renderPromise;
@@ -3520,16 +3624,6 @@
3520
3624
  this.followedRedirect = true;
3521
3625
  }
3522
3626
  }
3523
- goToSamePageAnchor() {
3524
- if (this.isSamePage) {
3525
- this.render(async () => {
3526
- this.cacheSnapshot();
3527
- this.performScroll();
3528
- this.changeHistory();
3529
- this.adapter.visitRendered(this);
3530
- });
3531
- }
3532
- }
3533
3627
  // Fetch request delegate
3534
3628
  prepareRequest(request) {
3535
3629
  if (this.acceptsStreamResponse) {
@@ -3583,9 +3677,6 @@
3583
3677
  } else {
3584
3678
  this.scrollToAnchor() || this.view.scrollToTop();
3585
3679
  }
3586
- if (this.isSamePage) {
3587
- this.delegate.visitScrolledToSamePageLocation(this.view.lastRenderedLocation, this.location);
3588
- }
3589
3680
  this.scrolled = true;
3590
3681
  }
3591
3682
  }
@@ -3615,9 +3706,7 @@
3615
3706
  return typeof this.response == "object";
3616
3707
  }
3617
3708
  shouldIssueRequest() {
3618
- if (this.isSamePage) {
3619
- return false;
3620
- } else if (this.action == "restore") {
3709
+ if (this.action == "restore") {
3621
3710
  return !this.hasCachedSnapshot();
3622
3711
  } else {
3623
3712
  return this.willRender;
@@ -3667,9 +3756,9 @@
3667
3756
  }
3668
3757
  visitStarted(visit2) {
3669
3758
  this.location = visit2.location;
3759
+ this.redirectedToLocation = null;
3670
3760
  visit2.loadCachedSnapshot();
3671
3761
  visit2.issueRequest();
3672
- visit2.goToSamePageAnchor();
3673
3762
  }
3674
3763
  visitRequestStarted(visit2) {
3675
3764
  this.progressBar.setValue(0);
@@ -3681,6 +3770,9 @@
3681
3770
  }
3682
3771
  visitRequestCompleted(visit2) {
3683
3772
  visit2.loadResponse();
3773
+ if (visit2.response.redirected) {
3774
+ this.redirectedToLocation = visit2.redirectedToLocation;
3775
+ }
3684
3776
  }
3685
3777
  visitRequestFailedWithStatusCode(visit2, statusCode) {
3686
3778
  switch (statusCode) {
@@ -3753,7 +3845,7 @@
3753
3845
  };
3754
3846
  reload(reason) {
3755
3847
  dispatch("turbo:reload", { detail: reason });
3756
- window.location.href = this.location?.toString() || window.location.href;
3848
+ window.location.href = (this.redirectedToLocation || this.location)?.toString() || window.location.href;
3757
3849
  }
3758
3850
  get navigator() {
3759
3851
  return this.session.navigator;
@@ -3761,7 +3853,6 @@
3761
3853
  };
3762
3854
  var CacheObserver = class {
3763
3855
  selector = "[data-turbo-temporary]";
3764
- deprecatedSelector = "[data-turbo-cache=false]";
3765
3856
  started = false;
3766
3857
  start() {
3767
3858
  if (!this.started) {
@@ -3781,16 +3872,7 @@
3781
3872
  }
3782
3873
  };
3783
3874
  get temporaryElements() {
3784
- return [...document.querySelectorAll(this.selector), ...this.temporaryElementsWithDeprecation];
3785
- }
3786
- get temporaryElementsWithDeprecation() {
3787
- const elements = document.querySelectorAll(this.deprecatedSelector);
3788
- if (elements.length) {
3789
- console.warn(
3790
- `The ${this.deprecatedSelector} selector is deprecated and will be removed in a future version. Use ${this.selector} instead.`
3791
- );
3792
- }
3793
- return [...elements];
3875
+ return [...document.querySelectorAll(this.selector)];
3794
3876
  }
3795
3877
  };
3796
3878
  var FrameRedirector = class {
@@ -3858,7 +3940,6 @@
3858
3940
  restorationIdentifier = uuid();
3859
3941
  restorationData = {};
3860
3942
  started = false;
3861
- pageLoaded = false;
3862
3943
  currentIndex = 0;
3863
3944
  constructor(delegate) {
3864
3945
  this.delegate = delegate;
@@ -3866,7 +3947,6 @@
3866
3947
  start() {
3867
3948
  if (!this.started) {
3868
3949
  addEventListener("popstate", this.onPopState, false);
3869
- addEventListener("load", this.onPageLoad, false);
3870
3950
  this.currentIndex = history.state?.turbo?.restorationIndex || 0;
3871
3951
  this.started = true;
3872
3952
  this.replace(new URL(window.location.href));
@@ -3875,7 +3955,6 @@
3875
3955
  stop() {
3876
3956
  if (this.started) {
3877
3957
  removeEventListener("popstate", this.onPopState, false);
3878
- removeEventListener("load", this.onPageLoad, false);
3879
3958
  this.started = false;
3880
3959
  }
3881
3960
  }
@@ -3919,29 +3998,19 @@
3919
3998
  }
3920
3999
  // Event handlers
3921
4000
  onPopState = (event) => {
3922
- if (this.shouldHandlePopState()) {
3923
- const { turbo } = event.state || {};
3924
- if (turbo) {
3925
- this.location = new URL(window.location.href);
3926
- const { restorationIdentifier, restorationIndex } = turbo;
3927
- this.restorationIdentifier = restorationIdentifier;
3928
- const direction = restorationIndex > this.currentIndex ? "forward" : "back";
3929
- this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(this.location, restorationIdentifier, direction);
3930
- this.currentIndex = restorationIndex;
3931
- }
4001
+ const { turbo } = event.state || {};
4002
+ this.location = new URL(window.location.href);
4003
+ if (turbo) {
4004
+ const { restorationIdentifier, restorationIndex } = turbo;
4005
+ this.restorationIdentifier = restorationIdentifier;
4006
+ const direction = restorationIndex > this.currentIndex ? "forward" : "back";
4007
+ this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(this.location, restorationIdentifier, direction);
4008
+ this.currentIndex = restorationIndex;
4009
+ } else {
4010
+ this.currentIndex++;
4011
+ this.delegate.historyPoppedWithEmptyState(this.location);
3932
4012
  }
3933
4013
  };
3934
- onPageLoad = async (_event) => {
3935
- await nextMicrotask();
3936
- this.pageLoaded = true;
3937
- };
3938
- // Private
3939
- shouldHandlePopState() {
3940
- return this.pageIsLoaded();
3941
- }
3942
- pageIsLoaded() {
3943
- return this.pageLoaded || document.readyState == "complete";
3944
- }
3945
4014
  };
3946
4015
  var LinkPrefetchObserver = class {
3947
4016
  started = false;
@@ -3999,7 +4068,8 @@
3999
4068
  new URLSearchParams(),
4000
4069
  target
4001
4070
  );
4002
- prefetchCache.setLater(location2.toString(), fetchRequest, this.#cacheTtl);
4071
+ fetchRequest.fetchOptions.priority = "low";
4072
+ prefetchCache.putLater(location2, fetchRequest, this.#cacheTtl);
4003
4073
  }
4004
4074
  }
4005
4075
  };
@@ -4012,7 +4082,7 @@
4012
4082
  };
4013
4083
  #tryToUsePrefetchedRequest = (event) => {
4014
4084
  if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "GET") {
4015
- const cached = prefetchCache.get(event.detail.url.toString());
4085
+ const cached = prefetchCache.get(event.detail.url);
4016
4086
  if (cached) {
4017
4087
  event.detail.fetchRequest = cached;
4018
4088
  }
@@ -4161,7 +4231,7 @@
4161
4231
  } else {
4162
4232
  await this.view.renderPage(snapshot, false, true, this.currentVisit);
4163
4233
  }
4164
- if (!snapshot.shouldPreserveScrollPosition) {
4234
+ if (snapshot.refreshScroll !== "preserve") {
4165
4235
  this.view.scrollToTop();
4166
4236
  }
4167
4237
  this.view.clearSnapshotCache();
@@ -4190,14 +4260,10 @@
4190
4260
  this.delegate.visitCompleted(visit2);
4191
4261
  delete this.currentVisit;
4192
4262
  }
4263
+ // Same-page links are no longer handled with a Visit.
4264
+ // This method is still needed for Turbo Native adapters.
4193
4265
  locationWithActionIsSamePage(location2, action) {
4194
- const anchor = getAnchor(location2);
4195
- const currentAnchor = getAnchor(this.view.lastRenderedLocation);
4196
- const isRestorationToTop = action === "restore" && typeof anchor === "undefined";
4197
- return action !== "replace" && getRequestURL(location2) === getRequestURL(this.view.lastRenderedLocation) && (isRestorationToTop || anchor != null && anchor !== currentAnchor);
4198
- }
4199
- visitScrolledToSamePageLocation(oldURL, newURL) {
4200
- this.delegate.visitScrolledToSamePageLocation(oldURL, newURL);
4266
+ return false;
4201
4267
  }
4202
4268
  // Visits
4203
4269
  get location() {
@@ -4516,12 +4582,17 @@
4516
4582
  }
4517
4583
  #setLanguage() {
4518
4584
  const { documentElement } = this.currentSnapshot;
4519
- const { lang } = this.newSnapshot;
4585
+ const { dir, lang } = this.newSnapshot;
4520
4586
  if (lang) {
4521
4587
  documentElement.setAttribute("lang", lang);
4522
4588
  } else {
4523
4589
  documentElement.removeAttribute("lang");
4524
4590
  }
4591
+ if (dir) {
4592
+ documentElement.setAttribute("dir", dir);
4593
+ } else {
4594
+ documentElement.removeAttribute("dir");
4595
+ }
4525
4596
  }
4526
4597
  async mergeHead() {
4527
4598
  const mergedHeadElements = this.mergeProvisionalElements();
@@ -4601,8 +4672,14 @@
4601
4672
  }
4602
4673
  activateNewBody() {
4603
4674
  document.adoptNode(this.newElement);
4675
+ this.removeNoscriptElements();
4604
4676
  this.activateNewBodyScriptElements();
4605
4677
  }
4678
+ removeNoscriptElements() {
4679
+ for (const noscriptElement of this.newElement.querySelectorAll("noscript")) {
4680
+ noscriptElement.remove();
4681
+ }
4682
+ }
4606
4683
  activateNewBodyScriptElements() {
4607
4684
  for (const inertScriptElement of this.newBodyScriptElements) {
4608
4685
  const activatedScriptElement = activateScriptElement(inertScriptElement);
@@ -4640,12 +4717,15 @@
4640
4717
  static renderElement(currentElement, newElement) {
4641
4718
  morphElements(currentElement, newElement, {
4642
4719
  callbacks: {
4643
- beforeNodeMorphed: (element) => !canRefreshFrame(element)
4720
+ beforeNodeMorphed: (node, newNode) => {
4721
+ if (shouldRefreshFrameWithMorphing(node, newNode) && !closestFrameReloadableWithMorphing(node)) {
4722
+ node.reload();
4723
+ return false;
4724
+ }
4725
+ return true;
4726
+ }
4644
4727
  }
4645
4728
  });
4646
- for (const frame of currentElement.querySelectorAll("turbo-frame")) {
4647
- if (canRefreshFrame(frame)) frame.reload();
4648
- }
4649
4729
  dispatch("turbo:morph", { detail: { currentElement, newElement } });
4650
4730
  }
4651
4731
  async preservingPermanentElements(callback) {
@@ -4658,51 +4738,12 @@
4658
4738
  return false;
4659
4739
  }
4660
4740
  };
4661
- function canRefreshFrame(frame) {
4662
- return frame instanceof FrameElement && frame.src && frame.refresh === "morph" && !frame.closest("[data-turbo-permanent]");
4663
- }
4664
- var SnapshotCache = class {
4665
- keys = [];
4666
- snapshots = {};
4741
+ var SnapshotCache = class extends LRUCache {
4667
4742
  constructor(size) {
4668
- this.size = size;
4669
- }
4670
- has(location2) {
4671
- return toCacheKey(location2) in this.snapshots;
4672
- }
4673
- get(location2) {
4674
- if (this.has(location2)) {
4675
- const snapshot = this.read(location2);
4676
- this.touch(location2);
4677
- return snapshot;
4678
- }
4679
- }
4680
- put(location2, snapshot) {
4681
- this.write(location2, snapshot);
4682
- this.touch(location2);
4683
- return snapshot;
4743
+ super(size, toCacheKey);
4684
4744
  }
4685
- clear() {
4686
- this.snapshots = {};
4687
- }
4688
- // Private
4689
- read(location2) {
4690
- return this.snapshots[toCacheKey(location2)];
4691
- }
4692
- write(location2, snapshot) {
4693
- this.snapshots[toCacheKey(location2)] = snapshot;
4694
- }
4695
- touch(location2) {
4696
- const key = toCacheKey(location2);
4697
- const index = this.keys.indexOf(key);
4698
- if (index > -1) this.keys.splice(index, 1);
4699
- this.keys.unshift(key);
4700
- this.trim();
4701
- }
4702
- trim() {
4703
- for (const key of this.keys.splice(this.size)) {
4704
- delete this.snapshots[key];
4705
- }
4745
+ get snapshots() {
4746
+ return this.entries;
4706
4747
  }
4707
4748
  };
4708
4749
  var PageView = class extends View {
@@ -4713,7 +4754,7 @@
4713
4754
  return this.snapshot.prefersViewTransitions && newSnapshot.prefersViewTransitions;
4714
4755
  }
4715
4756
  renderPage(snapshot, isPreview = false, willRender = true, visit2) {
4716
- const shouldMorphPage = this.isPageRefresh(visit2) && this.snapshot.shouldMorphPage;
4757
+ const shouldMorphPage = this.isPageRefresh(visit2) && (visit2?.refresh?.method || this.snapshot.refreshMethod) === "morph";
4717
4758
  const rendererClass = shouldMorphPage ? MorphingPageRenderer : PageRenderer;
4718
4759
  const renderer = new rendererClass(this.snapshot, snapshot, isPreview, willRender);
4719
4760
  if (!renderer.shouldRender) {
@@ -4748,7 +4789,7 @@
4748
4789
  return !visit2 || this.lastRenderedLocation.pathname === visit2.location.pathname && visit2.action === "replace";
4749
4790
  }
4750
4791
  shouldPreserveScrollPosition(visit2) {
4751
- return this.isPageRefresh(visit2) && this.snapshot.shouldPreserveScrollPosition;
4792
+ return this.isPageRefresh(visit2) && (visit2?.refresh?.scroll || this.snapshot.refreshScroll) === "preserve";
4752
4793
  }
4753
4794
  get snapshot() {
4754
4795
  return PageSnapshot.fromElement(this.element);
@@ -4905,11 +4946,13 @@
4905
4946
  this.navigator.proposeVisit(expandURL(location2), options);
4906
4947
  }
4907
4948
  }
4908
- refresh(url, requestId) {
4949
+ refresh(url, options = {}) {
4950
+ options = typeof options === "string" ? { requestId: options } : options;
4951
+ const { method, requestId, scroll } = options;
4909
4952
  const isRecentRequest = requestId && this.recentRequests.has(requestId);
4910
4953
  const isCurrentUrl = url === document.baseURI;
4911
4954
  if (!isRecentRequest && !this.navigator.currentVisit && isCurrentUrl) {
4912
- this.visit(url, { action: "replace", shouldCacheSnapshot: false });
4955
+ this.visit(url, { action: "replace", shouldCacheSnapshot: false, refresh: { method, scroll } });
4913
4956
  }
4914
4957
  }
4915
4958
  connectStreamSource(source) {
@@ -4988,6 +5031,11 @@
4988
5031
  });
4989
5032
  }
4990
5033
  }
5034
+ historyPoppedWithEmptyState(location2) {
5035
+ this.history.replace(location2);
5036
+ this.view.lastRenderedLocation = location2;
5037
+ this.view.cacheSnapshot();
5038
+ }
4991
5039
  // Scroll observer delegate
4992
5040
  scrollPositionChanged(position) {
4993
5041
  this.history.updateRestorationData({ scrollPosition: position });
@@ -5013,7 +5061,7 @@
5013
5061
  }
5014
5062
  // Navigator delegate
5015
5063
  allowsVisitingLocationWithAction(location2, action) {
5016
- return this.locationWithActionIsSamePage(location2, action) || this.applicationAllowsVisitingLocation(location2);
5064
+ return this.applicationAllowsVisitingLocation(location2);
5017
5065
  }
5018
5066
  visitProposedToLocation(location2, options) {
5019
5067
  extendURLWithDeprecatedProperties(location2);
@@ -5026,21 +5074,13 @@
5026
5074
  this.view.markVisitDirection(visit2.direction);
5027
5075
  }
5028
5076
  extendURLWithDeprecatedProperties(visit2.location);
5029
- if (!visit2.silent) {
5030
- this.notifyApplicationAfterVisitingLocation(visit2.location, visit2.action);
5031
- }
5077
+ this.notifyApplicationAfterVisitingLocation(visit2.location, visit2.action);
5032
5078
  }
5033
5079
  visitCompleted(visit2) {
5034
5080
  this.view.unmarkVisitDirection();
5035
5081
  clearBusyState(document.documentElement);
5036
5082
  this.notifyApplicationAfterPageLoad(visit2.getTimingMetrics());
5037
5083
  }
5038
- locationWithActionIsSamePage(location2, action) {
5039
- return this.navigator.locationWithActionIsSamePage(location2, action);
5040
- }
5041
- visitScrolledToSamePageLocation(oldURL, newURL) {
5042
- this.notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL);
5043
- }
5044
5084
  // Form submit observer delegate
5045
5085
  willSubmitForm(form, submitter2) {
5046
5086
  const action = getAction$1(form, submitter2);
@@ -5066,9 +5106,7 @@
5066
5106
  }
5067
5107
  // Page view delegate
5068
5108
  viewWillCacheSnapshot() {
5069
- if (!this.navigator.currentVisit?.silent) {
5070
- this.notifyApplicationBeforeCachingSnapshot();
5071
- }
5109
+ this.notifyApplicationBeforeCachingSnapshot();
5072
5110
  }
5073
5111
  allowsImmediateRender({ element }, options) {
5074
5112
  const event = this.notifyApplicationBeforeRender(element, options);
@@ -5140,14 +5178,6 @@
5140
5178
  detail: { url: this.location.href, timing }
5141
5179
  });
5142
5180
  }
5143
- notifyApplicationAfterVisitingSamePageLocation(oldURL, newURL) {
5144
- dispatchEvent(
5145
- new HashChangeEvent("hashchange", {
5146
- oldURL: oldURL.toString(),
5147
- newURL: newURL.toString()
5148
- })
5149
- );
5150
- }
5151
5181
  notifyApplicationAfterFrameLoad(frame) {
5152
5182
  return dispatch("turbo:frame-load", { target: frame });
5153
5183
  }
@@ -5207,7 +5237,7 @@
5207
5237
  }
5208
5238
  };
5209
5239
  var session = new Session(recentRequests);
5210
- var { cache, navigator: navigator$1 } = session;
5240
+ var { cache, navigator: sessionNavigator } = session;
5211
5241
  function start() {
5212
5242
  session.start();
5213
5243
  }
@@ -5226,12 +5256,6 @@
5226
5256
  function renderStreamMessage(message) {
5227
5257
  session.renderStreamMessage(message);
5228
5258
  }
5229
- function clearCache() {
5230
- console.warn(
5231
- "Please replace `Turbo.clearCache()` with `Turbo.cache.clear()`. The top-level function is deprecated and will be removed in a future version of Turbo.`"
5232
- );
5233
- session.clearCache();
5234
- }
5235
5259
  function setProgressBarDelay(delay) {
5236
5260
  console.warn(
5237
5261
  "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.`"
@@ -5250,26 +5274,35 @@
5250
5274
  );
5251
5275
  config.forms.mode = mode;
5252
5276
  }
5277
+ function morphBodyElements(currentBody, newBody) {
5278
+ MorphingPageRenderer.renderElement(currentBody, newBody);
5279
+ }
5280
+ function morphTurboFrameElements(currentFrame, newFrame) {
5281
+ MorphingFrameRenderer.renderElement(currentFrame, newFrame);
5282
+ }
5253
5283
  var Turbo = /* @__PURE__ */ Object.freeze({
5254
5284
  __proto__: null,
5255
- navigator: navigator$1,
5256
- session,
5257
- cache,
5258
5285
  PageRenderer,
5259
5286
  PageSnapshot,
5260
5287
  FrameRenderer,
5261
5288
  fetch: fetchWithTurboHeaders,
5262
5289
  config,
5290
+ session,
5291
+ cache,
5292
+ navigator: sessionNavigator,
5263
5293
  start,
5264
5294
  registerAdapter,
5265
5295
  visit,
5266
5296
  connectStreamSource,
5267
5297
  disconnectStreamSource,
5268
5298
  renderStreamMessage,
5269
- clearCache,
5270
5299
  setProgressBarDelay,
5271
5300
  setConfirmMethod,
5272
- setFormMode
5301
+ setFormMode,
5302
+ morphBodyElements,
5303
+ morphTurboFrameElements,
5304
+ morphChildren,
5305
+ morphElements
5273
5306
  });
5274
5307
  var TurboFrameMissingError = class extends Error {
5275
5308
  };
@@ -5313,15 +5346,23 @@
5313
5346
  this.formLinkClickObserver.stop();
5314
5347
  this.linkInterceptor.stop();
5315
5348
  this.formSubmitObserver.stop();
5349
+ if (!this.element.hasAttribute("recurse")) {
5350
+ this.#currentFetchRequest?.cancel();
5351
+ }
5316
5352
  }
5317
5353
  }
5318
5354
  disabledChanged() {
5319
- if (this.loadingStyle == FrameLoadingStyle.eager) {
5355
+ if (this.disabled) {
5356
+ this.#currentFetchRequest?.cancel();
5357
+ } else if (this.loadingStyle == FrameLoadingStyle.eager) {
5320
5358
  this.#loadSourceURL();
5321
5359
  }
5322
5360
  }
5323
5361
  sourceURLChanged() {
5324
5362
  if (this.#isIgnoringChangesTo("src")) return;
5363
+ if (!this.sourceURL) {
5364
+ this.#currentFetchRequest?.cancel();
5365
+ }
5325
5366
  if (this.element.isConnected) {
5326
5367
  this.complete = false;
5327
5368
  }
@@ -5403,12 +5444,13 @@
5403
5444
  }
5404
5445
  this.formSubmission = new FormSubmission(this, element, submitter2);
5405
5446
  const { fetchRequest } = this.formSubmission;
5406
- this.prepareRequest(fetchRequest);
5447
+ const frame = this.#findFrameElement(element, submitter2);
5448
+ this.prepareRequest(fetchRequest, frame);
5407
5449
  this.formSubmission.start();
5408
5450
  }
5409
5451
  // Fetch request delegate
5410
- prepareRequest(request) {
5411
- request.headers["Turbo-Frame"] = this.id;
5452
+ prepareRequest(request, frame = this) {
5453
+ request.headers["Turbo-Frame"] = frame.id;
5412
5454
  if (this.currentNavigationElement?.hasAttribute("data-turbo-stream")) {
5413
5455
  request.acceptResponseType(StreamMessage.contentType);
5414
5456
  }
@@ -5598,7 +5640,8 @@
5598
5640
  }
5599
5641
  #findFrameElement(element, submitter2) {
5600
5642
  const id = getAttribute("data-turbo-frame", submitter2, element) || this.element.getAttribute("target");
5601
- return getFrameElementById(id) ?? this.element;
5643
+ const target = this.#getFrameElementById(id);
5644
+ return target instanceof FrameElement ? target : this.element;
5602
5645
  }
5603
5646
  async extractForeignFrameElement(container) {
5604
5647
  let element;
@@ -5632,9 +5675,11 @@
5632
5675
  return false;
5633
5676
  }
5634
5677
  if (id) {
5635
- const frameElement = getFrameElementById(id);
5678
+ const frameElement = this.#getFrameElementById(id);
5636
5679
  if (frameElement) {
5637
5680
  return !frameElement.disabled;
5681
+ } else if (id == "_parent") {
5682
+ return false;
5638
5683
  }
5639
5684
  }
5640
5685
  if (!session.elementIsNavigatable(element)) {
@@ -5649,8 +5694,11 @@
5649
5694
  get id() {
5650
5695
  return this.element.id;
5651
5696
  }
5697
+ get disabled() {
5698
+ return this.element.disabled;
5699
+ }
5652
5700
  get enabled() {
5653
- return !this.element.disabled;
5701
+ return !this.disabled;
5654
5702
  }
5655
5703
  get sourceURL() {
5656
5704
  if (this.element.src) {
@@ -5699,15 +5747,15 @@
5699
5747
  callback();
5700
5748
  delete this.currentNavigationElement;
5701
5749
  }
5702
- };
5703
- function getFrameElementById(id) {
5704
- if (id != null) {
5705
- const element = document.getElementById(id);
5706
- if (element instanceof FrameElement) {
5707
- return element;
5750
+ #getFrameElementById(id) {
5751
+ if (id != null) {
5752
+ const element = id === "_parent" ? this.element.parentElement.closest("turbo-frame") : document.getElementById(id);
5753
+ if (element instanceof FrameElement) {
5754
+ return element;
5755
+ }
5708
5756
  }
5709
5757
  }
5710
- }
5758
+ };
5711
5759
  function activateElement(element, currentURL) {
5712
5760
  if (element) {
5713
5761
  const src = element.getAttribute("src");
@@ -5726,6 +5774,7 @@
5726
5774
  }
5727
5775
  var StreamActions = {
5728
5776
  after() {
5777
+ this.removeDuplicateTargetSiblings();
5729
5778
  this.targetElements.forEach((e) => e.parentElement?.insertBefore(this.templateContent, e.nextSibling));
5730
5779
  },
5731
5780
  append() {
@@ -5733,6 +5782,7 @@
5733
5782
  this.targetElements.forEach((e) => e.append(this.templateContent));
5734
5783
  },
5735
5784
  before() {
5785
+ this.removeDuplicateTargetSiblings();
5736
5786
  this.targetElements.forEach((e) => e.parentElement?.insertBefore(this.templateContent, e));
5737
5787
  },
5738
5788
  prepend() {
@@ -5764,7 +5814,10 @@
5764
5814
  });
5765
5815
  },
5766
5816
  refresh() {
5767
- session.refresh(this.baseURI, this.requestId);
5817
+ const method = this.getAttribute("method");
5818
+ const requestId = this.requestId;
5819
+ const scroll = this.getAttribute("scroll");
5820
+ session.refresh(this.baseURI, { method, requestId, scroll });
5768
5821
  }
5769
5822
  };
5770
5823
  var StreamElement = class _StreamElement extends HTMLElement {
@@ -5809,6 +5862,20 @@
5809
5862
  const newChildrenIds = [...this.templateContent?.children || []].filter((c) => !!c.getAttribute("id")).map((c) => c.getAttribute("id"));
5810
5863
  return existingChildren.filter((c) => newChildrenIds.includes(c.getAttribute("id")));
5811
5864
  }
5865
+ /**
5866
+ * Removes duplicate siblings (by ID)
5867
+ */
5868
+ removeDuplicateTargetSiblings() {
5869
+ this.duplicateSiblings.forEach((c) => c.remove());
5870
+ }
5871
+ /**
5872
+ * Gets the list of duplicate siblings (i.e. those with the same ID)
5873
+ */
5874
+ get duplicateSiblings() {
5875
+ const existingChildren = this.targetElements.flatMap((e) => [...e.parentElement.children]).filter((c) => !!c.id);
5876
+ const newChildrenIds = [...this.templateContent?.children || []].filter((c) => !!c.id).map((c) => c.id);
5877
+ return existingChildren.filter((c) => newChildrenIds.includes(c.id));
5878
+ }
5812
5879
  /**
5813
5880
  * Gets the action function to be performed.
5814
5881
  */
@@ -5935,10 +6002,10 @@
5935
6002
  customElements.define("turbo-stream-source", StreamSourceElement);
5936
6003
  }
5937
6004
  (() => {
5938
- let element = document.currentScript;
5939
- if (!element) return;
5940
- if (element.hasAttribute("data-turbo-suppress-warning")) return;
5941
- element = element.parentElement;
6005
+ const scriptElement = document.currentScript;
6006
+ if (!scriptElement) return;
6007
+ if (scriptElement.hasAttribute("data-turbo-suppress-warning")) return;
6008
+ let element = scriptElement.parentElement;
5942
6009
  while (element) {
5943
6010
  if (element == document.body) {
5944
6011
  return console.warn(
@@ -5952,7 +6019,7 @@
5952
6019
  ——
5953
6020
  Suppress this warning by adding a "data-turbo-suppress-warning" attribute to: %s
5954
6021
  `,
5955
- element.outerHTML
6022
+ scriptElement.outerHTML
5956
6023
  );
5957
6024
  }
5958
6025
  element = element.parentElement;
@@ -8578,7 +8645,7 @@
8578
8645
  };
8579
8646
  var useClickOutside = (composableController, options = {}) => {
8580
8647
  const controller = composableController;
8581
- const { onlyVisible, dispatchEvent: dispatchEvent2, events, eventPrefix } = Object.assign({}, defaultOptions$5, options);
8648
+ const { onlyVisible, dispatchEvent, events, eventPrefix } = Object.assign({}, defaultOptions$5, options);
8582
8649
  const onEvent = (event) => {
8583
8650
  const targetElement = (options === null || options === void 0 ? void 0 : options.element) || controller.element;
8584
8651
  if (targetElement.contains(event.target) || !isElementInViewport(targetElement) && onlyVisible) {
@@ -8587,7 +8654,7 @@
8587
8654
  if (controller.clickOutside) {
8588
8655
  controller.clickOutside(event);
8589
8656
  }
8590
- if (dispatchEvent2) {
8657
+ if (dispatchEvent) {
8591
8658
  const eventName = composeEventName("click:outside", controller, eventPrefix);
8592
8659
  const clickOutsideEvent = extendedEvent(eventName, event, {
8593
8660
  controller
@@ -9521,7 +9588,7 @@
9521
9588
 
9522
9589
  @hotwired/turbo/dist/turbo.es2017-esm.js:
9523
9590
  (*!
9524
- Turbo 8.0.13
9525
- Copyright © 2025 37signals LLC
9591
+ Turbo 8.0.23
9592
+ Copyright © 2026 37signals LLC
9526
9593
  *)
9527
9594
  */