turbo-rails 2.0.5 → 2.0.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 49c33a9c7eaf917754b0deb21c776b9db3a457e09453bfb4da400ce2ea676b6f
4
- data.tar.gz: a6823b945a543045681165fa5f429cc3a7fe0928b6ae274505b61a20dee90662
3
+ metadata.gz: 7bc53746ef39d3412872a00dade91b735622b4edca9b42d321c4f2114da6dd4b
4
+ data.tar.gz: 53974cb2408b7a98cf7cf6a33b9fc8c9ebf948c1e196597f33f499437ea47a47
5
5
  SHA512:
6
- metadata.gz: 93ae8bb2c473aa705eb4b46ba0fe35fc61aa71513993755f2d33fbb6fcdec96881f19b9ae020e46d0dab98aaa8b771ba0dff26c2317370126404d7883d81a51e
7
- data.tar.gz: 303817c62ed4ce953084dae5e11025501eddab78398783d4266394b81fb4f5622486160abf30959633cea66a5b5a3aaf9c94cce82da2d0e5148cd98d60e32512
6
+ metadata.gz: 16fdd0089b68c5f6ed5777324af45fb54a2101439319b70637d60eebaa29965e8f9b3ae9dc2b825505d6b9155a2e74b319c47231f80300aac622b32ed2d0c908
7
+ data.tar.gz: 8d8753af6214007c64250931c9bbba39b341a91decb60a71b6d5a1bf62a08803076127e81a9db3d87ca209a2753558f688cc2319df78a7def4c9a7ca878c0e55
data/README.md CHANGED
@@ -56,7 +56,7 @@ When the user clicks on the `Edit this todo` link, as a direct response to this
56
56
 
57
57
  ### A note on custom layouts
58
58
 
