@hotwired/turbo 8.0.3 → 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.3
2
+ Turbo 8.0.5
3
3
  Copyright © 2024 37signals LLC
4
4
  */
5
5
  /**
@@ -126,7 +126,7 @@ class FrameElement extends HTMLElement {
126
126
  loaded = Promise.resolve()
127
127
 
128
128
  static get observedAttributes() {
129
- return ["disabled", "complete", "loading", "src"]
129
+ return ["disabled", "loading", "src"]
130
130
  }
131
131
 
132
132
  constructor() {
@@ -149,11 +149,9 @@ class FrameElement extends HTMLElement {
149
149
  attributeChangedCallback(name) {
150
150
  if (name == "loading") {
151
151
  this.delegate.loadingStyleChanged();
152
- } else if (name == "complete") {
153
- this.delegate.completeChanged();
154
152
  } else if (name == "src") {
155
153
  this.delegate.sourceURLChanged();
156
- } else {
154
+ } else if (name == "disabled") {
157
155
  this.delegate.disabledChanged();
158
156
  }
159
157
  }
@@ -633,14 +631,18 @@ async function around(callback, reader) {
633
631
  return [before, after]
634
632
  }
635
633
 
636
- function doesNotTargetIFrame(anchor) {
637
- if (anchor.hasAttribute("target")) {
638
- 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)) {
639
639
  if (element instanceof HTMLIFrameElement) return false
640
640
  }
641
- }
642
641
 
643
- return true
642
+ return true
643
+ } else {
644
+ return true
645
+ }
644
646
  }
645
647
 
646
648
  function findLinkFromClickTarget(target) {
@@ -746,7 +748,7 @@ class FetchRequest {
746
748
  this.fetchOptions = {
747
749
  credentials: "same-origin",
748
750
  redirect: "follow",
749
- method: method,
751
+ method: method.toUpperCase(),
750
752
  headers: { ...this.defaultHeaders },
751
753
  body: body,
752
754
  signal: this.abortSignal,
@@ -769,7 +771,7 @@ class FetchRequest {
769
771
 
770
772
  this.url = url;
771
773
  this.fetchOptions.body = body;
772
- this.fetchOptions.method = fetchMethod;
774
+ this.fetchOptions.method = fetchMethod.toUpperCase();
773
775
  }
774
776
 
775
777
  get headers() {
@@ -1400,17 +1402,9 @@ function submissionDoesNotDismissDialog(form, submitter) {
1400
1402
  }
1401
1403
 
1402
1404
  function submissionDoesNotTargetIFrame(form, submitter) {
1403
- if (submitter?.hasAttribute("formtarget") || form.hasAttribute("target")) {
1404
- const target = submitter?.getAttribute("formtarget") || form.target;
1405
-
1406
- for (const element of document.getElementsByName(target)) {
1407
- if (element instanceof HTMLIFrameElement) return false
1408
- }
1405
+ const target = submitter?.getAttribute("formtarget") || form.getAttribute("target");
1409
1406
 
1410
- return true
1411
- } else {
1412
- return true
1413
- }
1407
+ return doesNotTargetIFrame(target)
1414
1408
  }
1415
1409
 
1416
1410
  class View {
@@ -1563,7 +1557,7 @@ class LinkInterceptor {
1563
1557
  }
1564
1558
 
1565
1559
  clickBubbled = (event) => {
1566
- if (this.respondsToEventTarget(event.target)) {
1560
+ if (this.clickEventIsSignificant(event)) {
1567
1561
  this.clickEvent = event;
1568
1562
  } else {
1569
1563
  delete this.clickEvent;
@@ -1571,7 +1565,7 @@ class LinkInterceptor {
1571
1565
  }
1572
1566
 
1573
1567
  linkClicked = (event) => {
1574
- if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) {
1568
+ if (this.clickEvent && this.clickEventIsSignificant(event)) {
1575
1569
  if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url, event.detail.originalEvent)) {
1576
1570
  this.clickEvent.preventDefault();
1577
1571
  event.preventDefault();
@@ -1585,9 +1579,11 @@ class LinkInterceptor {
1585
1579
  delete this.clickEvent;
1586
1580
  }
1587
1581
 
1588
- respondsToEventTarget(target) {
1589
- const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
1590
- 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
1591
1587
  }
1592
1588
  }
1593
1589
 
@@ -1622,7 +1618,7 @@ class LinkClickObserver {
1622
1618
  if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {
1623
1619
  const target = (event.composedPath && event.composedPath()[0]) || event.target;
1624
1620
  const link = findLinkFromClickTarget(target);
1625
- if (link && doesNotTargetIFrame(link)) {
1621
+ if (link && doesNotTargetIFrame(link.target)) {
1626
1622
  const location = getLocationForLink(link);
1627
1623
  if (this.delegate.willFollowLinkToLocation(link, location, event)) {
1628
1624
  event.preventDefault();
@@ -1791,6 +1787,10 @@ class Renderer {
1791
1787
  return true
1792
1788
  }
1793
1789
 
1790
+ get shouldAutofocus() {
1791
+ return true
1792
+ }
1793
+
1794
1794
  get reloadReason() {
1795
1795
  return
1796
1796
  }
@@ -1815,9 +1815,11 @@ class Renderer {
1815
1815
  }
1816
1816
 
1817
1817
  focusFirstAutofocusableElement() {
1818
- const element = this.connectedSnapshot.firstAutofocusableElement;
1819
- if (element) {
1820
- element.focus();
1818
+ if (this.shouldAutofocus) {
1819
+ const element = this.connectedSnapshot.firstAutofocusableElement;
1820
+ if (element) {
1821
+ element.focus();
1822
+ }
1821
1823
  }
1822
1824
  }
1823
1825
 
@@ -3168,7 +3170,7 @@ class LinkPrefetchObserver {
3168
3170
  }
3169
3171
 
3170
3172
  #tryToUsePrefetchedRequest = (event) => {
3171
- if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "get") {
3173
+ if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "GET") {
3172
3174
  const cached = prefetchCache.get(event.detail.url.toString());
3173
3175
 
3174
3176
  if (cached) {
@@ -3385,6 +3387,7 @@ class Navigator {
3385
3387
 
3386
3388
  visitCompleted(visit) {
3387
3389
  this.delegate.visitCompleted(visit);
3390
+ delete this.currentVisit;
3388
3391
  }
3389
3392
 
3390
3393
  locationWithActionIsSamePage(location, action) {
@@ -4567,6 +4570,81 @@ var Idiomorph = (function () {
4567
4570
  }
4568
4571
  })();
4569
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
+
4570
4648
  class PageRenderer extends Renderer {
4571
4649
  static renderElement(currentElement, newElement) {
4572
4650
  if (document.body && newElement instanceof HTMLBodyElement) {
@@ -4779,119 +4857,47 @@ class PageRenderer extends Renderer {
4779
4857
  }
4780
4858
  }
4781
4859
 
4782
- class MorphRenderer extends PageRenderer {
4783
- async render() {
4784
- if (this.willRender) await this.#morphBody();
4785
- }
4786
-
4787
- get renderMethod() {
4788
- return "morph"
4789
- }
4790
-
4791
- // Private
4792
-
4793
- async #morphBody() {
4794
- this.#morphElements(this.currentElement, this.newElement);
4795
- this.#reloadRemoteFrames();
4796
-
4797
- dispatch("turbo:morph", {
4798
- detail: {
4799
- currentElement: this.currentElement,
4800
- newElement: this.newElement
4801
- }
4802
- });
4803
- }
4804
-
4805
- #morphElements(currentElement, newElement, morphStyle = "outerHTML") {
4806
- this.isMorphingTurboFrame = this.#isFrameReloadedWithMorph(currentElement);
4807
-
4808
- Idiomorph.morph(currentElement, newElement, {
4809
- morphStyle: morphStyle,
4860
+ class MorphingPageRenderer extends PageRenderer {
4861
+ static renderElement(currentElement, newElement) {
4862
+ morphElements(currentElement, newElement, {
4810
4863
  callbacks: {
4811
- beforeNodeAdded: this.#shouldAddElement,
4812
- beforeNodeMorphed: this.#shouldMorphElement,
4813
- beforeAttributeUpdated: this.#shouldUpdateAttribute,
4814
- beforeNodeRemoved: this.#shouldRemoveElement,
4815
- afterNodeMorphed: this.#didMorphElement
4864
+ beforeNodeMorphed: element => !canRefreshFrame(element)
4816
4865
  }
4817
4866
  });
4818
- }
4819
4867
 
4820
- #shouldAddElement = (node) => {
4821
- return !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id))
4822
- }
4823
-
4824
- #shouldMorphElement = (oldNode, newNode) => {
4825
- if (oldNode instanceof HTMLElement) {
4826
- if (!oldNode.hasAttribute("data-turbo-permanent") && (this.isMorphingTurboFrame || !this.#isFrameReloadedWithMorph(oldNode))) {
4827
- const event = dispatch("turbo:before-morph-element", {
4828
- cancelable: true,
4829
- target: oldNode,
4830
- detail: {
4831
- newElement: newNode
4832
- }
4833
- });
4834
-
4835
- return !event.defaultPrevented
4836
- } else {
4837
- return false
4838
- }
4868
+ for (const frame of currentElement.querySelectorAll("turbo-frame")) {
4869
+ if (canRefreshFrame(frame)) refreshFrame(frame);
4839
4870
  }
4840
- }
4841
-
4842
- #shouldUpdateAttribute = (attributeName, target, mutationType) => {
4843
- const event = dispatch("turbo:before-morph-attribute", { cancelable: true, target, detail: { attributeName, mutationType } });
4844
4871
 
4845
- return !event.defaultPrevented
4872
+ dispatch("turbo:morph", { detail: { currentElement, newElement } });
4846
4873
  }
4847
4874
 
4848
- #didMorphElement = (oldNode, newNode) => {
4849
- if (newNode instanceof HTMLElement) {
4850
- dispatch("turbo:morph-element", {
4851
- target: oldNode,
4852
- detail: {
4853
- newElement: newNode
4854
- }
4855
- });
4856
- }
4857
- }
4858
-
4859
- #shouldRemoveElement = (node) => {
4860
- return this.#shouldMorphElement(node)
4875
+ async preservingPermanentElements(callback) {
4876
+ return await callback()
4861
4877
  }
4862
4878
 
4863
- #reloadRemoteFrames() {
4864
- this.#remoteFrames().forEach((frame) => {
4865
- if (this.#isFrameReloadedWithMorph(frame)) {
4866
- this.#renderFrameWithMorph(frame);
4867
- frame.reload();
4868
- }
4869
- });
4879
+ get renderMethod() {
4880
+ return "morph"
4870
4881
  }
4871
4882
 
4872
- #renderFrameWithMorph(frame) {
4873
- frame.addEventListener("turbo:before-frame-render", (event) => {
4874
- event.detail.render = this.#morphFrameUpdate;
4875
- }, { once: true });
4883
+ get shouldAutofocus() {
4884
+ return false
4876
4885
  }
4886
+ }
4877
4887
 
4878
- #morphFrameUpdate = (currentElement, newElement) => {
4879
- dispatch("turbo:before-frame-morph", {
4880
- target: currentElement,
4881
- detail: { currentElement, newElement }
4882
- });
4883
- this.#morphElements(currentElement, newElement.children, "innerHTML");
4884
- }
4888
+ function canRefreshFrame(frame) {
4889
+ return frame instanceof FrameElement &&
4890
+ frame.src &&
4891
+ frame.refresh === "morph" &&
4892
+ !frame.closest("[data-turbo-permanent]")
4893
+ }
4885
4894
 
4886
- #isFrameReloadedWithMorph(element) {
4887
- return element.src && element.refresh === "morph"
4888
- }
4895
+ function refreshFrame(frame) {
4896
+ frame.addEventListener("turbo:before-frame-render", ({ detail }) => {
4897
+ detail.render = MorphingFrameRenderer.renderElement;
4898
+ }, { once: true });
4889
4899
 
4890
- #remoteFrames() {
4891
- return Array.from(document.querySelectorAll('turbo-frame[src]')).filter(frame => {
4892
- return !frame.closest('[data-turbo-permanent]')
4893
- })
4894
- }
4900
+ frame.reload();
4895
4901
  }
4896
4902
 
4897
4903
  class SnapshotCache {
@@ -4960,9 +4966,9 @@ class PageView extends View {
4960
4966
 
4961
4967
  renderPage(snapshot, isPreview = false, willRender = true, visit) {
4962
4968
  const shouldMorphPage = this.isPageRefresh(visit) && this.snapshot.shouldMorphPage;
4963
- const rendererClass = shouldMorphPage ? MorphRenderer : PageRenderer;
4969
+ const rendererClass = shouldMorphPage ? MorphingPageRenderer : PageRenderer;
4964
4970
 
4965
- const renderer = new rendererClass(this.snapshot, snapshot, PageRenderer.renderElement, isPreview, willRender);
4971
+ const renderer = new rendererClass(this.snapshot, snapshot, rendererClass.renderElement, isPreview, willRender);
4966
4972
 
4967
4973
  if (!renderer.shouldRender) {
4968
4974
  this.forceReloaded = true;
@@ -5198,7 +5204,7 @@ class Session {
5198
5204
 
5199
5205
  refresh(url, requestId) {
5200
5206
  const isRecentRequest = requestId && this.recentRequests.has(requestId);
5201
- if (!isRecentRequest) {
5207
+ if (!isRecentRequest && !this.navigator.currentVisit) {
5202
5208
  this.visit(url, { action: "replace", shouldCacheSnapshot: false });
5203
5209
  }
5204
5210
  }
@@ -5773,20 +5779,12 @@ class FrameController {
5773
5779
 
5774
5780
  sourceURLReloaded() {
5775
5781
  const { src } = this.element;
5776
- this.#ignoringChangesToAttribute("complete", () => {
5777
- this.element.removeAttribute("complete");
5778
- });
5782
+ this.element.removeAttribute("complete");
5779
5783
  this.element.src = null;
5780
5784
  this.element.src = src;
5781
5785
  return this.element.loaded
5782
5786
  }
5783
5787
 
5784
- completeChanged() {
5785
- if (this.#isIgnoringChangesTo("complete")) return
5786
-
5787
- this.#loadSourceURL();
5788
- }
5789
-
5790
5788
  loadingStyleChanged() {
5791
5789
  if (this.loadingStyle == FrameLoadingStyle.lazy) {
5792
5790
  this.appearanceObserver.start();
@@ -6211,13 +6209,11 @@ class FrameController {
6211
6209
  }
6212
6210
 
6213
6211
  set complete(value) {
6214
- this.#ignoringChangesToAttribute("complete", () => {
6215
- if (value) {
6216
- this.element.setAttribute("complete", "");
6217
- } else {
6218
- this.element.removeAttribute("complete");
6219
- }
6220
- });
6212
+ if (value) {
6213
+ this.element.setAttribute("complete", "");
6214
+ } else {
6215
+ this.element.removeAttribute("complete");
6216
+ }
6221
6217
  }
6222
6218
 
6223
6219
  get isActive() {
@@ -6298,13 +6294,27 @@ const StreamActions = {
6298
6294
  },
6299
6295
 
6300
6296
  replace() {
6301
- 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
+ });
6302
6306
  },
6303
6307
 
6304
6308
  update() {
6309
+ const method = this.getAttribute("method");
6310
+
6305
6311
  this.targetElements.forEach((targetElement) => {
6306
- targetElement.innerHTML = "";
6307
- targetElement.append(this.templateContent);
6312
+ if (method === "morph") {
6313
+ morphChildren(targetElement, this.templateContent);
6314
+ } else {
6315
+ targetElement.innerHTML = "";
6316
+ targetElement.append(this.templateContent);
6317
+ }
6308
6318
  });
6309
6319
  },
6310
6320
 
@@ -6318,20 +6328,22 @@ const StreamActions = {
6318
6328
  /**
6319
6329
  * Renders updates to the page from a stream of messages.
6320
6330
  *
6321
- * 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:
6322
6332
  *
6323
- * - `append` - appends the result to the container
6324
- * - `prepend` - prepends the result to the container
6325
- * - `replace` - replaces the contents of the container
6326
- * - `remove` - removes the container
6327
- * - `before` - inserts the result before the target
6328
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
6329
6341
  *
6330
6342
  * @customElement turbo-stream
6331
6343
  * @example
6332
6344
  * <turbo-stream action="append" target="dom_id">
6333
6345
  * <template>
6334
- * Content to append to container designated with the dom_id.
6346
+ * Content to append to target designated with the dom_id.
6335
6347
  * </template>
6336
6348
  * </turbo-stream>
6337
6349
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- Turbo 8.0.3
2
+ Turbo 8.0.5
3
3
  Copyright © 2024 37signals LLC
4
4
  */
