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 +4 -4
- data/README.md +43 -5
- data/app/assets/javascripts/turbo.js +158 -139
- data/app/assets/javascripts/turbo.min.js +6 -6
- data/app/assets/javascripts/turbo.min.js.map +1 -1
- data/app/channels/turbo/streams/broadcasts.rb +15 -3
- data/app/channels/turbo/streams_channel.rb +15 -15
- data/app/controllers/turbo/frames/frame_request.rb +2 -2
- data/app/helpers/turbo/drive_helper.rb +2 -2
- data/app/helpers/turbo/streams/action_helper.rb +3 -2
- data/app/models/concerns/turbo/broadcastable.rb +19 -11
- data/app/models/turbo/streams/tag_builder.rb +26 -0
- data/lib/turbo/engine.rb +21 -6
- data/lib/turbo/version.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7bc53746ef39d3412872a00dade91b735622b4edca9b42d321c4f2114da6dd4b
|
4
|
+
data.tar.gz: 53974cb2408b7a98cf7cf6a33b9fc8c9ebf948c1e196597f33f499437ea47a47
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 `
|
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
|
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.
|
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", "
|
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(
|
489
|
-
if (
|
490
|
-
|
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
|
-
|
1172
|
-
|
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.
|
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.
|
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
|
-
|
1332
|
-
const
|
1333
|
-
|
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
|
-
|
1519
|
-
|
1520
|
-
element
|
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 === "
|
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
|
3802
|
-
|
3803
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
3899
|
-
return
|
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 ?
|
3957
|
-
const renderer = new rendererClass(this.snapshot, snapshot,
|
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
|
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
|
-
|
4907
|
-
|
4908
|
-
|
4909
|
-
|
4910
|
-
|
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
|
-
|
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
|
-
|
4987
|
-
|
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() {
|