turbo-rails 2.0.5 → 2.0.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +53 -6
- data/app/assets/javascripts/turbo.js +150 -121
- 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/helpers/turbo/streams_helper.rb +6 -0
- data/app/models/concerns/turbo/broadcastable.rb +19 -11
- data/config/routes.rb +3 -3
- data/lib/tasks/turbo_tasks.rake +0 -22
- data/lib/turbo/engine.rb +21 -6
- data/lib/turbo/version.rb +1 -1
- metadata +5 -5
- data/lib/install/turbo_needs_redis.rb +0 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4553ee49b6504b50ebd79364400cd57bcba2a4fd544aff65f88a4c5a12ffdaac
|
4
|
+
data.tar.gz: 7500ba88dc381e1601b0778c31e68be1a16ce72dcc535bbd8b54ba4eebac5d0e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 40376048e8a552fb9ac3570e6fce78f7d0741cdab4b8d3851305fb5541f50dd381a4d9616e7c4fb147a1d58d7b546ab6d07061ac79f3851b8163605084cabc62
|
7
|
+
data.tar.gz: 85414d553bc478bf46ba0294381b09031e58949edd93d47e8f02b721a0fcb556388a5b3b20741a3011f3547fb0281d9bdbcc7e518a97df27953474ce3d012add
|
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
|
|
@@ -109,9 +109,8 @@ This gem is automatically configured for applications made with Rails 7+ (unless
|
|
109
109
|
1. Add the `turbo-rails` gem to your Gemfile: `gem 'turbo-rails'`
|
110
110
|
2. Run `./bin/bundle install`
|
111
111
|
3. Run `./bin/rails turbo:install`
|
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
112
|
|
114
|
-
Running `turbo:install` will install through NPM if
|
113
|
+
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
114
|
|
116
115
|
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
116
|
|
@@ -125,6 +124,8 @@ import "@hotwired/turbo-rails"
|
|
125
124
|
|
126
125
|
You can watch [the video introduction to Hotwire](https://hotwired.dev/#screencast), which focuses extensively on demonstrating Turbo in a Rails demo. Then you should familiarize yourself with [Turbo handbook](https://turbo.hotwired.dev/handbook/introduction) to understand Drive, Frames, and Streams in-depth. Finally, dive into the code documentation by starting with [`Turbo::FramesHelper`](https://github.com/hotwired/turbo-rails/blob/main/app/helpers/turbo/frames_helper.rb), [`Turbo::StreamsHelper`](https://github.com/hotwired/turbo-rails/blob/main/app/helpers/turbo/streams_helper.rb), [`Turbo::Streams::TagBuilder`](https://github.com/hotwired/turbo-rails/blob/main/app/models/turbo/streams/tag_builder.rb), and [`Turbo::Broadcastable`](https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb).
|
127
126
|
|
127
|
+
Note that in development, the default Action Cable adapter is the single-process `async` adapter. This means that turbo updates are only broadcast within that same process. So you can't start `bin/rails console` and trigger Turbo broadcasts and expect them to show up in a browser connected to a server running in a separate `bin/dev` or `bin/rails server` process. Instead, you should use the web-console when needing to manaually trigger Turbo broadcasts inside the same process. Add "console" to any action or "<%= console %>" in any view to make the web console appear.
|
128
|
+
|
128
129
|
### RubyDoc Documentation
|
129
130
|
|
130
131
|
For the API documentation covering this gem's classes and packages, [visit the RubyDoc page](https://rubydoc.info/github/hotwired/turbo-rails/main).
|
@@ -152,10 +153,56 @@ The [`Turbo::TestAssertions::IntegrationTestAssertions`](./lib/turbo/test_assert
|
|
152
153
|
|
153
154
|
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
155
|
|
156
|
+
### Rendering Outside of a Request
|
157
|
+
|
158
|
+
Turbo utilizes [ActionController::Renderer][] to render templates and partials
|
159
|
+
outside the context of the request-response cycle. If you need to render a
|
160
|
+
Turbo-aware template, partial, or component, use [ActionController::Renderer][]:
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
ApplicationController.renderer.render template: "posts/show", assigns: { post: Post.first } # => "<html>…"
|
164
|
+
PostsController.renderer.render :show, assigns: { post: Post.first } # => "<html>…"
|
165
|
+
```
|
166
|
+
|
167
|
+
As a shortcut, you can also call render directly on the controller class itself:
|
168
|
+
|
169
|
+
```ruby
|
170
|
+
ApplicationController.render template: "posts/show", assigns: { post: Post.first } # => "<html>…"
|
171
|
+
PostsController.render :show, assigns: { post: Post.first } # => "<html>…"
|
172
|
+
```
|
173
|
+
|
174
|
+
[ActionController::Renderer]: https://api.rubyonrails.org/classes/ActionController/Renderer.html
|
175
|
+
|
155
176
|
## Development
|
156
177
|
|
157
178
|
Run the tests with `./bin/test`.
|
158
179
|
|
180
|
+
### Using local Turbo version
|
181
|
+
|
182
|
+
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:
|
183
|
+
|
184
|
+
```sh
|
185
|
+
cd <local-turbo-dir>
|
186
|
+
yarn link
|
187
|
+
|
188
|
+
cd <local-turbo-rails-dir>
|
189
|
+
yarn link @hotwired/turbo
|
190
|
+
|
191
|
+
# Build the JS distribution files...
|
192
|
+
yarn build
|
193
|
+
# ...and commit the changes
|
194
|
+
```
|
195
|
+
|
196
|
+
Now you can reference your version of turbo-rails in your Rails projects packaged with your local version of Turbo.
|
197
|
+
|
198
|
+
## Contributing
|
199
|
+
|
200
|
+
Having a way to reproduce your issue will help people confirm, investigate, and ultimately fix your issue. You can do this by providing an executable test case. To make this process easier, we have prepared an [executable bug report Rails application](./bug_report_template.rb) for you to use as a starting point.
|
201
|
+
|
202
|
+
This template includes the boilerplate code to set up a System Test case. Copy the content of the template into a `.rb` file and make the necessary changes to demonstrate the issue. You can execute it by running `ruby the_file.rb` in your terminal. If all goes well, you should see your test case failing.
|
203
|
+
|
204
|
+
You can then share your executable test case as a gist or paste the content into the issue description.
|
205
|
+
|
159
206
|
## License
|
160
207
|
|
161
208
|
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) {
|
@@ -483,13 +483,17 @@ async function around(callback, reader) {
|
|
483
483
|
return [ before, after ];
|
484
484
|
}
|
485
485
|
|
486
|
-
function doesNotTargetIFrame(
|
487
|
-
if (
|
488
|
-
|
486
|
+
function doesNotTargetIFrame(name) {
|
487
|
+
if (name === "_blank") {
|
488
|
+
return false;
|
489
|
+
} else if (name) {
|
490
|
+
for (const element of document.getElementsByName(name)) {
|
489
491
|
if (element instanceof HTMLIFrameElement) return false;
|
490
492
|
}
|
493
|
+
return true;
|
494
|
+
} else {
|
495
|
+
return true;
|
491
496
|
}
|
492
|
-
return true;
|
493
497
|
}
|
494
498
|
|
495
499
|
function findLinkFromClickTarget(target) {
|
@@ -596,7 +600,7 @@ class FetchRequest {
|
|
596
600
|
this.fetchOptions = {
|
597
601
|
credentials: "same-origin",
|
598
602
|
redirect: "follow",
|
599
|
-
method: method,
|
603
|
+
method: method.toUpperCase(),
|
600
604
|
headers: {
|
601
605
|
...this.defaultHeaders
|
602
606
|
},
|
@@ -616,7 +620,7 @@ class FetchRequest {
|
|
616
620
|
const [url, body] = buildResourceAndBody(this.url, fetchMethod, fetchBody, this.enctype);
|
617
621
|
this.url = url;
|
618
622
|
this.fetchOptions.body = body;
|
619
|
-
this.fetchOptions.method = fetchMethod;
|
623
|
+
this.fetchOptions.method = fetchMethod.toUpperCase();
|
620
624
|
}
|
621
625
|
get headers() {
|
622
626
|
return this.fetchOptions.headers;
|
@@ -1166,15 +1170,8 @@ function submissionDoesNotDismissDialog(form, submitter) {
|
|
1166
1170
|
}
|
1167
1171
|
|
1168
1172
|
function submissionDoesNotTargetIFrame(form, submitter) {
|
1169
|
-
|
1170
|
-
|
1171
|
-
for (const element of document.getElementsByName(target)) {
|
1172
|
-
if (element instanceof HTMLIFrameElement) return false;
|
1173
|
-
}
|
1174
|
-
return true;
|
1175
|
-
} else {
|
1176
|
-
return true;
|
1177
|
-
}
|
1173
|
+
const target = submitter?.getAttribute("formtarget") || form.getAttribute("target");
|
1174
|
+
return doesNotTargetIFrame(target);
|
1178
1175
|
}
|
1179
1176
|
|
1180
1177
|
class View {
|
@@ -1307,14 +1304,14 @@ class LinkInterceptor {
|
|
1307
1304
|
document.removeEventListener("turbo:before-visit", this.willVisit);
|
1308
1305
|
}
|
1309
1306
|
clickBubbled=event => {
|
1310
|
-
if (this.
|
1307
|
+
if (this.clickEventIsSignificant(event)) {
|
1311
1308
|
this.clickEvent = event;
|
1312
1309
|
} else {
|
1313
1310
|
delete this.clickEvent;
|
1314
1311
|
}
|
1315
1312
|
};
|
1316
1313
|
linkClicked=event => {
|
1317
|
-
if (this.clickEvent && this.
|
1314
|
+
if (this.clickEvent && this.clickEventIsSignificant(event)) {
|
1318
1315
|
if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url, event.detail.originalEvent)) {
|
1319
1316
|
this.clickEvent.preventDefault();
|
1320
1317
|
event.preventDefault();
|
@@ -1326,9 +1323,10 @@ class LinkInterceptor {
|
|
1326
1323
|
willVisit=_event => {
|
1327
1324
|
delete this.clickEvent;
|
1328
1325
|
};
|
1329
|
-
|
1330
|
-
const
|
1331
|
-
|
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;
|
1332
1330
|
}
|
1333
1331
|
}
|
1334
1332
|
|
@@ -1358,7 +1356,7 @@ class LinkClickObserver {
|
|
1358
1356
|
if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {
|
1359
1357
|
const target = event.composedPath && event.composedPath()[0] || event.target;
|
1360
1358
|
const link = findLinkFromClickTarget(target);
|
1361
|
-
if (link && doesNotTargetIFrame(link)) {
|
1359
|
+
if (link && doesNotTargetIFrame(link.target)) {
|
1362
1360
|
const location = getLocationForLink(link);
|
1363
1361
|
if (this.delegate.willFollowLinkToLocation(link, location, event)) {
|
1364
1362
|
event.preventDefault();
|
@@ -1496,6 +1494,9 @@ class Renderer {
|
|
1496
1494
|
get shouldRender() {
|
1497
1495
|
return true;
|
1498
1496
|
}
|
1497
|
+
get shouldAutofocus() {
|
1498
|
+
return true;
|
1499
|
+
}
|
1499
1500
|
get reloadReason() {
|
1500
1501
|
return;
|
1501
1502
|
}
|
@@ -1513,9 +1514,11 @@ class Renderer {
|
|
1513
1514
|
await Bardo.preservingPermanentElements(this, this.permanentElementMap, callback);
|
1514
1515
|
}
|
1515
1516
|
focusFirstAutofocusableElement() {
|
1516
|
-
|
1517
|
-
|
1518
|
-
element
|
1517
|
+
if (this.shouldAutofocus) {
|
1518
|
+
const element = this.connectedSnapshot.firstAutofocusableElement;
|
1519
|
+
if (element) {
|
1520
|
+
element.focus();
|
1521
|
+
}
|
1519
1522
|
}
|
1520
1523
|
}
|
1521
1524
|
enteringBardo(currentPermanentElement) {
|
@@ -2629,7 +2632,7 @@ class LinkPrefetchObserver {
|
|
2629
2632
|
this.#prefetchedLink = null;
|
2630
2633
|
};
|
2631
2634
|
#tryToUsePrefetchedRequest=event => {
|
2632
|
-
if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "
|
2635
|
+
if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "GET") {
|
2633
2636
|
const cached = prefetchCache.get(event.detail.url.toString());
|
2634
2637
|
if (cached) {
|
2635
2638
|
event.detail.fetchRequest = cached;
|
@@ -2798,6 +2801,7 @@ class Navigator {
|
|
2798
2801
|
}
|
2799
2802
|
visitCompleted(visit) {
|
2800
2803
|
this.delegate.visitCompleted(visit);
|
2804
|
+
delete this.currentVisit;
|
2801
2805
|
}
|
2802
2806
|
locationWithActionIsSamePage(location, action) {
|
2803
2807
|
const anchor = getAnchor(location);
|
@@ -3628,6 +3632,80 @@ var Idiomorph = function() {
|
|
3628
3632
|
};
|
3629
3633
|
}();
|
3630
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
|
+
|
3631
3709
|
class PageRenderer extends Renderer {
|
3632
3710
|
static renderElement(currentElement, newElement) {
|
3633
3711
|
if (document.body && newElement instanceof HTMLBodyElement) {
|
@@ -3796,108 +3874,47 @@ class PageRenderer extends Renderer {
|
|
3796
3874
|
}
|
3797
3875
|
}
|
3798
3876
|
|
3799
|
-
class
|
3800
|
-
|
3801
|
-
|
3802
|
-
}
|
3803
|
-
get renderMethod() {
|
3804
|
-
return "morph";
|
3805
|
-
}
|
3806
|
-
async #morphBody() {
|
3807
|
-
this.#morphElements(this.currentElement, this.newElement);
|
3808
|
-
this.#reloadRemoteFrames();
|
3809
|
-
dispatch("turbo:morph", {
|
3810
|
-
detail: {
|
3811
|
-
currentElement: this.currentElement,
|
3812
|
-
newElement: this.newElement
|
3813
|
-
}
|
3814
|
-
});
|
3815
|
-
}
|
3816
|
-
#morphElements(currentElement, newElement, morphStyle = "outerHTML") {
|
3817
|
-
this.isMorphingTurboFrame = this.#isFrameReloadedWithMorph(currentElement);
|
3818
|
-
Idiomorph.morph(currentElement, newElement, {
|
3819
|
-
morphStyle: morphStyle,
|
3877
|
+
class MorphingPageRenderer extends PageRenderer {
|
3878
|
+
static renderElement(currentElement, newElement) {
|
3879
|
+
morphElements(currentElement, newElement, {
|
3820
3880
|
callbacks: {
|
3821
|
-
|
3822
|
-
beforeNodeMorphed: this.#shouldMorphElement,
|
3823
|
-
beforeAttributeUpdated: this.#shouldUpdateAttribute,
|
3824
|
-
beforeNodeRemoved: this.#shouldRemoveElement,
|
3825
|
-
afterNodeMorphed: this.#didMorphElement
|
3881
|
+
beforeNodeMorphed: element => !canRefreshFrame(element)
|
3826
3882
|
}
|
3827
3883
|
});
|
3828
|
-
|
3829
|
-
|
3830
|
-
#shouldMorphElement=(oldNode, newNode) => {
|
3831
|
-
if (oldNode instanceof HTMLElement) {
|
3832
|
-
if (!oldNode.hasAttribute("data-turbo-permanent") && (this.isMorphingTurboFrame || !this.#isFrameReloadedWithMorph(oldNode))) {
|
3833
|
-
const event = dispatch("turbo:before-morph-element", {
|
3834
|
-
cancelable: true,
|
3835
|
-
target: oldNode,
|
3836
|
-
detail: {
|
3837
|
-
newElement: newNode
|
3838
|
-
}
|
3839
|
-
});
|
3840
|
-
return !event.defaultPrevented;
|
3841
|
-
} else {
|
3842
|
-
return false;
|
3843
|
-
}
|
3884
|
+
for (const frame of currentElement.querySelectorAll("turbo-frame")) {
|
3885
|
+
if (canRefreshFrame(frame)) refreshFrame(frame);
|
3844
3886
|
}
|
3845
|
-
|
3846
|
-
#shouldUpdateAttribute=(attributeName, target, mutationType) => {
|
3847
|
-
const event = dispatch("turbo:before-morph-attribute", {
|
3848
|
-
cancelable: true,
|
3849
|
-
target: target,
|
3850
|
-
detail: {
|
3851
|
-
attributeName: attributeName,
|
3852
|
-
mutationType: mutationType
|
3853
|
-
}
|
3854
|
-
});
|
3855
|
-
return !event.defaultPrevented;
|
3856
|
-
};
|
3857
|
-
#didMorphElement=(oldNode, newNode) => {
|
3858
|
-
if (newNode instanceof HTMLElement) {
|
3859
|
-
dispatch("turbo:morph-element", {
|
3860
|
-
target: oldNode,
|
3861
|
-
detail: {
|
3862
|
-
newElement: newNode
|
3863
|
-
}
|
3864
|
-
});
|
3865
|
-
}
|
3866
|
-
};
|
3867
|
-
#shouldRemoveElement=node => this.#shouldMorphElement(node);
|
3868
|
-
#reloadRemoteFrames() {
|
3869
|
-
this.#remoteFrames().forEach((frame => {
|
3870
|
-
if (this.#isFrameReloadedWithMorph(frame)) {
|
3871
|
-
this.#renderFrameWithMorph(frame);
|
3872
|
-
frame.reload();
|
3873
|
-
}
|
3874
|
-
}));
|
3875
|
-
}
|
3876
|
-
#renderFrameWithMorph(frame) {
|
3877
|
-
frame.addEventListener("turbo:before-frame-render", (event => {
|
3878
|
-
event.detail.render = this.#morphFrameUpdate;
|
3879
|
-
}), {
|
3880
|
-
once: true
|
3881
|
-
});
|
3882
|
-
}
|
3883
|
-
#morphFrameUpdate=(currentElement, newElement) => {
|
3884
|
-
dispatch("turbo:before-frame-morph", {
|
3885
|
-
target: currentElement,
|
3887
|
+
dispatch("turbo:morph", {
|
3886
3888
|
detail: {
|
3887
3889
|
currentElement: currentElement,
|
3888
3890
|
newElement: newElement
|
3889
3891
|
}
|
3890
3892
|
});
|
3891
|
-
this.#morphElements(currentElement, newElement.children, "innerHTML");
|
3892
|
-
};
|
3893
|
-
#isFrameReloadedWithMorph(element) {
|
3894
|
-
return element.src && element.refresh === "morph";
|
3895
3893
|
}
|
3896
|
-
|
3897
|
-
return
|
3894
|
+
async preservingPermanentElements(callback) {
|
3895
|
+
return await callback();
|
3896
|
+
}
|
3897
|
+
get renderMethod() {
|
3898
|
+
return "morph";
|
3899
|
+
}
|
3900
|
+
get shouldAutofocus() {
|
3901
|
+
return false;
|
3898
3902
|
}
|
3899
3903
|
}
|
3900
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();
|
3916
|
+
}
|
3917
|
+
|
3901
3918
|
class SnapshotCache {
|
3902
3919
|
keys=[];
|
3903
3920
|
snapshots={};
|
@@ -3951,8 +3968,8 @@ class PageView extends View {
|
|
3951
3968
|
}
|
3952
3969
|
renderPage(snapshot, isPreview = false, willRender = true, visit) {
|
3953
3970
|
const shouldMorphPage = this.isPageRefresh(visit) && this.snapshot.shouldMorphPage;
|
3954
|
-
const rendererClass = shouldMorphPage ?
|
3955
|
-
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);
|
3956
3973
|
if (!renderer.shouldRender) {
|
3957
3974
|
this.forceReloaded = true;
|
3958
3975
|
} else {
|
@@ -4143,7 +4160,7 @@ class Session {
|
|
4143
4160
|
}
|
4144
4161
|
refresh(url, requestId) {
|
4145
4162
|
const isRecentRequest = requestId && this.recentRequests.has(requestId);
|
4146
|
-
if (!isRecentRequest) {
|
4163
|
+
if (!isRecentRequest && !this.navigator.currentVisit) {
|
4147
4164
|
this.visit(url, {
|
4148
4165
|
action: "replace",
|
4149
4166
|
shouldCacheSnapshot: false
|
@@ -4969,12 +4986,24 @@ const StreamActions = {
|
|
4969
4986
|
this.targetElements.forEach((e => e.remove()));
|
4970
4987
|
},
|
4971
4988
|
replace() {
|
4972
|
-
|
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
|
+
}));
|
4973
4997
|
},
|
4974
4998
|
update() {
|
4999
|
+
const method = this.getAttribute("method");
|
4975
5000
|
this.targetElements.forEach((targetElement => {
|
4976
|
-
|
4977
|
-
|
5001
|
+
if (method === "morph") {
|
5002
|
+
morphChildren(targetElement, this.templateContent);
|
5003
|
+
} else {
|
5004
|
+
targetElement.innerHTML = "";
|
5005
|
+
targetElement.append(this.templateContent);
|
5006
|
+
}
|
4978
5007
|
}));
|
4979
5008
|
},
|
4980
5009
|
refresh() {
|