@hotwired/turbo 8.0.0-beta.4 → 8.0.0-rc.2

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.4
2
+ Turbo 8.0.0-rc.2
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
  }
@@ -2360,6 +2365,7 @@ class Visit {
2360
2365
  this.snapshotHTML = snapshotHTML;
2361
2366
  this.response = response;
2362
2367
  this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
2368
+ this.isPageRefresh = this.view.isPageRefresh(this);
2363
2369
  this.visitCachedSnapshot = visitCachedSnapshot;
2364
2370
  this.willRender = willRender;
2365
2371
  this.updateHistory = updateHistory;
@@ -2525,7 +2531,7 @@ class Visit {
2525
2531
  const isPreview = this.shouldIssueRequest();
2526
2532
  this.render(async () => {
2527
2533
  this.cacheSnapshot();
2528
- if (this.isSamePage) {
2534
+ if (this.isSamePage || this.isPageRefresh) {
2529
2535
  this.adapter.visitRendered(this);
2530
2536
  } else {
2531
2537
  if (this.view.renderPromise) await this.view.renderPromise;
@@ -3167,7 +3173,7 @@ class LinkPrefetchObserver {
3167
3173
  prepareRequest(request) {
3168
3174
  const link = request.target;
3169
3175
 
3170
- request.headers["Sec-Purpose"] = "prefetch";
3176
+ request.headers["X-Sec-Purpose"] = "prefetch";
3171
3177
 
3172
3178
  const turboFrame = link.closest("turbo-frame");
3173
3179
  const turboFrameTarget = link.getAttribute("data-turbo-frame") || turboFrame?.getAttribute("target") || turboFrame?.id;
@@ -3177,7 +3183,7 @@ class LinkPrefetchObserver {
3177
3183
  }
3178
3184
 
3179
3185
  if (link.hasAttribute("data-turbo-stream")) {
3180
- request.acceptResponseType("text/vnd.turbo-stream.html");
3186
+ request.acceptResponseType(StreamMessage.contentType);
3181
3187
  }
3182
3188
  }
3183
3189
 
@@ -3202,7 +3208,7 @@ class LinkPrefetchObserver {
3202
3208
  #isPrefetchable(link) {
3203
3209
  const href = link.getAttribute("href");
3204
3210
 
3205
- 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") {
3206
3212
  return false
3207
3213
  }
3208
3214
 
@@ -3218,7 +3224,8 @@ class LinkPrefetchObserver {
3218
3224
  return false
3219
3225
  }
3220
3226
 
3221
- if (link.dataset.turboMethod && link.dataset.turboMethod !== "get") {
3227
+ const turboMethod = link.getAttribute("data-turbo-method");
3228
+ if (turboMethod && turboMethod !== "get") {
3222
3229
  return false
3223
3230
  }
3224
3231
 
@@ -3226,13 +3233,9 @@ class LinkPrefetchObserver {
3226
3233
  return false
3227
3234
  }
3228
3235
 
3229
- if (link.pathname + link.search === document.location.pathname + document.location.search) {
3230
- return false
3231
- }
3232
-
3233
3236
  const turboPrefetchParent = findClosestRecursively(link, "[data-turbo-prefetch]");
3234
3237
 
3235
- if (turboPrefetchParent && turboPrefetchParent.dataset.turboPrefetch === "false") {
3238
+ if (turboPrefetchParent && turboPrefetchParent.getAttribute("data-turbo-prefetch") === "false") {
3236
3239
  return false
3237
3240
  }
3238
3241
 
@@ -3825,7 +3828,7 @@ var Idiomorph = (function () {
3825
3828
  * @returns {boolean}
3826
3829
  */
3827
3830
  function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
3828
- return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement;
3831
+ return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement && possibleActiveElement !== document.body;
3829
3832
  }
3830
3833
 
3831
3834
  /**
@@ -4547,92 +4550,6 @@ var Idiomorph = (function () {
4547
4550
  }
4548
4551
  })();
4549
4552
 
4550
- class MorphRenderer extends Renderer {
4551
- async render() {
4552
- if (this.willRender) await this.#morphBody();
4553
- }
4554
-
4555
- get renderMethod() {
4556
- return "morph"
4557
- }
4558
-
4559
- // Private
4560
-
4561
- async #morphBody() {
4562
- this.#morphElements(this.currentElement, this.newElement);
4563
- this.#reloadRemoteFrames();
4564
-
4565
- dispatch("turbo:morph", {
4566
- detail: {
4567
- currentElement: this.currentElement,
4568
- newElement: this.newElement
4569
- }
4570
- });
4571
- }
4572
-
4573
- #morphElements(currentElement, newElement, morphStyle = "outerHTML") {
4574
- this.isMorphingTurboFrame = this.#isFrameReloadedWithMorph(currentElement);
4575
-
4576
- Idiomorph.morph(currentElement, newElement, {
4577
- morphStyle: morphStyle,
4578
- callbacks: {
4579
- beforeNodeAdded: this.#shouldAddElement,
4580
- beforeNodeMorphed: this.#shouldMorphElement,
4581
- beforeNodeRemoved: this.#shouldRemoveElement
4582
- }
4583
- });
4584
- }
4585
-
4586
- #shouldAddElement = (node) => {
4587
- return !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id))
4588
- }
4589
-
4590
- #shouldMorphElement = (oldNode, newNode) => {
4591
- if (oldNode instanceof HTMLElement) {
4592
- return !oldNode.hasAttribute("data-turbo-permanent") && (this.isMorphingTurboFrame || !this.#isFrameReloadedWithMorph(oldNode))
4593
- } else {
4594
- return true
4595
- }
4596
- }
4597
-
4598
- #shouldRemoveElement = (node) => {
4599
- return this.#shouldMorphElement(node)
4600
- }
4601
-
4602
- #reloadRemoteFrames() {
4603
- this.#remoteFrames().forEach((frame) => {
4604
- if (this.#isFrameReloadedWithMorph(frame)) {
4605
- this.#renderFrameWithMorph(frame);
4606
- frame.reload();
4607
- }
4608
- });
4609
- }
4610
-
4611
- #renderFrameWithMorph(frame) {
4612
- frame.addEventListener("turbo:before-frame-render", (event) => {
4613
- event.detail.render = this.#morphFrameUpdate;
4614
- }, { once: true });
4615
- }
4616
-
4617
- #morphFrameUpdate = (currentElement, newElement) => {
4618
- dispatch("turbo:before-frame-morph", {
4619
- target: currentElement,
4620
- detail: { currentElement, newElement }
4621
- });
4622
- this.#morphElements(currentElement, newElement.children, "innerHTML");
4623
- }
4624
-
4625
- #isFrameReloadedWithMorph(element) {
4626
- return element.src && element.refresh === "morph"
4627
- }
4628
-
4629
- #remoteFrames() {
4630
- return Array.from(document.querySelectorAll('turbo-frame[src]')).filter(frame => {
4631
- return !frame.closest('[data-turbo-permanent]')
4632
- })
4633
- }
4634
- }
4635
-
4636
4553
  class PageRenderer extends Renderer {
4637
4554
  static renderElement(currentElement, newElement) {
4638
4555
  if (document.body && newElement instanceof HTMLBodyElement) {
@@ -4845,6 +4762,122 @@ class PageRenderer extends Renderer {
4845
4762
  }
4846
4763
  }
4847
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
+
4848
4881
  class SnapshotCache {
4849
4882
  keys = []
4850
4883
  snapshots = {}
@@ -5004,7 +5037,7 @@ class Preloader {
5004
5037
  // Fetch request delegate
5005
5038
 
5006
5039
  prepareRequest(fetchRequest) {
5007
- fetchRequest.headers["Sec-Purpose"] = "prefetch";
5040
+ fetchRequest.headers["X-Sec-Purpose"] = "prefetch";
5008
5041
  }
5009
5042
 
5010
5043
  async requestSucceededWithResponse(fetchRequest, fetchResponse) {
@@ -5138,8 +5171,10 @@ class Session {
5138
5171
  const frameElement = options.frame ? document.getElementById(options.frame) : null;
5139
5172
 
5140
5173
  if (frameElement instanceof FrameElement) {
5174
+ const action = options.action || getVisitAction(frameElement);
5175
+
5176
+ frameElement.delegate.proposeVisitIfNavigatedWithAction(frameElement, action);
5141
5177
  frameElement.src = location.toString();
5142
- frameElement.loaded;
5143
5178
  } else {
5144
5179
  this.navigator.proposeVisit(expandURL(location), options);
5145
5180
  }
@@ -5780,7 +5815,7 @@ class FrameController {
5780
5815
  // Appearance observer delegate
5781
5816
 
5782
5817
  elementAppearedInViewport(element) {
5783
- this.proposeVisitIfNavigatedWithAction(element, element);
5818
+ this.proposeVisitIfNavigatedWithAction(element, getVisitAction(element));
5784
5819
  this.#loadSourceURL();
5785
5820
  }
5786
5821
 
@@ -5868,7 +5903,7 @@ class FrameController {
5868
5903
  formSubmissionSucceededWithResponse(formSubmission, response) {
5869
5904
  const frame = this.#findFrameElement(formSubmission.formElement, formSubmission.submitter);
5870
5905
 
5871
- frame.delegate.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
5906
+ frame.delegate.proposeVisitIfNavigatedWithAction(frame, getVisitAction(formSubmission.submitter, formSubmission.formElement, frame));
5872
5907
  frame.delegate.loadResponse(response);
5873
5908
 
5874
5909
  if (!formSubmission.isSafe) {
@@ -5973,15 +6008,15 @@ class FrameController {
5973
6008
  #navigateFrame(element, url, submitter) {
5974
6009
  const frame = this.#findFrameElement(element, submitter);
5975
6010
 
5976
- frame.delegate.proposeVisitIfNavigatedWithAction(frame, element, submitter);
6011
+ frame.delegate.proposeVisitIfNavigatedWithAction(frame, getVisitAction(submitter, element, frame));
5977
6012
 
5978
6013
  this.#withCurrentNavigationElement(element, () => {
5979
6014
  frame.src = url;
5980
6015
  });
5981
6016
  }
5982
6017
 
5983
- proposeVisitIfNavigatedWithAction(frame, element, submitter) {
5984
- this.action = getVisitAction(submitter, element, frame);
6018
+ proposeVisitIfNavigatedWithAction(frame, action = null) {
6019
+ this.action = action;
5985
6020
 
5986
6021
  if (this.action) {
5987
6022
  const pageSnapshot = PageSnapshot.fromElement(frame).clone();
@@ -1,5 +1,5 @@
1
1
  /*!
2
- Turbo 8.0.0-beta.4
2
+ Turbo 8.0.0-rc.2
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
  }
@@ -2366,6 +2371,7 @@ Copyright © 2024 37signals LLC
2366
2371
  this.snapshotHTML = snapshotHTML;
2367
2372
  this.response = response;
2368
2373
  this.isSamePage = this.delegate.locationWithActionIsSamePage(this.location, this.action);
2374
+ this.isPageRefresh = this.view.isPageRefresh(this);
2369
2375
  this.visitCachedSnapshot = visitCachedSnapshot;
2370
2376
  this.willRender = willRender;
2371
2377
  this.updateHistory = updateHistory;
@@ -2531,7 +2537,7 @@ Copyright © 2024 37signals LLC
2531
2537
  const isPreview = this.shouldIssueRequest();
2532
2538
  this.render(async () => {
2533
2539
  this.cacheSnapshot();
2534
- if (this.isSamePage) {
2540
+ if (this.isSamePage || this.isPageRefresh) {
2535
2541
  this.adapter.visitRendered(this);
2536
2542
  } else {
2537
2543
  if (this.view.renderPromise) await this.view.renderPromise;
@@ -3173,7 +3179,7 @@ Copyright © 2024 37signals LLC
3173
3179
  prepareRequest(request) {
3174
3180
  const link = request.target;
3175
3181
 
3176
- request.headers["Sec-Purpose"] = "prefetch";
3182
+ request.headers["X-Sec-Purpose"] = "prefetch";
3177
3183
 
3178
3184
  const turboFrame = link.closest("turbo-frame");
3179
3185
  const turboFrameTarget = link.getAttribute("data-turbo-frame") || turboFrame?.getAttribute("target") || turboFrame?.id;
@@ -3183,7 +3189,7 @@ Copyright © 2024 37signals LLC
3183
3189
  }
3184
3190
 
3185
3191
  if (link.hasAttribute("data-turbo-stream")) {
3186
- request.acceptResponseType("text/vnd.turbo-stream.html");
3192
+ request.acceptResponseType(StreamMessage.contentType);
3187
3193
  }
3188
3194
  }
3189
3195
 
@@ -3208,7 +3214,7 @@ Copyright © 2024 37signals LLC
3208
3214
  #isPrefetchable(link) {
3209
3215
  const href = link.getAttribute("href");
3210
3216
 
3211
- 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") {
3212
3218
  return false
3213
3219
  }
3214
3220
 
@@ -3224,7 +3230,8 @@ Copyright © 2024 37signals LLC
3224
3230
  return false
3225
3231
  }
3226
3232
 
3227
- if (link.dataset.turboMethod && link.dataset.turboMethod !== "get") {
3233
+ const turboMethod = link.getAttribute("data-turbo-method");
3234
+ if (turboMethod && turboMethod !== "get") {
3228
3235
  return false
3229
3236
  }
3230
3237
 
@@ -3232,13 +3239,9 @@ Copyright © 2024 37signals LLC
3232
3239
  return false
3233
3240
  }
3234
3241
 
3235
- if (link.pathname + link.search === document.location.pathname + document.location.search) {
3236
- return false
3237
- }
3238
-
3239
3242
  const turboPrefetchParent = findClosestRecursively(link, "[data-turbo-prefetch]");
3240
3243
 
3241
- if (turboPrefetchParent && turboPrefetchParent.dataset.turboPrefetch === "false") {
3244
+ if (turboPrefetchParent && turboPrefetchParent.getAttribute("data-turbo-prefetch") === "false") {
3242
3245
  return false
3243
3246
  }
3244
3247
 
@@ -3831,7 +3834,7 @@ Copyright © 2024 37signals LLC
3831
3834
  * @returns {boolean}
3832
3835
  */
3833
3836
  function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
3834
- return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement;
3837
+ return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement && possibleActiveElement !== document.body;
3835
3838
  }
3836
3839
 
3837
3840
  /**
@@ -4553,92 +4556,6 @@ Copyright © 2024 37signals LLC
4553
4556
  }
4554
4557
  })();
4555
4558
 
4556
- class MorphRenderer extends Renderer {
4557
- async render() {
4558
- if (this.willRender) await this.#morphBody();
4559
- }
4560
-
4561
- get renderMethod() {
4562
- return "morph"
4563
- }
4564
-
4565
- // Private
4566
-
4567
- async #morphBody() {
4568
- this.#morphElements(this.currentElement, this.newElement);
4569
- this.#reloadRemoteFrames();
4570
-
4571
- dispatch("turbo:morph", {
4572
- detail: {
4573
- currentElement: this.currentElement,
4574
- newElement: this.newElement
4575
- }
4576
- });
4577
- }
4578
-
4579
- #morphElements(currentElement, newElement, morphStyle = "outerHTML") {
4580
- this.isMorphingTurboFrame = this.#isFrameReloadedWithMorph(currentElement);
4581
-
4582
- Idiomorph.morph(currentElement, newElement, {
4583
- morphStyle: morphStyle,
4584
- callbacks: {
4585
- beforeNodeAdded: this.#shouldAddElement,
4586
- beforeNodeMorphed: this.#shouldMorphElement,
4587
- beforeNodeRemoved: this.#shouldRemoveElement
4588
- }
4589
- });
4590
- }
4591
-
4592
- #shouldAddElement = (node) => {
4593
- return !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id))
4594
- }
4595
-
4596
- #shouldMorphElement = (oldNode, newNode) => {
4597
- if (oldNode instanceof HTMLElement) {
4598
- return !oldNode.hasAttribute("data-turbo-permanent") && (this.isMorphingTurboFrame || !this.#isFrameReloadedWithMorph(oldNode))
4599
- } else {
4600
- return true
4601
- }
4602
- }
4603
-
4604
- #shouldRemoveElement = (node) => {
4605
- return this.#shouldMorphElement(node)
4606
- }
4607
-
4608
- #reloadRemoteFrames() {
4609
- this.#remoteFrames().forEach((frame) => {
4610
- if (this.#isFrameReloadedWithMorph(frame)) {
4611
- this.#renderFrameWithMorph(frame);
4612
- frame.reload();
4613
- }
4614
- });
4615
- }
4616
-
4617
- #renderFrameWithMorph(frame) {
4618
- frame.addEventListener("turbo:before-frame-render", (event) => {
4619
- event.detail.render = this.#morphFrameUpdate;
4620
- }, { once: true });
4621
- }
4622
-
4623
- #morphFrameUpdate = (currentElement, newElement) => {
4624
- dispatch("turbo:before-frame-morph", {
4625
- target: currentElement,
4626
- detail: { currentElement, newElement }
4627
- });
4628
- this.#morphElements(currentElement, newElement.children, "innerHTML");
4629
- }
4630
-
4631
- #isFrameReloadedWithMorph(element) {
4632
- return element.src && element.refresh === "morph"
4633
- }
4634
-
4635
- #remoteFrames() {
4636
- return Array.from(document.querySelectorAll('turbo-frame[src]')).filter(frame => {
4637
- return !frame.closest('[data-turbo-permanent]')
4638
- })
4639
- }
4640
- }
4641
-
4642
4559
  class PageRenderer extends Renderer {
4643
4560
  static renderElement(currentElement, newElement) {
4644
4561
  if (document.body && newElement instanceof HTMLBodyElement) {
@@ -4851,6 +4768,122 @@ Copyright © 2024 37signals LLC
4851
4768
  }
4852
4769
  }
4853
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
+
4854
4887
  class SnapshotCache {
4855
4888
  keys = []
4856
4889
  snapshots = {}
@@ -5010,7 +5043,7 @@ Copyright © 2024 37signals LLC
5010
5043
  // Fetch request delegate
5011
5044
 
5012
5045
  prepareRequest(fetchRequest) {
5013
- fetchRequest.headers["Sec-Purpose"] = "prefetch";
5046
+ fetchRequest.headers["X-Sec-Purpose"] = "prefetch";
5014
5047
  }
5015
5048
 
5016
5049
  async requestSucceededWithResponse(fetchRequest, fetchResponse) {
@@ -5144,8 +5177,10 @@ Copyright © 2024 37signals LLC
5144
5177
  const frameElement = options.frame ? document.getElementById(options.frame) : null;
5145
5178
 
5146
5179
  if (frameElement instanceof FrameElement) {
5180
+ const action = options.action || getVisitAction(frameElement);
5181
+
5182
+ frameElement.delegate.proposeVisitIfNavigatedWithAction(frameElement, action);
5147
5183
  frameElement.src = location.toString();
5148
- frameElement.loaded;
5149
5184
  } else {
5150
5185
  this.navigator.proposeVisit(expandURL(location), options);
5151
5186
  }
@@ -5786,7 +5821,7 @@ Copyright © 2024 37signals LLC
5786
5821
  // Appearance observer delegate
5787
5822
 
5788
5823
  elementAppearedInViewport(element) {
5789
- this.proposeVisitIfNavigatedWithAction(element, element);
5824
+ this.proposeVisitIfNavigatedWithAction(element, getVisitAction(element));
5790
5825
  this.#loadSourceURL();
5791
5826
  }
5792
5827
 
@@ -5874,7 +5909,7 @@ Copyright © 2024 37signals LLC
5874
5909
  formSubmissionSucceededWithResponse(formSubmission, response) {
5875
5910
  const frame = this.#findFrameElement(formSubmission.formElement, formSubmission.submitter);
5876
5911
 
5877
- frame.delegate.proposeVisitIfNavigatedWithAction(frame, formSubmission.formElement, formSubmission.submitter);
5912
+ frame.delegate.proposeVisitIfNavigatedWithAction(frame, getVisitAction(formSubmission.submitter, formSubmission.formElement, frame));
5878
5913
  frame.delegate.loadResponse(response);
5879
5914
 
5880
5915
  if (!formSubmission.isSafe) {
@@ -5979,15 +6014,15 @@ Copyright © 2024 37signals LLC
5979
6014
  #navigateFrame(element, url, submitter) {
5980
6015
  const frame = this.#findFrameElement(element, submitter);
5981
6016
 
5982
- frame.delegate.proposeVisitIfNavigatedWithAction(frame, element, submitter);
6017
+ frame.delegate.proposeVisitIfNavigatedWithAction(frame, getVisitAction(submitter, element, frame));
5983
6018
 
5984
6019
  this.#withCurrentNavigationElement(element, () => {
5985
6020
  frame.src = url;
5986
6021
  });
5987
6022
  }
5988
6023
 
5989
- proposeVisitIfNavigatedWithAction(frame, element, submitter) {
5990
- this.action = getVisitAction(submitter, element, frame);
6024
+ proposeVisitIfNavigatedWithAction(frame, action = null) {
6025
+ this.action = action;
5991
6026
 
5992
6027
  if (this.action) {
5993
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.4",
3
+ "version": "8.0.0-rc.2",
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
  },