sprintify-ui 0.0.32 → 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 +8 -8
- 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 +2 -2
- package/src/components/BaseButtonGroup.vue +9 -51
- 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
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
>
|
|
8
8
|
<slot
|
|
9
9
|
name="option"
|
|
10
|
-
:
|
|
10
|
+
:selected="isSelected(option)"
|
|
11
11
|
:on-select="onSelect"
|
|
12
12
|
:option="option"
|
|
13
13
|
>
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
:disabled="disabled"
|
|
17
17
|
:class="[
|
|
18
18
|
buttonClass,
|
|
19
|
-
|
|
19
|
+
isSelected(option) ? buttonActiveClass : buttonInactiveClass,
|
|
20
20
|
]"
|
|
21
21
|
@click="onSelect(option)"
|
|
22
22
|
>
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
import { PropType } from 'vue';
|
|
32
32
|
import { NormalizedOption, Option } from '@/types/types';
|
|
33
33
|
import { cloneDeep, isArray, isObject } from 'lodash';
|
|
34
|
+
import { useHasOptions } from '@/composables/hasOptions';
|
|
34
35
|
|
|
35
36
|
const props = defineProps({
|
|
36
37
|
modelValue: {
|
|
@@ -87,57 +88,14 @@ const props = defineProps({
|
|
|
87
88
|
|
|
88
89
|
const emit = defineEmits(['update:modelValue']);
|
|
89
90
|
|
|
90
|
-
const normalizedModelValue =
|
|
91
|
-
()
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return props.modelValue.map((option) => {
|
|
97
|
-
return {
|
|
98
|
-
label: option[props.labelKey] as string,
|
|
99
|
-
value: option[props.valueKey] as string | number,
|
|
100
|
-
option: option,
|
|
101
|
-
} as NormalizedOption;
|
|
102
|
-
});
|
|
103
|
-
} else {
|
|
104
|
-
if (!isObject(props.modelValue)) {
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return {
|
|
109
|
-
label: props.modelValue[props.labelKey as never] as string,
|
|
110
|
-
value: props.modelValue[props.valueKey as never] as string | number,
|
|
111
|
-
option: props.modelValue,
|
|
112
|
-
} as NormalizedOption;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
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)
|
|
115
97
|
);
|
|
116
98
|
|
|
117
|
-
const normalizedOptions = computed((): NormalizedOption[] => {
|
|
118
|
-
return props.options.map((option) => {
|
|
119
|
-
return {
|
|
120
|
-
label: option[props.labelKey] as string,
|
|
121
|
-
value: option[props.valueKey] as string | number,
|
|
122
|
-
option: option,
|
|
123
|
-
} as NormalizedOption;
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
function isActive(option: NormalizedOption): boolean {
|
|
128
|
-
if (isArray(normalizedModelValue.value)) {
|
|
129
|
-
return normalizedModelValue.value.some((modelValue) => {
|
|
130
|
-
return modelValue.value === option.value;
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (isObject(normalizedModelValue.value)) {
|
|
135
|
-
return normalizedModelValue.value.value == option.value;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
99
|
function onSelect(option: NormalizedOption) {
|
|
142
100
|
if (props.multiple) {
|
|
143
101
|
let newModalValue = [] as NormalizedOption[];
|
|
@@ -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,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Ref } from 'vue';
|
|
2
|
+
import { NormalizedOption, Option } from '@/types/types';
|
|
3
|
+
import { isArray, isObject } from 'lodash';
|
|
4
|
+
|
|
5
|
+
export function useHasOptions(
|
|
6
|
+
modelValue: Ref<Option[] | Option | null | undefined>,
|
|
7
|
+
options: Ref<Option[]>,
|
|
8
|
+
labelKey: Ref<string>,
|
|
9
|
+
valueKey: Ref<string>,
|
|
10
|
+
multiple: Ref<boolean> = ref(false)
|
|
11
|
+
) {
|
|
12
|
+
const normalizedModelValue = computed(
|
|
13
|
+
(): NormalizedOption[] | NormalizedOption | null => {
|
|
14
|
+
if (multiple.value) {
|
|
15
|
+
if (!isArray(modelValue.value)) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
return modelValue.value.map((option) => {
|
|
19
|
+
return {
|
|
20
|
+
label: option[labelKey.value] as string,
|
|
21
|
+
value: option[valueKey.value] as string | number,
|
|
22
|
+
option: option,
|
|
23
|
+
} as NormalizedOption;
|
|
24
|
+
});
|
|
25
|
+
} else {
|
|
26
|
+
if (!isObject(modelValue.value)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
label: modelValue.value[labelKey.value as never] as string,
|
|
32
|
+
value: modelValue.value[valueKey.value as never] as string | number,
|
|
33
|
+
option: modelValue.value,
|
|
34
|
+
} as NormalizedOption;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const normalizedOptions = computed((): NormalizedOption[] => {
|
|
40
|
+
return options.value.map((option) => {
|
|
41
|
+
return {
|
|
42
|
+
label: option[labelKey.value] as string,
|
|
43
|
+
value: option[valueKey.value] as string | number,
|
|
44
|
+
option: option,
|
|
45
|
+
} as NormalizedOption;
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
function isSelected(option: NormalizedOption): boolean {
|
|
50
|
+
if (isArray(normalizedModelValue.value)) {
|
|
51
|
+
return normalizedModelValue.value.some((modelValue) => {
|
|
52
|
+
return modelValue.value === option.value;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (isObject(normalizedModelValue.value)) {
|
|
57
|
+
return normalizedModelValue.value.value == option.value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
normalizedOptions,
|
|
65
|
+
normalizedModelValue,
|
|
66
|
+
isSelected,
|
|
67
|
+
};
|
|
68
|
+
}
|
package/src/types/types.ts
CHANGED
|
@@ -21,16 +21,12 @@ export type OptionValue = string | number;
|
|
|
21
21
|
|
|
22
22
|
export type Option = Record<string, any>;
|
|
23
23
|
|
|
24
|
-
export type Selection = Option | null | undefined;
|
|
25
|
-
|
|
26
24
|
export type NormalizedOption = {
|
|
27
25
|
option: Option;
|
|
28
26
|
value: OptionValue;
|
|
29
27
|
label: string;
|
|
30
28
|
};
|
|
31
29
|
|
|
32
|
-
export type NormalizedSelection = NormalizedOption | null | undefined;
|
|
33
|
-
|
|
34
30
|
export type MediaLibraryPayload = {
|
|
35
31
|
to_remove: string[];
|
|
36
32
|
to_add: UploadedFile[];
|