glib-web 2.6.1 → 2.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/action.js CHANGED
@@ -2,7 +2,9 @@ import TypeUtils from "./utils/type";
2
2
 
3
3
  import ActionsRunMultiple from "./actions/runMultiple";
4
4
 
5
- import ActionsFormsSubmitV1 from "./actions/forms/submit";
5
+ import ActionsFormsSubmit from "./actions/forms/submit";
6
+ import ActionsFieldsReset from "./actions/fields/reset";
7
+ import ActionsFieldsFocus from "./actions/fields/focus";
6
8
 
7
9
  import ActionsHttpGetV1 from "./actions/http/get";
8
10
  import ActionsHttpPostV1 from "./actions/http/post";
@@ -60,7 +62,9 @@ import ActionComponentsUpdate from "./actions/components/update";
60
62
  const actions = {
61
63
  runMultiple: ActionsRunMultiple,
62
64
 
63
- "forms/submit": ActionsFormsSubmitV1,
65
+ "forms/submit": ActionsFormsSubmit,
66
+ "fields/reset": ActionsFieldsReset,
67
+ "fields/focus": ActionsFieldsFocus,
64
68
 
65
69
  "http/get": ActionsHttpGetV1,
66
70
  "http/post": ActionsHttpPostV1,
@@ -0,0 +1,6 @@
1
+ export default class {
2
+ execute(properties, component) {
3
+ const target = GLib.component.findById(properties.targetId);
4
+ target.action_focus();
5
+ }
6
+ }
@@ -0,0 +1,12 @@
1
+ import Vue from "vue";
2
+
3
+ export default class {
4
+ execute(properties, component) {
5
+ const target = GLib.component.findById(properties.targetId);
6
+ target.action_resetValue();
7
+
8
+ Vue.nextTick(() => {
9
+ GLib.action.execute(properties["onReset"], component);
10
+ });
11
+ }
12
+ }
@@ -2,6 +2,6 @@ export default class {
2
2
  execute(properties, component) {
3
3
  component.$dispatchEvent("forms/directSubmit", {
4
4
  url: properties.overrideUrl
5
- });
5
+ })
6
6
  }
7
7
  }