5
5
  (function (global, factory) {
@@ -132,7 +132,7 @@ Copyright © 2024 37signals LLC
132
132
  loaded = Promise.resolve()
133
133
 
134
134
  static get observedAttributes() {
135
- return ["disabled", "complete", "loading", "src"]
135
+ return ["disabled", "loading", "src"]
136
136
  }
137
137
 
138
138
  constructor() {
@@ -155,11 +155,9 @@ Copyright © 2024 37signals LLC
155
155
  attributeChangedCallback(name) {
156
156
  if (name == "loading") {
157
157
  this.delegate.loadingStyleChanged();
158
- } else if (name == "complete") {
159
- this.delegate.completeChanged();
160
158
  } else if (name == "src") {
161
159
  this.delegate.sourceURLChanged();
162
- } else {
160
+ } else if (name == "disabled") {
163
161
  this.delegate.disabledChanged();
164
162
  }
165
163
  }
@@ -639,14 +637,18 @@ Copyright © 2024 37signals LLC
639
637
  return [before, after]
640
638
  }
641
639
 
642
- function doesNotTargetIFrame(anchor) {
643
- if (anchor.hasAttribute("target")) {
644
- 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)) {
645
645
  if (element instanceof HTMLIFrameElement) return false
646
646
  }
647
- }
648
647
 
649
- return true
648
+ return true
649
+ } else {
650
+ return true
651
+ }
650
652
  }
