@hotwired/turbo 8.0.4 → 8.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- Turbo 8.0.4
2
+ Turbo 8.0.5
3
3
  Copyright © 2024 37signals LLC
4
4
  */
5
5
  /**
@@ -631,14 +631,18 @@ async function around(callback, reader) {
631
631
  return [before, after]
632
632
  }
633
633
 
634
- function doesNotTargetIFrame(anchor) {
635
- if (anchor.hasAttribute("target")) {
636
- for (const element of document.getElementsByName(anchor.target)) {
634
+ function doesNotTargetIFrame(name) {
635
+ if (name === "_blank") {
636
+ return false
637
+ } else if (name) {
638
+ for (const element of document.getElementsByName(name)) {
637
639
  if (element instanceof HTMLIFrameElement) return false
638
640
  }
639
- }
640
641
 
641
- return true
642
+ return true
643
+ } else {
644
+ return true
645
+ }
642
646
  }
643
647
 
644
648
  function findLinkFromClickTarget(target) {
@@ -744,7 +748,7 @@ class FetchRequest {
744
748
  this.fetchOptions = {
745
749
  credentials: "same-origin",
746
750
  redirect: "follow",
747
- method: method,
751
+ method: method.toUpperCase(),
748
752
  headers: { ...this.defaultHeaders },
749
753
  body: body,
750
754
  signal: this.abortSignal,
@@ -767,7 +771,7 @@ class FetchRequest {
767
771
 
768
772
  this.url = url;
769
773
  this.fetchOptions.body = body;
770
- this.fetchOptions.method = fetchMethod;
774
+ this.fetchOptions.method = fetchMethod.toUpperCase();
771
775
  }
772
776
 
773
777
  get headers() {
@@ -1398,17 +1402,9 @@ function submissionDoesNotDismissDialog(form, submitter) {
1398
1402
  }
1399
1403
 
1400
1404
  function submissionDoesNotTargetIFrame(form, submitter) {
1401
- if (submitter?.hasAttribute("formtarget") || form.hasAttribute("target")) {
1402
- const target = submitter?.getAttribute("formtarget") || form.target;
1403
-
1404
- for (const element of document.getElementsByName(target)) {
1405
- if (element instanceof HTMLIFrameElement) return false
1406
- }
1405
+ const target = submitter?.getAttribute("formtarget") || form.getAttribute("target");
1407
1406
 
1408
- return true
1409
- } else {
1410
- return true
1411
- }
1407
+ return doesNotTargetIFrame(target)
1412
1408
  }
1413
1409
 
1414
1410
  class View {
@@ -1561,7 +1557,7 @@ class LinkInterceptor {
1561
1557
  }
1562
1558
 
1563
1559
  clickBubbled = (event) => {
1564
- if (this.respondsToEventTarget(event.target)) {
1560
+ if (this.clickEventIsSignificant(event)) {
1565
1561
  this.clickEvent = event;
1566
1562
  } else {
1567
1563
  delete this.clickEvent;
@@ -1569,7 +1565,7 @@ class LinkInterceptor {
1569
1565
  }
1570
1566
 
1571
1567
  linkClicked = (event) => {
1572
- if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) {
1568
+ if (this.clickEvent && this.clickEventIsSignificant(event)) {
1573
1569
  if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url, event.detail.originalEvent)) {
1574
1570
  this.clickEvent.preventDefault();
1575
1571
  event.preventDefault();
@@ -1583,9 +1579,11 @@ class LinkInterceptor {
1583
1579
  delete this.clickEvent;
1584
1580
  }
1585
1581
 
1586
- respondsToEventTarget(target) {
1587
- const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
1588
- return element && element.closest("turbo-frame, html") == this.element
1582
+ clickEventIsSignificant(event) {
1583
+ const target = event.composed ? event.target?.parentElement : event.target;
1584
+ const element = findLinkFromClickTarget(target) || target;
1585
+
1586
+ return element instanceof Element && element.closest("turbo-frame, html") == this.element
1589
1587
  }
1590
1588
  }
1591
1589
 
@@ -1620,7 +1618,7 @@ class LinkClickObserver {
1620
1618
  if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {
1621
1619
  const target = (event.composedPath && event.composedPath()[0]) || event.target;
1622
1620
  const link = findLinkFromClickTarget(target);
1623
- if (link && doesNotTargetIFrame(link)) {
1621
+ if (link && doesNotTargetIFrame(link.target)) {
1624
1622
  const location = getLocationForLink(link);
1625
1623
  if (this.delegate.willFollowLinkToLocation(link, location, event)) {
1626
1624
  event.preventDefault();
@@ -1789,6 +1787,10 @@ class Renderer {
1789
1787
  return true
1790
1788
  }
1791
1789
 
1790
+ get shouldAutofocus() {
1791
+ return true
1792
+ }
1793
+
1792
1794
  get reloadReason() {
1793
1795
  return
1794
1796
  }
@@ -1813,9 +1815,11 @@ class Renderer {
1813
1815
  }
1814
1816
 
1815
1817
  focusFirstAutofocusableElement() {
1816
- const element = this.connectedSnapshot.firstAutofocusableElement;
1817
- if (element) {
1818
- element.focus();
1818
+ if (this.shouldAutofocus) {
1819
+ const element = this.connectedSnapshot.firstAutofocusableElement;
1820
+ if (element) {
1821
+ element.focus();
1822
+ }
1819
1823
  }
1820
1824
  }
1821
1825
 
@@ -3166,7 +3170,7 @@ class LinkPrefetchObserver {
3166
3170
  }
3167
3171
 
3168
3172
  #tryToUsePrefetchedRequest = (event) => {
3169
- if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "get") {
3173
+ if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "GET") {
3170
3174
  const cached = prefetchCache.get(event.detail.url.toString());
3171
3175
 
3172
3176
  if (cached) {
@@ -3383,6 +3387,7 @@ class Navigator {
3383
3387
 
3384
3388
  visitCompleted(visit) {
3385
3389
  this.delegate.visitCompleted(visit);
3390
+ delete this.currentVisit;
3386
3391
  }
3387
3392
 
3388
3393
  locationWithActionIsSamePage(location, action) {
@@ -4565,6 +4570,81 @@ var Idiomorph = (function () {
4565
4570
  }
4566
4571
  })();
4567
4572
 
4573
+ function morphElements(currentElement, newElement, { callbacks, ...options } = {}) {
4574
+ Idiomorph.morph(currentElement, newElement, {
4575
+ ...options,
4576
+ callbacks: new DefaultIdiomorphCallbacks(callbacks)
4577
+ });
4578
+ }
4579
+
4580
+ function morphChildren(currentElement, newElement) {
4581
+ morphElements(currentElement, newElement.children, {
4582
+ morphStyle: "innerHTML"
4583
+ });
4584
+ }
4585
+
4586
+ class DefaultIdiomorphCallbacks {
4587
+ #beforeNodeMorphed
4588
+
4589
+ constructor({ beforeNodeMorphed } = {}) {
4590
+ this.#beforeNodeMorphed = beforeNodeMorphed || (() => true);
4591
+ }
4592
+
4593
+ beforeNodeAdded = (node) => {
4594
+ return !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id))
4595
+ }
4596
+
4597
+ beforeNodeMorphed = (currentElement, newElement) => {
4598
+ if (currentElement instanceof Element) {
4599
+ if (!currentElement.hasAttribute("data-turbo-permanent") && this.#beforeNodeMorphed(currentElement, newElement)) {
4600
+ const event = dispatch("turbo:before-morph-element", {
4601
+ cancelable: true,
4602
+ target: currentElement,
4603
+ detail: { currentElement, newElement }
4604
+ });
4605
+
4606
+ return !event.defaultPrevented
4607
+ } else {
4608
+ return false
4609
+ }
4610
+ }
4611
+ }
4612
+
4613
+ beforeAttributeUpdated = (attributeName, target, mutationType) => {
4614
+ const event = dispatch("turbo:before-morph-attribute", {
4615
+ cancelable: true,
4616
+ target,
4617
+ detail: { attributeName, mutationType }
4618
+ });
4619
+
4620
+ return !event.defaultPrevented
4621
+ }
4622
+
4623
+ beforeNodeRemoved = (node) => {
4624
+ return this.beforeNodeMorphed(node)
4625
+ }
4626
+
4627
+ afterNodeMorphed = (currentElement, newElement) => {
4628
+ if (currentElement instanceof Element) {
4629
+ dispatch("turbo:morph-element", {
4630
+ target: currentElement,
4631
+ detail: { currentElement, newElement }
4632
+ });
4633
+ }
4634
+ }
4635
+ }
4636
+
4637
+ class MorphingFrameRenderer extends FrameRenderer {
4638
+ static renderElement(currentElement, newElement) {
4639
+ dispatch("turbo:before-frame-morph", {
4640
+ target: currentElement,
4641
+ detail: { currentElement, newElement }
4642
+ });
4643
+
4644
+ morphChildren(currentElement, newElement);
4645
+ }
4646
+ }
4647
+
4568
4648
  class PageRenderer extends Renderer {
4569
4649
  static renderElement(currentElement, newElement) {
4570
4650
  if (document.body && newElement instanceof HTMLBodyElement) {
@@ -4777,119 +4857,47 @@ class PageRenderer extends Renderer {
4777
4857
  }
4778
4858
  }
4779
4859
 
4780
- class MorphRenderer extends PageRenderer {
4781
- async render() {
4782
- if (this.willRender) await this.#morphBody();
4783
- }
4784
-
4785
- get renderMethod() {
4786
- return "morph"
4787
- }
4788
-
4789
- // Private
4790
-
4791
- async #morphBody() {
4792
- this.#morphElements(this.currentElement, this.newElement);
4793
- this.#reloadRemoteFrames();
4794
-
4795
- dispatch("turbo:morph", {
4796
- detail: {
4797
- currentElement: this.currentElement,
4798
- newElement: this.newElement
4799
- }
4800
- });
4801
- }
4802
-
4803
- #morphElements(currentElement, newElement, morphStyle = "outerHTML") {
4804
- this.isMorphingTurboFrame = this.#isFrameReloadedWithMorph(currentElement);
4805
-
4806
- Idiomorph.morph(currentElement, newElement, {
4807
- morphStyle: morphStyle,
4860
+ class MorphingPageRenderer extends PageRenderer {
4861
+ static renderElement(currentElement, newElement) {
4862
+ morphElements(currentElement, newElement, {
4808
4863
  callbacks: {
4809
- beforeNodeAdded: this.#shouldAddElement,
4810
- beforeNodeMorphed: this.#shouldMorphElement,
4811
- beforeAttributeUpdated: this.#shouldUpdateAttribute,
4812
- beforeNodeRemoved: this.#shouldRemoveElement,
4813
- afterNodeMorphed: this.#didMorphElement
4864
+ beforeNodeMorphed: element => !canRefreshFrame(element)
4814
4865
  }
4815
4866
  });
4816
- }
4817
4867
 
4818
- #shouldAddElement = (node) => {
4819
- return !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id))
4820
- }
4821
-
4822
- #shouldMorphElement = (oldNode, newNode) => {
4823
- if (oldNode instanceof HTMLElement) {
4824
- if (!oldNode.hasAttribute("data-turbo-permanent") && (this.isMorphingTurboFrame || !this.#isFrameReloadedWithMorph(oldNode))) {
4825
- const event = dispatch("turbo:before-morph-element", {
4826
- cancelable: true,
4827
- target: oldNode,
4828
- detail: {
4829
- newElement: newNode
4830
- }
4831
- });
4832
-
4833
- return !event.defaultPrevented
4834
- } else {
4835
- return false
4836
- }
4868
+ for (const frame of currentElement.querySelectorAll("turbo-frame")) {
4869
+ if (canRefreshFrame(frame)) refreshFrame(frame);
4837
4870
  }
4838
- }
4839
-
4840
- #shouldUpdateAttribute = (attributeName, target, mutationType) => {
4841
- const event = dispatch("turbo:before-morph-attribute", { cancelable: true, target, detail: { attributeName, mutationType } });
4842
4871
 
4843
- return !event.defaultPrevented
4872
+ dispatch("turbo:morph", { detail: { currentElement, newElement } });
4844
4873
  }
4845
4874
 
4846
- #didMorphElement = (oldNode, newNode) => {
4847
- if (newNode instanceof HTMLElement) {
4848
- dispatch("turbo:morph-element", {
4849
- target: oldNode,
4850
- detail: {
4851
- newElement: newNode
4852
- }
4853
- });
4854
- }
4855
- }
4856
-
4857
- #shouldRemoveElement = (node) => {
4858
- return this.#shouldMorphElement(node)
4875
+ async preservingPermanentElements(callback) {
4876
+ return await callback()
4859
4877
  }
4860
4878
 
4861
- #reloadRemoteFrames() {
4862
- this.#remoteFrames().forEach((frame) => {
4863
- if (this.#isFrameReloadedWithMorph(frame)) {
4864
- this.#renderFrameWithMorph(frame);
4865
- frame.reload();
4866
- }
4867
- });
4879
+ get renderMethod() {
4880
+ return "morph"
4868
4881
  }
4869
4882
 
4870
- #renderFrameWithMorph(frame) {
4871
- frame.addEventListener("turbo:before-frame-render", (event) => {
4872
- event.detail.render = this.#morphFrameUpdate;
4873
- }, { once: true });
4883
+ get shouldAutofocus() {
4884
+ return false
4874
4885
  }
4886
+ }
4875
4887
 
4876
- #morphFrameUpdate = (currentElement, newElement) => {
4877
- dispatch("turbo:before-frame-morph", {
4878
- target: currentElement,
4879
- detail: { currentElement, newElement }
4880
- });
4881
- this.#morphElements(currentElement, newElement.children, "innerHTML");
4882
- }
4888
+ function canRefreshFrame(frame) {
4889
+ return frame instanceof FrameElement &&
4890
+ frame.src &&
4891
+ frame.refresh === "morph" &&
4892
+ !frame.closest("[data-turbo-permanent]")
4893
+ }
4883
4894
 
4884
- #isFrameReloadedWithMorph(element) {
4885
- return element.src && element.refresh === "morph"
4886
- }
4895
+ function refreshFrame(frame) {
4896
+ frame.addEventListener("turbo:before-frame-render", ({ detail }) => {
4897
+ detail.render = MorphingFrameRenderer.renderElement;
4898
+ }, { once: true });
4887
4899
 
4888
- #remoteFrames() {
4889
- return Array.from(document.querySelectorAll('turbo-frame[src]')).filter(frame => {
4890
- return !frame.closest('[data-turbo-permanent]')
4891
- })
4892
- }
4900
+ frame.reload();
4893
4901
  }
4894
4902
 
4895
4903
  class SnapshotCache {
@@ -4958,9 +4966,9 @@ class PageView extends View {
4958
4966
 
4959
4967
  renderPage(snapshot, isPreview = false, willRender = true, visit) {
4960
4968
  const shouldMorphPage = this.isPageRefresh(visit) && this.snapshot.shouldMorphPage;
4961
- const rendererClass = shouldMorphPage ? MorphRenderer : PageRenderer;
4969
+ const rendererClass = shouldMorphPage ? MorphingPageRenderer : PageRenderer;
4962
4970
 
4963
- const renderer = new rendererClass(this.snapshot, snapshot, PageRenderer.renderElement, isPreview, willRender);
4971
+ const renderer = new rendererClass(this.snapshot, snapshot, rendererClass.renderElement, isPreview, willRender);
4964
4972
 
4965
4973
  if (!renderer.shouldRender) {
4966
4974
  this.forceReloaded = true;
@@ -5196,7 +5204,7 @@ class Session {
5196
5204
 
5197
5205
  refresh(url, requestId) {
5198
5206
  const isRecentRequest = requestId && this.recentRequests.has(requestId);
5199
- if (!isRecentRequest) {
5207
+ if (!isRecentRequest && !this.navigator.currentVisit) {
5200
5208
  this.visit(url, { action: "replace", shouldCacheSnapshot: false });
5201
5209
  }
5202
5210
  }
@@ -6286,13 +6294,27 @@ const StreamActions = {
6286
6294
  },
6287
6295
 
6288
6296
  replace() {
6289
- this.targetElements.forEach((e) => e.replaceWith(this.templateContent));
6297
+ const method = this.getAttribute("method");
6298
+
6299
+ this.targetElements.forEach((targetElement) => {
6300
+ if (method === "morph") {
6301
+ morphElements(targetElement, this.templateContent);
6302
+ } else {
6303
+ targetElement.replaceWith(this.templateContent);
6304
+ }
6305
+ });
6290
6306
  },
6291
6307
 
6292
6308
  update() {
6309
+ const method = this.getAttribute("method");
6310
+
6293
6311
  this.targetElements.forEach((targetElement) => {
6294
- targetElement.innerHTML = "";
6295
- targetElement.append(this.templateContent);
6312
+ if (method === "morph") {
6313
+ morphChildren(targetElement, this.templateContent);
6314
+ } else {
6315
+ targetElement.innerHTML = "";
6316
+ targetElement.append(this.templateContent);
6317
+ }
6296
6318
  });
6297
6319
  },
6298
6320
 
@@ -6306,20 +6328,22 @@ const StreamActions = {
6306
6328
  /**
6307
6329
  * Renders updates to the page from a stream of messages.
6308
6330
  *
6309
- * Using the `action` attribute, this can be configured one of four ways:
6331
+ * Using the `action` attribute, this can be configured one of eight ways:
6310
6332
  *
6311
- * - `append` - appends the result to the container
6312
- * - `prepend` - prepends the result to the container
6313
- * - `replace` - replaces the contents of the container
6314
- * - `remove` - removes the container
6315
- * - `before` - inserts the result before the target
6316
6333
  * - `after` - inserts the result after the target
6334
+ * - `append` - appends the result to the target
6335
+ * - `before` - inserts the result before the target
6336
+ * - `prepend` - prepends the result to the target
6337
+ * - `refresh` - initiates a page refresh
6338
+ * - `remove` - removes the target
6339
+ * - `replace` - replaces the outer HTML of the target
6340
+ * - `update` - replaces the inner HTML of the target
6317
6341
  *
6318
6342
  * @customElement turbo-stream
6319
6343
  * @example
6320
6344
  * <turbo-stream action="append" target="dom_id">
6321
6345
  * <template>
6322
- * Content to append to container designated with the dom_id.
6346
+ * Content to append to target designated with the dom_id.
6323
6347
  * </template>
6324
6348
  * </turbo-stream>
6325
6349
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- Turbo 8.0.4
2
+ Turbo 8.0.5
3
3
  Copyright © 2024 37signals LLC
4
4
  */
