@signal24/vue-foundation 4.24.1 → 4.25.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/demo/components/demo-vf-smart-select.vue +29 -13
- package/dist/src/components/vf-ez-smart-select.vue.d.ts +1 -0
- package/dist/src/components/vf-smart-select.types.d.ts +1 -0
- package/dist/src/components/vf-smart-select.vue.d.ts +7 -0
- package/dist/src/config.d.ts +1 -0
- package/dist/tsconfig.app.tsbuildinfo +1 -1
- package/dist/vue-foundation.css +1 -1
- package/dist/vue-foundation.es.js +765 -730
- package/package.json +1 -1
- package/src/components/vf-ez-smart-select.vue +9 -1
- package/src/components/vf-smart-select.types.ts +1 -0
- package/src/components/vf-smart-select.vue +62 -19
- package/src/config.ts +3 -1
- package/src/filters/index.ts +1 -1
- package/src/helpers/string.ts +6 -2
package/package.json
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<VfSmartSelect
|
|
2
|
+
<VfSmartSelect
|
|
3
|
+
v-model="selectedItem"
|
|
4
|
+
:options="computedOpts"
|
|
5
|
+
:formatter="ezFormatter"
|
|
6
|
+
:null-title="nullTitle"
|
|
7
|
+
:placeholder="placeholder"
|
|
8
|
+
:name="name"
|
|
9
|
+
/>
|
|
3
10
|
</template>
|
|
4
11
|
|
|
5
12
|
<script lang="ts" setup generic="T extends string">
|
|
@@ -19,6 +26,7 @@ const props = defineProps<{
|
|
|
19
26
|
placeholder?: string;
|
|
20
27
|
options: { [key in T]: string } | T[];
|
|
21
28
|
formatter?: (label: string, key: T) => string;
|
|
29
|
+
name?: string;
|
|
22
30
|
}>();
|
|
23
31
|
|
|
24
32
|
const computedOpts = computed(() => {
|
|
@@ -9,31 +9,40 @@
|
|
|
9
9
|
:class="{ nullable: !!nullTitle }"
|
|
10
10
|
:placeholder="effectivePlaceholder"
|
|
11
11
|
:required="required"
|
|
12
|
+
:name="name"
|
|
12
13
|
data-1p-ignore
|
|
13
14
|
@keydown="handleKeyDown"
|
|
14
15
|
@focus="handleInputFocused"
|
|
15
16
|
@blur="handleInputBlurred"
|
|
16
17
|
/>
|
|
17
|
-
<div v-if="shouldDisplayOptions" ref="optionsContainer" class="vf-smart-select-options">
|
|
18
|
+
<div v-if="shouldDisplayOptions" ref="optionsContainer" class="vf-smart-select-options" :class="{ grouped: isGrouped }">
|
|
18
19
|
<div v-if="!isLoaded" class="no-results">Loading...</div>
|
|
19
20
|
<template v-else>
|
|
20
|
-
<div
|
|
21
|
-
v-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
21
|
+
<div v-for="group in groupedOptions" :key="group.groupTitle" class="group">
|
|
22
|
+
<div v-if="group.groupTitle" class="group-title">
|
|
23
|
+
<slot name="group" :group="group.groupTitle">
|
|
24
|
+
{{ group.groupTitle }}
|
|
25
|
+
</slot>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div
|
|
29
|
+
v-for="option in group.options"
|
|
30
|
+
:key="option.key"
|
|
31
|
+
class="option"
|
|
32
|
+
:class="[highlightedOptionKey === option.key && 'highlighted', option.ref && classForOption?.(option.ref)]"
|
|
33
|
+
@mousemove="handleOptionHover(option)"
|
|
34
|
+
@mousedown="selectOption(option)"
|
|
35
|
+
>
|
|
36
|
+
<slot name="option" :option="option">
|
|
37
|
+
<div class="title" v-html="option.title" />
|
|
38
|
+
<div v-if="option.subtitle" class="subtitle" v-html="option.subtitle" />
|
|
39
|
+
</slot>
|
|
40
|
+
</div>
|
|
41
|
+
<div v-if="!effectiveOptions.length && searchText" class="no-results">
|
|
42
|
+
<slot name="no-results">
|
|
43
|
+
{{ effectiveNoResultsText }}
|
|
44
|
+
</slot>
|
|
45
|
+
</div>
|
|
37
46
|
</div>
|
|
38
47
|
</template>
|
|
39
48
|
</div>
|
|
@@ -41,7 +50,7 @@
|
|
|
41
50
|
</template>
|
|
42
51
|
|
|
43
52
|
<script lang="ts" setup generic="T, V = T">
|
|
44
|
-
import { debounce, isEqual } from 'lodash';
|
|
53
|
+
import { debounce, groupBy, isEqual, uniq } from 'lodash';
|
|
45
54
|
import { computed, onMounted, type Ref, ref, watch } from 'vue';
|
|
46
55
|
|
|
47
56
|
import { escapeHtml } from '../helpers/string';
|
|
@@ -68,6 +77,8 @@ const props = defineProps<{
|
|
|
68
77
|
valueField?: keyof T;
|
|
69
78
|
valueExtractor?: (option: T) => V;
|
|
70
79
|
labelField?: keyof T;
|
|
80
|
+
groupField?: keyof T;
|
|
81
|
+
groupFormatter?: (option: T) => string;
|
|
71
82
|
formatter?: (option: T) => string;
|
|
72
83
|
subtitleFormatter?: (option: T) => string;
|
|
73
84
|
classForOption?: (option: T) => string;
|
|
@@ -79,6 +90,7 @@ const props = defineProps<{
|
|
|
79
90
|
required?: boolean;
|
|
80
91
|
showCreateTextOnNewItem?: boolean;
|
|
81
92
|
autoNext?: boolean;
|
|
93
|
+
name?: string;
|
|
82
94
|
}>();
|
|
83
95
|
|
|
84
96
|
const emit = defineEmits<{
|
|
@@ -127,6 +139,11 @@ const effectiveKeyExtractor = computed(() => {
|
|
|
127
139
|
if (effectiveValueExtractor.value) return (option: T) => String(effectiveValueExtractor.value!(option));
|
|
128
140
|
return null;
|
|
129
141
|
});
|
|
142
|
+
const effectiveGroupFormatter = computed(() => {
|
|
143
|
+
if (props.groupFormatter) return props.groupFormatter;
|
|
144
|
+
if (props.groupField) return (option: T) => String(option[props.groupField!]);
|
|
145
|
+
return null;
|
|
146
|
+
});
|
|
130
147
|
const effectiveFormatter = computed(() => {
|
|
131
148
|
if (props.formatter) return props.formatter;
|
|
132
149
|
if (props.labelField) return (option: T) => String(option[props.labelField!]);
|
|
@@ -134,9 +151,11 @@ const effectiveFormatter = computed(() => {
|
|
|
134
151
|
});
|
|
135
152
|
|
|
136
153
|
const allOptions = computed(() => [...effectivePrependOptions.value, ...loadedOptions.value, ...effectiveAppendOptions.value]);
|
|
154
|
+
const isGrouped = computed(() => !!(props.groupField || props.groupFormatter));
|
|
137
155
|
|
|
138
156
|
const optionsDescriptors = computed(() => {
|
|
139
157
|
return allOptions.value.map((option, index) => {
|
|
158
|
+
const group = effectiveGroupFormatter.value?.(option);
|
|
140
159
|
const title = effectiveFormatter.value(option);
|
|
141
160
|
const subtitle = props.subtitleFormatter?.(option);
|
|
142
161
|
const strippedTitle = title ? title.trim().toLowerCase() : '';
|
|
@@ -158,6 +177,7 @@ const optionsDescriptors = computed(() => {
|
|
|
158
177
|
|
|
159
178
|
return {
|
|
160
179
|
key: effectiveKeyExtractor.value?.(option) ?? String(index),
|
|
180
|
+
group,
|
|
161
181
|
title,
|
|
162
182
|
subtitle,
|
|
163
183
|
searchContent: searchContent.join(''),
|
|
@@ -206,6 +226,24 @@ const effectiveOptions = computed(() => {
|
|
|
206
226
|
return options;
|
|
207
227
|
});
|
|
208
228
|
|
|
229
|
+
const groupedOptions = computed(() => {
|
|
230
|
+
if (!effectiveOptions.value[0]?.group) {
|
|
231
|
+
return [
|
|
232
|
+
{
|
|
233
|
+
groupTitle: '',
|
|
234
|
+
options: effectiveOptions.value
|
|
235
|
+
}
|
|
236
|
+
];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const groupTitles = uniq(effectiveOptions.value.map(option => option.group ?? ''));
|
|
240
|
+
const groupedOptions = groupBy(effectiveOptions.value, option => option.group);
|
|
241
|
+
return groupTitles.map(groupTitle => ({
|
|
242
|
+
groupTitle,
|
|
243
|
+
options: groupedOptions[groupTitle!]
|
|
244
|
+
}));
|
|
245
|
+
});
|
|
246
|
+
|
|
209
247
|
// watch props
|
|
210
248
|
watch(() => props.modelValue, handleValueChanged);
|
|
211
249
|
watch(
|
|
@@ -591,6 +629,11 @@ function focusNextInput() {
|
|
|
591
629
|
overflow: auto;
|
|
592
630
|
z-index: 101;
|
|
593
631
|
|
|
632
|
+
.group-title {
|
|
633
|
+
padding: 5px 8px;
|
|
634
|
+
color: #999;
|
|
635
|
+
}
|
|
636
|
+
|
|
594
637
|
.option,
|
|
595
638
|
.no-results {
|
|
596
639
|
padding: 5px 8px;
|
package/src/config.ts
CHANGED
|
@@ -3,13 +3,15 @@ interface IOptions {
|
|
|
3
3
|
errorHandler: (err: Error) => void;
|
|
4
4
|
defaultDateFormat: string;
|
|
5
5
|
defaultTimeFormat: string;
|
|
6
|
+
defaultCurrencyDivisor: number;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
export const VfOptions: IOptions = {
|
|
9
10
|
unhandledErrorSupportText: 'please contact support',
|
|
10
11
|
errorHandler: err => console.error('Unhandled error:', err),
|
|
11
12
|
defaultDateFormat: 'M/d/yy',
|
|
12
|
-
defaultTimeFormat: 'H:mm'
|
|
13
|
+
defaultTimeFormat: 'H:mm',
|
|
14
|
+
defaultCurrencyDivisor: 1
|
|
13
15
|
};
|
|
14
16
|
|
|
15
17
|
export function configureVf(options: Partial<IOptions>) {
|
package/src/filters/index.ts
CHANGED
|
@@ -64,7 +64,7 @@ function desnake(value: string | null) {
|
|
|
64
64
|
return value ? desnakeCase(value) : null;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
function usCurrency(value: string | number, divisor
|
|
67
|
+
function usCurrency(value: string | number, divisor?: number) {
|
|
68
68
|
return formatUSCurrency(value, divisor);
|
|
69
69
|
}
|
|
70
70
|
|
package/src/helpers/string.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import currency from 'currency.js';
|
|
2
2
|
import { v4 as uuidv4 } from 'uuid';
|
|
3
3
|
|
|
4
|
+
import { VfOptions } from '@/config';
|
|
5
|
+
|
|
4
6
|
// placing this here so we don't have to use the ESLint rule everywhere
|
|
5
7
|
// eslint-disable-next-line vue/prefer-import-from-vue
|
|
6
8
|
export { escapeHtml } from '@vue/shared';
|
|
@@ -19,8 +21,10 @@ export function formatPhone(value: string) {
|
|
|
19
21
|
return '(' + cleanValue.substring(0, 3) + ') ' + cleanValue.substring(3, 6) + '-' + cleanValue.substring(6);
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
export function formatUSCurrency(value: string | number, divisor
|
|
23
|
-
return currency(value)
|
|
24
|
+
export function formatUSCurrency(value: string | number, divisor?: number) {
|
|
25
|
+
return currency(value)
|
|
26
|
+
.divide(divisor ?? VfOptions.defaultCurrencyDivisor)
|
|
27
|
+
.format();
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
export function uuid() {
|