sprintify-ui 0.0.31 → 0.0.33
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/dist/sprintify-ui.es.js +3519 -3386
- package/dist/types/src/components/BaseAutocomplete.vue.d.ts +9 -9
- package/dist/types/src/components/BaseAutocompleteFetch.vue.d.ts +9 -9
- package/dist/types/src/components/BaseBelongsTo.vue.d.ts +10 -10
- package/dist/types/src/components/BaseButtonGroup.vue.d.ts +288 -0
- package/dist/types/src/components/BaseFilePicker.vue.d.ts +1 -1
- package/dist/types/src/components/BaseFileUploader.vue.d.ts +4 -4
- package/dist/types/src/components/BaseHasMany.vue.d.ts +0 -1
- package/dist/types/src/components/BaseTagAutocomplete.vue.d.ts +0 -1
- package/dist/types/src/components/BaseTagAutocompleteFetch.vue.d.ts +0 -1
- package/dist/types/src/components/index.d.ts +2 -1
- package/dist/types/src/composables/hasOptions.d.ts +7 -0
- package/dist/types/src/types/types.d.ts +0 -2
- package/package.json +1 -1
- package/src/components/BaseAutocomplete.vue +29 -40
- package/src/components/BaseAutocompleteFetch.vue +2 -2
- package/src/components/BaseBelongsTo.vue +3 -3
- package/src/components/BaseButtonGroup.stories.js +87 -0
- package/src/components/BaseButtonGroup.vue +134 -0
- package/src/components/BaseTagAutocomplete.stories.js +4 -6
- package/src/components/BaseTagAutocomplete.vue +35 -53
- package/src/components/index.ts +2 -0
- package/src/composables/hasOptions.ts +68 -0
- package/src/types/types.ts +0 -4
|
@@ -78,10 +78,7 @@
|
|
|
78
78
|
<slot
|
|
79
79
|
name="option"
|
|
80
80
|
:option="option.option"
|
|
81
|
-
:selected="
|
|
82
|
-
normalizedModelValue &&
|
|
83
|
-
normalizedModelValue.value == option.value
|
|
84
|
-
"
|
|
81
|
+
:selected="isSelected(option)"
|
|
85
82
|
:active="optionActive && optionActive.value == option.value"
|
|
86
83
|
>
|
|
87
84
|
<div
|
|
@@ -125,20 +122,16 @@
|
|
|
125
122
|
|
|
126
123
|
<script lang="ts" setup>
|
|
127
124
|
import { get } from 'lodash';
|
|
128
|
-
import { PropType, Ref } from 'vue';
|
|
129
|
-
import {
|
|
130
|
-
NormalizedOption,
|
|
131
|
-
Option,
|
|
132
|
-
Selection,
|
|
133
|
-
NormalizedSelection,
|
|
134
|
-
} from '@/types/types';
|
|
125
|
+
import { PropType, Ref, ComputedRef } from 'vue';
|
|
126
|
+
import { NormalizedOption, Option } from '@/types/types';
|
|
135
127
|
import { useInfiniteScroll, useMutationObserver } from '@vueuse/core';
|
|
136
128
|
import BaseSkeleton from './BaseSkeleton.vue';
|
|
129
|
+
import { useHasOptions } from '@/composables/hasOptions';
|
|
137
130
|
|
|
138
131
|
const props = defineProps({
|
|
139
132
|
modelValue: {
|
|
140
133
|
default: undefined,
|
|
141
|
-
type: [Object, null] as PropType<
|
|
134
|
+
type: [Object, null] as PropType<Option | null | undefined>,
|
|
142
135
|
},
|
|
143
136
|
options: {
|
|
144
137
|
required: true,
|
|
@@ -193,6 +186,19 @@ const selectionIndex = ref(0);
|
|
|
193
186
|
const inputElement = ref(null) as Ref<HTMLInputElement | null>;
|
|
194
187
|
const dropdown = ref(null) as Ref<HTMLElement | null>;
|
|
195
188
|
|
|
189
|
+
const hasOptions = useHasOptions(
|
|
190
|
+
computed(() => props.modelValue),
|
|
191
|
+
computed(() => props.options),
|
|
192
|
+
computed(() => props.labelKey),
|
|
193
|
+
computed(() => props.valueKey),
|
|
194
|
+
computed(() => false)
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const isSelected = hasOptions.isSelected;
|
|
198
|
+
const normalizedOptions = hasOptions.normalizedOptions;
|
|
199
|
+
const normalizedModelValue =
|
|
200
|
+
hasOptions.normalizedModelValue as ComputedRef<NormalizedOption | null>;
|
|
201
|
+
|
|
196
202
|
onMounted(() => {
|
|
197
203
|
useInfiniteScroll(
|
|
198
204
|
dropdown.value,
|
|
@@ -211,17 +217,7 @@ const optionActive = computed(() => {
|
|
|
211
217
|
);
|
|
212
218
|
});
|
|
213
219
|
|
|
214
|
-
|
|
215
|
-
if (!props.modelValue) {
|
|
216
|
-
return null;
|
|
217
|
-
}
|
|
218
|
-
return {
|
|
219
|
-
label: props.modelValue[props.labelKey] as string,
|
|
220
|
-
value: props.modelValue[props.valueKey] as string | number,
|
|
221
|
-
option: props.modelValue,
|
|
222
|
-
};
|
|
223
|
-
});
|
|
224
|
-
|
|
220
|
+
// Update the keywords when the model value changes
|
|
225
221
|
watch(
|
|
226
222
|
() => normalizedModelValue.value,
|
|
227
223
|
() => {
|
|
@@ -234,16 +230,6 @@ watch(
|
|
|
234
230
|
{ immediate: true }
|
|
235
231
|
);
|
|
236
232
|
|
|
237
|
-
const normalizedOptions = computed(() => {
|
|
238
|
-
return props.options.map((option) => {
|
|
239
|
-
return {
|
|
240
|
-
label: option[props.labelKey] as string,
|
|
241
|
-
value: option[props.valueKey] as string | number,
|
|
242
|
-
option: option,
|
|
243
|
-
} as NormalizedOption;
|
|
244
|
-
});
|
|
245
|
-
});
|
|
246
|
-
|
|
247
233
|
const filteredNormalizedOptions = computed((): NormalizedOption[] => {
|
|
248
234
|
return normalizedOptions.value.filter((option) => {
|
|
249
235
|
if (props.filter !== undefined) {
|
|
@@ -276,6 +262,8 @@ const onTextFocus = () => {
|
|
|
276
262
|
emit('focus');
|
|
277
263
|
};
|
|
278
264
|
|
|
265
|
+
// If keywords is changed but no new selection was made,
|
|
266
|
+
// update keywords to original value
|
|
279
267
|
const onTextBlur = () => {
|
|
280
268
|
timerId.value = setTimeout(() => {
|
|
281
269
|
showDropdown.value = false;
|
|
@@ -305,7 +293,7 @@ const onTextKeydown = (event: KeyboardEvent) => {
|
|
|
305
293
|
}
|
|
306
294
|
|
|
307
295
|
if (key === 'ArrowDown') {
|
|
308
|
-
if (selectionIndex.value <
|
|
296
|
+
if (selectionIndex.value < filteredNormalizedOptions.value.length - 1) {
|
|
309
297
|
selectionIndex.value++;
|
|
310
298
|
} else {
|
|
311
299
|
selectionIndex.value = 0;
|
|
@@ -317,7 +305,10 @@ const onTextKeydown = (event: KeyboardEvent) => {
|
|
|
317
305
|
if (selectionIndex.value > 0) {
|
|
318
306
|
selectionIndex.value--;
|
|
319
307
|
} else {
|
|
320
|
-
selectionIndex.value = Math.max(
|
|
308
|
+
selectionIndex.value = Math.max(
|
|
309
|
+
0,
|
|
310
|
+
filteredNormalizedOptions.value.length - 1
|
|
311
|
+
);
|
|
321
312
|
}
|
|
322
313
|
return;
|
|
323
314
|
}
|
|
@@ -333,9 +324,7 @@ const onTextKeydown = (event: KeyboardEvent) => {
|
|
|
333
324
|
|
|
334
325
|
const optionClass = (option: NormalizedOption) => {
|
|
335
326
|
const active = optionActive.value && optionActive.value.value == option.value;
|
|
336
|
-
const selected =
|
|
337
|
-
normalizedModelValue.value &&
|
|
338
|
-
normalizedModelValue.value.value == option.value;
|
|
327
|
+
const selected = isSelected(option);
|
|
339
328
|
|
|
340
329
|
if (selected) {
|
|
341
330
|
if (active) {
|
|
@@ -359,12 +348,12 @@ const clear = () => {
|
|
|
359
348
|
inputElement.value?.focus();
|
|
360
349
|
};
|
|
361
350
|
|
|
362
|
-
const onSelect = (normalizedModelValue:
|
|
351
|
+
const onSelect = (normalizedModelValue: Option | null | undefined) => {
|
|
363
352
|
update(normalizedModelValue);
|
|
364
353
|
inputElement.value?.blur();
|
|
365
354
|
};
|
|
366
355
|
|
|
367
|
-
const update = (normalizedSelection:
|
|
356
|
+
const update = (normalizedSelection: Option | null | undefined) => {
|
|
368
357
|
const selection = normalizedSelection ? normalizedSelection.option : null;
|
|
369
358
|
if (normalizedSelection) {
|
|
370
359
|
setKeywordsWithoutEvent(normalizedSelection.label);
|
|
@@ -61,14 +61,14 @@
|
|
|
61
61
|
import { config } from '../';
|
|
62
62
|
import { debounce } from 'lodash';
|
|
63
63
|
import { PropType, Ref } from 'vue';
|
|
64
|
-
import { Option
|
|
64
|
+
import { Option } from '@/types/types';
|
|
65
65
|
import { RouteLocationRaw } from 'vue-router';
|
|
66
66
|
import BaseAutocomplete from './BaseAutocomplete.vue';
|
|
67
67
|
|
|
68
68
|
const props = defineProps({
|
|
69
69
|
modelValue: {
|
|
70
70
|
default: undefined,
|
|
71
|
-
type: [Object, null] as PropType<
|
|
71
|
+
type: [Object, null] as PropType<Option | null | undefined>,
|
|
72
72
|
},
|
|
73
73
|
url: {
|
|
74
74
|
required: true,
|
|
@@ -24,12 +24,12 @@
|
|
|
24
24
|
</template>
|
|
25
25
|
|
|
26
26
|
<script lang="ts" setup>
|
|
27
|
-
import { Selection } from '@/types/types';
|
|
28
27
|
import { PropType } from 'vue';
|
|
29
28
|
import { RouteLocationRaw } from 'vue-router';
|
|
30
29
|
import { AxiosResponse } from 'axios';
|
|
31
30
|
import { config } from '@/index';
|
|
32
31
|
import BaseAutocompleteFetch from './BaseAutocompleteFetch.vue';
|
|
32
|
+
import { Option } from '@/types/types';
|
|
33
33
|
|
|
34
34
|
const props = defineProps({
|
|
35
35
|
modelValue: {
|
|
@@ -72,7 +72,7 @@ const props = defineProps({
|
|
|
72
72
|
},
|
|
73
73
|
currentModel: {
|
|
74
74
|
default: null,
|
|
75
|
-
type: [Object, null] as PropType<
|
|
75
|
+
type: [Object, null] as PropType<Option | null>,
|
|
76
76
|
},
|
|
77
77
|
createNewUrl: {
|
|
78
78
|
default: '',
|
|
@@ -120,7 +120,7 @@ watch(
|
|
|
120
120
|
{ immediate: true }
|
|
121
121
|
);
|
|
122
122
|
|
|
123
|
-
function onUpdate(newModel:
|
|
123
|
+
function onUpdate(newModel: Option | null) {
|
|
124
124
|
if (!newModel) {
|
|
125
125
|
model.value = null;
|
|
126
126
|
emit('update:modelValue', null);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import BaseButtonGroup from './BaseButtonGroup.vue';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
title: 'Form/BaseButtonGroup',
|
|
5
|
+
component: BaseButtonGroup,
|
|
6
|
+
argTypes: {},
|
|
7
|
+
args: {
|
|
8
|
+
labelKey: 'label',
|
|
9
|
+
valueKey: 'value',
|
|
10
|
+
options: [
|
|
11
|
+
{ label: 'Dark Vader', value: 'dark_vader' },
|
|
12
|
+
{ label: 'Darth Maul', value: 'darth_maul' },
|
|
13
|
+
{ label: 'Dark Sidious', value: 'dark_sidious' },
|
|
14
|
+
{ label: 'Obi Wan Kenobi', value: 'obiwan' },
|
|
15
|
+
{ label: 'Anakin Skywalker', value: 'anakin' },
|
|
16
|
+
{ label: 'Mace Windu', value: 'windu' },
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
decorators: [() => ({ template: '<div class="mb-36"><story/></div>' })],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const Template = (args) => ({
|
|
23
|
+
components: { BaseButtonGroup },
|
|
24
|
+
setup() {
|
|
25
|
+
const value = ref(null);
|
|
26
|
+
return { args, value };
|
|
27
|
+
},
|
|
28
|
+
template: `
|
|
29
|
+
<BaseButtonGroup v-model="value" v-bind="args"></BaseButtonGroup>
|
|
30
|
+
<p class="mt-5 text-sm">Value: <span class="bg-slate-200 font-mono px-1 py-px rounded">{{ value ?? 'NULL' }}</span></p>
|
|
31
|
+
`,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const Single = Template.bind({});
|
|
35
|
+
Single.args = {};
|
|
36
|
+
|
|
37
|
+
export const SingleRequired = Template.bind({});
|
|
38
|
+
SingleRequired.args = {
|
|
39
|
+
required: true,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const Multiple = Template.bind({});
|
|
43
|
+
Multiple.args = {
|
|
44
|
+
multiple: true,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const Disabled = Template.bind({});
|
|
48
|
+
Disabled.args = {
|
|
49
|
+
disabled: true,
|
|
50
|
+
modelValue: { label: 'Dark Maul', value: 'darth_maul' },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const SlotOption = (args) => ({
|
|
54
|
+
components: { BaseButtonGroup },
|
|
55
|
+
setup() {
|
|
56
|
+
const value = ref(null);
|
|
57
|
+
|
|
58
|
+
const options = [
|
|
59
|
+
{ label: 'Red', value: 'red' },
|
|
60
|
+
{ label: 'Blue', value: 'blue' },
|
|
61
|
+
{ label: 'Green', value: 'green' },
|
|
62
|
+
{ label: 'Black', value: 'black' },
|
|
63
|
+
{ label: 'Gray', value: 'gray' },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
return { value, options, args };
|
|
67
|
+
},
|
|
68
|
+
template: `
|
|
69
|
+
<BaseButtonGroup
|
|
70
|
+
v-bind="args"
|
|
71
|
+
v-model="value"
|
|
72
|
+
:options="options"
|
|
73
|
+
>
|
|
74
|
+
<template #option="{ option, selected, onSelect }">
|
|
75
|
+
<button
|
|
76
|
+
class="btn btn-xs flex items-center space-x-1 font-semibold"
|
|
77
|
+
:class="[selected ? 'btn-black' : '']"
|
|
78
|
+
type="button"
|
|
79
|
+
@click="onSelect(option)"
|
|
80
|
+
>
|
|
81
|
+
<div class="w-3 h-3 rounded" :style="{ backgroundColor: option.value }"></div>
|
|
82
|
+
<div>{{ option.label }}</div>
|
|
83
|
+
</button>
|
|
84
|
+
</template>
|
|
85
|
+
</BaseButtonGroup>
|
|
86
|
+
`,
|
|
87
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex flex-wrap" :style="{ margin: '-' + spacing }">
|
|
3
|
+
<div
|
|
4
|
+
v-for="option in normalizedOptions"
|
|
5
|
+
:key="option.value"
|
|
6
|
+
:style="{ padding: spacing }"
|
|
7
|
+
>
|
|
8
|
+
<slot
|
|
9
|
+
name="option"
|
|
10
|
+
:selected="isSelected(option)"
|
|
11
|
+
:on-select="onSelect"
|
|
12
|
+
:option="option"
|
|
13
|
+
>
|
|
14
|
+
<button
|
|
15
|
+
:type="buttonType"
|
|
16
|
+
:disabled="disabled"
|
|
17
|
+
:class="[
|
|
18
|
+
buttonClass,
|
|
19
|
+
isSelected(option) ? buttonActiveClass : buttonInactiveClass,
|
|
20
|
+
]"
|
|
21
|
+
@click="onSelect(option)"
|
|
22
|
+
>
|
|
23
|
+
{{ option.label }}
|
|
24
|
+
</button>
|
|
25
|
+
</slot>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script lang="ts" setup>
|
|
31
|
+
import { PropType } from 'vue';
|
|
32
|
+
import { NormalizedOption, Option } from '@/types/types';
|
|
33
|
+
import { cloneDeep, isArray, isObject } from 'lodash';
|
|
34
|
+
import { useHasOptions } from '@/composables/hasOptions';
|
|
35
|
+
|
|
36
|
+
const props = defineProps({
|
|
37
|
+
modelValue: {
|
|
38
|
+
default: undefined,
|
|
39
|
+
type: [Array, String, Number, null] as PropType<
|
|
40
|
+
Option[] | Option | undefined
|
|
41
|
+
>,
|
|
42
|
+
},
|
|
43
|
+
required: {
|
|
44
|
+
default: false,
|
|
45
|
+
type: Boolean,
|
|
46
|
+
},
|
|
47
|
+
disabled: {
|
|
48
|
+
default: false,
|
|
49
|
+
type: Boolean,
|
|
50
|
+
},
|
|
51
|
+
buttonType: {
|
|
52
|
+
default: 'button',
|
|
53
|
+
type: String as PropType<'button' | 'submit'>,
|
|
54
|
+
},
|
|
55
|
+
buttonClass: {
|
|
56
|
+
default: 'btn btn-sm',
|
|
57
|
+
type: String,
|
|
58
|
+
},
|
|
59
|
+
buttonActiveClass: {
|
|
60
|
+
default: 'btn-primary',
|
|
61
|
+
type: String,
|
|
62
|
+
},
|
|
63
|
+
buttonInactiveClass: {
|
|
64
|
+
default: '',
|
|
65
|
+
type: String,
|
|
66
|
+
},
|
|
67
|
+
spacing: {
|
|
68
|
+
default: '0.15rem',
|
|
69
|
+
type: String,
|
|
70
|
+
},
|
|
71
|
+
options: {
|
|
72
|
+
required: true,
|
|
73
|
+
type: Array as PropType<Option[]>,
|
|
74
|
+
},
|
|
75
|
+
labelKey: {
|
|
76
|
+
required: true,
|
|
77
|
+
type: String,
|
|
78
|
+
},
|
|
79
|
+
valueKey: {
|
|
80
|
+
required: true,
|
|
81
|
+
type: String,
|
|
82
|
+
},
|
|
83
|
+
multiple: {
|
|
84
|
+
default: false,
|
|
85
|
+
type: Boolean,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const emit = defineEmits(['update:modelValue']);
|
|
90
|
+
|
|
91
|
+
const { normalizedOptions, normalizedModelValue, isSelected } = useHasOptions(
|
|
92
|
+
computed(() => props.modelValue),
|
|
93
|
+
computed(() => props.options),
|
|
94
|
+
computed(() => props.labelKey),
|
|
95
|
+
computed(() => props.valueKey),
|
|
96
|
+
computed(() => props.multiple)
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
function onSelect(option: NormalizedOption) {
|
|
100
|
+
if (props.multiple) {
|
|
101
|
+
let newModalValue = [] as NormalizedOption[];
|
|
102
|
+
|
|
103
|
+
if (isArray(normalizedModelValue.value)) {
|
|
104
|
+
newModalValue = cloneDeep(normalizedModelValue.value);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const exists = newModalValue.find((o) => o.value == option.value);
|
|
108
|
+
|
|
109
|
+
if (exists) {
|
|
110
|
+
newModalValue = newModalValue.filter((o) => o.value != option.value);
|
|
111
|
+
} else {
|
|
112
|
+
newModalValue.push(option);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
emit(
|
|
116
|
+
'update:modelValue',
|
|
117
|
+
newModalValue.map((o) => o.option)
|
|
118
|
+
);
|
|
119
|
+
} else {
|
|
120
|
+
if (!props.required) {
|
|
121
|
+
if (
|
|
122
|
+
!isArray(normalizedModelValue.value) &&
|
|
123
|
+
option.value == normalizedModelValue.value?.value
|
|
124
|
+
) {
|
|
125
|
+
emit('update:modelValue', null);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const newOption = option.option;
|
|
131
|
+
emit('update:modelValue', newOption);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
</script>
|
|
@@ -64,16 +64,14 @@ export const SlotOption = (args) => ({
|
|
|
64
64
|
v-model="value"
|
|
65
65
|
:options="options"
|
|
66
66
|
>
|
|
67
|
-
<template #option="{ option, active
|
|
67
|
+
<template #option="{ option, active }">
|
|
68
68
|
<div
|
|
69
69
|
class="rounded px-2 font-semibold py-1 text-sm"
|
|
70
70
|
:class="{
|
|
71
|
-
'hover:bg-slate-100': !active
|
|
72
|
-
'bg-slate-200 hover:bg-slate-300': active
|
|
73
|
-
'bg-blue-500 text-white hover:bg-blue-600': !active && selected,
|
|
74
|
-
'bg-blue-600 text-white hover:bg-blue-700': active && selected,
|
|
71
|
+
'hover:bg-slate-100': !active,
|
|
72
|
+
'bg-slate-200 hover:bg-slate-300': active,
|
|
75
73
|
}"
|
|
76
|
-
:style="{ color:
|
|
74
|
+
:style="{ color: option.value }"
|
|
77
75
|
>
|
|
78
76
|
{{ option.label }}
|
|
79
77
|
</div>
|
|
@@ -82,7 +82,6 @@
|
|
|
82
82
|
<slot
|
|
83
83
|
name="option"
|
|
84
84
|
:option="option.option"
|
|
85
|
-
:selected="optionValues.includes(option.value)"
|
|
86
85
|
:active="optionActive && optionActive.value == option.value"
|
|
87
86
|
>
|
|
88
87
|
<div
|
|
@@ -127,10 +126,11 @@
|
|
|
127
126
|
<script lang="ts" setup>
|
|
128
127
|
import { cloneDeep, get } from 'lodash';
|
|
129
128
|
import { PropType, Ref, ComputedRef } from 'vue';
|
|
130
|
-
import { NormalizedOption, Option
|
|
129
|
+
import { NormalizedOption, Option } from '@/types/types';
|
|
131
130
|
import { useInfiniteScroll, useMutationObserver } from '@vueuse/core';
|
|
132
131
|
import { useNotificationsStore } from '@/stores/notifications';
|
|
133
132
|
import BaseSkeleton from './BaseSkeleton.vue';
|
|
133
|
+
import { useHasOptions } from '@/composables/hasOptions';
|
|
134
134
|
|
|
135
135
|
const props = defineProps({
|
|
136
136
|
modelValue: {
|
|
@@ -194,9 +194,23 @@ const keywords = ref('');
|
|
|
194
194
|
const showDropdown = ref(false);
|
|
195
195
|
const inputElement = ref(null) as Ref<HTMLInputElement | null>;
|
|
196
196
|
const dropdown = ref(null) as Ref<HTMLElement | null>;
|
|
197
|
-
const
|
|
197
|
+
const activeIndex = ref(0);
|
|
198
198
|
const selectionToDelete = ref(null) as Ref<NormalizedOption | null>;
|
|
199
199
|
|
|
200
|
+
const hasOptions = useHasOptions(
|
|
201
|
+
computed(() => props.modelValue),
|
|
202
|
+
computed(() => props.options),
|
|
203
|
+
computed(() => props.labelKey),
|
|
204
|
+
computed(() => props.valueKey),
|
|
205
|
+
computed(() => true)
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const isSelected = hasOptions.isSelected;
|
|
209
|
+
const normalizedOptions = hasOptions.normalizedOptions;
|
|
210
|
+
const normalizedModelValue = hasOptions.normalizedModelValue as ComputedRef<
|
|
211
|
+
NormalizedOption[]
|
|
212
|
+
>;
|
|
213
|
+
|
|
200
214
|
onMounted(() => {
|
|
201
215
|
useInfiniteScroll(
|
|
202
216
|
dropdown.value,
|
|
@@ -210,34 +224,11 @@ onMounted(() => {
|
|
|
210
224
|
const optionActive = computed(() => {
|
|
211
225
|
return (
|
|
212
226
|
filteredNormalizedOptions.value[
|
|
213
|
-
Math.min(
|
|
227
|
+
Math.min(activeIndex.value, filteredNormalizedOptions.value.length - 1)
|
|
214
228
|
] ?? null
|
|
215
229
|
);
|
|
216
230
|
});
|
|
217
231
|
|
|
218
|
-
const normalizedModelValue = computed(() => {
|
|
219
|
-
if (!props.modelValue) {
|
|
220
|
-
return [];
|
|
221
|
-
}
|
|
222
|
-
return props.modelValue.map((o) => {
|
|
223
|
-
return {
|
|
224
|
-
label: o[props.labelKey] as string,
|
|
225
|
-
value: o[props.valueKey] as string | number,
|
|
226
|
-
option: o,
|
|
227
|
-
};
|
|
228
|
-
});
|
|
229
|
-
}) as ComputedRef<NormalizedOption[]>;
|
|
230
|
-
|
|
231
|
-
const normalizedOptions = computed(() => {
|
|
232
|
-
return props.options.map((option) => {
|
|
233
|
-
return {
|
|
234
|
-
label: option[props.labelKey] as string,
|
|
235
|
-
value: option[props.valueKey] as string | number,
|
|
236
|
-
option: option,
|
|
237
|
-
} as NormalizedOption;
|
|
238
|
-
});
|
|
239
|
-
});
|
|
240
|
-
|
|
241
232
|
const filteredNormalizedOptions = computed((): NormalizedOption[] => {
|
|
242
233
|
return normalizedOptions.value
|
|
243
234
|
.filter((option) => {
|
|
@@ -250,20 +241,10 @@ const filteredNormalizedOptions = computed((): NormalizedOption[] => {
|
|
|
250
241
|
return option.label.toLowerCase().includes(keywords.value.toLowerCase());
|
|
251
242
|
})
|
|
252
243
|
.filter((option) => {
|
|
253
|
-
return !
|
|
244
|
+
return !isSelected(option);
|
|
254
245
|
});
|
|
255
246
|
});
|
|
256
247
|
|
|
257
|
-
const optionValues = computed(() => {
|
|
258
|
-
return normalizedModelValue.value.map((o) => {
|
|
259
|
-
return o.value;
|
|
260
|
-
});
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
const hasSelectedOption = (value: OptionValue): boolean => {
|
|
264
|
-
return optionValues.value.includes(value);
|
|
265
|
-
};
|
|
266
|
-
|
|
267
248
|
function preventUnfocus(elements: HTMLElement[]) {
|
|
268
249
|
elements.forEach((e) => {
|
|
269
250
|
e.removeEventListener('mousedown', dontLooseFocus);
|
|
@@ -294,7 +275,7 @@ const onTextBlur = () => {
|
|
|
294
275
|
};
|
|
295
276
|
|
|
296
277
|
const onTextInput = (event: Event) => {
|
|
297
|
-
|
|
278
|
+
activeIndex.value = 0;
|
|
298
279
|
selectionToDelete.value = null;
|
|
299
280
|
setKeywords(get(event, 'target.value', '') + '');
|
|
300
281
|
dropdown.value?.scrollTo({
|
|
@@ -315,19 +296,22 @@ const onTextKeydown = (event: KeyboardEvent) => {
|
|
|
315
296
|
}
|
|
316
297
|
|
|
317
298
|
if (key === 'ArrowDown') {
|
|
318
|
-
if (
|
|
319
|
-
|
|
299
|
+
if (activeIndex.value < filteredNormalizedOptions.value.length - 1) {
|
|
300
|
+
activeIndex.value++;
|
|
320
301
|
} else {
|
|
321
|
-
|
|
302
|
+
activeIndex.value = 0;
|
|
322
303
|
}
|
|
323
304
|
return;
|
|
324
305
|
}
|
|
325
306
|
|
|
326
307
|
if (key === 'ArrowUp') {
|
|
327
|
-
if (
|
|
328
|
-
|
|
308
|
+
if (activeIndex.value > 0) {
|
|
309
|
+
activeIndex.value--;
|
|
329
310
|
} else {
|
|
330
|
-
|
|
311
|
+
activeIndex.value = Math.max(
|
|
312
|
+
0,
|
|
313
|
+
filteredNormalizedOptions.value.length - 1
|
|
314
|
+
);
|
|
331
315
|
}
|
|
332
316
|
return;
|
|
333
317
|
}
|
|
@@ -343,9 +327,7 @@ const onTextKeydown = (event: KeyboardEvent) => {
|
|
|
343
327
|
|
|
344
328
|
const optionClass = (option: NormalizedOption) => {
|
|
345
329
|
const active = optionActive.value && optionActive.value.value == option.value;
|
|
346
|
-
const selected =
|
|
347
|
-
normalizedModelValue.value &&
|
|
348
|
-
normalizedModelValue.value.map((o) => o.value).includes(option.value);
|
|
330
|
+
const selected = isSelected(option);
|
|
349
331
|
|
|
350
332
|
if (selected) {
|
|
351
333
|
if (active) {
|
|
@@ -389,7 +371,7 @@ const addOption = (option: NormalizedOption) => {
|
|
|
389
371
|
return;
|
|
390
372
|
}
|
|
391
373
|
|
|
392
|
-
if (
|
|
374
|
+
if (isSelected(option)) {
|
|
393
375
|
return;
|
|
394
376
|
}
|
|
395
377
|
|
|
@@ -440,19 +422,19 @@ const beforeAddOption = () => {
|
|
|
440
422
|
};
|
|
441
423
|
|
|
442
424
|
const afterUpdate = () => {
|
|
443
|
-
|
|
425
|
+
validateActiveIndex();
|
|
444
426
|
};
|
|
445
427
|
|
|
446
428
|
const clearInput = () => {
|
|
447
429
|
setKeywords('');
|
|
448
430
|
};
|
|
449
431
|
|
|
450
|
-
const
|
|
432
|
+
const validateActiveIndex = () => {
|
|
451
433
|
// Wait for filteredOptions to update
|
|
452
434
|
nextTick(() => {
|
|
453
|
-
|
|
435
|
+
activeIndex.value = Math.max(
|
|
454
436
|
0,
|
|
455
|
-
Math.min(
|
|
437
|
+
Math.min(activeIndex.value, filteredNormalizedOptions.value.length - 1)
|
|
456
438
|
);
|
|
457
439
|
});
|
|
458
440
|
};
|
package/src/components/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ import BaseBelongsTo from './BaseBelongsTo.vue';
|
|
|
11
11
|
import BaseBoolean from './BaseBoolean.vue';
|
|
12
12
|
import BaseBreadcrumbs from './BaseBreadcrumbs.vue';
|
|
13
13
|
import BaseButton from './BaseButton.vue';
|
|
14
|
+
import BaseButtonGroup from './BaseButtonGroup.vue';
|
|
14
15
|
import BaseCard from './BaseCard.vue';
|
|
15
16
|
import BaseCardRow from './BaseCardRow.vue';
|
|
16
17
|
import BaseCharacterCounter from './BaseCharacterCounter.vue';
|
|
@@ -80,6 +81,7 @@ export {
|
|
|
80
81
|
BaseBoolean,
|
|
81
82
|
BaseBreadcrumbs,
|
|
82
83
|
BaseButton,
|
|
84
|
+
BaseButtonGroup,
|
|
83
85
|
BaseCard,
|
|
84
86
|
BaseCardRow,
|
|
85
87
|
BaseCharacterCounter,
|