5
5
  (function (global, factory) {
@@ -637,14 +637,18 @@ Copyright © 2024 37signals LLC
637
637
  return [before, after]
638
638
  }
639
639
 
640
- function doesNotTargetIFrame(anchor) {
641
- if (anchor.hasAttribute("target")) {
642
- for (const element of document.getElementsByName(anchor.target)) {
640
+ function doesNotTargetIFrame(name) {
641
+ if (name === "_blank") {
642
+ return false
643
+ } else if (name) {
644
+ for (const element of document.getElementsByName(name)) {
643
645
  if (element instanceof HTMLIFrameElement) return false
644
646
  }
645
- }
646
647
 
647
- return true
648
+ return true
649
+ } else {
650
+ return true
651
+ }
648
652
  }
649
653
 
650
654
  function findLinkFromClickTarget(target) {
@@ -750,7 +754,7 @@ Copyright © 2024 37signals LLC
750
754
  this.fetchOptions = {
751
755
  credentials: "same-origin",
752
756
  redirect: "follow",
753
- method: method,
757
+ method: method.toUpperCase(),
754
758
  headers: { ...this.defaultHeaders },
755
759
  body: body,
756
760
  signal: this.abortSignal,
@@ -773,7 +777,7 @@ Copyright © 2024 37signals LLC
773
777
 
774
778
  this.url = url;
775
779
  this.fetchOptions.body = body;
776
- this.fetchOptions.method = fetchMethod;
780
+ this.fetchOptions.method = fetchMethod.toUpperCase();
777
781
  }
778
782
 
779
783
  get headers() {
@@ -1404,17 +1408,9 @@ Copyright © 2024 37signals LLC
1404
1408
  }
1405
1409
 
1406
1410
  function submissionDoesNotTargetIFrame(form, submitter) {
1407
- if (submitter?.hasAttribute("formtarget") || form.hasAttribute("target")) {
1408
- const target = submitter?.getAttribute("formtarget") || form.target;
1409
-
1410
- for (const element of document.getElementsByName(target)) {
1411
- if (element instanceof HTMLIFrameElement) return false
1412
- }
1411
+ const target = submitter?.getAttribute("formtarget") || form.getAttribute("target");
1413
1412
 
1414
- return true
1415
- } else {
1416
- return true
1417
- }
1413
+ return doesNotTargetIFrame(target)
1418
1414
  }
1419
1415
 
1420
1416
  class View {
@@ -1567,7 +1563,7 @@ Copyright © 2024 37signals LLC
1567
1563
  }
1568
1564
 
1569
1565
  clickBubbled = (event) => {
1570
- if (this.respondsToEventTarget(event.target)) {
1566
+ if (this.clickEventIsSignificant(event)) {
1571
1567
  this.clickEvent = event;
1572
1568
  } else {
1573
1569
  delete this.clickEvent;
@@ -1575,7 +1571,7 @@ Copyright © 2024 37signals LLC
1575
1571
  }
1576
1572
 
1577
1573
  linkClicked = (event) => {
1578
- if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) {
1574
+ if (this.clickEvent && this.clickEventIsSignificant(event)) {
1579
1575
  if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url, event.detail.originalEvent)) {
1580
1576
  this.clickEvent.preventDefault();
1581
1577
  event.preventDefault();
@@ -1589,9 +1585,11 @@ Copyright © 2024 37signals LLC
1589
1585
  delete this.clickEvent;
1590
1586
  }
1591
1587
 
1592
- respondsToEventTarget(target) {
1593
- const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
1594
- return element && element.closest("turbo-frame, html") == this.element
1588
+ clickEventIsSignificant(event) {
1589
+ const target = event.composed ? event.target?.parentElement : event.target;
1590
+ const element = findLinkFromClickTarget(target) || target;
1591
+
1592
+ return element instanceof Element && element.closest("turbo-frame, html") == this.element
1595
1593
  }
1596
1594
  }
1597
1595
 
@@ -1626,7 +1624,7 @@ Copyright © 2024 37signals LLC
1626
1624
  if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {
1627
1625
  const target = (event.composedPath && event.composedPath()[0]) || event.target;
1628
1626
  const link = findLinkFromClickTarget(target);
1629
- if (link && doesNotTargetIFrame(link)) {
1627
+ if (link && doesNotTargetIFrame(link.target)) {
1630
1628
  const location = getLocationForLink(link);
1631
1629
  if (this.delegate.willFollowLinkToLocation(link, location, event)) {
1632
1630
  event.preventDefault();
@@ -1795,6 +1793,10 @@ Copyright © 2024 37signals LLC
1795
1793
  return true
1796
1794
  }
1797
1795
 
1796
+ get shouldAutofocus() {
1797
+ return true
1798
+ }
1799
+
1798
1800
  get reloadReason() {
1799
1801
  return
1800
1802
  }
@@ -1819,9 +1821,11 @@ Copyright © 2024 37signals LLC
1819
1821
  }
1820
1822
 
1821
1823
  focusFirstAutofocusableElement() {
1822
- const element = this.connectedSnapshot.firstAutofocusableElement;
1823
- if (element) {
1824
- element.focus();
1824
+ if (this.shouldAutofocus) {
1825
+ const element = this.connectedSnapshot.firstAutofocusableElement;
1826
+ if (element) {
1827
+ element.focus();
1828
+ }
1825
1829
  }
1826
1830
  }
1827
1831
 
@@ -3172,7 +3176,7 @@ Copyright © 2024 37signals LLC
3172
3176
  }
3173
3177
 
3174
3178
  #tryToUsePrefetchedRequest = (event) => {
3175
- if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "get") {
3179
+ if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "GET") {
3176
3180
  const cached = prefetchCache.get(event.detail.url.toString());
3177
3181
 
3178
3182
  if (cached) {
@@ -3389,6 +3393,7 @@ Copyright © 2024 37signals LLC
3389
3393
 
3390
3394
  visitCompleted(visit) {
3391
3395
  this.delegate.visitCompleted(visit);
3396
+ delete this.currentVisit;
3392
3397
  }
3393
3398
 
3394
3399
  locationWithActionIsSamePage(location, action) {
@@ -4571,6 +4576,81 @@ Copyright © 2024 37signals LLC
4571
4576
  }
4572
4577
  })();
4573
4578
 
4579
+ function morphElements(currentElement, newElement, { callbacks, ...options } = {}) {
4580
+ Idiomorph.morph(currentElement, newElement, {
4581
+ ...options,
4582
+ callbacks: new DefaultIdiomorphCallbacks(callbacks)
4583
+ });
4584
+ }
4585
+
4586
+ function morphChildren(currentElement, newElement) {
4587
+ morphElements(currentElement, newElement.children, {
4588
+ morphStyle: "innerHTML"
4589
+ });
4590
+ }
4591
+
4592
+ class DefaultIdiomorphCallbacks {
4593
+ #beforeNodeMorphed
4594
+
4595
+ constructor({ beforeNodeMorphed } = {}) {
4596
+ this.#beforeNodeMorphed = beforeNodeMorphed || (() => true);
4597
+ }
4598
+
4599
+ beforeNodeAdded = (node) => {
4600
+ return !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id))
4601
+ }
4602
+
4603
+ beforeNodeMorphed = (currentElement, newElement) => {
4604
+ if (currentElement instanceof Element) {
4605
+ if (!currentElement.hasAttribute("data-turbo-permanent") && this.#beforeNodeMorphed(currentElement, newElement)) {
4606
+ const event = dispatch("turbo:before-morph-element", {
4607
+ cancelable: true,
4608
+ target: currentElement,
4609
+ detail: { currentElement, newElement }
4610
+ });
4611
+
4612
+ return !event.defaultPrevented
4613
+ } else {
4614
+ return false
4615
+ }
4616
+ }
4617
+ }
4618
+
4619
+ beforeAttributeUpdated = (attributeName, target, mutationType) => {
4620
+ const event = dispatch("turbo:before-morph-attribute", {
4621
+ cancelable: true,
4622
+ target,
4623
+ detail: { attributeName, mutationType }
4624
+ });
4625
+
4626
+ return !event.defaultPrevented
4627
+ }
4628
+
4629
+ beforeNodeRemoved = (node) => {
4630
+ return this.beforeNodeMorphed(node)
4631
+ }
4632
+
4633
+ afterNodeMorphed = (currentElement, newElement) => {
4634
+ if (currentElement instanceof Element) {
4635
+ dispatch("turbo:morph-element", {
4636
+ target: currentElement,
4637
+ detail: { currentElement, newElement }
4638
+ });
4639
+ }
4640
+ }
4641
+ }
4642
+
4643
+ class MorphingFrameRenderer extends FrameRenderer {
4644
+ static renderElement(currentElement, newElement) {
4645
+ dispatch("turbo:before-frame-morph", {
4646
+ target: currentElement,
4647
+ detail: { currentElement, newElement }
4648
+ });
4649
+
4650
+ morphChildren(currentElement, newElement);
4651
+ }
4652
+ }
4653
+
4574
4654
  class PageRenderer extends Renderer {
4575
4655
  static renderElement(currentElement, newElement) {
4576
4656
  if (document.body && newElement instanceof HTMLBodyElement) {
@@ -4783,119 +4863,47 @@ Copyright © 2024 37signals LLC
4783
4863
  }
4784
4864
  }
4785
4865
 
4786
- class MorphRenderer extends PageRenderer {
4787
- async render() {
4788
- if (this.willRender) await this.#morphBody();
4789
- }
4790
-
4791
- get renderMethod() {
4792
- return "morph"
4793
- }
4794
-
4795
- // Private
4796
-
4797
- async #morphBody() {
4798
- this.#morphElements(this.currentElement, this.newElement);
4799
- this.#reloadRemoteFrames();
4800
-
4801
- dispatch("turbo:morph", {
4802
- detail: {
4803
- currentElement: this.currentElement,
4804
- newElement: this.newElement
4805
- }
4806
- });
4807
- }
4808
-
4809
- #morphElements(currentElement, newElement, morphStyle = "outerHTML") {
4810
- this.isMorphingTurboFrame = this.#isFrameReloadedWithMorph(currentElement);
4811
-
4812
- Idiomorph.morph(currentElement, newElement, {
4813
- morphStyle: morphStyle,
4866
+ class MorphingPageRenderer extends PageRenderer {
4867
+ static renderElement(currentElement, newElement) {
4868
+ morphElements(currentElement, newElement, {
4814
4869
  callbacks: {
4815
- beforeNodeAdded: this.#shouldAddElement,
4816
- beforeNodeMorphed: this.#shouldMorphElement,
4817
- beforeAttributeUpdated: this.#shouldUpdateAttribute,
4818
- beforeNodeRemoved: this.#shouldRemoveElement,
4819
- afterNodeMorphed: this.#didMorphElement
4870
+ beforeNodeMorphed: element => !canRefreshFrame(element)
4820
4871
  }
4821
4872
  });
4822
- }
4823
4873
 
4824
- #shouldAddElement = (node) => {
4825
- return !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id))
4826
- }
4827
-
4828
- #shouldMorphElement = (oldNode, newNode) => {
4829
- if (oldNode instanceof HTMLElement) {
4830
- if (!oldNode.hasAttribute("data-turbo-permanent") && (this.isMorphingTurboFrame || !this.#isFrameReloadedWithMorph(oldNode))) {
4831
- const event = dispatch("turbo:before-morph-element", {
4832
- cancelable: true,
4833
- target: oldNode,
4834
- detail: {
4835
- newElement: newNode
4836
- }
4837
- });
4838
-
4839
- return !event.defaultPrevented
4840
- } else {
4841
- return false
4842
- }
4874
+ for (const frame of currentElement.querySelectorAll("turbo-frame")) {
4875
+ if (canRefreshFrame(frame)) refreshFrame(frame);
4843
4876
  }
4844
- }
4845
-
4846
- #shouldUpdateAttribute = (attributeName, target, mutationType) => {
4847
- const event = dispatch("turbo:before-morph-attribute", { cancelable: true, target, detail: { attributeName, mutationType } });
4848
4877
 
4849
- return !event.defaultPrevented
4878
+ dispatch("turbo:morph", { detail: { currentElement, newElement } });
4850
4879
  }
4851
4880
 
4852
- #didMorphElement = (oldNode, newNode) => {
4853
- if (newNode instanceof HTMLElement) {
4854
- dispatch("turbo:morph-element", {
4855
- target: oldNode,
4856
- detail: {
4857
- newElement: newNode
4858
- }
4859
- });
4860
- }
4861
- }
4862
-
4863
- #shouldRemoveElement = (node) => {
4864
- return this.#shouldMorphElement(node)
4881
+ async preservingPermanentElements(callback) {
4882
+ return await callback()
4865
4883
  }
4866
4884
 
4867
- #reloadRemoteFrames() {
4868
- this.#remoteFrames().forEach((frame) => {
4869
- if (this.#isFrameReloadedWithMorph(frame)) {
4870
- this.#renderFrameWithMorph(frame);
4871
- frame.reload();
4872
- }
4873
- });
4885
+ get renderMethod() {
4886
+ return "morph"
4874
4887
  }
4875
4888
 
4876
- #renderFrameWithMorph(frame) {
4877
- frame.addEventListener("turbo:before-frame-render", (event) => {
4878
- event.detail.render = this.#morphFrameUpdate;
4879
- }, { once: true });
4889
+ get shouldAutofocus() {
4890
+ return false
4880
4891
  }
4892
+ }
4881
4893
 
4882
- #morphFrameUpdate = (currentElement, newElement) => {
4883
- dispatch("turbo:before-frame-morph", {
4884
- target: currentElement,
4885
- detail: { currentElement, newElement }
4886
- });
4887
- this.#morphElements(currentElement, newElement.children, "innerHTML");
4888
- }
4894
+ function canRefreshFrame(frame) {
4895
+ return frame instanceof FrameElement &&
4896
+ frame.src &&
4897
+ frame.refresh === "morph" &&
4898
+ !frame.closest("[data-turbo-permanent]")
4899
+ }
4889
4900
 
4890
- #isFrameReloadedWithMorph(element) {
4891
- return element.src && element.refresh === "morph"
4892
- }
4901
+ function refreshFrame(frame) {
4902
+ frame.addEventListener("turbo:before-frame-render", ({ detail }) => {
4903
+ detail.render = MorphingFrameRenderer.renderElement;
4904
+ }, { once: true });
4893
4905
 
4894
- #remoteFrames() {
4895
- return Array.from(document.querySelectorAll('turbo-frame[src]')).filter(frame => {
4896
- return !frame.closest('[data-turbo-permanent]')
4897
- })
4898
- }
4906
+ frame.reload();
4899
4907
  }