651
653
 
652
654
  function findLinkFromClickTarget(target) {
@@ -752,7 +754,7 @@ Copyright © 2024 37signals LLC
752
754
  this.fetchOptions = {
753
755
  credentials: "same-origin",
754
756
  redirect: "follow",
755
- method: method,
757
+ method: method.toUpperCase(),
756
758
  headers: { ...this.defaultHeaders },
757
759
  body: body,
758
760
  signal: this.abortSignal,
@@ -775,7 +777,7 @@ Copyright © 2024 37signals LLC
775
777
 
776
778
  this.url = url;
777
779
  this.fetchOptions.body = body;
778
- this.fetchOptions.method = fetchMethod;
780
+ this.fetchOptions.method = fetchMethod.toUpperCase();
779
781
  }
780
782
 
781
783
  get headers() {
@@ -1406,17 +1408,9 @@ Copyright © 2024 37signals LLC
1406
1408
  }
1407
1409
 
1408
1410
  function submissionDoesNotTargetIFrame(form, submitter) {
1409
- if (submitter?.hasAttribute("formtarget") || form.hasAttribute("target")) {
1410
- const target = submitter?.getAttribute("formtarget") || form.target;
1411
-
1412
- for (const element of document.getElementsByName(target)) {
1413
- if (element instanceof HTMLIFrameElement) return false
1414
- }
1411
+ const target = submitter?.getAttribute("formtarget") || form.getAttribute("target");
1415
1412
 
1416
- return true
1417
- } else {
1418
- return true
1419
- }
1413
+ return doesNotTargetIFrame(target)
1420
1414
  }
1421
1415
 
1422
1416
  class View {
@@ -1569,7 +1563,7 @@ Copyright © 2024 37signals LLC
1569
1563
  }
1570
1564
 
1571
1565
  clickBubbled = (event) => {
1572
- if (this.respondsToEventTarget(event.target)) {
1566
+ if (this.clickEventIsSignificant(event)) {
1573
1567
  this.clickEvent = event;
1574
1568
  } else {
1575
1569
  delete this.clickEvent;
@@ -1577,7 +1571,7 @@ Copyright © 2024 37signals LLC
1577
1571
  }
1578
1572
 
1579
1573
  linkClicked = (event) => {
1580
- if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) {
1574
+ if (this.clickEvent && this.clickEventIsSignificant(event)) {
1581
1575
  if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url, event.detail.originalEvent)) {
1582
1576
  this.clickEvent.preventDefault();
1583
1577
  event.preventDefault();
@@ -1591,9 +1585,11 @@ Copyright © 2024 37signals LLC
1591
1585
  delete this.clickEvent;
1592
1586
  }
1593
1587
 
1594
- respondsToEventTarget(target) {
1595
- const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
1596
- 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
1597
1593
  }
1598
1594
  }
1599
1595
 
@@ -1628,7 +1624,7 @@ Copyright © 2024 37signals LLC
1628
1624
  if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {
1629
1625
  const target = (event.composedPath && event.composedPath()[0]) || event.target;
1630
1626
  const link = findLinkFromClickTarget(target);
1631
- if (link && doesNotTargetIFrame(link)) {
1627
+ if (link && doesNotTargetIFrame(link.target)) {
1632
1628
  const location = getLocationForLink(link);
1633
1629
  if (this.delegate.willFollowLinkToLocation(link, location, event)) {
1634
1630
  event.preventDefault();
@@ -1797,6 +1793,10 @@ Copyright © 2024 37signals LLC
1797
1793
  return true
1798
1794
  }
1799
1795
 
1796
+ get shouldAutofocus() {
1797
+ return true
1798
+ }
1799
+
1800
1800
  get reloadReason() {
1801
1801
  return
1802
1802
  }
@@ -1821,9 +1821,11 @@ Copyright © 2024 37signals LLC
1821
1821
  }
1822
1822
 
1823
1823
  focusFirstAutofocusableElement() {
1824
- const element = this.connectedSnapshot.firstAutofocusableElement;
1825
- if (element) {
1826
- element.focus();
1824
+ if (this.shouldAutofocus) {
1825
+ const element = this.connectedSnapshot.firstAutofocusableElement;
1826
+ if (element) {
1827
+ element.focus();
1828
+ }
1827
1829
  }
1828
1830
  }
1829
1831
 
@@ -3174,7 +3176,7 @@ Copyright © 2024 37signals LLC
3174
3176
  }
3175
3177
 
3176
3178
  #tryToUsePrefetchedRequest = (event) => {
3177
- if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "get") {
3179
+ if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "GET") {
3178
3180
  const cached = prefetchCache.get(event.detail.url.toString());
3179
3181
 
3180
3182
  if (cached) {
@@ -3391,6 +3393,7 @@ Copyright © 2024 37signals LLC
3391
3393
 
3392
3394
  visitCompleted(visit) {
3393
3395
  this.delegate.visitCompleted(visit);
3396
+ delete this.currentVisit;
3394
3397
  }
3395
3398
 
3396
3399
  locationWithActionIsSamePage(location, action) {
@@ -4573,6 +4576,81 @@ Copyright © 2024 37signals LLC
4573
4576
  }
4574
4577
  })();
4575
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
+
4576
4654
  class PageRenderer extends Renderer {
4577
4655
  static renderElement(currentElement, newElement) {
4578
4656
  if (document.body && newElement instanceof HTMLBodyElement) {
@@ -4785,119 +4863,47 @@ Copyright © 2024 37signals LLC
4785
4863
  }
4786
4864
  }
4787
4865
 
4788
- class MorphRenderer extends PageRenderer {
4789
- async render() {
4790
- if (this.willRender) await this.#morphBody();
4791
- }
4792
-
4793
- get renderMethod() {
4794
- return "morph"
4795
- }
4796
-
4797
- // Private
4798
-
4799
- async #morphBody() {
4800
- this.#morphElements(this.currentElement, this.newElement);
4801
- this.#reloadRemoteFrames();
4802
-
4803
- dispatch("turbo:morph", {
4804
- detail: {
4805
- currentElement: this.currentElement,
4806
- newElement: this.newElement
4807
- }
4808
- });
4809
- }
4810
-
4811
- #morphElements(currentElement, newElement, morphStyle = "outerHTML") {
4812
- this.isMorphingTurboFrame = this.#isFrameReloadedWithMorph(currentElement);
4813
-
4814
- Idiomorph.morph(currentElement, newElement, {
4815
- morphStyle: morphStyle,
4866
+ class MorphingPageRenderer extends PageRenderer {
4867
+ static renderElement(currentElement, newElement) {
4868
+ morphElements(currentElement, newElement, {
4816
4869
  callbacks: {
4817
- beforeNodeAdded: this.#shouldAddElement,
4818
- beforeNodeMorphed: this.#shouldMorphElement,
4819
- beforeAttributeUpdated: this.#shouldUpdateAttribute,
4820
- beforeNodeRemoved: this.#shouldRemoveElement,
4821
- afterNodeMorphed: this.#didMorphElement
4870
+ beforeNodeMorphed: element => !canRefreshFrame(element)
4822
4871
  }
4823
4872
  });
4824
- }
4825
4873
 
4826
- #shouldAddElement = (node) => {
4827
- return !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id))
4828
- }
4829
-
4830
- #shouldMorphElement = (oldNode, newNode) => {
4831
- if (oldNode instanceof HTMLElement) {
4832
- if (!oldNode.hasAttribute("data-turbo-permanent") && (this.isMorphingTurboFrame || !this.#isFrameReloadedWithMorph(oldNode))) {
4833
- const event = dispatch("turbo:before-morph-element", {
4834
- cancelable: true,
4835
- target: oldNode,
4836
- detail: {
4837
- newElement: newNode
4838
- }
4839
- });
4840
-
4841
- return !event.defaultPrevented
4842
- } else {
4843
- return false
4844
- }
4874
+ for (const frame of currentElement.querySelectorAll("turbo-frame")) {
4875
+ if (canRefreshFrame(frame)) refreshFrame(frame);
4845
4876
  }
4846
- }
4847
-
4848
- #shouldUpdateAttribute = (attributeName, target, mutationType) => {
4849
- const event = dispatch("turbo:before-morph-attribute", { cancelable: true, target, detail: { attributeName, mutationType } });
4850
4877
 
4851
- return !event.defaultPrevented
4878
+ dispatch("turbo:morph", { detail: { currentElement, newElement } });
4852
4879
  }
4853
4880
 
4854
- #didMorphElement = (oldNode, newNode) => {
4855
- if (newNode instanceof HTMLElement) {
4856
- dispatch("turbo:morph-element", {
4857
- target: oldNode,
4858
- detail: {
4859
- newElement: newNode
4860
- }
4861
- });
4862
- }
4863
- }
4864
-
4865
- #shouldRemoveElement = (node) => {
4866
- return this.#shouldMorphElement(node)
4881
+ async preservingPermanentElements(callback) {
4882
+ return await callback()
4867
4883
  }
4868
4884
 
4869
- #reloadRemoteFrames() {
4870
- this.#remoteFrames().forEach((frame) => {
4871
- if (this.#isFrameReloadedWithMorph(frame)) {
4872
- this.#renderFrameWithMorph(frame);
4873
- frame.reload();
4874
- }
4875
- });
4885
+ get renderMethod() {
4886
+ return "morph"
4876
4887
  }
4877
4888
 
4878
- #renderFrameWithMorph(frame) {
4879
- frame.addEventListener("turbo:before-frame-render", (event) => {
4880
- event.detail.render = this.#morphFrameUpdate;
4881
- }, { once: true });
4889
+ get shouldAutofocus() {
4890
+ return false
4882
4891
  }
4892
+ }
4883
4893
 
