sprintify-ui 0.0.12 → 0.0.14
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/README.md +8 -7
- package/dist/sprintify-ui.es.js +4429 -3588
- package/dist/style.css +1 -1
- package/dist/tailwindcss/index.js +13 -4
- package/dist/types/src/components/BaseAutocomplete.vue.d.ts +8 -5
- package/dist/types/src/components/BaseAutocompleteFetch.vue.d.ts +8 -5
- package/dist/types/src/components/BaseBelongsTo.vue.d.ts +8 -5
- package/dist/types/src/components/BaseCharacterCounter.vue.d.ts +4 -4
- package/dist/types/src/components/BaseDatePicker.vue.d.ts +8 -5
- package/dist/types/src/components/BaseHasMany.vue.d.ts +277 -0
- package/dist/types/src/components/BaseInput.vue.d.ts +39 -1
- package/dist/types/src/components/{BaseMediaLibraryItem.vue.d.ts → BaseMediaItem.vue.d.ts} +26 -4
- package/dist/types/src/components/BaseMediaLibrary.vue.d.ts +23 -15
- package/dist/types/src/components/BaseMediaPreview.vue.d.ts +97 -0
- package/dist/types/src/components/BaseSelect.vue.d.ts +7 -7
- package/dist/types/src/components/BaseSideNavigationItem.vue.d.ts +20 -1
- package/dist/types/src/components/BaseTabItem.vue.d.ts +20 -1
- package/dist/types/src/components/BaseTagAutocomplete.vue.d.ts +25 -17
- package/dist/types/src/components/BaseTagAutocompleteFetch.vue.d.ts +37 -21
- package/dist/types/src/components/BaseTextarea.vue.d.ts +8 -5
- package/dist/types/src/components/index.d.ts +10 -4
- package/package.json +1 -1
- package/src/components/BaseAppDialogs.vue +2 -2
- package/src/components/BaseAppNotifications.vue +1 -1
- package/src/components/BaseAutocomplete.vue +18 -20
- package/src/components/BaseAutocompleteFetch.vue +2 -2
- package/src/components/BaseBelongsTo.vue +3 -2
- package/src/components/BaseClipboard.vue +1 -1
- package/src/components/BaseDatePicker.vue +2 -2
- package/src/components/BaseHasMany.vue +92 -0
- package/src/components/BaseInput.stories.js +20 -1
- package/src/components/BaseInput.vue +42 -14
- package/src/components/BaseMediaItem.stories.js +41 -0
- package/src/components/BaseMediaItem.vue +71 -0
- package/src/components/BaseMediaLibrary.stories.js +80 -0
- package/src/components/BaseMediaLibrary.vue +67 -68
- package/src/components/BaseMediaPreview.stories.js +72 -0
- package/src/components/BaseMediaPreview.vue +90 -0
- package/src/components/BaseMenu.vue +1 -1
- package/src/components/BaseSelect.vue +1 -1
- package/src/components/BaseSideNavigationItem.vue +11 -3
- package/src/components/BaseSystemAlert.vue +1 -1
- package/src/components/BaseTabItem.vue +13 -3
- package/src/components/BaseTable.vue +2 -2
- package/src/components/BaseTagAutocomplete.stories.js +129 -0
- package/src/components/BaseTagAutocomplete.vue +155 -57
- package/src/components/BaseTagAutocompleteFetch.stories.js +130 -0
- package/src/components/BaseTagAutocompleteFetch.vue +36 -25
- package/src/components/BaseTextarea.vue +2 -2
- package/src/components/HasMany.stories.js +135 -0
- package/src/components/index.ts +18 -6
- package/src/lang/en.json +1 -1
- package/src/lang/fr.json +1 -1
- package/dist/types/src/components/BasePaginationSimple.vue.d.ts +0 -25
- package/src/components/BaseMediaLibraryItem.vue +0 -92
- package/src/components/BasePaginationSimple.vue +0 -60
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<li role="presentation">
|
|
3
|
-
<router-link
|
|
3
|
+
<router-link
|
|
4
|
+
v-slot="{ href, navigate, isActive, isExactActive }"
|
|
5
|
+
:to="to"
|
|
6
|
+
custom
|
|
7
|
+
>
|
|
4
8
|
<a
|
|
5
9
|
:href="href"
|
|
6
10
|
class="group relative inline-block rounded-t-lg px-2 py-3 font-medium"
|
|
7
11
|
:class="[
|
|
8
|
-
|
|
12
|
+
(activeStrategy == 'default' ? isActive : isExactActive)
|
|
13
|
+
? 'text-blue-600'
|
|
14
|
+
: 'text-slate-600 hover:text-slate-900',
|
|
9
15
|
disabled ? 'cursor-not-allowed opacity-60' : '',
|
|
10
16
|
]"
|
|
11
17
|
@click="navigate"
|
|
@@ -13,7 +19,7 @@
|
|
|
13
19
|
<div
|
|
14
20
|
class="absolute left-0 bottom-0 w-full"
|
|
15
21
|
:class="[
|
|
16
|
-
isActive
|
|
22
|
+
(activeStrategy == 'default' ? isActive : isExactActive)
|
|
17
23
|
? 'h-[2px] bg-blue-600'
|
|
18
24
|
: 'group-hover:h-px group-hover:bg-slate-700',
|
|
19
25
|
]"
|
|
@@ -39,5 +45,9 @@ defineProps({
|
|
|
39
45
|
default: false,
|
|
40
46
|
type: Boolean,
|
|
41
47
|
},
|
|
48
|
+
activeStrategy: {
|
|
49
|
+
default: 'default',
|
|
50
|
+
type: String as PropType<'default' | 'exact'>,
|
|
51
|
+
},
|
|
42
52
|
});
|
|
43
53
|
</script>
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
<tr v-if="newCheckedRows.length">
|
|
86
86
|
<td
|
|
87
87
|
:colspan="columnCount"
|
|
88
|
-
class="sticky z-
|
|
88
|
+
class="sticky z-[1] p-0"
|
|
89
89
|
:style="{
|
|
90
90
|
top: theadHeight + 'px',
|
|
91
91
|
}"
|
|
@@ -789,7 +789,7 @@ defineExpose({
|
|
|
789
789
|
@apply bg-slate-50;
|
|
790
790
|
@apply sticky;
|
|
791
791
|
@apply top-0;
|
|
792
|
-
@apply z-
|
|
792
|
+
@apply z-[1];
|
|
793
793
|
@apply border-b border-slate-300;
|
|
794
794
|
@apply bg-opacity-75 backdrop-blur backdrop-filter;
|
|
795
795
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import BaseTagAutocomplete from './BaseTagAutocomplete.vue';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
title: 'Form/BaseTagAutocomplete',
|
|
5
|
+
component: BaseTagAutocomplete,
|
|
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
|
+
],
|
|
15
|
+
},
|
|
16
|
+
decorators: [() => ({ template: '<div class="mb-36"><story/></div>' })],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const Template = (args) => ({
|
|
20
|
+
components: { BaseTagAutocomplete },
|
|
21
|
+
setup() {
|
|
22
|
+
const value = ref(null);
|
|
23
|
+
return { args, value };
|
|
24
|
+
},
|
|
25
|
+
template: `
|
|
26
|
+
<BaseTagAutocomplete v-model="value" v-bind="args"></BaseTagAutocomplete>
|
|
27
|
+
<p class="mt-5 text-sm">Value: <span class="bg-slate-200 font-mono px-1 py-px rounded">{{ value ?? 'NULL' }}</span></p>
|
|
28
|
+
`,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const Demo = Template.bind({});
|
|
32
|
+
Demo.args = {};
|
|
33
|
+
|
|
34
|
+
export const Disabled = Template.bind({});
|
|
35
|
+
Disabled.args = {
|
|
36
|
+
options: [],
|
|
37
|
+
disabled: true,
|
|
38
|
+
modelValue: [{ label: 'Dark Maul', value: '1' }],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const Loading = Template.bind({});
|
|
42
|
+
Loading.args = {
|
|
43
|
+
loading: true,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const SlotOption = (args) => ({
|
|
47
|
+
components: { BaseTagAutocomplete },
|
|
48
|
+
setup() {
|
|
49
|
+
const value = ref(null);
|
|
50
|
+
|
|
51
|
+
const options = [
|
|
52
|
+
{ label: 'Red', value: 'red' },
|
|
53
|
+
{ label: 'Blue', value: 'blue' },
|
|
54
|
+
{ label: 'Green', value: 'green' },
|
|
55
|
+
{ label: 'Black', value: 'black' },
|
|
56
|
+
{ label: 'Gray', value: 'gray' },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
return { value, options, args };
|
|
60
|
+
},
|
|
61
|
+
template: `
|
|
62
|
+
<BaseTagAutocomplete
|
|
63
|
+
v-bind="args"
|
|
64
|
+
v-model="value"
|
|
65
|
+
:options="options"
|
|
66
|
+
>
|
|
67
|
+
<template #option="{ option, active, selected }">
|
|
68
|
+
<div
|
|
69
|
+
class="rounded px-2 font-semibold py-1 text-sm"
|
|
70
|
+
:class="{
|
|
71
|
+
'hover:bg-slate-100': !active && !selected,
|
|
72
|
+
'bg-slate-200 hover:bg-slate-300': active && !selected,
|
|
73
|
+
'bg-blue-500 text-white hover:bg-blue-600': !active && selected,
|
|
74
|
+
'bg-blue-600 text-white hover:bg-blue-700': active && selected,
|
|
75
|
+
}"
|
|
76
|
+
:style="{ color: selected ? '' : option.value }"
|
|
77
|
+
>
|
|
78
|
+
{{ option.label }}
|
|
79
|
+
</div>
|
|
80
|
+
</template>
|
|
81
|
+
</BaseTagAutocomplete>
|
|
82
|
+
`,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export const SlotFooter = (args) => {
|
|
86
|
+
return {
|
|
87
|
+
components: { BaseTagAutocomplete },
|
|
88
|
+
setup() {
|
|
89
|
+
const value = ref(null);
|
|
90
|
+
function onClick() {
|
|
91
|
+
alert(1);
|
|
92
|
+
}
|
|
93
|
+
return { args, value, onClick };
|
|
94
|
+
},
|
|
95
|
+
template: `
|
|
96
|
+
<BaseTagAutocomplete
|
|
97
|
+
v-model="value"
|
|
98
|
+
v-bind="args"
|
|
99
|
+
>
|
|
100
|
+
<template #footer>
|
|
101
|
+
<div class="text-center p-2 border-t">
|
|
102
|
+
<button @click=onClick class="btn btn-sm w-full btn-slate-200-outline">This is the footer 💯</button>
|
|
103
|
+
</div>
|
|
104
|
+
</template>
|
|
105
|
+
</BaseTagAutocomplete>
|
|
106
|
+
`,
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const SlotEmpty = (args) => {
|
|
111
|
+
return {
|
|
112
|
+
components: { BaseTagAutocomplete },
|
|
113
|
+
setup() {
|
|
114
|
+
const value = ref(null);
|
|
115
|
+
return { args, value };
|
|
116
|
+
},
|
|
117
|
+
template: `
|
|
118
|
+
<BaseTagAutocomplete
|
|
119
|
+
v-model="value"
|
|
120
|
+
v-bind="args"
|
|
121
|
+
:options="[]"
|
|
122
|
+
>
|
|
123
|
+
<template #empty>
|
|
124
|
+
<div class="text-center p-6 py-10 flex items-center justify-center">🤓🤓🤓</div>
|
|
125
|
+
</template>
|
|
126
|
+
</BaseTagAutocomplete>
|
|
127
|
+
`,
|
|
128
|
+
};
|
|
129
|
+
};
|
|
@@ -9,12 +9,19 @@
|
|
|
9
9
|
>
|
|
10
10
|
<div
|
|
11
11
|
class="flex items-stretch rounded border"
|
|
12
|
-
:class="
|
|
12
|
+
:class="[
|
|
13
|
+
disabled ? 'cursor-not-allowed opacity-60' : '',
|
|
14
|
+
selectionClass(selection),
|
|
15
|
+
]"
|
|
13
16
|
>
|
|
14
|
-
<div
|
|
17
|
+
<div
|
|
18
|
+
class="py-[5px] pl-3 text-sm"
|
|
19
|
+
:class="[disabled ? 'pr-3' : 'pr-1']"
|
|
20
|
+
>
|
|
15
21
|
{{ selection.label }}
|
|
16
22
|
</div>
|
|
17
23
|
<button
|
|
24
|
+
v-if="!disabled"
|
|
18
25
|
type="button"
|
|
19
26
|
class="flex shrink-0 appearance-none items-center justify-center border-0 bg-transparent pl-1 pr-3 text-xs outline-none"
|
|
20
27
|
@click="dontLooseFocus($event, () => removeOption(selection))"
|
|
@@ -27,6 +34,7 @@
|
|
|
27
34
|
<div class="grow p-0.5">
|
|
28
35
|
<input
|
|
29
36
|
ref="input"
|
|
37
|
+
:disabled="disabled"
|
|
30
38
|
:value="keywords"
|
|
31
39
|
type="text"
|
|
32
40
|
:placeholder="$t('sui.select_an_item')"
|
|
@@ -44,34 +52,73 @@
|
|
|
44
52
|
<div class="relative">
|
|
45
53
|
<div
|
|
46
54
|
v-show="showDropdown"
|
|
47
|
-
class="absolute top-1 z-
|
|
55
|
+
class="absolute top-1 z-menu min-h-[110px] w-full overflow-hidden rounded border border-slate-300 bg-white shadow-md"
|
|
48
56
|
>
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
class="block w-full cursor-pointer appearance-none rounded-sm border-none px-2 py-1 text-left text-sm focus:outline-none"
|
|
57
|
-
:class="optionClass(option)"
|
|
58
|
-
type="button"
|
|
59
|
-
tabindex="-1"
|
|
60
|
-
@click="dontLooseFocus($event, () => addOption(option))"
|
|
61
|
-
@mousedown="dontLooseFocus"
|
|
57
|
+
<div
|
|
58
|
+
ref="dropdown"
|
|
59
|
+
class="max-h-[214px] min-h-[75px] w-full overflow-y-auto"
|
|
60
|
+
>
|
|
61
|
+
<slot v-if="filteredNormalizedOptions.length == 0" name="empty">
|
|
62
|
+
<div
|
|
63
|
+
class="flex items-center justify-center px-5 py-10 text-center text-slate-600"
|
|
62
64
|
>
|
|
63
|
-
{{
|
|
64
|
-
</
|
|
65
|
-
</
|
|
66
|
-
|
|
65
|
+
{{ $t('sui.nothing_found') }}
|
|
66
|
+
</div>
|
|
67
|
+
</slot>
|
|
68
|
+
|
|
69
|
+
<ul v-else class="p-1">
|
|
70
|
+
<li
|
|
71
|
+
v-for="option in filteredNormalizedOptions"
|
|
72
|
+
:key="option.value"
|
|
73
|
+
class="block"
|
|
74
|
+
>
|
|
75
|
+
<button
|
|
76
|
+
class="block w-full cursor-pointer appearance-none border-none text-left focus:outline-none"
|
|
77
|
+
type="button"
|
|
78
|
+
tabindex="-1"
|
|
79
|
+
@click="onSelect(option)"
|
|
80
|
+
@mousedown.prevent="dontLooseFocus"
|
|
81
|
+
>
|
|
82
|
+
<slot
|
|
83
|
+
name="option"
|
|
84
|
+
:option="option.option"
|
|
85
|
+
:selected="optionValues.includes(option.value)"
|
|
86
|
+
:active="optionActive && optionActive.value == option.value"
|
|
87
|
+
>
|
|
88
|
+
<div
|
|
89
|
+
class="rounded px-2 py-1 text-sm"
|
|
90
|
+
:class="optionClass(option)"
|
|
91
|
+
>
|
|
92
|
+
{{ option.label }}
|
|
93
|
+
</div>
|
|
94
|
+
</slot>
|
|
95
|
+
</button>
|
|
96
|
+
</li>
|
|
97
|
+
</ul>
|
|
98
|
+
</div>
|
|
67
99
|
|
|
68
|
-
<
|
|
69
|
-
<div class="
|
|
70
|
-
|
|
100
|
+
<div ref="footer">
|
|
101
|
+
<div v-if="$slots.footer" class="bg-white">
|
|
102
|
+
<slot :options="filteredNormalizedOptions" name="footer" />
|
|
71
103
|
</div>
|
|
72
|
-
</
|
|
104
|
+
</div>
|
|
73
105
|
|
|
74
|
-
<
|
|
106
|
+
<div
|
|
107
|
+
v-if="loading"
|
|
108
|
+
class="absolute inset-0 h-full w-full space-y-1 bg-white p-2"
|
|
109
|
+
>
|
|
110
|
+
<div class="space-y-1">
|
|
111
|
+
<BaseSkeleton class="h-7 w-full" delay="0ms"></BaseSkeleton>
|
|
112
|
+
<BaseSkeleton
|
|
113
|
+
class="h-7 w-full opacity-70"
|
|
114
|
+
delay="50ms"
|
|
115
|
+
></BaseSkeleton>
|
|
116
|
+
<BaseSkeleton
|
|
117
|
+
class="h-7 w-full opacity-30"
|
|
118
|
+
delay="100ms"
|
|
119
|
+
></BaseSkeleton>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
75
122
|
</div>
|
|
76
123
|
</div>
|
|
77
124
|
</div>
|
|
@@ -81,9 +128,9 @@
|
|
|
81
128
|
import { cloneDeep, get } from 'lodash';
|
|
82
129
|
import { PropType, Ref, ComputedRef } from 'vue';
|
|
83
130
|
import { NormalizedOption, Option, OptionValue } from '@/types/types';
|
|
84
|
-
import { useInfiniteScroll } from '@vueuse/core';
|
|
85
|
-
import BaseLoadingCover from './BaseLoadingCover.vue';
|
|
131
|
+
import { useInfiniteScroll, useMutationObserver } from '@vueuse/core';
|
|
86
132
|
import { useNotificationsStore } from '@/stores/notifications';
|
|
133
|
+
import BaseSkeleton from './BaseSkeleton.vue';
|
|
87
134
|
|
|
88
135
|
const props = defineProps({
|
|
89
136
|
modelValue: {
|
|
@@ -122,13 +169,13 @@ const props = defineProps({
|
|
|
122
169
|
default: false,
|
|
123
170
|
type: Boolean,
|
|
124
171
|
},
|
|
125
|
-
|
|
172
|
+
max: {
|
|
126
173
|
default: undefined,
|
|
127
174
|
type: Number,
|
|
128
175
|
},
|
|
129
|
-
|
|
176
|
+
filter: {
|
|
130
177
|
default: undefined,
|
|
131
|
-
type:
|
|
178
|
+
type: Function as PropType<(option: NormalizedOption) => boolean>,
|
|
132
179
|
},
|
|
133
180
|
});
|
|
134
181
|
|
|
@@ -156,14 +203,14 @@ onMounted(() => {
|
|
|
156
203
|
() => {
|
|
157
204
|
emit('scrollBottom');
|
|
158
205
|
},
|
|
159
|
-
{ distance:
|
|
206
|
+
{ distance: 60 }
|
|
160
207
|
);
|
|
161
208
|
});
|
|
162
209
|
|
|
163
210
|
const optionActive = computed(() => {
|
|
164
211
|
return (
|
|
165
|
-
|
|
166
|
-
Math.min(selectionIndex.value,
|
|
212
|
+
filteredNormalizedOptions.value[
|
|
213
|
+
Math.min(selectionIndex.value, filteredNormalizedOptions.value.length - 1)
|
|
167
214
|
] ?? null
|
|
168
215
|
);
|
|
169
216
|
});
|
|
@@ -182,13 +229,25 @@ const normalizedModelValue = computed(() => {
|
|
|
182
229
|
}) as ComputedRef<NormalizedOption[]>;
|
|
183
230
|
|
|
184
231
|
const normalizedOptions = computed(() => {
|
|
185
|
-
return props.options
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
+
const filteredNormalizedOptions = computed((): NormalizedOption[] => {
|
|
242
|
+
return normalizedOptions.value
|
|
243
|
+
.filter((option) => {
|
|
244
|
+
if (props.filter !== undefined) {
|
|
245
|
+
return props.filter(option);
|
|
246
|
+
}
|
|
247
|
+
if (!option.label) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
return option.label.toLowerCase().includes(keywords.value.toLowerCase());
|
|
192
251
|
})
|
|
193
252
|
.filter((option) => {
|
|
194
253
|
return !hasSelectedOption(option.value);
|
|
@@ -205,6 +264,15 @@ const hasSelectedOption = (value: OptionValue): boolean => {
|
|
|
205
264
|
return optionValues.value.includes(value);
|
|
206
265
|
};
|
|
207
266
|
|
|
267
|
+
function preventUnfocus(elements: HTMLElement[]) {
|
|
268
|
+
elements.forEach((e) => {
|
|
269
|
+
e.removeEventListener('mousedown', dontLooseFocus);
|
|
270
|
+
});
|
|
271
|
+
elements.forEach((e) => {
|
|
272
|
+
e.addEventListener('mousedown', dontLooseFocus);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
208
276
|
const dontLooseFocus = (event: Event, next: null | (() => void) = null) => {
|
|
209
277
|
event.preventDefault();
|
|
210
278
|
inputElement.value?.focus();
|
|
@@ -274,13 +342,24 @@ const onTextKeydown = (event: KeyboardEvent) => {
|
|
|
274
342
|
};
|
|
275
343
|
|
|
276
344
|
const optionClass = (option: NormalizedOption) => {
|
|
277
|
-
|
|
278
|
-
|
|
345
|
+
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);
|
|
349
|
+
|
|
350
|
+
if (selected) {
|
|
351
|
+
if (active) {
|
|
352
|
+
return 'bg-blue-600 hover:bg-blue-700 text-white';
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return 'bg-blue-500 hover:bg-blue-600 text-white';
|
|
279
356
|
}
|
|
280
|
-
|
|
281
|
-
|
|
357
|
+
|
|
358
|
+
if (active) {
|
|
359
|
+
return 'bg-slate-200 hover:bg-slate-300';
|
|
282
360
|
}
|
|
283
|
-
|
|
361
|
+
|
|
362
|
+
return 'bg-white hover:bg-slate-100';
|
|
284
363
|
};
|
|
285
364
|
|
|
286
365
|
const selectionClass = (selection: NormalizedOption): string => {
|
|
@@ -293,24 +372,18 @@ const selectionClass = (selection: NormalizedOption): string => {
|
|
|
293
372
|
return 'bg-slate-200 border-slate-300';
|
|
294
373
|
};
|
|
295
374
|
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
const toggleOption = (option: NormalizedOption) => {
|
|
302
|
-
if (hasSelectedOption(option.value)) {
|
|
303
|
-
removeOption(option);
|
|
304
|
-
} else {
|
|
305
|
-
addOption(option);
|
|
306
|
-
}
|
|
375
|
+
const onSelect = (normalizedModelValue: NormalizedOption) => {
|
|
376
|
+
addOption(normalizedModelValue);
|
|
377
|
+
inputElement.value?.blur();
|
|
307
378
|
};
|
|
308
379
|
|
|
309
380
|
const addOption = (option: NormalizedOption) => {
|
|
310
381
|
if (props.max && normalizedModelValue.value.length >= props.max) {
|
|
311
382
|
notifications.push({
|
|
312
383
|
title: i18n.t('sui.whoops'),
|
|
313
|
-
text: i18n.t('sui.you_cannot_select_more_than_x_items', {
|
|
384
|
+
text: i18n.t('sui.you_cannot_select_more_than_x_items', {
|
|
385
|
+
count: props.max,
|
|
386
|
+
}),
|
|
314
387
|
color: 'warning',
|
|
315
388
|
});
|
|
316
389
|
return;
|
|
@@ -383,4 +456,29 @@ const validateSelectionIndex = () => {
|
|
|
383
456
|
);
|
|
384
457
|
});
|
|
385
458
|
};
|
|
459
|
+
|
|
460
|
+
const setKeywords = (input: string) => {
|
|
461
|
+
keywords.value = input;
|
|
462
|
+
emit('typing', input);
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
const footer = ref(null) as Ref<HTMLDivElement | null>;
|
|
466
|
+
|
|
467
|
+
function preventUnfocusOnFooter() {
|
|
468
|
+
const elements = (footer.value?.querySelectorAll('button, a') ??
|
|
469
|
+
[]) as HTMLElement[];
|
|
470
|
+
preventUnfocus(elements);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
onMounted(() => {
|
|
474
|
+
preventUnfocusOnFooter();
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
useMutationObserver(
|
|
478
|
+
footer,
|
|
479
|
+
() => {
|
|
480
|
+
preventUnfocusOnFooter();
|
|
481
|
+
},
|
|
482
|
+
{ attributes: false, childList: true }
|
|
483
|
+
);
|
|
386
484
|
</script>
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import BaseTagAutocompleteFetch from './BaseTagAutocompleteFetch.vue';
|
|
2
|
+
import BaseApp from './BaseApp.vue';
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
title: 'Form/BaseTagAutocompleteFetch',
|
|
6
|
+
component: BaseTagAutocompleteFetch,
|
|
7
|
+
argTypes: {},
|
|
8
|
+
args: {
|
|
9
|
+
url: 'https://effettandem.com/api/content/articles',
|
|
10
|
+
labelKey: 'title',
|
|
11
|
+
valueKey: 'id',
|
|
12
|
+
disabled: false,
|
|
13
|
+
},
|
|
14
|
+
decorators: [() => ({ template: '<div class="mb-36"><story/></div>' })],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const Template = (args) => {
|
|
18
|
+
return {
|
|
19
|
+
components: { BaseTagAutocompleteFetch, BaseApp },
|
|
20
|
+
setup() {
|
|
21
|
+
const value = ref([]);
|
|
22
|
+
return { args, value };
|
|
23
|
+
},
|
|
24
|
+
template: `
|
|
25
|
+
<BaseTagAutocompleteFetch
|
|
26
|
+
v-model="value"
|
|
27
|
+
v-bind="args"
|
|
28
|
+
></BaseTagAutocompleteFetch>
|
|
29
|
+
<p class="mt-5 text-sm">Value: <span class="bg-slate-200 font-mono px-1 py-px rounded">{{ value ?? 'NULL' }}</span></p>
|
|
30
|
+
<BaseApp />
|
|
31
|
+
`,
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const Demo = Template.bind({});
|
|
36
|
+
Demo.args = {};
|
|
37
|
+
|
|
38
|
+
export const Disabled = Template.bind({});
|
|
39
|
+
Disabled.args = {
|
|
40
|
+
modelValue: [{ label: 'Dark Maul', value: '1' }],
|
|
41
|
+
labelKey: 'label',
|
|
42
|
+
valueKey: 'value',
|
|
43
|
+
disabled: true,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const Maximum = Template.bind({});
|
|
47
|
+
Maximum.args = {
|
|
48
|
+
max: 3,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const SlotOption = (args) => {
|
|
52
|
+
return {
|
|
53
|
+
components: { BaseTagAutocompleteFetch },
|
|
54
|
+
setup() {
|
|
55
|
+
const value = ref([]);
|
|
56
|
+
return { args, value };
|
|
57
|
+
},
|
|
58
|
+
template: `
|
|
59
|
+
<div class="mb-20">
|
|
60
|
+
<BaseTagAutocompleteFetch
|
|
61
|
+
v-model="value"
|
|
62
|
+
v-bind="args"
|
|
63
|
+
>
|
|
64
|
+
<template #option="{ option, active, selected }">
|
|
65
|
+
<div
|
|
66
|
+
class="rounded px-2 py-1"
|
|
67
|
+
:class="{
|
|
68
|
+
'hover:bg-slate-100': !active && !selected,
|
|
69
|
+
'bg-slate-200 hover:bg-slate-300': active && !selected,
|
|
70
|
+
'bg-blue-500 text-white hover:bg-blue-600': !active && selected,
|
|
71
|
+
'bg-blue-600 text-white hover:bg-blue-700': active && selected,
|
|
72
|
+
}"
|
|
73
|
+
>
|
|
74
|
+
<p class="text-sm font-medium">{{ option.title }}</p>
|
|
75
|
+
<p class="opacity-60 text-xs">{{ option.owner?.name }}</p>
|
|
76
|
+
</div>
|
|
77
|
+
</template>
|
|
78
|
+
</BaseTagAutocompleteFetch>
|
|
79
|
+
</div>
|
|
80
|
+
`,
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const SlotFooter = (args) => {
|
|
85
|
+
return {
|
|
86
|
+
components: { BaseTagAutocompleteFetch },
|
|
87
|
+
setup() {
|
|
88
|
+
const value = ref([]);
|
|
89
|
+
function onClick() {
|
|
90
|
+
alert(1);
|
|
91
|
+
}
|
|
92
|
+
return { args, value, onClick };
|
|
93
|
+
},
|
|
94
|
+
template: `
|
|
95
|
+
<BaseTagAutocompleteFetch
|
|
96
|
+
v-model="value"
|
|
97
|
+
v-bind="args"
|
|
98
|
+
>
|
|
99
|
+
<template #footer>
|
|
100
|
+
<div class="text-center p-2 border-t">
|
|
101
|
+
<button @click=onClick class="btn btn-sm w-full btn-slate-200-outline">This is the footer 💯</button>
|
|
102
|
+
</div>
|
|
103
|
+
</template>
|
|
104
|
+
</BaseTagAutocompleteFetch>
|
|
105
|
+
`,
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const SlotEmpty = (args) => {
|
|
110
|
+
return {
|
|
111
|
+
components: { BaseTagAutocompleteFetch },
|
|
112
|
+
setup() {
|
|
113
|
+
const value = ref([]);
|
|
114
|
+
return { args, value };
|
|
115
|
+
},
|
|
116
|
+
template: `
|
|
117
|
+
<BaseTagAutocompleteFetch
|
|
118
|
+
v-model="value"
|
|
119
|
+
v-bind="args"
|
|
120
|
+
>
|
|
121
|
+
<template #empty="props">
|
|
122
|
+
<div>
|
|
123
|
+
<div v-if="props.firstSearch" class="text-center py-10 p-6">🤓🤓🤓</div>
|
|
124
|
+
<div v-else class="text-center p-6">Start your search... 🔎</div>
|
|
125
|
+
</div>
|
|
126
|
+
</template>
|
|
127
|
+
</BaseTagAutocompleteFetch>
|
|
128
|
+
`,
|
|
129
|
+
};
|
|
130
|
+
};
|