turbo-rails 2.0.4 → 2.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ce426f6fcdec338c0a3eb5c3f97ee2660ef618d23535671ddf464d2650041997
4
- data.tar.gz: eab0160dacd564b0e002ea3fe658bbc6f4770b7c912867c335758d2c3237c24b
3
+ metadata.gz: 7bc53746ef39d3412872a00dade91b735622b4edca9b42d321c4f2114da6dd4b
4
+ data.tar.gz: 53974cb2408b7a98cf7cf6a33b9fc8c9ebf948c1e196597f33f499437ea47a47
5
5
  SHA512:
6
- metadata.gz: d80adee8976dad280b8ff058d5c0615617f37ca3ba5c164db63dce81ad9263b8cfdf704b50c0aaeac9d5d675ad66298ca34902f2e31f5cfc493f8041ca32d12e
7
- data.tar.gz: 696dc8a61b6b0860ca5b2db540f4e573e7531a293a84854068bb1277997f3c73aa993527afdd4a9dc5c741fc8c83ef258cf2022d3a354c6b4d3e2fef2cca0dd1
6
+ metadata.gz: 16fdd0089b68c5f6ed5777324af45fb54a2101439319b70637d60eebaa29965e8f9b3ae9dc2b825505d6b9155a2e74b319c47231f80300aac622b32ed2d0c908
7
+ data.tar.gz: 8d8753af6214007c64250931c9bbba39b341a91decb60a71b6d5a1bf62a08803076127e81a9db3d87ca209a2753558f688cc2319df78a7def4c9a7ca878c0e55
data/README.md CHANGED
@@ -56,7 +56,7 @@ When the user clicks on the `Edit this todo` link, as a direct response to this
56
56
 
57
57
  ### A note on custom layouts
58
58
 
