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.
- package/actions/dialogs/show.js +1 -0
- package/actions/logics/set.js +5 -2
- package/components/button.vue +1 -1
- package/components/composable/upload.js +1 -1
- package/components/fields/_patternText.vue +1 -1
- package/components/fields/_select.vue +1 -1
- package/components/fields/check.vue +1 -1
- package/components/fields/checkGroup.vue +2 -2
- package/components/fields/chipGroup.vue +1 -1
- package/components/fields/creditCard.vue +1 -1
- package/components/fields/dynamicGroup.vue +1 -1
- package/components/fields/dynamicSelect.vue +1 -1
- package/components/fields/file.vue +1 -1
- package/components/fields/googlePlace.vue +1 -1
- package/components/fields/hidden.vue +1 -1
- package/components/fields/location.vue +1 -1
- package/components/fields/multiUpload.vue +24 -16
- package/components/fields/otpField.vue +1 -1
- package/components/fields/phone.vue +1 -1
- package/components/fields/radio.vue +1 -1
- package/components/fields/radioGroup.vue +1 -1
- package/components/fields/rating.vue +1 -1
- package/components/fields/richText.vue +1 -1
- package/components/fields/sign.vue +2 -2
- package/components/fields/stripe/stripeFields.vue +1 -1
- package/components/fields/stripe/stripeIndividualFields.vue +1 -1
- package/components/fields/stripeExternalAccount.vue +1 -1
- package/components/fields/stripeToken.vue +1 -1
- package/components/fields/text.vue +1 -1
- package/components/fields/textarea.vue +1 -1
- package/components/fields/timer.vue +1 -1
- package/components/mixins/generic.js +4 -20
- package/components/mixins/styles.js +21 -23
- package/components/popover.vue +10 -2
- package/components/validation.js +179 -0
- package/cypress/e2e/glib-web/display.cy.ts +16 -0
- package/nav/dialog.vue +1 -2
- package/package.json +1 -1
- package/store.js +9 -2
- package/utils/glibDirectUpload.js +3 -1
- package/utils/history.js +2 -1
- package/utils/launch/dialog.js +79 -0
- package/utils/launch/popover.js +62 -0
- package/utils/launch/sheet.js +43 -0
- package/utils/launch/snackbar.js +36 -0
- package/utils/launch.js +4 -211
package/actions/dialogs/show.js
CHANGED
package/actions/logics/set.js
CHANGED
|
@@ -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);
|
package/components/button.vue
CHANGED
|
@@ -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
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div :style="$styles()" :class="classes()" v-if="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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
|
-
<div :style="$styles()" :class="$classes()" v-if="
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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
|
|
47
|
+
import { ref, computed } from "vue";
|
|
48
48
|
import * as uploader from "../composable/upload";
|
|
49
49
|
import { useFileUtils } from "../composable/file";
|
|
50
50
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div :style="$styles()" :class="$classes()" v-if="
|
|
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="
|
|
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="
|
|
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="
|
|
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,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
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
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 {
|
|
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
|
-
|
|
46
|
-
|
|
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 = "";
|
package/components/popover.vue
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
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
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
|
-
|
|
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
|
-
|
|
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
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
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
|
-
}
|