package/app.vue CHANGED
@@ -114,16 +114,17 @@ export default {
114
114
  this.$wsInitPhoenixSocket(this.page.phoenixSocket);
115
115
  this.$wsInitActionCable(this.page.actionCable);
116
116
 
117
- if (this.$gtag) {
118
- if (!Utils.settings.isDev) {
119
- console.log(`Tracking analytics: ${window.location}`);
120
- this.$gtag.pageview({
121
- page_location: window.location
122
- });
123
- }
124
- } else {
125
- console.warn("Analytics not setup. Set `settings.gtagId`.");
126
- }
117
+ // TODO: Remove gtag dependency
118
+ // if (this.$gtag) {
119
+ // if (!Utils.settings.isDev) {
120
+ // console.log(`Tracking analytics: ${window.location}`);
121
+ // this.$gtag.pageview({
122
+ // page_location: window.location
123
+ // });
124
+ // }
125
+ // } else {
126
+ // console.warn("Analytics not setup. Set `settings.gtagId`.");
127
+ // }
127
128
 
128
129
  // Use setTimeout() to wait until page is rendered. Don't use nextTick() because
129
130
  // it would execute much earlier (before all components finish initializing).
@@ -7,12 +7,19 @@
7
7
  :style="genericStyles()"
8
8
  :class="$classes()"
9
9
  :href="$href()"
10
+ :small="$classes().includes('small')"
10
11
  @click="$onClick()"
11
12
  v-on="on"
12
13
  >
13
14
  {{ spec.text }}
14
15
  </v-chip>
15
- <v-chip v-else :style="genericStyles()" :class="$classes()" v-on="on">
16
+ <v-chip
17
+ v-else
18
+ :style="genericStyles()"
19
+ :class="$classes()"
20
+ :small="$classes().includes('small')"
21
+ v-on="on"
22
+ >
16
23
  {{ spec.text }}
17
24
  </v-chip>
18
25
  </common-badge>
@@ -20,12 +20,12 @@
20
20
  <script>
21
21
  export default {
22
22
  props: {
23
- spec: { type: Object, required: true }
23
+ spec: { type: Object, required: true },
24
24
  },
25
25
  data() {
26
26
  return {
27
27
  groupName: null,
28
- fieldType: null
28
+ fieldType: null,
29
29
  };
30
30
  },
31
31
  methods: {
@@ -35,7 +35,7 @@ export default {
35
35
  let groupName = null;
36
36
  this.$type.ifObject(
37
37
  this.groupElement,
38
- val => (groupName = val.getAttribute("name"))
38
+ (val) => (groupName = val.getAttribute("name"))
39
39
  );
40
40
 
41
41
  this.fieldName = this.spec.name || groupName;
@@ -44,7 +44,7 @@ export default {
44
44
  ? this.spec.checkValue
45
45
  : this.spec.value;
46
46
 
47
- Utils.type.ifArray(this.spec.styleClasses, classes => {
47
+ Utils.type.ifArray(this.spec.styleClasses, (classes) => {
48
48
  if (classes.remove("switch")) {
49
49
  this.fieldType = "switch";
50
50
  }
@@ -53,20 +53,22 @@ export default {
53
53
  changed(event) {
54
54
  // Execute later to ensure the checkbox's checked state has been updated.
55
55
  setTimeout(() => {
56
- this.$type.ifObject(this.groupElement, val =>
56
+ this.$type.ifObject(this.groupElement, (val) =>
57
57
  val.dispatchEvent(new Event("change"))
58
58
  );
59
59
  }, 100);
60
60
 
61
- this.$executeOnChange();
61
+ setTimeout(() => {
62
+ this.$executeOnChange();
63
+ }, 500);
62
64
  },
63
65
  $internalizeValue(val) {
64
66
  if (val == this.spec.checkValue) {
65
67
  return val;
66
68
  }
67
69
  return this.spec.uncheckValue;
68
- }
69
- }
70
+ },
71
+ },
70
72
  };
71
73
  </script>
72
74
 
@@ -29,17 +29,17 @@ export default {
29
29
  // "fields-hidden": HiddenField
30
30
  // },
31
31
  props: {
32
- spec: { type: Object, required: true }
32
+ spec: { type: Object, required: true },
33
33
  },
34
34
  data() {
35
35
  return {
36
36
  anyChecked: false,
37
37
  errorMessage: null,
38
38
  rules: [
39
- _v => {
39
+ (_v) => {
40
40
  return this.anyChecked || this.errorMessage || true;
41
- }
42
- ]
41
+ },
42
+ ],
43
43
  };
44
44
  },
45
45
  // computed: {
@@ -51,7 +51,7 @@ export default {
51
51
  $ready() {
52
52
  const validation = this.spec.validation || {};
53
53
 
54
- Utils.type.ifObject(validation.required, required => {
54
+ Utils.type.ifObject(validation.required, (required) => {
55
55
  this.errorMessage = required.message;
56
56
  });
57
57
 
@@ -59,7 +59,7 @@ export default {
59
59
  const vm = this;
60
60
  this.$el.addEventListener(
61
61
  "change",
62
- function(e) {
62
+ function (e) {
63
63
  vm.detectChecked();
64
64
  },
65
65
  false
@@ -70,7 +70,7 @@ export default {
70
70
  const vm = this;
71
71
  this.$el
72
72
  .querySelectorAll("input[type=checkbox]")
73
- .forEach(function(checkbox) {
73
+ .forEach(function (checkbox) {
74
74
  if (checkbox.checked) {
75
75
  vm.anyChecked = true;
76
76
  return;
@@ -79,12 +79,15 @@ export default {
79
79
  },
80
80
  childSpec(item) {
81
81
  if (this.spec.readOnly) {
82
- return Object.assign(item, { readOnly: this.spec.readOnly });
82
+ return Object.assign(item, {
83
+ readOnly: this.spec.readOnly,
84
+ onChange: this.spec.onChange,
85
+ });
83
86
  }
84
87
 
85
- return item;
86
- }
87
- }
88
+ return Object.assign(item, { onChange: this.spec.onChange });
89
+ },
90
+ },
88
91
  };
89
92
  </script>
90
93
 
@@ -49,7 +49,7 @@
49
49
  </div>
50
50
  <input
51
51
  ref="directUploadFile"
52
- style="display: none;"
52
+ style="display: none"
53
53
  type="file"
54
54
  @change="uploadFiles"
55
55
  />
@@ -78,7 +78,7 @@ import Uploader from "../../utils/uploader";
78
78
 
79
79
  export default {
80
80
  props: {
81
- spec: { type: Object, required: true }
81
+ spec: { type: Object, required: true },
82
82
  },
83
83
  data() {
84
84
  return {
@@ -88,13 +88,13 @@ export default {
88
88
  fileImage: null,
89
89
  fileValue: null,
90
90
  inputElement: null,
91
- placeholder: {}
91
+ placeholder: {},
92
92
  };
93
93
  },
94
94
  computed: {
95
- showProgress: function() {
95
+ showProgress: function () {
96
96
  return this.progress.value >= 0;
97
- }
97
+ },
98
98
  },
99
99
  methods: {
100
100
  $ready() {
@@ -119,7 +119,7 @@ export default {
119
119
  },
120
120
  displayImagePreview(file, blob) {
121
121
  let reader = new FileReader();
122
- reader.onload = e => {
122
+ reader.onload = (e) => {
123
123
  this.fileTitle = file.name;
124
124
  this.fileImage = e.target.result;
125
125
  this.fileValue = blob.signed_id;
@@ -151,7 +151,9 @@ export default {
151
151
 
152
152
  // This only works for single uploads. For multi-uploads, we'll need to make sure
153
153
  // that all uploads have finished successfully.
154
- this.$executeOnChange();
154
+ setTimeout(() => {
155
+ this.$executeOnChange();
156
+ }, 500);
155
157
  }
156
158
 
157
159
  input.disabled = false;
@@ -165,15 +167,15 @@ export default {
165
167
  }
166
168
  },
167
169
  uploadFiles(e) {
168
- Array.from(e.target.files).forEach(file => this.uploadFile(file));
169
- }
170
+ Array.from(e.target.files).forEach((file) => this.uploadFile(file));
171
+ },
170
172
  // toggleDisplayProgressIndicator() {
171
173
  // this.showProgress = !this.showProgress
172
174
  // },
173
175
  // updateProgressIndicator(e) {
174
176
  // this.valueProgress = e.detail.progress
175
177
  // }
176
- }
178
+ },
177
179
  };
178
180
  </script>
179
181
 
@@ -30,11 +30,13 @@
30
30
  <script>
31
31
  export default {
32
32
  props: {
33
- spec: { type: Object, required: true }
33
+ spec: { type: Object, required: true },
34
34
  },
35
35
  methods: {
36
36
  onChange() {
37
- this.$executeOnChange();
37
+ setTimeout(() => {
38
+ this.$executeOnChange();
39
+ }, 500);
38
40
  },
39
41
  updateValue(variable) {
40
42
  if (variable.value) {
@@ -50,8 +52,8 @@ export default {
50
52
  } else {
51
53
  return "";
52
54
  }
53
- }
54
- }
55
+ },
56
+ },
55
57
  };
56
58
  </script>
57
59
 
@@ -55,7 +55,7 @@ import bus from "../../utils/eventBus";
55
55
  Quill.register("modules/imageDropAndPaste", QuillImageDropAndPaste);
56
56
 
57
57
  var ImageBlot = Quill.import("formats/image");
58
- ImageBlot.sanitize = function(url) {
58
+ ImageBlot.sanitize = function (url) {
59
59
  return url;
60
60
  };
61
61
 
@@ -69,16 +69,16 @@ class Parser {
69
69
  turndownService.use(gfm);
70
70
  turndownService.addRule("strikethrough", {
71
71
  filter: ["del", "s", "strike"],
72
- replacement: function(content) {
72
+ replacement: function (content) {
73
73
  return "~~" + content + "~~";
74
- }
74
+ },
75
75
  });
76
76
 
77
77
  turndownService.addRule("codeblock", {
78
78
  filter: ["pre"],
79
- replacement: function(content) {
79
+ replacement: function (content) {
80
80
  return "```\n" + content + "```";
81
- }
81
+ },
82
82
  });
83
83
  return turndownService.turndown(data);
84
84
  }
@@ -124,8 +124,12 @@ class TextEditor {
124
124
 
125
125
  // replace {{image1}} to real image1 url
126
126
  replaceWithRealImage(html) {
127
+ if (!html) {
128
+ return "";
129
+ }
130
+
127
131
  const vm = this.context;
128
- return html.replace(/\{\{image([0-9]+)\}\}/g, function(_, index) {
132
+ return html.replace(/\{\{image([0-9]+)\}\}/g, function (_, index) {
129
133
  const image = vm.spec.images[index - 1];
130
134
  if (
131
135
  image &&
@@ -143,10 +147,14 @@ class TextEditor {
143
147
 
144
148
  // replace image1 url with {{image1}}
145
149
  replaceWithFakeImage(html) {
150
+ if (!html) {
151
+ return "";
152
+ }
153
+
146
154
  const vm = this.context;
147
155
  let index = 0;
148
156
  vm.imageKeys.clear();
149
- return html.replace(/src="([^"]+)"|\!\[\]\((.+)\)/g, function(_, g1, g2) {
157
+ return html.replace(/src="([^"]+)"|\!\[\]\((.+)\)/g, function (_, g1, g2) {
150
158
  var imageValue = g1 || g2;
151
159
  // It seems that quill encodes '&' in the URL to '&amp;' which would screw up key matching.
152
160
  var decodedValue = imageValue.replace(/&amp;/g, "&");
@@ -164,28 +172,28 @@ class TextEditor {
164
172
  export default {
165
173
  components: { VueEditor },
166
174
  props: {
167
- spec: { type: Object, required: true }
175
+ spec: { type: Object, required: true },
168
176
  },
169
177
  data: () => ({
170
178
  customToolbar: [
171
179
  ["bold", "italic", "strike"],
172
180
  [{ header: 1 }, { header: 2 }, { header: 3 }],
173
181
  [{ list: "ordered" }, { list: "bullet" }],
174
- ["image", "link"]
182
+ ["image", "link"],
175
183
  ],
176
184
  editorSettings: {
177
185
  modules: {
178
186
  imageDropAndPaste: {
179
187
  // add an custom image handler
180
- handler: function(imageDataUrl, type, imageData) {
188
+ handler: function (imageDataUrl, type, imageData) {
181
189
  bus.$emit("richText/dropOrPaste", {
182
190
  file: imageData.toFile(),
183
191
  editor: this.quill,
184
- cursorLocation: this.getIndex()
192
+ cursorLocation: this.getIndex(),
185
193
  });
186
- }
187
- }
188
- }
194
+ },
195
+ },
196
+ },
189
197
  },
190
198
  richEditorValue: "",
191
199
  rawEditorValue: "",
@@ -197,7 +205,7 @@ export default {
197
205
  textEditor: null,
198
206
  mode: null,
199
207
  produce: null,
200
- accept: null
208
+ accept: null,
201
209
  }),
202
210
  computed: {
203
211
  showProgress() {
@@ -205,7 +213,7 @@ export default {
205
213
  },
206
214
  rawMode() {
207
215
  return this.mode == 1;
208
- }
216
+ },
209
217
  },
210
218
  watch: {},
211
219
  mounted() {
@@ -235,7 +243,7 @@ export default {
235
243
  this.produce
236
244
  );
237
245
  },
238
- uploadImage: function(file, editor, cursorLocation) {
246
+ uploadImage: function (file, editor, cursorLocation) {
239
247
  let vm = this;
240
248
  const uploaderSpec = this.imageUploader;
241
249
  // const input = this.$refs.directUploadFile
@@ -274,34 +282,34 @@ export default {
274
282
  "html"
275
283
  );
276
284
  },
277
- onRichTextEditorChanged: eventFiltering.debounce(function() {
285
+ onRichTextEditorChanged: eventFiltering.debounce(function () {
278
286
  this.producedValue = this.textEditor.producedValue(
279
287
  this.richEditorValue,
280
288
  "html",
281
289
  this.produce
282
290
  );
283
291
 
284
- this.$executeOnChange();
292
+ this.$executeOnChange(this.producedValue);
285
293
  }),
286
- onRawTextEditorChanged: eventFiltering.debounce(function() {
294
+ onRawTextEditorChanged: eventFiltering.debounce(function () {
287
295
  this.producedValue = this.textEditor.producedValue(
288
296
  this.rawEditorValue,
289
297
  "markdown",
290
298
  this.produce
291
299
  );
292
300
 
293
- this.$executeOnChange();
301
+ this.$executeOnChange(this.producedValue);
294
302
  }),
295
- insertImage: function(file, Editor, cursorLocation, blob) {
303
+ insertImage: function (file, Editor, cursorLocation, blob) {
296
304
  let vm = this;
297
305
  var reader = new FileReader();
298
- reader.onload = function(e) {
306
+ reader.onload = function (e) {
299
307
  // vm.fileUrl = e.target.result;
300
308
  if (file.type.indexOf("image") !== -1) {
301
309
  var image = new Image();
302
310
  image.src = URL.createObjectURL(file);
303
311
 
304
- image.onload = function() {
312
+ image.onload = function () {
305
313
  Editor.insertEmbed(cursorLocation, "image", image.src);
306
314
  };
307
315
 
@@ -312,7 +320,7 @@ export default {
312
320
  reader.readAsDataURL(file);
313
321
  vm.progress.value = -1;
314
322
  },
315
- updateToolbar: function(wrapper, toolbar, container) {
323
+ updateToolbar: function (wrapper, toolbar, container) {
316
324
  if (wrapper.scrollTop >= container.offsetTop) {
317
325
  toolbar.classList.add("sticky");
318
326
  toolbar.style.top = `${wrapper.offsetTop}px`;
@@ -325,7 +333,7 @@ export default {
325
333
  toolbar.style.width = "auto";
326
334
  }
327
335
  },
328
- registerScrollEvent: function() {
336
+ registerScrollEvent: function () {
329
337
  const wrapper = document.querySelector(".v-dialog") || window;
330
338
  const toolbar = this.$el.querySelector(".ql-toolbar");
331
339
  const container = this.$el.querySelector(".ql-container");
@@ -338,8 +346,8 @@ export default {
338
346
  this.updateToolbar(wrapper, toolbar, container)
339
347
  );
340
348
  }
341
- }
342
- }
349
+ },
350
+ },
343
351
  };
344
352
  </script>
345
353
 
@@ -1,6 +1,7 @@
1
1
  <template>
2
2
  <div :style="$styles()" :class="classes()">
3
3
  <v-text-field
4
+ ref="field"
4
5
  v-model="fieldModel"
5
6
  :label="spec.label"
6
7
  :name="fieldName"
@@ -136,6 +137,9 @@ export default {
136
137
  classes() {
137
138
  return this.$classes().concat("g-text-field--hintless");
138
139
  },
140
+ action_focus() {
141
+ this.$refs.field.focus();
142
+ },
139
143
  onChange: eventFiltering.debounce(function() {
140
144
  this.$executeOnChange();
141
145
  }, 300)
@@ -57,15 +57,23 @@ export default {
57
57
  let width, height;
58
58
  const aspectRatio = image.naturalWidth / image.naturalHeight;
59
59
 
60
+ console.log("FIT1");
61
+
60
62
  if (this.spec.width == "matchParent") {
63
+ console.log("FIT2");
64
+
61
65
  width = "100%";
62
66
  // This breaks the image height when displayed in a badge.
63
67
  // height = `${100 / aspectRatio}%`;
64
68
  } else {
65
69
  if (this.spec.width) {
70
+ console.log("FIT3");
71
+
66
72
  width = `${this.spec.width}px`;
67
73
  height = `${this.spec.width / aspectRatio}px`;
68
74
  } else if (this.spec.height) {
75
+ console.log("FIT4", this.spec.height);
76
+
69
77
  width = `${this.spec.height * aspectRatio}px`;
70
78
  height = `${this.spec.height}px`;
71
79
  }
@@ -166,17 +166,26 @@ export default {
166
166
  // this._linkFieldModels();
167
167
  this.$ready();
168
168
  },
169
- $recursiveUpdate() {
169
+ async $recursiveUpdate() {
170
170
  this.$update();
171
171
  this.$forceUpdate();
172
172
 
173
- // Execute on next tick to ensure that the child has received the updated spec.
174
- this.$nextTick(() => {
175
- this.$children.find(child => {
176
- child.$recursiveUpdate();
177
- });
173
+ // Execute on next tick to ensure that the children have received the updated spec.
174
+ // Important: The nextTick() needs to be used in each of the recursion.
175
+ await this.$nextTick();
176
+
177
+ this.$children.find(child => {
178
+ child.$recursiveUpdate();
178
179
  });
179
180
  },
181
+ // _recursiveUpdate() {
182
+ // this.$update();
183
+ // this.$forceUpdate();
184
+
185
+ // this.$children.find(child => {
186
+ // child._recursiveUpdate();
187
+ // });
188
+ // },
180
189
  $dispatchEvent(name, data) {
181
190
  const event = new Event(name, { bubbles: true });
182
191
 
@@ -205,6 +205,9 @@ export default {
205
205
  this.fieldModel = this._sanitizeValue(this.spec.value);
206
206
  }
207
207
  },
208
+ action_resetValue() {
209
+ this.fieldModel = this._sanitizeValue(this.spec.value);
210
+ },
208
211
  $classes(spec, defaultViewName) {
209
212
  const properties = Object.assign(
210
213
  { view: defaultViewName },
@@ -12,6 +12,7 @@
12
12
  methods because those methods use `spec` attributes, which will force $ready() to be called.
13
13
  -->
14
14
  <component
15
+ ref="ccomp"
15
16
  :is="template"
16
17
  v-if="customData"
17
18
  :name="spec.template"
@@ -29,14 +30,14 @@ export default {
29
30
  components: {
30
31
  "template-thumbnail": ThumbnailTemplate,
31
32
  "template-featured": FeaturedTemplate,
32
- "template-unsupported": UnsupportedTemplate
33
+ "template-unsupported": UnsupportedTemplate,
33
34
  },
34
35
  props: {
35
- spec: { type: Object, required: true }
36
+ spec: { type: Object, required: true },
36
37
  },
37
38
  data() {
38
39
  return {
39
- customData: null
40
+ customData: null,
40
41
  };
41
42
  },
42
43
  computed: {
@@ -51,16 +52,19 @@ export default {
51
52
  }
52
53
  return "template-unsupported";
53
54
  }
54
- }
55
+ },
55
56
  },
56
57
  methods: {
57
58
  $ready() {
58
59
  const onClick = this.spec.onClick ? { onClick: this.spec.onClick } : {};
59
60
  this.customData = Object.assign(onClick, this.spec.data);
61
+ if (this.$refs.ccomp) {
62
+ this.$refs.ccomp.$forceUpdate();
63
+ }
60
64
  },
61
65
  $registryEnabled() {
62
66
  return false;
63
- }
64
- }
67
+ },
68
+ },
65
69
  };
66
70
  </script>
@@ -29,6 +29,7 @@
29
29
  >
30
30
  <!-- Using `item.id` as key is important to make sure the item gets updated
31
31
  when dragging ends. -->
32
+ <!-- TODO: This div is causing image issue -->
32
33
  <div
33
34
  v-for="(item, index) in childViews"
34
35
  :key="item.id || index"
@@ -41,3 +41,7 @@ String.prototype.compare = function(str) {
41
41
  }
42
42
  return 0;
43
43
  };
44
+
45
+ String.prototype.squish = function() {
46
+ return this.replace(/\s+/g, " ").trim();
47
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glib-web",
3
- "version": "2.6.1",
3
+ "version": "2.6.3",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -47,4 +47,4 @@
47
47
  "prettier": "^1.18.2",
48
48
  "typescript": "^4.9.5"
49
49
  }
50
- }
50
+ }
@@ -1,14 +1,17 @@
1
+ import Vue from "vue";
2
+
1
3
  export default class {
2
4
  static get _registry() {
3
5
  return window.vueApp.registeredComponents;
4
6
  }
5
7
 
6
8
  static findById(id) {
7
- const component = this._registry[id];
8
- if (component) {
9
- return component;
10
- }
11
- console.error("Component not found: " + id);
9
+ return this._registry[id];
10
+ // const component = this._registry[id];
11
+ // if (component) {
12
+ // return component;
13
+ // }
14
+ // console.error("Component not found: " + id);
12
15
  }
13
16
 
14
17
  // static clearRegistry() {
@@ -37,4 +40,21 @@ export default class {
37
40
  static vueName(component) {
38
41
  return component.$options._componentTag;
39
42
  }
43
+
44
+ static async preserveScroll(promise) {
45
+ const pageBody = this.$pageBody;
46
+ const originalTop = pageBody.scrollTop;
47
+ const originalLeft = pageBody.scrollLeft;
48
+
49
+ await promise;
50
+
51
+ Vue.nextTick(() => {
52
+ pageBody.scrollTop = originalTop;
53
+ pageBody.scrollLeft = originalLeft;
54
+ });
55
+ }
56
+
57
+ static get $pageBody() {
58
+ return document.getElementById("page_body");
59
+ }
40
60
  }