turbo-rails 0.5.0 → 0.5.5

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