turbo-rails 2.0.16 → 2.0.20

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7de922709135b46c78da967fc906370e96bace376d0c03b205b5b7fb0bf0c1db
4
- data.tar.gz: 36c2463b1a996f8d225a3ba33eb73bb07d2c34eccf8316a4670cebca2ed1c90b
3
+ metadata.gz: 06430d7095a020ded9bf6f76888c703184a7ffdd0c99e91f5461644c7fbdd062
4
+ data.tar.gz: b8b15ac1f37352d7a75782fe095c621340bb648eb24b5e36009634441efe098f
5
5
  SHA512:
6
- metadata.gz: 88303af9181adb99632e23da782c7ab7063c7ce194271b3b9dbf92d89d1e1d2cd3cf1a88147f38ca957d74f7ff310cbf455e3c636cf705ef76cc46b6583b84b1
7
- data.tar.gz: '02446187359ca3060de17e6bc435f159d5d5aa98afee83280487372bf63a70b901a44314119119ee9ff4947fba8b93d971d6f143815240cbd0bed9dcd5a7d69e'
6
+ metadata.gz: e4d105df336ab191ba9c3673bec4d47d7b4aa561cc3a865ada719ea5c478f70587a55bdd0b97a55b5846ab39afd6b18d863e50f74ec0c40657a407b00ad2cb2a
7
+ data.tar.gz: 9b293373d747dbe2964328636a3c5d4eae68fbf522a2f704948c0ea53b671c90ad8b13c8049e1a327a25127691e3cc9c3814634ae5cb9d174f94d9ff54c4f3b0
data/README.md CHANGED
@@ -206,7 +206,7 @@ Turbo can coexist with Rails UJS, but you need to take a series of upgrade steps
206
206
 
207
207
  ## Testing
208
208
 
