@hotwired/turbo 8.0.0-beta.3 → 8.0.0-rc.1

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.0-beta.3
2
+ Turbo 8.0.0-rc.1
3
3
  Copyright © 2024 37signals LLC
4
4
  */
5
5
  /**
@@ -1469,7 +1469,12 @@ class View {
1469
1469
  // Rendering
1470
1470
 
1471
1471
  async render(renderer) {
1472
- const { isPreview, shouldRender, newSnapshot: snapshot } = renderer;
1472
+ const { isPreview, shouldRender, willRender, newSnapshot: snapshot } = renderer;
1473
+
1474
+ // A workaround to ignore tracked element mismatch reloads when performing
1475
+ // a promoted Visit from a frame navigation
1476
+ const shouldInvalidate = willRender;
1477
+
1473
1478
  if (shouldRender) {
1474
1479
  try {
1475
1480
  this.renderPromise = new Promise((resolve) => (this.#resolveRenderPromise = resolve));
@@ -1477,7 +1482,7 @@ class View {
1477
1482
  await this.prepareToRenderSnapshot(renderer);
1478
1483
 
1479
1484
  const renderInterception = new Promise((resolve) => (this.#resolveInterceptionPromise = resolve));
1480
- const options = { resume: this.#resolveInterceptionPromise, render: this.renderer.renderElement };
1485
+ const options = { resume: this.#resolveInterceptionPromise, render: this.renderer.renderElement, renderMethod: this.renderer.renderMethod };
1481
1486
  const immediateRender = this.delegate.allowsImmediateRender(snapshot, options);
1482
1487
  if (!immediateRender) await renderInterception;
1483
1488
 
@@ -1490,7 +1495,7 @@ class View {
1490
1495
  this.#resolveRenderPromise(undefined);
1491
1496
  delete this.renderPromise;
1492
1497
  }
1493
- } else {
1498
+ } else if (shouldInvalidate) {
1494
1499
  this.invalidate(renderer.reloadReason);
1495
1500
  }
1496
1501
  }
@@ -1937,8 +1942,6 @@ function readScrollBehavior(value, defaultValue) {
1937
1942
  }
1938
1943
  }
1939
1944
 
1940
- const ProgressBarID = "turbo-progress-bar";
1941
-
1942
1945
  class ProgressBar {
1943
1946
  static animationDuration = 300 /*ms*/
1944
1947
 
