glib-web 4.1.2 → 4.2.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.
Files changed (46) hide show
  1. package/actions/dialogs/show.js +1 -0
  2. package/actions/logics/set.js +5 -2
  3. package/components/button.vue +1 -1
  4. package/components/composable/upload.js +1 -1
  5. package/components/fields/_patternText.vue +1 -1
  6. package/components/fields/_select.vue +1 -1
  7. package/components/fields/check.vue +1 -1
  8. package/components/fields/checkGroup.vue +2 -2
  9. package/components/fields/chipGroup.vue +1 -1
  10. package/components/fields/creditCard.vue +1 -1
  11. package/components/fields/dynamicGroup.vue +1 -1
  12. package/components/fields/dynamicSelect.vue +1 -1
  13. package/components/fields/file.vue +1 -1
  14. package/components/fields/googlePlace.vue +1 -1
  15. package/components/fields/hidden.vue +1 -1
  16. package/components/fields/location.vue +1 -1
  17. package/components/fields/multiUpload.vue +24 -16
  18. package/components/fields/otpField.vue +1 -1
  19. package/components/fields/phone.vue +1 -1
  20. package/components/fields/radio.vue +1 -1
  21. package/components/fields/radioGroup.vue +1 -1
  22. package/components/fields/rating.vue +1 -1
  23. package/components/fields/richText.vue +1 -1
  24. package/components/fields/sign.vue +2 -2
  25. package/components/fields/stripe/stripeFields.vue +1 -1
  26. package/components/fields/stripe/stripeIndividualFields.vue +1 -1
  27. package/components/fields/stripeExternalAccount.vue +1 -1
  28. package/components/fields/stripeToken.vue +1 -1
  29. package/components/fields/text.vue +1 -1
  30. package/components/fields/textarea.vue +1 -1
  31. package/components/fields/timer.vue +1 -1
  32. package/components/mixins/generic.js +4 -20
  33. package/components/mixins/styles.js +21 -23
  34. package/components/popover.vue +10 -2
  35. package/components/validation.js +179 -0
  36. package/cypress/e2e/glib-web/display.cy.ts +16 -0
  37. package/nav/dialog.vue +1 -2
  38. package/package.json +1 -1
  39. package/store.js +9 -2
  40. package/utils/glibDirectUpload.js +3 -1
  41. package/utils/history.js +2 -1
  42. package/utils/launch/dialog.js +79 -0
  43. package/utils/launch/popover.js +62 -0
  44. package/utils/launch/sheet.js +43 -0
  45. package/utils/launch/snackbar.js +36 -0
  46. package/utils/launch.js +4 -211
