turbo-rails 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +24 -0
- data/.gitignore +2 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +147 -0
- data/MIT-LICENSE +20 -0
- data/README.md +66 -0
- data/Rakefile +11 -0
- data/app/assets/javascripts/turbo.js +3161 -0
- data/app/channels/turbo/streams/broadcasts.rb +66 -0
- data/app/channels/turbo/streams/stream_name.rb +24 -0
- data/app/channels/turbo/streams_channel.rb +17 -0
- data/app/controllers/turbo/frames/frame_request.rb +24 -0
- data/app/controllers/turbo/native/navigation.rb +49 -0
- data/app/controllers/turbo/native/navigation_controller.rb +13 -0
- data/app/controllers/turbo/streams/turbo_streams_tag_builder.rb +22 -0
- data/app/helpers/turbo/drive_helper.rb +16 -0
- data/app/helpers/turbo/frames_helper.rb +23 -0
- data/app/helpers/turbo/includes_helper.rb +5 -0
- data/app/helpers/turbo/streams/action_helper.rb +25 -0
- data/app/helpers/turbo/streams_helper.rb +22 -0
- data/app/javascript/turbo/cable.js +16 -0
- data/app/javascript/turbo/cable_stream_source_element.js +27 -0
- data/app/javascript/turbo/index.js +3 -0
- data/app/jobs/turbo/streams/action_broadcast_job.rb +6 -0
- data/app/jobs/turbo/streams/broadcast_job.rb +7 -0
- data/app/models/concerns/turbo/broadcastable.rb +236 -0
- data/app/models/turbo/streams/tag_builder.rb +127 -0
- data/config/routes.rb +6 -0
- data/lib/install/turbo.rb +11 -0
- data/lib/tasks/turbo_tasks.rake +6 -0
- data/lib/turbo-rails.rb +17 -0
- data/lib/turbo/engine.rb +65 -0
- data/lib/turbo/test_assertions.rb +22 -0
- data/lib/turbo/version.rb +3 -0
- data/package.json +42 -0
- data/rollup.config.js +23 -0
- data/test/drive/drive_helper_test.rb +8 -0
- data/test/dummy/.babelrc +18 -0
- data/test/dummy/.gitignore +3 -0
- data/test/dummy/.postcssrc.yml +3 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/config/manifest.js +2 -0
- data/test/dummy/app/assets/images/.keep +0 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/assets/stylesheets/scaffold.css +80 -0
- data/test/dummy/app/channels/application_cable/channel.rb +4 -0
- data/test/dummy/app/channels/application_cable/connection.rb +4 -0
- data/test/dummy/app/controllers/application_controller.rb +2 -0
- data/test/dummy/app/controllers/concerns/.keep +0 -0
- data/test/dummy/app/controllers/messages_controller.rb +12 -0
- data/test/dummy/app/controllers/trays_controller.rb +17 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/javascript/packs/application.js +0 -0
- data/test/dummy/app/jobs/application_job.rb +2 -0
- data/test/dummy/app/mailboxes/application_mailbox.rb +2 -0
- data/test/dummy/app/mailboxes/messages_mailbox.rb +4 -0
- data/test/dummy/app/mailers/application_mailer.rb +4 -0
- data/test/dummy/app/models/application_record.rb +3 -0
- data/test/dummy/app/models/concerns/.keep +0 -0
- data/test/dummy/app/models/message.rb +29 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
- data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
- data/test/dummy/app/views/messages/_message.html.erb +1 -0
- data/test/dummy/app/views/messages/_message.turbo_stream.erb +1 -0
- data/test/dummy/app/views/messages/show.turbo_stream.erb +9 -0
- data/test/dummy/app/views/trays/index.html.erb +3 -0
- data/test/dummy/app/views/trays/show.html.erb +3 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +36 -0
- data/test/dummy/bin/update +31 -0
- data/test/dummy/bin/yarn +11 -0
- data/test/dummy/config.ru +5 -0
- data/test/dummy/config/application.rb +22 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/cable.yml +10 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +34 -0
- data/test/dummy/config/environments/production.rb +96 -0
- data/test/dummy/config/environments/test.rb +38 -0
- data/test/dummy/config/initializers/application_controller_renderer.rb +8 -0
- data/test/dummy/config/initializers/assets.rb +14 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/content_security_policy.rb +22 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +33 -0
- data/test/dummy/config/puma.rb +34 -0
- data/test/dummy/config/routes.rb +4 -0
- data/test/dummy/config/spring.rb +6 -0
- data/test/dummy/config/webpack/development.js +3 -0
- data/test/dummy/config/webpack/environment.js +3 -0
- data/test/dummy/config/webpack/production.js +3 -0
- data/test/dummy/config/webpack/test.js +3 -0
- data/test/dummy/config/webpacker.yml +65 -0
- data/test/dummy/lib/assets/.keep +0 -0
- data/test/dummy/log/.keep +0 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- 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 +21 -0
- data/test/frames/frames_helper_test.rb +15 -0
- data/test/native/navigation_controller_test.rb +42 -0
- data/test/streams/broadcastable_test.rb +80 -0
- data/test/streams/streams_channel_test.rb +105 -0
- data/test/streams/streams_controller_test.rb +29 -0
- data/test/turbo_test.rb +10 -0
- data/turbo-rails.gemspec +16 -0
- data/yarn.lock +282 -0
- metadata +254 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 02b7b8f50542dc4f9ab484c802b39f3d6534a8738eed4f9366df1b15f7df47b7
|
4
|
+
data.tar.gz: c0d269a2181eb5f26b8cfa359e75f08fdcc7a3a16c3c7dd1eb83147db961d7cf
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e2804f7d6c3aa12d2c6ae1a622f6f762f6e2a42024621de99399c9c898f9f48a3d7e3d82e810b49cb0a39c39298b9e244bc7551d8236eaf1e1cb901959b3fd65
|
7
|
+
data.tar.gz: e4cfc154aae81e25edf7442a820d9ef5c33050879ab67b9b42a78aa51e8da365eab0c6d9669d3b708291ac31299e6739ead947062737af95788121e84990f328
|
@@ -0,0 +1,24 @@
|
|
1
|
+
name: CI
|
2
|
+
on: [push, pull_request]
|
3
|
+
jobs:
|
4
|
+
tests:
|
5
|
+
strategy:
|
6
|
+
matrix:
|
7
|
+
ruby-version:
|
8
|
+
- "2.7"
|
9
|
+
- head
|
10
|
+
|
11
|
+
name: ${{ format('Tests (Ruby {0})', matrix.ruby-version) }}
|
12
|
+
runs-on: ubuntu-latest
|
13
|
+
|
14
|
+
steps:
|
15
|
+
- uses: actions/checkout@v1
|
16
|
+
|
17
|
+
- name: Install Ruby
|
18
|
+
uses: ruby/setup-ruby@v1
|
19
|
+
with:
|
20
|
+
ruby-version: ${{ matrix.ruby-version }}
|
21
|
+
bundler-cache: true
|
22
|
+
|
23
|
+
- name: Run tests
|
24
|
+
run: bundle exec rake
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
turbo-rails (0.5.0)
|
5
|
+
rails (>= 6.0.0)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
actioncable (6.1.0)
|
11
|
+
actionpack (= 6.1.0)
|
12
|
+
activesupport (= 6.1.0)
|
13
|
+
nio4r (~> 2.0)
|
14
|
+
websocket-driver (>= 0.6.1)
|
15
|
+
actionmailbox (6.1.0)
|
16
|
+
actionpack (= 6.1.0)
|
17
|
+
activejob (= 6.1.0)
|
18
|
+
activerecord (= 6.1.0)
|
19
|
+
activestorage (= 6.1.0)
|
20
|
+
activesupport (= 6.1.0)
|
21
|
+
mail (>= 2.7.1)
|
22
|
+
actionmailer (6.1.0)
|
23
|
+
actionpack (= 6.1.0)
|
24
|
+
actionview (= 6.1.0)
|
25
|
+
activejob (= 6.1.0)
|
26
|
+
activesupport (= 6.1.0)
|
27
|
+
mail (~> 2.5, >= 2.5.4)
|
28
|
+
rails-dom-testing (~> 2.0)
|
29
|
+
actionpack (6.1.0)
|
30
|
+
actionview (= 6.1.0)
|
31
|
+
activesupport (= 6.1.0)
|
32
|
+
rack (~> 2.0, >= 2.0.9)
|
33
|
+
rack-test (>= 0.6.3)
|
34
|
+
rails-dom-testing (~> 2.0)
|
35
|
+
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
36
|
+
actiontext (6.1.0)
|
37
|
+
actionpack (= 6.1.0)
|
38
|
+
activerecord (= 6.1.0)
|
39
|
+
activestorage (= 6.1.0)
|
40
|
+
activesupport (= 6.1.0)
|
41
|
+
nokogiri (>= 1.8.5)
|
42
|
+
actionview (6.1.0)
|
43
|
+
activesupport (= 6.1.0)
|
44
|
+
builder (~> 3.1)
|
45
|
+
erubi (~> 1.4)
|
46
|
+
rails-dom-testing (~> 2.0)
|
47
|
+
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
48
|
+
activejob (6.1.0)
|
49
|
+
activesupport (= 6.1.0)
|
50
|
+
globalid (>= 0.3.6)
|
51
|
+
activemodel (6.1.0)
|
52
|
+
activesupport (= 6.1.0)
|
53
|
+
activerecord (6.1.0)
|
54
|
+
activemodel (= 6.1.0)
|
55
|
+
activesupport (= 6.1.0)
|
56
|
+
activestorage (6.1.0)
|
57
|
+
actionpack (= 6.1.0)
|
58
|
+
activejob (= 6.1.0)
|
59
|
+
activerecord (= 6.1.0)
|
60
|
+
activesupport (= 6.1.0)
|
61
|
+
marcel (~> 0.3.1)
|
62
|
+
mimemagic (~> 0.3.2)
|
63
|
+
activesupport (6.1.0)
|
64
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
65
|
+
i18n (>= 1.6, < 2)
|
66
|
+
minitest (>= 5.1)
|
67
|
+
tzinfo (~> 2.0)
|
68
|
+
zeitwerk (~> 2.3)
|
69
|
+
builder (3.2.4)
|
70
|
+
byebug (11.0.1)
|
71
|
+
concurrent-ruby (1.1.7)
|
72
|
+
crass (1.0.6)
|
73
|
+
erubi (1.10.0)
|
74
|
+
globalid (0.4.2)
|
75
|
+
activesupport (>= 4.2.0)
|
76
|
+
i18n (1.8.5)
|
77
|
+
concurrent-ruby (~> 1.0)
|
78
|
+
loofah (2.8.0)
|
79
|
+
crass (~> 1.0.2)
|
80
|
+
nokogiri (>= 1.5.9)
|
81
|
+
mail (2.7.1)
|
82
|
+
mini_mime (>= 0.1.1)
|
83
|
+
marcel (0.3.3)
|
84
|
+
mimemagic (~> 0.3.2)
|
85
|
+
method_source (1.0.0)
|
86
|
+
mimemagic (0.3.5)
|
87
|
+
mini_mime (1.0.2)
|
88
|
+
mini_portile2 (2.4.0)
|
89
|
+
minitest (5.14.2)
|
90
|
+
nio4r (2.5.4)
|
91
|
+
nokogiri (1.10.10)
|
92
|
+
mini_portile2 (~> 2.4.0)
|
93
|
+
rack (2.2.3)
|
94
|
+
rack-test (1.1.0)
|
95
|
+
rack (>= 1.0, < 3)
|
96
|
+
rails (6.1.0)
|
97
|
+
actioncable (= 6.1.0)
|
98
|
+
actionmailbox (= 6.1.0)
|
99
|
+
actionmailer (= 6.1.0)
|
100
|
+
actionpack (= 6.1.0)
|
101
|
+
actiontext (= 6.1.0)
|
102
|
+
actionview (= 6.1.0)
|
103
|
+
activejob (= 6.1.0)
|
104
|
+
activemodel (= 6.1.0)
|
105
|
+
activerecord (= 6.1.0)
|
106
|
+
activestorage (= 6.1.0)
|
107
|
+
activesupport (= 6.1.0)
|
108
|
+
bundler (>= 1.15.0)
|
109
|
+
railties (= 6.1.0)
|
110
|
+
sprockets-rails (>= 2.0.0)
|
111
|
+
rails-dom-testing (2.0.3)
|
112
|
+
activesupport (>= 4.2.0)
|
113
|
+
nokogiri (>= 1.6)
|
114
|
+
rails-html-sanitizer (1.3.0)
|
115
|
+
loofah (~> 2.3)
|
116
|
+
railties (6.1.0)
|
117
|
+
actionpack (= 6.1.0)
|
118
|
+
activesupport (= 6.1.0)
|
119
|
+
method_source
|
120
|
+
rake (>= 0.8.7)
|
121
|
+
thor (~> 1.0)
|
122
|
+
rake (13.0.0)
|
123
|
+
sprockets (4.0.2)
|
124
|
+
concurrent-ruby (~> 1.0)
|
125
|
+
rack (> 1, < 3)
|
126
|
+
sprockets-rails (3.2.2)
|
127
|
+
actionpack (>= 4.0)
|
128
|
+
activesupport (>= 4.0)
|
129
|
+
sprockets (>= 3.0.0)
|
130
|
+
thor (1.0.1)
|
131
|
+
tzinfo (2.0.4)
|
132
|
+
concurrent-ruby (~> 1.0)
|
133
|
+
websocket-driver (0.7.3)
|
134
|
+
websocket-extensions (>= 0.1.0)
|
135
|
+
websocket-extensions (0.1.5)
|
136
|
+
zeitwerk (2.4.2)
|
137
|
+
|
138
|
+
PLATFORMS
|
139
|
+
ruby
|
140
|
+
|
141
|
+
DEPENDENCIES
|
142
|
+
byebug
|
143
|
+
rake
|
144
|
+
turbo-rails!
|
145
|
+
|
146
|
+
BUNDLED WITH
|
147
|
+
2.1.4
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2020 Basecamp
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# Turbo
|
2
|
+
|
3
|
+
[Turbo](https://turbo.hotwire.dev) gives you the speed of a single-page web application without having to write any JavaScript. Turbo accelerates links and form submissions without requiring you to change your server-side generated HTML. It lets you carve up a page into independent frames, which can be lazy-loaded and operate as independent components. And finally, helps you make partial page updates using just HTML and a set of CRUD-like container tags. These three techniques reduce the amount of custom JavaScript that many web applications need to write by an order of magnitude. And for the few dynamic bits that are left, you're invited to finished the job with Stimulus.
|
4
|
+
|
5
|
+
On top of accelerating web applications, Turbo was built from the ground-up to form the foundation of hybrid native applications. Write the navigational shell of your Android or iOS app using the standard platform tooling, then seamlessly fill in features from the web, following native navigation patterns. Not every mobile screen needs to be written in Swift or Kotlin to feel native. With Turbo, you spend less time wrangling JSON, waiting on app stores to approve updates, or reimplementing features you've already created in HTML.
|
6
|
+
|
7
|
+
Turbo is a language-agnostic framework written in TypeScript, but this gem builds on top of those basics to make the integration with Rails as smooth as possible. You can deliver turbo updates via model callbacks over Action Cable, respond to controller actions with native navigation or standard redirects, and render turbo frames with helpers and layout-free responses.
|
8
|
+
|
9
|
+
|
10
|
+
## Turbo Drive
|
11
|
+
|
12
|
+
Turbo is a continuation of the ideas from the previous Turbolinks framework, and the heart of that past approach lives on as Turbo Drive. When installed, Turbo automatically intercepts all clicks on `<a href>` links to the same domain. When you click an eligible link, Turbo prevents the browser from following it. Instead, Turbo changes the browser’s URL using the History API, requests the new page using `fetch`, and then renders the HTML response.
|
13
|
+
|
14
|
+
During rendering, Turbo replaces the current `<body>` element outright and merges the contents of the `<head>` element. The JavaScript window and document objects, and the HTML `<html>` element, persist from one rendering to the next.
|
15
|
+
|
16
|
+
Whereas Turbolinks previously just dealt with links, Turbo can now also process form submissions and responses. This means the entire flow in the web application is wrapped into Turbo, making all the parts fast. No more need for `data-remote=true`.
|
17
|
+
|
18
|
+
|
19
|
+
## Turbo Frames
|
20
|
+
|
21
|
+
Turbo reinvents the old HTML technique of frames without any of the drawbacks that lead to developers abandoning it. With Turbo Frames, you can treat a subset of the page as its own component, where links and form submissions replace only that part. This removes an entire class of problems around partial interactivity that before would have required custom JavaScript.
|
22
|
+
|
23
|
+
It also makes it dead easy to carve a single page into smaller pieces that can all live on their own cache timeline. While the bulk of the page might easily be cached between users, a small personalized toolbar perhaps cannot. With Turbo::Frames, you can designate the toolbar as a frame, which will be lazy-loaded automatically by the publicly-cached root page. This means simpler pages, easier caching schemes with fewer dependent keys, and all without needing to write a lick of custom JavaScript.
|
24
|
+
|
25
|
+
|
26
|
+
## Turbo Streams
|
27
|
+
|
28
|
+
Partial page updates that are delivered asynchronously over a web socket connection is the hallmark of modern, reactive web applications. With Turbo Streams, you can get all of that modern goodness using the existing server-side HTML you're already rendering to deliver the first page load. With a set of simple CRUD container tags, you can send HTML fragments over the web socket (or in response to direct interactions), and see the page change in response to new data. Again, no need to construct an entirely separate API, no need to wrangle JSON, no need to reimplement the HTML construction in JavaScript. Take the HTML you're already making, wrap it in an update tag, and, voila, your page comes alive.
|
29
|
+
|
30
|
+
With this Rails integration, you can create these asynchronous updates directly in response to your model changes. Turbo uses Active Jobs to provide asynchronous partial rendering and Action Cable to deliver those updates to subscribers.
|
31
|
+
|
32
|
+
|
33
|
+
## Installation
|
34
|
+
|
35
|
+
The JavaScript for Turbo can either be run through the asset pipeline, which is included with this gem, or through the package that lives on NPM, through Webpacker. If you use the asset pipeline, installation is as follows:
|
36
|
+
|
37
|
+
1. Add the `turbo-rails` gem to your Gemfile: `gem 'turbo-rails'`
|
38
|
+
2. Run `./bin/bundle install`
|
39
|
+
3. Run `./bin/rails turbo:install`
|
40
|
+
|
41
|
+
If you use Webpacker, it's:
|
42
|
+
|
43
|
+
1. Add the `turbo-rails` gem to your Gemfile: `gem 'turbo-rails'`
|
44
|
+
2. Run `./bin/bundle install`
|
45
|
+
3. Run `./bin/yarn add @hotwired/turbo-rails`
|
46
|
+
4. Add it to your application's JavaScript pack:
|
47
|
+
|
48
|
+
```js
|
49
|
+
import { Turbo, cable } from "@hotwired/turbo-rails"
|
50
|
+
```
|
51
|
+
|
52
|
+
|
53
|
+
## Usage
|
54
|
+
|
55
|
+
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).
|
56
|
+
|
57
|
+
|
58
|
+
## Development
|
59
|
+
|
60
|
+
* To run the Rails tests: `bundle exec rake`.
|
61
|
+
* To compile the JavaScript for the asset pipeline: `yarn build`
|
62
|
+
|
63
|
+
|
64
|
+
## License
|
65
|
+
|
66
|
+
Turbo is released under the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,3161 @@
|
|
1
|
+
(function() {
|
2
|
+
if (window.Reflect === undefined || window.customElements === undefined || window.customElements.polyfillWrapFlushCallback) {
|
3
|
+
return;
|
4
|
+
}
|
5
|
+
const BuiltInHTMLElement = HTMLElement;
|
6
|
+
const wrapperForTheName = {
|
7
|
+
HTMLElement: function HTMLElement() {
|
8
|
+
return Reflect.construct(BuiltInHTMLElement, [], this.constructor);
|
9
|
+
}
|
10
|
+
};
|
11
|
+
window.HTMLElement = wrapperForTheName["HTMLElement"];
|
12
|
+
HTMLElement.prototype = BuiltInHTMLElement.prototype;
|
13
|
+
HTMLElement.prototype.constructor = HTMLElement;
|
14
|
+
Object.setPrototypeOf(HTMLElement, BuiltInHTMLElement);
|
15
|
+
})();
|
16
|
+
|
17
|
+
const submittersByForm = new WeakMap;
|
18
|
+
|
19
|
+
function findSubmitterFromClickTarget(target) {
|
20
|
+
const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
|
21
|
+
const candidate = element ? element.closest("input, button") : null;
|
22
|
+
return (candidate === null || candidate === void 0 ? void 0 : candidate.getAttribute("type")) == "submit" ? candidate : null;
|
23
|
+
}
|
24
|
+
|
25
|
+
function clickCaptured(event) {
|
26
|
+
const submitter = findSubmitterFromClickTarget(event.target);
|
27
|
+
if (submitter && submitter.form) {
|
28
|
+
submittersByForm.set(submitter.form, submitter);
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
(function() {
|
33
|
+
if ("SubmitEvent" in window) return;
|
34
|
+
addEventListener("click", clickCaptured, true);
|
35
|
+
Object.defineProperty(Event.prototype, "submitter", {
|
36
|
+
get() {
|
37
|
+
if (this.type == "submit" && this.target instanceof HTMLFormElement) {
|
38
|
+
return submittersByForm.get(this.target);
|
39
|
+
}
|
40
|
+
}
|
41
|
+
});
|
42
|
+
})();
|
43
|
+
|
44
|
+
class Location {
|
45
|
+
constructor(url) {
|
46
|
+
const linkWithAnchor = document.createElement("a");
|
47
|
+
linkWithAnchor.href = url;
|
48
|
+
this.absoluteURL = linkWithAnchor.href;
|
49
|
+
const anchorLength = linkWithAnchor.hash.length;
|
50
|
+
if (anchorLength < 2) {
|
51
|
+
this.requestURL = this.absoluteURL;
|
52
|
+
} else {
|
53
|
+
this.requestURL = this.absoluteURL.slice(0, -anchorLength);
|
54
|
+
this.anchor = linkWithAnchor.hash.slice(1);
|
55
|
+
}
|
56
|
+
}
|
57
|
+
static get currentLocation() {
|
58
|
+
return this.wrap(window.location.toString());
|
59
|
+
}
|
60
|
+
static wrap(locatable) {
|
61
|
+
if (typeof locatable == "string") {
|
62
|
+
return new this(locatable);
|
63
|
+
} else if (locatable != null) {
|
64
|
+
return locatable;
|
65
|
+
}
|
66
|
+
}
|
67
|
+
getOrigin() {
|
68
|
+
return this.absoluteURL.split("/", 3).join("/");
|
69
|
+
}
|
70
|
+
getPath() {
|
71
|
+
return (this.requestURL.match(/\/\/[^/]*(\/[^?;]*)/) || [])[1] || "/";
|
72
|
+
}
|
73
|
+
getPathComponents() {
|
74
|
+
return this.getPath().split("/").slice(1);
|
75
|
+
}
|
76
|
+
getLastPathComponent() {
|
77
|
+
return this.getPathComponents().slice(-1)[0];
|
78
|
+
}
|
79
|
+
getExtension() {
|
80
|
+
return (this.getLastPathComponent().match(/\.[^.]*$/) || [])[0] || "";
|
81
|
+
}
|
82
|
+
isHTML() {
|
83
|
+
return !!this.getExtension().match(/^(?:|\.(?:htm|html|xhtml))$/);
|
84
|
+
}
|
85
|
+
isPrefixedBy(location) {
|
86
|
+
const prefixURL = getPrefixURL(location);
|
87
|
+
return this.isEqualTo(location) || stringStartsWith(this.absoluteURL, prefixURL);
|
88
|
+
}
|
89
|
+
isEqualTo(location) {
|
90
|
+
return location && this.absoluteURL === location.absoluteURL;
|
91
|
+
}
|
92
|
+
toCacheKey() {
|
93
|
+
return this.requestURL;
|
94
|
+
}
|
95
|
+
toJSON() {
|
96
|
+
return this.absoluteURL;
|
97
|
+
}
|
98
|
+
toString() {
|
99
|
+
return this.absoluteURL;
|
100
|
+
}
|
101
|
+
valueOf() {
|
102
|
+
return this.absoluteURL;
|
103
|
+
}
|
104
|
+
}
|
105
|
+
|
106
|
+
function getPrefixURL(location) {
|
107
|
+
return addTrailingSlash(location.getOrigin() + location.getPath());
|
108
|
+
}
|
109
|
+
|
110
|
+
function addTrailingSlash(url) {
|
111
|
+
return stringEndsWith(url, "/") ? url : url + "/";
|
112
|
+
}
|
113
|
+
|
114
|
+
function stringStartsWith(string, prefix) {
|
115
|
+
return string.slice(0, prefix.length) === prefix;
|
116
|
+
}
|
117
|
+
|
118
|
+
function stringEndsWith(string, suffix) {
|
119
|
+
return string.slice(-suffix.length) === suffix;
|
120
|
+
}
|
121
|
+
|
122
|
+
class FetchResponse {
|
123
|
+
constructor(response) {
|
124
|
+
this.response = response;
|
125
|
+
}
|
126
|
+
get succeeded() {
|
127
|
+
return this.response.ok;
|
128
|
+
}
|
129
|
+
get failed() {
|
130
|
+
return !this.succeeded;
|
131
|
+
}
|
132
|
+
get redirected() {
|
133
|
+
return this.response.redirected;
|
134
|
+
}
|
135
|
+
get location() {
|
136
|
+
return Location.wrap(this.response.url);
|
137
|
+
}
|
138
|
+
get isHTML() {
|
139
|
+
return this.contentType && this.contentType.match(/^text\/html|^application\/xhtml\+xml/);
|
140
|
+
}
|
141
|
+
get statusCode() {
|
142
|
+
return this.response.status;
|
143
|
+
}
|
144
|
+
get contentType() {
|
145
|
+
return this.header("Content-Type");
|
146
|
+
}
|
147
|
+
get responseText() {
|
148
|
+
return this.response.text();
|
149
|
+
}
|
150
|
+
get responseHTML() {
|
151
|
+
if (this.isHTML) {
|
152
|
+
return this.response.text();
|
153
|
+
} else {
|
154
|
+
return Promise.resolve(undefined);
|
155
|
+
}
|
156
|
+
}
|
157
|
+
header(name) {
|
158
|
+
return this.response.headers.get(name);
|
159
|
+
}
|
160
|
+
}
|
161
|
+
|
162
|
+
function dispatch(eventName, {target: target, cancelable: cancelable, detail: detail} = {}) {
|
163
|
+
const event = new CustomEvent(eventName, {
|
164
|
+
cancelable: cancelable,
|
165
|
+
bubbles: true,
|
166
|
+
detail: detail
|
167
|
+
});
|
168
|
+
void (target || document.documentElement).dispatchEvent(event);
|
169
|
+
return event;
|
170
|
+
}
|
171
|
+
|
172
|
+
function nextAnimationFrame() {
|
173
|
+
return new Promise((resolve => requestAnimationFrame((() => resolve()))));
|
174
|
+
}
|
175
|
+
|
176
|
+
function nextMicrotask() {
|
177
|
+
return Promise.resolve();
|
178
|
+
}
|
179
|
+
|
180
|
+
function unindent(strings, ...values) {
|
181
|
+
const lines = interpolate(strings, values).replace(/^\n/, "").split("\n");
|
182
|
+
const match = lines[0].match(/^\s+/);
|
183
|
+
const indent = match ? match[0].length : 0;
|
184
|
+
return lines.map((line => line.slice(indent))).join("\n");
|
185
|
+
}
|
186
|
+
|
187
|
+
function interpolate(strings, values) {
|
188
|
+
return strings.reduce(((result, string, i) => {
|
189
|
+
const value = values[i] == undefined ? "" : values[i];
|
190
|
+
return result + string + value;
|
191
|
+
}), "");
|
192
|
+
}
|
193
|
+
|
194
|
+
function uuid() {
|
195
|
+
return Array.apply(null, {
|
196
|
+
length: 36
|
197
|
+
}).map(((_, i) => {
|
198
|
+
if (i == 8 || i == 13 || i == 18 || i == 23) {
|
199
|
+
return "-";
|
200
|
+
} else if (i == 14) {
|
201
|
+
return "4";
|
202
|
+
} else if (i == 19) {
|
203
|
+
return (Math.floor(Math.random() * 4) + 8).toString(16);
|
204
|
+
} else {
|
205
|
+
return Math.floor(Math.random() * 15).toString(16);
|
206
|
+
}
|
207
|
+
})).join("");
|
208
|
+
}
|
209
|
+
|
210
|
+
var FetchMethod;
|
211
|
+
|
212
|
+
(function(FetchMethod) {
|
213
|
+
FetchMethod[FetchMethod["get"] = 0] = "get";
|
214
|
+
FetchMethod[FetchMethod["post"] = 1] = "post";
|
215
|
+
FetchMethod[FetchMethod["put"] = 2] = "put";
|
216
|
+
FetchMethod[FetchMethod["patch"] = 3] = "patch";
|
217
|
+
FetchMethod[FetchMethod["delete"] = 4] = "delete";
|
218
|
+
})(FetchMethod || (FetchMethod = {}));
|
219
|
+
|
220
|
+
function fetchMethodFromString(method) {
|
221
|
+
switch (method.toLowerCase()) {
|
222
|
+
case "get":
|
223
|
+
return FetchMethod.get;
|
224
|
+
|
225
|
+
case "post":
|
226
|
+
return FetchMethod.post;
|
227
|
+
|
228
|
+
case "put":
|
229
|
+
return FetchMethod.put;
|
230
|
+
|
231
|
+
case "patch":
|
232
|
+
return FetchMethod.patch;
|
233
|
+
|
234
|
+
case "delete":
|
235
|
+
return FetchMethod.delete;
|
236
|
+
}
|
237
|
+
}
|
238
|
+
|
239
|
+
class FetchRequest {
|
240
|
+
constructor(delegate, method, location, body) {
|
241
|
+
this.abortController = new AbortController;
|
242
|
+
this.delegate = delegate;
|
243
|
+
this.method = method;
|
244
|
+
this.location = location;
|
245
|
+
this.body = body;
|
246
|
+
}
|
247
|
+
get url() {
|
248
|
+
const url = this.location.absoluteURL;
|
249
|
+
const query = this.params.toString();
|
250
|
+
if (this.isIdempotent && query.length) {
|
251
|
+
return [ url, query ].join(url.includes("?") ? "&" : "?");
|
252
|
+
} else {
|
253
|
+
return url;
|
254
|
+
}
|
255
|
+
}
|
256
|
+
get params() {
|
257
|
+
return this.entries.reduce(((params, [name, value]) => {
|
258
|
+
params.append(name, value.toString());
|
259
|
+
return params;
|
260
|
+
}), new URLSearchParams);
|
261
|
+
}
|
262
|
+
get entries() {
|
263
|
+
return this.body ? Array.from(this.body.entries()) : [];
|
264
|
+
}
|
265
|
+
cancel() {
|
266
|
+
this.abortController.abort();
|
267
|
+
}
|
268
|
+
async perform() {
|
269
|
+
const {fetchOptions: fetchOptions} = this;
|
270
|
+
dispatch("turbo:before-fetch-request", {
|
271
|
+
detail: {
|
272
|
+
fetchOptions: fetchOptions
|
273
|
+
}
|
274
|
+
});
|
275
|
+
try {
|
276
|
+
this.delegate.requestStarted(this);
|
277
|
+
const response = await fetch(this.url, fetchOptions);
|
278
|
+
return await this.receive(response);
|
279
|
+
} catch (error) {
|
280
|
+
this.delegate.requestErrored(this, error);
|
281
|
+
throw error;
|
282
|
+
} finally {
|
283
|
+
this.delegate.requestFinished(this);
|
284
|
+
}
|
285
|
+
}
|
286
|
+
async receive(response) {
|
287
|
+
const fetchResponse = new FetchResponse(response);
|
288
|
+
const event = dispatch("turbo:before-fetch-response", {
|
289
|
+
cancelable: true,
|
290
|
+
detail: {
|
291
|
+
fetchResponse: fetchResponse
|
292
|
+
}
|
293
|
+
});
|
294
|
+
if (event.defaultPrevented) {
|
295
|
+
this.delegate.requestPreventedHandlingResponse(this, fetchResponse);
|
296
|
+
} else if (fetchResponse.succeeded) {
|
297
|
+
this.delegate.requestSucceededWithResponse(this, fetchResponse);
|
298
|
+
} else {
|
299
|
+
this.delegate.requestFailedWithResponse(this, fetchResponse);
|
300
|
+
}
|
301
|
+
return fetchResponse;
|
302
|
+
}
|
303
|
+
get fetchOptions() {
|
304
|
+
return {
|
305
|
+
method: FetchMethod[this.method].toUpperCase(),
|
306
|
+
credentials: "same-origin",
|
307
|
+
headers: this.headers,
|
308
|
+
redirect: "follow",
|
309
|
+
body: this.isIdempotent ? undefined : this.body,
|
310
|
+
signal: this.abortSignal
|
311
|
+
};
|
312
|
+
}
|
313
|
+
get isIdempotent() {
|
314
|
+
return this.method == FetchMethod.get;
|
315
|
+
}
|
316
|
+
get headers() {
|
317
|
+
return Object.assign({
|
318
|
+
Accept: "text/html, application/xhtml+xml"
|
319
|
+
}, this.additionalHeaders);
|
320
|
+
}
|
321
|
+
get additionalHeaders() {
|
322
|
+
if (typeof this.delegate.additionalHeadersForRequest == "function") {
|
323
|
+
return this.delegate.additionalHeadersForRequest(this);
|
324
|
+
} else {
|
325
|
+
return {};
|
326
|
+
}
|
327
|
+
}
|
328
|
+
get abortSignal() {
|
329
|
+
return this.abortController.signal;
|
330
|
+
}
|
331
|
+
}
|
332
|
+
|
333
|
+
class FormInterceptor {
|
334
|
+
constructor(delegate, element) {
|
335
|
+
this.submitBubbled = event => {
|
336
|
+
if (event.target instanceof HTMLFormElement) {
|
337
|
+
const form = event.target;
|
338
|
+
const submitter = event.submitter || undefined;
|
339
|
+
if (this.delegate.shouldInterceptFormSubmission(form, submitter)) {
|
340
|
+
event.preventDefault();
|
341
|
+
event.stopImmediatePropagation();
|
342
|
+
this.delegate.formSubmissionIntercepted(form, submitter);
|
343
|
+
}
|
344
|
+
}
|
345
|
+
};
|
346
|
+
this.delegate = delegate;
|
347
|
+
this.element = element;
|
348
|
+
}
|
349
|
+
start() {
|
350
|
+
this.element.addEventListener("submit", this.submitBubbled);
|
351
|
+
}
|
352
|
+
stop() {
|
353
|
+
this.element.removeEventListener("submit", this.submitBubbled);
|
354
|
+
}
|
355
|
+
}
|
356
|
+
|
357
|
+
var FormSubmissionState;
|
358
|
+
|
359
|
+
(function(FormSubmissionState) {
|
360
|
+
FormSubmissionState[FormSubmissionState["initialized"] = 0] = "initialized";
|
361
|
+
FormSubmissionState[FormSubmissionState["requesting"] = 1] = "requesting";
|
362
|
+
FormSubmissionState[FormSubmissionState["waiting"] = 2] = "waiting";
|
363
|
+
FormSubmissionState[FormSubmissionState["receiving"] = 3] = "receiving";
|
364
|
+
FormSubmissionState[FormSubmissionState["stopping"] = 4] = "stopping";
|
365
|
+
FormSubmissionState[FormSubmissionState["stopped"] = 5] = "stopped";
|
366
|
+
})(FormSubmissionState || (FormSubmissionState = {}));
|
367
|
+
|
368
|
+
class FormSubmission {
|
369
|
+
constructor(delegate, formElement, submitter, mustRedirect = false) {
|
370
|
+
this.state = FormSubmissionState.initialized;
|
371
|
+
this.delegate = delegate;
|
372
|
+
this.formElement = formElement;
|
373
|
+
this.formData = buildFormData(formElement, submitter);
|
374
|
+
this.submitter = submitter;
|
375
|
+
this.fetchRequest = new FetchRequest(this, this.method, this.location, this.formData);
|
376
|
+
this.mustRedirect = mustRedirect;
|
377
|
+
}
|
378
|
+
get method() {
|
379
|
+
var _a;
|
380
|
+
const method = ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formmethod")) || this.formElement.method;
|
381
|
+
return fetchMethodFromString(method.toLowerCase()) || FetchMethod.get;
|
382
|
+
}
|
383
|
+
get action() {
|
384
|
+
var _a;
|
385
|
+
return ((_a = this.submitter) === null || _a === void 0 ? void 0 : _a.getAttribute("formaction")) || this.formElement.action;
|
386
|
+
}
|
387
|
+
get location() {
|
388
|
+
return Location.wrap(this.action);
|
389
|
+
}
|
390
|
+
async start() {
|
391
|
+
const {initialized: initialized, requesting: requesting} = FormSubmissionState;
|
392
|
+
if (this.state == initialized) {
|
393
|
+
this.state = requesting;
|
394
|
+
return this.fetchRequest.perform();
|
395
|
+
}
|
396
|
+
}
|
397
|
+
stop() {
|
398
|
+
const {stopping: stopping, stopped: stopped} = FormSubmissionState;
|
399
|
+
if (this.state != stopping && this.state != stopped) {
|
400
|
+
this.state = stopping;
|
401
|
+
this.fetchRequest.cancel();
|
402
|
+
return true;
|
403
|
+
}
|
404
|
+
}
|
405
|
+
additionalHeadersForRequest(request) {
|
406
|
+
const headers = {};
|
407
|
+
if (this.method != FetchMethod.get) {
|
408
|
+
const token = getCookieValue(getMetaContent("csrf-param")) || getMetaContent("csrf-token");
|
409
|
+
if (token) {
|
410
|
+
headers["X-CSRF-Token"] = token;
|
411
|
+
}
|
412
|
+
}
|
413
|
+
return headers;
|
414
|
+
}
|
415
|
+
requestStarted(request) {
|
416
|
+
this.state = FormSubmissionState.waiting;
|
417
|
+
dispatch("turbo:submit-start", {
|
418
|
+
target: this.formElement,
|
419
|
+
detail: {
|
420
|
+
formSubmission: this
|
421
|
+
}
|
422
|
+
});
|
423
|
+
this.delegate.formSubmissionStarted(this);
|
424
|
+
}
|
425
|
+
requestPreventedHandlingResponse(request, response) {
|
426
|
+
this.result = {
|
427
|
+
success: response.succeeded,
|
428
|
+
fetchResponse: response
|
429
|
+
};
|
430
|
+
}
|
431
|
+
requestSucceededWithResponse(request, response) {
|
432
|
+
if (this.requestMustRedirect(request) && !response.redirected) {
|
433
|
+
const error = new Error("Form responses must redirect to another location");
|
434
|
+
this.delegate.formSubmissionErrored(this, error);
|
435
|
+
} else {
|
436
|
+
this.state = FormSubmissionState.receiving;
|
437
|
+
this.result = {
|
438
|
+
success: true,
|
439
|
+
fetchResponse: response
|
440
|
+
};
|
441
|
+
this.delegate.formSubmissionSucceededWithResponse(this, response);
|
442
|
+
}
|
443
|
+
}
|
444
|
+
requestFailedWithResponse(request, response) {
|
445
|
+
this.result = {
|
446
|
+
success: false,
|
447
|
+
fetchResponse: response
|
448
|
+
};
|
449
|
+
this.delegate.formSubmissionFailedWithResponse(this, response);
|
450
|
+
}
|
451
|
+
requestErrored(request, error) {
|
452
|
+
this.result = {
|
453
|
+
success: false,
|
454
|
+
error: error
|
455
|
+
};
|
456
|
+
this.delegate.formSubmissionErrored(this, error);
|
457
|
+
}
|
458
|
+
requestFinished(request) {
|
459
|
+
this.state = FormSubmissionState.stopped;
|
460
|
+
dispatch("turbo:submit-end", {
|
461
|
+
target: this.formElement,
|
462
|
+
detail: Object.assign({
|
463
|
+
formSubmission: this
|
464
|
+
}, this.result)
|
465
|
+
});
|
466
|
+
this.delegate.formSubmissionFinished(this);
|
467
|
+
}
|
468
|
+
requestMustRedirect(request) {
|
469
|
+
return !request.isIdempotent && this.mustRedirect;
|
470
|
+
}
|
471
|
+
}
|
472
|
+
|
473
|
+
function buildFormData(formElement, submitter) {
|
474
|
+
const formData = new FormData(formElement);
|
475
|
+
const name = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("name");
|
476
|
+
const value = submitter === null || submitter === void 0 ? void 0 : submitter.getAttribute("value");
|
477
|
+
if (name && formData.get(name) != value) {
|
478
|
+
formData.append(name, value || "");
|
479
|
+
}
|
480
|
+
return formData;
|
481
|
+
}
|
482
|
+
|
483
|
+
function getCookieValue(cookieName) {
|
484
|
+
if (cookieName != null) {
|
485
|
+
const cookies = document.cookie ? document.cookie.split("; ") : [];
|
486
|
+
const cookie = cookies.find((cookie => cookie.startsWith(cookieName)));
|
487
|
+
if (cookie) {
|
488
|
+
const value = cookie.split("=").slice(1).join("=");
|
489
|
+
return value ? decodeURIComponent(value) : undefined;
|
490
|
+
}
|
491
|
+
}
|
492
|
+
}
|
493
|
+
|
494
|
+
function getMetaContent(name) {
|
495
|
+
const element = document.querySelector(`meta[name="${name}"]`);
|
496
|
+
return element && element.content;
|
497
|
+
}
|
498
|
+
|
499
|
+
class LinkInterceptor {
|
500
|
+
constructor(delegate, element) {
|
501
|
+
this.clickBubbled = event => {
|
502
|
+
if (this.respondsToEventTarget(event.target)) {
|
503
|
+
this.clickEvent = event;
|
504
|
+
} else {
|
505
|
+
delete this.clickEvent;
|
506
|
+
}
|
507
|
+
};
|
508
|
+
this.linkClicked = event => {
|
509
|
+
if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) {
|
510
|
+
if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url)) {
|
511
|
+
this.clickEvent.preventDefault();
|
512
|
+
event.preventDefault();
|
513
|
+
this.delegate.linkClickIntercepted(event.target, event.detail.url);
|
514
|
+
}
|
515
|
+
}
|
516
|
+
delete this.clickEvent;
|
517
|
+
};
|
518
|
+
this.willVisit = () => {
|
519
|
+
delete this.clickEvent;
|
520
|
+
};
|
521
|
+
this.delegate = delegate;
|
522
|
+
this.element = element;
|
523
|
+
}
|
524
|
+
start() {
|
525
|
+
this.element.addEventListener("click", this.clickBubbled);
|
526
|
+
document.addEventListener("turbo:click", this.linkClicked);
|
527
|
+
document.addEventListener("turbo:before-visit", this.willVisit);
|
528
|
+
}
|
529
|
+
stop() {
|
530
|
+
this.element.removeEventListener("click", this.clickBubbled);
|
531
|
+
document.removeEventListener("turbo:click", this.linkClicked);
|
532
|
+
document.removeEventListener("turbo:before-visit", this.willVisit);
|
533
|
+
}
|
534
|
+
respondsToEventTarget(target) {
|
535
|
+
const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
|
536
|
+
return element && element.closest("turbo-frame, html") == this.element;
|
537
|
+
}
|
538
|
+
}
|
539
|
+
|
540
|
+
class FrameController {
|
541
|
+
constructor(element) {
|
542
|
+
this.resolveVisitPromise = () => {};
|
543
|
+
this.element = element;
|
544
|
+
this.linkInterceptor = new LinkInterceptor(this, this.element);
|
545
|
+
this.formInterceptor = new FormInterceptor(this, this.element);
|
546
|
+
}
|
547
|
+
connect() {
|
548
|
+
this.linkInterceptor.start();
|
549
|
+
this.formInterceptor.start();
|
550
|
+
}
|
551
|
+
disconnect() {
|
552
|
+
this.linkInterceptor.stop();
|
553
|
+
this.formInterceptor.stop();
|
554
|
+
}
|
555
|
+
shouldInterceptLinkClick(element, url) {
|
556
|
+
return this.shouldInterceptNavigation(element);
|
557
|
+
}
|
558
|
+
linkClickIntercepted(element, url) {
|
559
|
+
this.navigateFrame(element, url);
|
560
|
+
}
|
561
|
+
shouldInterceptFormSubmission(element) {
|
562
|
+
return this.shouldInterceptNavigation(element);
|
563
|
+
}
|
564
|
+
formSubmissionIntercepted(element, submitter) {
|
565
|
+
if (this.formSubmission) {
|
566
|
+
this.formSubmission.stop();
|
567
|
+
}
|
568
|
+
this.formSubmission = new FormSubmission(this, element, submitter);
|
569
|
+
if (this.formSubmission.fetchRequest.isIdempotent) {
|
570
|
+
this.navigateFrame(element, this.formSubmission.fetchRequest.url);
|
571
|
+
} else {
|
572
|
+
this.formSubmission.start();
|
573
|
+
}
|
574
|
+
}
|
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
|
+
additionalHeadersForRequest(request) {
|
587
|
+
return {
|
588
|
+
"Turbo-Frame": this.id
|
589
|
+
};
|
590
|
+
}
|
591
|
+
requestStarted(request) {
|
592
|
+
this.element.setAttribute("busy", "");
|
593
|
+
}
|
594
|
+
requestPreventedHandlingResponse(request, response) {
|
595
|
+
this.resolveVisitPromise();
|
596
|
+
}
|
597
|
+
async requestSucceededWithResponse(request, response) {
|
598
|
+
await this.loadResponse(response);
|
599
|
+
this.resolveVisitPromise();
|
600
|
+
}
|
601
|
+
requestFailedWithResponse(request, response) {
|
602
|
+
console.error(response);
|
603
|
+
this.resolveVisitPromise();
|
604
|
+
}
|
605
|
+
requestErrored(request, error) {
|
606
|
+
console.error(error);
|
607
|
+
this.resolveVisitPromise();
|
608
|
+
}
|
609
|
+
requestFinished(request) {
|
610
|
+
this.element.removeAttribute("busy");
|
611
|
+
}
|
612
|
+
formSubmissionStarted(formSubmission) {}
|
613
|
+
formSubmissionSucceededWithResponse(formSubmission, response) {
|
614
|
+
const frame = this.findFrameElement(formSubmission.formElement);
|
615
|
+
frame.controller.loadResponse(response);
|
616
|
+
}
|
617
|
+
formSubmissionFailedWithResponse(formSubmission, fetchResponse) {}
|
618
|
+
formSubmissionErrored(formSubmission, error) {}
|
619
|
+
formSubmissionFinished(formSubmission) {}
|
620
|
+
navigateFrame(element, url) {
|
621
|
+
const frame = this.findFrameElement(element);
|
622
|
+
frame.src = url;
|
623
|
+
}
|
624
|
+
findFrameElement(element) {
|
625
|
+
var _a;
|
626
|
+
const id = element.getAttribute("data-turbo-frame");
|
627
|
+
return (_a = getFrameElementById(id)) !== null && _a !== void 0 ? _a : this.element;
|
628
|
+
}
|
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
|
+
async extractForeignFrameElement(container) {
|
641
|
+
let element;
|
642
|
+
const id = CSS.escape(this.id);
|
643
|
+
if (element = activateElement(container.querySelector(`turbo-frame#${id}`))) {
|
644
|
+
return element;
|
645
|
+
}
|
646
|
+
if (element = activateElement(container.querySelector(`turbo-frame[src][recurse~=${id}]`))) {
|
647
|
+
await element.loaded;
|
648
|
+
return await this.extractForeignFrameElement(element);
|
649
|
+
}
|
650
|
+
}
|
651
|
+
loadFrameElement(frameElement) {
|
652
|
+
var _a;
|
653
|
+
const destinationRange = document.createRange();
|
654
|
+
destinationRange.selectNodeContents(this.element);
|
655
|
+
destinationRange.deleteContents();
|
656
|
+
const sourceRange = (_a = frameElement.ownerDocument) === null || _a === void 0 ? void 0 : _a.createRange();
|
657
|
+
if (sourceRange) {
|
658
|
+
sourceRange.selectNodeContents(frameElement);
|
659
|
+
this.element.appendChild(sourceRange.extractContents());
|
660
|
+
}
|
661
|
+
}
|
662
|
+
focusFirstAutofocusableElement() {
|
663
|
+
const element = this.firstAutofocusableElement;
|
664
|
+
if (element) {
|
665
|
+
element.focus();
|
666
|
+
return true;
|
667
|
+
}
|
668
|
+
return false;
|
669
|
+
}
|
670
|
+
scrollFrameIntoView(frame) {
|
671
|
+
if (this.element.autoscroll || frame.autoscroll) {
|
672
|
+
const element = this.element.firstElementChild;
|
673
|
+
const block = readScrollLogicalPosition(this.element.getAttribute("data-autoscroll-block"), "end");
|
674
|
+
if (element) {
|
675
|
+
element.scrollIntoView({
|
676
|
+
block: block
|
677
|
+
});
|
678
|
+
return true;
|
679
|
+
}
|
680
|
+
}
|
681
|
+
return false;
|
682
|
+
}
|
683
|
+
shouldInterceptNavigation(element) {
|
684
|
+
const id = element.getAttribute("data-turbo-frame") || this.element.getAttribute("target");
|
685
|
+
if (!this.enabled || id == "_top") {
|
686
|
+
return false;
|
687
|
+
}
|
688
|
+
if (id) {
|
689
|
+
const frameElement = getFrameElementById(id);
|
690
|
+
if (frameElement) {
|
691
|
+
return !frameElement.disabled;
|
692
|
+
}
|
693
|
+
}
|
694
|
+
return true;
|
695
|
+
}
|
696
|
+
get firstAutofocusableElement() {
|
697
|
+
const element = this.element.querySelector("[autofocus]");
|
698
|
+
return element instanceof HTMLElement ? element : null;
|
699
|
+
}
|
700
|
+
get id() {
|
701
|
+
return this.element.id;
|
702
|
+
}
|
703
|
+
get enabled() {
|
704
|
+
return !this.element.disabled;
|
705
|
+
}
|
706
|
+
}
|
707
|
+
|
708
|
+
function getFrameElementById(id) {
|
709
|
+
if (id != null) {
|
710
|
+
const element = document.getElementById(id);
|
711
|
+
if (element instanceof FrameElement) {
|
712
|
+
return element;
|
713
|
+
}
|
714
|
+
}
|
715
|
+
}
|
716
|
+
|
717
|
+
function readScrollLogicalPosition(value, defaultValue) {
|
718
|
+
if (value == "end" || value == "start" || value == "center" || value == "nearest") {
|
719
|
+
return value;
|
720
|
+
} else {
|
721
|
+
return defaultValue;
|
722
|
+
}
|
723
|
+
}
|
724
|
+
|
725
|
+
function fragmentFromHTML(html = "") {
|
726
|
+
const foreignDocument = document.implementation.createHTMLDocument();
|
727
|
+
return foreignDocument.createRange().createContextualFragment(html);
|
728
|
+
}
|
729
|
+
|
730
|
+
function activateElement(element) {
|
731
|
+
if (element && element.ownerDocument !== document) {
|
732
|
+
element = document.importNode(element, true);
|
733
|
+
}
|
734
|
+
if (element instanceof FrameElement) {
|
735
|
+
return element;
|
736
|
+
}
|
737
|
+
}
|
738
|
+
|
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
|
+
const StreamActions = {
|
810
|
+
append() {
|
811
|
+
var _a;
|
812
|
+
(_a = this.targetElement) === null || _a === void 0 ? void 0 : _a.append(this.templateContent);
|
813
|
+
},
|
814
|
+
prepend() {
|
815
|
+
var _a;
|
816
|
+
(_a = this.targetElement) === null || _a === void 0 ? void 0 : _a.prepend(this.templateContent);
|
817
|
+
},
|
818
|
+
remove() {
|
819
|
+
var _a;
|
820
|
+
(_a = this.targetElement) === null || _a === void 0 ? void 0 : _a.remove();
|
821
|
+
},
|
822
|
+
replace() {
|
823
|
+
var _a;
|
824
|
+
(_a = this.targetElement) === null || _a === void 0 ? void 0 : _a.replaceWith(this.templateContent);
|
825
|
+
},
|
826
|
+
update() {
|
827
|
+
if (this.targetElement) {
|
828
|
+
this.targetElement.innerHTML = "";
|
829
|
+
this.targetElement.append(this.templateContent);
|
830
|
+
}
|
831
|
+
}
|
832
|
+
};
|
833
|
+
|
834
|
+
class StreamElement extends HTMLElement {
|
835
|
+
async connectedCallback() {
|
836
|
+
try {
|
837
|
+
await this.render();
|
838
|
+
} catch (error) {
|
839
|
+
console.error(error);
|
840
|
+
} finally {
|
841
|
+
this.disconnect();
|
842
|
+
}
|
843
|
+
}
|
844
|
+
async render() {
|
845
|
+
var _a;
|
846
|
+
return (_a = this.renderPromise) !== null && _a !== void 0 ? _a : this.renderPromise = (async () => {
|
847
|
+
if (this.dispatchEvent(this.beforeRenderEvent)) {
|
848
|
+
await nextAnimationFrame();
|
849
|
+
this.performAction();
|
850
|
+
}
|
851
|
+
})();
|
852
|
+
}
|
853
|
+
disconnect() {
|
854
|
+
try {
|
855
|
+
this.remove();
|
856
|
+
} catch (_a) {}
|
857
|
+
}
|
858
|
+
get performAction() {
|
859
|
+
if (this.action) {
|
860
|
+
const actionFunction = StreamActions[this.action];
|
861
|
+
if (actionFunction) {
|
862
|
+
return actionFunction;
|
863
|
+
}
|
864
|
+
this.raise("unknown action");
|
865
|
+
}
|
866
|
+
this.raise("action attribute is missing");
|
867
|
+
}
|
868
|
+
get targetElement() {
|
869
|
+
var _a;
|
870
|
+
if (this.target) {
|
871
|
+
return (_a = this.ownerDocument) === null || _a === void 0 ? void 0 : _a.getElementById(this.target);
|
872
|
+
}
|
873
|
+
this.raise("target attribute is missing");
|
874
|
+
}
|
875
|
+
get templateContent() {
|
876
|
+
return this.templateElement.content;
|
877
|
+
}
|
878
|
+
get templateElement() {
|
879
|
+
if (this.firstElementChild instanceof HTMLTemplateElement) {
|
880
|
+
return this.firstElementChild;
|
881
|
+
}
|
882
|
+
this.raise("first child element must be a <template> element");
|
883
|
+
}
|
884
|
+
get action() {
|
885
|
+
return this.getAttribute("action");
|
886
|
+
}
|
887
|
+
get target() {
|
888
|
+
return this.getAttribute("target");
|
889
|
+
}
|
890
|
+
raise(message) {
|
891
|
+
throw new Error(`${this.description}: ${message}`);
|
892
|
+
}
|
893
|
+
get description() {
|
894
|
+
var _a, _b;
|
895
|
+
return (_b = ((_a = this.outerHTML.match(/<[^>]+>/)) !== null && _a !== void 0 ? _a : [])[0]) !== null && _b !== void 0 ? _b : "<turbo-stream>";
|
896
|
+
}
|
897
|
+
get beforeRenderEvent() {
|
898
|
+
return new CustomEvent("turbo:before-stream-render", {
|
899
|
+
bubbles: true,
|
900
|
+
cancelable: true
|
901
|
+
});
|
902
|
+
}
|
903
|
+
}
|
904
|
+
|
905
|
+
customElements.define("turbo-stream", StreamElement);
|
906
|
+
|
907
|
+
(() => {
|
908
|
+
let element = document.currentScript;
|
909
|
+
if (!element) return;
|
910
|
+
if (element.hasAttribute("data-turbo-suppress-warning")) return;
|
911
|
+
while (element = element.parentElement) {
|
912
|
+
if (element == document.body) {
|
913
|
+
return console.warn(unindent`
|
914
|
+
You are loading Turbo from a <script> element inside the <body> element. This is probably not what you meant to do!
|
915
|
+
|
916
|
+
Load your application’s JavaScript bundle inside the <head> element instead. <script> elements in <body> are evaluated with each page change.
|
917
|
+
|
918
|
+
For more information, see: https://turbo.hotwire.dev/handbook/building#working-with-script-elements
|
919
|
+
|
920
|
+
——
|
921
|
+
Suppress this warning by adding a "data-turbo-suppress-warning" attribute to: %s
|
922
|
+
`, element.outerHTML);
|
923
|
+
}
|
924
|
+
}
|
925
|
+
})();
|
926
|
+
|
927
|
+
class ProgressBar {
|
928
|
+
constructor() {
|
929
|
+
this.hiding = false;
|
930
|
+
this.value = 0;
|
931
|
+
this.visible = false;
|
932
|
+
this.trickle = () => {
|
933
|
+
this.setValue(this.value + Math.random() / 100);
|
934
|
+
};
|
935
|
+
this.stylesheetElement = this.createStylesheetElement();
|
936
|
+
this.progressElement = this.createProgressElement();
|
937
|
+
this.installStylesheetElement();
|
938
|
+
this.setValue(0);
|
939
|
+
}
|
940
|
+
static get defaultCSS() {
|
941
|
+
return unindent`
|
942
|
+
.turbo-progress-bar {
|
943
|
+
position: fixed;
|
944
|
+
display: block;
|
945
|
+
top: 0;
|
946
|
+
left: 0;
|
947
|
+
height: 3px;
|
948
|
+
background: #0076ff;
|
949
|
+
z-index: 9999;
|
950
|
+
transition:
|
951
|
+
width ${ProgressBar.animationDuration}ms ease-out,
|
952
|
+
opacity ${ProgressBar.animationDuration / 2}ms ${ProgressBar.animationDuration / 2}ms ease-in;
|
953
|
+
transform: translate3d(0, 0, 0);
|
954
|
+
}
|
955
|
+
`;
|
956
|
+
}
|
957
|
+
show() {
|
958
|
+
if (!this.visible) {
|
959
|
+
this.visible = true;
|
960
|
+
this.installProgressElement();
|
961
|
+
this.startTrickling();
|
962
|
+
}
|
963
|
+
}
|
964
|
+
hide() {
|
965
|
+
if (this.visible && !this.hiding) {
|
966
|
+
this.hiding = true;
|
967
|
+
this.fadeProgressElement((() => {
|
968
|
+
this.uninstallProgressElement();
|
969
|
+
this.stopTrickling();
|
970
|
+
this.visible = false;
|
971
|
+
this.hiding = false;
|
972
|
+
}));
|
973
|
+
}
|
974
|
+
}
|
975
|
+
setValue(value) {
|
976
|
+
this.value = value;
|
977
|
+
this.refresh();
|
978
|
+
}
|
979
|
+
installStylesheetElement() {
|
980
|
+
document.head.insertBefore(this.stylesheetElement, document.head.firstChild);
|
981
|
+
}
|
982
|
+
installProgressElement() {
|
983
|
+
this.progressElement.style.width = "0";
|
984
|
+
this.progressElement.style.opacity = "1";
|
985
|
+
document.documentElement.insertBefore(this.progressElement, document.body);
|
986
|
+
this.refresh();
|
987
|
+
}
|
988
|
+
fadeProgressElement(callback) {
|
989
|
+
this.progressElement.style.opacity = "0";
|
990
|
+
setTimeout(callback, ProgressBar.animationDuration * 1.5);
|
991
|
+
}
|
992
|
+
uninstallProgressElement() {
|
993
|
+
if (this.progressElement.parentNode) {
|
994
|
+
document.documentElement.removeChild(this.progressElement);
|
995
|
+
}
|
996
|
+
}
|
997
|
+
startTrickling() {
|
998
|
+
if (!this.trickleInterval) {
|
999
|
+
this.trickleInterval = window.setInterval(this.trickle, ProgressBar.animationDuration);
|
1000
|
+
}
|
1001
|
+
}
|
1002
|
+
stopTrickling() {
|
1003
|
+
window.clearInterval(this.trickleInterval);
|
1004
|
+
delete this.trickleInterval;
|
1005
|
+
}
|
1006
|
+
refresh() {
|
1007
|
+
requestAnimationFrame((() => {
|
1008
|
+
this.progressElement.style.width = `${10 + this.value * 90}%`;
|
1009
|
+
}));
|
1010
|
+
}
|
1011
|
+
createStylesheetElement() {
|
1012
|
+
const element = document.createElement("style");
|
1013
|
+
element.type = "text/css";
|
1014
|
+
element.textContent = ProgressBar.defaultCSS;
|
1015
|
+
return element;
|
1016
|
+
}
|
1017
|
+
createProgressElement() {
|
1018
|
+
const element = document.createElement("div");
|
1019
|
+
element.className = "turbo-progress-bar";
|
1020
|
+
return element;
|
1021
|
+
}
|
1022
|
+
}
|
1023
|
+
|
1024
|
+
ProgressBar.animationDuration = 300;
|
1025
|
+
|
1026
|
+
class HeadDetails {
|
1027
|
+
constructor(children) {
|
1028
|
+
this.detailsByOuterHTML = children.reduce(((result, element) => {
|
1029
|
+
const {outerHTML: outerHTML} = element;
|
1030
|
+
const details = outerHTML in result ? result[outerHTML] : {
|
1031
|
+
type: elementType(element),
|
1032
|
+
tracked: elementIsTracked(element),
|
1033
|
+
elements: []
|
1034
|
+
};
|
1035
|
+
return Object.assign(Object.assign({}, result), {
|
1036
|
+
[outerHTML]: Object.assign(Object.assign({}, details), {
|
1037
|
+
elements: [ ...details.elements, element ]
|
1038
|
+
})
|
1039
|
+
});
|
1040
|
+
}), {});
|
1041
|
+
}
|
1042
|
+
static fromHeadElement(headElement) {
|
1043
|
+
const children = headElement ? [ ...headElement.children ] : [];
|
1044
|
+
return new this(children);
|
1045
|
+
}
|
1046
|
+
getTrackedElementSignature() {
|
1047
|
+
return Object.keys(this.detailsByOuterHTML).filter((outerHTML => this.detailsByOuterHTML[outerHTML].tracked)).join("");
|
1048
|
+
}
|
1049
|
+
getScriptElementsNotInDetails(headDetails) {
|
1050
|
+
return this.getElementsMatchingTypeNotInDetails("script", headDetails);
|
1051
|
+
}
|
1052
|
+
getStylesheetElementsNotInDetails(headDetails) {
|
1053
|
+
return this.getElementsMatchingTypeNotInDetails("stylesheet", headDetails);
|
1054
|
+
}
|
1055
|
+
getElementsMatchingTypeNotInDetails(matchedType, headDetails) {
|
1056
|
+
return Object.keys(this.detailsByOuterHTML).filter((outerHTML => !(outerHTML in headDetails.detailsByOuterHTML))).map((outerHTML => this.detailsByOuterHTML[outerHTML])).filter((({type: type}) => type == matchedType)).map((({elements: [element]}) => element));
|
1057
|
+
}
|
1058
|
+
getProvisionalElements() {
|
1059
|
+
return Object.keys(this.detailsByOuterHTML).reduce(((result, outerHTML) => {
|
1060
|
+
const {type: type, tracked: tracked, elements: elements} = this.detailsByOuterHTML[outerHTML];
|
1061
|
+
if (type == null && !tracked) {
|
1062
|
+
return [ ...result, ...elements ];
|
1063
|
+
} else if (elements.length > 1) {
|
1064
|
+
return [ ...result, ...elements.slice(1) ];
|
1065
|
+
} else {
|
1066
|
+
return result;
|
1067
|
+
}
|
1068
|
+
}), []);
|
1069
|
+
}
|
1070
|
+
getMetaValue(name) {
|
1071
|
+
const element = this.findMetaElementByName(name);
|
1072
|
+
return element ? element.getAttribute("content") : null;
|
1073
|
+
}
|
1074
|
+
findMetaElementByName(name) {
|
1075
|
+
return Object.keys(this.detailsByOuterHTML).reduce(((result, outerHTML) => {
|
1076
|
+
const {elements: [element]} = this.detailsByOuterHTML[outerHTML];
|
1077
|
+
return elementIsMetaElementWithName(element, name) ? element : result;
|
1078
|
+
}), undefined);
|
1079
|
+
}
|
1080
|
+
}
|
1081
|
+
|
1082
|
+
function elementType(element) {
|
1083
|
+
if (elementIsScript(element)) {
|
1084
|
+
return "script";
|
1085
|
+
} else if (elementIsStylesheet(element)) {
|
1086
|
+
return "stylesheet";
|
1087
|
+
}
|
1088
|
+
}
|
1089
|
+
|
1090
|
+
function elementIsTracked(element) {
|
1091
|
+
return element.getAttribute("data-turbo-track") == "reload";
|
1092
|
+
}
|
1093
|
+
|
1094
|
+
function elementIsScript(element) {
|
1095
|
+
const tagName = element.tagName.toLowerCase();
|
1096
|
+
return tagName == "script";
|
1097
|
+
}
|
1098
|
+
|
1099
|
+
function elementIsStylesheet(element) {
|
1100
|
+
const tagName = element.tagName.toLowerCase();
|
1101
|
+
return tagName == "style" || tagName == "link" && element.getAttribute("rel") == "stylesheet";
|
1102
|
+
}
|
1103
|
+
|
1104
|
+
function elementIsMetaElementWithName(element, name) {
|
1105
|
+
const tagName = element.tagName.toLowerCase();
|
1106
|
+
return tagName == "meta" && element.getAttribute("name") == name;
|
1107
|
+
}
|
1108
|
+
|
1109
|
+
class Snapshot {
|
1110
|
+
constructor(headDetails, bodyElement) {
|
1111
|
+
this.headDetails = headDetails;
|
1112
|
+
this.bodyElement = bodyElement;
|
1113
|
+
}
|
1114
|
+
static wrap(value) {
|
1115
|
+
if (value instanceof this) {
|
1116
|
+
return value;
|
1117
|
+
} else if (typeof value == "string") {
|
1118
|
+
return this.fromHTMLString(value);
|
1119
|
+
} else {
|
1120
|
+
return this.fromHTMLElement(value);
|
1121
|
+
}
|
1122
|
+
}
|
1123
|
+
static fromHTMLString(html) {
|
1124
|
+
const {documentElement: documentElement} = (new DOMParser).parseFromString(html, "text/html");
|
1125
|
+
return this.fromHTMLElement(documentElement);
|
1126
|
+
}
|
1127
|
+
static fromHTMLElement(htmlElement) {
|
1128
|
+
const headElement = htmlElement.querySelector("head");
|
1129
|
+
const bodyElement = htmlElement.querySelector("body") || document.createElement("body");
|
1130
|
+
const headDetails = HeadDetails.fromHeadElement(headElement);
|
1131
|
+
return new this(headDetails, bodyElement);
|
1132
|
+
}
|
1133
|
+
clone() {
|
1134
|
+
const {bodyElement: bodyElement} = Snapshot.fromHTMLString(this.bodyElement.outerHTML);
|
1135
|
+
return new Snapshot(this.headDetails, bodyElement);
|
1136
|
+
}
|
1137
|
+
getRootLocation() {
|
1138
|
+
const root = this.getSetting("root", "/");
|
1139
|
+
return new Location(root);
|
1140
|
+
}
|
1141
|
+
getCacheControlValue() {
|
1142
|
+
return this.getSetting("cache-control");
|
1143
|
+
}
|
1144
|
+
getElementForAnchor(anchor) {
|
1145
|
+
try {
|
1146
|
+
return this.bodyElement.querySelector(`[id='${anchor}'], a[name='${anchor}']`);
|
1147
|
+
} catch (_a) {
|
1148
|
+
return null;
|
1149
|
+
}
|
1150
|
+
}
|
1151
|
+
getPermanentElements() {
|
1152
|
+
return [ ...this.bodyElement.querySelectorAll("[id][data-turbo-permanent]") ];
|
1153
|
+
}
|
1154
|
+
getPermanentElementById(id) {
|
1155
|
+
return this.bodyElement.querySelector(`#${id}[data-turbo-permanent]`);
|
1156
|
+
}
|
1157
|
+
getPermanentElementsPresentInSnapshot(snapshot) {
|
1158
|
+
return this.getPermanentElements().filter((({id: id}) => snapshot.getPermanentElementById(id)));
|
1159
|
+
}
|
1160
|
+
findFirstAutofocusableElement() {
|
1161
|
+
return this.bodyElement.querySelector("[autofocus]");
|
1162
|
+
}
|
1163
|
+
hasAnchor(anchor) {
|
1164
|
+
return this.getElementForAnchor(anchor) != null;
|
1165
|
+
}
|
1166
|
+
isPreviewable() {
|
1167
|
+
return this.getCacheControlValue() != "no-preview";
|
1168
|
+
}
|
1169
|
+
isCacheable() {
|
1170
|
+
return this.getCacheControlValue() != "no-cache";
|
1171
|
+
}
|
1172
|
+
isVisitable() {
|
1173
|
+
return this.getSetting("visit-control") != "reload";
|
1174
|
+
}
|
1175
|
+
getSetting(name, defaultValue) {
|
1176
|
+
const value = this.headDetails.getMetaValue(`turbo-${name}`);
|
1177
|
+
return value == null ? defaultValue : value;
|
1178
|
+
}
|
1179
|
+
}
|
1180
|
+
|
1181
|
+
var TimingMetric;
|
1182
|
+
|
1183
|
+
(function(TimingMetric) {
|
1184
|
+
TimingMetric["visitStart"] = "visitStart";
|
1185
|
+
TimingMetric["requestStart"] = "requestStart";
|
1186
|
+
TimingMetric["requestEnd"] = "requestEnd";
|
1187
|
+
TimingMetric["visitEnd"] = "visitEnd";
|
1188
|
+
})(TimingMetric || (TimingMetric = {}));
|
1189
|
+
|
1190
|
+
var VisitState;
|
1191
|
+
|
1192
|
+
(function(VisitState) {
|
1193
|
+
VisitState["initialized"] = "initialized";
|
1194
|
+
VisitState["started"] = "started";
|
1195
|
+
VisitState["canceled"] = "canceled";
|
1196
|
+
VisitState["failed"] = "failed";
|
1197
|
+
VisitState["completed"] = "completed";
|
1198
|
+
})(VisitState || (VisitState = {}));
|
1199
|
+
|
1200
|
+
const defaultOptions = {
|
1201
|
+
action: "advance",
|
1202
|
+
historyChanged: false
|
1203
|
+
};
|
1204
|
+
|
1205
|
+
var SystemStatusCode;
|
1206
|
+
|
1207
|
+
(function(SystemStatusCode) {
|
1208
|
+
SystemStatusCode[SystemStatusCode["networkFailure"] = 0] = "networkFailure";
|
1209
|
+
SystemStatusCode[SystemStatusCode["timeoutFailure"] = -1] = "timeoutFailure";
|
1210
|
+
SystemStatusCode[SystemStatusCode["contentTypeMismatch"] = -2] = "contentTypeMismatch";
|
1211
|
+
})(SystemStatusCode || (SystemStatusCode = {}));
|
1212
|
+
|
1213
|
+
class Visit {
|
1214
|
+
constructor(delegate, location, restorationIdentifier, options = {}) {
|
1215
|
+
this.identifier = uuid();
|
1216
|
+
this.timingMetrics = {};
|
1217
|
+
this.followedRedirect = false;
|
1218
|
+
this.historyChanged = false;
|
1219
|
+
this.scrolled = false;
|
1220
|
+
this.snapshotCached = false;
|
1221
|
+
this.state = VisitState.initialized;
|
1222
|
+
this.performScroll = () => {
|
1223
|
+
if (!this.scrolled) {
|
1224
|
+
if (this.action == "restore") {
|
1225
|
+
this.scrollToRestoredPosition() || this.scrollToTop();
|
1226
|
+
} else {
|
1227
|
+
this.scrollToAnchor() || this.scrollToTop();
|
1228
|
+
}
|
1229
|
+
this.scrolled = true;
|
1230
|
+
}
|
1231
|
+
};
|
1232
|
+
this.delegate = delegate;
|
1233
|
+
this.location = location;
|
1234
|
+
this.restorationIdentifier = restorationIdentifier || uuid();
|
1235
|
+
const {action: action, historyChanged: historyChanged, referrer: referrer, snapshotHTML: snapshotHTML, response: response} = Object.assign(Object.assign({}, defaultOptions), options);
|
1236
|
+
this.action = action;
|
1237
|
+
this.historyChanged = historyChanged;
|
1238
|
+
this.referrer = referrer;
|
1239
|
+
this.snapshotHTML = snapshotHTML;
|
1240
|
+
this.response = response;
|
1241
|
+
}
|
1242
|
+
get adapter() {
|
1243
|
+
return this.delegate.adapter;
|
1244
|
+
}
|
1245
|
+
get view() {
|
1246
|
+
return this.delegate.view;
|
1247
|
+
}
|
1248
|
+
get history() {
|
1249
|
+
return this.delegate.history;
|
1250
|
+
}
|
1251
|
+
get restorationData() {
|
1252
|
+
return this.history.getRestorationDataForIdentifier(this.restorationIdentifier);
|
1253
|
+
}
|
1254
|
+
start() {
|
1255
|
+
if (this.state == VisitState.initialized) {
|
1256
|
+
this.recordTimingMetric(TimingMetric.visitStart);
|
1257
|
+
this.state = VisitState.started;
|
1258
|
+
this.adapter.visitStarted(this);
|
1259
|
+
this.delegate.visitStarted(this);
|
1260
|
+
}
|
1261
|
+
}
|
1262
|
+
cancel() {
|
1263
|
+
if (this.state == VisitState.started) {
|
1264
|
+
if (this.request) {
|
1265
|
+
this.request.cancel();
|
1266
|
+
}
|
1267
|
+
this.cancelRender();
|
1268
|
+
this.state = VisitState.canceled;
|
1269
|
+
}
|
1270
|
+
}
|
1271
|
+
complete() {
|
1272
|
+
if (this.state == VisitState.started) {
|
1273
|
+
this.recordTimingMetric(TimingMetric.visitEnd);
|
1274
|
+
this.state = VisitState.completed;
|
1275
|
+
this.adapter.visitCompleted(this);
|
1276
|
+
this.delegate.visitCompleted(this);
|
1277
|
+
}
|
1278
|
+
}
|
1279
|
+
fail() {
|
1280
|
+
if (this.state == VisitState.started) {
|
1281
|
+
this.state = VisitState.failed;
|
1282
|
+
this.adapter.visitFailed(this);
|
1283
|
+
}
|
1284
|
+
}
|
1285
|
+
changeHistory() {
|
1286
|
+
if (!this.historyChanged) {
|
1287
|
+
const actionForHistory = this.location.isEqualTo(this.referrer) ? "replace" : this.action;
|
1288
|
+
const method = this.getHistoryMethodForAction(actionForHistory);
|
1289
|
+
this.history.update(method, this.location, this.restorationIdentifier);
|
1290
|
+
this.historyChanged = true;
|
1291
|
+
}
|
1292
|
+
}
|
1293
|
+
issueRequest() {
|
1294
|
+
if (this.hasPreloadedResponse()) {
|
1295
|
+
this.simulateRequest();
|
1296
|
+
} else if (this.shouldIssueRequest() && !this.request) {
|
1297
|
+
this.request = new FetchRequest(this, FetchMethod.get, this.location);
|
1298
|
+
this.request.perform();
|
1299
|
+
}
|
1300
|
+
}
|
1301
|
+
simulateRequest() {
|
1302
|
+
if (this.response) {
|
1303
|
+
this.startRequest();
|
1304
|
+
this.recordResponse();
|
1305
|
+
this.finishRequest();
|
1306
|
+
}
|
1307
|
+
}
|
1308
|
+
startRequest() {
|
1309
|
+
this.recordTimingMetric(TimingMetric.requestStart);
|
1310
|
+
this.adapter.visitRequestStarted(this);
|
1311
|
+
}
|
1312
|
+
recordResponse(response = this.response) {
|
1313
|
+
this.response = response;
|
1314
|
+
if (response) {
|
1315
|
+
const {statusCode: statusCode} = response;
|
1316
|
+
if (isSuccessful(statusCode)) {
|
1317
|
+
this.adapter.visitRequestCompleted(this);
|
1318
|
+
} else {
|
1319
|
+
this.adapter.visitRequestFailedWithStatusCode(this, statusCode);
|
1320
|
+
}
|
1321
|
+
}
|
1322
|
+
}
|
1323
|
+
finishRequest() {
|
1324
|
+
this.recordTimingMetric(TimingMetric.requestEnd);
|
1325
|
+
this.adapter.visitRequestFinished(this);
|
1326
|
+
}
|
1327
|
+
loadResponse() {
|
1328
|
+
if (this.response) {
|
1329
|
+
const {statusCode: statusCode, responseHTML: responseHTML} = this.response;
|
1330
|
+
this.render((() => {
|
1331
|
+
this.cacheSnapshot();
|
1332
|
+
if (isSuccessful(statusCode) && responseHTML != null) {
|
1333
|
+
this.view.render({
|
1334
|
+
snapshot: Snapshot.fromHTMLString(responseHTML)
|
1335
|
+
}, this.performScroll);
|
1336
|
+
this.adapter.visitRendered(this);
|
1337
|
+
this.complete();
|
1338
|
+
} else {
|
1339
|
+
this.view.render({
|
1340
|
+
error: responseHTML
|
1341
|
+
}, this.performScroll);
|
1342
|
+
this.adapter.visitRendered(this);
|
1343
|
+
this.fail();
|
1344
|
+
}
|
1345
|
+
}));
|
1346
|
+
}
|
1347
|
+
}
|
1348
|
+
getCachedSnapshot() {
|
1349
|
+
const snapshot = this.view.getCachedSnapshotForLocation(this.location) || this.getPreloadedSnapshot();
|
1350
|
+
if (snapshot && (!this.location.anchor || snapshot.hasAnchor(this.location.anchor))) {
|
1351
|
+
if (this.action == "restore" || snapshot.isPreviewable()) {
|
1352
|
+
return snapshot;
|
1353
|
+
}
|
1354
|
+
}
|
1355
|
+
}
|
1356
|
+
getPreloadedSnapshot() {
|
1357
|
+
if (this.snapshotHTML) {
|
1358
|
+
return Snapshot.wrap(this.snapshotHTML);
|
1359
|
+
}
|
1360
|
+
}
|
1361
|
+
hasCachedSnapshot() {
|
1362
|
+
return this.getCachedSnapshot() != null;
|
1363
|
+
}
|
1364
|
+
loadCachedSnapshot() {
|
1365
|
+
const snapshot = this.getCachedSnapshot();
|
1366
|
+
if (snapshot) {
|
1367
|
+
const isPreview = this.shouldIssueRequest();
|
1368
|
+
this.render((() => {
|
1369
|
+
this.cacheSnapshot();
|
1370
|
+
this.view.render({
|
1371
|
+
snapshot: snapshot,
|
1372
|
+
isPreview: isPreview
|
1373
|
+
}, this.performScroll);
|
1374
|
+
this.adapter.visitRendered(this);
|
1375
|
+
if (!isPreview) {
|
1376
|
+
this.complete();
|
1377
|
+
}
|
1378
|
+
}));
|
1379
|
+
}
|
1380
|
+
}
|
1381
|
+
followRedirect() {
|
1382
|
+
if (this.redirectedToLocation && !this.followedRedirect) {
|
1383
|
+
this.location = this.redirectedToLocation;
|
1384
|
+
this.history.replace(this.redirectedToLocation, this.restorationIdentifier);
|
1385
|
+
this.followedRedirect = true;
|
1386
|
+
}
|
1387
|
+
}
|
1388
|
+
requestStarted() {
|
1389
|
+
this.startRequest();
|
1390
|
+
}
|
1391
|
+
requestPreventedHandlingResponse(request, response) {}
|
1392
|
+
async requestSucceededWithResponse(request, response) {
|
1393
|
+
const responseHTML = await response.responseHTML;
|
1394
|
+
if (responseHTML == undefined) {
|
1395
|
+
this.recordResponse({
|
1396
|
+
statusCode: SystemStatusCode.contentTypeMismatch
|
1397
|
+
});
|
1398
|
+
} else {
|
1399
|
+
this.redirectedToLocation = response.redirected ? response.location : undefined;
|
1400
|
+
this.recordResponse({
|
1401
|
+
statusCode: response.statusCode,
|
1402
|
+
responseHTML: responseHTML
|
1403
|
+
});
|
1404
|
+
}
|
1405
|
+
}
|
1406
|
+
async requestFailedWithResponse(request, response) {
|
1407
|
+
const responseHTML = await response.responseHTML;
|
1408
|
+
if (responseHTML == undefined) {
|
1409
|
+
this.recordResponse({
|
1410
|
+
statusCode: SystemStatusCode.contentTypeMismatch
|
1411
|
+
});
|
1412
|
+
} else {
|
1413
|
+
this.recordResponse({
|
1414
|
+
statusCode: response.statusCode,
|
1415
|
+
responseHTML: responseHTML
|
1416
|
+
});
|
1417
|
+
}
|
1418
|
+
}
|
1419
|
+
requestErrored(request, error) {
|
1420
|
+
this.recordResponse({
|
1421
|
+
statusCode: SystemStatusCode.networkFailure
|
1422
|
+
});
|
1423
|
+
}
|
1424
|
+
requestFinished() {
|
1425
|
+
this.finishRequest();
|
1426
|
+
}
|
1427
|
+
scrollToRestoredPosition() {
|
1428
|
+
const {scrollPosition: scrollPosition} = this.restorationData;
|
1429
|
+
if (scrollPosition) {
|
1430
|
+
this.view.scrollToPosition(scrollPosition);
|
1431
|
+
return true;
|
1432
|
+
}
|
1433
|
+
}
|
1434
|
+
scrollToAnchor() {
|
1435
|
+
if (this.location.anchor != null) {
|
1436
|
+
this.view.scrollToAnchor(this.location.anchor);
|
1437
|
+
return true;
|
1438
|
+
}
|
1439
|
+
}
|
1440
|
+
scrollToTop() {
|
1441
|
+
this.view.scrollToPosition({
|
1442
|
+
x: 0,
|
1443
|
+
y: 0
|
1444
|
+
});
|
1445
|
+
}
|
1446
|
+
recordTimingMetric(metric) {
|
1447
|
+
this.timingMetrics[metric] = (new Date).getTime();
|
1448
|
+
}
|
1449
|
+
getTimingMetrics() {
|
1450
|
+
return Object.assign({}, this.timingMetrics);
|
1451
|
+
}
|
1452
|
+
getHistoryMethodForAction(action) {
|
1453
|
+
switch (action) {
|
1454
|
+
case "replace":
|
1455
|
+
return history.replaceState;
|
1456
|
+
|
1457
|
+
case "advance":
|
1458
|
+
case "restore":
|
1459
|
+
return history.pushState;
|
1460
|
+
}
|
1461
|
+
}
|
1462
|
+
hasPreloadedResponse() {
|
1463
|
+
return typeof this.response == "object";
|
1464
|
+
}
|
1465
|
+
shouldIssueRequest() {
|
1466
|
+
return this.action == "restore" ? !this.hasCachedSnapshot() : true;
|
1467
|
+
}
|
1468
|
+
cacheSnapshot() {
|
1469
|
+
if (!this.snapshotCached) {
|
1470
|
+
this.view.cacheSnapshot();
|
1471
|
+
this.snapshotCached = true;
|
1472
|
+
}
|
1473
|
+
}
|
1474
|
+
render(callback) {
|
1475
|
+
this.cancelRender();
|
1476
|
+
this.frame = requestAnimationFrame((() => {
|
1477
|
+
delete this.frame;
|
1478
|
+
callback.call(this);
|
1479
|
+
}));
|
1480
|
+
}
|
1481
|
+
cancelRender() {
|
1482
|
+
if (this.frame) {
|
1483
|
+
cancelAnimationFrame(this.frame);
|
1484
|
+
delete this.frame;
|
1485
|
+
}
|
1486
|
+
}
|
1487
|
+
}
|
1488
|
+
|
1489
|
+
function isSuccessful(statusCode) {
|
1490
|
+
return statusCode >= 200 && statusCode < 300;
|
1491
|
+
}
|
1492
|
+
|
1493
|
+
class BrowserAdapter {
|
1494
|
+
constructor(session) {
|
1495
|
+
this.progressBar = new ProgressBar;
|
1496
|
+
this.showProgressBar = () => {
|
1497
|
+
this.progressBar.show();
|
1498
|
+
};
|
1499
|
+
this.session = session;
|
1500
|
+
}
|
1501
|
+
visitProposedToLocation(location, options) {
|
1502
|
+
this.navigator.startVisit(location, uuid(), options);
|
1503
|
+
}
|
1504
|
+
visitStarted(visit) {
|
1505
|
+
visit.issueRequest();
|
1506
|
+
visit.changeHistory();
|
1507
|
+
visit.loadCachedSnapshot();
|
1508
|
+
}
|
1509
|
+
visitRequestStarted(visit) {
|
1510
|
+
this.progressBar.setValue(0);
|
1511
|
+
if (visit.hasCachedSnapshot() || visit.action != "restore") {
|
1512
|
+
this.showProgressBarAfterDelay();
|
1513
|
+
} else {
|
1514
|
+
this.showProgressBar();
|
1515
|
+
}
|
1516
|
+
}
|
1517
|
+
visitRequestCompleted(visit) {
|
1518
|
+
visit.loadResponse();
|
1519
|
+
}
|
1520
|
+
visitRequestFailedWithStatusCode(visit, statusCode) {
|
1521
|
+
switch (statusCode) {
|
1522
|
+
case SystemStatusCode.networkFailure:
|
1523
|
+
case SystemStatusCode.timeoutFailure:
|
1524
|
+
case SystemStatusCode.contentTypeMismatch:
|
1525
|
+
return this.reload();
|
1526
|
+
|
1527
|
+
default:
|
1528
|
+
return visit.loadResponse();
|
1529
|
+
}
|
1530
|
+
}
|
1531
|
+
visitRequestFinished(visit) {
|
1532
|
+
this.progressBar.setValue(1);
|
1533
|
+
this.hideProgressBar();
|
1534
|
+
}
|
1535
|
+
visitCompleted(visit) {
|
1536
|
+
visit.followRedirect();
|
1537
|
+
}
|
1538
|
+
pageInvalidated() {
|
1539
|
+
this.reload();
|
1540
|
+
}
|
1541
|
+
visitFailed(visit) {}
|
1542
|
+
visitRendered(visit) {}
|
1543
|
+
showProgressBarAfterDelay() {
|
1544
|
+
this.progressBarTimeout = window.setTimeout(this.showProgressBar, this.session.progressBarDelay);
|
1545
|
+
}
|
1546
|
+
hideProgressBar() {
|
1547
|
+
this.progressBar.hide();
|
1548
|
+
if (this.progressBarTimeout != null) {
|
1549
|
+
window.clearTimeout(this.progressBarTimeout);
|
1550
|
+
delete this.progressBarTimeout;
|
1551
|
+
}
|
1552
|
+
}
|
1553
|
+
reload() {
|
1554
|
+
window.location.reload();
|
1555
|
+
}
|
1556
|
+
get navigator() {
|
1557
|
+
return this.session.navigator;
|
1558
|
+
}
|
1559
|
+
}
|
1560
|
+
|
1561
|
+
class FormSubmitObserver {
|
1562
|
+
constructor(delegate) {
|
1563
|
+
this.started = false;
|
1564
|
+
this.submitCaptured = () => {
|
1565
|
+
removeEventListener("submit", this.submitBubbled, false);
|
1566
|
+
addEventListener("submit", this.submitBubbled, false);
|
1567
|
+
};
|
1568
|
+
this.submitBubbled = event => {
|
1569
|
+
if (!event.defaultPrevented) {
|
1570
|
+
const form = event.target instanceof HTMLFormElement ? event.target : undefined;
|
1571
|
+
const submitter = event.submitter || undefined;
|
1572
|
+
if (form) {
|
1573
|
+
if (this.delegate.willSubmitForm(form, submitter)) {
|
1574
|
+
event.preventDefault();
|
1575
|
+
this.delegate.formSubmitted(form, submitter);
|
1576
|
+
}
|
1577
|
+
}
|
1578
|
+
}
|
1579
|
+
};
|
1580
|
+
this.delegate = delegate;
|
1581
|
+
}
|
1582
|
+
start() {
|
1583
|
+
if (!this.started) {
|
1584
|
+
addEventListener("submit", this.submitCaptured, true);
|
1585
|
+
this.started = true;
|
1586
|
+
}
|
1587
|
+
}
|
1588
|
+
stop() {
|
1589
|
+
if (this.started) {
|
1590
|
+
removeEventListener("submit", this.submitCaptured, true);
|
1591
|
+
this.started = false;
|
1592
|
+
}
|
1593
|
+
}
|
1594
|
+
}
|
1595
|
+
|
1596
|
+
class FrameRedirector {
|
1597
|
+
constructor(element) {
|
1598
|
+
this.element = element;
|
1599
|
+
this.linkInterceptor = new LinkInterceptor(this, element);
|
1600
|
+
this.formInterceptor = new FormInterceptor(this, element);
|
1601
|
+
}
|
1602
|
+
start() {
|
1603
|
+
this.linkInterceptor.start();
|
1604
|
+
this.formInterceptor.start();
|
1605
|
+
}
|
1606
|
+
stop() {
|
1607
|
+
this.linkInterceptor.stop();
|
1608
|
+
this.formInterceptor.stop();
|
1609
|
+
}
|
1610
|
+
shouldInterceptLinkClick(element, url) {
|
1611
|
+
return this.shouldRedirect(element);
|
1612
|
+
}
|
1613
|
+
linkClickIntercepted(element, url) {
|
1614
|
+
const frame = this.findFrameElement(element);
|
1615
|
+
if (frame) {
|
1616
|
+
frame.src = url;
|
1617
|
+
}
|
1618
|
+
}
|
1619
|
+
shouldInterceptFormSubmission(element, submitter) {
|
1620
|
+
return this.shouldRedirect(element, submitter);
|
1621
|
+
}
|
1622
|
+
formSubmissionIntercepted(element, submitter) {
|
1623
|
+
const frame = this.findFrameElement(element);
|
1624
|
+
if (frame) {
|
1625
|
+
frame.formSubmissionIntercepted(element, submitter);
|
1626
|
+
}
|
1627
|
+
}
|
1628
|
+
shouldRedirect(element, submitter) {
|
1629
|
+
const frame = this.findFrameElement(element);
|
1630
|
+
return frame ? frame != element.closest("turbo-frame") : false;
|
1631
|
+
}
|
1632
|
+
findFrameElement(element) {
|
1633
|
+
const id = element.getAttribute("data-turbo-frame");
|
1634
|
+
if (id && id != "_top") {
|
1635
|
+
const frame = this.element.querySelector(`#${id}:not([disabled])`);
|
1636
|
+
if (frame instanceof FrameElement) {
|
1637
|
+
return frame;
|
1638
|
+
}
|
1639
|
+
}
|
1640
|
+
}
|
1641
|
+
}
|
1642
|
+
|
1643
|
+
class History {
|
1644
|
+
constructor(delegate) {
|
1645
|
+
this.restorationIdentifier = uuid();
|
1646
|
+
this.restorationData = {};
|
1647
|
+
this.started = false;
|
1648
|
+
this.pageLoaded = false;
|
1649
|
+
this.onPopState = event => {
|
1650
|
+
if (this.shouldHandlePopState()) {
|
1651
|
+
const {turbo: turbo} = event.state || {};
|
1652
|
+
if (turbo) {
|
1653
|
+
const location = Location.currentLocation;
|
1654
|
+
this.location = location;
|
1655
|
+
const {restorationIdentifier: restorationIdentifier} = turbo;
|
1656
|
+
this.restorationIdentifier = restorationIdentifier;
|
1657
|
+
this.delegate.historyPoppedToLocationWithRestorationIdentifier(location, restorationIdentifier);
|
1658
|
+
}
|
1659
|
+
}
|
1660
|
+
};
|
1661
|
+
this.onPageLoad = async event => {
|
1662
|
+
await nextMicrotask();
|
1663
|
+
this.pageLoaded = true;
|
1664
|
+
};
|
1665
|
+
this.delegate = delegate;
|
1666
|
+
}
|
1667
|
+
start() {
|
1668
|
+
if (!this.started) {
|
1669
|
+
this.previousScrollRestoration = history.scrollRestoration;
|
1670
|
+
history.scrollRestoration = "manual";
|
1671
|
+
addEventListener("popstate", this.onPopState, false);
|
1672
|
+
addEventListener("load", this.onPageLoad, false);
|
1673
|
+
this.started = true;
|
1674
|
+
this.replace(Location.currentLocation);
|
1675
|
+
}
|
1676
|
+
}
|
1677
|
+
stop() {
|
1678
|
+
var _a;
|
1679
|
+
if (this.started) {
|
1680
|
+
history.scrollRestoration = (_a = this.previousScrollRestoration) !== null && _a !== void 0 ? _a : "auto";
|
1681
|
+
removeEventListener("popstate", this.onPopState, false);
|
1682
|
+
removeEventListener("load", this.onPageLoad, false);
|
1683
|
+
this.started = false;
|
1684
|
+
}
|
1685
|
+
}
|
1686
|
+
push(location, restorationIdentifier) {
|
1687
|
+
this.update(history.pushState, location, restorationIdentifier);
|
1688
|
+
}
|
1689
|
+
replace(location, restorationIdentifier) {
|
1690
|
+
this.update(history.replaceState, location, restorationIdentifier);
|
1691
|
+
}
|
1692
|
+
update(method, location, restorationIdentifier = uuid()) {
|
1693
|
+
const state = {
|
1694
|
+
turbo: {
|
1695
|
+
restorationIdentifier: restorationIdentifier
|
1696
|
+
}
|
1697
|
+
};
|
1698
|
+
method.call(history, state, "", location.absoluteURL);
|
1699
|
+
this.location = location;
|
1700
|
+
this.restorationIdentifier = restorationIdentifier;
|
1701
|
+
}
|
1702
|
+
getRestorationDataForIdentifier(restorationIdentifier) {
|
1703
|
+
return this.restorationData[restorationIdentifier] || {};
|
1704
|
+
}
|
1705
|
+
updateRestorationData(additionalData) {
|
1706
|
+
const {restorationIdentifier: restorationIdentifier} = this;
|
1707
|
+
const restorationData = this.restorationData[restorationIdentifier];
|
1708
|
+
this.restorationData[restorationIdentifier] = Object.assign(Object.assign({}, restorationData), additionalData);
|
1709
|
+
}
|
1710
|
+
shouldHandlePopState() {
|
1711
|
+
return this.pageIsLoaded();
|
1712
|
+
}
|
1713
|
+
pageIsLoaded() {
|
1714
|
+
return this.pageLoaded || document.readyState == "complete";
|
1715
|
+
}
|
1716
|
+
}
|
1717
|
+
|
1718
|
+
class LinkClickObserver {
|
1719
|
+
constructor(delegate) {
|
1720
|
+
this.started = false;
|
1721
|
+
this.clickCaptured = () => {
|
1722
|
+
removeEventListener("click", this.clickBubbled, false);
|
1723
|
+
addEventListener("click", this.clickBubbled, false);
|
1724
|
+
};
|
1725
|
+
this.clickBubbled = event => {
|
1726
|
+
if (this.clickEventIsSignificant(event)) {
|
1727
|
+
const link = this.findLinkFromClickTarget(event.target);
|
1728
|
+
if (link) {
|
1729
|
+
const location = this.getLocationForLink(link);
|
1730
|
+
if (this.delegate.willFollowLinkToLocation(link, location)) {
|
1731
|
+
event.preventDefault();
|
1732
|
+
this.delegate.followedLinkToLocation(link, location);
|
1733
|
+
}
|
1734
|
+
}
|
1735
|
+
}
|
1736
|
+
};
|
1737
|
+
this.delegate = delegate;
|
1738
|
+
}
|
1739
|
+
start() {
|
1740
|
+
if (!this.started) {
|
1741
|
+
addEventListener("click", this.clickCaptured, true);
|
1742
|
+
this.started = true;
|
1743
|
+
}
|
1744
|
+
}
|
1745
|
+
stop() {
|
1746
|
+
if (this.started) {
|
1747
|
+
removeEventListener("click", this.clickCaptured, true);
|
1748
|
+
this.started = false;
|
1749
|
+
}
|
1750
|
+
}
|
1751
|
+
clickEventIsSignificant(event) {
|
1752
|
+
return !(event.target && event.target.isContentEditable || event.defaultPrevented || event.which > 1 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey);
|
1753
|
+
}
|
1754
|
+
findLinkFromClickTarget(target) {
|
1755
|
+
if (target instanceof Element) {
|
1756
|
+
return target.closest("a[href]:not([target^=_]):not([download])");
|
1757
|
+
}
|
1758
|
+
}
|
1759
|
+
getLocationForLink(link) {
|
1760
|
+
return new Location(link.getAttribute("href") || "");
|
1761
|
+
}
|
1762
|
+
}
|
1763
|
+
|
1764
|
+
class Navigator {
|
1765
|
+
constructor(delegate) {
|
1766
|
+
this.delegate = delegate;
|
1767
|
+
}
|
1768
|
+
proposeVisit(location, options = {}) {
|
1769
|
+
if (this.delegate.allowsVisitingLocation(location)) {
|
1770
|
+
this.delegate.visitProposedToLocation(location, options);
|
1771
|
+
}
|
1772
|
+
}
|
1773
|
+
startVisit(location, restorationIdentifier, options = {}) {
|
1774
|
+
this.stop();
|
1775
|
+
this.currentVisit = new Visit(this, Location.wrap(location), restorationIdentifier, Object.assign({
|
1776
|
+
referrer: this.location
|
1777
|
+
}, options));
|
1778
|
+
this.currentVisit.start();
|
1779
|
+
}
|
1780
|
+
submitForm(form, submitter) {
|
1781
|
+
this.stop();
|
1782
|
+
this.formSubmission = new FormSubmission(this, form, submitter, true);
|
1783
|
+
this.formSubmission.start();
|
1784
|
+
}
|
1785
|
+
stop() {
|
1786
|
+
if (this.formSubmission) {
|
1787
|
+
this.formSubmission.stop();
|
1788
|
+
delete this.formSubmission;
|
1789
|
+
}
|
1790
|
+
if (this.currentVisit) {
|
1791
|
+
this.currentVisit.cancel();
|
1792
|
+
delete this.currentVisit;
|
1793
|
+
}
|
1794
|
+
}
|
1795
|
+
get adapter() {
|
1796
|
+
return this.delegate.adapter;
|
1797
|
+
}
|
1798
|
+
get view() {
|
1799
|
+
return this.delegate.view;
|
1800
|
+
}
|
1801
|
+
get history() {
|
1802
|
+
return this.delegate.history;
|
1803
|
+
}
|
1804
|
+
formSubmissionStarted(formSubmission) {}
|
1805
|
+
async formSubmissionSucceededWithResponse(formSubmission, fetchResponse) {
|
1806
|
+
console.log("Form submission succeeded", formSubmission);
|
1807
|
+
if (formSubmission == this.formSubmission) {
|
1808
|
+
const responseHTML = await fetchResponse.responseHTML;
|
1809
|
+
if (responseHTML) {
|
1810
|
+
if (formSubmission.method != FetchMethod.get) {
|
1811
|
+
console.log("Clearing snapshot cache after successful form submission");
|
1812
|
+
this.view.clearSnapshotCache();
|
1813
|
+
}
|
1814
|
+
const {statusCode: statusCode} = fetchResponse;
|
1815
|
+
const visitOptions = {
|
1816
|
+
response: {
|
1817
|
+
statusCode: statusCode,
|
1818
|
+
responseHTML: responseHTML
|
1819
|
+
}
|
1820
|
+
};
|
1821
|
+
console.log("Visiting", fetchResponse.location, visitOptions);
|
1822
|
+
this.proposeVisit(fetchResponse.location, visitOptions);
|
1823
|
+
}
|
1824
|
+
}
|
1825
|
+
}
|
1826
|
+
formSubmissionFailedWithResponse(formSubmission, fetchResponse) {
|
1827
|
+
console.error("Form submission failed", formSubmission, fetchResponse);
|
1828
|
+
}
|
1829
|
+
formSubmissionErrored(formSubmission, error) {
|
1830
|
+
console.error("Form submission failed", formSubmission, error);
|
1831
|
+
}
|
1832
|
+
formSubmissionFinished(formSubmission) {}
|
1833
|
+
visitStarted(visit) {
|
1834
|
+
this.delegate.visitStarted(visit);
|
1835
|
+
}
|
1836
|
+
visitCompleted(visit) {
|
1837
|
+
this.delegate.visitCompleted(visit);
|
1838
|
+
}
|
1839
|
+
get location() {
|
1840
|
+
return this.history.location;
|
1841
|
+
}
|
1842
|
+
get restorationIdentifier() {
|
1843
|
+
return this.history.restorationIdentifier;
|
1844
|
+
}
|
1845
|
+
}
|
1846
|
+
|
1847
|
+
var PageStage;
|
1848
|
+
|
1849
|
+
(function(PageStage) {
|
1850
|
+
PageStage[PageStage["initial"] = 0] = "initial";
|
1851
|
+
PageStage[PageStage["loading"] = 1] = "loading";
|
1852
|
+
PageStage[PageStage["interactive"] = 2] = "interactive";
|
1853
|
+
PageStage[PageStage["complete"] = 3] = "complete";
|
1854
|
+
PageStage[PageStage["invalidated"] = 4] = "invalidated";
|
1855
|
+
})(PageStage || (PageStage = {}));
|
1856
|
+
|
1857
|
+
class PageObserver {
|
1858
|
+
constructor(delegate) {
|
1859
|
+
this.stage = PageStage.initial;
|
1860
|
+
this.started = false;
|
1861
|
+
this.interpretReadyState = () => {
|
1862
|
+
const {readyState: readyState} = this;
|
1863
|
+
if (readyState == "interactive") {
|
1864
|
+
this.pageIsInteractive();
|
1865
|
+
} else if (readyState == "complete") {
|
1866
|
+
this.pageIsComplete();
|
1867
|
+
}
|
1868
|
+
};
|
1869
|
+
this.delegate = delegate;
|
1870
|
+
}
|
1871
|
+
start() {
|
1872
|
+
if (!this.started) {
|
1873
|
+
if (this.stage == PageStage.initial) {
|
1874
|
+
this.stage = PageStage.loading;
|
1875
|
+
}
|
1876
|
+
document.addEventListener("readystatechange", this.interpretReadyState, false);
|
1877
|
+
this.started = true;
|
1878
|
+
}
|
1879
|
+
}
|
1880
|
+
stop() {
|
1881
|
+
if (this.started) {
|
1882
|
+
document.removeEventListener("readystatechange", this.interpretReadyState, false);
|
1883
|
+
this.started = false;
|
1884
|
+
}
|
1885
|
+
}
|
1886
|
+
invalidate() {
|
1887
|
+
if (this.stage != PageStage.invalidated) {
|
1888
|
+
this.stage = PageStage.invalidated;
|
1889
|
+
this.delegate.pageInvalidated();
|
1890
|
+
}
|
1891
|
+
}
|
1892
|
+
pageIsInteractive() {
|
1893
|
+
if (this.stage == PageStage.loading) {
|
1894
|
+
this.stage = PageStage.interactive;
|
1895
|
+
this.delegate.pageBecameInteractive();
|
1896
|
+
}
|
1897
|
+
}
|
1898
|
+
pageIsComplete() {
|
1899
|
+
this.pageIsInteractive();
|
1900
|
+
if (this.stage == PageStage.interactive) {
|
1901
|
+
this.stage = PageStage.complete;
|
1902
|
+
this.delegate.pageLoaded();
|
1903
|
+
}
|
1904
|
+
}
|
1905
|
+
get readyState() {
|
1906
|
+
return document.readyState;
|
1907
|
+
}
|
1908
|
+
}
|
1909
|
+
|
1910
|
+
class ScrollObserver {
|
1911
|
+
constructor(delegate) {
|
1912
|
+
this.started = false;
|
1913
|
+
this.onScroll = () => {
|
1914
|
+
this.updatePosition({
|
1915
|
+
x: window.pageXOffset,
|
1916
|
+
y: window.pageYOffset
|
1917
|
+
});
|
1918
|
+
};
|
1919
|
+
this.delegate = delegate;
|
1920
|
+
}
|
1921
|
+
start() {
|
1922
|
+
if (!this.started) {
|
1923
|
+
addEventListener("scroll", this.onScroll, false);
|
1924
|
+
this.onScroll();
|
1925
|
+
this.started = true;
|
1926
|
+
}
|
1927
|
+
}
|
1928
|
+
stop() {
|
1929
|
+
if (this.started) {
|
1930
|
+
removeEventListener("scroll", this.onScroll, false);
|
1931
|
+
this.started = false;
|
1932
|
+
}
|
1933
|
+
}
|
1934
|
+
updatePosition(position) {
|
1935
|
+
this.delegate.scrollPositionChanged(position);
|
1936
|
+
}
|
1937
|
+
}
|
1938
|
+
|
1939
|
+
class StreamMessage {
|
1940
|
+
constructor(html) {
|
1941
|
+
this.templateElement = document.createElement("template");
|
1942
|
+
this.templateElement.innerHTML = html;
|
1943
|
+
}
|
1944
|
+
static wrap(message) {
|
1945
|
+
if (typeof message == "string") {
|
1946
|
+
return new this(message);
|
1947
|
+
} else {
|
1948
|
+
return message;
|
1949
|
+
}
|
1950
|
+
}
|
1951
|
+
get fragment() {
|
1952
|
+
const fragment = document.createDocumentFragment();
|
1953
|
+
for (const element of this.foreignElements) {
|
1954
|
+
fragment.appendChild(document.importNode(element, true));
|
1955
|
+
}
|
1956
|
+
return fragment;
|
1957
|
+
}
|
1958
|
+
get foreignElements() {
|
1959
|
+
return this.templateChildren.reduce(((streamElements, child) => {
|
1960
|
+
if (child.tagName.toLowerCase() == "turbo-stream") {
|
1961
|
+
return [ ...streamElements, child ];
|
1962
|
+
} else {
|
1963
|
+
return streamElements;
|
1964
|
+
}
|
1965
|
+
}), []);
|
1966
|
+
}
|
1967
|
+
get templateChildren() {
|
1968
|
+
return Array.from(this.templateElement.content.children);
|
1969
|
+
}
|
1970
|
+
}
|
1971
|
+
|
1972
|
+
class StreamObserver {
|
1973
|
+
constructor(delegate) {
|
1974
|
+
this.sources = new Set;
|
1975
|
+
this.started = false;
|
1976
|
+
this.prepareFetchRequest = event => {
|
1977
|
+
var _a;
|
1978
|
+
const fetchOptions = (_a = event.detail) === null || _a === void 0 ? void 0 : _a.fetchOptions;
|
1979
|
+
if (fetchOptions) {
|
1980
|
+
const {headers: headers} = fetchOptions;
|
1981
|
+
headers.Accept = [ "text/html; turbo-stream", headers.Accept ].join(", ");
|
1982
|
+
}
|
1983
|
+
};
|
1984
|
+
this.inspectFetchResponse = event => {
|
1985
|
+
const response = fetchResponseFromEvent(event);
|
1986
|
+
if (response && fetchResponseIsStream(response)) {
|
1987
|
+
event.preventDefault();
|
1988
|
+
this.receiveMessageResponse(response);
|
1989
|
+
}
|
1990
|
+
};
|
1991
|
+
this.receiveMessageEvent = event => {
|
1992
|
+
if (this.started && typeof event.data == "string") {
|
1993
|
+
this.receiveMessageHTML(event.data);
|
1994
|
+
}
|
1995
|
+
};
|
1996
|
+
this.delegate = delegate;
|
1997
|
+
}
|
1998
|
+
start() {
|
1999
|
+
if (!this.started) {
|
2000
|
+
this.started = true;
|
2001
|
+
addEventListener("turbo:before-fetch-request", this.prepareFetchRequest, true);
|
2002
|
+
addEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
|
2003
|
+
}
|
2004
|
+
}
|
2005
|
+
stop() {
|
2006
|
+
if (this.started) {
|
2007
|
+
this.started = false;
|
2008
|
+
removeEventListener("turbo:before-fetch-request", this.prepareFetchRequest, true);
|
2009
|
+
removeEventListener("turbo:before-fetch-response", this.inspectFetchResponse, false);
|
2010
|
+
}
|
2011
|
+
}
|
2012
|
+
connectStreamSource(source) {
|
2013
|
+
if (!this.streamSourceIsConnected(source)) {
|
2014
|
+
this.sources.add(source);
|
2015
|
+
source.addEventListener("message", this.receiveMessageEvent, false);
|
2016
|
+
}
|
2017
|
+
}
|
2018
|
+
disconnectStreamSource(source) {
|
2019
|
+
if (this.streamSourceIsConnected(source)) {
|
2020
|
+
this.sources.delete(source);
|
2021
|
+
source.removeEventListener("message", this.receiveMessageEvent, false);
|
2022
|
+
}
|
2023
|
+
}
|
2024
|
+
streamSourceIsConnected(source) {
|
2025
|
+
return this.sources.has(source);
|
2026
|
+
}
|
2027
|
+
async receiveMessageResponse(response) {
|
2028
|
+
const html = await response.responseHTML;
|
2029
|
+
if (html) {
|
2030
|
+
this.receiveMessageHTML(html);
|
2031
|
+
}
|
2032
|
+
}
|
2033
|
+
receiveMessageHTML(html) {
|
2034
|
+
this.delegate.receivedMessageFromStream(new StreamMessage(html));
|
2035
|
+
}
|
2036
|
+
}
|
2037
|
+
|
2038
|
+
function fetchResponseFromEvent(event) {
|
2039
|
+
var _a;
|
2040
|
+
const fetchResponse = (_a = event.detail) === null || _a === void 0 ? void 0 : _a.fetchResponse;
|
2041
|
+
if (fetchResponse instanceof FetchResponse) {
|
2042
|
+
return fetchResponse;
|
2043
|
+
}
|
2044
|
+
}
|
2045
|
+
|
2046
|
+
function fetchResponseIsStream(response) {
|
2047
|
+
var _a;
|
2048
|
+
const contentType = (_a = response.contentType) !== null && _a !== void 0 ? _a : "";
|
2049
|
+
return /text\/html;.*\bturbo-stream\b/.test(contentType);
|
2050
|
+
}
|
2051
|
+
|
2052
|
+
function isAction(action) {
|
2053
|
+
return action == "advance" || action == "replace" || action == "restore";
|
2054
|
+
}
|
2055
|
+
|
2056
|
+
class Renderer {
|
2057
|
+
renderView(callback) {
|
2058
|
+
this.delegate.viewWillRender(this.newBody);
|
2059
|
+
callback();
|
2060
|
+
this.delegate.viewRendered(this.newBody);
|
2061
|
+
}
|
2062
|
+
invalidateView() {
|
2063
|
+
this.delegate.viewInvalidated();
|
2064
|
+
}
|
2065
|
+
createScriptElement(element) {
|
2066
|
+
if (element.getAttribute("data-turbo-eval") == "false") {
|
2067
|
+
return element;
|
2068
|
+
} else {
|
2069
|
+
const createdScriptElement = document.createElement("script");
|
2070
|
+
createdScriptElement.textContent = element.textContent;
|
2071
|
+
createdScriptElement.async = false;
|
2072
|
+
copyElementAttributes(createdScriptElement, element);
|
2073
|
+
return createdScriptElement;
|
2074
|
+
}
|
2075
|
+
}
|
2076
|
+
}
|
2077
|
+
|
2078
|
+
function copyElementAttributes(destinationElement, sourceElement) {
|
2079
|
+
for (const {name: name, value: value} of [ ...sourceElement.attributes ]) {
|
2080
|
+
destinationElement.setAttribute(name, value);
|
2081
|
+
}
|
2082
|
+
}
|
2083
|
+
|
2084
|
+
class ErrorRenderer extends Renderer {
|
2085
|
+
constructor(delegate, html) {
|
2086
|
+
super();
|
2087
|
+
this.delegate = delegate;
|
2088
|
+
this.htmlElement = (() => {
|
2089
|
+
const htmlElement = document.createElement("html");
|
2090
|
+
htmlElement.innerHTML = html;
|
2091
|
+
return htmlElement;
|
2092
|
+
})();
|
2093
|
+
this.newHead = this.htmlElement.querySelector("head") || document.createElement("head");
|
2094
|
+
this.newBody = this.htmlElement.querySelector("body") || document.createElement("body");
|
2095
|
+
}
|
2096
|
+
static render(delegate, callback, html) {
|
2097
|
+
return new this(delegate, html).render(callback);
|
2098
|
+
}
|
2099
|
+
render(callback) {
|
2100
|
+
this.renderView((() => {
|
2101
|
+
this.replaceHeadAndBody();
|
2102
|
+
this.activateBodyScriptElements();
|
2103
|
+
callback();
|
2104
|
+
}));
|
2105
|
+
}
|
2106
|
+
replaceHeadAndBody() {
|
2107
|
+
const {documentElement: documentElement, head: head, body: body} = document;
|
2108
|
+
documentElement.replaceChild(this.newHead, head);
|
2109
|
+
documentElement.replaceChild(this.newBody, body);
|
2110
|
+
}
|
2111
|
+
activateBodyScriptElements() {
|
2112
|
+
for (const replaceableElement of this.getScriptElements()) {
|
2113
|
+
const parentNode = replaceableElement.parentNode;
|
2114
|
+
if (parentNode) {
|
2115
|
+
const element = this.createScriptElement(replaceableElement);
|
2116
|
+
parentNode.replaceChild(element, replaceableElement);
|
2117
|
+
}
|
2118
|
+
}
|
2119
|
+
}
|
2120
|
+
getScriptElements() {
|
2121
|
+
return [ ...document.documentElement.querySelectorAll("script") ];
|
2122
|
+
}
|
2123
|
+
}
|
2124
|
+
|
2125
|
+
class SnapshotCache {
|
2126
|
+
constructor(size) {
|
2127
|
+
this.keys = [];
|
2128
|
+
this.snapshots = {};
|
2129
|
+
this.size = size;
|
2130
|
+
}
|
2131
|
+
has(location) {
|
2132
|
+
return location.toCacheKey() in this.snapshots;
|
2133
|
+
}
|
2134
|
+
get(location) {
|
2135
|
+
if (this.has(location)) {
|
2136
|
+
const snapshot = this.read(location);
|
2137
|
+
this.touch(location);
|
2138
|
+
return snapshot;
|
2139
|
+
}
|
2140
|
+
}
|
2141
|
+
put(location, snapshot) {
|
2142
|
+
this.write(location, snapshot);
|
2143
|
+
this.touch(location);
|
2144
|
+
return snapshot;
|
2145
|
+
}
|
2146
|
+
clear() {
|
2147
|
+
this.snapshots = {};
|
2148
|
+
}
|
2149
|
+
read(location) {
|
2150
|
+
return this.snapshots[location.toCacheKey()];
|
2151
|
+
}
|
2152
|
+
write(location, snapshot) {
|
2153
|
+
this.snapshots[location.toCacheKey()] = snapshot;
|
2154
|
+
}
|
2155
|
+
touch(location) {
|
2156
|
+
const key = location.toCacheKey();
|
2157
|
+
const index = this.keys.indexOf(key);
|
2158
|
+
if (index > -1) this.keys.splice(index, 1);
|
2159
|
+
this.keys.unshift(key);
|
2160
|
+
this.trim();
|
2161
|
+
}
|
2162
|
+
trim() {
|
2163
|
+
for (const key of this.keys.splice(this.size)) {
|
2164
|
+
delete this.snapshots[key];
|
2165
|
+
}
|
2166
|
+
}
|
2167
|
+
}
|
2168
|
+
|
2169
|
+
class SnapshotRenderer extends Renderer {
|
2170
|
+
constructor(delegate, currentSnapshot, newSnapshot, isPreview) {
|
2171
|
+
super();
|
2172
|
+
this.delegate = delegate;
|
2173
|
+
this.currentSnapshot = currentSnapshot;
|
2174
|
+
this.currentHeadDetails = currentSnapshot.headDetails;
|
2175
|
+
this.newSnapshot = newSnapshot;
|
2176
|
+
this.newHeadDetails = newSnapshot.headDetails;
|
2177
|
+
this.newBody = newSnapshot.bodyElement;
|
2178
|
+
this.isPreview = isPreview;
|
2179
|
+
}
|
2180
|
+
static render(delegate, callback, currentSnapshot, newSnapshot, isPreview) {
|
2181
|
+
return new this(delegate, currentSnapshot, newSnapshot, isPreview).render(callback);
|
2182
|
+
}
|
2183
|
+
render(callback) {
|
2184
|
+
if (this.shouldRender()) {
|
2185
|
+
this.mergeHead();
|
2186
|
+
this.renderView((() => {
|
2187
|
+
this.replaceBody();
|
2188
|
+
if (!this.isPreview) {
|
2189
|
+
this.focusFirstAutofocusableElement();
|
2190
|
+
}
|
2191
|
+
callback();
|
2192
|
+
}));
|
2193
|
+
} else {
|
2194
|
+
this.invalidateView();
|
2195
|
+
}
|
2196
|
+
}
|
2197
|
+
mergeHead() {
|
2198
|
+
this.copyNewHeadStylesheetElements();
|
2199
|
+
this.copyNewHeadScriptElements();
|
2200
|
+
this.removeCurrentHeadProvisionalElements();
|
2201
|
+
this.copyNewHeadProvisionalElements();
|
2202
|
+
}
|
2203
|
+
replaceBody() {
|
2204
|
+
const placeholders = this.relocateCurrentBodyPermanentElements();
|
2205
|
+
this.activateNewBody();
|
2206
|
+
this.assignNewBody();
|
2207
|
+
this.replacePlaceholderElementsWithClonedPermanentElements(placeholders);
|
2208
|
+
}
|
2209
|
+
shouldRender() {
|
2210
|
+
return this.newSnapshot.isVisitable() && this.trackedElementsAreIdentical();
|
2211
|
+
}
|
2212
|
+
trackedElementsAreIdentical() {
|
2213
|
+
return this.currentHeadDetails.getTrackedElementSignature() == this.newHeadDetails.getTrackedElementSignature();
|
2214
|
+
}
|
2215
|
+
copyNewHeadStylesheetElements() {
|
2216
|
+
for (const element of this.getNewHeadStylesheetElements()) {
|
2217
|
+
document.head.appendChild(element);
|
2218
|
+
}
|
2219
|
+
}
|
2220
|
+
copyNewHeadScriptElements() {
|
2221
|
+
for (const element of this.getNewHeadScriptElements()) {
|
2222
|
+
document.head.appendChild(this.createScriptElement(element));
|
2223
|
+
}
|
2224
|
+
}
|
2225
|
+
removeCurrentHeadProvisionalElements() {
|
2226
|
+
for (const element of this.getCurrentHeadProvisionalElements()) {
|
2227
|
+
document.head.removeChild(element);
|
2228
|
+
}
|
2229
|
+
}
|
2230
|
+
copyNewHeadProvisionalElements() {
|
2231
|
+
for (const element of this.getNewHeadProvisionalElements()) {
|
2232
|
+
document.head.appendChild(element);
|
2233
|
+
}
|
2234
|
+
}
|
2235
|
+
relocateCurrentBodyPermanentElements() {
|
2236
|
+
return this.getCurrentBodyPermanentElements().reduce(((placeholders, permanentElement) => {
|
2237
|
+
const newElement = this.newSnapshot.getPermanentElementById(permanentElement.id);
|
2238
|
+
if (newElement) {
|
2239
|
+
const placeholder = createPlaceholderForPermanentElement(permanentElement);
|
2240
|
+
replaceElementWithElement(permanentElement, placeholder.element);
|
2241
|
+
replaceElementWithElement(newElement, permanentElement);
|
2242
|
+
return [ ...placeholders, placeholder ];
|
2243
|
+
} else {
|
2244
|
+
return placeholders;
|
2245
|
+
}
|
2246
|
+
}), []);
|
2247
|
+
}
|
2248
|
+
replacePlaceholderElementsWithClonedPermanentElements(placeholders) {
|
2249
|
+
for (const {element: element, permanentElement: permanentElement} of placeholders) {
|
2250
|
+
const clonedElement = permanentElement.cloneNode(true);
|
2251
|
+
replaceElementWithElement(element, clonedElement);
|
2252
|
+
}
|
2253
|
+
}
|
2254
|
+
activateNewBody() {
|
2255
|
+
document.adoptNode(this.newBody);
|
2256
|
+
this.activateNewBodyScriptElements();
|
2257
|
+
}
|
2258
|
+
activateNewBodyScriptElements() {
|
2259
|
+
for (const inertScriptElement of this.getNewBodyScriptElements()) {
|
2260
|
+
const activatedScriptElement = this.createScriptElement(inertScriptElement);
|
2261
|
+
replaceElementWithElement(inertScriptElement, activatedScriptElement);
|
2262
|
+
}
|
2263
|
+
}
|
2264
|
+
assignNewBody() {
|
2265
|
+
if (document.body) {
|
2266
|
+
replaceElementWithElement(document.body, this.newBody);
|
2267
|
+
} else {
|
2268
|
+
document.documentElement.appendChild(this.newBody);
|
2269
|
+
}
|
2270
|
+
}
|
2271
|
+
focusFirstAutofocusableElement() {
|
2272
|
+
const element = this.newSnapshot.findFirstAutofocusableElement();
|
2273
|
+
if (elementIsFocusable(element)) {
|
2274
|
+
element.focus();
|
2275
|
+
}
|
2276
|
+
}
|
2277
|
+
getNewHeadStylesheetElements() {
|
2278
|
+
return this.newHeadDetails.getStylesheetElementsNotInDetails(this.currentHeadDetails);
|
2279
|
+
}
|
2280
|
+
getNewHeadScriptElements() {
|
2281
|
+
return this.newHeadDetails.getScriptElementsNotInDetails(this.currentHeadDetails);
|
2282
|
+
}
|
2283
|
+
getCurrentHeadProvisionalElements() {
|
2284
|
+
return this.currentHeadDetails.getProvisionalElements();
|
2285
|
+
}
|
2286
|
+
getNewHeadProvisionalElements() {
|
2287
|
+
return this.newHeadDetails.getProvisionalElements();
|
2288
|
+
}
|
2289
|
+
getCurrentBodyPermanentElements() {
|
2290
|
+
return this.currentSnapshot.getPermanentElementsPresentInSnapshot(this.newSnapshot);
|
2291
|
+
}
|
2292
|
+
getNewBodyScriptElements() {
|
2293
|
+
return [ ...this.newBody.querySelectorAll("script") ];
|
2294
|
+
}
|
2295
|
+
}
|
2296
|
+
|
2297
|
+
function createPlaceholderForPermanentElement(permanentElement) {
|
2298
|
+
const element = document.createElement("meta");
|
2299
|
+
element.setAttribute("name", "turbo-permanent-placeholder");
|
2300
|
+
element.setAttribute("content", permanentElement.id);
|
2301
|
+
return {
|
2302
|
+
element: element,
|
2303
|
+
permanentElement: permanentElement
|
2304
|
+
};
|
2305
|
+
}
|
2306
|
+
|
2307
|
+
function replaceElementWithElement(fromElement, toElement) {
|
2308
|
+
const parentElement = fromElement.parentElement;
|
2309
|
+
if (parentElement) {
|
2310
|
+
return parentElement.replaceChild(toElement, fromElement);
|
2311
|
+
}
|
2312
|
+
}
|
2313
|
+
|
2314
|
+
function elementIsFocusable(element) {
|
2315
|
+
return element && typeof element.focus == "function";
|
2316
|
+
}
|
2317
|
+
|
2318
|
+
class View {
|
2319
|
+
constructor(delegate) {
|
2320
|
+
this.htmlElement = document.documentElement;
|
2321
|
+
this.snapshotCache = new SnapshotCache(10);
|
2322
|
+
this.delegate = delegate;
|
2323
|
+
}
|
2324
|
+
getRootLocation() {
|
2325
|
+
return this.getSnapshot().getRootLocation();
|
2326
|
+
}
|
2327
|
+
getElementForAnchor(anchor) {
|
2328
|
+
return this.getSnapshot().getElementForAnchor(anchor);
|
2329
|
+
}
|
2330
|
+
getSnapshot() {
|
2331
|
+
return Snapshot.fromHTMLElement(this.htmlElement);
|
2332
|
+
}
|
2333
|
+
clearSnapshotCache() {
|
2334
|
+
this.snapshotCache.clear();
|
2335
|
+
}
|
2336
|
+
shouldCacheSnapshot() {
|
2337
|
+
return this.getSnapshot().isCacheable();
|
2338
|
+
}
|
2339
|
+
async cacheSnapshot() {
|
2340
|
+
if (this.shouldCacheSnapshot()) {
|
2341
|
+
this.delegate.viewWillCacheSnapshot();
|
2342
|
+
const snapshot = this.getSnapshot();
|
2343
|
+
const location = this.lastRenderedLocation || Location.currentLocation;
|
2344
|
+
await nextMicrotask();
|
2345
|
+
this.snapshotCache.put(location, snapshot.clone());
|
2346
|
+
}
|
2347
|
+
}
|
2348
|
+
getCachedSnapshotForLocation(location) {
|
2349
|
+
return this.snapshotCache.get(location);
|
2350
|
+
}
|
2351
|
+
render({snapshot: snapshot, error: error, isPreview: isPreview}, callback) {
|
2352
|
+
this.markAsPreview(isPreview);
|
2353
|
+
if (snapshot) {
|
2354
|
+
this.renderSnapshot(snapshot, isPreview, callback);
|
2355
|
+
} else {
|
2356
|
+
this.renderError(error, callback);
|
2357
|
+
}
|
2358
|
+
}
|
2359
|
+
scrollToAnchor(anchor) {
|
2360
|
+
const element = this.getElementForAnchor(anchor);
|
2361
|
+
if (element) {
|
2362
|
+
this.scrollToElement(element);
|
2363
|
+
} else {
|
2364
|
+
this.scrollToPosition({
|
2365
|
+
x: 0,
|
2366
|
+
y: 0
|
2367
|
+
});
|
2368
|
+
}
|
2369
|
+
}
|
2370
|
+
scrollToElement(element) {
|
2371
|
+
element.scrollIntoView();
|
2372
|
+
}
|
2373
|
+
scrollToPosition({x: x, y: y}) {
|
2374
|
+
window.scrollTo(x, y);
|
2375
|
+
}
|
2376
|
+
markAsPreview(isPreview) {
|
2377
|
+
if (isPreview) {
|
2378
|
+
this.htmlElement.setAttribute("data-turbo-preview", "");
|
2379
|
+
} else {
|
2380
|
+
this.htmlElement.removeAttribute("data-turbo-preview");
|
2381
|
+
}
|
2382
|
+
}
|
2383
|
+
renderSnapshot(snapshot, isPreview, callback) {
|
2384
|
+
SnapshotRenderer.render(this.delegate, callback, this.getSnapshot(), snapshot, isPreview || false);
|
2385
|
+
}
|
2386
|
+
renderError(error, callback) {
|
2387
|
+
ErrorRenderer.render(this.delegate, callback, error || "");
|
2388
|
+
}
|
2389
|
+
}
|
2390
|
+
|
2391
|
+
class Session {
|
2392
|
+
constructor() {
|
2393
|
+
this.navigator = new Navigator(this);
|
2394
|
+
this.history = new History(this);
|
2395
|
+
this.view = new View(this);
|
2396
|
+
this.adapter = new BrowserAdapter(this);
|
2397
|
+
this.pageObserver = new PageObserver(this);
|
2398
|
+
this.linkClickObserver = new LinkClickObserver(this);
|
2399
|
+
this.formSubmitObserver = new FormSubmitObserver(this);
|
2400
|
+
this.scrollObserver = new ScrollObserver(this);
|
2401
|
+
this.streamObserver = new StreamObserver(this);
|
2402
|
+
this.frameRedirector = new FrameRedirector(document.documentElement);
|
2403
|
+
this.enabled = true;
|
2404
|
+
this.progressBarDelay = 500;
|
2405
|
+
this.started = false;
|
2406
|
+
}
|
2407
|
+
start() {
|
2408
|
+
if (!this.started) {
|
2409
|
+
this.pageObserver.start();
|
2410
|
+
this.linkClickObserver.start();
|
2411
|
+
this.formSubmitObserver.start();
|
2412
|
+
this.scrollObserver.start();
|
2413
|
+
this.streamObserver.start();
|
2414
|
+
this.frameRedirector.start();
|
2415
|
+
this.history.start();
|
2416
|
+
this.started = true;
|
2417
|
+
this.enabled = true;
|
2418
|
+
}
|
2419
|
+
}
|
2420
|
+
disable() {
|
2421
|
+
this.enabled = false;
|
2422
|
+
}
|
2423
|
+
stop() {
|
2424
|
+
if (this.started) {
|
2425
|
+
this.pageObserver.stop();
|
2426
|
+
this.linkClickObserver.stop();
|
2427
|
+
this.formSubmitObserver.stop();
|
2428
|
+
this.scrollObserver.stop();
|
2429
|
+
this.streamObserver.stop();
|
2430
|
+
this.frameRedirector.stop();
|
2431
|
+
this.history.stop();
|
2432
|
+
this.started = false;
|
2433
|
+
}
|
2434
|
+
}
|
2435
|
+
registerAdapter(adapter) {
|
2436
|
+
this.adapter = adapter;
|
2437
|
+
}
|
2438
|
+
visit(location, options = {}) {
|
2439
|
+
this.navigator.proposeVisit(Location.wrap(location), options);
|
2440
|
+
}
|
2441
|
+
connectStreamSource(source) {
|
2442
|
+
this.streamObserver.connectStreamSource(source);
|
2443
|
+
}
|
2444
|
+
disconnectStreamSource(source) {
|
2445
|
+
this.streamObserver.disconnectStreamSource(source);
|
2446
|
+
}
|
2447
|
+
renderStreamMessage(message) {
|
2448
|
+
document.documentElement.appendChild(StreamMessage.wrap(message).fragment);
|
2449
|
+
}
|
2450
|
+
clearCache() {
|
2451
|
+
this.view.clearSnapshotCache();
|
2452
|
+
}
|
2453
|
+
setProgressBarDelay(delay) {
|
2454
|
+
this.progressBarDelay = delay;
|
2455
|
+
}
|
2456
|
+
get location() {
|
2457
|
+
return this.history.location;
|
2458
|
+
}
|
2459
|
+
get restorationIdentifier() {
|
2460
|
+
return this.history.restorationIdentifier;
|
2461
|
+
}
|
2462
|
+
historyPoppedToLocationWithRestorationIdentifier(location) {
|
2463
|
+
if (this.enabled) {
|
2464
|
+
this.navigator.proposeVisit(location, {
|
2465
|
+
action: "restore",
|
2466
|
+
historyChanged: true
|
2467
|
+
});
|
2468
|
+
} else {
|
2469
|
+
this.adapter.pageInvalidated();
|
2470
|
+
}
|
2471
|
+
}
|
2472
|
+
scrollPositionChanged(position) {
|
2473
|
+
this.history.updateRestorationData({
|
2474
|
+
scrollPosition: position
|
2475
|
+
});
|
2476
|
+
}
|
2477
|
+
willFollowLinkToLocation(link, location) {
|
2478
|
+
return this.linkIsVisitable(link) && this.locationIsVisitable(location) && this.applicationAllowsFollowingLinkToLocation(link, location);
|
2479
|
+
}
|
2480
|
+
followedLinkToLocation(link, location) {
|
2481
|
+
const action = this.getActionForLink(link);
|
2482
|
+
this.visit(location, {
|
2483
|
+
action: action
|
2484
|
+
});
|
2485
|
+
}
|
2486
|
+
allowsVisitingLocation(location) {
|
2487
|
+
return this.applicationAllowsVisitingLocation(location);
|
2488
|
+
}
|
2489
|
+
visitProposedToLocation(location, options) {
|
2490
|
+
this.adapter.visitProposedToLocation(location, options);
|
2491
|
+
}
|
2492
|
+
visitStarted(visit) {
|
2493
|
+
this.notifyApplicationAfterVisitingLocation(visit.location);
|
2494
|
+
}
|
2495
|
+
visitCompleted(visit) {
|
2496
|
+
this.notifyApplicationAfterPageLoad(visit.getTimingMetrics());
|
2497
|
+
}
|
2498
|
+
willSubmitForm(form, submitter) {
|
2499
|
+
return true;
|
2500
|
+
}
|
2501
|
+
formSubmitted(form, submitter) {
|
2502
|
+
this.navigator.submitForm(form, submitter);
|
2503
|
+
}
|
2504
|
+
pageBecameInteractive() {
|
2505
|
+
this.view.lastRenderedLocation = this.location;
|
2506
|
+
this.notifyApplicationAfterPageLoad();
|
2507
|
+
}
|
2508
|
+
pageLoaded() {}
|
2509
|
+
pageInvalidated() {
|
2510
|
+
this.adapter.pageInvalidated();
|
2511
|
+
}
|
2512
|
+
receivedMessageFromStream(message) {
|
2513
|
+
this.renderStreamMessage(message);
|
2514
|
+
}
|
2515
|
+
viewWillRender(newBody) {
|
2516
|
+
this.notifyApplicationBeforeRender(newBody);
|
2517
|
+
}
|
2518
|
+
viewRendered() {
|
2519
|
+
this.view.lastRenderedLocation = this.history.location;
|
2520
|
+
this.notifyApplicationAfterRender();
|
2521
|
+
}
|
2522
|
+
viewInvalidated() {
|
2523
|
+
this.pageObserver.invalidate();
|
2524
|
+
}
|
2525
|
+
viewWillCacheSnapshot() {
|
2526
|
+
this.notifyApplicationBeforeCachingSnapshot();
|
2527
|
+
}
|
2528
|
+
applicationAllowsFollowingLinkToLocation(link, location) {
|
2529
|
+
const event = this.notifyApplicationAfterClickingLinkToLocation(link, location);
|
2530
|
+
return !event.defaultPrevented;
|
2531
|
+
}
|
2532
|
+
applicationAllowsVisitingLocation(location) {
|
2533
|
+
const event = this.notifyApplicationBeforeVisitingLocation(location);
|
2534
|
+
return !event.defaultPrevented;
|
2535
|
+
}
|
2536
|
+
notifyApplicationAfterClickingLinkToLocation(link, location) {
|
2537
|
+
return dispatch("turbo:click", {
|
2538
|
+
target: link,
|
2539
|
+
detail: {
|
2540
|
+
url: location.absoluteURL
|
2541
|
+
},
|
2542
|
+
cancelable: true
|
2543
|
+
});
|
2544
|
+
}
|
2545
|
+
notifyApplicationBeforeVisitingLocation(location) {
|
2546
|
+
return dispatch("turbo:before-visit", {
|
2547
|
+
detail: {
|
2548
|
+
url: location.absoluteURL
|
2549
|
+
},
|
2550
|
+
cancelable: true
|
2551
|
+
});
|
2552
|
+
}
|
2553
|
+
notifyApplicationAfterVisitingLocation(location) {
|
2554
|
+
return dispatch("turbo:visit", {
|
2555
|
+
detail: {
|
2556
|
+
url: location.absoluteURL
|
2557
|
+
}
|
2558
|
+
});
|
2559
|
+
}
|
2560
|
+
notifyApplicationBeforeCachingSnapshot() {
|
2561
|
+
return dispatch("turbo:before-cache");
|
2562
|
+
}
|
2563
|
+
notifyApplicationBeforeRender(newBody) {
|
2564
|
+
return dispatch("turbo:before-render", {
|
2565
|
+
detail: {
|
2566
|
+
newBody: newBody
|
2567
|
+
}
|
2568
|
+
});
|
2569
|
+
}
|
2570
|
+
notifyApplicationAfterRender() {
|
2571
|
+
return dispatch("turbo:render");
|
2572
|
+
}
|
2573
|
+
notifyApplicationAfterPageLoad(timing = {}) {
|
2574
|
+
return dispatch("turbo:load", {
|
2575
|
+
detail: {
|
2576
|
+
url: this.location.absoluteURL,
|
2577
|
+
timing: timing
|
2578
|
+
}
|
2579
|
+
});
|
2580
|
+
}
|
2581
|
+
getActionForLink(link) {
|
2582
|
+
const action = link.getAttribute("data-turbo-action");
|
2583
|
+
return isAction(action) ? action : "advance";
|
2584
|
+
}
|
2585
|
+
linkIsVisitable(link) {
|
2586
|
+
const container = link.closest("[data-turbo]");
|
2587
|
+
if (container) {
|
2588
|
+
return container.getAttribute("data-turbo") != "false";
|
2589
|
+
} else {
|
2590
|
+
return true;
|
2591
|
+
}
|
2592
|
+
}
|
2593
|
+
locationIsVisitable(location) {
|
2594
|
+
return location.isPrefixedBy(this.view.getRootLocation()) && location.isHTML();
|
2595
|
+
}
|
2596
|
+
}
|
2597
|
+
|
2598
|
+
const session = new Session;
|
2599
|
+
|
2600
|
+
const {navigator: navigator} = session;
|
2601
|
+
|
2602
|
+
function start() {
|
2603
|
+
session.start();
|
2604
|
+
}
|
2605
|
+
|
2606
|
+
function registerAdapter(adapter) {
|
2607
|
+
session.registerAdapter(adapter);
|
2608
|
+
}
|
2609
|
+
|
2610
|
+
function visit(location, options) {
|
2611
|
+
session.visit(location, options);
|
2612
|
+
}
|
2613
|
+
|
2614
|
+
function connectStreamSource(source) {
|
2615
|
+
session.connectStreamSource(source);
|
2616
|
+
}
|
2617
|
+
|
2618
|
+
function disconnectStreamSource(source) {
|
2619
|
+
session.disconnectStreamSource(source);
|
2620
|
+
}
|
2621
|
+
|
2622
|
+
function renderStreamMessage(message) {
|
2623
|
+
session.renderStreamMessage(message);
|
2624
|
+
}
|
2625
|
+
|
2626
|
+
function clearCache() {
|
2627
|
+
session.clearCache();
|
2628
|
+
}
|
2629
|
+
|
2630
|
+
function setProgressBarDelay(delay) {
|
2631
|
+
session.setProgressBarDelay(delay);
|
2632
|
+
}
|
2633
|
+
|
2634
|
+
start();
|
2635
|
+
|
2636
|
+
var turbo_es2017Esm = Object.freeze({
|
2637
|
+
__proto__: null,
|
2638
|
+
clearCache: clearCache,
|
2639
|
+
connectStreamSource: connectStreamSource,
|
2640
|
+
disconnectStreamSource: disconnectStreamSource,
|
2641
|
+
navigator: navigator,
|
2642
|
+
registerAdapter: registerAdapter,
|
2643
|
+
renderStreamMessage: renderStreamMessage,
|
2644
|
+
setProgressBarDelay: setProgressBarDelay,
|
2645
|
+
start: start,
|
2646
|
+
visit: visit
|
2647
|
+
});
|
2648
|
+
|
2649
|
+
let consumer;
|
2650
|
+
|
2651
|
+
async function getConsumer() {
|
2652
|
+
if (consumer) return consumer;
|
2653
|
+
const {createConsumer: createConsumer} = await Promise.resolve().then((function() {
|
2654
|
+
return index;
|
2655
|
+
}));
|
2656
|
+
return setConsumer(createConsumer());
|
2657
|
+
}
|
2658
|
+
|
2659
|
+
function setConsumer(newConsumer) {
|
2660
|
+
return consumer = newConsumer;
|
2661
|
+
}
|
2662
|
+
|
2663
|
+
async function subscribeTo(channel, mixin) {
|
2664
|
+
const {subscriptions: subscriptions} = await getConsumer();
|
2665
|
+
return subscriptions.create(channel, mixin);
|
2666
|
+
}
|
2667
|
+
|
2668
|
+
var cable = Object.freeze({
|
2669
|
+
__proto__: null,
|
2670
|
+
getConsumer: getConsumer,
|
2671
|
+
setConsumer: setConsumer,
|
2672
|
+
subscribeTo: subscribeTo
|
2673
|
+
});
|
2674
|
+
|
2675
|
+
class TurboCableStreamSourceElement extends HTMLElement {
|
2676
|
+
async connectedCallback() {
|
2677
|
+
connectStreamSource(this);
|
2678
|
+
this.subscription = subscribeTo(this.channel, {
|
2679
|
+
received: this.dispatchMessageEvent.bind(this)
|
2680
|
+
});
|
2681
|
+
}
|
2682
|
+
disconnectedCallback() {
|
2683
|
+
disconnectStreamSource(this);
|
2684
|
+
if (this.subscription) this.subscription.unsubscribe();
|
2685
|
+
}
|
2686
|
+
dispatchMessageEvent(data) {
|
2687
|
+
const event = new MessageEvent("message", {
|
2688
|
+
data: data
|
2689
|
+
});
|
2690
|
+
return this.dispatchEvent(event);
|
2691
|
+
}
|
2692
|
+
get channel() {
|
2693
|
+
const channel = this.getAttribute("channel");
|
2694
|
+
const signed_stream_name = this.getAttribute("signed-stream-name");
|
2695
|
+
return {
|
2696
|
+
channel: channel,
|
2697
|
+
signed_stream_name: signed_stream_name
|
2698
|
+
};
|
2699
|
+
}
|
2700
|
+
}
|
2701
|
+
|
2702
|
+
customElements.define("turbo-cable-stream-source", TurboCableStreamSourceElement);
|
2703
|
+
|
2704
|
+
var adapters = {
|
2705
|
+
logger: self.console,
|
2706
|
+
WebSocket: self.WebSocket
|
2707
|
+
};
|
2708
|
+
|
2709
|
+
var logger = {
|
2710
|
+
log(...messages) {
|
2711
|
+
if (this.enabled) {
|
2712
|
+
messages.push(Date.now());
|
2713
|
+
adapters.logger.log("[ActionCable]", ...messages);
|
2714
|
+
}
|
2715
|
+
}
|
2716
|
+
};
|
2717
|
+
|
2718
|
+
const now = () => (new Date).getTime();
|
2719
|
+
|
2720
|
+
const secondsSince = time => (now() - time) / 1e3;
|
2721
|
+
|
2722
|
+
const clamp = (number, min, max) => Math.max(min, Math.min(max, number));
|
2723
|
+
|
2724
|
+
class ConnectionMonitor {
|
2725
|
+
constructor(connection) {
|
2726
|
+
this.visibilityDidChange = this.visibilityDidChange.bind(this);
|
2727
|
+
this.connection = connection;
|
2728
|
+
this.reconnectAttempts = 0;
|
2729
|
+
}
|
2730
|
+
start() {
|
2731
|
+
if (!this.isRunning()) {
|
2732
|
+
this.startedAt = now();
|
2733
|
+
delete this.stoppedAt;
|
2734
|
+
this.startPolling();
|
2735
|
+
addEventListener("visibilitychange", this.visibilityDidChange);
|
2736
|
+
logger.log(`ConnectionMonitor started. pollInterval = ${this.getPollInterval()} ms`);
|
2737
|
+
}
|
2738
|
+
}
|
2739
|
+
stop() {
|
2740
|
+
if (this.isRunning()) {
|
2741
|
+
this.stoppedAt = now();
|
2742
|
+
this.stopPolling();
|
2743
|
+
removeEventListener("visibilitychange", this.visibilityDidChange);
|
2744
|
+
logger.log("ConnectionMonitor stopped");
|
2745
|
+
}
|
2746
|
+
}
|
2747
|
+
isRunning() {
|
2748
|
+
return this.startedAt && !this.stoppedAt;
|
2749
|
+
}
|
2750
|
+
recordPing() {
|
2751
|
+
this.pingedAt = now();
|
2752
|
+
}
|
2753
|
+
recordConnect() {
|
2754
|
+
this.reconnectAttempts = 0;
|
2755
|
+
this.recordPing();
|
2756
|
+
delete this.disconnectedAt;
|
2757
|
+
logger.log("ConnectionMonitor recorded connect");
|
2758
|
+
}
|
2759
|
+
recordDisconnect() {
|
2760
|
+
this.disconnectedAt = now();
|
2761
|
+
logger.log("ConnectionMonitor recorded disconnect");
|
2762
|
+
}
|
2763
|
+
startPolling() {
|
2764
|
+
this.stopPolling();
|
2765
|
+
this.poll();
|
2766
|
+
}
|
2767
|
+
stopPolling() {
|
2768
|
+
clearTimeout(this.pollTimeout);
|
2769
|
+
}
|
2770
|
+
poll() {
|
2771
|
+
this.pollTimeout = setTimeout((() => {
|
2772
|
+
this.reconnectIfStale();
|
2773
|
+
this.poll();
|
2774
|
+
}), this.getPollInterval());
|
2775
|
+
}
|
2776
|
+
getPollInterval() {
|
2777
|
+
const {min: min, max: max, multiplier: multiplier} = this.constructor.pollInterval;
|
2778
|
+
const interval = multiplier * Math.log(this.reconnectAttempts + 1);
|
2779
|
+
return Math.round(clamp(interval, min, max) * 1e3);
|
2780
|
+
}
|
2781
|
+
reconnectIfStale() {
|
2782
|
+
if (this.connectionIsStale()) {
|
2783
|
+
logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, pollInterval = ${this.getPollInterval()} ms, time disconnected = ${secondsSince(this.disconnectedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`);
|
2784
|
+
this.reconnectAttempts++;
|
2785
|
+
if (this.disconnectedRecently()) {
|
2786
|
+
logger.log("ConnectionMonitor skipping reopening recent disconnect");
|
2787
|
+
} else {
|
2788
|
+
logger.log("ConnectionMonitor reopening");
|
2789
|
+
this.connection.reopen();
|
2790
|
+
}
|
2791
|
+
}
|
2792
|
+
}
|
2793
|
+
connectionIsStale() {
|
2794
|
+
return secondsSince(this.pingedAt ? this.pingedAt : this.startedAt) > this.constructor.staleThreshold;
|
2795
|
+
}
|
2796
|
+
disconnectedRecently() {
|
2797
|
+
return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold;
|
2798
|
+
}
|
2799
|
+
visibilityDidChange() {
|
2800
|
+
if (document.visibilityState === "visible") {
|
2801
|
+
setTimeout((() => {
|
2802
|
+
if (this.connectionIsStale() || !this.connection.isOpen()) {
|
2803
|
+
logger.log(`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`);
|
2804
|
+
this.connection.reopen();
|
2805
|
+
}
|
2806
|
+
}), 200);
|
2807
|
+
}
|
2808
|
+
}
|
2809
|
+
}
|
2810
|
+
|
2811
|
+
ConnectionMonitor.pollInterval = {
|
2812
|
+
min: 3,
|
2813
|
+
max: 30,
|
2814
|
+
multiplier: 5
|
2815
|
+
};
|
2816
|
+
|
2817
|
+
ConnectionMonitor.staleThreshold = 6;
|
2818
|
+
|
2819
|
+
var INTERNAL = {
|
2820
|
+
message_types: {
|
2821
|
+
welcome: "welcome",
|
2822
|
+
disconnect: "disconnect",
|
2823
|
+
ping: "ping",
|
2824
|
+
confirmation: "confirm_subscription",
|
2825
|
+
rejection: "reject_subscription"
|
2826
|
+
},
|
2827
|
+
disconnect_reasons: {
|
2828
|
+
unauthorized: "unauthorized",
|
2829
|
+
invalid_request: "invalid_request",
|
2830
|
+
server_restart: "server_restart"
|
2831
|
+
},
|
2832
|
+
default_mount_path: "/cable",
|
2833
|
+
protocols: [ "actioncable-v1-json", "actioncable-unsupported" ]
|
2834
|
+
};
|
2835
|
+
|
2836
|
+
const {message_types: message_types, protocols: protocols} = INTERNAL;
|
2837
|
+
|
2838
|
+
const supportedProtocols = protocols.slice(0, protocols.length - 1);
|
2839
|
+
|
2840
|
+
const indexOf = [].indexOf;
|
2841
|
+
|
2842
|
+
class Connection {
|
2843
|
+
constructor(consumer) {
|
2844
|
+
this.open = this.open.bind(this);
|
2845
|
+
this.consumer = consumer;
|
2846
|
+
this.subscriptions = this.consumer.subscriptions;
|
2847
|
+
this.monitor = new ConnectionMonitor(this);
|
2848
|
+
this.disconnected = true;
|
2849
|
+
}
|
2850
|
+
send(data) {
|
2851
|
+
if (this.isOpen()) {
|
2852
|
+
this.webSocket.send(JSON.stringify(data));
|
2853
|
+
return true;
|
2854
|
+
} else {
|
2855
|
+
return false;
|
2856
|
+
}
|
2857
|
+
}
|
2858
|
+
open() {
|
2859
|
+
if (this.isActive()) {
|
2860
|
+
logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`);
|
2861
|
+
return false;
|
2862
|
+
} else {
|
2863
|
+
logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${protocols}`);
|
2864
|
+
if (this.webSocket) {
|
2865
|
+
this.uninstallEventHandlers();
|
2866
|
+
}
|
2867
|
+
this.webSocket = new adapters.WebSocket(this.consumer.url, protocols);
|
2868
|
+
this.installEventHandlers();
|
2869
|
+
this.monitor.start();
|
2870
|
+
return true;
|
2871
|
+
}
|
2872
|
+
}
|
2873
|
+
close({allowReconnect: allowReconnect} = {
|
2874
|
+
allowReconnect: true
|
2875
|
+
}) {
|
2876
|
+
if (!allowReconnect) {
|
2877
|
+
this.monitor.stop();
|
2878
|
+
}
|
2879
|
+
if (this.isActive()) {
|
2880
|
+
return this.webSocket.close();
|
2881
|
+
}
|
2882
|
+
}
|
2883
|
+
reopen() {
|
2884
|
+
logger.log(`Reopening WebSocket, current state is ${this.getState()}`);
|
2885
|
+
if (this.isActive()) {
|
2886
|
+
try {
|
2887
|
+
return this.close();
|
2888
|
+
} catch (error) {
|
2889
|
+
logger.log("Failed to reopen WebSocket", error);
|
2890
|
+
} finally {
|
2891
|
+
logger.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`);
|
2892
|
+
setTimeout(this.open, this.constructor.reopenDelay);
|
2893
|
+
}
|
2894
|
+
} else {
|
2895
|
+
return this.open();
|
2896
|
+
}
|
2897
|
+
}
|
2898
|
+
getProtocol() {
|
2899
|
+
if (this.webSocket) {
|
2900
|
+
return this.webSocket.protocol;
|
2901
|
+
}
|
2902
|
+
}
|
2903
|
+
isOpen() {
|
2904
|
+
return this.isState("open");
|
2905
|
+
}
|
2906
|
+
isActive() {
|
2907
|
+
return this.isState("open", "connecting");
|
2908
|
+
}
|
2909
|
+
isProtocolSupported() {
|
2910
|
+
return indexOf.call(supportedProtocols, this.getProtocol()) >= 0;
|
2911
|
+
}
|
2912
|
+
isState(...states) {
|
2913
|
+
return indexOf.call(states, this.getState()) >= 0;
|
2914
|
+
}
|
2915
|
+
getState() {
|
2916
|
+
if (this.webSocket) {
|
2917
|
+
for (let state in adapters.WebSocket) {
|
2918
|
+
if (adapters.WebSocket[state] === this.webSocket.readyState) {
|
2919
|
+
return state.toLowerCase();
|
2920
|
+
}
|
2921
|
+
}
|
2922
|
+
}
|
2923
|
+
return null;
|
2924
|
+
}
|
2925
|
+
installEventHandlers() {
|
2926
|
+
for (let eventName in this.events) {
|
2927
|
+
const handler = this.events[eventName].bind(this);
|
2928
|
+
this.webSocket[`on${eventName}`] = handler;
|
2929
|
+
}
|
2930
|
+
}
|
2931
|
+
uninstallEventHandlers() {
|
2932
|
+
for (let eventName in this.events) {
|
2933
|
+
this.webSocket[`on${eventName}`] = function() {};
|
2934
|
+
}
|
2935
|
+
}
|
2936
|
+
}
|
2937
|
+
|
2938
|
+
Connection.reopenDelay = 500;
|
2939
|
+
|
2940
|
+
Connection.prototype.events = {
|
2941
|
+
message(event) {
|
2942
|
+
if (!this.isProtocolSupported()) {
|
2943
|
+
return;
|
2944
|
+
}
|
2945
|
+
const {identifier: identifier, message: message, reason: reason, reconnect: reconnect, type: type} = JSON.parse(event.data);
|
2946
|
+
switch (type) {
|
2947
|
+
case message_types.welcome:
|
2948
|
+
this.monitor.recordConnect();
|
2949
|
+
return this.subscriptions.reload();
|
2950
|
+
|
2951
|
+
case message_types.disconnect:
|
2952
|
+
logger.log(`Disconnecting. Reason: ${reason}`);
|
2953
|
+
return this.close({
|
2954
|
+
allowReconnect: reconnect
|
2955
|
+
});
|
2956
|
+
|
2957
|
+
case message_types.ping:
|
2958
|
+
return this.monitor.recordPing();
|
2959
|
+
|
2960
|
+
case message_types.confirmation:
|
2961
|
+
return this.subscriptions.notify(identifier, "connected");
|
2962
|
+
|
2963
|
+
case message_types.rejection:
|
2964
|
+
return this.subscriptions.reject(identifier);
|
2965
|
+
|
2966
|
+
default:
|
2967
|
+
return this.subscriptions.notify(identifier, "received", message);
|
2968
|
+
}
|
2969
|
+
},
|
2970
|
+
open() {
|
2971
|
+
logger.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`);
|
2972
|
+
this.disconnected = false;
|
2973
|
+
if (!this.isProtocolSupported()) {
|
2974
|
+
logger.log("Protocol is unsupported. Stopping monitor and disconnecting.");
|
2975
|
+
return this.close({
|
2976
|
+
allowReconnect: false
|
2977
|
+
});
|
2978
|
+
}
|
2979
|
+
},
|
2980
|
+
close(event) {
|
2981
|
+
logger.log("WebSocket onclose event");
|
2982
|
+
if (this.disconnected) {
|
2983
|
+
return;
|
2984
|
+
}
|
2985
|
+
this.disconnected = true;
|
2986
|
+
this.monitor.recordDisconnect();
|
2987
|
+
return this.subscriptions.notifyAll("disconnected", {
|
2988
|
+
willAttemptReconnect: this.monitor.isRunning()
|
2989
|
+
});
|
2990
|
+
},
|
2991
|
+
error() {
|
2992
|
+
logger.log("WebSocket onerror event");
|
2993
|
+
}
|
2994
|
+
};
|
2995
|
+
|
2996
|
+
const extend = function(object, properties) {
|
2997
|
+
if (properties != null) {
|
2998
|
+
for (let key in properties) {
|
2999
|
+
const value = properties[key];
|
3000
|
+
object[key] = value;
|
3001
|
+
}
|
3002
|
+
}
|
3003
|
+
return object;
|
3004
|
+
};
|
3005
|
+
|
3006
|
+
class Subscription {
|
3007
|
+
constructor(consumer, params = {}, mixin) {
|
3008
|
+
this.consumer = consumer;
|
3009
|
+
this.identifier = JSON.stringify(params);
|
3010
|
+
extend(this, mixin);
|
3011
|
+
}
|
3012
|
+
perform(action, data = {}) {
|
3013
|
+
data.action = action;
|
3014
|
+
return this.send(data);
|
3015
|
+
}
|
3016
|
+
send(data) {
|
3017
|
+
return this.consumer.send({
|
3018
|
+
command: "message",
|
3019
|
+
identifier: this.identifier,
|
3020
|
+
data: JSON.stringify(data)
|
3021
|
+
});
|
3022
|
+
}
|
3023
|
+
unsubscribe() {
|
3024
|
+
return this.consumer.subscriptions.remove(this);
|
3025
|
+
}
|
3026
|
+
}
|
3027
|
+
|
3028
|
+
class Subscriptions {
|
3029
|
+
constructor(consumer) {
|
3030
|
+
this.consumer = consumer;
|
3031
|
+
this.subscriptions = [];
|
3032
|
+
}
|
3033
|
+
create(channelName, mixin) {
|
3034
|
+
const channel = channelName;
|
3035
|
+
const params = typeof channel === "object" ? channel : {
|
3036
|
+
channel: channel
|
3037
|
+
};
|
3038
|
+
const subscription = new Subscription(this.consumer, params, mixin);
|
3039
|
+
return this.add(subscription);
|
3040
|
+
}
|
3041
|
+
add(subscription) {
|
3042
|
+
this.subscriptions.push(subscription);
|
3043
|
+
this.consumer.ensureActiveConnection();
|
3044
|
+
this.notify(subscription, "initialized");
|
3045
|
+
this.sendCommand(subscription, "subscribe");
|
3046
|
+
return subscription;
|
3047
|
+
}
|
3048
|
+
remove(subscription) {
|
3049
|
+
this.forget(subscription);
|
3050
|
+
if (!this.findAll(subscription.identifier).length) {
|
3051
|
+
this.sendCommand(subscription, "unsubscribe");
|
3052
|
+
}
|
3053
|
+
return subscription;
|
3054
|
+
}
|
3055
|
+
reject(identifier) {
|
3056
|
+
return this.findAll(identifier).map((subscription => {
|
3057
|
+
this.forget(subscription);
|
3058
|
+
this.notify(subscription, "rejected");
|
3059
|
+
return subscription;
|
3060
|
+
}));
|
3061
|
+
}
|
3062
|
+
forget(subscription) {
|
3063
|
+
this.subscriptions = this.subscriptions.filter((s => s !== subscription));
|
3064
|
+
return subscription;
|
3065
|
+
}
|
3066
|
+
findAll(identifier) {
|
3067
|
+
return this.subscriptions.filter((s => s.identifier === identifier));
|
3068
|
+
}
|
3069
|
+
reload() {
|
3070
|
+
return this.subscriptions.map((subscription => this.sendCommand(subscription, "subscribe")));
|
3071
|
+
}
|
3072
|
+
notifyAll(callbackName, ...args) {
|
3073
|
+
return this.subscriptions.map((subscription => this.notify(subscription, callbackName, ...args)));
|
3074
|
+
}
|
3075
|
+
notify(subscription, callbackName, ...args) {
|
3076
|
+
let subscriptions;
|
3077
|
+
if (typeof subscription === "string") {
|
3078
|
+
subscriptions = this.findAll(subscription);
|
3079
|
+
} else {
|
3080
|
+
subscriptions = [ subscription ];
|
3081
|
+
}
|
3082
|
+
return subscriptions.map((subscription => typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined));
|
3083
|
+
}
|
3084
|
+
sendCommand(subscription, command) {
|
3085
|
+
const {identifier: identifier} = subscription;
|
3086
|
+
return this.consumer.send({
|
3087
|
+
command: command,
|
3088
|
+
identifier: identifier
|
3089
|
+
});
|
3090
|
+
}
|
3091
|
+
}
|
3092
|
+
|
3093
|
+
class Consumer {
|
3094
|
+
constructor(url) {
|
3095
|
+
this._url = url;
|
3096
|
+
this.subscriptions = new Subscriptions(this);
|
3097
|
+
this.connection = new Connection(this);
|
3098
|
+
}
|
3099
|
+
get url() {
|
3100
|
+
return createWebSocketURL(this._url);
|
3101
|
+
}
|
3102
|
+
send(data) {
|
3103
|
+
return this.connection.send(data);
|
3104
|
+
}
|
3105
|
+
connect() {
|
3106
|
+
return this.connection.open();
|
3107
|
+
}
|
3108
|
+
disconnect() {
|
3109
|
+
return this.connection.close({
|
3110
|
+
allowReconnect: false
|
3111
|
+
});
|
3112
|
+
}
|
3113
|
+
ensureActiveConnection() {
|
3114
|
+
if (!this.connection.isActive()) {
|
3115
|
+
return this.connection.open();
|
3116
|
+
}
|
3117
|
+
}
|
3118
|
+
}
|
3119
|
+
|
3120
|
+
function createWebSocketURL(url) {
|
3121
|
+
if (typeof url === "function") {
|
3122
|
+
url = url();
|
3123
|
+
}
|
3124
|
+
if (url && !/^wss?:/i.test(url)) {
|
3125
|
+
const a = document.createElement("a");
|
3126
|
+
a.href = url;
|
3127
|
+
a.href = a.href;
|
3128
|
+
a.protocol = a.protocol.replace("http", "ws");
|
3129
|
+
return a.href;
|
3130
|
+
} else {
|
3131
|
+
return url;
|
3132
|
+
}
|
3133
|
+
}
|
3134
|
+
|
3135
|
+
function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) {
|
3136
|
+
return new Consumer(url);
|
3137
|
+
}
|
3138
|
+
|
3139
|
+
function getConfig(name) {
|
3140
|
+
const element = document.head.querySelector(`meta[name='action-cable-${name}']`);
|
3141
|
+
if (element) {
|
3142
|
+
return element.getAttribute("content");
|
3143
|
+
}
|
3144
|
+
}
|
3145
|
+
|
3146
|
+
var index = Object.freeze({
|
3147
|
+
__proto__: null,
|
3148
|
+
Connection: Connection,
|
3149
|
+
ConnectionMonitor: ConnectionMonitor,
|
3150
|
+
Consumer: Consumer,
|
3151
|
+
INTERNAL: INTERNAL,
|
3152
|
+
Subscription: Subscription,
|
3153
|
+
Subscriptions: Subscriptions,
|
3154
|
+
adapters: adapters,
|
3155
|
+
createWebSocketURL: createWebSocketURL,
|
3156
|
+
logger: logger,
|
3157
|
+
createConsumer: createConsumer,
|
3158
|
+
getConfig: getConfig
|
3159
|
+
});
|
3160
|
+
|
3161
|
+
export { turbo_es2017Esm as Turbo, cable };
|