4884
- #morphFrameUpdate = (currentElement, newElement) => {
4885
- dispatch("turbo:before-frame-morph", {
4886
- target: currentElement,
4887
- detail: { currentElement, newElement }
4888
- });
4889
- this.#morphElements(currentElement, newElement.children, "innerHTML");
4890
- }
4894
+ function canRefreshFrame(frame) {
4895
+ return frame instanceof FrameElement &&
4896
+ frame.src &&
4897
+ frame.refresh === "morph" &&
4898
+ !frame.closest("[data-turbo-permanent]")
4899
+ }
4891
4900
 
4892
- #isFrameReloadedWithMorph(element) {
4893
- return element.src && element.refresh === "morph"
4894
- }
4901
+ function refreshFrame(frame) {
4902
+ frame.addEventListener("turbo:before-frame-render", ({ detail }) => {
4903
+ detail.render = MorphingFrameRenderer.renderElement;
4904
+ }, { once: true });
4895
4905
 
4896
- #remoteFrames() {
4897
- return Array.from(document.querySelectorAll('turbo-frame[src]')).filter(frame => {
4898
- return !frame.closest('[data-turbo-permanent]')
4899
- })
4900
- }
4906
+ frame.reload();
4901
4907
  }
4902
4908
 
4903
4909
  class SnapshotCache {
@@ -4966,9 +4972,9 @@ Copyright © 2024 37signals LLC
4966
4972
 
4967
4973
  renderPage(snapshot, isPreview = false, willRender = true, visit) {
4968
4974
  const shouldMorphPage = this.isPageRefresh(visit) && this.snapshot.shouldMorphPage;
4969
- const rendererClass = shouldMorphPage ? MorphRenderer : PageRenderer;
4975
+ const rendererClass = shouldMorphPage ? MorphingPageRenderer : PageRenderer;
4970
4976
 
4971
- const renderer = new rendererClass(this.snapshot, snapshot, PageRenderer.renderElement, isPreview, willRender);
4977
+ const renderer = new rendererClass(this.snapshot, snapshot, rendererClass.renderElement, isPreview, willRender);
4972
4978
 
4973
4979
  if (!renderer.shouldRender) {
4974
4980
  this.forceReloaded = true;
@@ -5204,7 +5210,7 @@ Copyright © 2024 37signals LLC
5204
5210
 
5205
5211
  refresh(url, requestId) {
5206
5212
  const isRecentRequest = requestId && this.recentRequests.has(requestId);
5207
- if (!isRecentRequest) {
5213
+ if (!isRecentRequest && !this.navigator.currentVisit) {
5208
5214
  this.visit(url, { action: "replace", shouldCacheSnapshot: false });
5209
5215
  }
5210
5216
  }
@@ -5779,20 +5785,12 @@ Copyright © 2024 37signals LLC
5779
5785
 
5780
5786
  sourceURLReloaded() {
5781
5787
  const { src } = this.element;
5782
- this.#ignoringChangesToAttribute("complete", () => {
5783
- this.element.removeAttribute("complete");
5784
- });
5788
+ this.element.removeAttribute("complete");
5785
5789
  this.element.src = null;
5786
5790
  this.element.src = src;
5787
5791
  return this.element.loaded
5788
5792
  }
5789
5793
 
5790
- completeChanged() {
5791
- if (this.#isIgnoringChangesTo("complete")) return
5792
-
5793
- this.#loadSourceURL();
5794
- }
5795
-
5796
5794
  loadingStyleChanged() {
5797
5795
  if (this.loadingStyle == FrameLoadingStyle.lazy) {
5798
5796
  this.appearanceObserver.start();
@@ -6217,13 +6215,11 @@ Copyright © 2024 37signals LLC
6217
6215
  }
6218
6216
 
6219
6217
  set complete(value) {
6220
- this.#ignoringChangesToAttribute("complete", () => {
6221
- if (value) {
6222
- this.element.setAttribute("complete", "");
6223
- } else {
6224
- this.element.removeAttribute("complete");
6225
- }
6226
- });
6218
+ if (value) {
6219
+ this.element.setAttribute("complete", "");
6220
+ } else {
6221
+ this.element.removeAttribute("complete");
6222
+ }
6227
6223
  }
6228
6224
 
6229
6225
  get isActive() {
@@ -6304,13 +6300,27 @@ Copyright © 2024 37signals LLC
6304
6300
  },
6305
6301
 
6306
6302
  replace() {
6307
- 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
+ });
6308
6312
  },