209
- The [`Turbo::TestAssertions`](./lib/turbo/test_assertions.rb) concern provides Turbo Stream test helpers that assert the presence or absence ofs s `<turbo-stream>` elements in a rendered fragment of HTML. `Turbo::TestAssertions` are automatically included in [`ActiveSupport::TestCase`](https://edgeapi.rubyonrails.org/classes/ActiveSupport/TestCase.html) and depend on the presence of [`rails-dom-testing`](https://github.com/rails/rails-dom-testing/) assertions.
209
+ The [`Turbo::TestAssertions`](./lib/turbo/test_assertions.rb) concern provides Turbo Stream test helpers that assert the presence or absence of `<turbo-stream>` elements in a rendered fragment of HTML. `Turbo::TestAssertions` are automatically included in [`ActiveSupport::TestCase`](https://edgeapi.rubyonrails.org/classes/ActiveSupport/TestCase.html) and depend on the presence of [`rails-dom-testing`](https://github.com/rails/rails-dom-testing/) assertions.
210
210
 
211
211
  The [`Turbo::TestAssertions::IntegrationTestAssertions`](./lib/turbo/test_assertions/integration_test_assertions.rb) are built on top of `Turbo::TestAssertions`, and add support for passing a `status:` keyword. They are automatically included in [`ActionDispatch::IntegrationTest`](https://edgeguides.rubyonrails.org/testing.html#integration-testing).
212
212
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- Turbo 8.0.13
2
+ Turbo 8.0.19
3
3
  Copyright © 2025 37signals LLC
4
4
  */
5
5
  (function(prototype) {
@@ -409,7 +409,11 @@ function doesNotTargetIFrame(name) {
409
409
  }
410
410
 
411
411
  function findLinkFromClickTarget(target) {
412
- return findClosestRecursively(target, "a[href]:not([target^=_]):not([download])");
412
+ const link = findClosestRecursively(target, "a[href], a[xlink\\:href]");
413
+ if (!link) return null;
414
+ if (link.hasAttribute("download")) return null;
415
+ if (link.hasAttribute("target") && link.target !== "_self") return null;
416
+ return link;
413
417
  }
414
418
 
415
419
  function getLocationForLink(link) {
@@ -488,8 +492,8 @@ function getExtension(url) {
488
492
  }
489
493
 
490
494
  function isPrefixedBy(baseURL, url) {
491
- const prefix = getPrefix(url);
492
- return baseURL.href === expandURL(prefix).href || baseURL.href.startsWith(prefix);
495
+ const prefix = addTrailingSlash(url.origin + url.pathname);
496
+ return addTrailingSlash(baseURL.href) === prefix || baseURL.href.startsWith(prefix);
493
497
  }
494
498
 
495
499
  function locationIsVisitable(location, rootLocation) {
@@ -517,10 +521,6 @@ function getLastPathComponent(url) {
517
521
  return getPathComponents(url).slice(-1)[0];
518
522
  }
519
523
 
520
- function getPrefix(url) {
521
- return addTrailingSlash(url.origin + url.pathname);
522
- }
523
-
524
524
  function addTrailingSlash(value) {
525
525
  return value.endsWith("/") ? value : value + "/";
526
526
  }
@@ -588,14 +588,12 @@ class LimitedSet extends Set {
588
588
 
589
589
  const recentRequests = new LimitedSet(20);
590
590
 
591
- const nativeFetch = window.fetch;
592
-
593
591
  function fetchWithTurboHeaders(url, options = {}) {
594
592
  const modifiedHeaders = new Headers(options.headers || {});
595
593
  const requestUID = uuid();
596
594
  recentRequests.add(requestUID);
597
595
  modifiedHeaders.append("X-Turbo-Request-Id", requestUID);
598
- return nativeFetch(url, {
596
+ return window.fetch(url, {
599
597
  ...options,
600
598
  headers: modifiedHeaders
601
599
  });
@@ -1243,8 +1241,8 @@ class View {
1243
1241
  scrollToAnchor(anchor) {
1244
1242
  const element = this.snapshot.getElementForAnchor(anchor);
1245
1243
  if (element) {
1246
- this.scrollToElement(element);
1247
1244
  this.focusElement(element);
1245
+ this.scrollToElement(element);
1248
1246
  } else {
1249
1247
  this.scrollToPosition({
1250
1248
  x: 0,
@@ -1725,12 +1723,8 @@ var Idiomorph = function() {
1725
1723
  }
1726
1724
  function morphOuterHTML(ctx, oldNode, newNode) {
1727
1725
  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
1726
  morphChildren(ctx, oldParent, newNode, oldNode, oldNode.nextSibling);
1732
- childNodes = Array.from(oldParent.childNodes);
1733
- return childNodes.slice(index, childNodes.length - rightMargin);
1727
+ return Array.from(oldParent.childNodes);
1734
1728
  }
1735
1729
  function saveAndRestoreFocus(ctx, fn) {
1736
1730
  if (!ctx.config.restoreFocus) return fn();
@@ -1740,8 +1734,8 @@ var Idiomorph = function() {
1740
1734
  }
1741
1735
  const {id: activeElementId, selectionStart: selectionStart, selectionEnd: selectionEnd} = activeElement;
1742
1736
  const results = fn();
1743
- if (activeElementId && activeElementId !== document.activeElement?.id) {
1744
- activeElement = ctx.target.querySelector(`#${activeElementId}`);
1737
+ if (activeElementId && activeElementId !== document.activeElement?.getAttribute("id")) {
1738
+ activeElement = ctx.target.querySelector(`[id="${activeElementId}"]`);
1745
1739
  activeElement?.focus();
1746
1740
  }
1747
1741
  if (activeElement && !activeElement.selectionEnd && selectionEnd) {
@@ -1768,11 +1762,14 @@ var Idiomorph = function() {
1768
1762
  continue;
1769
1763
  }
1770
1764
  }
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;
1765
+ if (newChild instanceof Element) {
1766
+ const newChildId = newChild.getAttribute("id");
1767
+ if (ctx.persistentIds.has(newChildId)) {
1768
+ const movedChild = moveBeforeById(oldParent, newChildId, insertionPoint, ctx);
1769
+ morphNode(movedChild, newChild, ctx);
1770
+ insertionPoint = movedChild.nextSibling;
1771
+ continue;
1772
+ }
1776
1773
  }
1777
1774
  const insertedNode = createNode(oldParent, newChild, insertionPoint, ctx);
1778
1775
  if (insertedNode) {
@@ -1824,7 +1821,7 @@ var Idiomorph = function() {
1824
1821
  softMatch = undefined;
1825
1822
  }
1826
1823
  }
1827
- if (cursor.contains(document.activeElement)) break;
1824
+ if (ctx.activeElementAndParents.includes(cursor)) break;
1828
1825
  cursor = cursor.nextSibling;
1829
1826
  }
1830
1827
  return softMatch || null;
@@ -1843,7 +1840,7 @@ var Idiomorph = function() {
1843
1840
  function isSoftMatch(oldNode, newNode) {
1844
1841
  const oldElt = oldNode;
1845
1842
  const newElt = newNode;
1846
- return oldElt.nodeType === newElt.nodeType && oldElt.tagName === newElt.tagName && (!oldElt.id || oldElt.id === newElt.id);
1843
+ return oldElt.nodeType === newElt.nodeType && oldElt.tagName === newElt.tagName && (!oldElt.getAttribute?.("id") || oldElt.getAttribute?.("id") === newElt.getAttribute?.("id"));
1847
1844
  }
1848
1845
  return findBestMatch;
1849
1846
  }();
@@ -1866,13 +1863,13 @@ var Idiomorph = function() {
1866
1863
  return cursor;
1867
1864
  }
1868
1865
  function moveBeforeById(parentNode, id, after, ctx) {
1869
- const target = ctx.target.querySelector(`#${id}`) || ctx.pantry.querySelector(`#${id}`);
1866
+ const target = ctx.target.getAttribute?.("id") === id && ctx.target || ctx.target.querySelector(`[id="${id}"]`) || ctx.pantry.querySelector(`[id="${id}"]`);
1870
1867
  removeElementFromAncestorsIdMaps(target, ctx);
1871
1868
  moveBefore(parentNode, target, after);
1872
1869
  return target;
1873
1870
  }
1874
1871
  function removeElementFromAncestorsIdMaps(element, ctx) {
1875
- const id = element.id;
1872
+ const id = element.getAttribute("id");
1876
1873
  while (element = element.parentNode) {
1877
1874
  let idSet = ctx.idMap.get(element);
1878
1875
  if (idSet) {
@@ -2116,6 +2113,7 @@ var Idiomorph = function() {
2116
2113
  idMap: idMap,
2117
2114
  persistentIds: persistentIds,
2118
2115
  pantry: createPantry(),
2116
+ activeElementAndParents: createActiveElementAndParents(oldNode),
2119
2117
  callbacks: mergedConfig.callbacks,
2120
2118
  head: mergedConfig.head
2121
2119
  };
@@ -2133,16 +2131,29 @@ var Idiomorph = function() {
2133
2131
  document.body.insertAdjacentElement("afterend", pantry);
2134
2132
  return pantry;
2135
2133
  }
2134
+ function createActiveElementAndParents(oldNode) {
2135
+ let activeElementAndParents = [];
2136
+ let elt = document.activeElement;
2137
+ if (elt?.tagName !== "BODY" && oldNode.contains(elt)) {
2138
+ while (elt) {
2139
+ activeElementAndParents.push(elt);
2140
+ if (elt === oldNode) break;
2141
+ elt = elt.parentElement;
2142
+ }
2143
+ }
2144
+ return activeElementAndParents;
2145
+ }
2136
2146
  function findIdElements(root) {
2137
2147
  let elements = Array.from(root.querySelectorAll("[id]"));
2138
- if (root.id) {
2148
+ if (root.getAttribute?.("id")) {
2139
2149
  elements.push(root);
2140
2150
  }
2141
2151
  return elements;
2142
2152
  }
2143
2153
  function populateIdMapWithTree(idMap, persistentIds, root, elements) {
2144
2154
  for (const elt of elements) {
2145
- if (persistentIds.has(elt.id)) {
2155
+ const id = elt.getAttribute("id");
2156
+ if (persistentIds.has(id)) {
2146
2157
  let current = elt;
2147
2158
  while (current) {
2148
2159
  let idSet = idMap.get(current);
@@ -2150,7 +2161,7 @@ var Idiomorph = function() {
2150
2161
  idSet = new Set;
2151
2162
  idMap.set(current, idSet);
2152
2163
  }
2153
- idSet.add(elt.id);
2164
+ idSet.add(id);
2154
2165
  if (current === root) break;
2155
2166
  current = current.parentElement;
2156
2167
  }
@@ -2213,7 +2224,7 @@ var Idiomorph = function() {
2213
2224
  return newContent;
2214
2225
  } else if (newContent instanceof Node) {
2215
2226
  if (newContent.parentNode) {
2216
- return createDuckTypedParent(newContent);
2227
+ return new SlicedParentNode(newContent);
2217
2228
  } else {
2218
2229
  const dummyParent = document.createElement("div");
2219
2230
  dummyParent.append(newContent);
@@ -2227,19 +2238,43 @@ var Idiomorph = function() {
2227
2238
  return dummyParent;
2228
2239
  }
2229
2240
  }
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
+ class SlicedParentNode {
2242
+ constructor(node) {
2243
+ this.originalNode = node;
2244
+ this.realParentNode = node.parentNode;
2245
+ this.previousSibling = node.previousSibling;
2246
+ this.nextSibling = node.nextSibling;
2247
+ }
2248
+ get childNodes() {
2249
+ const nodes = [];
2250
+ let cursor = this.previousSibling ? this.previousSibling.nextSibling : this.realParentNode.firstChild;
2251
+ while (cursor && cursor != this.nextSibling) {
2252
+ nodes.push(cursor);
2253
+ cursor = cursor.nextSibling;
2241
2254
  }
2242
- };
2255
+ return nodes;
2256
+ }
2257
+ querySelectorAll(selector) {
2258
+ return this.childNodes.reduce(((results, node) => {
2259
+ if (node instanceof Element) {
2260
+ if (node.matches(selector)) results.push(node);
2261
+ const nodeList = node.querySelectorAll(selector);
2262
+ for (let i = 0; i < nodeList.length; i++) {
2263
+ results.push(nodeList[i]);
2264
+ }
2265
+ }
2266
+ return results;
2267
+ }), []);
2268
+ }
2269
+ insertBefore(node, referenceNode) {
2270
+ return this.realParentNode.insertBefore(node, referenceNode);
2271
+ }
2272
+ moveBefore(node, referenceNode) {
2273
+ return this.realParentNode.moveBefore(node, referenceNode);
2274
+ }
2275
+ get __idiomorphRoot() {
2276
+ return this.originalNode;
2277
+ }
2243
2278
  }
2244
2279
  function parseContent(newContent) {
2245
2280
  let parser = new DOMParser;
@@ -2281,12 +2316,25 @@ function morphElements(currentElement, newElement, {callbacks: callbacks, ...opt
2281
2316
  });
2282
2317
  }
2283
2318
 
2284
- function morphChildren(currentElement, newElement) {
2319
+ function morphChildren(currentElement, newElement, options = {}) {
2285
2320
  morphElements(currentElement, newElement.childNodes, {
2321
+ ...options,
2286
2322
  morphStyle: "innerHTML"
2287
2323
  });
2288
2324
  }
2289
2325
 
2326
+ function shouldRefreshFrameWithMorphing(currentFrame, newFrame) {
2327
+ return currentFrame instanceof FrameElement && currentFrame.shouldReloadWithMorph && (!newFrame || areFramesCompatibleForRefreshing(currentFrame, newFrame)) && !currentFrame.closest("[data-turbo-permanent]");
2328
+ }
2329
+
2330
+ function areFramesCompatibleForRefreshing(currentFrame, newFrame) {
2331
+ return newFrame instanceof Element && newFrame.nodeName === "TURBO-FRAME" && currentFrame.id === newFrame.id && (!newFrame.getAttribute("src") || urlsAreEqual(currentFrame.src, newFrame.getAttribute("src")));
2332
+ }
2333
+
2334
+ function closestFrameReloadableWithMorphing(node) {
2335
+ return node.parentElement.closest("turbo-frame[src][refresh=morph]");
2336
+ }
2337
+
2290
2338
  class DefaultIdiomorphCallbacks {
2291
2339
  #beforeNodeMorphed;
2292
2340
  constructor({beforeNodeMorphed: beforeNodeMorphed} = {}) {
@@ -2344,7 +2392,17 @@ class MorphingFrameRenderer extends FrameRenderer {
2344
2392
  newElement: newElement
2345
2393
  }
2346
2394
  });
2347
- morphChildren(currentElement, newElement);
2395
+ morphChildren(currentElement, newElement, {
2396
+ callbacks: {
2397
+ beforeNodeMorphed: (node, newNode) => {
2398
+ if (shouldRefreshFrameWithMorphing(node, newNode) && closestFrameReloadableWithMorphing(node) === currentElement) {
2399
+ node.reload();
2400
+ return false;
2401
+ }
2402
+ return true;
2403
+ }
2404
+ }
2405
+ });
2348
2406
  }
2349
2407
  async preservingPermanentElements(callback) {
2350
2408
  return await callback();
@@ -2596,7 +2654,8 @@ class PageSnapshot extends Snapshot {
2596
2654
  return this.getSetting("visit-control") != "reload";
2597
2655
  }
2598
2656
  get prefersViewTransitions() {
2599
- return this.headSnapshot.getMetaValue("view-transition") === "same-origin";
2657
+ const viewTransitionEnabled = this.getSetting("view-transition") === "true" || this.headSnapshot.getMetaValue("view-transition") === "same-origin";
2658
+ return viewTransitionEnabled && !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
2600
2659
  }
2601
2660
  get shouldMorphPage() {
2602
2661
  return this.getSetting("refresh-method") === "morph";
@@ -3011,6 +3070,7 @@ class BrowserAdapter {
3011
3070
  }
3012
3071
  visitStarted(visit) {
3013
3072
  this.location = visit.location;
3073
+ this.redirectedToLocation = null;
3014
3074
  visit.loadCachedSnapshot();
3015
3075
  visit.issueRequest();
3016
3076
  visit.goToSamePageAnchor();
@@ -3025,6 +3085,9 @@ class BrowserAdapter {
3025
3085
  }
3026
3086
  visitRequestCompleted(visit) {
3027
3087
  visit.loadResponse();
3088
+ if (visit.response.redirected) {
3089
+ this.redirectedToLocation = visit.redirectedToLocation;
3090
+ }
3028
3091
  }
3029
3092
  visitRequestFailedWithStatusCode(visit, statusCode) {
3030
3093
  switch (statusCode) {
@@ -3095,7 +3158,7 @@ class BrowserAdapter {
3095
3158
  dispatch("turbo:reload", {
3096
3159
  detail: reason
3097
3160
  });
3098
- window.location.href = this.location?.toString() || window.location.href;
3161
+ window.location.href = (this.redirectedToLocation || this.location)?.toString() || window.location.href;
3099
3162
  }
3100
3163
  get navigator() {
3101
3164
  return this.session.navigator;
@@ -3338,6 +3401,7 @@ class LinkPrefetchObserver {
3338
3401
  if (this.delegate.canPrefetchRequestToLocation(link, location)) {
3339
3402
  this.#prefetchedLink = link;
3340
3403
  const fetchRequest = new FetchRequest(this, FetchMethod.get, location, new URLSearchParams, target);
3404
+ fetchRequest.fetchOptions.priority = "low";
3341
3405
  prefetchCache.setLater(location.toString(), fetchRequest, this.#cacheTtl);
3342
3406
  }
3343
3407
  }
@@ -3988,12 +4052,15 @@ class MorphingPageRenderer extends PageRenderer {
3988
4052
  static renderElement(currentElement, newElement) {
3989
4053
  morphElements(currentElement, newElement, {
3990
4054
  callbacks: {
3991
- beforeNodeMorphed: element => !canRefreshFrame(element)
4055
+ beforeNodeMorphed: (node, newNode) => {
4056
+ if (shouldRefreshFrameWithMorphing(node, newNode) && !closestFrameReloadableWithMorphing(node)) {
4057
+ node.reload();
4058
+ return false;
4059
+ }
4060
+ return true;
4061
+ }
3992
4062
  }
3993
4063
  });
3994
- for (const frame of currentElement.querySelectorAll("turbo-frame")) {
3995
- if (canRefreshFrame(frame)) frame.reload();
3996
- }
3997
4064
  dispatch("turbo:morph", {
3998
4065
  detail: {
3999
4066
  currentElement: currentElement,
@@ -4012,10 +4079,6 @@ class MorphingPageRenderer extends PageRenderer {
4012
4079
  }
4013
4080
  }
4014
4081
 
4015
- function canRefreshFrame(frame) {
4016
- return frame instanceof FrameElement && frame.src && frame.refresh === "morph" && !frame.closest("[data-turbo-permanent]");
4017
- }
4018
-
4019
4082
  class SnapshotCache {
4020
4083
  keys=[];
4021
4084
  snapshots={};
@@ -4616,6 +4679,14 @@ function setFormMode(mode) {
4616
4679
  config.forms.mode = mode;
4617
4680
  }
4618
4681
 
4682
+ function morphBodyElements(currentBody, newBody) {
4683
+ MorphingPageRenderer.renderElement(currentBody, newBody);
4684
+ }
4685
+
4686
+ function morphTurboFrameElements(currentFrame, newFrame) {
4687
+ MorphingFrameRenderer.renderElement(currentFrame, newFrame);
4688
+ }
4689
+
4619
4690
  var Turbo = Object.freeze({
4620
4691
  __proto__: null,
4621
4692
  navigator: navigator$1,
@@ -4635,7 +4706,11 @@ var Turbo = Object.freeze({
4635
4706
  clearCache: clearCache,
4636
4707
  setProgressBarDelay: setProgressBarDelay,
4637
4708
  setConfirmMethod: setConfirmMethod,
4638
- setFormMode: setFormMode
4709
+ setFormMode: setFormMode,
4710
+ morphBodyElements: morphBodyElements,
4711
+ morphTurboFrameElements: morphTurboFrameElements,
4712
+ morphChildren: morphChildren,
4713
+ morphElements: morphElements
4639
4714
  });
4640
4715
 
4641
4716
  class TurboFrameMissingError extends Error {}
@@ -5330,6 +5405,10 @@ var Turbo$1 = Object.freeze({
5330
5405
  fetchEnctypeFromString: fetchEnctypeFromString,
5331
5406
  fetchMethodFromString: fetchMethodFromString,
5332
5407
  isSafe: isSafe,
5408
+ morphBodyElements: morphBodyElements,
5409
+ morphChildren: morphChildren,
5410
+ morphElements: morphElements,
5411
+ morphTurboFrameElements: morphTurboFrameElements,
5333
5412
  navigator: navigator$1,
5334
5413
  registerAdapter: registerAdapter,
5335
5414
  renderStreamMessage: renderStreamMessage,