sprintify-ui 0.0.97 → 0.0.99
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 +8010 -7924
- package/dist/types/src/components/BaseAutocomplete.vue.d.ts +32 -10
- package/dist/types/src/components/BaseAutocompleteDropdown.vue.d.ts +88 -0
- package/dist/types/src/components/BaseAutocompleteFetch.vue.d.ts +20 -5
- package/dist/types/src/components/BaseBelongsTo.vue.d.ts +18 -3
- package/dist/types/src/components/BaseDatePicker.vue.d.ts +1 -1
- package/dist/types/src/components/BaseHasMany.vue.d.ts +17 -0
- package/dist/types/src/components/BaseLoadingCover.vue.d.ts +1 -1
- package/dist/types/src/components/BaseSwitch.vue.d.ts +1 -1
- package/dist/types/src/components/BaseTagAutocomplete.vue.d.ts +66 -6
- package/dist/types/src/components/BaseTagAutocompleteFetch.vue.d.ts +19 -2
- package/dist/types/src/composables/clickOutside.d.ts +1 -1
- package/dist/types/src/index.d.ts +2 -0
- package/package.json +10 -10
- package/src/components/BaseAutocomplete.vue +53 -183
- package/src/components/BaseAutocompleteDropdown.vue +344 -0
- package/src/components/BaseAutocompleteFetch.vue +8 -3
- package/src/components/BaseDropdown.vue +2 -2
- package/src/components/BaseModalSide.stories.js +1 -3
- package/src/components/BaseModalSide.vue +5 -5
- package/src/components/BaseTagAutocomplete.stories.js +46 -1
- package/src/components/BaseTagAutocomplete.vue +184 -253
- package/src/components/BaseTagAutocompleteFetch.stories.js +4 -4
- package/src/components/BaseTagAutocompleteFetch.vue +10 -5
- package/src/composables/clickOutside.ts +4 -2
- package/src/index.ts +3 -1
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div>
|
|
2
|
+
<div ref="autocomplete">
|
|
3
3
|
<div
|
|
4
|
-
class="
|
|
5
|
-
:class="[
|
|
4
|
+
class="rounded border bg-white p-1"
|
|
5
|
+
:class="[
|
|
6
|
+
hasErrorInternal ? 'border-red-600' : 'border-slate-300',
|
|
7
|
+
wrapperClass,
|
|
8
|
+
]"
|
|
6
9
|
>
|
|
7
10
|
<div class="-m-0.5 flex flex-wrap">
|
|
8
11
|
<div
|
|
@@ -17,18 +20,14 @@
|
|
|
17
20
|
selectionClass(selection),
|
|
18
21
|
]"
|
|
19
22
|
>
|
|
20
|
-
<div
|
|
21
|
-
class="py-[5px] pl-3 text-sm"
|
|
22
|
-
:class="[disabled ? 'pr-3' : 'pr-1']"
|
|
23
|
-
>
|
|
23
|
+
<div :class="[selectionLabelClass]">
|
|
24
24
|
{{ selection.label }}
|
|
25
25
|
</div>
|
|
26
26
|
<button
|
|
27
27
|
v-if="!disabled"
|
|
28
28
|
type="button"
|
|
29
29
|
class="flex shrink-0 appearance-none items-center justify-center border-0 bg-transparent pl-1 pr-3 text-xs outline-none"
|
|
30
|
-
@click="
|
|
31
|
-
@mousedown="dontLooseFocus"
|
|
30
|
+
@click="removeOption(selection)"
|
|
32
31
|
>
|
|
33
32
|
✕
|
|
34
33
|
</button>
|
|
@@ -36,14 +35,15 @@
|
|
|
36
35
|
</div>
|
|
37
36
|
<div class="grow p-0.5">
|
|
38
37
|
<input
|
|
39
|
-
ref="
|
|
40
|
-
:disabled="disabled"
|
|
38
|
+
ref="inputElement"
|
|
41
39
|
:value="keywords"
|
|
42
40
|
type="text"
|
|
43
|
-
:placeholder="$t('sui.select_an_item')"
|
|
44
|
-
class="
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
:placeholder="placeholder ? placeholder : $t('sui.select_an_item')"
|
|
42
|
+
class="w-full min-w-[50px] border-none p-0 pl-1 shadow-none outline-none focus:border-none focus:shadow-none focus:outline-none focus:ring-0 disabled:cursor-not-allowed"
|
|
43
|
+
:class="[inputClass]"
|
|
44
|
+
autocomplete="off"
|
|
45
|
+
:disabled="disabled"
|
|
46
|
+
@click="open"
|
|
47
47
|
@input="onTextInput"
|
|
48
48
|
@keydown="onTextKeydown"
|
|
49
49
|
/>
|
|
@@ -53,74 +53,34 @@
|
|
|
53
53
|
|
|
54
54
|
<div class="relative">
|
|
55
55
|
<div
|
|
56
|
-
v-
|
|
57
|
-
class="
|
|
56
|
+
v-if="opened || dropdownShow == 'always'"
|
|
57
|
+
:class="[
|
|
58
|
+
inline
|
|
59
|
+
? 'relative mt-1'
|
|
60
|
+
: 'absolute top-1 z-menu min-h-[110px] w-full overflow-hidden rounded border border-slate-300 bg-white shadow-md',
|
|
61
|
+
]"
|
|
58
62
|
>
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
</div>
|
|
70
|
-
</slot>
|
|
71
|
-
|
|
72
|
-
<ul v-else class="p-1">
|
|
73
|
-
<li
|
|
74
|
-
v-for="option in filteredNormalizedOptions"
|
|
75
|
-
:key="option.value"
|
|
76
|
-
class="block"
|
|
77
|
-
>
|
|
78
|
-
<button
|
|
79
|
-
class="block w-full cursor-pointer appearance-none border-none text-left focus:outline-none"
|
|
80
|
-
type="button"
|
|
81
|
-
tabindex="-1"
|
|
82
|
-
@click="onSelect(option)"
|
|
83
|
-
@mousedown.prevent="dontLooseFocus"
|
|
84
|
-
>
|
|
85
|
-
<slot
|
|
86
|
-
name="option"
|
|
87
|
-
:option="option.option"
|
|
88
|
-
:active="optionActive && optionActive.value == option.value"
|
|
89
|
-
>
|
|
90
|
-
<div
|
|
91
|
-
class="rounded px-2 py-1 text-sm"
|
|
92
|
-
:class="optionClass(option)"
|
|
93
|
-
>
|
|
94
|
-
{{ option.label }}
|
|
95
|
-
</div>
|
|
96
|
-
</slot>
|
|
97
|
-
</button>
|
|
98
|
-
</li>
|
|
99
|
-
</ul>
|
|
100
|
-
</div>
|
|
101
|
-
|
|
102
|
-
<div ref="footer">
|
|
103
|
-
<div v-if="$slots.footer" class="bg-white">
|
|
104
|
-
<slot :options="filteredNormalizedOptions" name="footer" />
|
|
105
|
-
</div>
|
|
106
|
-
</div>
|
|
107
|
-
|
|
108
|
-
<div
|
|
109
|
-
v-if="loading"
|
|
110
|
-
class="absolute inset-0 h-full w-full space-y-1 bg-white p-2"
|
|
63
|
+
<BaseAutocompleteDropdown
|
|
64
|
+
:selected="normalizedModelValue"
|
|
65
|
+
:options="filteredNormalizedOptions"
|
|
66
|
+
:size="size"
|
|
67
|
+
:loading="loading"
|
|
68
|
+
:loading-bottom="loadingBottom"
|
|
69
|
+
:dropdown-class="inline ? '' : 'p-1'"
|
|
70
|
+
:keywords="keywords"
|
|
71
|
+
@select="onSelect"
|
|
72
|
+
@scroll-bottom="emit('scrollBottom')"
|
|
111
73
|
>
|
|
112
|
-
<
|
|
113
|
-
<
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
</div>
|
|
123
|
-
</div>
|
|
74
|
+
<template #empty="emptyProps">
|
|
75
|
+
<slot name="empty" v-bind="{ ...emptyProps, ...slotProps }" />
|
|
76
|
+
</template>
|
|
77
|
+
<template #option="optionProps">
|
|
78
|
+
<slot name="option" v-bind="{ ...optionProps, ...slotProps }" />
|
|
79
|
+
</template>
|
|
80
|
+
<template #footer="footerProps">
|
|
81
|
+
<slot name="footer" v-bind="{ ...footerProps, ...slotProps }" />
|
|
82
|
+
</template>
|
|
83
|
+
</BaseAutocompleteDropdown>
|
|
124
84
|
</div>
|
|
125
85
|
</div>
|
|
126
86
|
</div>
|
|
@@ -130,11 +90,14 @@
|
|
|
130
90
|
import { cloneDeep, get } from 'lodash';
|
|
131
91
|
import { PropType, Ref, ComputedRef } from 'vue';
|
|
132
92
|
import { NormalizedOption, Option } from '@/types';
|
|
133
|
-
import { useInfiniteScroll, useMutationObserver } from '@vueuse/core';
|
|
134
|
-
import { useNotificationsStore } from '@/stores/notifications';
|
|
135
|
-
import BaseSkeleton from '@/components/BaseSkeleton.vue';
|
|
136
93
|
import { useHasOptions } from '@/composables/hasOptions';
|
|
137
94
|
import { useField } from '@/composables/field';
|
|
95
|
+
import { useClickOutside } from '@/composables/clickOutside';
|
|
96
|
+
import { useNotificationsStore } from '@/stores/notifications';
|
|
97
|
+
import BaseAutocompleteDropdown from './BaseAutocompleteDropdown.vue';
|
|
98
|
+
|
|
99
|
+
const i18n = useI18n();
|
|
100
|
+
const notifications = useNotificationsStore();
|
|
138
101
|
|
|
139
102
|
const props = defineProps({
|
|
140
103
|
modelValue: {
|
|
@@ -165,6 +128,10 @@ const props = defineProps({
|
|
|
165
128
|
default: false,
|
|
166
129
|
type: Boolean,
|
|
167
130
|
},
|
|
131
|
+
loadingBottom: {
|
|
132
|
+
default: false,
|
|
133
|
+
type: Boolean,
|
|
134
|
+
},
|
|
168
135
|
required: {
|
|
169
136
|
default: false,
|
|
170
137
|
type: Boolean,
|
|
@@ -185,12 +152,25 @@ const props = defineProps({
|
|
|
185
152
|
default: false,
|
|
186
153
|
type: Boolean,
|
|
187
154
|
},
|
|
155
|
+
inline: {
|
|
156
|
+
default: false,
|
|
157
|
+
type: Boolean,
|
|
158
|
+
},
|
|
159
|
+
size: {
|
|
160
|
+
default: 'base',
|
|
161
|
+
type: String as PropType<'xs' | 'sm' | 'base'>,
|
|
162
|
+
},
|
|
163
|
+
dropdownShow: {
|
|
164
|
+
default: 'focus',
|
|
165
|
+
type: String as PropType<'focus' | 'always'>,
|
|
166
|
+
},
|
|
188
167
|
});
|
|
189
168
|
|
|
190
169
|
const emit = defineEmits([
|
|
191
170
|
'update:modelValue',
|
|
192
171
|
'typing',
|
|
193
|
-
'
|
|
172
|
+
'open',
|
|
173
|
+
'close',
|
|
194
174
|
'scrollBottom',
|
|
195
175
|
]);
|
|
196
176
|
|
|
@@ -201,17 +181,6 @@ const { hasErrorInternal, emitUpdate } = useField({
|
|
|
201
181
|
emit: emit,
|
|
202
182
|
});
|
|
203
183
|
|
|
204
|
-
const i18n = useI18n();
|
|
205
|
-
const notifications = useNotificationsStore();
|
|
206
|
-
|
|
207
|
-
const timerId = ref(0);
|
|
208
|
-
const keywords = ref('');
|
|
209
|
-
const showDropdown = ref(false);
|
|
210
|
-
const inputElement = ref(null) as Ref<HTMLInputElement | null>;
|
|
211
|
-
const dropdown = ref(null) as Ref<HTMLElement | null>;
|
|
212
|
-
const activeIndex = ref(0);
|
|
213
|
-
const selectionToDelete = ref(null) as Ref<NormalizedOption | null>;
|
|
214
|
-
|
|
215
184
|
const hasOptions = useHasOptions(
|
|
216
185
|
computed(() => props.modelValue),
|
|
217
186
|
computed(() => props.options),
|
|
@@ -220,33 +189,25 @@ const hasOptions = useHasOptions(
|
|
|
220
189
|
computed(() => true)
|
|
221
190
|
);
|
|
222
191
|
|
|
192
|
+
const keywords = ref('');
|
|
193
|
+
const autocomplete = ref(null) as Ref<HTMLElement | null>;
|
|
194
|
+
const inputElement = ref(null) as Ref<HTMLInputElement | null>;
|
|
195
|
+
const shouldFilter = ref(false);
|
|
196
|
+
const opened = ref(false);
|
|
197
|
+
const selectionToDelete = ref(null) as Ref<NormalizedOption | null>;
|
|
198
|
+
|
|
223
199
|
const isSelected = hasOptions.isSelected;
|
|
224
200
|
const normalizedOptions = hasOptions.normalizedOptions;
|
|
225
201
|
const normalizedModelValue = hasOptions.normalizedModelValue as ComputedRef<
|
|
226
202
|
NormalizedOption[]
|
|
227
203
|
>;
|
|
228
204
|
|
|
229
|
-
onMounted(() => {
|
|
230
|
-
useInfiniteScroll(
|
|
231
|
-
dropdown.value,
|
|
232
|
-
() => {
|
|
233
|
-
emit('scrollBottom');
|
|
234
|
-
},
|
|
235
|
-
{ distance: 60 }
|
|
236
|
-
);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
const optionActive = computed(() => {
|
|
240
|
-
return (
|
|
241
|
-
filteredNormalizedOptions.value[
|
|
242
|
-
Math.min(activeIndex.value, filteredNormalizedOptions.value.length - 1)
|
|
243
|
-
] ?? null
|
|
244
|
-
);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
205
|
const filteredNormalizedOptions = computed((): NormalizedOption[] => {
|
|
248
206
|
return normalizedOptions.value
|
|
249
207
|
.filter((option) => {
|
|
208
|
+
if (shouldFilter.value === false) {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
250
211
|
if (props.filter !== undefined) {
|
|
251
212
|
return props.filter(option);
|
|
252
213
|
}
|
|
@@ -260,121 +221,61 @@ const filteredNormalizedOptions = computed((): NormalizedOption[] => {
|
|
|
260
221
|
});
|
|
261
222
|
});
|
|
262
223
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
});
|
|
267
|
-
elements.forEach((e) => {
|
|
268
|
-
e.addEventListener('mousedown', dontLooseFocus);
|
|
269
|
-
});
|
|
270
|
-
}
|
|
224
|
+
useClickOutside(autocomplete, () => {
|
|
225
|
+
close();
|
|
226
|
+
});
|
|
271
227
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if
|
|
276
|
-
|
|
228
|
+
function open() {
|
|
229
|
+
// Always focus as a safety
|
|
230
|
+
focus();
|
|
231
|
+
// Only emit open if value changes
|
|
232
|
+
if (!opened.value) {
|
|
233
|
+
opened.value = true;
|
|
234
|
+
emit('open');
|
|
277
235
|
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const onTextFocus = () => {
|
|
281
|
-
clearTimeout(timerId.value);
|
|
282
|
-
showDropdown.value = true;
|
|
283
|
-
emit('focus');
|
|
284
|
-
};
|
|
236
|
+
}
|
|
285
237
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
238
|
+
function close() {
|
|
239
|
+
shouldFilter.value = false;
|
|
240
|
+
opened.value = false;
|
|
241
|
+
blur();
|
|
242
|
+
emit('close');
|
|
243
|
+
}
|
|
291
244
|
|
|
292
245
|
const onTextInput = (event: Event) => {
|
|
293
|
-
|
|
294
|
-
|
|
246
|
+
open();
|
|
247
|
+
shouldFilter.value = true;
|
|
295
248
|
setKeywords(get(event, 'target.value', '') + '');
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
249
|
+
emit('typing', keywords.value);
|
|
250
|
+
|
|
251
|
+
selectionToDelete.value = null;
|
|
299
252
|
};
|
|
300
253
|
|
|
301
254
|
const onTextKeydown = (event: KeyboardEvent) => {
|
|
302
255
|
const key = event.key;
|
|
303
256
|
|
|
304
|
-
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (key === 'Backspace' && keywords.value == '') {
|
|
309
|
-
attemptRemoveLastSelection();
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if (key === 'ArrowDown') {
|
|
314
|
-
if (activeIndex.value < filteredNormalizedOptions.value.length - 1) {
|
|
315
|
-
activeIndex.value++;
|
|
316
|
-
} else {
|
|
317
|
-
activeIndex.value = 0;
|
|
318
|
-
}
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
257
|
+
// Prevent default behavior for up/down arrows
|
|
321
258
|
|
|
322
259
|
if (key === 'ArrowUp') {
|
|
323
|
-
|
|
324
|
-
activeIndex.value--;
|
|
325
|
-
} else {
|
|
326
|
-
activeIndex.value = Math.max(
|
|
327
|
-
0,
|
|
328
|
-
filteredNormalizedOptions.value.length - 1
|
|
329
|
-
);
|
|
330
|
-
}
|
|
260
|
+
event.preventDefault();
|
|
331
261
|
return;
|
|
332
262
|
}
|
|
333
263
|
|
|
334
|
-
if (key === '
|
|
264
|
+
if (key === 'ArrowDown') {
|
|
335
265
|
event.preventDefault();
|
|
336
|
-
if (optionActive.value) {
|
|
337
|
-
addOption(optionActive.value);
|
|
338
|
-
}
|
|
339
266
|
return;
|
|
340
267
|
}
|
|
341
|
-
};
|
|
342
|
-
|
|
343
|
-
const optionClass = (option: NormalizedOption) => {
|
|
344
|
-
const active = optionActive.value && optionActive.value.value == option.value;
|
|
345
|
-
const selected = isSelected(option);
|
|
346
|
-
|
|
347
|
-
if (selected) {
|
|
348
|
-
if (active) {
|
|
349
|
-
return 'bg-blue-600 hover:bg-blue-700 text-white';
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
return 'bg-blue-500 hover:bg-blue-600 text-white';
|
|
353
|
-
}
|
|
354
268
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
return 'bg-white hover:bg-slate-100';
|
|
360
|
-
};
|
|
361
|
-
|
|
362
|
-
const selectionClass = (selection: NormalizedOption): string => {
|
|
363
|
-
if (
|
|
364
|
-
selectionToDelete.value &&
|
|
365
|
-
selectionToDelete.value.value == selection.value
|
|
366
|
-
) {
|
|
367
|
-
return 'bg-red-200 border-red-300 text-red-800';
|
|
269
|
+
// Attempt to remove last selection on backspace
|
|
270
|
+
if (key === 'Backspace' && keywords.value == '') {
|
|
271
|
+
attemptRemoveLastSelection();
|
|
272
|
+
return;
|
|
368
273
|
}
|
|
369
|
-
return 'bg-slate-200 border-slate-300';
|
|
370
274
|
};
|
|
371
275
|
|
|
372
|
-
const onSelect = (
|
|
373
|
-
|
|
374
|
-
inputElement.value?.blur();
|
|
375
|
-
};
|
|
276
|
+
const onSelect = (option: NormalizedOption) => {
|
|
277
|
+
focus();
|
|
376
278
|
|
|
377
|
-
const addOption = (option: NormalizedOption) => {
|
|
378
279
|
if (props.max && normalizedModelValue.value.length >= props.max) {
|
|
379
280
|
notifications.push({
|
|
380
281
|
title: i18n.t('sui.whoops'),
|
|
@@ -390,11 +291,37 @@ const addOption = (option: NormalizedOption) => {
|
|
|
390
291
|
return;
|
|
391
292
|
}
|
|
392
293
|
|
|
393
|
-
|
|
394
|
-
|
|
294
|
+
selectionToDelete.value = null;
|
|
395
295
|
update([...normalizedModelValue.value, option]);
|
|
396
|
-
|
|
397
296
|
setKeywords('');
|
|
297
|
+
emit('typing', keywords.value);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
function update(value: NormalizedOption[]) {
|
|
301
|
+
// Re-activate filter
|
|
302
|
+
shouldFilter.value = false;
|
|
303
|
+
// Emit update
|
|
304
|
+
emitUpdate(value.map((v) => v.option));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function setKeywords(input: string) {
|
|
308
|
+
keywords.value = input;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function focus() {
|
|
312
|
+
inputElement.value?.focus();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function blur() {
|
|
316
|
+
inputElement.value?.blur();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const slotProps = {
|
|
320
|
+
focus,
|
|
321
|
+
blur,
|
|
322
|
+
open,
|
|
323
|
+
close,
|
|
324
|
+
keywords: computed(() => keywords.value),
|
|
398
325
|
};
|
|
399
326
|
|
|
400
327
|
const attemptRemoveLastSelection = () => {
|
|
@@ -418,61 +345,65 @@ const attemptRemoveLastSelection = () => {
|
|
|
418
345
|
};
|
|
419
346
|
|
|
420
347
|
const removeOption = (option: NormalizedOption) => {
|
|
348
|
+
focus();
|
|
421
349
|
let newModelValue = cloneDeep(normalizedModelValue.value);
|
|
422
350
|
newModelValue = newModelValue.filter((v) => v.value != option.value);
|
|
423
351
|
update(newModelValue);
|
|
424
352
|
};
|
|
425
353
|
|
|
426
|
-
|
|
427
|
-
emitUpdate(value.map((v) => v.option));
|
|
428
|
-
afterUpdate();
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const beforeAddOption = () => {
|
|
432
|
-
selectionToDelete.value = null;
|
|
433
|
-
clearInput();
|
|
434
|
-
};
|
|
435
|
-
|
|
436
|
-
const afterUpdate = () => {
|
|
437
|
-
validateActiveIndex();
|
|
438
|
-
};
|
|
354
|
+
// Element Classes
|
|
439
355
|
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
356
|
+
const wrapperClass = computed(() => {
|
|
357
|
+
if (props.size == 'xs') {
|
|
358
|
+
return 'min-h-[34px]';
|
|
359
|
+
}
|
|
360
|
+
if (props.size == 'sm') {
|
|
361
|
+
return 'min-h-[38px]';
|
|
362
|
+
}
|
|
363
|
+
return 'min-h-[42px]';
|
|
364
|
+
});
|
|
443
365
|
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
}
|
|
452
|
-
|
|
366
|
+
const inputClass = computed(() => {
|
|
367
|
+
const base = 'h-[32px] text-base';
|
|
368
|
+
if (props.size == 'xs') {
|
|
369
|
+
return base + ' xs:text-xs xs:h-[22px]';
|
|
370
|
+
}
|
|
371
|
+
if (props.size == 'sm') {
|
|
372
|
+
return base + ' xs:text-sm xs:h-[28px]';
|
|
373
|
+
}
|
|
374
|
+
return base;
|
|
375
|
+
});
|
|
453
376
|
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
377
|
+
const selectionClass = (selection: NormalizedOption): string => {
|
|
378
|
+
if (
|
|
379
|
+
selectionToDelete.value &&
|
|
380
|
+
selectionToDelete.value.value == selection.value
|
|
381
|
+
) {
|
|
382
|
+
return 'bg-red-200 border-red-300 text-red-800';
|
|
383
|
+
}
|
|
384
|
+
return 'bg-slate-200 border-slate-300';
|
|
457
385
|
};
|
|
458
386
|
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
[]
|
|
464
|
-
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
387
|
+
const selectionLabelClass = computed((): string => {
|
|
388
|
+
let base = 'py-[5px] pl-[0.75em] text-sm';
|
|
389
|
+
props.disabled ? (base += ' pr-[0.75em]') : (base += ' pr-1');
|
|
390
|
+
if (props.size == 'xs') {
|
|
391
|
+
const classes = base + ' xs:py-[3px] xs:pl-2 xs:text-xs';
|
|
392
|
+
return classes;
|
|
393
|
+
}
|
|
394
|
+
if (props.size == 'sm') {
|
|
395
|
+
const classes = base + ' xs:py-[3px] xs:pl-2 xs:text-sm';
|
|
396
|
+
return classes;
|
|
397
|
+
}
|
|
398
|
+
const classes = base;
|
|
399
|
+
return classes;
|
|
469
400
|
});
|
|
470
401
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
);
|
|
402
|
+
defineExpose({
|
|
403
|
+
focus,
|
|
404
|
+
blur,
|
|
405
|
+
close,
|
|
406
|
+
open,
|
|
407
|
+
setKeywords,
|
|
408
|
+
});
|
|
478
409
|
</script>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import BaseTagAutocompleteFetch from './BaseTagAutocompleteFetch.vue';
|
|
2
2
|
import ShowValue from '@/../.storybook/components/ShowValue.vue';
|
|
3
|
-
import { createFieldStory
|
|
3
|
+
import { createFieldStory } from '../../.storybook/utils';
|
|
4
4
|
import BaseAppNotifications from './BaseAppNotifications.vue';
|
|
5
5
|
|
|
6
6
|
export default {
|
|
@@ -58,7 +58,7 @@ Maximum.args = {
|
|
|
58
58
|
|
|
59
59
|
export const SlotOption = (args) => {
|
|
60
60
|
return {
|
|
61
|
-
components: {},
|
|
61
|
+
components: { BaseTagAutocompleteFetch },
|
|
62
62
|
setup() {
|
|
63
63
|
const value = ref([]);
|
|
64
64
|
return { args, value };
|
|
@@ -91,7 +91,7 @@ export const SlotOption = (args) => {
|
|
|
91
91
|
|
|
92
92
|
export const SlotFooter = (args) => {
|
|
93
93
|
return {
|
|
94
|
-
components: {},
|
|
94
|
+
components: { BaseTagAutocompleteFetch },
|
|
95
95
|
setup() {
|
|
96
96
|
const value = ref([]);
|
|
97
97
|
function onClick() {
|
|
@@ -118,7 +118,7 @@ export const SlotFooter = (args) => {
|
|
|
118
118
|
|
|
119
119
|
export const SlotEmpty = (args) => {
|
|
120
120
|
return {
|
|
121
|
-
components: {},
|
|
121
|
+
components: { BaseTagAutocompleteFetch },
|
|
122
122
|
setup() {
|
|
123
123
|
const value = ref([]);
|
|
124
124
|
return { args, value };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<BaseTagAutocomplete
|
|
3
3
|
:loading="showLoading && page == 1"
|
|
4
|
+
:loading-bottom="showLoading && page > 1"
|
|
4
5
|
:model-value="modelValue"
|
|
5
6
|
:disabled="disabled"
|
|
6
7
|
:placeholder="placeholder"
|
|
@@ -10,7 +11,7 @@
|
|
|
10
11
|
:has-error="hasError"
|
|
11
12
|
:max="max"
|
|
12
13
|
:filter="() => true"
|
|
13
|
-
@
|
|
14
|
+
@open="onOpen"
|
|
14
15
|
@typing="onTyping"
|
|
15
16
|
@scroll-bottom="scrollBottom"
|
|
16
17
|
@update:model-value="$emit('update:modelValue', $event)"
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
</template>
|
|
38
39
|
|
|
39
40
|
<script lang="ts" setup>
|
|
40
|
-
import { debounce } from 'lodash';
|
|
41
|
+
import { debounce, throttle } from 'lodash';
|
|
41
42
|
import { config } from '@/index';
|
|
42
43
|
import { PropType, Ref } from 'vue';
|
|
43
44
|
import { Option } from '@/types';
|
|
@@ -109,18 +110,22 @@ const onTyping = (query: string) => {
|
|
|
109
110
|
}
|
|
110
111
|
};
|
|
111
112
|
|
|
112
|
-
const
|
|
113
|
+
const onOpen = () => {
|
|
113
114
|
if (!firstSearch.value) {
|
|
114
115
|
search();
|
|
115
116
|
}
|
|
116
117
|
};
|
|
117
118
|
|
|
118
|
-
const scrollBottom = () => {
|
|
119
|
+
const scrollBottom = throttle(() => {
|
|
120
|
+
if (fetching.value) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
119
124
|
if (!reachedEnd.value) {
|
|
120
125
|
page.value++;
|
|
121
126
|
search();
|
|
122
127
|
}
|
|
123
|
-
};
|
|
128
|
+
}, 500);
|
|
124
129
|
|
|
125
130
|
const search = () => {
|
|
126
131
|
if (fetching.value) {
|
|
@@ -6,7 +6,7 @@ interface UseClickOutsideOptions {
|
|
|
6
6
|
|
|
7
7
|
export function useClickOutside(
|
|
8
8
|
element: MaybeElementRef,
|
|
9
|
-
callback: (
|
|
9
|
+
callback: () => void,
|
|
10
10
|
options: UseClickOutsideOptions = {}
|
|
11
11
|
) {
|
|
12
12
|
function cleanup() {
|
|
@@ -44,7 +44,9 @@ export function useClickOutside(
|
|
|
44
44
|
|
|
45
45
|
const outside = !contains && !containsIncludes;
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
if (outside) {
|
|
48
|
+
callback();
|
|
49
|
+
}
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
const stop = () => {
|