6309
6313
 
6310
6314
  update() {
6315
+ const method = this.getAttribute("method");
6316
+
6311
6317
  this.targetElements.forEach((targetElement) => {
6312
- targetElement.innerHTML = "";
6313
- targetElement.append(this.templateContent);
6318
+ if (method === "morph") {
6319
+ morphChildren(targetElement, this.templateContent);
6320
+ } else {
6321
+ targetElement.innerHTML = "";
6322
+ targetElement.append(this.templateContent);
6323
+ }
6314
6324
  });
6315
6325
  },
6316
6326
 
@@ -6324,20 +6334,22 @@ Copyright © 2024 37signals LLC
6324
6334
  /**
6325
6335
  * Renders updates to the page from a stream of messages.
6326
6336
  *
6327
- * 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:
6328
6338
  *
6329
- * - `append` - appends the result to the container
6330
- * - `prepend` - prepends the result to the container
6331
- * - `replace` - replaces the contents of the container
6332
- * - `remove` - removes the container
6333
- * - `before` - inserts the result before the target
6334
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
6335
6347
  *
6336
6348
  * @customElement turbo-stream
6337
6349
  * @example
6338
6350
  * <turbo-stream action="append" target="dom_id">
6339
6351
  * <template>
6340
- * Content to append to container designated with the dom_id.
6352
+ * Content to append to target designated with the dom_id.
6341
6353
  * </template>
6342
6354
  * </turbo-stream>
6343
6355
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotwired/turbo",
3
- "version": "8.0.3",
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",