@@ -4,6 +4,7 @@ export default class {
4
4
  execute(spec, component) {
5
5
  const dialog = dialogs.value.last();
6
6
  if (spec.updateExisting && dialog) {
7
+ Object.assign(dialog.spec, spec);
7
8
  dialog.updateContent(spec);
8
9
  return;
9
10
  }
@@ -27,7 +27,10 @@ export default class {
27
27
 
28
28
  if (spec.targetId) {
29
29
  targetComponent = GLib.component.findById(spec.targetId);
30
- } else {
30
+ } else if (spec.targetIds) {
31
+ targetComponent = spec.targetIds.map((id) => GLib.component.findById(id));
32
+ }
33
+ else {
31
34
  console.error(`id not recognized=${spec.targetId}`);
32
35
  }
33
36
 
@@ -37,7 +40,7 @@ export default class {
37
40
  data[key] = jsonLogic.apply(properties[key], spec.variables || fieldModels);
38
41
  }
39
42
  Utils.type.ifObject(spec.data, (additionalData) => data = merge(data, additionalData));
40
- targetComponent.action_merge(data);
43
+ Array.isArray(targetComponent) ? targetComponent.forEach((comp) => comp.action_merge(data)) : targetComponent.action_merge(data);
41
44
  });
42
45
 
43
46
  GLib.action.execute(spec.onSet, targetComponent);
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <common-button :spec="spec" @[menter]="handleMouseEvent(spec.onMouseEnter)"
2
+ <common-button :style="$styles()" :class="$classes()" :spec="spec" @[menter]="handleMouseEvent(spec.onMouseEnter)"
3
3
  @[mleave]="handleMouseEvent(spec.onMouseLeave)" />
4
4
  </template>
5
5
 
@@ -55,7 +55,7 @@ function uploadOneFile({ files, key, spec, container, onAfterUploaded }) {
55
55
  files.value[key].progress,
56
56
  spec.storagePrefix,
57
57
  spec.metadata,
58
- spec.tagging
58
+ spec.tagging || spec.tags
59
59
  );
60
60
 
61
61
  // track progress
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :style="$styles()" :class="classes()" v-if="!isInputIgnored">
2
+ <div :style="$styles()" :class="classes()" v-if="loadIf">
3
3
  <!-- See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date for why we need to use `pattern` -->
4
4
  <v-text-field :color="gcolor" v-model="fieldModel" :name="fieldName" :label="spec.label" :hint="spec.hint"
5
5
  :type="type" :readonly="spec.readOnly" :disabled="inputDisabled" :min="$sanitizeValue(spec.min)"
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div ref="container" :style="$styles()" :class="classes()" v-if="!isInputIgnored">
2
+ <div ref="container" :style="$styles()" :class="classes()" v-if="loadIf">
3
3
  <v-autocomplete :color="gcolor" v-model="fieldModel" :label="label" :items="normalizedOptions"
4
4
  :chips="spec.multiple" :multiple="spec.multiple" :readonly="spec.readOnly" :clearable="!spec.readOnly"
5
5
  :placeholder="spec.placeholder" :rules="rules" persistent-hint :append-icon="append.icon" validate-on="blur"
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :class="$classes()" :style="$styles()" v-if="!isInputIgnored">
2
+ <div :class="$classes()" :style="$styles()" v-if="loadIf">
3
3
  <!-- This hidden field should always be there to make sure the submitted param is not empty,
4
4
  which could cause "Not accessible" error on the server. -->
5
5
  <input v-model="uncheckValue" type="hidden" :name="fieldName" />
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :style="$styles()" :class="$classes()" v-if="!isInputIgnored">
2
+ <div :style="$styles()" :class="$classes()" v-if="loadIf">
3
3
  <fields-check v-for="(childView, index) in childViews" :spec="childView" :key="index"
4
4
  @oncheck="updateFieldModel"></fields-check>
5
5
  <v-input ref="validator" :model-value="fieldModel" :rules="$validation()" :name="fieldName" validate-on="input"
@@ -8,7 +8,7 @@
8
8
  <template v-if="values.length > 0">
9
9
  <input type="hidden" v-for="(value, index) in values" :value="value" :key="index" :name="fieldName" />
10
10
  </template>
11
- <input v-else type="hidden" :name="fieldName" :value="null" />
11
+ <input v-else type="hidden" :name="fieldName" :value="spec.uncheckValue" />
12
12
  </div>
13
13
  </template>
14
14
 
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div ref="container" :style="$styles()" :class="$classes()" v-if="!isInputIgnored">
2
+ <div ref="container" :style="$styles()" :class="$classes()" v-if="loadIf">
3
3
  <v-chip-group :color="gcolor" v-model="fieldModel" @update:modelValue="onChange()" :multiple="spec.multiple" column>
4
4
  <v-chip v-for="(option, index) in spec.options" :key="index" filter :variant="variant">{{
5
5
  option.text }}</v-chip>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :style="$styles()" :class="$classes()" v-if="!isInputIgnored">
2
+ <div :style="$styles()" :class="$classes()" v-if="loadIf">
3
3
  <div class="stretcher">
4
4
  <StripeIndividualFields v-if="$classes().includes('individual')" :spec="spec" ref="delegate"
5
5
  :on-complete="_retrieveToken" :on-error="_displayError" />
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :style="$styles()" :class="$classes()" v-if="!isInputIgnored">
2
+ <div :style="$styles()" :class="$classes()" v-if="loadIf">
3
3
  <div v-for="(group, groupIndex) in groupSpecs" :key="`group${groupIndex}`">
4
4
  <input v-if="isDeleted(groupIndex)" type="hidden" :name="`${spec.name}[${groupIndex}][_destroy]`" value="1" />
5
5
  <div :style="{ display: isDeleted(groupIndex) ? 'none' : 'block' }">
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :style="$styles()" v-if="!isInputIgnored">
2
+ <div :style="$styles()" v-if="loadIf">
3
3
  <v-autocomplete ref="autocomplete" v-model="model" :items="allItems" :loading="$isBusy"
4
4
  :v-model:search-input="search" :label="spec.label" hide-no-data hide-selected no-filter return-object
5
5
  :chips="true" :deletable-chips="true" :multiple="spec.multiple">
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div ref="fileUploadContainer" :style="$styles()" :class="$classes()" v-if="!isInputIgnored">
2
+ <div ref="fileUploadContainer" :style="$styles()" :class="$classes()" v-if="loadIf">
3
3
  <label v-if="spec.label" class="v-label v-label--active">{{ spec.label }}</label>
4
4
  <div class="preview-container" :class="[fileTitle ? 'uploaded' : '']">
5
5
  <v-avatar v-if="placeholder.type == 'avatar'" :style="$styles(placeholder)">
@@ -1,7 +1,7 @@
1
1
  <!-- TODO: This probably can be merged with latLong-v1 or map-v1, i.e. add infoWindow support to latLong or map -->
2
2
 
3
3
  <template>
4
- <v-container fluid class="pa-0" v-if="!isInputIgnored">
4
+ <v-container fluid class="pa-0" v-if="loadIf">
5
5
  <div class="v-input v-text-field theme--light">
6
6
  <div class="v-input__control">
7
7
  <div class="v-input__slot">
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <input type="hidden" :name="fieldName" :value="fieldModel" :disabled="inputDisabled" v-if="!isInputIgnored" />
2
+ <input type="hidden" :name="fieldName" :value="fieldModel" :disabled="inputDisabled" v-if="loadIf" />
3
3
  </template>
4
4
 
5
5
  <script>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :style="$styles()" :class="$classes()" v-if="!isInputIgnored">
2
+ <div :style="$styles()" :class="$classes()" v-if="loadIf">
3
3
 
4
4
  <fields-text ref="autocomplete" type="text" :spec="spec"></fields-text>
5
5
 
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :style="$styles()" :class="$classes()" v-if="!isInputIgnored">
2
+ <div :style="$styles()" :class="$classes()" v-if="loadIf">
3
3
  <div ref="container" @click="handleClick" @drop="handleDrop" @dragover="handleDragOver" @dragleave="handleDragLeave"
4
4
  class="gdrop-file border-[2px]">
5
5
  <input ref="fileSelect" type="file" multiple style="display: none">
@@ -217,23 +217,31 @@ export default defineComponent({
217
217
  const uploadTitle = props.spec.uploadTitle || 'File added';
218
218
 
219
219
  const files = ref({});
220
- if (props.spec.files && props.spec.files.length > 0) {
221
- files.value = props.spec.files.reduce((prev, curr) => {
222
- const key = makeKey();
223
-
224
- prev[key] = new Item({
225
- status: null, // pending, completed, failed
226
- url: curr.url,
227
- name: curr.name,
228
- signedId: curr.signed_id,
229
- type: curr.type,
230
- el: null
231
- });
232
-
233
- return prev;
234
- }, {});
220
+
221
+ function initFiles(fls) {
222
+ if (fls) {
223
+ files.value = fls.reduce((prev, curr) => {
224
+ const key = makeKey();
225
+
226
+ prev[key] = new Item({
227
+ status: null, // pending, completed, failed
228
+ url: curr.url,
229
+ name: curr.name,
230
+ signedId: curr.signed_id,
231
+ type: curr.type,
232
+ el: null
233
+ });
234
+
235
+ return prev;
236
+ }, {});
237
+ }
235
238
  }
236
239
 
240
+ initFiles(props.spec.files);
241
+ watch(props.spec, (value) => {
242
+ initFiles(value.files);
243
+ });
244
+
237
245
  if (props.spec.onFinishUpload) {
238
246
  const { uploaded } = useFilesState(files);
239
247
  watch(uploaded, (val) => {
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div ref="otp" :style="$styles()" :class="$classes()" v-if="!isInputIgnored">
2
+ <div ref="otp" :style="$styles()" :class="$classes()" v-if="loadIf">
3
3
  <v-otp-input ref="field" v-model="fieldModel" :name="fieldName" :disabled="inputDisabled" :rounded="4"
4
4
  :max-width="maxWidth" type="spec.type || 'number'" :length="length" :variant="variant"
5
5
  @change="$executeOnChange()" />
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :style="$styles()" :class="$classes()" v-if="!isInputIgnored">
2
+ <div :style="$styles()" :class="$classes()" v-if="loadIf">
3
3
  <v-phone-input :guessCountry="guessCountry" displayFormat="e164" :defaultCountry="defaultCountry" :color="gcolor"
4
4
  v-model="fieldModel" :label="spec.label" :name="fieldName" :hint="spec.hint" :placeholder="spec.placeholder"
5
5
  :maxlength="spec.maxLength || 255" :readonly="spec.readOnly" :rules="$validation()"
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :class="$classes()" :style="$styles()" v-if="!isInputIgnored">
2
+ <div :class="$classes()" :style="$styles()" v-if="loadIf">
3
3
  <v-radio :label="spec.label" :value="(spec.value || '').presence() || vuetifyEmptyString" :readonly="spec.readOnly"
4
4
  :on-icon="spec.onIcon" :off-icon="spec.offIcon" @click="$onClick()" :color="gcolor"
5
5
  :class="spec.imageUrl || spec.icon ? 'custom-radio' : ''">
@@ -5,7 +5,7 @@
5
5
  -->
6
6
  <v-radio-group v-model="fieldModel" :name="fieldName" :readonly="spec.readOnly" :disabled="inputDisabled"
7
7
  :rules="$validation()" :inline="spec.row" validate-on="blur" @change="$executeOnChange()" :class="$classes()"
8
- :style="$styles()" v-if="!isInputIgnored">
8
+ :style="$styles()" v-if="loadIf">
9
9
  <div v-for="(childView, index) in spec.childViews" :key="index" style="width: 100%;">
10
10
  <glib-component :spec="childSpec(childView)" />
11
11
  </div>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div v-if="!isInputIgnored">
2
+ <div v-if="loadIf">
3
3
  <v-rating v-model="fieldModel" :name="fieldName" empty-icon="star_outline" full-icon="star" half-icon="star_half"
4
4
  :half-increments="spec.halfIncrements" hover length="5" :readonly="spec.readOnly" :disabled="inputDisabled"
5
5
  :color="spec.color" :bg-color="spec.color" :size="spec.size"></v-rating>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div ref="container" :style="$styles()" :class="$classes()" v-if="!isInputIgnored">
2
+ <div ref="container" :style="$styles()" :class="$classes()" v-if="loadIf">
3
3
  <v-tabs v-model="mode" fixed-tabs>
4
4
  <v-tab @click="onRichTextClicked">Editor</v-tab>
5
5
  <v-tab @click="onRawTextClicked">Code</v-tab>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :class="$classes()" :style="$styles()" v-if="!isInputIgnored">
2
+ <div :class="$classes()" :style="$styles()" v-if="loadIf">
3
3
  <div class="glib-canvas-container">
4
4
  <canvas style="width: 100%; height: 100%" @mousedown="handleDrawStart" @mousemove="handleDrawing"
5
5
  @mouseup="handleDrawEnd" @touchstart="handleDrawStart" @touchmove="handleDrawing" @touchend="handleDrawEnd"
@@ -44,7 +44,7 @@
44
44
  </style>
45
45
 
46
46
  <script setup>
47
- import { ref, computed, defineProps } from "vue";
47
+ import { ref, computed } from "vue";
48
48
  import * as uploader from "../composable/upload";
49
49
  import { useFileUtils } from "../composable/file";
50
50
 
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div>
3
- <div ref="container" v-if="!isInputIgnored">
3
+ <div ref="container" v-if="loadIf">
4
4
  <!-- A Stripe Element will be inserted here. -->
5
5
  </div>
6
6
  </div>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div v-if="!isInputIgnored">
2
+ <div v-if="loadIf">
3
3
  <div class="field">
4
4
  <div class="outlined">
5
5
  <div id="cardNumber" ref="cardNumber" class="input"></div>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :style="$styles()" :class="$classes()" v-if="!isInputIgnored">
2
+ <div :style="$styles()" :class="$classes()" v-if="loadIf">
3
3
  <v-text-field v-model="accountNumber" label="Account Number" placeholder="000123456" :variant="variant"
4
4
  :rules="accountNumberRules" :error-messages="errorMessages" @keyup="$onTyping({ duration: 200 })" />
5
5
 
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :style="$styles()" :class="$classes()" v-if="!isInputIgnored">
2
+ <div :style="$styles()" :class="$classes()" v-if="loadIf">
3
3
  <StripeIndividualFields v-if="$classes().includes('individual')" :spec="spec" :on-complete="_retrieveToken"
4
4
  :on-error="_displayError" />
5
5
  <StripeFields v-else :spec="spec" :on-complete="_retrieveToken" :on-error="_displayError" />
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :style="$styles()" :class="$classes()" v-if="!isInputIgnored">
2
+ <div :style="$styles()" :class="$classes()" v-if="loadIf">
3
3
  <v-text-field :color="gcolor" ref="field" v-model="fieldModel" :label="spec.label" :name="fieldName"
4
4
  :placeholder="spec.placeholder" :density="density" :hint="spec.hint" :maxlength="spec.maxLength || 255"
5
5
  :readonly="spec.readOnly" :disabled="inputDisabled" :type="config.type" :rules="rules"
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :style="styles()" :class="classes()" v-if="!isInputIgnored">
2
+ <div :style="styles()" :class="classes()" v-if="loadIf">
3
3
  <v-textarea :color="gcolor" v-model="fieldModel" :label="spec.label" :name="fieldName" :hint="spec.hint"
4
4
  :placeholder="spec.placeholder" :maxlength="spec.maxLength || 255" :readonly="spec.readOnly" :height="height"
5
5
  :rules="$validation()" counter :outlined="$classes().includes('outlined')" :disabled="inputDisabled"
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :style="$styles()" :class="classes()" v-if="!isInputIgnored">
2
+ <div :style="$styles()" :class="classes()" v-if="loadIf">
3
3
  <div v-if="days_mode">
4
4
  <div align="center">
5
5
  <v-row align="center" justify="center">
@@ -1,4 +1,5 @@
1
1
  import { vueApp } from "../../store";
2
+ import { ValidationFactory } from "../validation";
2
3
 
3
4
  export default {
4
5
  methods: {
@@ -24,26 +25,9 @@ export default {
24
25
  $validation(rules) {
25
26
  var augmentedRules = rules || [];
26
27
  Utils.type.ifObject(this.spec.validation, val => {
27
- Utils.type.ifObject(val.required, spec => {
28
- const validationFunc = (v) => {
29
- if (!Utils.type.isNotNull(v)) return spec.message;
30
- if (Array.isArray(v) && v.length <= 0) return spec.message;
31
- if (Utils.type.isString(v) && v.length <= 0) return spec.message;
32
-
33
-
34
- return true;
35
- };
36
- augmentedRules = augmentedRules.concat([validationFunc]);
37
- });
38
- Utils.type.ifObject(val.format, spec => {
39
- augmentedRules = augmentedRules.concat([
40
- v => {
41
- if (v && !v.toString().match(spec.regex)) {
42
- return spec.message;
43
- }
44
- return true;
45
- }
46
- ]);
28
+ Object.keys(val).forEach((key) => {
29
+ const validator = ValidationFactory.getValidator(key, val[key]);
30
+ augmentedRules = augmentedRules.concat([validator.build.bind(validator)]);
47
31
  });
48
32
  });
49
33
  return augmentedRules;
@@ -3,7 +3,7 @@ import { fieldModels, watchFieldModels } from "../composable/conditional";
3
3
  import { dirtySpecs } from "../composable/dirtyState";
4
4
  import { determineColor } from "../../utils/constant";
5
5
  import Action from "../../action";
6
- import { vueApp } from "../../store";
6
+ import { inject, provide } from "vue";
7
7
 
8
8
  const NUMBER_PRECISION = 2;
9
9
  const isNeedToBeFixed = (val, component) => {
@@ -19,12 +19,25 @@ export default {
19
19
  _submitWhenNotDisplayed: !this.spec || !Utils.type.isNotNull(this.spec.submitWhenNotDisplayed) ? false : !!this.spec.submitWhenNotDisplayed,
20
20
  _watchers: [],
21
21
  _isValueChanged: false,
22
- ignoredInputs: new Set(),
23
22
  // Some components do not support null or empty string value, so we need to use an intermediary value.
24
23
  // See https://github.com/vuetifyjs/vuetify/issues/8876
25
24
  vuetifyEmptyString: "<EMPTY_STRING>"
26
25
  };
27
26
  },
27
+ provide() {
28
+ const obj = {};
29
+
30
+ if (!this.spec) return obj;
31
+
32
+ if (this.spec.id) {
33
+ obj['panelContext'] = this;
34
+ }
35
+
36
+ return obj;
37
+ },
38
+ inject: {
39
+ panelContext: { default: null }
40
+ },
28
41
  computed: {
29
42
  fieldName() {
30
43
  if (this.spec && this.spec.name) return this.spec.name;
@@ -32,34 +45,19 @@ export default {
32
45
  inputDisabled() {
33
46
  return this.spec.disabled;
34
47
  },
35
- submitWhenNotDisplayed() {
36
- if (!this.spec) return true;
37
- if (this._show) return true;
38
-
39
- return this._submitWhenNotDisplayed;
40
- },
41
48
  gcolor() {
42
49
  if (!this.spec) return '';
43
50
  return determineColor(this.spec.styleClasses, this.spec.color || 'primary');
44
51
  },
45
- isInputIgnored() {
46
- return vueApp.ignoredInputs.has(this.fieldName) && !this._submitWhenNotDisplayed;
52
+ loadIf() {
53
+ if (this._submitWhenNotDisplayed) return true;
54
+ if (this.panelContext) {
55
+ return this.panelContext._show && this._show;
56
+ }
57
+ return this._show;
47
58
  }
48
59
  },
49
60
  watch: {
50
- _show: {
51
- handler(val) {
52
- this.ignoredInputs.forEach((input) => vueApp.ignoredInputs.delete(input));
53
- this.ignoredInputs.clear();
54
- if (!val && this.$el && this.$el instanceof HTMLElement && !this._submitWhenNotDisplayed) {
55
- Array.from(this.$el.querySelectorAll('input,select,textarea')).forEach((el) => {
56
- this.ignoredInputs.add(el.name);
57
- vueApp.ignoredInputs.add(el.name);
58
- });
59
- }
60
- },
61
- // immediate: true
62
- },
63
61
  fieldModel: function (val) {
64
62
  if (val === this.vuetifyEmptyString) {
65
63
  val = "";
@@ -17,6 +17,7 @@
17
17
  </template>
18
18
 
19
19
  <script>
20
+ import { popovers } from "../store";
20
21
  import bus from "../utils/eventBus";
21
22
 
22
23
  export default {
@@ -30,6 +31,9 @@ export default {
30
31
  floatingSpec: {}
31
32
  };
32
33
  },
34
+ mounted() {
35
+ popovers.value.push(this);
36
+ },
33
37
  methods: {
34
38
  $mounted() {
35
39
  bus.$once(`popover/close-${this.spec.key}`, (options) => {
@@ -37,7 +41,7 @@ export default {
37
41
  this.toggle = false;
38
42
  });
39
43
  document.addEventListener('click', this.handleClose);
40
- window.addEventListener('resize', () => this.toggle = false);
44
+ window.addEventListener('resize', () => this.close());
41
45
  },
42
46
  handleClose(event) {
43
47
  let element = null;
@@ -49,8 +53,12 @@ export default {
49
53
  const isClickInside = element.contains(event.target);
50
54
 
51
55
  if (!isClickInside) {
52
- this.toggle = false;
56
+ this.close();
53
57
  }
58
+ },
59
+ close() {
60
+ this.toggle = false;
61
+ popovers.value.remove(this);
54
62
  }
55
63
  }
56
64
  };
@@ -0,0 +1,179 @@
1
+ class AbstractValidator {
2
+ constructor(validationOptions) {
3
+ this.validationOptions = validationOptions;
4
+ }
5
+
6
+ build(model) {
7
+ return true;
8
+ }
9
+
10
+ isAllowBlank() {
11
+ return !!this.validationOptions.allow_blank || !!this.validationOptions.allow_nil;
12
+ }
13
+ }
14
+
15
+ function isPresent(value) {
16
+ if (value == undefined) return false;
17
+ if (value == null) return false;
18
+ if (Array.isArray(value) && value.length <= 0) return false;
19
+ if (typeof value == 'string' && value.length <= 0) return false;
20
+ if (typeof value == 'object' && Object.keys(value).length <= 0) return false;
21
+
22
+ return true;
23
+ }
24
+
25
+ class PresenceValidator extends AbstractValidator {
26
+ build(model) {
27
+ return isPresent(model) ? true : this.validationOptions.message;
28
+ }
29
+ }
30
+ class RequiredValidator extends PresenceValidator { }
31
+
32
+ class AbsenceValidator extends AbstractValidator {
33
+ build(model) {
34
+ return !isPresent(model) ? true : this.validationOptions.message;
35
+ }
36
+ }
37
+
38
+ class AcceptanceValidator extends AbstractValidator {
39
+ build(model) {
40
+ if (this.isAllowBlank() && !isPresent(model)) return true;
41
+
42
+ const { accept } = this.validationOptions;
43
+ return accept.includes(model) ? true : this.validationOptions.message;
44
+ }
45
+ }
46
+
47
+ class NumericalityValidator extends AbstractValidator {
48
+ build(model) {
49
+ if (this.isAllowBlank() && !isPresent(model)) return true;
50
+
51
+ let result = true;
52
+ ['greater_than', 'greater_than_or_equal_to', 'equal_to', 'less_than', 'less_than_or_equal_to', 'other_than', 'odd', 'even'].forEach((key) => {
53
+ let count = this.validationOptions[key];
54
+ if (!isPresent(count)) return;
55
+ if (this.validate(key, count, model)) return;
56
+
57
+ result = this.errorMessage(key, count);
58
+ });
59
+
60
+ return result;
61
+ }
62
+
63
+ validate(key, count, value) {
64
+ const obj = {
65
+ 'greater_than': () => count < value,
66
+ 'greater_than_or_equal_to': () => count <= value,
67
+ 'equal_to': () => count == value,
68
+ 'less_than': () => count > value,
69
+ 'less_than_or_equal_to': () => count >= value,
70
+ 'other_than': () => count != value,
71
+ 'in': () => count.includes(value),
72
+ 'odd': () => value % 2 == 1,
73
+ 'even': () => value % 2 == 0
74
+ };
75
+
76
+ return obj[key]();
77
+ }
78
+
79
+ errorMessage(key, count) {
80
+ const message = this.validationOptions.message;
81
+ if (typeof message == 'string') return message;
82
+
83
+ return message[key].replace('%{count}', count);
84
+ }
85
+ }
86
+
87
+ class FormatValidator extends AbstractValidator {
88
+ build(model) {
89
+ if (this.isAllowBlank() && !isPresent(model)) return true;
90
+
91
+ const regex = this.validationOptions.with || this.validationOptions.without || this.validationOptions.regex;
92
+ let match = !!(model || '').match(regex);
93
+ match = this.isInverse() ? !match : match;
94
+
95
+ return match ? true : this.validationOptions.message;
96
+ }
97
+
98
+ isInverse() {
99
+ return !!this.validationOptions.without;
100
+ }
101
+ }
102
+
103
+ class InclusionValidator extends AbstractValidator {
104
+ build(model) {
105
+ if (this.isAllowBlank() && !isPresent(model)) return true;
106
+
107
+ const within = this.validationOptions.in || this.validationOptions.within;
108
+
109
+ return within.includes(model) ? true : this.validationOptions.message;
110
+ }
111
+ }
112
+
113
+ class ExclusionValidator extends AbstractValidator {
114
+ build(model) {
115
+ if (this.isAllowBlank() && !isPresent(model)) return true;
116
+
117
+ const within = this.validationOptions.in || this.validationOptions.within;
118
+
119
+ return !within.includes(model) ? true : this.validationOptions.message;
120
+ }
121
+ }
122
+
123
+ class LengthValidator extends AbsenceValidator {
124
+ build(model) {
125
+ model ||= '';
126
+ if (this.isAllowBlank() && !isPresent(model)) return true;
127
+
128
+ const { minimum, maximum } = this.validationOptions;
129
+ let result;
130
+ if (isPresent(minimum)) {
131
+ result = model.length > minimum ? true : this.errorMessage('too_short', minimum);
132
+ }
133
+ if (result !== true) return result;
134
+ if (isPresent(maximum)) {
135
+ result = model.length < maximum ? true : this.errorMessage('too_long', maximum);
136
+ }
137
+ if (result !== true) return result;
138
+ if (isPresent(this.validationOptions.is)) {
139
+ result = model.length === this.validationOptions.is ? true : this.errorMessage('wrong_length', this.validationOptions.is);
140
+ }
141
+ if (result !== true) return result;
142
+ if (isPresent(this.validationOptions.in)) {
143
+ result = (model.length >= this.validationOptions.is || model.length <= this.validatorOptions.is) ? true : this.errorMessage('wrong_length', this.validationOptions.in);
144
+ }
145
+
146
+ return result;
147
+ }
148
+
149
+ errorMessage(key, count) {
150
+ const message = this.validationOptions.message;
151
+ const singular = count <= 1;
152
+ if (typeof message == 'string') {
153
+ return message.replace('%{count}', count);
154
+ }
155
+
156
+ return message[key][singular ? 'one' : 'other'].replace('%{count}', count);
157
+ }
158
+ }
159
+
160
+ const clsMap = {
161
+ 'absence': AbsenceValidator,
162
+ 'presence': PresenceValidator,
163
+ 'required': RequiredValidator,
164
+ 'acceptance': AcceptanceValidator,
165
+ 'numericality': NumericalityValidator,
166
+ 'format': FormatValidator,
167
+ 'inclusion': InclusionValidator,
168
+ 'exclusion': ExclusionValidator,
169
+ 'length': LengthValidator
170
+ };
171
+
172
+ class ValidationFactory {
173
+ static getValidator(validatorName, validatorOptions) {
174
+ return new clsMap[validatorName](validatorOptions);
175
+ }
176
+ }
177
+
178
+ export { ValidationFactory }
179
+
@@ -8,5 +8,21 @@ describe('display', () => {
8
8
  cy.contains('submit (if form valid)').should('have.class', 'v-btn--disabled')
9
9
  cy.contains('hide select').click()
10
10
  cy.contains('submit (if form valid)').should('not.have.class', 'v-btn--disabled')
11
+ cy.contains('submit').click()
12
+
13
+ const result = `Method: POST
14
+ Form Data:
15
+ {
16
+ "date": "2024-07-24",
17
+ "chip_group": "option2",
18
+ "radio_group": "option3",
19
+ "check_group": "option1",
20
+ "text": "John Doe",
21
+ "textarea": "Lorem ipsum et dumet bla bla bla..."
22
+ }`
23
+
24
+ cy.get('.unformatted').should('contain.text', result)
11
25
  })
26
+
27
+
12
28
  })
package/nav/dialog.vue CHANGED
@@ -102,7 +102,7 @@ export default {
102
102
  return null;
103
103
  },
104
104
  disableCloseButton() {
105
- return this.spec.disableCloseButton || Utils.type.isString(this.url);
105
+ return this.spec.disableCloseButton;
106
106
  }
107
107
  },
108
108
  mounted() {
@@ -203,7 +203,6 @@ export default {
203
203
  this.header = response.header;
204
204
  this.body = response.body;
205
205
  this.footer = response.footer;
206
- this.spec.disableCloseButton = response.disableCloseButton;
207
206
 
208
207
  // Make sure action only executes after the content has finished populating.
209
208
  this.$nextTick(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glib-web",
3
- "version": "4.1.2",
3
+ "version": "4.2.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/store.js CHANGED
@@ -18,7 +18,6 @@ export const vueApp = reactive({
18
18
  richTextValues: {},
19
19
  draggedComponent: null,
20
20
  lastNavigationCount: null,
21
- ignoredInputs: new Set(),
22
21
  tooltipSpec: {},
23
22
  bottomBanners: {},
24
23
  uploader: {},
@@ -28,12 +27,18 @@ export const vueApp = reactive({
28
27
  export const jsonView = reactive({ page: window.__page });
29
28
 
30
29
  export const dialogs = ref([]);
30
+ export const popovers = ref([]);
31
31
 
32
32
  export const closeAllDialog = () => {
33
33
  dialogs.value.forEach((dialog) => dialog.model = false);
34
34
  dialogs.value = [];
35
35
  };
36
36
 
37
+ export const closeAllPopover = () => {
38
+ popovers.value.forEach((popover) => popover.toggle = false);
39
+ popovers.value = [];
40
+ };
41
+
37
42
  export const ctx = () => dialogs.value.slice(-1)[0] || vueApp;
38
43
 
39
44
  export const glibevent = reactive({
@@ -43,7 +48,9 @@ export const glibevent = reactive({
43
48
  });
44
49
 
45
50
  function glibEventHandler(e) {
46
- vueApp.ignoredInputs = new Set();
51
+
52
+ closeAllDialog();
53
+ closeAllPopover();
47
54
 
48
55
  const { onUnload } = jsonView.page;
49
56
  if (onUnload) {
@@ -27,7 +27,9 @@ class GlibDirectUpload extends DirectUpload {
27
27
  super(file, url, delegate, customHeaders);
28
28
  this.prefix = prefix;
29
29
  this.metadata = { custom: metadata };
30
- this.tagging = tagging;
30
+
31
+ if (tagging && typeof tagging == 'string') this.tagging = tagging;
32
+ else if (tagging && typeof tagging == 'object') this.tagging = new URLSearchParams(tagging).toString();
31
33
  }
32
34
 
33
35
  create(callback) {
package/utils/history.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { nextTick } from "vue";
2
- import { closeAllDialog, jsonView, vueApp } from "../store";
2
+ import { closeAllDialog, closeAllPopover, jsonView, vueApp } from "../store";
3
3
  import { useDirtyState } from "../components/composable/dirtyState";
4
4
 
5
5
  const { isDirty, clearDirtyState, updateDirtyState } = useDirtyState();
@@ -70,6 +70,7 @@ export default class {
70
70
  }
71
71
 
72
72
  closeAllDialog();
73
+ closeAllPopover();
73
74
 
74
75
  // Save scroll position of the current page when navigating through back/forward button
75
76
  this.bodyScrollTops[this.navigationCount] = this._pageBody.scrollTop;
@@ -0,0 +1,79 @@
1
+ import { createApp, h, nextTick } from "vue";
2
+ import { dialogs } from "../../store";
3
+ import Dialog from "../../nav/dialog.vue";
4
+ import { Vue } from "../..";
5
+ import Action from "../../action";
6
+
7
+ export default class LaunchDialog {
8
+ static closestBody(component) {
9
+ const element = component.$el instanceof HTMLElement ? component.$el : component.$el.nextElementSibling;
10
+ const dialog = element.closest(".v-dialog");
11
+ if (dialog) {
12
+ return dialog.querySelector(".dialogs-body");
13
+ }
14
+ return null;
15
+ }
16
+
17
+
18
+ static topDialog() {
19
+ return dialogs.value.last();
20
+ }
21
+
22
+ static open(properties, component) {
23
+
24
+ // https://css-tricks.com/creating-vue-js-component-instances-programmatically/
25
+ const props = {
26
+ spec: properties
27
+ };
28
+
29
+ const instance = createApp(
30
+ {
31
+ render: function () {
32
+ return h(Dialog, props);
33
+ }
34
+ }
35
+ );
36
+ Object.assign(instance._context, Vue._context);
37
+
38
+ if (component) {
39
+ const placeholder = document.createElement('div');
40
+ document.body.appendChild(placeholder);
41
+ instance.mount(placeholder);
42
+ } else {
43
+ console.error("A dialog has to be displayed in a component");
44
+ }
45
+ }
46
+
47
+ static reload(properties, component) {
48
+ Utils.type.ifObject(dialogs.value.last(), dialog => {
49
+ dialog.reload(properties);
50
+ });
51
+ }
52
+
53
+ static close(properties, component) {
54
+ Utils.type.ifObject(dialogs.value.last(), dialog => {
55
+ dialog.close();
56
+ });
57
+
58
+ nextTick(() => {
59
+ Action.execute(properties.onClose, component);
60
+ });
61
+ }
62
+
63
+ // This is only meant to be used internally
64
+ static alert(message, component) {
65
+ const properties = {
66
+ message: message
67
+ };
68
+ const spec = Object.assign({}, properties, {
69
+ disableCloseButton: true,
70
+ buttons: [
71
+ {
72
+ text: "OK",
73
+ onClick: properties.onClose
74
+ }
75
+ ]
76
+ });
77
+ this.open(spec, component);
78
+ }
79
+ }
@@ -0,0 +1,62 @@
1
+ import { createApp, h } from "vue";
2
+ import Popover from "../../components/popover.vue";
3
+ import { Vue } from "../..";
4
+ import { autoUpdate, computePosition, flip, shift, offset } from "@floating-ui/dom";
5
+
6
+ import bus from "../eventBus";
7
+
8
+ export default class LaunchPopover {
9
+ static open(properties, component) {
10
+ const instance = createApp(
11
+ {
12
+ render: function () {
13
+ return h(Popover, { spec: properties });
14
+ }
15
+ }
16
+ );
17
+ Object.assign(instance._context, Vue._context);
18
+
19
+ if (component) {
20
+ const placeholder = document.createElement('div');
21
+
22
+ // const pageBody = Utils.launch.dialog.closestBody(component) || Utils.history._pageBody;
23
+ const dialogBody = Utils.launch.dialog.closestBody(component);
24
+ if (dialogBody) {
25
+ // Put the popover on the dialog so that clicking it doesn't close a non-persistent dialog.
26
+ dialogBody.appendChild(placeholder);
27
+ } else {
28
+ // Attach the popover to the document body to prevent clipping caused by the parent's `overflow` property.
29
+ // See https://stackoverflow.com/questions/51393427/z-index-to-ignore-max-height-and-always-come-on-top
30
+ document.body.appendChild(placeholder);
31
+ }
32
+
33
+ instance.mount(placeholder);
34
+
35
+ const reference = component.$el instanceof HTMLElement ? component.$el : component.$el.nextElementSibling;
36
+
37
+ const floating = placeholder;
38
+ const placement = properties.placement || 'right';
39
+ const offsetSize = properties.offset || 8;
40
+
41
+ // These properties need to be set to get an accurate calculation. See https://floating-ui.com/docs/computeposition
42
+ Object.assign(placeholder.style, { position: 'absolute', display: 'block', top: 0, left: 0 });
43
+
44
+ autoUpdate(reference, floating, () => {
45
+ computePosition(reference, floating, { placement: placement, middleware: [flip(), shift({ padding: 4 }), offset(offsetSize)] })
46
+ .then(({ x, y }) => {
47
+ // zIndex should be larger than dialog's, which is 2400.
48
+ Object.assign(placeholder.style, { top: `${y}px`, left: `${x}px`, zIndex: 2500 });
49
+ });
50
+ });
51
+
52
+ } else {
53
+ console.error("A popover has to be displayed in a component");
54
+ }
55
+
56
+ return instance;
57
+ }
58
+
59
+ static close(properties) {
60
+ bus.$emit(`popover/close-${properties.key}`, properties);
61
+ }
62
+ }
@@ -0,0 +1,43 @@
1
+ import { createApp, h } from "vue";
2
+ import Sheet from "../../nav/sheet.vue";
3
+ import { Vue } from "../..";
4
+
5
+ export default class LaunchSheet {
6
+ static open(properties, component) {
7
+ const instance = createApp(
8
+ {
9
+ render: function () {
10
+ return h(Sheet, { spec: properties });
11
+ }
12
+ }
13
+ );
14
+ Object.assign(instance._context, Vue._context);
15
+
16
+ if (component) {
17
+ const placeholder = document.createElement('div');
18
+ document.body.appendChild(placeholder);
19
+ instance.mount(placeholder);
20
+ } else {
21
+ console.error("A sheet has to be displayed in a component");
22
+ }
23
+
24
+ return instance;
25
+ }
26
+
27
+ static confirm(properties, onConfirm, component) {
28
+ const spec = Object.assign({}, properties, {
29
+ buttons: [
30
+ {
31
+ text: "Confirm",
32
+ onClickFunction: onConfirm
33
+ }
34
+ ]
35
+ });
36
+ this.open(spec, component);
37
+ }
38
+
39
+ static alert(message, buttons, component) {
40
+ const spec = { message: message, buttons: buttons };
41
+ this.open(spec, component);
42
+ }
43
+ }
@@ -0,0 +1,36 @@
1
+ import { createApp, h } from "vue";
2
+ import Snackbar from "../../nav/snackbar.vue";
3
+ import { Vue } from "../..";
4
+
5
+ export default class LaunchSnackbar {
6
+ static open(properties, component) {
7
+ const instance = createApp(
8
+ {
9
+ render: function () {
10
+ return h(Snackbar, { spec: properties });
11
+ }
12
+ }
13
+ );
14
+ Object.assign(instance._context, Vue._context);
15
+
16
+ if (component) {
17
+ const placeholder = document.createElement('div');
18
+ document.body.appendChild(placeholder);
19
+ instance.mount(placeholder);
20
+ instance.show = true;
21
+ } else {
22
+ console.error("A Snackbar has to be displayed in a component");
23
+ }
24
+
25
+ return instance;
26
+ }
27
+
28
+ static error(message, component) {
29
+ this.open({ message: message, styleClasses: ["error"] }, component);
30
+ }
31
+
32
+ static indicator(message, component) {
33
+ const instance = this.open({ message: message, timeout: -1 }, component);
34
+ instance.indicator = true;
35
+ }
36
+ }
package/utils/launch.js CHANGED
@@ -1,14 +1,7 @@
1
- import { Vue } from "../index";
2
- import Dialog from "../nav/dialog.vue";
3
- import Sheet from "../nav/sheet.vue";
4
- import Snackbar from "../nav/snackbar.vue";
5
- import Popover from "../components/popover.vue";
6
- import { computePosition, flip, offset } from '@floating-ui/dom';
7
- import bus from "./eventBus";
8
- import { nextTick } from "vue";
9
-
10
- import { createApp, h } from "vue";
11
- import { dialogs } from "../store";
1
+ import LaunchDialog from "./launch/dialog";
2
+ import LaunchPopover from "./launch/popover";
3
+ import LaunchSheet from "./launch/sheet";
4
+ import LaunchSnackbar from "./launch/snackbar";
12
5
 
13
6
  export default class {
14
7
  static get dialog() {
@@ -27,203 +20,3 @@ export default class {
27
20
  return LaunchPopover;
28
21
  }
29
22
  }
30
-
31
- class LaunchPopover {
32
- static open(properties, component) {
33
- const instance = createApp(
34
- {
35
- render: function () {
36
- return h(Popover, { spec: properties });
37
- }
38
- }
39
- );
40
- Object.assign(instance._context, Vue._context);
41
-
42
- if (component) {
43
- const placeholder = document.createElement('div');
44
-
45
- // const pageBody = Utils.launch.dialog.closestBody(component) || Utils.history._pageBody;
46
- const dialogBody = Utils.launch.dialog.closestBody(component);
47
- if (dialogBody) {
48
- // Put the popover on the dialog so that clicking it doesn't close a non-persistent dialog.
49
- dialogBody.appendChild(placeholder);
50
- } else {
51
- // Attach the popover to the document body to prevent clipping caused by the parent's `overflow` property.
52
- // See https://stackoverflow.com/questions/51393427/z-index-to-ignore-max-height-and-always-come-on-top
53
- document.body.appendChild(placeholder);
54
- }
55
-
56
- instance.mount(placeholder);
57
-
58
- const reference = component.$el instanceof HTMLElement ? component.$el : component.$el.nextElementSibling;
59
-
60
- const floating = placeholder;
61
- const placement = properties.placement || 'right';
62
- const offsetSize = properties.offset || 8;
63
-
64
- // These properties need to be set to get an accurate calculation. See https://floating-ui.com/docs/computeposition
65
- Object.assign(placeholder.style, { position: 'absolute', display: 'block', top: 0, left: 0 });
66
-
67
- computePosition(reference, floating, { placement: placement, middleware: [flip(), offset(offsetSize)] })
68
- .then(({ x, y }) => {
69
- // zIndex should be larger than dialog's, which is 2400.
70
- Object.assign(placeholder.style, { top: `${y}px`, left: `${x}px`, zIndex: 2500 });
71
- });
72
- } else {
73
- console.error("A popover has to be displayed in a component");
74
- }
75
-
76
- return instance;
77
- }
78
-
79
- static close(properties) {
80
- bus.$emit(`popover/close-${properties.key}`, properties);
81
- }
82
- }
83
-
84
- class LaunchDialog {
85
- static closestBody(component) {
86
- const element = component.$el instanceof HTMLElement ? component.$el : component.$el.nextElementSibling;
87
- const dialog = element.closest(".v-dialog");
88
- if (dialog) {
89
- return dialog.querySelector(".dialogs-body");
90
- }
91
- return null;
92
- }
93
-
94
-
95
- static topDialog() {
96
- return dialogs.value.last();
97
- }
98
-
99
- static open(properties, component) {
100
-
101
- // https://css-tricks.com/creating-vue-js-component-instances-programmatically/
102
- const props = {
103
- spec: properties
104
- };
105
-
106
- const instance = createApp(
107
- {
108
- render: function () {
109
- return h(Dialog, props);
110
- }
111
- }
112
- );
113
- Object.assign(instance._context, Vue._context);
114
-
115
- if (component) {
116
- const placeholder = document.createElement('div');
117
- document.body.appendChild(placeholder);
118
- instance.mount(placeholder);
119
- } else {
120
- console.error("A dialog has to be displayed in a component");
121
- }
122
- }
123
-
124
- static reload(properties, component) {
125
- Utils.type.ifObject(dialogs.value.last(), dialog => {
126
- dialog.reload(properties);
127
- });
128
- }
129
-
130
- static close(properties, component) {
131
- Utils.type.ifObject(dialogs.value.last(), dialog => {
132
- dialog.close();
133
- });
134
-
135
- nextTick(() => {
136
- Action.execute(properties.onClose, component);
137
- });
138
- }
139
-
140
- // This is only meant to be used internally
141
- static alert(message, component) {
142
- const properties = {
143
- message: message
144
- };
145
- const spec = Object.assign({}, properties, {
146
- disableCloseButton: true,
147
- buttons: [
148
- {
149
- text: "OK",
150
- onClick: properties.onClose
151
- }
152
- ]
153
- });
154
- this.open(spec, component);
155
- }
156
- }
157
-
158
- class LaunchSheet {
159
- static open(properties, component) {
160
- const instance = createApp(
161
- {
162
- render: function () {
163
- return h(Sheet, { spec: properties });
164
- }
165
- }
166
- );
167
- Object.assign(instance._context, Vue._context);
168
-
169
- if (component) {
170
- const placeholder = document.createElement('div');
171
- document.body.appendChild(placeholder);
172
- instance.mount(placeholder);
173
- } else {
174
- console.error("A sheet has to be displayed in a component");
175
- }
176
-
177
- return instance;
178
- }
179
-
180
- static confirm(properties, onConfirm, component) {
181
- const spec = Object.assign({}, properties, {
182
- buttons: [
183
- {
184
- text: "Confirm",
185
- onClickFunction: onConfirm
186
- }
187
- ]
188
- });
189
- this.open(spec, component);
190
- }
191
-
192
- static alert(message, buttons, component) {
193
- const spec = { message: message, buttons: buttons };
194
- this.open(spec, component);
195
- }
196
- }
197
-
198
- class LaunchSnackbar {
199
- static open(properties, component) {
200
- const instance = createApp(
201
- {
202
- render: function () {
203
- return h(Snackbar, { spec: properties });
204
- }
205
- }
206
- );
207
- Object.assign(instance._context, Vue._context);
208
-
209
- if (component) {
210
- const placeholder = document.createElement('div');
211
- document.body.appendChild(placeholder);
212
- instance.mount(placeholder);
213
- instance.show = true;
214
- } else {
215
- console.error("A Snackbar has to be displayed in a component");
216
- }
217
-
218
- return instance;
219
- }
220
-
221
- static error(message, component) {
222
- this.open({ message: message, styleClasses: ["error"] }, component);
223
- }
224
-
225
- static indicator(message, component) {
226
- const instance = this.open({ message: message, timeout: -1 }, component);
227
- instance.indicator = true;
228
- }
229
- }