59
- In order to render turbo frame requests without the application layout, Turbo registers a custom [layout method](https://api.rubyonrails.org/classes/ActionView/Layouts/ClassMethods.html#method-i-layout).
59
+ In order to render turbo frame requests without the application layout, Turbo registers a custom [layout method](https://api.rubyonrails.org/classes/ActionView/Layouts/ClassMethods.html#method-i-layout).
60
60
  If your application uses custom layout resolution, you have to make sure to return `"turbo_rails/frame"` (or `false` for TurboRails < 1.4.0) for turbo frame requests:
61
61
 
62
62
  ```ruby
@@ -64,7 +64,7 @@ layout :custom_layout
64
64
 
65
65
  def custom_layout
66
66
  return "turbo_rails/frame" if turbo_frame_request?
67
-
67
+
68
68
  # ... your custom layout logic
69
69
  ```
70
70
 
@@ -74,14 +74,14 @@ If you are using a custom, but "static" layout,
74
74
  layout "some_static_layout"
75
75
  ```
76
76
 
77
- you **have** to change it to a layout method in order to conditionally return `false` for turbo frame requests:
77
+ you **have** to change it to a layout method in order to conditionally return `"turbo_rails/frame"` for turbo frame requests:
78
78
 
79
79
  ```ruby
80
80
  layout :custom_layout
81
81
 
82
82
  def custom_layout
83
83
  return "turbo_rails/frame" if turbo_frame_request?
84
-
84
+
85
85
  "some_static_layout"
86
86
  ```
87
87
 
@@ -111,7 +111,7 @@ This gem is automatically configured for applications made with Rails 7+ (unless
111
111
  3. Run `./bin/rails turbo:install`
112
112
  4. Run `./bin/rails turbo:install:redis` to change the development Action Cable adapter from Async (the default one) to Redis. The Async adapter does not support Turbo Stream broadcasting.
113
113
 
114
- Running `turbo:install` will install through NPM if Node.js is used in the application. Otherwise the asset pipeline version is used. To use the asset pipeline version, you must have `importmap-rails` installed first and listed higher in the Gemfile.
114
+ Running `turbo:install` will install through NPM or Bun if a JavaScript runtime is used in the application. Otherwise the asset pipeline version is used. To use the asset pipeline version, you must have `importmap-rails` installed first and listed higher in the Gemfile.
115
115
 
116
116
  If you're using node and need to use the cable consumer, you can import [`cable`](https://github.com/hotwired/turbo-rails/blob/main/app/javascript/turbo/cable.js) (`import { cable } from "@hotwired/turbo-rails"`), but ensure that your application actually *uses* the members it `import`s when using this style (see [turbo-rails#48](https://github.com/hotwired/turbo-rails/issues/48)).
117
117
 
@@ -152,10 +152,48 @@ The [`Turbo::TestAssertions::IntegrationTestAssertions`](./lib/turbo/test_assert
152
152
 
153
153
  The [`Turbo::Broadcastable::TestHelper`](./lib/turbo/broadcastable/test_helper.rb) concern provides Action Cable-aware test helpers that assert that `<turbo-stream>` elements were or were not broadcast over Action Cable. `Turbo::Broadcastable::TestHelper` is automatically included in [`ActiveSupport::TestCase`](https://edgeapi.rubyonrails.org/classes/ActiveSupport/TestCase.html).
154
154
 
155
+ ### Rendering Outside of a Request
156
+
157
+ Turbo utilizes [ActionController::Renderer][] to render templates and partials
158
+ outside the context of the request-response cycle. If you need to render a
159
+ Turbo-aware template, partial, or component, use [ActionController::Renderer][]:
160
+
161
+ ```ruby
162
+ ApplicationController.renderer.render template: "posts/show", assigns: { post: Post.first } # => "<html>…"
163
+ PostsController.renderer.render :show, assigns: { post: Post.first } # => "<html>…"
164
+ ```
165
+
166
+ As a shortcut, you can also call render directly on the controller class itself:
167
+
168
+ ```ruby
169
+ ApplicationController.render template: "posts/show", assigns: { post: Post.first } # => "<html>…"
170
+ PostsController.render :show, assigns: { post: Post.first } # => "<html>…"
171
+ ```
172
+
173
+ [ActionController::Renderer]: https://api.rubyonrails.org/classes/ActionController/Renderer.html
174
+
155
175
  ## Development
156
176
 
157
177
  Run the tests with `./bin/test`.
158
178
 
179
+ ### Using local Turbo version
180
+
181
+ Often you might want to test changes made locally to [Turbo lib](https://github.com/hotwired/turbo) itself. To package your local development version of Turbo you can use [yarn link](https://classic.yarnpkg.com/lang/en/docs/cli/link/) feature:
182
+
183
+ ```sh
184
+ cd <local-turbo-dir>
185
+ yarn link
186
+
187
+ cd <local-turbo-rails-dir>
188
+ yarn link @hotwired/turbo
189
+
190
+ # Build the JS distribution files...
191
+ yarn build
192
+ # ...and commit the changes
193
+ ```
194
+
195
+ Now you can reference your version of turbo-rails in your Rails projects packaged with your local version of Turbo.
196
+
159
197
  ## License
160
198
 
161
199
  Turbo is released under the [MIT License](https://opensource.org/licenses/MIT).
@@ -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(prototype) {
@@ -72,7 +72,7 @@ class FrameElement extends HTMLElement {
72
72
  static delegateConstructor=undefined;
73
73
  loaded=Promise.resolve();
74
74
  static get observedAttributes() {
75
- return [ "disabled", "complete", "loading", "src" ];
75
+ return [ "disabled", "loading", "src" ];
76
76
  }
77
77
  constructor() {
78
78
  super();
@@ -90,11 +90,9 @@ class FrameElement extends HTMLElement {
90
90
  attributeChangedCallback(name) {
91
91
  if (name == "loading") {
92
92
  this.delegate.loadingStyleChanged();
93
- } else if (name == "complete") {
94
- this.delegate.completeChanged();
95
93
  } else if (name == "src") {
96
94
  this.delegate.sourceURLChanged();
97
- } else {
95
+ } else if (name == "disabled") {
98
96
  this.delegate.disabledChanged();
99
97
  }
100
98
  }
@@ -485,13 +483,17 @@ async function around(callback, reader) {
485
483
  return [ before, after ];
486
484
  }
487
485
 
488
- function doesNotTargetIFrame(anchor) {
489
- if (anchor.hasAttribute("target")) {
490
- for (const element of document.getElementsByName(anchor.target)) {
486
+ function doesNotTargetIFrame(name) {
487
+ if (name === "_blank") {
488
+ return false;
489
+ } else if (name) {
490
+ for (const element of document.getElementsByName(name)) {
491
491
  if (element instanceof HTMLIFrameElement) return false;
492
492
  }
493
+ return true;
494
+ } else {
495
+ return true;
493
496
  }
494
- return true;
495
497
  }
496
498
 
497
499
  function findLinkFromClickTarget(target) {
@@ -598,7 +600,7 @@ class FetchRequest {
598
600
  this.fetchOptions = {
599
601
  credentials: "same-origin",
600
602
  redirect: "follow",
601
- method: method,
603
+ method: method.toUpperCase(),
602
604
  headers: {
603
605
  ...this.defaultHeaders
604
606
  },
@@ -618,7 +620,7 @@ class FetchRequest {
618
620
  const [url, body] = buildResourceAndBody(this.url, fetchMethod, fetchBody, this.enctype);
619
621
  this.url = url;
620
622
  this.fetchOptions.body = body;
621
- this.fetchOptions.method = fetchMethod;
623
+ this.fetchOptions.method = fetchMethod.toUpperCase();
622
624
  }
623
625
  get headers() {
624
626
  return this.fetchOptions.headers;
@@ -1168,15 +1170,8 @@ function submissionDoesNotDismissDialog(form, submitter) {
1168
1170
  }
1169
1171
 
1170
1172
  function submissionDoesNotTargetIFrame(form, submitter) {
1171
- if (submitter?.hasAttribute("formtarget") || form.hasAttribute("target")) {
1172
- const target = submitter?.getAttribute("formtarget") || form.target;
1173
- for (const element of document.getElementsByName(target)) {
1174
- if (element instanceof HTMLIFrameElement) return false;
1175
- }
1176
- return true;
1177
- } else {
1178
- return true;
1179
- }
1173
+ const target = submitter?.getAttribute("formtarget") || form.getAttribute("target");
1174
+ return doesNotTargetIFrame(target);
1180
1175
  }
1181
1176
 
1182
1177
  class View {
@@ -1309,14 +1304,14 @@ class LinkInterceptor {
1309
1304
  document.removeEventListener("turbo:before-visit", this.willVisit);
1310
1305
  }
1311
1306
  clickBubbled=event => {
1312
- if (this.respondsToEventTarget(event.target)) {
1307
+ if (this.clickEventIsSignificant(event)) {
1313
1308
  this.clickEvent = event;
1314
1309
  } else {
1315
1310
  delete this.clickEvent;
1316
1311
  }
1317
1312
  };
1318
1313
  linkClicked=event => {
1319
- if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) {
1314
+ if (this.clickEvent && this.clickEventIsSignificant(event)) {
1320
1315
  if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url, event.detail.originalEvent)) {
1321
1316
  this.clickEvent.preventDefault();
1322
1317
  event.preventDefault();
@@ -1328,9 +1323,10 @@ class LinkInterceptor {
1328
1323
  willVisit=_event => {
1329
1324
  delete this.clickEvent;
1330
1325
  };
1331
- respondsToEventTarget(target) {
1332
- const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
1333
- return element && element.closest("turbo-frame, html") == this.element;
1326
+ clickEventIsSignificant(event) {
1327
+ const target = event.composed ? event.target?.parentElement : event.target;
1328
+ const element = findLinkFromClickTarget(target) || target;
1329
+ return element instanceof Element && element.closest("turbo-frame, html") == this.element;
1334
1330
  }
1335
1331
  }
1336
1332
 
@@ -1360,7 +1356,7 @@ class LinkClickObserver {
1360
1356
  if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {
1361
1357
  const target = event.composedPath && event.composedPath()[0] || event.target;
1362
1358
  const link = findLinkFromClickTarget(target);
1363
- if (link && doesNotTargetIFrame(link)) {
1359
+ if (link && doesNotTargetIFrame(link.target)) {
1364
1360
  const location = getLocationForLink(link);
1365
1361
  if (this.delegate.willFollowLinkToLocation(link, location, event)) {
1366
1362
  event.preventDefault();
@@ -1498,6 +1494,9 @@ class Renderer {
1498
1494
  get shouldRender() {
1499
1495
  return true;
1500
1496
  }
1497
+ get shouldAutofocus() {
1498
+ return true;
1499
+ }
1501
1500
  get reloadReason() {
1502
1501
  return;
1503
1502
  }
@@ -1515,9 +1514,11 @@ class Renderer {
1515
1514
  await Bardo.preservingPermanentElements(this, this.permanentElementMap, callback);
1516
1515
  }
1517
1516
  focusFirstAutofocusableElement() {
1518
- const element = this.connectedSnapshot.firstAutofocusableElement;
1519
- if (element) {
1520
- element.focus();
1517
+ if (this.shouldAutofocus) {
1518
+ const element = this.connectedSnapshot.firstAutofocusableElement;
1519
+ if (element) {
1520
+ element.focus();
1521
+ }
1521
1522
  }
1522
1523
  }
1523
1524
  enteringBardo(currentPermanentElement) {
@@ -2631,7 +2632,7 @@ class LinkPrefetchObserver {
2631
2632
  this.#prefetchedLink = null;
2632
2633
  };
2633
2634
  #tryToUsePrefetchedRequest=event => {
2634
- if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "get") {
2635
+ if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "GET") {
2635
2636
  const cached = prefetchCache.get(event.detail.url.toString());
2636
2637
  if (cached) {
2637
2638
  event.detail.fetchRequest = cached;
@@ -2800,6 +2801,7 @@ class Navigator {
2800
2801
  }
2801
2802
  visitCompleted(visit) {
2802
2803
  this.delegate.visitCompleted(visit);
2804
+ delete this.currentVisit;
2803
2805
  }
2804
2806
  locationWithActionIsSamePage(location, action) {
2805
2807
  const anchor = getAnchor(location);
@@ -3630,6 +3632,80 @@ var Idiomorph = function() {
3630
3632
  };
3631
3633
  }();
3632
3634
 
3635
+ function morphElements(currentElement, newElement, {callbacks: callbacks, ...options} = {}) {
3636
+ Idiomorph.morph(currentElement, newElement, {
3637
+ ...options,
3638
+ callbacks: new DefaultIdiomorphCallbacks(callbacks)
3639
+ });
3640
+ }
3641
+
3642
+ function morphChildren(currentElement, newElement) {
3643
+ morphElements(currentElement, newElement.children, {
3644
+ morphStyle: "innerHTML"
3645
+ });
3646
+ }
3647
+
3648
+ class DefaultIdiomorphCallbacks {
3649
+ #beforeNodeMorphed;
3650
+ constructor({beforeNodeMorphed: beforeNodeMorphed} = {}) {
3651
+ this.#beforeNodeMorphed = beforeNodeMorphed || (() => true);
3652
+ }
3653
+ beforeNodeAdded=node => !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id));
3654
+ beforeNodeMorphed=(currentElement, newElement) => {
3655
+ if (currentElement instanceof Element) {
3656
+ if (!currentElement.hasAttribute("data-turbo-permanent") && this.#beforeNodeMorphed(currentElement, newElement)) {
3657
+ const event = dispatch("turbo:before-morph-element", {
3658
+ cancelable: true,
3659
+ target: currentElement,
3660
+ detail: {
3661
+ currentElement: currentElement,
3662
+ newElement: newElement
3663
+ }
3664
+ });
3665
+ return !event.defaultPrevented;
3666
+ } else {
3667
+ return false;
3668
+ }
3669
+ }
3670
+ };
3671
+ beforeAttributeUpdated=(attributeName, target, mutationType) => {
3672
+ const event = dispatch("turbo:before-morph-attribute", {
3673
+ cancelable: true,
3674
+ target: target,
3675
+ detail: {
3676
+ attributeName: attributeName,
3677
+ mutationType: mutationType
3678
+ }
3679
+ });
3680
+ return !event.defaultPrevented;
3681
+ };
3682
+ beforeNodeRemoved=node => this.beforeNodeMorphed(node);
3683
+ afterNodeMorphed=(currentElement, newElement) => {
3684
+ if (currentElement instanceof Element) {
3685
+ dispatch("turbo:morph-element", {
3686
+ target: currentElement,
3687
+ detail: {
3688
+ currentElement: currentElement,
3689
+ newElement: newElement
3690
+ }
3691
+ });
3692
+ }
3693
+ };
3694
+ }
3695
+
3696
+ class MorphingFrameRenderer extends FrameRenderer {
3697
+ static renderElement(currentElement, newElement) {
3698
+ dispatch("turbo:before-frame-morph", {
3699
+ target: currentElement,
3700
+ detail: {
3701
+ currentElement: currentElement,
3702
+ newElement: newElement
3703
+ }
3704
+ });
3705
+ morphChildren(currentElement, newElement);
3706
+ }
3707
+ }
3708
+
3633
3709
  class PageRenderer extends Renderer {
3634
3710
  static renderElement(currentElement, newElement) {
3635
3711
  if (document.body && newElement instanceof HTMLBodyElement) {
@@ -3798,106 +3874,45 @@ class PageRenderer extends Renderer {
3798
3874
  }
3799
3875
  }
3800
3876
 
3801
- class MorphRenderer extends PageRenderer {
3802
- async render() {
3803
- if (this.willRender) await this.#morphBody();
3804
- }
3805
- get renderMethod() {
3806
- return "morph";
3807
- }
3808
- async #morphBody() {
3809
- this.#morphElements(this.currentElement, this.newElement);
3810
- this.#reloadRemoteFrames();
3811
- dispatch("turbo:morph", {
3812
- detail: {
3813
- currentElement: this.currentElement,
3814
- newElement: this.newElement
3815
- }
3816
- });
3817
- }
3818
- #morphElements(currentElement, newElement, morphStyle = "outerHTML") {
3819
- this.isMorphingTurboFrame = this.#isFrameReloadedWithMorph(currentElement);
3820
- Idiomorph.morph(currentElement, newElement, {
3821
- morphStyle: morphStyle,
3877
+ class MorphingPageRenderer extends PageRenderer {
3878
+ static renderElement(currentElement, newElement) {
3879
+ morphElements(currentElement, newElement, {
3822
3880
  callbacks: {
3823
- beforeNodeAdded: this.#shouldAddElement,
3824
- beforeNodeMorphed: this.#shouldMorphElement,
3825
- beforeAttributeUpdated: this.#shouldUpdateAttribute,
3826
- beforeNodeRemoved: this.#shouldRemoveElement,
3827
- afterNodeMorphed: this.#didMorphElement
3828
- }
3829
- });
3830
- }
3831
- #shouldAddElement=node => !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id));
3832
- #shouldMorphElement=(oldNode, newNode) => {
3833
- if (oldNode instanceof HTMLElement) {
3834
- if (!oldNode.hasAttribute("data-turbo-permanent") && (this.isMorphingTurboFrame || !this.#isFrameReloadedWithMorph(oldNode))) {
3835
- const event = dispatch("turbo:before-morph-element", {
3836
- cancelable: true,
3837
- target: oldNode,
3838
- detail: {
3839
- newElement: newNode
3840
- }
3841
- });
3842
- return !event.defaultPrevented;
3843
- } else {
3844
- return false;
3845
- }
3846
- }
3847
- };
3848
- #shouldUpdateAttribute=(attributeName, target, mutationType) => {
3849
- const event = dispatch("turbo:before-morph-attribute", {
3850
- cancelable: true,
3851
- target: target,
3852
- detail: {
3853
- attributeName: attributeName,
3854
- mutationType: mutationType
3881
+ beforeNodeMorphed: element => !canRefreshFrame(element)
3855
3882
  }
3856
3883
  });
3857
- return !event.defaultPrevented;
3858
- };
3859
- #didMorphElement=(oldNode, newNode) => {
3860
- if (newNode instanceof HTMLElement) {
3861
- dispatch("turbo:morph-element", {
3862
- target: oldNode,
3863
- detail: {
3864
- newElement: newNode
3865
- }
3866
- });
3884
+ for (const frame of currentElement.querySelectorAll("turbo-frame")) {
3885
+ if (canRefreshFrame(frame)) refreshFrame(frame);
3867
3886
  }
3868
- };
3869
- #shouldRemoveElement=node => this.#shouldMorphElement(node);
3870
- #reloadRemoteFrames() {
3871
- this.#remoteFrames().forEach((frame => {
3872
- if (this.#isFrameReloadedWithMorph(frame)) {
3873
- this.#renderFrameWithMorph(frame);
3874
- frame.reload();
3875
- }
3876
- }));
3877
- }
3878
- #renderFrameWithMorph(frame) {
3879
- frame.addEventListener("turbo:before-frame-render", (event => {
3880
- event.detail.render = this.#morphFrameUpdate;
3881
- }), {
3882
- once: true
3883
- });
3884
- }
3885
- #morphFrameUpdate=(currentElement, newElement) => {
3886
- dispatch("turbo:before-frame-morph", {
3887
- target: currentElement,
3887
+ dispatch("turbo:morph", {
3888
3888
  detail: {
3889
3889
  currentElement: currentElement,
3890
3890
  newElement: newElement
3891
3891
  }
3892
3892
  });
3893
- this.#morphElements(currentElement, newElement.children, "innerHTML");
3894
- };
3895
- #isFrameReloadedWithMorph(element) {
3896
- return element.src && element.refresh === "morph";
3897
3893
  }
3898
- #remoteFrames() {
3899
- return Array.from(document.querySelectorAll("turbo-frame[src]")).filter((frame => !frame.closest("[data-turbo-permanent]")));
3894
+ async preservingPermanentElements(callback) {
3895
+ return await callback();
3896
+ }
3897
+ get renderMethod() {
3898
+ return "morph";
3900
3899
  }
3900
+ get shouldAutofocus() {
3901
+ return false;
3902
+ }
3903
+ }
3904
+
3905
+ function canRefreshFrame(frame) {
3906
+ return frame instanceof FrameElement && frame.src && frame.refresh === "morph" && !frame.closest("[data-turbo-permanent]");
3907
+ }
3908
+
3909
+ function refreshFrame(frame) {
3910
+ frame.addEventListener("turbo:before-frame-render", (({detail: detail}) => {
3911
+ detail.render = MorphingFrameRenderer.renderElement;
3912
+ }), {
3913
+ once: true
3914
+ });
3915
+ frame.reload();
3901
3916
  }
3902
3917
 
3903
3918
  class SnapshotCache {
@@ -3953,8 +3968,8 @@ class PageView extends View {
3953
3968
  }
3954
3969
  renderPage(snapshot, isPreview = false, willRender = true, visit) {
3955
3970
  const shouldMorphPage = this.isPageRefresh(visit) && this.snapshot.shouldMorphPage;
3956
- const rendererClass = shouldMorphPage ? MorphRenderer : PageRenderer;
3957
- const renderer = new rendererClass(this.snapshot, snapshot, PageRenderer.renderElement, isPreview, willRender);
3971
+ const rendererClass = shouldMorphPage ? MorphingPageRenderer : PageRenderer;
3972
+ const renderer = new rendererClass(this.snapshot, snapshot, rendererClass.renderElement, isPreview, willRender);
3958
3973
  if (!renderer.shouldRender) {
3959
3974
  this.forceReloaded = true;
3960
3975
  } else {
@@ -4145,7 +4160,7 @@ class Session {
4145
4160
  }
4146
4161
  refresh(url, requestId) {
4147
4162
  const isRecentRequest = requestId && this.recentRequests.has(requestId);
4148
- if (!isRecentRequest) {
4163
+ if (!isRecentRequest && !this.navigator.currentVisit) {
4149
4164
  this.visit(url, {
4150
4165
  action: "replace",
4151
4166
  shouldCacheSnapshot: false
@@ -4561,17 +4576,11 @@ class FrameController {
4561
4576
  }
4562
4577
  sourceURLReloaded() {
4563
4578
  const {src: src} = this.element;
4564
- this.#ignoringChangesToAttribute("complete", (() => {
4565
- this.element.removeAttribute("complete");
4566
- }));
4579
+ this.element.removeAttribute("complete");
4567
4580
  this.element.src = null;
4568
4581
  this.element.src = src;
4569
4582
  return this.element.loaded;
4570
4583
  }
4571
- completeChanged() {
4572
- if (this.#isIgnoringChangesTo("complete")) return;
4573
- this.#loadSourceURL();
4574
- }
4575
4584
  loadingStyleChanged() {
4576
4585
  if (this.loadingStyle == FrameLoadingStyle.lazy) {
4577
4586
  this.appearanceObserver.start();
@@ -4903,13 +4912,11 @@ class FrameController {
4903
4912
  return this.element.hasAttribute("complete");
4904
4913
  }
4905
4914
  set complete(value) {
4906
- this.#ignoringChangesToAttribute("complete", (() => {
4907
- if (value) {
4908
- this.element.setAttribute("complete", "");
4909
- } else {
4910
- this.element.removeAttribute("complete");
4911
- }
4912
- }));
4915
+ if (value) {
4916
+ this.element.setAttribute("complete", "");
4917
+ } else {
4918
+ this.element.removeAttribute("complete");
4919
+ }
4913
4920
  }
4914
4921
  get isActive() {
4915
4922
  return this.element.isActive && this.#connected;
@@ -4979,12 +4986,24 @@ const StreamActions = {
4979
4986
  this.targetElements.forEach((e => e.remove()));
4980
4987
  },
4981
4988
  replace() {
4982
- this.targetElements.forEach((e => e.replaceWith(this.templateContent)));
4989
+ const method = this.getAttribute("method");
4990
+ this.targetElements.forEach((targetElement => {
4991
+ if (method === "morph") {
4992
+ morphElements(targetElement, this.templateContent);
4993
+ } else {
4994
+ targetElement.replaceWith(this.templateContent);
4995
+ }
4996
+ }));
4983
4997
  },
4984
4998
  update() {
4999
+ const method = this.getAttribute("method");
4985
5000
  this.targetElements.forEach((targetElement => {
4986
- targetElement.innerHTML = "";
4987
- targetElement.append(this.templateContent);
5001
+ if (method === "morph") {
5002
+ morphChildren(targetElement, this.templateContent);
5003
+ } else {
5004
+ targetElement.innerHTML = "";
5005
+ targetElement.append(this.templateContent);
5006
+ }
4988
5007
  }));
4989
5008
  },
4990
5009
  refresh() {