@@ -2043,8 +2046,6 @@ class ProgressBar {
2043
2046
 
2044
2047
  createStylesheetElement() {
2045
2048
  const element = document.createElement("style");
2046
- element.id = ProgressBarID;
2047
- element.setAttribute("data-turbo-permanent", "");
2048
2049
  element.type = "text/css";
2049
2050
  element.textContent = ProgressBar.defaultCSS;
2050
2051
  if (this.cspNonce) {
@@ -2364,6 +2365,7 @@ class Visit {
2364
2365
  this.snapshotHTML = snapshotHTML;
2365
2366
  this.response = response;
2366
2367
  this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
2368
+ this.isPageRefresh = this.view.isPageRefresh(this);
2367
2369
  this.visitCachedSnapshot = visitCachedSnapshot;
2368
2370
  this.willRender = willRender;
2369
2371
  this.updateHistory = updateHistory;
@@ -2529,7 +2531,7 @@ class Visit {
2529
2531
  const isPreview = this.shouldIssueRequest();
2530
2532
  this.render(async () => {
2531
2533
  this.cacheSnapshot();
2532
- if (this.isSamePage) {
2534
+ if (this.isSamePage || this.isPageRefresh) {
2533
2535
  this.adapter.visitRendered(this);
2534
2536
  } else {
2535
2537
  if (this.view.renderPromise) await this.view.renderPromise;
@@ -3171,20 +3173,17 @@ class LinkPrefetchObserver {
3171
3173
  prepareRequest(request) {
3172
3174
  const link = request.target;
3173
3175
 
3174
- request.headers["Sec-Purpose"] = "prefetch";
3176
+ request.headers["X-Sec-Purpose"] = "prefetch";
3175
3177
 
3176
- if (link.dataset.turboFrame && link.dataset.turboFrame !== "_top") {
3177
- request.headers["Turbo-Frame"] = link.dataset.turboFrame;
3178
- } else if (link.dataset.turboFrame !== "_top") {
3179
- const turboFrame = link.closest("turbo-frame");
3178
+ const turboFrame = link.closest("turbo-frame");
3179
+ const turboFrameTarget = link.getAttribute("data-turbo-frame") || turboFrame?.getAttribute("target") || turboFrame?.id;
3180
3180
 
3181
- if (turboFrame) {
3182
- request.headers["Turbo-Frame"] = turboFrame.id;
3183
- }
3181
+ if (turboFrameTarget && turboFrameTarget !== "_top") {
3182
+ request.headers["Turbo-Frame"] = turboFrameTarget;
3184
3183
  }
3185
3184
 
3186
3185
  if (link.hasAttribute("data-turbo-stream")) {
3187
- request.acceptResponseType("text/vnd.turbo-stream.html");
3186
+ request.acceptResponseType(StreamMessage.contentType);
3188
3187
  }
3189
3188
  }
3190
3189
 
@@ -3209,7 +3208,7 @@ class LinkPrefetchObserver {
3209
3208
  #isPrefetchable(link) {
3210
3209
  const href = link.getAttribute("href");
3211
3210
 
3212
- if (!href || href === "#" || link.dataset.turbo === "false" || link.dataset.turboPrefetch === "false") {
3211
+ if (!href || href === "#" || link.getAttribute("data-turbo") === "false" || link.getAttribute("data-turbo-prefetch") === "false") {
3213
3212
  return false
3214
3213
  }
3215
3214
 
@@ -3225,7 +3224,8 @@ class LinkPrefetchObserver {
3225
3224
  return false
3226
3225
  }
3227
3226
 
3228
- if (link.dataset.turboMethod && link.dataset.turboMethod !== "get") {
3227
+ const turboMethod = link.getAttribute("data-turbo-method");
3228
+ if (turboMethod && turboMethod !== "get") {
3229
3229
  return false
3230
3230
  }
3231
3231
 
@@ -3233,13 +3233,9 @@ class LinkPrefetchObserver {
3233
3233
  return false
3234
3234
  }
3235
3235
 
3236
- if (link.pathname + link.search === document.location.pathname + document.location.search) {
3237
- return false
3238
- }
3239
-
3240
3236
  const turboPrefetchParent = findClosestRecursively(link, "[data-turbo-prefetch]");
3241
3237
 
3242
- if (turboPrefetchParent && turboPrefetchParent.dataset.turboPrefetch === "false") {
3238
+ if (turboPrefetchParent && turboPrefetchParent.getAttribute("data-turbo-prefetch") === "false") {
3243
3239
  return false
3244
3240
  }
3245
3241
 
@@ -4554,92 +4550,6 @@ var Idiomorph = (function () {
4554
4550
  }
4555
4551
  })();
4556
4552
 
4557
- class MorphRenderer extends Renderer {
4558
- async render() {
4559
- if (this.willRender) await this.#morphBody();
4560
- }
4561
-
4562
- get renderMethod() {
4563
- return "morph"
4564
- }
4565
-
4566
- // Private
4567
-
4568
- async #morphBody() {
4569
- this.#morphElements(this.currentElement, this.newElement);
4570
- this.#reloadRemoteFrames();
4571
-
4572
- dispatch("turbo:morph", {
4573
- detail: {
4574
- currentElement: this.currentElement,
4575
- newElement: this.newElement
4576
- }
4577
- });
4578
- }
4579
-
4580
- #morphElements(currentElement, newElement, morphStyle = "outerHTML") {
4581
- this.isMorphingTurboFrame = this.#isFrameReloadedWithMorph(currentElement);
4582
-
4583
- Idiomorph.morph(currentElement, newElement, {
4584
- morphStyle: morphStyle,
4585
- callbacks: {
4586
- beforeNodeAdded: this.#shouldAddElement,
4587
- beforeNodeMorphed: this.#shouldMorphElement,
4588
- beforeNodeRemoved: this.#shouldRemoveElement
4589
- }
4590
- });
4591
- }
4592
-
4593
- #shouldAddElement = (node) => {
4594
- return !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id))
4595
- }
4596
-
4597
- #shouldMorphElement = (oldNode, newNode) => {
4598
- if (oldNode instanceof HTMLElement) {
4599
- return !oldNode.hasAttribute("data-turbo-permanent") && (this.isMorphingTurboFrame || !this.#isFrameReloadedWithMorph(oldNode))
4600
- } else {
4601
- return true
4602
- }
4603
- }
4604
-
4605
- #shouldRemoveElement = (node) => {
4606
- return this.#shouldMorphElement(node)
4607
- }
4608
-
4609
- #reloadRemoteFrames() {
4610
- this.#remoteFrames().forEach((frame) => {
4611
- if (this.#isFrameReloadedWithMorph(frame)) {
4612
- this.#renderFrameWithMorph(frame);
4613
- frame.reload();
4614
- }
4615
- });
4616
- }
4617
-
4618
- #renderFrameWithMorph(frame) {
4619
- frame.addEventListener("turbo:before-frame-render", (event) => {
4620
- event.detail.render = this.#morphFrameUpdate;
4621
- }, { once: true });
4622
- }
4623
-
4624
- #morphFrameUpdate = (currentElement, newElement) => {
4625
- dispatch("turbo:before-frame-morph", {
4626
- target: currentElement,
4627
- detail: { currentElement, newElement }
4628
- });
4629
- this.#morphElements(currentElement, newElement.children, "innerHTML");
4630
- }
4631
-
4632
- #isFrameReloadedWithMorph(element) {
4633
- return element.src && element.refresh === "morph"
4634
- }
4635
-
4636
- #remoteFrames() {
4637
- return Array.from(document.querySelectorAll('turbo-frame[src]')).filter(frame => {
4638
- return !frame.closest('[data-turbo-permanent]')
4639
- })
4640
- }
4641
- }
4642
-
4643
4553
  class PageRenderer extends Renderer {
4644
4554
  static renderElement(currentElement, newElement) {
4645
4555
  if (document.body && newElement instanceof HTMLBodyElement) {
@@ -4717,7 +4627,7 @@ class PageRenderer extends Renderer {
4717
4627
  await newStylesheetElements;
4718
4628
 
4719
4629
  if (this.willRender) {
4720
- this.removeUnusedHeadStylesheetElements();
4630
+ this.removeUnusedDynamicStylesheetElements();
4721
4631
  }
4722
4632
  }
4723
4633
 
@@ -4750,8 +4660,8 @@ class PageRenderer extends Renderer {
4750
4660
  }
4751
4661
  }
4752
4662
 
4753
- removeUnusedHeadStylesheetElements() {
4754
- for (const element of this.unusedHeadStylesheetElements) {
4663
+ removeUnusedDynamicStylesheetElements() {
4664
+ for (const element of this.unusedDynamicStylesheetElements) {
4755
4665
  document.head.removeChild(element);
4756
4666
  }
4757
4667
  }
@@ -4821,13 +4731,9 @@ class PageRenderer extends Renderer {
4821
4731
  await this.renderElement(this.currentElement, this.newElement);
4822
4732
  }
4823
4733
 
4824
- get unusedHeadStylesheetElements() {
4734
+ get unusedDynamicStylesheetElements() {
4825
4735
  return this.oldHeadStylesheetElements.filter((element) => {
4826
- return !(element.hasAttribute("data-turbo-permanent") ||
4827
- // Trix dynamically adds styles to the head that we want to keep around which have a
4828
- // `data-tag-name` attribute. Long term we should moves those styles to Trix's CSS file
4829
- // but for now we'll just skip removing them
4830
- element.hasAttribute("data-tag-name"))
4736
+ return element.getAttribute("data-turbo-track") === "dynamic"
4831
4737
  })
4832
4738
  }
4833
4739
 
@@ -4856,6 +4762,122 @@ class PageRenderer extends Renderer {
4856
4762
  }
4857
4763
  }
4858
4764
 
4765
+ class MorphRenderer extends PageRenderer {
4766
+ async render() {
4767
+ if (this.willRender) await this.#morphBody();
4768
+ }
4769
+
4770
+ get renderMethod() {
4771
+ return "morph"
4772
+ }
4773
+
4774
+ // Private
4775
+
4776
+ async #morphBody() {
4777
+ this.#morphElements(this.currentElement, this.newElement);
4778
+ this.#reloadRemoteFrames();
4779
+
4780
+ dispatch("turbo:morph", {
4781
+ detail: {
4782
+ currentElement: this.currentElement,
4783
+ newElement: this.newElement
4784
+ }
4785
+ });
4786
+ }
4787
+
4788
+ #morphElements(currentElement, newElement, morphStyle = "outerHTML") {
4789
+ this.isMorphingTurboFrame = this.#isFrameReloadedWithMorph(currentElement);
4790
+
4791
+ Idiomorph.morph(currentElement, newElement, {
4792
+ ignoreActiveValue: true,
4793
+ morphStyle: morphStyle,
4794
+ callbacks: {
4795
+ beforeNodeAdded: this.#shouldAddElement,
4796
+ beforeNodeMorphed: this.#shouldMorphElement,
4797
+ beforeAttributeUpdated: this.#shouldUpdateAttribute,
4798
+ beforeNodeRemoved: this.#shouldRemoveElement,
4799
+ afterNodeMorphed: this.#didMorphElement
4800
+ }
4801
+ });
4802
+ }
4803
+
4804
+ #shouldAddElement = (node) => {
4805
+ return !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id))
4806
+ }
4807
+
4808
+ #shouldMorphElement = (oldNode, newNode) => {
4809
+ if (oldNode instanceof HTMLElement) {
4810
+ if (!oldNode.hasAttribute("data-turbo-permanent") && (this.isMorphingTurboFrame || !this.#isFrameReloadedWithMorph(oldNode))) {
4811
+ const event = dispatch("turbo:before-morph-element", {
4812
+ cancelable: true,
4813
+ target: oldNode,
4814
+ detail: {
4815
+ newElement: newNode
4816
+ }
4817
+ });
4818
+
4819
+ return !event.defaultPrevented
4820
+ } else {
4821
+ return false
4822
+ }
4823
+ }
4824
+ }
4825
+
4826
+ #shouldUpdateAttribute = (attributeName, target, mutationType) => {
4827
+ const event = dispatch("turbo:before-morph-attribute", { cancelable: true, target, detail: { attributeName, mutationType } });
4828
+
4829
+ return !event.defaultPrevented
4830
+ }
4831
+
4832
+ #didMorphElement = (oldNode, newNode) => {
4833
+ if (newNode instanceof HTMLElement) {
4834
+ dispatch("turbo:morph-element", {
4835
+ target: oldNode,
4836
+ detail: {
4837
+ newElement: newNode
4838
+ }
4839
+ });
4840
+ }
4841
+ }
4842
+
4843
+ #shouldRemoveElement = (node) => {
4844
+ return this.#shouldMorphElement(node)
4845
+ }
4846
+
4847
+ #reloadRemoteFrames() {
4848
+ this.#remoteFrames().forEach((frame) => {
4849
+ if (this.#isFrameReloadedWithMorph(frame)) {
4850
+ this.#renderFrameWithMorph(frame);
4851
+ frame.reload();
4852
+ }
4853
+ });
4854
+ }
4855
+
4856
+ #renderFrameWithMorph(frame) {
4857
+ frame.addEventListener("turbo:before-frame-render", (event) => {
4858
+ event.detail.render = this.#morphFrameUpdate;
4859
+ }, { once: true });
4860
+ }
4861
+
4862
+ #morphFrameUpdate = (currentElement, newElement) => {
4863
+ dispatch("turbo:before-frame-morph", {
4864
+ target: currentElement,
4865
+ detail: { currentElement, newElement }
4866
+ });
4867
+ this.#morphElements(currentElement, newElement.children, "innerHTML");
4868
+ }
4869
+
4870
+ #isFrameReloadedWithMorph(element) {
4871
+ return element.src && element.refresh === "morph"
4872
+ }
4873
+
4874
+ #remoteFrames() {
4875
+ return Array.from(document.querySelectorAll('turbo-frame[src]')).filter(frame => {
4876
+ return !frame.closest('[data-turbo-permanent]')
4877
+ })
4878
+ }
4879
+ }
4880
+
4859
4881
  class SnapshotCache {
4860
4882
  keys = []
4861
4883
  snapshots = {}
@@ -5015,7 +5037,7 @@ class Preloader {
5015
5037
  // Fetch request delegate
5016
5038
 
5017
5039
  prepareRequest(fetchRequest) {
5018
- fetchRequest.headers["Sec-Purpose"] = "prefetch";
5040
+ fetchRequest.headers["X-Sec-Purpose"] = "prefetch";
5019
5041
  }
5020
5042
 
5021
5043
  async requestSucceededWithResponse(fetchRequest, fetchResponse) {
@@ -5149,8 +5171,10 @@ class Session {
5149
5171
  const frameElement = options.frame ? document.getElementById(options.frame) : null;
5150
5172
 
5151
5173
  if (frameElement instanceof FrameElement) {
5174
+ const action = options.action || getVisitAction(frameElement);
5175
+
5176
+ frameElement.delegate.proposeVisitIfNavigatedWithAction(frameElement, action);
5152
5177
  frameElement.src = location.toString();
5153
- frameElement.loaded;
5154
5178
  } else {
5155
5179
  this.navigator.proposeVisit(expandURL(location), options);
5156
5180
  }
@@ -5791,7 +5815,7 @@ class FrameController {
5791
5815
  // Appearance observer delegate
5792
5816
 
5793
5817
  elementAppearedInViewport(element) {
5794
- this.proposeVisitIfNavigatedWithAction(element, element);
5818
+ this.proposeVisitIfNavigatedWithAction(element, getVisitAction(element));
5795
5819
  this.#loadSourceURL();
5796
5820
  }
5797
5821
 
@@ -5879,7 +5903,7 @@ class FrameController {
5879
5903
  formSubmissionSucceededWithResponse(formSubmission, response) {
5880
5904
  const frame = this.#findFrameElement(formSubmission.formElement, formSubmission.submitter);
5881
5905
 
5882
- frame.delegate.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
5906
+ frame.delegate.proposeVisitIfNavigatedWithAction(frame, getVisitAction(formSubmission.submitter, formSubmission.formElement, frame));
5883
5907
  frame.delegate.loadResponse(response);
5884
5908
 
5885
5909
  if (!formSubmission.isSafe) {
@@ -5984,15 +6008,15 @@ class FrameController {
5984
6008
  #navigateFrame(element, url, submitter) {
5985
6009
  const frame = this.#findFrameElement(element, submitter);
5986
6010
 
5987
- frame.delegate.proposeVisitIfNavigatedWithAction(frame, element, submitter);
6011
+ frame.delegate.proposeVisitIfNavigatedWithAction(frame, getVisitAction(submitter, element, frame));
5988
6012
 
5989
6013
  this.#withCurrentNavigationElement(element, () => {
5990
6014
  frame.src = url;
5991
6015
  });
5992
6016
  }
5993
6017
 
5994
- proposeVisitIfNavigatedWithAction(frame, element, submitter) {
5995
- this.action = getVisitAction(submitter, element, frame);
6018
+ proposeVisitIfNavigatedWithAction(frame, action = null) {
6019
+ this.action = action;
5996
6020
 
5997
6021
  if (this.action) {
5998
6022
  const pageSnapshot = PageSnapshot.fromElement(frame).clone();
@@ -1,5 +1,5 @@
1
1
  /*!
2
- Turbo 8.0.0-beta.3
2
+ Turbo 8.0.0-rc.1
3
3
  Copyright © 2024 37signals LLC
4
4
  */
5
5
  (function (global, factory) {
@@ -1475,7 +1475,12 @@ Copyright © 2024 37signals LLC
1475
1475
  // Rendering
1476
1476
 
1477
1477
  async render(renderer) {
1478
- const { isPreview, shouldRender, newSnapshot: snapshot } = renderer;
1478
+ const { isPreview, shouldRender, willRender, newSnapshot: snapshot } = renderer;
1479
+
1480
+ // A workaround to ignore tracked element mismatch reloads when performing
1481
+ // a promoted Visit from a frame navigation
1482
+ const shouldInvalidate = willRender;
1483
+
1479
1484
  if (shouldRender) {
1480
1485
  try {
1481
1486
  this.renderPromise = new Promise((resolve) => (this.#resolveRenderPromise = resolve));
@@ -1483,7 +1488,7 @@ Copyright © 2024 37signals LLC
1483
1488
  await this.prepareToRenderSnapshot(renderer);
1484
1489
 
1485
1490
  const renderInterception = new Promise((resolve) => (this.#resolveInterceptionPromise = resolve));
1486
- const options = { resume: this.#resolveInterceptionPromise, render: this.renderer.renderElement };
1491
+ const options = { resume: this.#resolveInterceptionPromise, render: this.renderer.renderElement, renderMethod: this.renderer.renderMethod };
1487
1492
  const immediateRender = this.delegate.allowsImmediateRender(snapshot, options);
1488
1493
  if (!immediateRender) await renderInterception;
1489
1494
 
@@ -1496,7 +1501,7 @@ Copyright © 2024 37signals LLC
1496
1501
  this.#resolveRenderPromise(undefined);
1497
1502
  delete this.renderPromise;
1498
1503
  }
1499
- } else {
1504
+ } else if (shouldInvalidate) {
1500
1505
  this.invalidate(renderer.reloadReason);
1501
1506
  }
1502
1507
  }
@@ -1943,8 +1948,6 @@ Copyright © 2024 37signals LLC
1943
1948
  }
1944
1949
  }
1945
1950
 
1946
- const ProgressBarID = "turbo-progress-bar";
1947
-
1948
1951
  class ProgressBar {
1949
1952
  static animationDuration = 300 /*ms*/
1950
1953
 
@@ -2049,8 +2052,6 @@ Copyright © 2024 37signals LLC
2049
2052
 
2050
2053
  createStylesheetElement() {
2051
2054
  const element = document.createElement("style");
2052
- element.id = ProgressBarID;
2053
- element.setAttribute("data-turbo-permanent", "");
2054
2055
  element.type = "text/css";
2055
2056
  element.textContent = ProgressBar.defaultCSS;
2056
2057
  if (this.cspNonce) {
@@ -2370,6 +2371,7 @@ Copyright © 2024 37signals LLC
2370
2371
  this.snapshotHTML = snapshotHTML;
2371
2372
  this.response = response;
2372
2373
  this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
2374
+ this.isPageRefresh = this.view.isPageRefresh(this);
2373
2375
  this.visitCachedSnapshot = visitCachedSnapshot;
2374
2376
  this.willRender = willRender;
2375
2377
  this.updateHistory = updateHistory;
@@ -2535,7 +2537,7 @@ Copyright © 2024 37signals LLC
2535
2537
  const isPreview = this.shouldIssueRequest();
2536
2538
  this.render(async () => {
2537
2539
  this.cacheSnapshot();
2538
- if (this.isSamePage) {
2540
+ if (this.isSamePage || this.isPageRefresh) {
2539
2541
  this.adapter.visitRendered(this);
2540
2542
  } else {
2541
2543
  if (this.view.renderPromise) await this.view.renderPromise;
@@ -3177,20 +3179,17 @@ Copyright © 2024 37signals LLC
3177
3179
  prepareRequest(request) {
3178
3180
  const link = request.target;
3179
3181
 
3180
- request.headers["Sec-Purpose"] = "prefetch";
3182
+ request.headers["X-Sec-Purpose"] = "prefetch";
3181
3183
 
3182
- if (link.dataset.turboFrame && link.dataset.turboFrame !== "_top") {
3183
- request.headers["Turbo-Frame"] = link.dataset.turboFrame;
3184
- } else if (link.dataset.turboFrame !== "_top") {
3185
- const turboFrame = link.closest("turbo-frame");
3184
+ const turboFrame = link.closest("turbo-frame");
3185
+ const turboFrameTarget = link.getAttribute("data-turbo-frame") || turboFrame?.getAttribute("target") || turboFrame?.id;
3186
3186
 
3187
- if (turboFrame) {
3188
- request.headers["Turbo-Frame"] = turboFrame.id;
3189
- }
3187
+ if (turboFrameTarget && turboFrameTarget !== "_top") {
3188
+ request.headers["Turbo-Frame"] = turboFrameTarget;
3190
3189
  }
3191
3190
 
3192
3191
  if (link.hasAttribute("data-turbo-stream")) {
3193
- request.acceptResponseType("text/vnd.turbo-stream.html");
3192
+ request.acceptResponseType(StreamMessage.contentType);
3194
3193
  }
3195
3194
  }
3196
3195
 
@@ -3215,7 +3214,7 @@ Copyright © 2024 37signals LLC
3215
3214
  #isPrefetchable(link) {
3216
3215
  const href = link.getAttribute("href");
3217
3216
 
3218
- if (!href || href === "#" || link.dataset.turbo === "false" || link.dataset.turboPrefetch === "false") {
3217
+ if (!href || href === "#" || link.getAttribute("data-turbo") === "false" || link.getAttribute("data-turbo-prefetch") === "false") {
3219
3218
  return false
3220
3219
  }
3221
3220
 
@@ -3231,7 +3230,8 @@ Copyright © 2024 37signals LLC
3231
3230
  return false
3232
3231
  }
3233
3232
 
3234
- if (link.dataset.turboMethod && link.dataset.turboMethod !== "get") {
3233
+ const turboMethod = link.getAttribute("data-turbo-method");
3234
+ if (turboMethod && turboMethod !== "get") {
3235
3235
  return false
3236
3236
  }
3237
3237
 
@@ -3239,13 +3239,9 @@ Copyright © 2024 37signals LLC
3239
3239
  return false
3240
3240
  }
3241
3241
 
3242
- if (link.pathname + link.search === document.location.pathname + document.location.search) {
3243
- return false
3244
- }
3245
-
3246
3242
  const turboPrefetchParent = findClosestRecursively(link, "[data-turbo-prefetch]");
3247
3243
 
3248
- if (turboPrefetchParent && turboPrefetchParent.dataset.turboPrefetch === "false") {
3244
+ if (turboPrefetchParent && turboPrefetchParent.getAttribute("data-turbo-prefetch") === "false") {
3249
3245
  return false
3250
3246
  }
3251
3247
 
@@ -4560,92 +4556,6 @@ Copyright © 2024 37signals LLC
4560
4556
  }
4561
4557
  })();
4562
4558
 
4563
- class MorphRenderer extends Renderer {
4564
- async render() {
4565
- if (this.willRender) await this.#morphBody();
4566
- }
4567
-
4568
- get renderMethod() {
4569
- return "morph"
4570
- }
4571
-
4572
- // Private
4573
-
4574
- async #morphBody() {
4575
- this.#morphElements(this.currentElement, this.newElement);
4576
- this.#reloadRemoteFrames();
4577
-
4578
- dispatch("turbo:morph", {
4579
- detail: {
4580
- currentElement: this.currentElement,
4581
- newElement: this.newElement
4582
- }
4583
- });
4584
- }
4585
-
4586
- #morphElements(currentElement, newElement, morphStyle = "outerHTML") {
4587
- this.isMorphingTurboFrame = this.#isFrameReloadedWithMorph(currentElement);
4588
-
4589
- Idiomorph.morph(currentElement, newElement, {
4590
- morphStyle: morphStyle,
4591
- callbacks: {
4592
- beforeNodeAdded: this.#shouldAddElement,
4593
- beforeNodeMorphed: this.#shouldMorphElement,
4594
- beforeNodeRemoved: this.#shouldRemoveElement
4595
- }
4596
- });
4597
- }
4598
-
4599
- #shouldAddElement = (node) => {
4600
- return !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id))
4601
- }
4602
-
4603
- #shouldMorphElement = (oldNode, newNode) => {
4604
- if (oldNode instanceof HTMLElement) {
4605
- return !oldNode.hasAttribute("data-turbo-permanent") && (this.isMorphingTurboFrame || !this.#isFrameReloadedWithMorph(oldNode))
4606
- } else {
4607
- return true
4608
- }
4609
- }
4610
-
4611
- #shouldRemoveElement = (node) => {
4612
- return this.#shouldMorphElement(node)
4613
- }
4614
-
4615
- #reloadRemoteFrames() {
4616
- this.#remoteFrames().forEach((frame) => {
4617
- if (this.#isFrameReloadedWithMorph(frame)) {
4618
- this.#renderFrameWithMorph(frame);
4619
- frame.reload();
4620
- }
4621
- });
4622
- }
4623
-
4624
- #renderFrameWithMorph(frame) {
4625
- frame.addEventListener("turbo:before-frame-render", (event) => {
4626
- event.detail.render = this.#morphFrameUpdate;
4627
- }, { once: true });
4628
- }
4629
-
4630
- #morphFrameUpdate = (currentElement, newElement) => {
4631
- dispatch("turbo:before-frame-morph", {
4632
- target: currentElement,
4633
- detail: { currentElement, newElement }
4634
- });
4635
- this.#morphElements(currentElement, newElement.children, "innerHTML");
4636
- }
4637
-
4638
- #isFrameReloadedWithMorph(element) {
4639
- return element.src && element.refresh === "morph"
4640
- }
4641
-
4642
- #remoteFrames() {
4643
- return Array.from(document.querySelectorAll('turbo-frame[src]')).filter(frame => {
4644
- return !frame.closest('[data-turbo-permanent]')
4645
- })
4646
- }
4647
- }
4648
-
4649
4559
  class PageRenderer extends Renderer {
4650
4560
  static renderElement(currentElement, newElement) {
4651
4561
  if (document.body && newElement instanceof HTMLBodyElement) {
@@ -4723,7 +4633,7 @@ Copyright © 2024 37signals LLC
4723
4633
  await newStylesheetElements;
4724
4634
 
4725
4635
  if (this.willRender) {
4726
- this.removeUnusedHeadStylesheetElements();
4636
+ this.removeUnusedDynamicStylesheetElements();
4727
4637
  }
4728
4638
  }
4729
4639
 
@@ -4756,8 +4666,8 @@ Copyright © 2024 37signals LLC
4756
4666
  }
4757
4667
  }
4758
4668
 
4759
- removeUnusedHeadStylesheetElements() {
4760
- for (const element of this.unusedHeadStylesheetElements) {
4669
+ removeUnusedDynamicStylesheetElements() {
4670
+ for (const element of this.unusedDynamicStylesheetElements) {
4761
4671
  document.head.removeChild(element);
4762
4672
  }
4763
4673
  }
@@ -4827,13 +4737,9 @@ Copyright © 2024 37signals LLC
4827
4737
  await this.renderElement(this.currentElement, this.newElement);
4828
4738
  }
4829
4739
 
4830
- get unusedHeadStylesheetElements() {
4740
+ get unusedDynamicStylesheetElements() {
4831
4741
  return this.oldHeadStylesheetElements.filter((element) => {
4832
- return !(element.hasAttribute("data-turbo-permanent") ||
4833
- // Trix dynamically adds styles to the head that we want to keep around which have a
4834
- // `data-tag-name` attribute. Long term we should moves those styles to Trix's CSS file
4835
- // but for now we'll just skip removing them
4836
- element.hasAttribute("data-tag-name"))
4742
+ return element.getAttribute("data-turbo-track") === "dynamic"
4837
4743
  })
4838
4744
  }
4839
4745
 
@@ -4862,6 +4768,122 @@ Copyright © 2024 37signals LLC
4862
4768
  }
4863
4769
  }
4864
4770
 
4771
+ class MorphRenderer extends PageRenderer {
4772
+ async render() {
4773
+ if (this.willRender) await this.#morphBody();
4774
+ }
4775
+
4776
+ get renderMethod() {
4777
+ return "morph"
4778
+ }
4779
+
4780
+ // Private
4781
+
4782
+ async #morphBody() {
4783
+ this.#morphElements(this.currentElement, this.newElement);
4784
+ this.#reloadRemoteFrames();
4785
+
4786
+ dispatch("turbo:morph", {
4787
+ detail: {
4788
+ currentElement: this.currentElement,
4789
+ newElement: this.newElement
4790
+ }
4791
+ });
4792
+ }
4793
+
4794
+ #morphElements(currentElement, newElement, morphStyle = "outerHTML") {
4795
+ this.isMorphingTurboFrame = this.#isFrameReloadedWithMorph(currentElement);
4796
+
4797
+ Idiomorph.morph(currentElement, newElement, {
4798
+ ignoreActiveValue: true,
4799
+ morphStyle: morphStyle,
4800
+ callbacks: {
4801
+ beforeNodeAdded: this.#shouldAddElement,
4802
+ beforeNodeMorphed: this.#shouldMorphElement,
4803
+ beforeAttributeUpdated: this.#shouldUpdateAttribute,
4804
+ beforeNodeRemoved: this.#shouldRemoveElement,
4805
+ afterNodeMorphed: this.#didMorphElement
4806
+ }
4807
+ });
4808
+ }
4809
+
4810
+ #shouldAddElement = (node) => {
4811
+ return !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id))
4812
+ }
4813
+
4814
+ #shouldMorphElement = (oldNode, newNode) => {
4815
+ if (oldNode instanceof HTMLElement) {
4816
+ if (!oldNode.hasAttribute("data-turbo-permanent") && (this.isMorphingTurboFrame || !this.#isFrameReloadedWithMorph(oldNode))) {
4817
+ const event = dispatch("turbo:before-morph-element", {
4818
+ cancelable: true,
4819
+ target: oldNode,
4820
+ detail: {
4821
+ newElement: newNode
4822
+ }
4823
+ });
4824
+
4825
+ return !event.defaultPrevented
4826
+ } else {
4827
+ return false
4828
+ }
4829
+ }
4830
+ }
4831
+
4832
+ #shouldUpdateAttribute = (attributeName, target, mutationType) => {
4833
+ const event = dispatch("turbo:before-morph-attribute", { cancelable: true, target, detail: { attributeName, mutationType } });
4834
+
4835
+ return !event.defaultPrevented
4836
+ }
4837
+
4838
+ #didMorphElement = (oldNode, newNode) => {
4839
+ if (newNode instanceof HTMLElement) {
4840
+ dispatch("turbo:morph-element", {
4841
+ target: oldNode,
4842
+ detail: {
4843
+ newElement: newNode
4844
+ }
4845
+ });
4846
+ }
4847
+ }
4848
+
4849
+ #shouldRemoveElement = (node) => {
4850
+ return this.#shouldMorphElement(node)
4851
+ }
4852
+
4853
+ #reloadRemoteFrames() {
4854
+ this.#remoteFrames().forEach((frame) => {
4855
+ if (this.#isFrameReloadedWithMorph(frame)) {
4856
+ this.#renderFrameWithMorph(frame);
4857
+ frame.reload();
4858
+ }
4859
+ });
4860
+ }
4861
+
4862
+ #renderFrameWithMorph(frame) {
4863
+ frame.addEventListener("turbo:before-frame-render", (event) => {
4864
+ event.detail.render = this.#morphFrameUpdate;
4865
+ }, { once: true });
4866
+ }
4867
+
4868
+ #morphFrameUpdate = (currentElement, newElement) => {
4869
+ dispatch("turbo:before-frame-morph", {
4870
+ target: currentElement,
4871
+ detail: { currentElement, newElement }
4872
+ });
4873
+ this.#morphElements(currentElement, newElement.children, "innerHTML");
4874
+ }
4875
+
4876
+ #isFrameReloadedWithMorph(element) {
4877
+ return element.src && element.refresh === "morph"
4878
+ }
4879
+
4880
+ #remoteFrames() {
4881
+ return Array.from(document.querySelectorAll('turbo-frame[src]')).filter(frame => {
4882
+ return !frame.closest('[data-turbo-permanent]')
4883
+ })
4884
+ }
4885
+ }
4886
+
4865
4887
  class SnapshotCache {
4866
4888
  keys = []
4867
4889
  snapshots = {}
@@ -5021,7 +5043,7 @@ Copyright © 2024 37signals LLC
5021
5043
  // Fetch request delegate
5022
5044
 
5023
5045
  prepareRequest(fetchRequest) {
5024
- fetchRequest.headers["Sec-Purpose"] = "prefetch";
5046
+ fetchRequest.headers["X-Sec-Purpose"] = "prefetch";
5025
5047
  }
5026
5048
 
5027
5049
  async requestSucceededWithResponse(fetchRequest, fetchResponse) {
@@ -5155,8 +5177,10 @@ Copyright © 2024 37signals LLC
5155
5177
  const frameElement = options.frame ? document.getElementById(options.frame) : null;
5156
5178
 
5157
5179
  if (frameElement instanceof FrameElement) {
5180
+ const action = options.action || getVisitAction(frameElement);
5181
+
5182
+ frameElement.delegate.proposeVisitIfNavigatedWithAction(frameElement, action);
5158
5183
  frameElement.src = location.toString();
5159
- frameElement.loaded;
5160
5184
  } else {
5161
5185
  this.navigator.proposeVisit(expandURL(location), options);
5162
5186
  }
@@ -5797,7 +5821,7 @@ Copyright © 2024 37signals LLC
5797
5821
  // Appearance observer delegate
5798
5822
 
5799
5823
  elementAppearedInViewport(element) {
5800
- this.proposeVisitIfNavigatedWithAction(element, element);
5824
+ this.proposeVisitIfNavigatedWithAction(element, getVisitAction(element));
5801
5825
  this.#loadSourceURL();
5802
5826
  }
5803
5827
 
@@ -5885,7 +5909,7 @@ Copyright © 2024 37signals LLC
5885
5909
  formSubmissionSucceededWithResponse(formSubmission, response) {
5886
5910
  const frame = this.#findFrameElement(formSubmission.formElement, formSubmission.submitter);
5887
5911
 
5888
- frame.delegate.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
5912
+ frame.delegate.proposeVisitIfNavigatedWithAction(frame, getVisitAction(formSubmission.submitter, formSubmission.formElement, frame));
5889
5913
  frame.delegate.loadResponse(response);
5890
5914
 
5891
5915
  if (!formSubmission.isSafe) {
@@ -5990,15 +6014,15 @@ Copyright © 2024 37signals LLC
5990
6014
  #navigateFrame(element, url, submitter) {
5991
6015
  const frame = this.#findFrameElement(element, submitter);
5992
6016
 
5993
- frame.delegate.proposeVisitIfNavigatedWithAction(frame, element, submitter);
6017
+ frame.delegate.proposeVisitIfNavigatedWithAction(frame, getVisitAction(submitter, element, frame));
5994
6018
 
5995
6019
  this.#withCurrentNavigationElement(element, () => {
5996
6020
  frame.src = url;
5997
6021
  });
5998
6022
  }
5999
6023
 
6000
- proposeVisitIfNavigatedWithAction(frame, element, submitter) {
6001
- this.action = getVisitAction(submitter, element, frame);
6024
+ proposeVisitIfNavigatedWithAction(frame, action = null) {
6025
+ this.action = action;
6002
6026
 
6003
6027
  if (this.action) {
6004
6028
  const pageSnapshot = PageSnapshot.fromElement(frame).clone();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotwired/turbo",
3
- "version": "8.0.0-beta.3",
3
+ "version": "8.0.0-rc.1",
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",
@@ -44,7 +44,7 @@
44
44
  "chai": "~4.3.4",
45
45
  "eslint": "^8.13.0",
46
46
  "express": "^4.18.2",
47
- "idiomorph": "^0.3.0",
47
+ "idiomorph": "https://github.com/bigskysoftware/idiomorph.git",
48
48
  "multer": "^1.4.2",
49
49
  "rollup": "^2.35.1"
50
50
  },