4900
4908
 
4901
4909
  class SnapshotCache {
@@ -4964,9 +4972,9 @@ Copyright © 2024 37signals LLC
4964
4972
 
4965
4973
  renderPage(snapshot, isPreview = false, willRender = true, visit) {
4966
4974
  const shouldMorphPage = this.isPageRefresh(visit) && this.snapshot.shouldMorphPage;
4967
- const rendererClass = shouldMorphPage ? MorphRenderer : PageRenderer;
4975
+ const rendererClass = shouldMorphPage ? MorphingPageRenderer : PageRenderer;
4968
4976
 
4969
- const renderer = new rendererClass(this.snapshot, snapshot, PageRenderer.renderElement, isPreview, willRender);
4977
+ const renderer = new rendererClass(this.snapshot, snapshot, rendererClass.renderElement, isPreview, willRender);
4970
4978
 
4971
4979
  if (!renderer.shouldRender) {
4972
4980
  this.forceReloaded = true;
@@ -5202,7 +5210,7 @@ Copyright © 2024 37signals LLC
5202
5210
 
5203
5211
  refresh(url, requestId) {
5204
5212
  const isRecentRequest = requestId && this.recentRequests.has(requestId);
5205
- if (!isRecentRequest) {
5213
+ if (!isRecentRequest && !this.navigator.currentVisit) {
5206
5214
  this.visit(url, { action: "replace", shouldCacheSnapshot: false });
5207
5215
  }
5208
5216
  }
@@ -6292,13 +6300,27 @@ Copyright © 2024 37signals LLC
6292
6300
  },
6293
6301
 
6294
6302
  replace() {
6295
- this.targetElements.forEach((e) => e.replaceWith(this.templateContent));
6303
+ const method = this.getAttribute("method");
6304
+
6305
+ this.targetElements.forEach((targetElement) => {
6306
+ if (method === "morph") {
6307
+ morphElements(targetElement, this.templateContent);
6308
+ } else {
6309
+ targetElement.replaceWith(this.templateContent);
6310
+ }
6311
+ });
6296
6312
  },
6297
6313
 
6298
6314
  update() {
6315
+ const method = this.getAttribute("method");
6316
+
6299
6317
  this.targetElements.forEach((targetElement) => {
6300
- targetElement.innerHTML = "";
6301
- targetElement.append(this.templateContent);
6318
+ if (method === "morph") {
6319
+ morphChildren(targetElement, this.templateContent);
6320
+ } else {
6321
+ targetElement.innerHTML = "";
6322
+ targetElement.append(this.templateContent);
6323
+ }
6302
6324
  });
6303
6325
  },
6304
6326
 
@@ -6312,20 +6334,22 @@ Copyright © 2024 37signals LLC
6312
6334
  /**
6313
6335
  * Renders updates to the page from a stream of messages.
6314
6336
  *
6315
- * Using the `action` attribute, this can be configured one of four ways:
6337
+ * Using the `action` attribute, this can be configured one of eight ways:
6316
6338
  *
6317
- * - `append` - appends the result to the container
6318
- * - `prepend` - prepends the result to the container
6319
- * - `replace` - replaces the contents of the container
6320
- * - `remove` - removes the container
6321
- * - `before` - inserts the result before the target
6322
6339
  * - `after` - inserts the result after the target
6340
+ * - `append` - appends the result to the target
6341
+ * - `before` - inserts the result before the target
6342
+ * - `prepend` - prepends the result to the target
6343
+ * - `refresh` - initiates a page refresh
6344
+ * - `remove` - removes the target
6345
+ * - `replace` - replaces the outer HTML of the target
6346
+ * - `update` - replaces the inner HTML of the target
6323
6347
  *
6324
6348
  * @customElement turbo-stream
6325
6349
  * @example
6326
6350
  * <turbo-stream action="append" target="dom_id">
6327
6351
  * <template>
6328
- * Content to append to container designated with the dom_id.
6352
+ * Content to append to target designated with the dom_id.
6329
6353
  * </template>
6330
6354
  * </turbo-stream>
6331
6355
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotwired/turbo",
3
- "version": "8.0.4",
3
+ "version": "8.0.5",
4
4
  "description": "The speed of a single-page web application without having to write any JavaScript",
5
5
  "module": "dist/turbo.es2017-esm.js",
6
6
  "main": "dist/turbo.es2017-umd.js",