glib-web 3.25.2 → 3.26.0

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/.eslintrc.js CHANGED
@@ -3,7 +3,7 @@ module.exports = {
3
3
  "browser": true,
4
4
  "es2021": true
5
5
  },
6
- "extends": "plugin:vue/vue3-essential",
6
+ "extends": ["plugin:vue/vue3-essential", "eslint:recommended"],
7
7
  "overrides": [
8
8
  {
9
9
  "env": {
@@ -25,10 +25,13 @@ module.exports = {
25
25
  "vue"
26
26
  ],
27
27
  "rules": {
28
- "vue/multi-word-component-names": ["off"]
28
+ "vue/multi-word-component-names": ["off"],
29
+ "no-unused-vars": "warn"
29
30
  },
30
31
  "globals": {
31
32
  "GLib": "readonly",
32
- "Utils": "readonly"
33
+ "Utils": "readonly",
34
+ "google": "readonly",
35
+ "Stripe": "readonly"
33
36
  }
34
37
  };
package/action.js CHANGED
@@ -77,6 +77,12 @@ import ActionLogicsSet from "./actions/logics/set";
77
77
 
78
78
  import ActionListsAppend from "./actions/lists/append";
79
79
 
80
+ import ActionBottomBannersOpen from "./actions/bottom_banners/open";
81
+ import ActionBottomBannersClose from "./actions/bottom_banners/close";
82
+
83
+ import ActionGlobalStatesWatch from "./actions/global_states/watch";
84
+ import ActionGlobalStatesSet from "./actions/global_states/set";
85
+
80
86
  import { dialogs, vueApp } from "./store";
81
87
 
82
88
  const actions = {
@@ -154,7 +160,13 @@ const actions = {
154
160
 
155
161
  "logics/set": ActionLogicsSet,
156
162
 
157
- "lists/append": ActionListsAppend
163
+ "lists/append": ActionListsAppend,
164
+
165
+ "bottomBanners/open": ActionBottomBannersOpen,
166
+ "bottomBanners/close": ActionBottomBannersClose,
167
+
168
+ "globalStates/watch": ActionGlobalStatesWatch,
169
+ "globalStates/set": ActionGlobalStatesSet
158
170
  };
159
171
 
160
172
  const customActions = {};
@@ -242,4 +254,5 @@ export default class Action {
242
254
  }
243
255
  }
244
256
 
257
+ // TODO: not recommended
245
258
  window.Action = Action;
@@ -1,3 +1,5 @@
1
+ import Action from "../../action";
2
+
1
3
  export default class {
2
4
  execute(properties, component) {
3
5
  Utils.dom.setCsrf(properties["token"]);
@@ -0,0 +1,9 @@
1
+ import { vueApp } from "../../store";
2
+
3
+ export default class {
4
+ execute(properties, component) {
5
+ const { targetId } = properties;
6
+
7
+ delete vueApp.bottomBanners[targetId];
8
+ }
9
+ }
@@ -0,0 +1,11 @@
1
+ import { vueApp } from "../../store";
2
+
3
+ export default class {
4
+ execute(properties, component) {
5
+ const { targetId, newView } = properties;
6
+
7
+ if (!targetId) console.error('targetId is required');
8
+
9
+ vueApp.bottomBanners[targetId] ||= newView;
10
+ }
11
+ }
@@ -1,3 +1,5 @@
1
+ import Action from "../../action";
2
+
1
3
  export default class {
2
4
  execute(spec, component) {
3
5
  const target = GLib.component.findById(spec.targetId);
@@ -1,3 +1,5 @@
1
+ import Action from "../../action";
2
+
1
3
  export default class {
2
4
  execute(spec, component) {
3
5
  const target = component.$closest(spec.view);
@@ -1,3 +1,5 @@
1
+ import Action from "../../action";
2
+
1
3
  // Experimental. Can this be merged with `components_set` ?
2
4
  export default class {
3
5
  execute(spec, component) {
@@ -1,4 +1,5 @@
1
1
  import { nextTick } from "vue";
2
+ import Action from "../../action";
2
3
 
3
4
  export default class {
4
5
  execute(spec, component) {
@@ -1,4 +1,5 @@
1
1
  import { nextTick } from "vue";
2
+ import Action from "../../action";
2
3
 
3
4
  export default class {
4
5
  execute(spec, component) {
@@ -0,0 +1,13 @@
1
+ import { vueApp } from "../../store";
2
+ import merge from "lodash.merge";
3
+
4
+ export default class {
5
+ execute(properties) {
6
+ const { state, data } = properties;
7
+ vueApp[state] = merge(vueApp[state], data);
8
+
9
+ // workaround to trigger watcher
10
+ const random = (Math.random() + 1).toString(36).substring(7);
11
+ vueApp[state][random] = 'update!';
12
+ }
13
+ }
@@ -0,0 +1,19 @@
1
+ import { watch } from "vue";
2
+ import { vueApp } from "../../store";
3
+ import merge from "lodash.merge";
4
+ import Action from "../../action";
5
+
6
+ export default class {
7
+ execute(properties, component) {
8
+ const { state, onChange } = properties;
9
+
10
+ watch(vueApp[state], (value) => {
11
+ const formData = {
12
+ glib: { globalState: value }
13
+ };
14
+
15
+ const data = merge({}, onChange, { formData: formData });
16
+ Action.execute(data, component);
17
+ });
18
+ }
19
+ }
@@ -1,4 +1,4 @@
1
- import { executeGlobalEvent, vueApp } from "../../store";
1
+ import { vueApp, executeGlobalEvent } from "../../store";
2
2
 
3
3
  export default class {
4
4
  execute(properties, component) {
@@ -4,8 +4,8 @@ import Type from "../../utils/type";
4
4
  export default class {
5
5
  execute(properties, component) {
6
6
  vueApp.richTextValues = {};
7
- // TODO
8
- // window.history.deleteAll()
7
+
8
+ Utils.history.destroy();
9
9
 
10
10
  Type.ifObject(properties["onClose"], it => {
11
11
  // Allow time for history.back() to complete, which is important for actions that need
package/app.vue CHANGED
@@ -25,6 +25,13 @@
25
25
  </v-container>
26
26
  </v-main>
27
27
  </component>
28
+ <div :id="tooltipId">
29
+ <panels-responsive :spec="vueApp.tooltipSpec"></panels-responsive>
30
+ </div>
31
+ <div class="glib-bottomBanner">
32
+ <glib-component v-for="(bottomBanner, index) in Object.values(vueApp.bottomBanners)" :spec="bottomBanner"
33
+ :key="index"></glib-component>
34
+ </div>
28
35
  </v-app>
29
36
  </template>
30
37
 
@@ -39,6 +46,7 @@ import { useDirtyState } from "./components/composable/dirtyState";
39
46
  import { useSocket } from "./components/composable/socket";
40
47
  import { usePasteable } from "./components/composable/pasteable";
41
48
  import { computed } from "vue";
49
+ import { TOOLTIP_ID } from "./constant";
42
50
 
43
51
  const { watchDirtyState } = useDirtyState();
44
52
 
@@ -46,8 +54,9 @@ export default {
46
54
  setup(props) {
47
55
  const filePaster = computed(() => props.page.filePaster);
48
56
  usePasteable(filePaster);
57
+ const tooltipId = TOOLTIP_ID;
49
58
 
50
- return { vueApp };
59
+ return { vueApp, tooltipId };
51
60
  },
52
61
  components: {
53
62
  "nav-layout": NavLayout,
@@ -74,6 +83,9 @@ export default {
74
83
  footer() {
75
84
  return this.page.footer || {};
76
85
  },
86
+ bodyHeader() {
87
+ return this.page.bodyHeader || {};
88
+ },
77
89
  bodyFooter() {
78
90
  return this.page.bodyFooter || {};
79
91
  },
@@ -338,6 +350,12 @@ body,
338
350
  overflow-y: overlay;
339
351
  }
340
352
 
353
+ .glib-bottomBanner {
354
+ position: fixed;
355
+ bottom: 0;
356
+ right: 0;
357
+ z-index: 99;
358
+ }
341
359
 
342
360
  /******/
343
361
  </style>
@@ -1,11 +1,13 @@
1
1
  import { Chart, Colors } from "chart.js";
2
2
  import chartDataLabels from 'chartjs-plugin-datalabels';
3
3
  import doughnutLabel from 'chartjs-plugin-doughnutlabel-v3';
4
- import { Vue } from "../..";
4
+ import { Vue, vueApp } from "../..";
5
5
  import { computePosition, flip, offset } from '@floating-ui/dom';
6
6
 
7
7
  import 'chartkick/chart.js';
8
8
 
9
+ import { TOOLTIP_ID } from "../../constant";
10
+
9
11
  Chart.register(chartDataLabels);
10
12
  Chart.register(doughnutLabel);
11
13
  Chart.register(Colors);
@@ -14,7 +16,6 @@ import VueChartkick from 'vue-chartkick';
14
16
  import { computed } from "vue";
15
17
  Vue.use(VueChartkick);
16
18
 
17
-
18
19
  const multipleDataSeries = (dataSeries) => {
19
20
  return dataSeries.map((value) => {
20
21
  let points = null;
@@ -32,25 +33,20 @@ const multipleDataSeries = (dataSeries) => {
32
33
  const singleDataSeries = (dataSeries) => dataSeries.reduce((prev, curr) => Object.assign(prev, { [curr.title]: curr.value }), {});
33
34
 
34
35
  const getOrCreateTooltip = (chart) => {
35
- let tooltipEl = chart.canvas.parentNode.querySelector('#_glib-chart-tooltip');
36
+ const tooltipEl = document.getElementById(TOOLTIP_ID);
36
37
  let referenceEl = chart.canvas.parentNode.querySelector('#_glib-chart-reference');
37
38
 
38
- if (!tooltipEl) {
39
- tooltipEl = document.createElement('div');
40
- tooltipEl.id = '_glib-chart-tooltip';
41
- tooltipEl.style.opacity = 1;
42
- tooltipEl.style.pointerEvents = 'none';
43
- tooltipEl.style.position = 'absolute';
44
- tooltipEl.style.transition = 'all .1s ease';
45
- tooltipEl.style.lineHeight = 1;
39
+ tooltipEl.style.opacity = 1;
40
+ tooltipEl.style.pointerEvents = 'none';
41
+ tooltipEl.style.position = 'fixed';
42
+ tooltipEl.style.transition = 'all .1s ease';
43
+ tooltipEl.style.lineHeight = 1;
46
44
 
47
- chart.canvas.parentNode.appendChild(tooltipEl);
48
- }
49
45
 
50
46
  if (!referenceEl) {
51
47
  referenceEl = document.createElement('div');
52
48
  referenceEl.id = '_glib-chart-reference';
53
- referenceEl.style.position = 'absolute';
49
+ referenceEl.style.position = 'fixed';
54
50
  referenceEl.style.width = '1px';
55
51
  referenceEl.style.height = '1px';
56
52
  referenceEl.style.opacity = 0;
@@ -67,7 +63,7 @@ const externalTooltipHandler = (multiple, dataSeries) => {
67
63
 
68
64
  if (!tooltipData) return;
69
65
 
70
- const innerHTML = tooltipData.tooltip;
66
+ const jsonView = tooltipData.tooltip;
71
67
 
72
68
  // Tooltip Element
73
69
  const { chart, tooltip } = context;
@@ -80,7 +76,7 @@ const externalTooltipHandler = (multiple, dataSeries) => {
80
76
  }
81
77
 
82
78
  if (tooltip.body) {
83
- tooltipEl.innerHTML = innerHTML;
79
+ vueApp.tooltipSpec = jsonView;
84
80
  }
85
81
 
86
82
  const position = chart.canvas.getBoundingClientRect();
@@ -1,5 +1,5 @@
1
- import { reactive, watch, watchEffect } from "vue";
2
- import { glibevent, ctx, dialogs } from "../../store";
1
+ import { reactive, watchEffect } from "vue";
2
+ import { glibevent, ctx } from "../../store";
3
3
  import { fieldModels } from "./conditional";
4
4
 
5
5
  export const dirtySpecs = reactive({});
@@ -60,16 +60,41 @@ function useDropableUtils() {
60
60
  }
61
61
 
62
62
  function useFilesState(files) {
63
- const uploading = computed(() => Object.values(files.value).reduce((prev, curr) => prev || curr.status == 'pending', false));
64
- const uploaded = computed(() => Object.values(files.value).reduce((prev, curr) => prev && !!curr.signedId, true));
63
+ const uploading = computed(() => {
64
+ return Object.values(files.value).length > 0 ? Object.values(files.value).reduce((prev, curr) => prev || curr.status == 'pending', false) : true;
65
+ });
66
+ const uploaded = computed(() => {
67
+ return Object.values(files.value).length > 0 ? Object.values(files.value).reduce((prev, curr) => prev && !!curr.signedId, true) : false;
68
+ });
69
+ const uploadingFileLength = computed(() => Object.values(files.value).reduce((prev, curr) => {
70
+ if (curr.status == 'pending') prev = prev + 1;
71
+ return prev;
72
+ }, 0));
73
+ const uploadedFileLength = computed(() => Object.values(files.value).reduce((prev, curr) => {
74
+ if (!!curr.signedId && curr.status == 'completed') prev = prev + 1;
75
+ return prev;
76
+ }, 0));
77
+
78
+ return { uploading, uploaded, uploadingFileLength, uploadedFileLength };
79
+ }
65
80
 
66
- return { uploading, uploaded };
81
+ function submitOnAllUploaded({ url, formData, files }) {
82
+ const { uploaded } = useFilesState(files);
83
+ return watch(uploaded, (val) => {
84
+ if (val) {
85
+ GLib.action.execute({
86
+ action: 'http/post',
87
+ url: url.value,
88
+ formData: formData.value
89
+ }, {});
90
+ }
91
+ });
67
92
  }
68
93
 
69
94
  function setBusyWhenUploading({ files }) {
70
95
  const { uploading } = useFilesState(files);
71
96
 
72
- watch(uploading, (val, oldVal) => {
97
+ watch(uploading, (val) => {
73
98
  if (val) {
74
99
  vueApp.indicator = true;
75
100
  } else {
@@ -90,15 +115,10 @@ const showError = (options) => {
90
115
  {}
91
116
  );
92
117
  };
93
- function uploadFiles({ droppedFiles, files, directUploadUrl, accepts = {}, responseMessages = {}, container }) {
118
+ function uploadFiles({ droppedFiles, files, spec, container }) {
119
+ let { responseMessages, accepts } = spec;
120
+ responseMessages ||= {};
94
121
  const { maxFileLength } = accepts;
95
- // const droppedFilesSizeInByte = Array.from(droppedFiles).reduce((prev, curr) => prev + curr.size, 0);
96
-
97
- // if (droppedFilesSizeInByte > maxFileSize * 1000) {
98
- // showError(accepts.maxFileSizeErrorText);
99
-
100
- // return;
101
- // }
102
122
 
103
123
  if (droppedFiles.length > maxFileLength) {
104
124
  showError(accepts.maxFileLengthErrorText);
@@ -112,51 +132,60 @@ function uploadFiles({ droppedFiles, files, directUploadUrl, accepts = {}, respo
112
132
  // show new dropped file and track progress
113
133
  const key = makeKey();
114
134
  files.value[key] = new Item({ el: droppedFiles[index], status: 'pending' });
135
+ uploadOneFile({ files, key, spec, container });
136
+ }
137
+ }
115
138
 
116
- const uploader = new Uploader(files.value[key].el, directUploadUrl, files.value[key].progress);
139
+ function uploadOneFile({ files, key, spec, container, onAfterUploaded }) {
140
+ const { directUploadUrl, accepts } = spec;
141
+ let responseMessages = spec.responseMessages || {};
142
+ const uploader = new Uploader(files.value[key].el, directUploadUrl, files.value[key].progress);
117
143
 
118
- // track progress
119
- uploader.directUploadWillStoreFileWithXHR = (request) => {
120
- request.upload.addEventListener("progress", event => {
121
- files.value[key].progress.value = (event.loaded / event.total) * 100;
122
- });
144
+ // track progress
145
+ uploader.directUploadWillStoreFileWithXHR = (request) => {
146
+ request.upload.addEventListener("progress", event => {
147
+ files.value[key].progress.value = (event.loaded / event.total) * 100;
148
+ });
123
149
 
124
- // can be use for aborting request
125
- files.value[key].xhr = request;
126
- };
150
+ // can be use for aborting request
151
+ files.value[key].xhr = request;
152
+ };
127
153
 
128
- // validate per file, skip if invalid
129
- if (!uploader.validateFile({ accepts: accepts })) {
130
- delete files.value[key];
131
- continue;
132
- }
154
+ // validate per file, skip if invalid
155
+ if (!uploader.validateFile({ accepts: accepts })) {
156
+ delete files.value[key];
157
+ }
133
158
 
134
- // upload per file
135
- uploader.start((error, blob) => {
136
- if (!error) {
137
- Object.assign(files.value[key], {
138
- status: 'completed',
139
- signedId: blob.signed_id,
140
- message: responseMessages['200']
141
- });
142
- // trigger form.vue onchange
143
- if (container && container.value) {
144
- triggerOnChange(container.value);
145
- }
146
- } else {
147
- const message = responseMessages[error.slice(-3) || 'else'];
148
- Object.assign(files.value[key], {
149
- status: 'failed',
150
- message: message
151
- });
159
+ // upload per file
160
+ uploader.start((error, blob) => {
161
+ if (!error) {
162
+ Object.assign(files.value[key], {
163
+ status: 'completed',
164
+ signedId: blob.signed_id,
165
+ message: responseMessages['200']
166
+ });
167
+ if (onAfterUploaded) {
168
+ onAfterUploaded(files.value[key]);
152
169
  }
153
- });
154
- }
170
+
171
+ // trigger form.vue onchange
172
+ if (container && container.value) {
173
+ triggerOnChange(container.value);
174
+ }
175
+ } else {
176
+ const message = responseMessages[error.slice(-3) || 'else'];
177
+ Object.assign(files.value[key], {
178
+ status: 'failed',
179
+ message: message
180
+ });
181
+ }
182
+ });
155
183
  }
156
184
 
157
- function useDropUpload({ accepts, onFinishUpload, onDragStyle, container, directUploadUrl, fileSelect, files }) {
185
+ function useDropUpload({ container, fileSelect, files, spec, uploader }) {
186
+ const { onDragStyle } = spec;
158
187
  onMounted(() => {
159
- setBusyWhenUploading({ files });
188
+ uploader.setBusyWhenUploading({ files });
160
189
 
161
190
  // handle style changes
162
191
  let dragEl = null;
@@ -174,26 +203,22 @@ function useDropUpload({ accepts, onFinishUpload, onDragStyle, container, direct
174
203
  container.value.ondragover = (event) => event.preventDefault();
175
204
  container.value.ondrop = (event) => {
176
205
  event.preventDefault();
177
- uploadFiles({
206
+ uploader.uploadFiles({
178
207
  droppedFiles: event.dataTransfer.files,
179
- accepts,
180
208
  container,
181
- directUploadUrl,
182
209
  files,
183
- responseMessages: {}
210
+ spec
184
211
  });
185
212
  container.value.classList.remove(...onDragStyle);
186
213
  };
187
214
 
188
215
  container.value.onclick = (event) => {
189
216
  const onchange = () => {
190
- uploadFiles(
217
+ uploader.uploadFiles(
191
218
  {
192
219
  droppedFiles: event.target.files,
193
- files: files,
194
- directUploadUrl,
195
- accepts,
196
- responseMessages: {},
220
+ files,
221
+ spec,
197
222
  container
198
223
  }
199
224
  );
@@ -202,16 +227,8 @@ function useDropUpload({ accepts, onFinishUpload, onDragStyle, container, direct
202
227
  fileSelect.value.onchange = onchange;
203
228
  fileSelect.value.click();
204
229
  };
205
-
206
- // handle when all file uploaded, all files have signedIds
207
- const { uploaded } = useFilesState(files);
208
- watch(uploaded, (val, oldVal) => {
209
- if (val) {
210
- nextTick(() => onFinishUpload(Object.values(files.value).map((file) => file.signedId)));
211
- }
212
- });
213
230
  });
214
231
 
215
- };
232
+ }
216
233
 
217
- export { useDropUpload, useDropableUtils, setBusyWhenUploading, uploadFiles, useFilesState };
234
+ export { useDropUpload, useDropableUtils, setBusyWhenUploading, submitOnAllUploaded, uploadFiles, uploadOneFile, useFilesState };
@@ -32,7 +32,7 @@ export function useAutocomplete({ inputRef, options, onPlaceChanged }) {
32
32
  return { autocompleteInstance };
33
33
  }
34
34
 
35
- export function useGmap({ mapRef, infoWindowRef, options, locations, showCluster = false, showDirection = false }) {
35
+ export function useGmap({ mapRef, infoWindowRef, options, locations, showCluster = false }) {
36
36
  let mapInstance = null;
37
37
  const markers = ref([]);
38
38
  const cluster = ref({});
@@ -1,32 +1,26 @@
1
- import { onMounted, ref, watch } from "vue";
2
- import { uploadFiles, useFilesState, setBusyWhenUploading, useDropableUtils } from "./dropable";
1
+ import { computed, onMounted, ref, watch } from "vue";
2
+ import * as dropUploader from "./dropable";
3
+ import * as delegateUploader from "./upload_delegator";
3
4
 
4
- const { signedIds } = useDropableUtils();
5
+ const { signedIds } = dropUploader.useDropableUtils();
5
6
 
6
7
  const onPaste = async (event, filePaster) => {
7
- const { directUploadUrl, accepts, url, inputName } = filePaster;
8
+ const { url, inputName, strategy } = filePaster;
8
9
  const pastedFiles = event.clipboardData.files;
9
10
  if (pastedFiles.length <= 0) return;
10
11
 
11
12
  const files = ref({});
12
- setBusyWhenUploading({ files });
13
- const { uploaded } = useFilesState(files);
14
- watch(uploaded, (val) => {
15
- if (val) {
16
- GLib.action.execute({
17
- action: 'http/post',
18
- url,
19
- formData: inputName ? { [inputName]: { signed_ids: signedIds(files) } } : { signed_ids: signedIds(files) }
20
- }, {});
21
- files.value = {};
22
- }
23
- });
24
- uploadFiles({
13
+ files.value = {};
14
+
15
+ const uploader = strategy == 'delegate' ? delegateUploader : dropUploader;
16
+
17
+ uploader.setBusyWhenUploading({ files });
18
+ const formData = computed(() => inputName ? { [inputName]: { signed_ids: signedIds(files) } } : { signed_ids: signedIds(files) });
19
+ uploader.submitOnAllUploaded({ files, formData, url: { value: url } });
20
+ uploader.uploadFiles({
25
21
  droppedFiles: pastedFiles,
26
22
  files,
27
- accepts,
28
- directUploadUrl,
29
- responseMessages: {}
23
+ spec: filePaster
30
24
  });
31
25
 
32
26
  };
@@ -36,7 +30,7 @@ let handler = () => { };
36
30
  function usePasteable(filePaster) {
37
31
  handler = (event) => onPaste(event, filePaster.value);
38
32
  onMounted(() => {
39
- watch(filePaster, (val, oldVal) => {
33
+ watch(filePaster, (val) => {
40
34
  if (val) {
41
35
  document.addEventListener('paste', handler);
42
36
  } else {
@@ -29,9 +29,10 @@ class SocketHandler {
29
29
  }
30
30
 
31
31
  get component() {
32
- if (this.targetId) {
33
- return GLib.component.findById(this.targetId);
32
+ if (!this.targetId) {
33
+ return null;
34
34
  }
35
+ return GLib.component.findById(this.targetId);
35
36
  }
36
37
  }
37
38
  class ExampleSocketHandler extends SocketHandler {
@@ -109,7 +110,7 @@ const useSocket = (config, defaultConsumer = null) => {
109
110
  rejected() {
110
111
  handler.rejected();
111
112
  },
112
- disconnected({ willAttemptReconnect: boolean }) {
113
+ disconnected({ willAttemptReconnect }) {
113
114
  handler.disconnected();
114
115
  }
115
116
  }
@@ -0,0 +1,24 @@
1
+ import { vueApp } from "../../store";
2
+ import { useDropableUtils } from "./dropable";
3
+
4
+ const { makeKey } = useDropableUtils();
5
+
6
+ function uploadFiles(obj) {
7
+ const { droppedFiles, spec } = obj;
8
+ Object.assign(
9
+ vueApp.uploader,
10
+ {
11
+ files: Array.from(droppedFiles).reduce((prev, curr) => {
12
+ prev[makeKey()] = curr;
13
+
14
+ return prev;
15
+ }, {}),
16
+ spec: spec
17
+ }
18
+ );
19
+ }
20
+
21
+ function submitOnAllUploaded(obj) { }
22
+ function setBusyWhenUploading(obj) { }
23
+
24
+ export { uploadFiles, submitOnAllUploaded, setBusyWhenUploading };
@@ -196,21 +196,23 @@
196
196
  <script>
197
197
  import { ref, computed, defineComponent, watch, getCurrentInstance } from 'vue';
198
198
  import { VIcon } from 'vuetify/components';
199
- import { setBusyWhenUploading, useDropableUtils, uploadFiles, useFilesState } from "../composable/dropable";
199
+ import * as dropUploader from "../composable/dropable";
200
+ import * as delegateUploader from "../composable/upload_delegator";
200
201
  import { triggerOnChange } from "../composable/form";
201
202
  import { nextTick } from "vue";
202
- const { makeKey, Item } = useDropableUtils();
203
+ const { makeKey, Item, signedIds } = dropUploader.useDropableUtils();
203
204
 
204
205
  export default defineComponent({
205
206
  props: { spec: { type: Object } },
206
207
  components: { VIcon },
207
208
  setup(props) {
209
+ const uploader = props.spec.strategy == 'delegate' ? delegateUploader : dropUploader;
210
+
208
211
  const fileSelect = ref(null);
209
212
  const container = ref(null);
210
213
  const icon = ref(null);
211
214
  const { ctx } = getCurrentInstance();
212
215
 
213
- const responseMessages = props.spec.responseMessages;
214
216
  const uploadTitle = props.spec.uploadTitle || 'File added';
215
217
 
216
218
  const files = ref({});
@@ -232,15 +234,20 @@ export default defineComponent({
232
234
  }
233
235
 
234
236
  if (props.spec.onFinishUpload) {
235
- const { uploaded } = useFilesState(files);
236
- watch(uploaded, (val, oldVal) => {
237
+ const { uploaded } = dropUploader.useFilesState(files);
238
+ watch(uploaded, (val) => {
237
239
  if (val) {
238
240
  nextTick(() => GLib.action.execute(props.spec.onFinishUpload, ctx));
239
241
  }
240
242
  });
241
243
  }
242
244
 
243
- setBusyWhenUploading({ files });
245
+ uploader.setBusyWhenUploading({ files });
246
+ if (props.spec.url) {
247
+ const formData = computed(() => ({ signed_ids: signedIds(files) }));
248
+ const url = computed(() => props.spec.url);
249
+ uploader.submitOnAllUploaded({ files, url, formData });
250
+ }
244
251
 
245
252
  const showUploadedFile = computed(() => {
246
253
  return Object.keys(files.value).length > 0;
@@ -248,13 +255,11 @@ export default defineComponent({
248
255
 
249
256
  function handleDrop(e) {
250
257
  e.preventDefault();
251
- uploadFiles(
258
+ uploader.uploadFiles(
252
259
  {
253
260
  droppedFiles: e.dataTransfer.files,
254
261
  files: files,
255
- directUploadUrl: props.spec.directUploadUrl,
256
- accepts: props.spec.accepts,
257
- responseMessages: responseMessages,
262
+ spec: props.spec,
258
263
  container: container
259
264
  }
260
265
  );
@@ -296,13 +301,11 @@ export default defineComponent({
296
301
 
297
302
  function handleClick(e) {
298
303
  const onchange = () => {
299
- uploadFiles(
304
+ uploader.uploadFiles(
300
305
  {
301
306
  droppedFiles: e.target.files,
302
307
  files: files,
303
- directUploadUrl: props.spec.directUploadUrl,
304
- accepts: props.spec.accepts,
305
- responseMessages: responseMessages,
308
+ spec: props.spec,
306
309
  container: container
307
310
  }
308
311
  );
@@ -323,7 +326,6 @@ export default defineComponent({
323
326
  handleClick,
324
327
  handleRemoveFile,
325
328
  showUploadedFile,
326
- responseMessages,
327
329
  uploadTitle,
328
330
  props
329
331
  };
@@ -166,21 +166,6 @@ export default {
166
166
  $update() {
167
167
  this.$ready();
168
168
  },
169
- async $recursiveUpdate(stack) {
170
- // const children = this.$children;
171
- if (this.spec) {
172
- // this.$update();
173
- // this.$forceUpdate();
174
-
175
- // Execute on next tick to ensure that the child has received the updated spec.
176
- // $children is removed in vue 3
177
- // this.$nextTick(() => {
178
- // this.$children.find(child => {
179
- // child.$recursiveUpdate();
180
- // });
181
- // });
182
- }
183
- },
184
169
  $dispatchEvent(name, data) {
185
170
  const event = new Event(name, { bubbles: true });
186
171
 
@@ -2,6 +2,7 @@ import Hash from "../../utils/hash";
2
2
  import { fieldModels, watchFieldModels } from "../composable/conditional";
3
3
  import { dirtySpecs } from "../composable/dirtyState";
4
4
  import { determineColor } from "../../utils/constant";
5
+ import Action from "../../action";
5
6
 
6
7
  const NUMBER_PRECISION = 2;
7
8
  const isNeedToBeFixed = (val, component) => {
@@ -54,7 +55,7 @@ export default {
54
55
  }
55
56
  },
56
57
  watch: {
57
- fieldModel: function (val, oldVal) {
58
+ fieldModel: function (val) {
58
59
  if (val === this.vuetifyEmptyString) {
59
60
  val = "";
60
61
  }
@@ -140,6 +141,7 @@ export default {
140
141
  if (oldValue != value) {
141
142
  this.$nextTick(() => {
142
143
  // Call either onIfTrue or onIfFalse.
144
+ if (value == null) return;
143
145
  Action.execute(this.spec[`onIf${value.toString().capitalize()}`], this);
144
146
  });
145
147
  }
@@ -2,11 +2,13 @@
2
2
  <div :style="'width: 100%'" :class="$classes()">
3
3
  <v-tabs v-model="tab" :bg-color="spec.backgroundColor || 'white'" slider-color="primary" color="primary" show-arrows
4
4
  :height="spec.height || 60" :grow="$classes().includes('no-grow') ? false : true">
5
- <v-tab v-for="(item, index) in spec.buttons" @click="handleClick(index)" :key="index" height="100%" :value="index">
5
+ <v-tab v-for="(item, index) in spec.buttons" @click="handleClick(index)" :key="index" height="100%"
6
+ :value="index">
6
7
  <common-badge :spec="item">
7
8
  <div class="tab-content">
8
- <common-icon :class="index == spec.activeIndex ? 'text-primary' : null" :spec="item.icon" v-if="item.icon" />
9
- <span :style="{ marginLeft: '2px' }" :class="index == spec.activeIndex ? 'text-primary' : null">{{ item.text
9
+ <common-icon :class="index == spec.activeIndex ? 'text-primary' : null" :spec="item.icon"
10
+ v-if="item.icon" />
11
+ <span :style="{ marginLeft: '2px' }" :class="index == spec.activeIndex ? 'text-primary' : null">{{ item.text
10
12
  }}</span>
11
13
  </div>
12
14
  </common-badge>
@@ -46,14 +48,14 @@ export default {
46
48
  methods: {
47
49
  handleClick(index) {
48
50
  this.$nextTick(function () {
49
- const activeTab = this.buttons[index]
50
- const onClick = activeTab?.onClick
51
+ const activeTab = this.buttons[index];
52
+ const onClick = activeTab?.onClick;
51
53
 
52
54
  if (onClick) {
53
- Action.execute(onClick, this)
55
+ Action.execute(onClick, this);
54
56
  }
55
- this.tab = index
56
- })
57
+ this.tab = index;
58
+ });
57
59
  },
58
60
  $ready() {
59
61
  if (this.spec.activeIndex) {
@@ -84,6 +86,7 @@ export default {
84
86
  <!-- Overridable -->
85
87
  <style lang="scss" scoped>
86
88
  .tab-content {
89
+ display: flex;
87
90
  padding: 5px;
88
91
  }
89
92
  </style>
@@ -91,18 +91,20 @@
91
91
  <script>
92
92
  import { computed, getCurrentInstance } from "vue";
93
93
  import { defineComponent, ref } from "vue";
94
- import { uploadFiles, useFilesState, setBusyWhenUploading, useDropableUtils } from "./composable/dropable";
94
+ import * as dropUploader from "./composable/dropable";
95
+ import * as delegateUploader from "./composable/upload_delegator";
95
96
  import { watch } from "vue";
96
97
  import merge from 'lodash.merge';
97
- const { signedIds } = useDropableUtils();
98
+ const { signedIds } = dropUploader.useDropableUtils();
98
99
 
99
100
  export default defineComponent({
100
101
  props: { spec: { type: Object, required: true } },
101
102
  setup(props) {
103
+
102
104
  const selected = computed(() => props.spec.selected);
103
105
  const { ctx } = getCurrentInstance();
104
106
  const files = ref({});
105
- const { uploaded } = useFilesState(files);
107
+
106
108
  let dropData = null;
107
109
 
108
110
  const makeFormData = (dragData) => {
@@ -116,20 +118,15 @@ export default defineComponent({
116
118
  return merge(dropData, dragData);
117
119
  };
118
120
 
119
- setBusyWhenUploading({ files });
120
-
121
+ const uploader = props.spec.strategy == 'delegate' ? delegateUploader : dropUploader;
122
+ uploader.setBusyWhenUploading({ files });
121
123
  // if all file uploaded send http post
122
- watch(uploaded, (val) => {
123
- if (val) {
124
- GLib.action.execute({
125
- action: 'http/post',
126
- url: props.spec.url,
127
- formData: makeFormData({ signed_ids: signedIds(files) })
128
- }, ctx);
129
- dropData = null;
130
- files.value = {};
131
- }
132
- });
124
+ if (props.spec.url) {
125
+ const formData = computed(() => makeFormData({ signed_ids: signedIds(files) }));
126
+ const url = computed(() => props.spec.url);
127
+ uploader.submitOnAllUploaded({ url, formData, files });
128
+ }
129
+
133
130
 
134
131
  const makeItems = () => (props.spec.items.map((item) => {
135
132
  if (selected.value == item.id) {
@@ -161,7 +158,7 @@ export default defineComponent({
161
158
  }
162
159
  });
163
160
 
164
- const handleDragLeave = (event, item) => {
161
+ const handleDragLeave = (event) => {
165
162
  if (!event.target || !event.relatedTarget) return false;
166
163
  if (event.target.contains(event.relatedTarget) || event.relatedTarget.contains(event.target)) return false;
167
164
  lastDragItem.value = null;
@@ -178,12 +175,11 @@ export default defineComponent({
178
175
  // 1. files
179
176
  // 2. other components
180
177
  if (event.dataTransfer.files.length > 0) {
181
- uploadFiles({
178
+ files.value = {};
179
+ uploader.uploadFiles({
182
180
  droppedFiles: event.dataTransfer.files,
183
- accepts: props.spec.accepts,
184
- directUploadUrl: props.spec.directUploadUrl,
181
+ spec: props.spec,
185
182
  files: files,
186
- responseMessages: {}
187
183
  });
188
184
  } else {
189
185
  const dragData = JSON.parse(event.dataTransfer.getData('text'));
package/constant.js ADDED
@@ -0,0 +1 @@
1
+ export const TOOLTIP_ID = '_glib-tooltip';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glib-web",
3
- "version": "3.25.2",
3
+ "version": "3.26.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -38,7 +38,7 @@
38
38
  "devDependencies": {
39
39
  "@types/chart.js": "^2.9.34",
40
40
  "eslint": "^8.36.0",
41
- "eslint-plugin-vue": "^9.17.0",
41
+ "eslint-plugin-vue": "^9.26.0",
42
42
  "prettier": "^1.18.2",
43
43
  "typescript": "^4.9.5"
44
44
  }
package/store.js CHANGED
@@ -15,7 +15,10 @@ export const vueApp = reactive({
15
15
  temp: {},
16
16
  richTextValues: {},
17
17
  draggedComponent: null,
18
- lastNavigationCount: null
18
+ lastNavigationCount: null,
19
+ tooltipSpec: {},
20
+ bottomBanners: {},
21
+ uploader: {}
19
22
  });
20
23
 
21
24
  export const dialogs = reactive([]);
package/utils/history.js CHANGED
@@ -69,7 +69,6 @@ export default class {
69
69
 
70
70
  const data = event.state;
71
71
  data.content.__poppedState = true;
72
- //
73
72
 
74
73
  vueApp.page = data.content;
75
74
  vueApp.lastNavigationCount = data.navigationCount;
@@ -100,4 +99,14 @@ export default class {
100
99
  return false;
101
100
  }
102
101
  }
102
+
103
+ static destroy() {
104
+ if (this.navigationCount <= 0) return false;
105
+
106
+ window.onpopstate = () => { };
107
+ window.history.go(-this.navigationCount);
108
+ this.restoreOnBackOrForward();
109
+
110
+ return true;
111
+ }
103
112
  }
@@ -1,73 +0,0 @@
1
- export default {
2
- data() {
3
- return {
4
- options: {
5
- annotation: {
6
- drawTime: "afterDraw",
7
- events: ["mouseleave", "mouseenter"],
8
- annotations: []
9
- }
10
- }
11
- };
12
- },
13
- created() {
14
- if (this.spec.lineVertical) {
15
- this.options.annotation.annotations = this.spec.lineVertical.map(
16
- (element, index) => this.annotation(element, index)
17
- );
18
- }
19
- },
20
- methods: {
21
- annotation(options, index) {
22
- const { text, x } = options;
23
-
24
- return {
25
- type: "line",
26
- mode: "vertical",
27
- scaleID: "x-axis-0",
28
- value: new Date(x).getTime(),
29
- borderColor: "rgba(0,0,255,0.3)",
30
- borderWidth: 3,
31
- borderDash: [5, 10],
32
- label: {
33
- content: text,
34
- enabled: true,
35
- position: "top",
36
- backgroundColor: "rgba(0,0,0,0.3)"
37
- },
38
- onMouseenter: function(e) {
39
- Object.assign(
40
- this.chartInstance.options.annotation.annotations[index],
41
- { borderColor: "rgba(0,0,255,0.8)" },
42
- {
43
- label: {
44
- content: text,
45
- enabled: true,
46
- position: "top",
47
- backgroundColor: "rgba(0,0,0,0.8)"
48
- }
49
- }
50
- );
51
-
52
- this.chartInstance.update();
53
- },
54
- onMouseleave: function(e) {
55
- Object.assign(
56
- this.chartInstance.options.annotation.annotations[index],
57
- { borderColor: "rgba(0,0,255,0.3)" },
58
- {
59
- label: {
60
- content: text,
61
- enabled: true,
62
- position: "top",
63
- backgroundColor: "rgba(0,0,0,0.3)"
64
- }
65
- }
66
- );
67
-
68
- this.chartInstance.update();
69
- }
70
- };
71
- }
72
- }
73
- };
@@ -1,31 +0,0 @@
1
- export default {
2
- data() {
3
- return {
4
- options: {
5
- dataName: "",
6
- tooltips: {
7
- intersect: false,
8
- callbacks: {
9
- afterLabel: this.afterLabel
10
- }
11
- }
12
- }
13
- };
14
- },
15
- methods: {
16
- afterLabel(tooltipItem) {
17
- if (!this.dataName) {
18
- console.error("dataName is empty");
19
- }
20
-
21
- const selectedSeries = this.spec[this.dataName][tooltipItem.datasetIndex];
22
- const point = selectedSeries.points.find(
23
- point => point.x == tooltipItem.xLabel
24
- );
25
- const tooltip = point ? point.tooltip : null;
26
- if (tooltip) {
27
- return tooltip;
28
- }
29
- }
30
- }
31
- };