59
- In order to render turbo frame requests without the application layout, Turbo registers a custom [layout method](https://api.rubyonrails.org/classes/ActionView/Layouts/ClassMethods.html#method-i-layout).
59
+ In order to render turbo frame requests without the application layout, Turbo registers a custom [layout method](https://api.rubyonrails.org/classes/ActionView/Layouts/ClassMethods.html#method-i-layout).
60
60
  If your application uses custom layout resolution, you have to make sure to return `"turbo_rails/frame"` (or `false` for TurboRails < 1.4.0) for turbo frame requests:
61
61
 
62
62
  ```ruby
@@ -64,7 +64,7 @@ layout :custom_layout
64
64
 
65
65
  def custom_layout
66
66
  return "turbo_rails/frame" if turbo_frame_request?
67
-
67
+
68
68
  # ... your custom layout logic
69
69
  ```
70
70
 
@@ -74,14 +74,14 @@ If you are using a custom, but "static" layout,
74
74
  layout "some_static_layout"
75
75
  ```
76
76
 
77
- you **have** to change it to a layout method in order to conditionally return `false` for turbo frame requests:
77
+ you **have** to change it to a layout method in order to conditionally return `"turbo_rails/frame"` for turbo frame requests:
78
78
 
79
79
  ```ruby
80
80
  layout :custom_layout
81
81
 
82
82
  def custom_layout
83
83
  return "turbo_rails/frame" if turbo_frame_request?
84
-
84
+
85
85
  "some_static_layout"
86
86
  ```
87
87
 
@@ -111,7 +111,7 @@ This gem is automatically configured for applications made with Rails 7+ (unless
111
111
  3. Run `./bin/rails turbo:install`
112
112
  4. Run `./bin/rails turbo:install:redis` to change the development Action Cable adapter from Async (the default one) to Redis. The Async adapter does not support Turbo Stream broadcasting.
113
113
 
114
- Running `turbo:install` will install through NPM if Node.js is used in the application. Otherwise the asset pipeline version is used. To use the asset pipeline version, you must have `importmap-rails` installed first and listed higher in the Gemfile.
114
+ Running `turbo:install` will install through NPM or Bun if a JavaScript runtime is used in the application. Otherwise the asset pipeline version is used. To use the asset pipeline version, you must have `importmap-rails` installed first and listed higher in the Gemfile.
115
115
 
116
116
  If you're using node and need to use the cable consumer, you can import [`cable`](https://github.com/hotwired/turbo-rails/blob/main/app/javascript/turbo/cable.js) (`import { cable } from "@hotwired/turbo-rails"`), but ensure that your application actually *uses* the members it `import`s when using this style (see [turbo-rails#48](https://github.com/hotwired/turbo-rails/issues/48)).
117
117
 
@@ -152,10 +152,48 @@ The [`Turbo::TestAssertions::IntegrationTestAssertions`](./lib/turbo/test_assert
152
152
 
153
153
  The [`Turbo::Broadcastable::TestHelper`](./lib/turbo/broadcastable/test_helper.rb) concern provides Action Cable-aware test helpers that assert that `<turbo-stream>` elements were or were not broadcast over Action Cable. `Turbo::Broadcastable::TestHelper` is automatically included in [`ActiveSupport::TestCase`](https://edgeapi.rubyonrails.org/classes/ActiveSupport/TestCase.html).
154
154
 
155
+ ### Rendering Outside of a Request
156
+
157
+ Turbo utilizes [ActionController::Renderer][] to render templates and partials
158
+ outside the context of the request-response cycle. If you need to render a
159
+ Turbo-aware template, partial, or component, use [ActionController::Renderer][]:
160
+
161
+ ```ruby
162
+ ApplicationController.renderer.render template: "posts/show", assigns: { post: Post.first } # => "<html>…"
163
+ PostsController.renderer.render :show, assigns: { post: Post.first } # => "<html>…"
164
+ ```
165
+
166
+ As a shortcut, you can also call render directly on the controller class itself:
167
+
168
+ ```ruby
169
+ ApplicationController.render template: "posts/show", assigns: { post: Post.first } # => "<html>…"
170
+ PostsController.render :show, assigns: { post: Post.first } # => "<html>…"
171
+ ```
172
+
173
+ [ActionController::Renderer]: https://api.rubyonrails.org/classes/ActionController/Renderer.html
174
+
155
175
  ## Development
156
176
 
157
177
  Run the tests with `./bin/test`.
158
178
 
179
+ ### Using local Turbo version
180
+
181
+ Often you might want to test changes made locally to [Turbo lib](https://github.com/hotwired/turbo) itself. To package your local development version of Turbo you can use [yarn link](https://classic.yarnpkg.com/lang/en/docs/cli/link/) feature:
182
+
183
+ ```sh
184
+ cd <local-turbo-dir>
185
+ yarn link
186
+
187
+ cd <local-turbo-rails-dir>
188
+ yarn link @hotwired/turbo
189
+
190
+ # Build the JS distribution files...
191
+ yarn build
192
+ # ...and commit the changes
193
+ ```
194
+
195
+ Now you can reference your version of turbo-rails in your Rails projects packaged with your local version of Turbo.
196
+
159
197
  ## License
160
198
 
161
199
  Turbo is released under the [MIT License](https://opensource.org/licenses/MIT).
@@ -1,5 +1,5 @@
1
1
  /*!
2
- Turbo 8.0.4
2
+ Turbo 8.0.5
3
3
  Copyright © 2024 37signals LLC
4
4
  */
5
5
  (function(prototype) {
@@ -483,13 +483,17 @@ async function around(callback, reader) {
483
483
  return [ before, after ];
484
484
  }
485
485
 
486
- function doesNotTargetIFrame(anchor) {
487
- if (anchor.hasAttribute("target")) {
488
- for (const element of document.getElementsByName(anchor.target)) {
486
+ function doesNotTargetIFrame(name) {
487
+ if (name === "_blank") {
488
+ return false;
489
+ } else if (name) {
490
+ for (const element of document.getElementsByName(name)) {
489
491
  if (element instanceof HTMLIFrameElement) return false;
490
492
  }
493
+ return true;
494
+ } else {
495
+ return true;
491
496
  }
492
- return true;
493
497
  }
494
498
 
495
499
  function findLinkFromClickTarget(target) {
@@ -596,7 +600,7 @@ class FetchRequest {
596
600
  this.fetchOptions = {
597
601
  credentials: "same-origin",
598
602
  redirect: "follow",
599
- method: method,
603
+ method: method.toUpperCase(),
600
604
  headers: {
601
605
  ...this.defaultHeaders
602
606
  },
@@ -616,7 +620,7 @@ class FetchRequest {
616
620
  const [url, body] = buildResourceAndBody(this.url, fetchMethod, fetchBody, this.enctype);
617
621
  this.url = url;
618
622
  this.fetchOptions.body = body;
619
- this.fetchOptions.method = fetchMethod;
623
+ this.fetchOptions.method = fetchMethod.toUpperCase();
620
624
  }
621
625
  get headers() {
622
626
  return this.fetchOptions.headers;
@@ -1166,15 +1170,8 @@ function submissionDoesNotDismissDialog(form, submitter) {
1166
1170
  }
1167
1171
 
1168
1172
  function submissionDoesNotTargetIFrame(form, submitter) {
1169
- if (submitter?.hasAttribute("formtarget") || form.hasAttribute("target")) {
1170
- const target = submitter?.getAttribute("formtarget") || form.target;
1171
- for (const element of document.getElementsByName(target)) {
1172
- if (element instanceof HTMLIFrameElement) return false;
1173
- }
1174
- return true;
1175
- } else {
1176
- return true;
1177
- }
1173
+ const target = submitter?.getAttribute("formtarget") || form.getAttribute("target");
1174
+ return doesNotTargetIFrame(target);
1178
1175
  }
1179
1176
 
1180
1177
  class View {
@@ -1307,14 +1304,14 @@ class LinkInterceptor {
1307
1304
  document.removeEventListener("turbo:before-visit", this.willVisit);
1308
1305
  }
1309
1306
  clickBubbled=event => {
1310
- if (this.respondsToEventTarget(event.target)) {
1307
+ if (this.clickEventIsSignificant(event)) {
1311
1308
  this.clickEvent = event;
1312
1309
  } else {
1313
1310
  delete this.clickEvent;
1314
1311
  }
1315
1312
  };
1316
1313
  linkClicked=event => {
1317
- if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) {
1314
+ if (this.clickEvent && this.clickEventIsSignificant(event)) {
1318
1315
  if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url, event.detail.originalEvent)) {
1319
1316
  this.clickEvent.preventDefault();
1320
1317
  event.preventDefault();
@@ -1326,9 +1323,10 @@ class LinkInterceptor {
1326
1323
  willVisit=_event => {
1327
1324
  delete this.clickEvent;
1328
1325
  };
1329
- respondsToEventTarget(target) {
1330
- const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
1331
- return element && element.closest("turbo-frame, html") == this.element;
1326
+ clickEventIsSignificant(event) {
1327
+ const target = event.composed ? event.target?.parentElement : event.target;
1328
+ const element = findLinkFromClickTarget(target) || target;
1329
+ return element instanceof Element && element.closest("turbo-frame, html") == this.element;
1332
1330
  }
1333
1331
  }
1334
1332
 
@@ -1358,7 +1356,7 @@ class LinkClickObserver {
1358
1356
  if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {
1359
1357
  const target = event.composedPath && event.composedPath()[0] || event.target;
1360
1358
  const link = findLinkFromClickTarget(target);
1361
- if (link && doesNotTargetIFrame(link)) {
1359
+ if (link && doesNotTargetIFrame(link.target)) {
1362
1360
  const location = getLocationForLink(link);
1363
1361
  if (this.delegate.willFollowLinkToLocation(link, location, event)) {
1364
1362
  event.preventDefault();
@@ -1496,6 +1494,9 @@ class Renderer {
1496
1494
  get shouldRender() {
1497
1495
  return true;
1498
1496
  }
1497
+ get shouldAutofocus() {
1498
+ return true;
1499
+ }
1499
1500
  get reloadReason() {
1500
1501
  return;
1501
1502
  }
@@ -1513,9 +1514,11 @@ class Renderer {
1513
1514
  await Bardo.preservingPermanentElements(this, this.permanentElementMap, callback);
1514
1515
  }
1515
1516
  focusFirstAutofocusableElement() {
1516
- const element = this.connectedSnapshot.firstAutofocusableElement;
1517
- if (element) {
1518
- element.focus();
1517
+ if (this.shouldAutofocus) {
1518
+ const element = this.connectedSnapshot.firstAutofocusableElement;
1519
+ if (element) {
1520
+ element.focus();
1521
+ }
1519
1522
  }
1520
1523
  }
1521
1524
  enteringBardo(currentPermanentElement) {
@@ -2629,7 +2632,7 @@ class LinkPrefetchObserver {
2629
2632
  this.#prefetchedLink = null;
2630
2633
  };
2631
2634
  #tryToUsePrefetchedRequest=event => {
2632
- if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "get") {
2635
+ if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "GET") {
2633
2636
  const cached = prefetchCache.get(event.detail.url.toString());
2634
2637
  if (cached) {
2635
2638
  event.detail.fetchRequest = cached;
@@ -2798,6 +2801,7 @@ class Navigator {
2798
2801
  }
2799
2802
  visitCompleted(visit) {
2800
2803
  this.delegate.visitCompleted(visit);
2804
+ delete this.currentVisit;
2801
2805
  }
2802
2806
  locationWithActionIsSamePage(location, action) {
2803
2807
  const anchor = getAnchor(location);
@@ -3628,6 +3632,80 @@ var Idiomorph = function() {
3628
3632
  };
3629
3633
  }();
3630
3634
 
3635
+ function morphElements(currentElement, newElement, {callbacks: callbacks, ...options} = {}) {
3636
+ Idiomorph.morph(currentElement, newElement, {
3637
+ ...options,
3638
+ callbacks: new DefaultIdiomorphCallbacks(callbacks)
3639
+ });
3640
+ }
3641
+
3642
+ function morphChildren(currentElement, newElement) {
3643
+ morphElements(currentElement, newElement.children, {
3644
+ morphStyle: "innerHTML"
3645
+ });
3646
+ }
3647
+
3648
+ class DefaultIdiomorphCallbacks {
3649
+ #beforeNodeMorphed;
3650
+ constructor({beforeNodeMorphed: beforeNodeMorphed} = {}) {
3651
+ this.#beforeNodeMorphed = beforeNodeMorphed || (() => true);
3652
+ }
3653
+ beforeNodeAdded=node => !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id));
3654
+ beforeNodeMorphed=(currentElement, newElement) => {
3655
+ if (currentElement instanceof Element) {
3656
+ if (!currentElement.hasAttribute("data-turbo-permanent") && this.#beforeNodeMorphed(currentElement, newElement)) {
3657
+ const event = dispatch("turbo:before-morph-element", {
3658
+ cancelable: true,
3659
+ target: currentElement,
3660
+ detail: {
3661
+ currentElement: currentElement,
3662
+ newElement: newElement
3663
+ }
3664
+ });
3665
+ return !event.defaultPrevented;
3666
+ } else {
3667
+ return false;
3668
+ }
3669
+ }
3670
+ };
3671
+ beforeAttributeUpdated=(attributeName, target, mutationType) => {
3672
+ const event = dispatch("turbo:before-morph-attribute", {
3673
+ cancelable: true,
3674
+ target: target,
3675
+ detail: {
3676
+ attributeName: attributeName,
3677
+ mutationType: mutationType
3678
+ }
3679
+ });
3680
+ return !event.defaultPrevented;
3681
+ };
3682
+ beforeNodeRemoved=node => this.beforeNodeMorphed(node);
3683
+ afterNodeMorphed=(currentElement, newElement) => {
3684
+ if (currentElement instanceof Element) {
3685
+ dispatch("turbo:morph-element", {
3686
+ target: currentElement,
3687
+ detail: {
3688
+ currentElement: currentElement,
3689
+ newElement: newElement
3690
+ }
3691
+ });
3692
+ }
3693
+ };
3694
+ }
3695
+
3696
+ class MorphingFrameRenderer extends FrameRenderer {
3697
+ static renderElement(currentElement, newElement) {
3698
+ dispatch("turbo:before-frame-morph", {
3699
+ target: currentElement,
3700
+ detail: {
3701
+ currentElement: currentElement,
3702
+ newElement: newElement
3703
+ }
3704
+ });
3705
+ morphChildren(currentElement, newElement);
3706
+ }
3707
+ }
3708
+
3631
3709
  class PageRenderer extends Renderer {
3632
3710
  static renderElement(currentElement, newElement) {
3633
3711
  if (document.body && newElement instanceof HTMLBodyElement) {
@@ -3796,108 +3874,47 @@ class PageRenderer extends Renderer {
3796
3874
  }
3797
3875
  }
3798
3876
 
3799
- class MorphRenderer extends PageRenderer {
3800
- async render() {
3801
- if (this.willRender) await this.#morphBody();
3802
- }
3803
- get renderMethod() {
3804
- return "morph";
3805
- }
3806
- async #morphBody() {
3807
- this.#morphElements(this.currentElement, this.newElement);
3808
- this.#reloadRemoteFrames();
3809
- dispatch("turbo:morph", {
3810
- detail: {
3811
- currentElement: this.currentElement,
3812
- newElement: this.newElement
3813
- }
3814
- });
3815
- }
3816
- #morphElements(currentElement, newElement, morphStyle = "outerHTML") {
3817
- this.isMorphingTurboFrame = this.#isFrameReloadedWithMorph(currentElement);
3818
- Idiomorph.morph(currentElement, newElement, {
3819
- morphStyle: morphStyle,
3877
+ class MorphingPageRenderer extends PageRenderer {
3878
+ static renderElement(currentElement, newElement) {
3879
+ morphElements(currentElement, newElement, {
3820
3880
  callbacks: {
3821
- beforeNodeAdded: this.#shouldAddElement,
3822
- beforeNodeMorphed: this.#shouldMorphElement,
3823
- beforeAttributeUpdated: this.#shouldUpdateAttribute,
3824
- beforeNodeRemoved: this.#shouldRemoveElement,
3825
- afterNodeMorphed: this.#didMorphElement
3881
+ beforeNodeMorphed: element => !canRefreshFrame(element)
3826
3882
  }
3827
3883
  });
3828
- }
3829
- #shouldAddElement=node => !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id));
3830
- #shouldMorphElement=(oldNode, newNode) => {
3831
- if (oldNode instanceof HTMLElement) {
3832
- if (!oldNode.hasAttribute("data-turbo-permanent") && (this.isMorphingTurboFrame || !this.#isFrameReloadedWithMorph(oldNode))) {
3833
- const event = dispatch("turbo:before-morph-element", {
3834
- cancelable: true,
3835
- target: oldNode,
3836
- detail: {
3837
- newElement: newNode
3838
- }
3839
- });
3840
- return !event.defaultPrevented;
3841
- } else {
3842
- return false;
3843
- }
3884
+ for (const frame of currentElement.querySelectorAll("turbo-frame")) {
3885
+ if (canRefreshFrame(frame)) refreshFrame(frame);
3844
3886
  }
3845
- };
3846
- #shouldUpdateAttribute=(attributeName, target, mutationType) => {
3847
- const event = dispatch("turbo:before-morph-attribute", {
3848
- cancelable: true,
3849
- target: target,
3850
- detail: {
3851
- attributeName: attributeName,
3852
- mutationType: mutationType
3853
- }
3854
- });
3855
- return !event.defaultPrevented;
3856
- };
3857
- #didMorphElement=(oldNode, newNode) => {
3858
- if (newNode instanceof HTMLElement) {
3859
- dispatch("turbo:morph-element", {
3860
- target: oldNode,
3861
- detail: {
3862
- newElement: newNode
3863
- }
3864
- });
3865
- }
3866
- };
3867
- #shouldRemoveElement=node => this.#shouldMorphElement(node);
3868
- #reloadRemoteFrames() {
3869
- this.#remoteFrames().forEach((frame => {
3870
- if (this.#isFrameReloadedWithMorph(frame)) {
3871
- this.#renderFrameWithMorph(frame);
3872
- frame.reload();
3873
- }
3874
- }));
3875
- }
3876
- #renderFrameWithMorph(frame) {
3877
- frame.addEventListener("turbo:before-frame-render", (event => {
3878
- event.detail.render = this.#morphFrameUpdate;
3879
- }), {
3880
- once: true
3881
- });
3882
- }
3883
- #morphFrameUpdate=(currentElement, newElement) => {
3884
- dispatch("turbo:before-frame-morph", {
3885
- target: currentElement,
3887
+ dispatch("turbo:morph", {
3886
3888
  detail: {
3887
3889
  currentElement: currentElement,
3888
3890
  newElement: newElement
3889
3891
  }
3890
3892
  });
3891
- this.#morphElements(currentElement, newElement.children, "innerHTML");
3892
- };
3893
- #isFrameReloadedWithMorph(element) {
3894
- return element.src && element.refresh === "morph";
3895
3893
  }
3896
- #remoteFrames() {
3897
- return Array.from(document.querySelectorAll("turbo-frame[src]")).filter((frame => !frame.closest("[data-turbo-permanent]")));
3894
+ async preservingPermanentElements(callback) {
3895
+ return await callback();
3896
+ }
3897
+ get renderMethod() {
3898
+ return "morph";
3899
+ }
3900
+ get shouldAutofocus() {
3901
+ return false;
3898
3902
  }
3899
3903
  }
3900
3904
 
3905
+ function canRefreshFrame(frame) {
3906
+ return frame instanceof FrameElement && frame.src && frame.refresh === "morph" && !frame.closest("[data-turbo-permanent]");
3907
+ }
3908
+
3909
+ function refreshFrame(frame) {
3910
+ frame.addEventListener("turbo:before-frame-render", (({detail: detail}) => {
3911
+ detail.render = MorphingFrameRenderer.renderElement;
3912
+ }), {
3913
+ once: true
3914
+ });
3915
+ frame.reload();
3916
+ }
3917
+
3901
3918
  class SnapshotCache {
3902
3919
  keys=[];
3903
3920
  snapshots={};
@@ -3951,8 +3968,8 @@ class PageView extends View {
3951
3968
  }
3952
3969
  renderPage(snapshot, isPreview = false, willRender = true, visit) {
3953
3970
  const shouldMorphPage = this.isPageRefresh(visit) && this.snapshot.shouldMorphPage;
3954
- const rendererClass = shouldMorphPage ? MorphRenderer : PageRenderer;
3955
- const renderer = new rendererClass(this.snapshot, snapshot, PageRenderer.renderElement, isPreview, willRender);
3971
+ const rendererClass = shouldMorphPage ? MorphingPageRenderer : PageRenderer;
3972
+ const renderer = new rendererClass(this.snapshot, snapshot, rendererClass.renderElement, isPreview, willRender);
3956
3973
  if (!renderer.shouldRender) {
3957
3974
  this.forceReloaded = true;
3958
3975
  } else {
@@ -4143,7 +4160,7 @@ class Session {
4143
4160
  }
4144
4161
  refresh(url, requestId) {
4145
4162
  const isRecentRequest = requestId && this.recentRequests.has(requestId);
4146
- if (!isRecentRequest) {
4163
+ if (!isRecentRequest && !this.navigator.currentVisit) {
4147
4164
  this.visit(url, {
4148
4165
  action: "replace",
4149
4166
  shouldCacheSnapshot: false
@@ -4969,12 +4986,24 @@ const StreamActions = {
4969
4986
  this.targetElements.forEach((e => e.remove()));
4970
4987
  },
4971
4988
  replace() {
4972
- this.targetElements.forEach((e => e.replaceWith(this.templateContent)));
4989
+ const method = this.getAttribute("method");
4990
+ this.targetElements.forEach((targetElement => {
4991
+ if (method === "morph") {
4992
+ morphElements(targetElement, this.templateContent);
4993
+ } else {
4994
+ targetElement.replaceWith(this.templateContent);
4995
+ }
4996
+ }));
4973
4997
  },
4974
4998
  update() {
4999
+ const method = this.getAttribute("method");
4975
5000
  this.targetElements.forEach((targetElement => {
4976
- targetElement.innerHTML = "";
4977
- targetElement.append(this.templateContent);
5001
+ if (method === "morph") {
5002
+ morphChildren(targetElement, this.templateContent);
5003
+ } else {
5004
+ targetElement.innerHTML = "";
5005
+ targetElement.append(this.templateContent);
5006
+ }
4978
5007
  }));
4979
5008
  },
4980
5009
  refresh() {