glib-web 6.0.3 → 6.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/action.js +2 -0
- package/actions/lists/remove.js +23 -0
- package/app.scss +14 -1
- package/components/_internal_button.vue +2 -44
- package/components/_internal_icon.vue +5 -10
- package/components/composable/listAutoload.js +32 -0
- package/components/composable/tableAutoload.js +33 -0
- package/components/fab.vue +19 -9
- package/components/fields/_patternText.vue +100 -19
- package/components/fields/_select.vue +8 -1
- package/components/fields/text.vue +12 -1
- package/components/mixins/buttonProps.js +30 -0
- package/components/mixins/longClick.js +13 -7
- package/components/mixins/scrolling.js +2 -2
- package/components/mixins/styles.js +8 -0
- package/components/panels/custom.vue +10 -7
- package/components/panels/list.vue +42 -10
- package/components/panels/table.vue +1 -76
- package/components/validation.js +24 -19
- package/cypress/e2e/glib-web/fields.cy.js +29 -0
- package/cypress/e2e/glib-web/fieldsDateTime.cy.js +67 -0
- package/cypress/e2e/glib-web/fieldsSelect.cy.js +48 -0
- package/cypress/e2e/glib-web/list.cy.js +103 -10
- package/cypress/support/component.js +1 -1
- package/cypress/support/e2e.js +1 -1
- package/package.json +1 -1
- package/plugins/vuetify.js +2 -1
- package/templates/comment.vue +8 -0
- package/templates/editable.vue +6 -1
- package/templates/featured.vue +7 -0
- package/templates/thumbnail.vue +8 -2
- package/utils/constant.js +9 -1
- package/components/composable/tableExport.js +0 -84
- package/components/composable/tableImport.js +0 -163
- package/cypress/e2e/glib-web/listsAppend.cy.js +0 -27
package/action.js
CHANGED
|
@@ -81,6 +81,7 @@ import ActionLogicsSet from "./actions/logics/set";
|
|
|
81
81
|
import ActionLogicsRun from "./actions/logics/run";
|
|
82
82
|
|
|
83
83
|
import ActionListsAppend from "./actions/lists/append";
|
|
84
|
+
import ActionListsRemove from "./actions/lists/remove";
|
|
84
85
|
|
|
85
86
|
import ActionBottomBannersOpen from "./actions/bottom_banners/open";
|
|
86
87
|
import ActionBottomBannersClose from "./actions/bottom_banners/close";
|
|
@@ -182,6 +183,7 @@ const actions = {
|
|
|
182
183
|
"logics/run": ActionLogicsRun,
|
|
183
184
|
|
|
184
185
|
"lists/append": ActionListsAppend,
|
|
186
|
+
"lists/remove": ActionListsRemove,
|
|
185
187
|
|
|
186
188
|
// deprecated
|
|
187
189
|
"bottomBanners/open": ActionBottomBannersOpen,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { nextTick } from "vue";
|
|
2
|
+
import { realComponent } from "../../components/helper";
|
|
3
|
+
|
|
4
|
+
export default class {
|
|
5
|
+
execute(spec, component) {
|
|
6
|
+
let target = component;
|
|
7
|
+
|
|
8
|
+
if (spec.targetId) {
|
|
9
|
+
target = realComponent(GLib.component.findById(spec.targetId));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (!target) {
|
|
13
|
+
console.warn("Component ID not found", spec.targetId);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
target.action_removeRow(spec.rowId);
|
|
18
|
+
|
|
19
|
+
nextTick(() => {
|
|
20
|
+
GLib.action.execute(spec.onRemove, component);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
package/app.scss
CHANGED
|
@@ -188,4 +188,17 @@ blockquote, pre, ol, ul, dd {
|
|
|
188
188
|
text-align: center;
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
|
-
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// A checkbox whose label carries a tooltip gets a "hover me for more info"
|
|
194
|
+
// affordance (dotted underline + help cursor). Scoped to `.has-tooltip` so plain
|
|
195
|
+
// checkboxes — the vast majority — render with no decoration. The marker class is
|
|
196
|
+
// emitted by `$classes()` whenever the spec has a `tooltip`.
|
|
197
|
+
.fields-check.has-tooltip {
|
|
198
|
+
.v-label {
|
|
199
|
+
text-decoration: underline;
|
|
200
|
+
text-decoration-style: dotted;
|
|
201
|
+
text-underline-offset: 0.15em;
|
|
202
|
+
cursor: help;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -13,33 +13,18 @@
|
|
|
13
13
|
</template>
|
|
14
14
|
|
|
15
15
|
<script>
|
|
16
|
-
import { determineColor, determineDensity, determineVariant, determineSize } from '../utils/constant';
|
|
17
16
|
import GlibBase from "./base/glibBase.js";
|
|
18
|
-
|
|
17
|
+
import ButtonProps from "./mixins/buttonProps.js";
|
|
19
18
|
|
|
20
19
|
export default {
|
|
21
20
|
extends: GlibBase,
|
|
21
|
+
mixins: [ButtonProps],
|
|
22
22
|
props: {
|
|
23
23
|
spec: { type: Object, required: true },
|
|
24
24
|
type: { type: String, default: "button" },
|
|
25
25
|
hideTextOnXs: { type: Boolean },
|
|
26
26
|
eventHandlers: { type: Object, default: null }
|
|
27
27
|
},
|
|
28
|
-
computed: {
|
|
29
|
-
color() {
|
|
30
|
-
// return this.spec.color
|
|
31
|
-
return this.spec.color || determineColor(this.spec.styleClasses);
|
|
32
|
-
},
|
|
33
|
-
density() {
|
|
34
|
-
return determineDensity(this.spec.styleClasses);
|
|
35
|
-
},
|
|
36
|
-
size() {
|
|
37
|
-
return determineSize(this.spec.styleClasses);
|
|
38
|
-
},
|
|
39
|
-
variant() {
|
|
40
|
-
return determineVariant(this.spec.styleClasses, 'elevated');
|
|
41
|
-
}
|
|
42
|
-
},
|
|
43
28
|
methods: {
|
|
44
29
|
onClick(e) {
|
|
45
30
|
if (this.type == 'submit') {
|
|
@@ -49,33 +34,6 @@ export default {
|
|
|
49
34
|
e.stopPropagation(); // prevent event bubbling
|
|
50
35
|
|
|
51
36
|
this.$onClick();
|
|
52
|
-
},
|
|
53
|
-
// $ready() {
|
|
54
|
-
// this.$type.ifArray(this.spec.styleClasses, val => {
|
|
55
|
-
// this.linkStyling = val.includes("link");
|
|
56
|
-
// });
|
|
57
|
-
|
|
58
|
-
// // const vm = this
|
|
59
|
-
// // variants.forEach((value) => {
|
|
60
|
-
// // if (vm.$classes().includes(value)) {
|
|
61
|
-
// // vm.variant = value
|
|
62
|
-
// // }
|
|
63
|
-
// // })
|
|
64
|
-
// },
|
|
65
|
-
styles() {
|
|
66
|
-
const styles = this.$styles();
|
|
67
|
-
// if (this.linkStyling) {
|
|
68
|
-
if (this.$classes().includes('link')) {
|
|
69
|
-
styles["color"] = styles["color"] || "#1976d2";
|
|
70
|
-
}
|
|
71
|
-
return styles;
|
|
72
|
-
},
|
|
73
|
-
// classes() {
|
|
74
|
-
// const classes = this.$classes();
|
|
75
|
-
// return classes;
|
|
76
|
-
// },
|
|
77
|
-
$registryEnabled() {
|
|
78
|
-
return false;
|
|
79
37
|
}
|
|
80
38
|
}
|
|
81
39
|
};
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<script>
|
|
14
14
|
import GlibBase from "./base/glibBase.js";
|
|
15
15
|
import { vueApp } from "../store";
|
|
16
|
-
import { determineColor } from '../utils/constant';
|
|
16
|
+
import { determineColor, resolveIconName } from '../utils/constant';
|
|
17
17
|
import { computed } from "vue";
|
|
18
18
|
|
|
19
19
|
export default {
|
|
@@ -44,15 +44,10 @@ export default {
|
|
|
44
44
|
return this.isBusy ? "autorenew" : this.name;
|
|
45
45
|
},
|
|
46
46
|
icon() {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
} else {
|
|
52
|
-
// Fallback needed when logics_set clears/modifies the spec in ways that remove icon properties.
|
|
53
|
-
// Without this check, "Cannot read properties of undefined (reading 'name')" error occurs.
|
|
54
|
-
return { name: '', size: null };
|
|
55
|
-
}
|
|
47
|
+
return {
|
|
48
|
+
name: resolveIconName(this.spec) || '',
|
|
49
|
+
size: this.spec.fa?.size ?? this.spec.material?.size ?? null
|
|
50
|
+
};
|
|
56
51
|
}
|
|
57
52
|
}
|
|
58
53
|
};
|
|
@@ -33,11 +33,41 @@ function useListAutoload() {
|
|
|
33
33
|
return proxy.$onVisibilityChange(el, callback);
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
// When the list has a fixed height it scrolls internally; those `scroll` events
|
|
37
|
+
// don't bubble to `#page_body`, so the viewport-change listeners alone never
|
|
38
|
+
// re-evaluate the anchor visibility. Attach the handler to the list's own
|
|
39
|
+
// scroll container (the nearest scrollable ancestor of the anchor) as well.
|
|
40
|
+
const scrollContainer = () => {
|
|
41
|
+
let el = bottomAnchorEl.value || topAnchorEl.value;
|
|
42
|
+
while (el && el !== document.body && el.nodeType === 1) {
|
|
43
|
+
const overflowY = window.getComputedStyle(el).overflowY;
|
|
44
|
+
if (overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay") {
|
|
45
|
+
return el;
|
|
46
|
+
}
|
|
47
|
+
el = el.parentElement;
|
|
48
|
+
}
|
|
49
|
+
return getProxy(instance)?.$el || null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const addScrollContainerListener = (handler) => {
|
|
53
|
+
const el = scrollContainer();
|
|
54
|
+
if (!isFunction(handler) || !el || !isFunction(el.addEventListener)) return;
|
|
55
|
+
el.addEventListener("scroll", handler);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const removeScrollContainerListener = (handler) => {
|
|
59
|
+
const el = scrollContainer();
|
|
60
|
+
if (!isFunction(handler) || !el || !isFunction(el.removeEventListener)) return;
|
|
61
|
+
el.removeEventListener("scroll", handler);
|
|
62
|
+
};
|
|
63
|
+
|
|
36
64
|
const enableInfiniteScrollIfApplicable = async (spec) => {
|
|
37
65
|
await nextTick();
|
|
38
66
|
|
|
39
67
|
removeViewportChangeListeners(topScrollHandler.value);
|
|
40
68
|
removeViewportChangeListeners(bottomScrollHandler.value);
|
|
69
|
+
removeScrollContainerListener(topScrollHandler.value);
|
|
70
|
+
removeScrollContainerListener(bottomScrollHandler.value);
|
|
41
71
|
|
|
42
72
|
const prevPage = spec?.prevPage || {};
|
|
43
73
|
const nextPage = spec?.nextPage || {};
|
|
@@ -79,6 +109,7 @@ function useListAutoload() {
|
|
|
79
109
|
);
|
|
80
110
|
|
|
81
111
|
addViewportChangeListeners(topScrollHandler.value);
|
|
112
|
+
addScrollContainerListener(topScrollHandler.value);
|
|
82
113
|
};
|
|
83
114
|
|
|
84
115
|
const updateScrollAfterPrepending = (previousFirstRowIndex) => {
|
|
@@ -164,6 +195,7 @@ function useListAutoload() {
|
|
|
164
195
|
);
|
|
165
196
|
|
|
166
197
|
addViewportChangeListeners(bottomScrollHandler.value);
|
|
198
|
+
addScrollContainerListener(bottomScrollHandler.value);
|
|
167
199
|
};
|
|
168
200
|
|
|
169
201
|
const handleBottomScroll = (onScrollToBottom) => {
|
|
@@ -31,6 +31,35 @@ function useTableAutoload() {
|
|
|
31
31
|
return proxy.$onVisibilityChange(el, callback);
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
// When the table has a fixed height it scrolls internally; those `scroll` events
|
|
35
|
+
// don't bubble to `#page_body`, so the viewport-change listeners alone never
|
|
36
|
+
// re-evaluate the anchor visibility. Attach the handler to the table's own
|
|
37
|
+
// scroll container (the nearest scrollable ancestor of the anchor) as well.
|
|
38
|
+
const scrollContainer = () => {
|
|
39
|
+
const proxy = getProxy(instance);
|
|
40
|
+
let el = proxy?.$refs?.bottomAnchor || proxy?.$refs?.topAnchor;
|
|
41
|
+
while (el && el !== document.body && el.nodeType === 1) {
|
|
42
|
+
const overflowY = window.getComputedStyle(el).overflowY;
|
|
43
|
+
if (overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay") {
|
|
44
|
+
return el;
|
|
45
|
+
}
|
|
46
|
+
el = el.parentElement;
|
|
47
|
+
}
|
|
48
|
+
return proxy?.$el || null;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const addScrollContainerListener = (handler) => {
|
|
52
|
+
const el = scrollContainer();
|
|
53
|
+
if (!isFunction(handler) || !el || !isFunction(el.addEventListener)) return;
|
|
54
|
+
el.addEventListener("scroll", handler);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const removeScrollContainerListener = (handler) => {
|
|
58
|
+
const el = scrollContainer();
|
|
59
|
+
if (!isFunction(handler) || !el || !isFunction(el.removeEventListener)) return;
|
|
60
|
+
el.removeEventListener("scroll", handler);
|
|
61
|
+
};
|
|
62
|
+
|
|
34
63
|
const bottomAnchorStyles = computed(() => {
|
|
35
64
|
return {
|
|
36
65
|
display: infiniteScroll.value && nextPageUrl.value ? "block" : "none"
|
|
@@ -42,6 +71,8 @@ function useTableAutoload() {
|
|
|
42
71
|
|
|
43
72
|
removeViewportChangeListeners(topScrollHandler.value);
|
|
44
73
|
removeViewportChangeListeners(bottomScrollHandler.value);
|
|
74
|
+
removeScrollContainerListener(topScrollHandler.value);
|
|
75
|
+
removeScrollContainerListener(bottomScrollHandler.value);
|
|
45
76
|
|
|
46
77
|
const proxy = getProxy(instance);
|
|
47
78
|
if (!proxy) return;
|
|
@@ -81,6 +112,7 @@ function useTableAutoload() {
|
|
|
81
112
|
);
|
|
82
113
|
|
|
83
114
|
addViewportChangeListeners(topScrollHandler.value);
|
|
115
|
+
addScrollContainerListener(topScrollHandler.value);
|
|
84
116
|
};
|
|
85
117
|
|
|
86
118
|
const handleTopScroll = (onScrollToTop) => {
|
|
@@ -103,6 +135,7 @@ function useTableAutoload() {
|
|
|
103
135
|
);
|
|
104
136
|
|
|
105
137
|
addViewportChangeListeners(bottomScrollHandler.value);
|
|
138
|
+
addScrollContainerListener(bottomScrollHandler.value);
|
|
106
139
|
};
|
|
107
140
|
|
|
108
141
|
const handleBottomScroll = (onScrollToBottom) => {
|
package/components/fab.vue
CHANGED
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
<v-fab app :disabled="spec.disabled" :class="$classes()" :href="$href()" :variant="variant"
|
|
3
|
+
:rounded="$classes().includes('rounded') || null" :density="density" :size="size" :color="color"
|
|
4
|
+
:location="spec.location || 'bottom end'" :icon="iconValue" @click="$onClick">
|
|
5
|
+
</v-fab>
|
|
5
6
|
</template>
|
|
6
7
|
|
|
7
8
|
<script>
|
|
8
9
|
import GlibBase from "./base/glibBase.js";
|
|
10
|
+
import ButtonProps from "./mixins/buttonProps.js";
|
|
11
|
+
import { resolveIconName } from "../utils/constant";
|
|
12
|
+
|
|
9
13
|
export default {
|
|
10
14
|
extends: GlibBase,
|
|
11
|
-
|
|
15
|
+
mixins: [ButtonProps],
|
|
16
|
+
props: {
|
|
17
|
+
spec: { type: Object, required: true }
|
|
18
|
+
},
|
|
19
|
+
computed: {
|
|
20
|
+
iconValue() {
|
|
21
|
+
return resolveIconName(this.spec.icon);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
12
24
|
};
|
|
13
25
|
</script>
|
|
14
26
|
|
|
15
|
-
<style scoped>
|
|
16
|
-
.fab {
|
|
17
|
-
|
|
18
|
-
right: 48px;
|
|
19
|
-
bottom: 24px;
|
|
27
|
+
<style lang="scss" scoped>
|
|
28
|
+
.v-fab.glib-clickable:not(.v-btn):not(.disabled):not(.no-hover):hover:before {
|
|
29
|
+
background: unset;
|
|
20
30
|
}
|
|
21
31
|
</style>
|
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
<!-- See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date for why we need to use `pattern` -->
|
|
5
5
|
|
|
6
6
|
<template v-else>
|
|
7
|
-
<v-text-field ref="field" :color="gcolor" :label="spec.label" :hint="spec.hint"
|
|
8
|
-
:
|
|
9
|
-
:
|
|
10
|
-
:
|
|
7
|
+
<v-text-field ref="field" v-model="inputText" :color="gcolor" :label="spec.label" :hint="spec.hint"
|
|
8
|
+
:readonly="spec.readOnly" :disabled="inputDisabled" :style="$styles()"
|
|
9
|
+
:density="$classes().includes('compact') ? 'compact' : 'default'" :clearable="spec.clearable"
|
|
10
|
+
:variant="variant" validate-on="blur" persistent-placeholder :placeholder="spec.placeholder"
|
|
11
|
+
:error-messages="parseError" append-inner-icon="calendar_today" @click:append-inner="openPicker"
|
|
12
|
+
@click:clear="onClear" @blur="parseTypedDate" @keydown.enter="parseTypedDate" />
|
|
11
13
|
<input ref="nativePicker" v-model="fieldModel" class="native-picker" :type="type" :name="fieldName"
|
|
12
14
|
:min="sanitizeValue(spec.min)" :max="sanitizeValue(spec.max)" :value="sanitizeValue(fieldModel)"
|
|
13
15
|
@change="handleNativeChange" />
|
|
@@ -18,6 +20,7 @@
|
|
|
18
20
|
|
|
19
21
|
<script>
|
|
20
22
|
import { useDate } from "vuetify";
|
|
23
|
+
import moment from "moment";
|
|
21
24
|
import { sanitize } from "../composable/date";
|
|
22
25
|
import { useGlibInput } from "../composable/form";
|
|
23
26
|
import { useInputVariant } from "../composable/inputVariant";
|
|
@@ -25,6 +28,20 @@ import buttonDate from "./_buttonDate.vue";
|
|
|
25
28
|
import { isPresent, isString, isFunction } from "../../utils/type";
|
|
26
29
|
import GlibBase from "../base/glibBase.js";
|
|
27
30
|
|
|
31
|
+
// Accepted formats for flexible parsing of typed/pasted dates. moment is lenient
|
|
32
|
+
// (non-strict) so these cover the common cases (ISO, slash/dash variants, named
|
|
33
|
+
// months) including the field's own displayed `fullDate` format.
|
|
34
|
+
const PARSE_FORMATS = [
|
|
35
|
+
moment.ISO_8601,
|
|
36
|
+
"YYYY-MM-DD", "YYYY/MM/DD",
|
|
37
|
+
"MM/DD/YYYY", "M/D/YYYY", "MM-DD-YYYY",
|
|
38
|
+
"DD/MM/YYYY", "D/M/YYYY", "DD-MM-YYYY",
|
|
39
|
+
"MMM D YYYY", "MMM D, YYYY", "MMMM D YYYY", "MMMM D, YYYY",
|
|
40
|
+
"ddd, MMM DD, YYYY",
|
|
41
|
+
"YYYY-MM-DDTHH:mm", "YYYY-MM-DD HH:mm",
|
|
42
|
+
"MM/DD/YYYY HH:mm", "MM/DD/YYYY h:mm A"
|
|
43
|
+
];
|
|
44
|
+
|
|
28
45
|
export default {
|
|
29
46
|
components: { buttonDate },
|
|
30
47
|
extends: GlibBase,
|
|
@@ -40,23 +57,29 @@ export default {
|
|
|
40
57
|
|
|
41
58
|
return { adapter, variant };
|
|
42
59
|
},
|
|
60
|
+
data() {
|
|
61
|
+
return {
|
|
62
|
+
// Text shown in (and typed into) the editable field. Kept in sync with the
|
|
63
|
+
// formatted display value via the `format` watcher; parsed back into
|
|
64
|
+
// `fieldModel` on blur/enter.
|
|
65
|
+
inputText: undefined,
|
|
66
|
+
parseError: undefined
|
|
67
|
+
};
|
|
68
|
+
},
|
|
43
69
|
computed: {
|
|
44
70
|
format() {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
71
|
+
return this.formatValue(this.fieldModel);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
watch: {
|
|
75
|
+
format: {
|
|
76
|
+
immediate: true,
|
|
77
|
+
handler(val) {
|
|
78
|
+
// Reflect external changes (native picker, initial value, clearing) into
|
|
79
|
+
// the visible text. Does not fire while typing, since `fieldModel` only
|
|
80
|
+
// changes once the typed text is parsed.
|
|
81
|
+
this.inputText = isPresent(val) ? val : undefined;
|
|
57
82
|
}
|
|
58
|
-
|
|
59
|
-
return this.adapter.format(this.fieldModel, this.spec.format);
|
|
60
83
|
}
|
|
61
84
|
},
|
|
62
85
|
methods: {
|
|
@@ -68,6 +91,23 @@ export default {
|
|
|
68
91
|
return sanitize(val, this.type, timeZone);
|
|
69
92
|
}
|
|
70
93
|
},
|
|
94
|
+
formatValue(val) {
|
|
95
|
+
if (!isPresent(val)) return;
|
|
96
|
+
if (!isPresent(this.spec.format)) {
|
|
97
|
+
switch (this.type) {
|
|
98
|
+
case 'date':
|
|
99
|
+
return this.adapter.format(val, 'fullDate');
|
|
100
|
+
|
|
101
|
+
case 'datetime-local':
|
|
102
|
+
return this.adapter.format(val, 'keyboardDateTime24h');
|
|
103
|
+
|
|
104
|
+
default:
|
|
105
|
+
return this.adapter.format(val, 'keyboardDateTime24h');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return this.adapter.format(val, this.spec.format);
|
|
110
|
+
},
|
|
71
111
|
openPicker() {
|
|
72
112
|
if (this.inputDisabled || this.spec.readOnly) return;
|
|
73
113
|
|
|
@@ -86,7 +126,48 @@ export default {
|
|
|
86
126
|
picker.click();
|
|
87
127
|
}
|
|
88
128
|
},
|
|
89
|
-
|
|
129
|
+
parseTypedDate() {
|
|
130
|
+
const text = isString(this.inputText) ? this.inputText.trim() : this.inputText;
|
|
131
|
+
|
|
132
|
+
// Empty input clears the value (also reached when typing then deleting).
|
|
133
|
+
if (!isPresent(text) || text === '') {
|
|
134
|
+
this.onClear();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const parsed = moment(text, PARSE_FORMATS);
|
|
139
|
+
if (!parsed.isValid()) {
|
|
140
|
+
this.parseError = 'Invalid date';
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Pass '' as the time zone so the typed wall-clock date is preserved rather
|
|
145
|
+
// than shifted, matching `handleNativeChange`. `sanitizeValue` slices it to
|
|
146
|
+
// the field's storage format (YYYY-MM-DD for date, etc.).
|
|
147
|
+
const value = this.sanitizeValue(parsed.toISOString(true), '');
|
|
148
|
+
|
|
149
|
+
// Enforce the same min/max the native picker honours. Sanitized values share
|
|
150
|
+
// a comparable per-type format, so lexicographic comparison reflects order.
|
|
151
|
+
const min = this.sanitizeValue(this.spec.min);
|
|
152
|
+
const max = this.sanitizeValue(this.spec.max);
|
|
153
|
+
if (isPresent(min) && value < min) {
|
|
154
|
+
this.parseError = `Date must be on or after ${this.formatValue(min)}`;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (isPresent(max) && value > max) {
|
|
158
|
+
this.parseError = `Date must be on or before ${this.formatValue(max)}`;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
this.parseError = undefined;
|
|
163
|
+
this.fieldModel = value;
|
|
164
|
+
this.inputText = this.format;
|
|
165
|
+
this.$executeOnChange();
|
|
166
|
+
},
|
|
167
|
+
onClear() {
|
|
168
|
+
this.parseError = undefined;
|
|
169
|
+
this.inputText = undefined;
|
|
170
|
+
this.fieldModel = undefined;
|
|
90
171
|
this.$executeOnChange();
|
|
91
172
|
},
|
|
92
173
|
handleNativeChange(event) {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
:disabled="inputDisabled" :multiple="spec.multiple" :readonly="spec.readOnly" :clearable="spec.clearable"
|
|
7
7
|
:placeholder="spec.placeholder" :rules="$validation()" persistent-hint :append-icon="append.icon"
|
|
8
8
|
validate-on="input" item-title='text' :variant="variant" :density="density" persistent-placeholder
|
|
9
|
-
:menu-props="{ maxWidth: 0 }" @update:modelValue="onChange" @focus="focused = true" @blur="focused = false">
|
|
9
|
+
:menu-props="{ maxWidth: 0 }" :custom-filter="filterBySubtitle" @update:modelValue="onChange" @focus="focused = true" @blur="focused = false">
|
|
10
10
|
|
|
11
11
|
<template #item="{ props, item }">
|
|
12
12
|
<v-list-subheader v-if="item.type == 'view'">
|
|
@@ -381,6 +381,13 @@ export default {
|
|
|
381
381
|
triggerOnChange(containerEl);
|
|
382
382
|
}
|
|
383
383
|
},
|
|
384
|
+
filterBySubtitle(value, query, item) {
|
|
385
|
+
const q = (query || '').toString().toLowerCase();
|
|
386
|
+
if (!q) return true;
|
|
387
|
+
const text = (value || '').toString().toLowerCase();
|
|
388
|
+
const subtitle = (item?.raw?.subtitle || '').toString().toLowerCase();
|
|
389
|
+
return text.includes(q) || subtitle.includes(q);
|
|
390
|
+
},
|
|
384
391
|
removeItem(item) {
|
|
385
392
|
// fieldModel may not be an array when useChips is true but multiple is false.
|
|
386
393
|
// This can happen when spec.useChips is explicitly set to true while spec.multiple is false.
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
:suffix="spec.rightText" :min="spec.min" :max="spec.max" :autofocus="spec.autoFocus || false" validate-on="blur"
|
|
8
8
|
:variant="variant" persistent-placeholder :clearable="spec.clearable" :rounded="spec.rounded"
|
|
9
9
|
:autocomplete="spec.autocomplete" @click:appendInner="onRightIconClick" @input="onChange"
|
|
10
|
-
@keyup="$onTyping({ duration: 2000 })" @wheel
|
|
10
|
+
@keyup="$onTyping({ duration: 2000 })" @wheel="onWheel" />
|
|
11
11
|
</div>
|
|
12
12
|
</template>
|
|
13
13
|
|
|
@@ -131,6 +131,17 @@ export default {
|
|
|
131
131
|
action_focus() {
|
|
132
132
|
this.$refs.field.focus();
|
|
133
133
|
},
|
|
134
|
+
onWheel() {
|
|
135
|
+
// Only number inputs change their value on wheel scroll, and only while
|
|
136
|
+
// focused. Blur the focused number input so the value stays put while the
|
|
137
|
+
// wheel event still bubbles up and scrolls the page. All other field types
|
|
138
|
+
// (and unfocused number fields) are left untouched so the page can scroll.
|
|
139
|
+
if (this.config.type !== "number") return;
|
|
140
|
+
const input = this.$refs.field?.$el?.querySelector("input");
|
|
141
|
+
if (input && document.activeElement === input) {
|
|
142
|
+
input.blur();
|
|
143
|
+
}
|
|
144
|
+
},
|
|
134
145
|
onChange: eventFiltering.debounce(function () {
|
|
135
146
|
this.$executeOnChange();
|
|
136
147
|
}, 300),
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { determineColor, determineDensity, determineVariant, determineSize } from '../../utils/constant';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
computed: {
|
|
5
|
+
color() {
|
|
6
|
+
return this.spec.color || determineColor(this.spec.styleClasses);
|
|
7
|
+
},
|
|
8
|
+
density() {
|
|
9
|
+
return determineDensity(this.spec.styleClasses);
|
|
10
|
+
},
|
|
11
|
+
size() {
|
|
12
|
+
return determineSize(this.spec.styleClasses);
|
|
13
|
+
},
|
|
14
|
+
variant() {
|
|
15
|
+
return determineVariant(this.spec.styleClasses, 'elevated');
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
methods: {
|
|
19
|
+
styles() {
|
|
20
|
+
const styles = this.$styles();
|
|
21
|
+
if (this.$classes().includes('link')) {
|
|
22
|
+
styles["color"] = styles["color"] || "#1976d2";
|
|
23
|
+
}
|
|
24
|
+
return styles;
|
|
25
|
+
},
|
|
26
|
+
$registryEnabled() {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
// From https://github.com/ittus/vue-long-click
|
|
2
2
|
export default ({ delay = 400, interval = 50 }) => ({
|
|
3
|
-
|
|
3
|
+
beforeMount: function(el, binding) {
|
|
4
4
|
if (typeof binding.value !== "function") {
|
|
5
|
-
|
|
6
|
-
let warn = `[longclick:] provided expression '${binding.expression}' is not a function, but has to be`;
|
|
7
|
-
if (compName) {
|
|
8
|
-
warn += `Found in component '${compName}' `;
|
|
9
|
-
}
|
|
10
|
-
console.warn(warn)
|
|
5
|
+
console.warn(`[longclick:] provided value is not a function`);
|
|
11
6
|
return;
|
|
12
7
|
}
|
|
13
8
|
|
|
@@ -48,9 +43,20 @@ export default ({ delay = 400, interval = 50 }) => ({
|
|
|
48
43
|
binding.value(e);
|
|
49
44
|
};
|
|
50
45
|
|
|
46
|
+
el._longClickStart = start;
|
|
47
|
+
el._longClickCancel = cancel;
|
|
48
|
+
|
|
51
49
|
["mousedown", "touchstart"].forEach(e => el.addEventListener(e, start));
|
|
52
50
|
["click", "mouseup", "touchend", "touchcancel"].forEach(e => {
|
|
53
51
|
el.addEventListener(e, cancel);
|
|
54
52
|
});
|
|
53
|
+
},
|
|
54
|
+
unmounted: function(el) {
|
|
55
|
+
if (el._longClickStart) {
|
|
56
|
+
["mousedown", "touchstart"].forEach(e => el.removeEventListener(e, el._longClickStart));
|
|
57
|
+
}
|
|
58
|
+
if (el._longClickCancel) {
|
|
59
|
+
["click", "mouseup", "touchend", "touchcancel"].forEach(e => el.removeEventListener(e, el._longClickCancel));
|
|
60
|
+
}
|
|
55
61
|
}
|
|
56
62
|
});
|
|
@@ -13,10 +13,10 @@ export default {
|
|
|
13
13
|
rect.left >= 0 &&
|
|
14
14
|
rect.bottom <=
|
|
15
15
|
(window.innerHeight ||
|
|
16
|
-
document.documentElement.clientHeight) /*or $(window).height() */ &&
|
|
16
|
+
document.documentElement.clientHeight) + 2 /*or $(window).height() */ &&
|
|
17
17
|
rect.right <=
|
|
18
18
|
(window.innerWidth ||
|
|
19
|
-
document.documentElement.clientWidth) /*or $(window).width() */
|
|
19
|
+
document.documentElement.clientWidth) + 2 /*or $(window).width() */
|
|
20
20
|
);
|
|
21
21
|
},
|
|
22
22
|
// From https://stackoverflow.com/questions/123999/how-to-tell-if-a-dom-element-is-visible-in-the-current-viewport/7557433#7557433
|
|
@@ -301,6 +301,14 @@ export default {
|
|
|
301
301
|
classes.push("glib-clickable")
|
|
302
302
|
);
|
|
303
303
|
|
|
304
|
+
// Marker for elements that carry a tooltip. The tooltip itself is wired up
|
|
305
|
+
// via JS hover/click listeners and leaves no other trace in the DOM, so this
|
|
306
|
+
// class is the only hook consumers have to style a "this has a tooltip"
|
|
307
|
+
// affordance (e.g. a dotted underline) scoped to elements that actually have one.
|
|
308
|
+
Utils.type.ifObject(properties.tooltip, () =>
|
|
309
|
+
classes.push("has-tooltip")
|
|
310
|
+
);
|
|
311
|
+
|
|
304
312
|
// if (properties.readOnly) {
|
|
305
313
|
// classes.push("glib-input-disabled");
|
|
306
314
|
// }
|
|
@@ -44,15 +44,18 @@ export default {
|
|
|
44
44
|
computed: {
|
|
45
45
|
template() {
|
|
46
46
|
const name = `template-${this.spec.template.replace("/", "-")}`;
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
|
|
48
|
+
// Built-in templates registered locally on this component.
|
|
49
|
+
if (this.$options.components[name]) {
|
|
49
50
|
return name;
|
|
50
|
-
} else {
|
|
51
|
-
if (getCurrentInstance().appContext.components[name]) {
|
|
52
|
-
return name;
|
|
53
|
-
}
|
|
54
|
-
return "template-unsupported";
|
|
55
51
|
}
|
|
52
|
+
|
|
53
|
+
// Project-defined templates registered globally as `template-<name>`.
|
|
54
|
+
if (getCurrentInstance().appContext.components[name]) {
|
|
55
|
+
return name;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return "template-unsupported";
|
|
56
59
|
},
|
|
57
60
|
contentSpec() {
|
|
58
61
|
const onClick = this.spec.onClick ? { onClick: this.spec.onClick } : {};
|