turbo-rails 0.5.2 → 0.5.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 +16 -1
- data/app/assets/javascripts/turbo.js +271 -144
- data/app/helpers/turbo/includes_helper.rb +2 -0
- data/app/helpers/turbo/streams/action_helper.rb +1 -1
- data/app/helpers/turbo/streams_helper.rb +4 -4
- data/app/models/concerns/turbo/broadcastable.rb +8 -3
- data/lib/install/turbo_with_asset_pipeline.rb +19 -3
- data/lib/install/turbo_with_webpacker.rb +1 -2
- data/lib/turbo/engine.rb +1 -1
- data/lib/turbo/version.rb +1 -1
- metadata +7 -173
- data/.github/workflows/ci.yml +0 -30
- data/.gitignore +0 -2
- data/Gemfile +0 -6
- data/Gemfile.lock +0 -147
- data/package.json +0 -47
- data/rollup.config.js +0 -23
- data/test/drive/drive_helper_test.rb +0 -8
- data/test/dummy/.babelrc +0 -18
- data/test/dummy/.gitignore +0 -3
- data/test/dummy/.postcssrc.yml +0 -3
- data/test/dummy/Rakefile +0 -6
- data/test/dummy/app/assets/config/manifest.js +0 -2
- data/test/dummy/app/assets/images/.keep +0 -0
- data/test/dummy/app/assets/stylesheets/application.css +0 -15
- data/test/dummy/app/assets/stylesheets/scaffold.css +0 -80
- data/test/dummy/app/channels/application_cable/channel.rb +0 -4
- data/test/dummy/app/channels/application_cable/connection.rb +0 -4
- data/test/dummy/app/controllers/application_controller.rb +0 -2
- data/test/dummy/app/controllers/concerns/.keep +0 -0
- data/test/dummy/app/controllers/messages_controller.rb +0 -12
- data/test/dummy/app/controllers/trays_controller.rb +0 -17
- data/test/dummy/app/helpers/application_helper.rb +0 -2
- data/test/dummy/app/javascript/packs/application.js +0 -0
- data/test/dummy/app/jobs/application_job.rb +0 -2
- data/test/dummy/app/mailboxes/application_mailbox.rb +0 -2
- data/test/dummy/app/mailboxes/messages_mailbox.rb +0 -4
- data/test/dummy/app/mailers/application_mailer.rb +0 -4
- data/test/dummy/app/models/application_record.rb +0 -3
- data/test/dummy/app/models/concerns/.keep +0 -0
- data/test/dummy/app/models/message.rb +0 -29
- data/test/dummy/app/views/layouts/application.html.erb +0 -14
- data/test/dummy/app/views/layouts/mailer.html.erb +0 -13
- data/test/dummy/app/views/layouts/mailer.text.erb +0 -1
- data/test/dummy/app/views/messages/_message.html.erb +0 -1
- data/test/dummy/app/views/messages/_message.turbo_stream.erb +0 -1
- data/test/dummy/app/views/messages/show.turbo_stream.erb +0 -9
- data/test/dummy/app/views/trays/index.html.erb +0 -3
- data/test/dummy/app/views/trays/show.html.erb +0 -3
- data/test/dummy/bin/bundle +0 -3
- data/test/dummy/bin/rails +0 -4
- data/test/dummy/bin/rake +0 -4
- data/test/dummy/bin/setup +0 -36
- data/test/dummy/bin/update +0 -31
- data/test/dummy/bin/yarn +0 -11
- data/test/dummy/config.ru +0 -5
- data/test/dummy/config/application.rb +0 -22
- data/test/dummy/config/boot.rb +0 -5
- data/test/dummy/config/cable.yml +0 -10
- data/test/dummy/config/environment.rb +0 -5
- data/test/dummy/config/environments/development.rb +0 -34
- data/test/dummy/config/environments/production.rb +0 -96
- data/test/dummy/config/environments/test.rb +0 -38
- data/test/dummy/config/initializers/application_controller_renderer.rb +0 -8
- data/test/dummy/config/initializers/assets.rb +0 -14
- data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
- data/test/dummy/config/initializers/content_security_policy.rb +0 -22
- data/test/dummy/config/initializers/cookies_serializer.rb +0 -5
- data/test/dummy/config/initializers/filter_parameter_logging.rb +0 -4
- data/test/dummy/config/initializers/inflections.rb +0 -16
- data/test/dummy/config/initializers/mime_types.rb +0 -4
- data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
- data/test/dummy/config/locales/en.yml +0 -33
- data/test/dummy/config/puma.rb +0 -34
- data/test/dummy/config/routes.rb +0 -4
- data/test/dummy/config/spring.rb +0 -6
- data/test/dummy/config/webpack/development.js +0 -3
- data/test/dummy/config/webpack/environment.js +0 -3
- data/test/dummy/config/webpack/production.js +0 -3
- data/test/dummy/config/webpack/test.js +0 -3
- data/test/dummy/config/webpacker.yml +0 -65
- data/test/dummy/lib/assets/.keep +0 -0
- data/test/dummy/log/.keep +0 -0
- data/test/dummy/public/404.html +0 -67
- data/test/dummy/public/422.html +0 -67
- data/test/dummy/public/500.html +0 -66
- data/test/dummy/public/apple-touch-icon-precomposed.png +0 -0
- data/test/dummy/public/apple-touch-icon.png +0 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/frames/frame_request_controller_test.rb +0 -21
- data/test/frames/frames_helper_test.rb +0 -21
- data/test/native/navigation_controller_test.rb +0 -42
- data/test/streams/broadcastable_test.rb +0 -80
- data/test/streams/streams_channel_test.rb +0 -105
- data/test/streams/streams_controller_test.rb +0 -29
- data/test/turbo_test.rb +0 -10
- data/turbo-rails.gemspec +0 -17
- data/yarn.lock +0 -283
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bde12fe28a415f37f7000160bfe9577c066d0a450b2a8b3a072acb11a620f229
|
4
|
+
data.tar.gz: 06e64c67ade4fa00e7ba13eabf31e3f78fef72e8e9faedd66861b0d370c9e94e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d18cfb444d1511997d07d15e6696ef4a22dafef448ddbe21b0037c60c1b565ab4a24a623393dd9dbadb8685336f0fd6487d0350d4895d336b041b5d07bca7f66
|
7
|
+
data.tar.gz: 1e094bc98631469c394117c65f8e73387c577c4d3e1fdfa329c9bf5ef3f5fd62ea7ba0a4efc394f8696e087f8f06c8870e7fe1ab4066bbe1049f659f22aa8163
|
data/README.md
CHANGED
@@ -40,14 +40,29 @@ The JavaScript for Turbo can either be run through the asset pipeline, which is
|
|
40
40
|
|
41
41
|
Running `turbo:install` will install through NPM if Webpacker is installed in the application. Otherwise the asset pipeline version is used.
|
42
42
|
|
43
|
-
If you're using Webpack and need to use either the cable consumer or the Turbo instance, you can import [`Turbo`](https://turbo.hotwire.dev/reference/drive) and/or [`cable`](https://github.com/hotwired/turbo-rails/blob/
|
43
|
+
If you're using Webpack and need to use either the cable consumer or the Turbo instance, you can import [`Turbo`](https://turbo.hotwire.dev/reference/drive) and/or [`cable`](https://github.com/hotwired/turbo-rails/blob/main/app/javascript/turbo/cable.js) (`import { Turbo, 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)).
|
44
44
|
|
45
|
+
If you're using a [native adapter](https://turbo.hotwire.dev/handbook/native), you'll need to assign `window.Turbo`, even if it's not used for anything else:
|
46
|
+
|
47
|
+
```js
|
48
|
+
import { Turbo } from "@hotwired/turbo-rails"
|
49
|
+
window.Turbo = Turbo
|
50
|
+
```
|
45
51
|
|
46
52
|
## Usage
|
47
53
|
|
48
54
|
You can watch [the video introduction to Hotwire](https://hotwire.dev/#screencast), which focuses extensively on demonstration Turbo in a Rails demo. Then you should familiarize yourself with [Turbo handbook](https://turbo.hotwire.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).
|
49
55
|
|
50
56
|
|
57
|
+
## Compatibility with Rails UJS
|
58
|
+
|
59
|
+
Rails UJS includes helpers for sending links and forms over XMLHttpRequest, so you can respond with Ajax. Turbo supersedes this functionality, so you should ensure that you're either running Rails 6.1 with the defaults that turn this off for forms, or that you add `config.action_view.form_with_generates_remote_forms = false` to your `config/application.rb`.
|
60
|
+
|
61
|
+
Note that the helpers that turn `link_to` into remote invocations will _not_ currently work with Turbo. Links that have been made remote will not stick within frames nor will they allow you to respond with turbo stream actions. The recommendation is to replace these links with styled `button_to`, so you'll flow through a regular form, and you'll be better off with a11y compliance.
|
62
|
+
|
63
|
+
You can still use the `data-confirm` and `data-disable-with`.
|
64
|
+
|
65
|
+
|
51
66
|
## Development
|
52
67
|
|
53
68
|
* To run the Rails tests: `bundle exec rake`.
|
@@ -19,7 +19,7 @@ const submittersByForm = new WeakMap;
|
|
19
19
|
function findSubmitterFromClickTarget(target) {
|
20
20
|
const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
|
21
21
|
const candidate = element ? element.closest("input, button") : null;
|
22
|
-
return (candidate === null || candidate === void 0 ? void 0 : candidate.
|
22
|
+
return (candidate === null || candidate === void 0 ? void 0 : candidate.type) == "submit" ? candidate : null;
|
23
23
|
}
|
24
24
|
|
25
25
|
function clickCaptured(event) {
|
@@ -41,6 +41,97 @@ function clickCaptured(event) {
|
|
41
41
|
});
|
42
42
|
})();
|
43
43
|
|
44
|
+
var FrameLoadingStyle;
|
45
|
+
|
46
|
+
(function(FrameLoadingStyle) {
|
47
|
+
FrameLoadingStyle["eager"] = "eager";
|
48
|
+
FrameLoadingStyle["lazy"] = "lazy";
|
49
|
+
})(FrameLoadingStyle || (FrameLoadingStyle = {}));
|
50
|
+
|
51
|
+
class FrameElement extends HTMLElement {
|
52
|
+
constructor() {
|
53
|
+
super();
|
54
|
+
this.loaded = Promise.resolve();
|
55
|
+
this.delegate = new FrameElement.delegateConstructor(this);
|
56
|
+
}
|
57
|
+
static get observedAttributes() {
|
58
|
+
return [ "loading", "src" ];
|
59
|
+
}
|
60
|
+
connectedCallback() {
|
61
|
+
this.delegate.connect();
|
62
|
+
}
|
63
|
+
disconnectedCallback() {
|
64
|
+
this.delegate.disconnect();
|
65
|
+
}
|
66
|
+
attributeChangedCallback(name) {
|
67
|
+
if (name == "loading") {
|
68
|
+
this.delegate.loadingStyleChanged();
|
69
|
+
} else if (name == "src") {
|
70
|
+
this.delegate.sourceURLChanged();
|
71
|
+
}
|
72
|
+
}
|
73
|
+
get src() {
|
74
|
+
return this.getAttribute("src");
|
75
|
+
}
|
76
|
+
set src(value) {
|
77
|
+
if (value) {
|
78
|
+
this.setAttribute("src", value);
|
79
|
+
} else {
|
80
|
+
this.removeAttribute("src");
|
81
|
+
}
|
82
|
+
}
|
83
|
+
get loading() {
|
84
|
+
return frameLoadingStyleFromString(this.getAttribute("loading") || "");
|
85
|
+
}
|
86
|
+
set loading(value) {
|
87
|
+
if (value) {
|
88
|
+
this.setAttribute("loading", value);
|
89
|
+
} else {
|
90
|
+
this.removeAttribute("loading");
|
91
|
+
}
|
92
|
+
}
|
93
|
+
get disabled() {
|
94
|
+
return this.hasAttribute("disabled");
|
95
|
+
}
|
96
|
+
set disabled(value) {
|
97
|
+
if (value) {
|
98
|
+
this.setAttribute("disabled", "");
|
99
|
+
} else {
|
100
|
+
this.removeAttribute("disabled");
|
101
|
+
}
|
102
|
+
}
|
103
|
+
get autoscroll() {
|
104
|
+
return this.hasAttribute("autoscroll");
|
105
|
+
}
|
106
|
+
set autoscroll(value) {
|
107
|
+
if (value) {
|
108
|
+
this.setAttribute("autoscroll", "");
|
109
|
+
} else {
|
110
|
+
this.removeAttribute("autoscroll");
|
111
|
+
}
|
112
|
+
}
|
113
|
+
get complete() {
|
114
|
+
return !this.delegate.isLoading;
|
115
|
+
}
|
116
|
+
get isActive() {
|
117
|
+
return this.ownerDocument === document && !this.isPreview;
|
118
|
+
}
|
119
|
+
get isPreview() {
|
120
|
+
var _a, _b;
|
121
|
+
return (_b = (_a = this.ownerDocument) === null || _a === void 0 ? void 0 : _a.documentElement) === null || _b === void 0 ? void 0 : _b.hasAttribute("data-turbo-preview");
|
122
|
+
}
|
123
|
+
}
|
124
|
+
|
125
|
+
function frameLoadingStyleFromString(style) {
|
126
|
+
switch (style.toLowerCase()) {
|
127
|
+
case "lazy":
|
128
|
+
return FrameLoadingStyle.lazy;
|
129
|
+
|
130
|
+
default:
|
131
|
+
return FrameLoadingStyle.eager;
|
132
|
+
}
|
133
|
+
}
|
134
|
+
|
44
135
|
class Location {
|
45
136
|
constructor(url) {
|
46
137
|
const linkWithAnchor = document.createElement("a");
|
@@ -129,6 +220,12 @@ class FetchResponse {
|
|
129
220
|
get failed() {
|
130
221
|
return !this.succeeded;
|
131
222
|
}
|
223
|
+
get clientError() {
|
224
|
+
return this.statusCode >= 400 && this.statusCode <= 499;
|
225
|
+
}
|
226
|
+
get serverError() {
|
227
|
+
return this.statusCode >= 500 && this.statusCode <= 599;
|
228
|
+
}
|
132
229
|
get redirected() {
|
133
230
|
return this.response.redirected;
|
134
231
|
}
|
@@ -136,7 +233,7 @@ class FetchResponse {
|
|
136
233
|
return Location.wrap(this.response.url);
|
137
234
|
}
|
138
235
|
get isHTML() {
|
139
|
-
return this.contentType && this.contentType.match(/^text\/html
|
236
|
+
return this.contentType && this.contentType.match(/^(?:text\/([^\s;,]+\b)?html|application\/xhtml\+xml)\b/);
|
140
237
|
}
|
141
238
|
get statusCode() {
|
142
239
|
return this.response.status;
|
@@ -330,27 +427,30 @@ class FetchRequest {
|
|
330
427
|
}
|
331
428
|
}
|
332
429
|
|
333
|
-
class
|
430
|
+
class AppearanceObserver {
|
334
431
|
constructor(delegate, element) {
|
335
|
-
this.
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
event.preventDefault();
|
341
|
-
event.stopImmediatePropagation();
|
342
|
-
this.delegate.formSubmissionIntercepted(form, submitter);
|
343
|
-
}
|
432
|
+
this.started = false;
|
433
|
+
this.intersect = entries => {
|
434
|
+
const lastEntry = entries.slice(-1)[0];
|
435
|
+
if (lastEntry === null || lastEntry === void 0 ? void 0 : lastEntry.isIntersecting) {
|
436
|
+
this.delegate.elementAppearedInViewport(this.element);
|
344
437
|
}
|
345
438
|
};
|
346
439
|
this.delegate = delegate;
|
347
440
|
this.element = element;
|
441
|
+
this.intersectionObserver = new IntersectionObserver(this.intersect);
|
348
442
|
}
|
349
443
|
start() {
|
350
|
-
|
444
|
+
if (!this.started) {
|
445
|
+
this.started = true;
|
446
|
+
this.intersectionObserver.observe(this.element);
|
447
|
+
}
|
351
448
|
}
|
352
449
|
stop() {
|
353
|
-
|
450
|
+
if (this.started) {
|
451
|
+
this.started = false;
|
452
|
+
this.intersectionObserver.unobserve(this.element);
|
453
|
+
}
|
354
454
|
}
|
355
455
|
}
|
356
456
|
|
@@ -377,7 +477,7 @@ class FormSubmission {
|
|
377
477
|
}
|
378
478
|
get method() {
|
379
479
|
var _a;
|
380
|
-
const method = ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formmethod")) || this.formElement.method;
|
480
|
+
const method = ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formmethod")) || this.formElement.getAttribute("method") || "";
|
381
481
|
return fetchMethodFromString(method.toLowerCase()) || FetchMethod.get;
|
382
482
|
}
|
383
483
|
get action() {
|
@@ -429,7 +529,9 @@ class FormSubmission {
|
|
429
529
|
};
|
430
530
|
}
|
431
531
|
requestSucceededWithResponse(request, response) {
|
432
|
-
if (
|
532
|
+
if (response.clientError || response.serverError) {
|
533
|
+
this.delegate.formSubmissionFailedWithResponse(this, response);
|
534
|
+
} else if (this.requestMustRedirect(request) && responseSucceededWithoutRedirect(response)) {
|
433
535
|
const error = new Error("Form responses must redirect to another location");
|
434
536
|
this.delegate.formSubmissionErrored(this, error);
|
435
537
|
} else {
|
@@ -496,6 +598,34 @@ function getMetaContent(name) {
|
|
496
598
|
return element && element.content;
|
497
599
|
}
|
498
600
|
|
601
|
+
function responseSucceededWithoutRedirect(response) {
|
602
|
+
return response.statusCode == 200 && !response.redirected;
|
603
|
+
}
|
604
|
+
|
605
|
+
class FormInterceptor {
|
606
|
+
constructor(delegate, element) {
|
607
|
+
this.submitBubbled = event => {
|
608
|
+
if (event.target instanceof HTMLFormElement) {
|
609
|
+
const form = event.target;
|
610
|
+
const submitter = event.submitter || undefined;
|
611
|
+
if (this.delegate.shouldInterceptFormSubmission(form, submitter)) {
|
612
|
+
event.preventDefault();
|
613
|
+
event.stopImmediatePropagation();
|
614
|
+
this.delegate.formSubmissionIntercepted(form, submitter);
|
615
|
+
}
|
616
|
+
}
|
617
|
+
};
|
618
|
+
this.delegate = delegate;
|
619
|
+
this.element = element;
|
620
|
+
}
|
621
|
+
start() {
|
622
|
+
this.element.addEventListener("submit", this.submitBubbled);
|
623
|
+
}
|
624
|
+
stop() {
|
625
|
+
this.element.removeEventListener("submit", this.submitBubbled);
|
626
|
+
}
|
627
|
+
}
|
628
|
+
|
499
629
|
class LinkInterceptor {
|
500
630
|
constructor(delegate, element) {
|
501
631
|
this.clickBubbled = event => {
|
@@ -541,17 +671,61 @@ class FrameController {
|
|
541
671
|
constructor(element) {
|
542
672
|
this.resolveVisitPromise = () => {};
|
543
673
|
this.element = element;
|
674
|
+
this.appearanceObserver = new AppearanceObserver(this, this.element);
|
544
675
|
this.linkInterceptor = new LinkInterceptor(this, this.element);
|
545
676
|
this.formInterceptor = new FormInterceptor(this, this.element);
|
546
677
|
}
|
547
678
|
connect() {
|
679
|
+
if (this.loadingStyle == FrameLoadingStyle.lazy) {
|
680
|
+
this.appearanceObserver.start();
|
681
|
+
}
|
548
682
|
this.linkInterceptor.start();
|
549
683
|
this.formInterceptor.start();
|
550
684
|
}
|
551
685
|
disconnect() {
|
686
|
+
this.appearanceObserver.stop();
|
552
687
|
this.linkInterceptor.stop();
|
553
688
|
this.formInterceptor.stop();
|
554
689
|
}
|
690
|
+
sourceURLChanged() {
|
691
|
+
if (this.loadingStyle == FrameLoadingStyle.eager) {
|
692
|
+
this.loadSourceURL();
|
693
|
+
}
|
694
|
+
}
|
695
|
+
loadingStyleChanged() {
|
696
|
+
if (this.loadingStyle == FrameLoadingStyle.lazy) {
|
697
|
+
this.appearanceObserver.start();
|
698
|
+
} else {
|
699
|
+
this.appearanceObserver.stop();
|
700
|
+
this.loadSourceURL();
|
701
|
+
}
|
702
|
+
}
|
703
|
+
async loadSourceURL() {
|
704
|
+
if (this.isActive && this.sourceURL && this.sourceURL != this.loadingURL) {
|
705
|
+
try {
|
706
|
+
this.loadingURL = this.sourceURL;
|
707
|
+
this.element.loaded = this.visit(this.sourceURL);
|
708
|
+
this.appearanceObserver.stop();
|
709
|
+
await this.element.loaded;
|
710
|
+
} finally {
|
711
|
+
delete this.loadingURL;
|
712
|
+
}
|
713
|
+
}
|
714
|
+
}
|
715
|
+
async loadResponse(response) {
|
716
|
+
const fragment = fragmentFromHTML(await response.responseHTML);
|
717
|
+
if (fragment) {
|
718
|
+
const element = await this.extractForeignFrameElement(fragment);
|
719
|
+
await nextAnimationFrame();
|
720
|
+
this.loadFrameElement(element);
|
721
|
+
this.scrollFrameIntoView(element);
|
722
|
+
await nextAnimationFrame();
|
723
|
+
this.focusFirstAutofocusableElement();
|
724
|
+
}
|
725
|
+
}
|
726
|
+
elementAppearedInViewport(element) {
|
727
|
+
this.loadSourceURL();
|
728
|
+
}
|
555
729
|
shouldInterceptLinkClick(element, url) {
|
556
730
|
return this.shouldInterceptNavigation(element);
|
557
731
|
}
|
@@ -572,17 +746,6 @@ class FrameController {
|
|
572
746
|
this.formSubmission.start();
|
573
747
|
}
|
574
748
|
}
|
575
|
-
async visit(url) {
|
576
|
-
const location = Location.wrap(url);
|
577
|
-
const request = new FetchRequest(this, FetchMethod.get, location);
|
578
|
-
return new Promise((resolve => {
|
579
|
-
this.resolveVisitPromise = () => {
|
580
|
-
this.resolveVisitPromise = () => {};
|
581
|
-
resolve();
|
582
|
-
};
|
583
|
-
request.perform();
|
584
|
-
}));
|
585
|
-
}
|
586
749
|
additionalHeadersForRequest(request) {
|
587
750
|
return {
|
588
751
|
"Turbo-Frame": this.id
|
@@ -612,11 +775,24 @@ class FrameController {
|
|
612
775
|
formSubmissionStarted(formSubmission) {}
|
613
776
|
formSubmissionSucceededWithResponse(formSubmission, response) {
|
614
777
|
const frame = this.findFrameElement(formSubmission.formElement);
|
615
|
-
frame.
|
778
|
+
frame.delegate.loadResponse(response);
|
779
|
+
}
|
780
|
+
formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
|
781
|
+
this.element.delegate.loadResponse(fetchResponse);
|
616
782
|
}
|
617
|
-
formSubmissionFailedWithResponse(formSubmission, fetchResponse) {}
|
618
783
|
formSubmissionErrored(formSubmission, error) {}
|
619
784
|
formSubmissionFinished(formSubmission) {}
|
785
|
+
async visit(url) {
|
786
|
+
const location = Location.wrap(url);
|
787
|
+
const request = new FetchRequest(this, FetchMethod.get, location);
|
788
|
+
return new Promise((resolve => {
|
789
|
+
this.resolveVisitPromise = () => {
|
790
|
+
this.resolveVisitPromise = () => {};
|
791
|
+
resolve();
|
792
|
+
};
|
793
|
+
request.perform();
|
794
|
+
}));
|
795
|
+
}
|
620
796
|
navigateFrame(element, url) {
|
621
797
|
const frame = this.findFrameElement(element);
|
622
798
|
frame.src = url;
|
@@ -626,17 +802,6 @@ class FrameController {
|
|
626
802
|
const id = element.getAttribute("data-turbo-frame");
|
627
803
|
return (_a = getFrameElementById(id)) !== null && _a !== void 0 ? _a : this.element;
|
628
804
|
}
|
629
|
-
async loadResponse(response) {
|
630
|
-
const fragment = fragmentFromHTML(await response.responseHTML);
|
631
|
-
const element = await this.extractForeignFrameElement(fragment);
|
632
|
-
if (element) {
|
633
|
-
await nextAnimationFrame();
|
634
|
-
this.loadFrameElement(element);
|
635
|
-
this.scrollFrameIntoView(element);
|
636
|
-
await nextAnimationFrame();
|
637
|
-
this.focusFirstAutofocusableElement();
|
638
|
-
}
|
639
|
-
}
|
640
805
|
async extractForeignFrameElement(container) {
|
641
806
|
let element;
|
642
807
|
const id = CSS.escape(this.id);
|
@@ -647,6 +812,8 @@ class FrameController {
|
|
647
812
|
await element.loaded;
|
648
813
|
return await this.extractForeignFrameElement(element);
|
649
814
|
}
|
815
|
+
console.error(`Response has no matching <turbo-frame id="${id}"> element`);
|
816
|
+
return new FrameElement;
|
650
817
|
}
|
651
818
|
loadFrameElement(frameElement) {
|
652
819
|
var _a;
|
@@ -703,6 +870,18 @@ class FrameController {
|
|
703
870
|
get enabled() {
|
704
871
|
return !this.element.disabled;
|
705
872
|
}
|
873
|
+
get sourceURL() {
|
874
|
+
return this.element.src;
|
875
|
+
}
|
876
|
+
get loadingStyle() {
|
877
|
+
return this.element.loading;
|
878
|
+
}
|
879
|
+
get isLoading() {
|
880
|
+
return this.formSubmission !== undefined || this.loadingURL !== undefined;
|
881
|
+
}
|
882
|
+
get isActive() {
|
883
|
+
return this.element.isActive;
|
884
|
+
}
|
706
885
|
}
|
707
886
|
|
708
887
|
function getFrameElementById(id) {
|
@@ -722,9 +901,11 @@ function readScrollLogicalPosition(value, defaultValue) {
|
|
722
901
|
}
|
723
902
|
}
|
724
903
|
|
725
|
-
function fragmentFromHTML(html
|
726
|
-
|
727
|
-
|
904
|
+
function fragmentFromHTML(html) {
|
905
|
+
if (html) {
|
906
|
+
const foreignDocument = document.implementation.createHTMLDocument();
|
907
|
+
return foreignDocument.createRange().createContextualFragment(html);
|
908
|
+
}
|
728
909
|
}
|
729
910
|
|
730
911
|
function activateElement(element) {
|
@@ -736,76 +917,6 @@ function activateElement(element) {
|
|
736
917
|
}
|
737
918
|
}
|
738
919
|
|
739
|
-
class FrameElement extends HTMLElement {
|
740
|
-
constructor() {
|
741
|
-
super();
|
742
|
-
this.controller = new FrameController(this);
|
743
|
-
}
|
744
|
-
static get observedAttributes() {
|
745
|
-
return [ "src" ];
|
746
|
-
}
|
747
|
-
connectedCallback() {
|
748
|
-
this.controller.connect();
|
749
|
-
}
|
750
|
-
disconnectedCallback() {
|
751
|
-
this.controller.disconnect();
|
752
|
-
}
|
753
|
-
attributeChangedCallback() {
|
754
|
-
if (this.src && this.isActive) {
|
755
|
-
const value = this.controller.visit(this.src);
|
756
|
-
Object.defineProperty(this, "loaded", {
|
757
|
-
value: value,
|
758
|
-
configurable: true
|
759
|
-
});
|
760
|
-
}
|
761
|
-
}
|
762
|
-
formSubmissionIntercepted(element, submitter) {
|
763
|
-
this.controller.formSubmissionIntercepted(element, submitter);
|
764
|
-
}
|
765
|
-
get src() {
|
766
|
-
return this.getAttribute("src");
|
767
|
-
}
|
768
|
-
set src(value) {
|
769
|
-
if (value) {
|
770
|
-
this.setAttribute("src", value);
|
771
|
-
} else {
|
772
|
-
this.removeAttribute("src");
|
773
|
-
}
|
774
|
-
}
|
775
|
-
get loaded() {
|
776
|
-
return Promise.resolve(undefined);
|
777
|
-
}
|
778
|
-
get disabled() {
|
779
|
-
return this.hasAttribute("disabled");
|
780
|
-
}
|
781
|
-
set disabled(value) {
|
782
|
-
if (value) {
|
783
|
-
this.setAttribute("disabled", "");
|
784
|
-
} else {
|
785
|
-
this.removeAttribute("disabled");
|
786
|
-
}
|
787
|
-
}
|
788
|
-
get autoscroll() {
|
789
|
-
return this.hasAttribute("autoscroll");
|
790
|
-
}
|
791
|
-
set autoscroll(value) {
|
792
|
-
if (value) {
|
793
|
-
this.setAttribute("autoscroll", "");
|
794
|
-
} else {
|
795
|
-
this.removeAttribute("autoscroll");
|
796
|
-
}
|
797
|
-
}
|
798
|
-
get isActive() {
|
799
|
-
return this.ownerDocument === document && !this.isPreview;
|
800
|
-
}
|
801
|
-
get isPreview() {
|
802
|
-
var _a, _b;
|
803
|
-
return (_b = (_a = this.ownerDocument) === null || _a === void 0 ? void 0 : _a.documentElement) === null || _b === void 0 ? void 0 : _b.hasAttribute("data-turbo-preview");
|
804
|
-
}
|
805
|
-
}
|
806
|
-
|
807
|
-
customElements.define("turbo-frame", FrameElement);
|
808
|
-
|
809
920
|
const StreamActions = {
|
810
921
|
append() {
|
811
922
|
var _a;
|
@@ -902,6 +1013,10 @@ class StreamElement extends HTMLElement {
|
|
902
1013
|
}
|
903
1014
|
}
|
904
1015
|
|
1016
|
+
FrameElement.delegateConstructor = FrameController;
|
1017
|
+
|
1018
|
+
customElements.define("turbo-frame", FrameElement);
|
1019
|
+
|
905
1020
|
customElements.define("turbo-stream", StreamElement);
|
906
1021
|
|
907
1022
|
(() => {
|
@@ -1570,7 +1685,8 @@ class FormSubmitObserver {
|
|
1570
1685
|
const form = event.target instanceof HTMLFormElement ? event.target : undefined;
|
1571
1686
|
const submitter = event.submitter || undefined;
|
1572
1687
|
if (form) {
|
1573
|
-
|
1688
|
+
const method = (submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("formmethod")) || form.method;
|
1689
|
+
if (method != "dialog" && this.delegate.willSubmitForm(form, submitter)) {
|
1574
1690
|
event.preventDefault();
|
1575
1691
|
this.delegate.formSubmitted(form, submitter);
|
1576
1692
|
}
|
@@ -1622,7 +1738,7 @@ class FrameRedirector {
|
|
1622
1738
|
formSubmissionIntercepted(element, submitter) {
|
1623
1739
|
const frame = this.findFrameElement(element);
|
1624
1740
|
if (frame) {
|
1625
|
-
frame.formSubmissionIntercepted(element, submitter);
|
1741
|
+
frame.delegate.formSubmissionIntercepted(element, submitter);
|
1626
1742
|
}
|
1627
1743
|
}
|
1628
1744
|
shouldRedirect(element, submitter) {
|
@@ -1666,8 +1782,6 @@ class History {
|
|
1666
1782
|
}
|
1667
1783
|
start() {
|
1668
1784
|
if (!this.started) {
|
1669
|
-
this.previousScrollRestoration = history.scrollRestoration;
|
1670
|
-
history.scrollRestoration = "manual";
|
1671
1785
|
addEventListener("popstate", this.onPopState, false);
|
1672
1786
|
addEventListener("load", this.onPageLoad, false);
|
1673
1787
|
this.started = true;
|
@@ -1675,9 +1789,7 @@ class History {
|
|
1675
1789
|
}
|
1676
1790
|
}
|
1677
1791
|
stop() {
|
1678
|
-
var _a;
|
1679
1792
|
if (this.started) {
|
1680
|
-
history.scrollRestoration = (_a = this.previousScrollRestoration) !== null && _a !== void 0 ? _a : "auto";
|
1681
1793
|
removeEventListener("popstate", this.onPopState, false);
|
1682
1794
|
removeEventListener("load", this.onPageLoad, false);
|
1683
1795
|
this.started = false;
|
@@ -1707,6 +1819,19 @@ class History {
|
|
1707
1819
|
const restorationData = this.restorationData[restorationIdentifier];
|
1708
1820
|
this.restorationData[restorationIdentifier] = Object.assign(Object.assign({}, restorationData), additionalData);
|
1709
1821
|
}
|
1822
|
+
assumeControlOfScrollRestoration() {
|
1823
|
+
var _a;
|
1824
|
+
if (!this.previousScrollRestoration) {
|
1825
|
+
this.previousScrollRestoration = (_a = history.scrollRestoration) !== null && _a !== void 0 ? _a : "auto";
|
1826
|
+
history.scrollRestoration = "manual";
|
1827
|
+
}
|
1828
|
+
}
|
1829
|
+
relinquishControlOfScrollRestoration() {
|
1830
|
+
if (this.previousScrollRestoration) {
|
1831
|
+
history.scrollRestoration = this.previousScrollRestoration;
|
1832
|
+
delete this.previousScrollRestoration;
|
1833
|
+
}
|
1834
|
+
}
|
1710
1835
|
shouldHandlePopState() {
|
1711
1836
|
return this.pageIsLoaded();
|
1712
1837
|
}
|
@@ -1803,12 +1928,10 @@ class Navigator {
|
|
1803
1928
|
}
|
1804
1929
|
formSubmissionStarted(formSubmission) {}
|
1805
1930
|
async formSubmissionSucceededWithResponse(formSubmission, fetchResponse) {
|
1806
|
-
console.log("Form submission succeeded", formSubmission);
|
1807
1931
|
if (formSubmission == this.formSubmission) {
|
1808
1932
|
const responseHTML = await fetchResponse.responseHTML;
|
1809
1933
|
if (responseHTML) {
|
1810
1934
|
if (formSubmission.method != FetchMethod.get) {
|
1811
|
-
console.log("Clearing snapshot cache after successful form submission");
|
1812
1935
|
this.view.clearSnapshotCache();
|
1813
1936
|
}
|
1814
1937
|
const {statusCode: statusCode} = fetchResponse;
|
@@ -1818,17 +1941,21 @@ class Navigator {
|
|
1818
1941
|
responseHTML: responseHTML
|
1819
1942
|
}
|
1820
1943
|
};
|
1821
|
-
console.log("Visiting", fetchResponse.location, visitOptions);
|
1822
1944
|
this.proposeVisit(fetchResponse.location, visitOptions);
|
1823
1945
|
}
|
1824
1946
|
}
|
1825
1947
|
}
|
1826
|
-
formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
|
1827
|
-
|
1828
|
-
|
1829
|
-
|
1830
|
-
|
1948
|
+
async formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
|
1949
|
+
const responseHTML = await fetchResponse.responseHTML;
|
1950
|
+
if (responseHTML) {
|
1951
|
+
const snapshot = Snapshot.fromHTMLString(responseHTML);
|
1952
|
+
this.view.render({
|
1953
|
+
snapshot: snapshot
|
1954
|
+
}, (() => {}));
|
1955
|
+
this.view.clearSnapshotCache();
|
1956
|
+
}
|
1831
1957
|
}
|
1958
|
+
formSubmissionErrored(formSubmission, error) {}
|
1832
1959
|
formSubmissionFinished(formSubmission) {}
|
1833
1960
|
visitStarted(visit) {
|
1834
1961
|
this.delegate.visitStarted(visit);
|
@@ -1851,7 +1978,6 @@ var PageStage;
|
|
1851
1978
|
PageStage[PageStage["loading"] = 1] = "loading";
|
1852
1979
|
PageStage[PageStage["interactive"] = 2] = "interactive";
|
1853
1980
|
PageStage[PageStage["complete"] = 3] = "complete";
|
1854
|
-
PageStage[PageStage["invalidated"] = 4] = "invalidated";
|
1855
1981
|
})(PageStage || (PageStage = {}));
|
1856
1982
|
|
1857
1983
|
class PageObserver {
|
@@ -1866,6 +1992,9 @@ class PageObserver {
|
|
1866
1992
|
this.pageIsComplete();
|
1867
1993
|
}
|
1868
1994
|
};
|
1995
|
+
this.pageWillUnload = () => {
|
1996
|
+
this.delegate.pageWillUnload();
|
1997
|
+
};
|
1869
1998
|
this.delegate = delegate;
|
1870
1999
|
}
|
1871
2000
|
start() {
|
@@ -1874,21 +2003,17 @@ class PageObserver {
|
|
1874
2003
|
this.stage = PageStage.loading;
|
1875
2004
|
}
|
1876
2005
|
document.addEventListener("readystatechange", this.interpretReadyState, false);
|
2006
|
+
addEventListener("pagehide", this.pageWillUnload, false);
|
1877
2007
|
this.started = true;
|
1878
2008
|
}
|
1879
2009
|
}
|
1880
2010
|
stop() {
|
1881
2011
|
if (this.started) {
|
1882
2012
|
document.removeEventListener("readystatechange", this.interpretReadyState, false);
|
2013
|
+
removeEventListener("pagehide", this.pageWillUnload, false);
|
1883
2014
|
this.started = false;
|
1884
2015
|
}
|
1885
2016
|
}
|
1886
|
-
invalidate() {
|
1887
|
-
if (this.stage != PageStage.invalidated) {
|
1888
|
-
this.stage = PageStage.invalidated;
|
1889
|
-
this.delegate.pageInvalidated();
|
1890
|
-
}
|
1891
|
-
}
|
1892
2017
|
pageIsInteractive() {
|
1893
2018
|
if (this.stage == PageStage.loading) {
|
1894
2019
|
this.stage = PageStage.interactive;
|
@@ -1978,7 +2103,7 @@ class StreamObserver {
|
|
1978
2103
|
const fetchOptions = (_a = event.detail) === null || _a === void 0 ? void 0 : _a.fetchOptions;
|
1979
2104
|
if (fetchOptions) {
|
1980
2105
|
const {headers: headers} = fetchOptions;
|
1981
|
-
headers.Accept = [ "text/
|
2106
|
+
headers.Accept = [ "text/vnd.turbo-stream.html", headers.Accept ].join(", ");
|
1982
2107
|
}
|
1983
2108
|
};
|
1984
2109
|
this.inspectFetchResponse = event => {
|
@@ -2046,7 +2171,7 @@ function fetchResponseFromEvent(event) {
|
|
2046
2171
|
function fetchResponseIsStream(response) {
|
2047
2172
|
var _a;
|
2048
2173
|
const contentType = (_a = response.contentType) !== null && _a !== void 0 ? _a : "";
|
2049
|
-
return
|
2174
|
+
return /^text\/vnd\.turbo-stream\.html\b/.test(contentType);
|
2050
2175
|
}
|
2051
2176
|
|
2052
2177
|
function isAction(action) {
|
@@ -2475,7 +2600,7 @@ class Session {
|
|
2475
2600
|
});
|
2476
2601
|
}
|
2477
2602
|
willFollowLinkToLocation(link, location) {
|
2478
|
-
return this.
|
2603
|
+
return this.elementIsNavigable(link) && this.locationIsVisitable(location) && this.applicationAllowsFollowingLinkToLocation(link, location);
|
2479
2604
|
}
|
2480
2605
|
followedLinkToLocation(link, location) {
|
2481
2606
|
const action = this.getActionForLink(link);
|
@@ -2496,7 +2621,7 @@ class Session {
|
|
2496
2621
|
this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
|
2497
2622
|
}
|
2498
2623
|
willSubmitForm(form, submitter) {
|
2499
|
-
return
|
2624
|
+
return this.elementIsNavigable(form) && this.elementIsNavigable(submitter);
|
2500
2625
|
}
|
2501
2626
|
formSubmitted(form, submitter) {
|
2502
2627
|
this.navigator.submitForm(form, submitter);
|
@@ -2505,9 +2630,11 @@ class Session {
|
|
2505
2630
|
this.view.lastRenderedLocation = this.location;
|
2506
2631
|
this.notifyApplicationAfterPageLoad();
|
2507
2632
|
}
|
2508
|
-
pageLoaded() {
|
2509
|
-
|
2510
|
-
|
2633
|
+
pageLoaded() {
|
2634
|
+
this.history.assumeControlOfScrollRestoration();
|
2635
|
+
}
|
2636
|
+
pageWillUnload() {
|
2637
|
+
this.history.relinquishControlOfScrollRestoration();
|
2511
2638
|
}
|
2512
2639
|
receivedMessageFromStream(message) {
|
2513
2640
|
this.renderStreamMessage(message);
|
@@ -2520,7 +2647,7 @@ class Session {
|
|
2520
2647
|
this.notifyApplicationAfterRender();
|
2521
2648
|
}
|
2522
2649
|
viewInvalidated() {
|
2523
|
-
this.
|
2650
|
+
this.adapter.pageInvalidated();
|
2524
2651
|
}
|
2525
2652
|
viewWillCacheSnapshot() {
|
2526
2653
|
this.notifyApplicationBeforeCachingSnapshot();
|
@@ -2582,8 +2709,8 @@ class Session {
|
|
2582
2709
|
const action = link.getAttribute("data-turbo-action");
|
2583
2710
|
return isAction(action) ? action : "advance";
|
2584
2711
|
}
|
2585
|
-
|
2586
|
-
const container =
|
2712
|
+
elementIsNavigable(element) {
|
2713
|
+
const container = element === null || element === void 0 ? void 0 : element.closest("[data-turbo]");
|
2587
2714
|
if (container) {
|
2588
2715
|
return container.getAttribute("data-turbo") != "false";
|
2589
2716
|
} else {
|