sprintify-ui 0.0.76 → 0.0.78
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 +7357 -6179
- package/dist/types/src/components/BaseAutocomplete.vue.d.ts +130 -6
- package/dist/types/src/components/BaseAutocompleteFetch.vue.d.ts +99 -5
- package/dist/types/src/components/BaseBelongsTo.vue.d.ts +96 -2
- package/dist/types/src/components/BaseDropdown.vue.d.ts +131 -0
- package/dist/types/src/components/BaseInput.vue.d.ts +1 -1
- package/dist/types/src/components/BaseLocaleForm.vue.d.ts +1 -1
- package/dist/types/src/components/BaseNavbarSideItem.vue.d.ts +3 -3
- package/dist/types/src/components/BaseRadioGroup.vue.d.ts +4 -4
- package/dist/types/src/components/BaseTagAutocomplete.vue.d.ts +3 -3
- package/dist/types/src/components/BaseTagAutocompleteFetch.vue.d.ts +3 -3
- package/dist/types/src/components/index.d.ts +2 -1
- package/package.json +3 -1
- package/src/components/BaseAutocomplete.stories.js +46 -0
- package/src/components/BaseAutocomplete.vue +223 -108
- package/src/components/BaseAutocompleteFetch.stories.js +40 -0
- package/src/components/BaseAutocompleteFetch.vue +43 -6
- package/src/components/BaseBelongsTo.stories.js +40 -0
- package/src/components/BaseBelongsTo.vue +25 -0
- package/src/components/BaseDropdown.stories.js +102 -0
- package/src/components/BaseDropdown.vue +144 -0
- package/src/components/index.ts +2 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div>
|
|
2
|
+
<div ref="autocomplete">
|
|
3
3
|
<div class="relative">
|
|
4
4
|
<div class="relative">
|
|
5
5
|
<input
|
|
@@ -9,27 +9,38 @@
|
|
|
9
9
|
:placeholder="
|
|
10
10
|
placeholder ? placeholder : $t('sui.autocomplete_placeholder')
|
|
11
11
|
"
|
|
12
|
-
class="w-full rounded
|
|
13
|
-
:class="[
|
|
12
|
+
class="w-full rounded disabled:cursor-not-allowed disabled:text-slate-300"
|
|
13
|
+
:class="[
|
|
14
|
+
hasErrorInternal ? 'border-red-600' : 'border-slate-300',
|
|
15
|
+
inputClass,
|
|
16
|
+
!visibleFocus
|
|
17
|
+
? [
|
|
18
|
+
'focus:ring-0',
|
|
19
|
+
hasErrorInternal
|
|
20
|
+
? 'focus:border-red-600'
|
|
21
|
+
: 'focus:border-slate-300',
|
|
22
|
+
]
|
|
23
|
+
: '',
|
|
24
|
+
]"
|
|
14
25
|
autocomplete="off"
|
|
15
26
|
:disabled="disabled"
|
|
16
|
-
@focus="onTextFocus"
|
|
17
|
-
@blur="onTextBlur"
|
|
18
27
|
@input="onTextInput"
|
|
19
28
|
@keydown="onTextKeydown"
|
|
20
29
|
/>
|
|
21
30
|
<div
|
|
22
|
-
class="pointer-events-none absolute top-0 left-0 flex h-full items-center justify-center
|
|
31
|
+
class="pointer-events-none absolute top-0 left-0 flex h-full items-center justify-center"
|
|
32
|
+
:class="[iconWrapClass]"
|
|
23
33
|
>
|
|
24
34
|
<BaseIcon
|
|
25
|
-
class="
|
|
35
|
+
class="text-slate-400"
|
|
36
|
+
:class="[iconClass]"
|
|
26
37
|
icon="heroicons:magnifying-glass-solid"
|
|
27
38
|
/>
|
|
28
39
|
</div>
|
|
29
40
|
</div>
|
|
30
41
|
|
|
31
42
|
<div
|
|
32
|
-
v-if="normalizedModelValue && !disabled"
|
|
43
|
+
v-if="normalizedModelValue && !disabled && modelValueShow"
|
|
33
44
|
class="absolute top-0 right-0 flex h-full items-center p-1"
|
|
34
45
|
>
|
|
35
46
|
<button
|
|
@@ -39,7 +50,8 @@
|
|
|
39
50
|
>
|
|
40
51
|
<BaseIcon
|
|
41
52
|
icon="heroicons:x-mark"
|
|
42
|
-
class="
|
|
53
|
+
class="text-slate-500 group-hover:text-slate-700"
|
|
54
|
+
:class="[iconClass]"
|
|
43
55
|
/>
|
|
44
56
|
</button>
|
|
45
57
|
</div>
|
|
@@ -47,8 +59,13 @@
|
|
|
47
59
|
|
|
48
60
|
<div class="relative">
|
|
49
61
|
<div
|
|
50
|
-
v-show="
|
|
51
|
-
class="
|
|
62
|
+
v-show="opened || dropdownShow == 'always'"
|
|
63
|
+
class="min-h-[110px] w-full overflow-hidden"
|
|
64
|
+
:class="[
|
|
65
|
+
inline
|
|
66
|
+
? 'relative'
|
|
67
|
+
: 'absolute top-1 z-menu rounded border border-slate-300 bg-white shadow-md',
|
|
68
|
+
]"
|
|
52
69
|
>
|
|
53
70
|
<div
|
|
54
71
|
ref="dropdown"
|
|
@@ -62,9 +79,9 @@
|
|
|
62
79
|
</div>
|
|
63
80
|
</slot>
|
|
64
81
|
|
|
65
|
-
<ul v-else class="p-1">
|
|
82
|
+
<ul v-else :class="[inline ? 'p-0 pt-1' : 'p-1']">
|
|
66
83
|
<li
|
|
67
|
-
v-for="option in filteredNormalizedOptions"
|
|
84
|
+
v-for="(option, index) in filteredNormalizedOptions"
|
|
68
85
|
:key="option.value"
|
|
69
86
|
class="block"
|
|
70
87
|
>
|
|
@@ -73,7 +90,7 @@
|
|
|
73
90
|
type="button"
|
|
74
91
|
tabindex="-1"
|
|
75
92
|
@click="onSelect(option)"
|
|
76
|
-
@
|
|
93
|
+
@mouseenter="selectionIndex = index"
|
|
77
94
|
>
|
|
78
95
|
<slot
|
|
79
96
|
name="option"
|
|
@@ -82,10 +99,19 @@
|
|
|
82
99
|
:active="optionActive && optionActive.value == option.value"
|
|
83
100
|
>
|
|
84
101
|
<div
|
|
85
|
-
class="rounded px-2 py-1 text-sm"
|
|
86
|
-
:class="optionClass(option)"
|
|
102
|
+
class="flex items-center rounded px-2 py-1 text-sm"
|
|
103
|
+
:class="[optionClass(option), optionSizeClass]"
|
|
87
104
|
>
|
|
88
|
-
|
|
105
|
+
<div class="grow">
|
|
106
|
+
{{ option.label }}
|
|
107
|
+
</div>
|
|
108
|
+
<div class="shrink-0">
|
|
109
|
+
<BaseIcon
|
|
110
|
+
v-if="isSelected(option)"
|
|
111
|
+
icon="heroicons:check-20-solid"
|
|
112
|
+
:class="iconClass"
|
|
113
|
+
></BaseIcon>
|
|
114
|
+
</div>
|
|
89
115
|
</div>
|
|
90
116
|
</slot>
|
|
91
117
|
</button>
|
|
@@ -93,12 +119,11 @@
|
|
|
93
119
|
</ul>
|
|
94
120
|
</div>
|
|
95
121
|
|
|
96
|
-
<div
|
|
122
|
+
<div>
|
|
97
123
|
<div v-if="$slots.footer" class="bg-white">
|
|
98
124
|
<slot
|
|
99
125
|
:options="filteredNormalizedOptions"
|
|
100
126
|
:keywords="keywords"
|
|
101
|
-
:hide-dropdown="hideDropdown"
|
|
102
127
|
name="footer"
|
|
103
128
|
/>
|
|
104
129
|
</div>
|
|
@@ -129,7 +154,7 @@
|
|
|
129
154
|
import { get } from 'lodash';
|
|
130
155
|
import { PropType, Ref, ComputedRef } from 'vue';
|
|
131
156
|
import { NormalizedOption, Option } from '@/types';
|
|
132
|
-
import { useInfiniteScroll
|
|
157
|
+
import { useInfiniteScroll } from '@vueuse/core';
|
|
133
158
|
import BaseSkeleton from '@/components/BaseSkeleton.vue';
|
|
134
159
|
import { useHasOptions } from '@/composables/hasOptions';
|
|
135
160
|
import { useField } from '@/composables/field';
|
|
@@ -180,14 +205,37 @@ const props = defineProps({
|
|
|
180
205
|
default: false,
|
|
181
206
|
type: Boolean,
|
|
182
207
|
},
|
|
208
|
+
inline: {
|
|
209
|
+
default: false,
|
|
210
|
+
type: Boolean,
|
|
211
|
+
},
|
|
212
|
+
size: {
|
|
213
|
+
default: 'base',
|
|
214
|
+
type: String as PropType<'xs' | 'sm' | 'base'>,
|
|
215
|
+
},
|
|
216
|
+
dropdownShow: {
|
|
217
|
+
default: 'focus',
|
|
218
|
+
type: String as PropType<'focus' | 'always'>,
|
|
219
|
+
},
|
|
220
|
+
modelValueShow: {
|
|
221
|
+
default: true,
|
|
222
|
+
type: Boolean,
|
|
223
|
+
},
|
|
224
|
+
visibleFocus: {
|
|
225
|
+
default: true,
|
|
226
|
+
type: Boolean,
|
|
227
|
+
},
|
|
183
228
|
});
|
|
184
229
|
|
|
185
230
|
const emit = defineEmits([
|
|
186
231
|
'update:modelValue',
|
|
187
232
|
'typing',
|
|
233
|
+
'blur',
|
|
188
234
|
'focus',
|
|
189
235
|
'scrollBottom',
|
|
190
236
|
'clear',
|
|
237
|
+
'open',
|
|
238
|
+
'close',
|
|
191
239
|
]);
|
|
192
240
|
|
|
193
241
|
const { hasErrorInternal, emitUpdate } = useField({
|
|
@@ -197,12 +245,14 @@ const { hasErrorInternal, emitUpdate } = useField({
|
|
|
197
245
|
emit: emit,
|
|
198
246
|
});
|
|
199
247
|
|
|
200
|
-
|
|
248
|
+
let timerId = 0;
|
|
201
249
|
const keywords = ref('');
|
|
202
|
-
const showDropdown = ref(false);
|
|
203
250
|
const selectionIndex = ref(0);
|
|
251
|
+
const autocomplete = ref(null) as Ref<HTMLElement | null>;
|
|
204
252
|
const inputElement = ref(null) as Ref<HTMLInputElement | null>;
|
|
205
253
|
const dropdown = ref(null) as Ref<HTMLElement | null>;
|
|
254
|
+
const shouldFilter = ref(false);
|
|
255
|
+
const opened = ref(false);
|
|
206
256
|
|
|
207
257
|
const hasOptions = useHasOptions(
|
|
208
258
|
computed(() => props.modelValue),
|
|
@@ -217,16 +267,6 @@ const normalizedOptions = hasOptions.normalizedOptions;
|
|
|
217
267
|
const normalizedModelValue =
|
|
218
268
|
hasOptions.normalizedModelValue as ComputedRef<NormalizedOption | null>;
|
|
219
269
|
|
|
220
|
-
onMounted(() => {
|
|
221
|
-
useInfiniteScroll(
|
|
222
|
-
dropdown.value,
|
|
223
|
-
() => {
|
|
224
|
-
emit('scrollBottom');
|
|
225
|
-
},
|
|
226
|
-
{ distance: 60 }
|
|
227
|
-
);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
270
|
const optionActive = computed(() => {
|
|
231
271
|
return (
|
|
232
272
|
filteredNormalizedOptions.value[
|
|
@@ -239,16 +279,22 @@ const optionActive = computed(() => {
|
|
|
239
279
|
watch(
|
|
240
280
|
() => normalizedModelValue.value,
|
|
241
281
|
() => {
|
|
282
|
+
if (props.modelValueShow === false) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
242
285
|
if (normalizedModelValue.value) {
|
|
243
|
-
|
|
286
|
+
setKeywords(normalizedModelValue.value?.label);
|
|
244
287
|
} else {
|
|
245
|
-
|
|
288
|
+
setKeywords('');
|
|
246
289
|
}
|
|
247
290
|
},
|
|
248
291
|
{ immediate: true }
|
|
249
292
|
);
|
|
250
293
|
|
|
251
294
|
const filteredNormalizedOptions = computed((): NormalizedOption[] => {
|
|
295
|
+
if (shouldFilter.value === false) {
|
|
296
|
+
return normalizedOptions.value;
|
|
297
|
+
}
|
|
252
298
|
return normalizedOptions.value.filter((option) => {
|
|
253
299
|
if (props.filter !== undefined) {
|
|
254
300
|
return props.filter(option);
|
|
@@ -260,40 +306,69 @@ const filteredNormalizedOptions = computed((): NormalizedOption[] => {
|
|
|
260
306
|
});
|
|
261
307
|
});
|
|
262
308
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
});
|
|
267
|
-
elements.forEach((e) => {
|
|
268
|
-
e.addEventListener('mousedown', dontLooseFocus);
|
|
269
|
-
});
|
|
270
|
-
}
|
|
309
|
+
onMounted(() => {
|
|
310
|
+
window.addEventListener('mousedown', onMouseDown);
|
|
311
|
+
});
|
|
271
312
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
};
|
|
313
|
+
onBeforeMount(() => {
|
|
314
|
+
window.removeEventListener('mousedown', onMouseDown);
|
|
315
|
+
});
|
|
276
316
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
};
|
|
317
|
+
function onMouseDown(event: MouseEvent) {
|
|
318
|
+
if (!autocomplete.value) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
282
321
|
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
322
|
+
// Get the element that was clicked
|
|
323
|
+
const clickedElement = event.target as HTMLElement | null;
|
|
324
|
+
|
|
325
|
+
if (!clickedElement) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// If the element that was not clicked on the input,
|
|
330
|
+
// prevent default
|
|
331
|
+
if (clickedElement !== inputElement.value) {
|
|
332
|
+
event.preventDefault();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// `el` is the element you're detecting clicks outside of
|
|
336
|
+
if (autocomplete.value.contains(clickedElement)) {
|
|
337
|
+
open();
|
|
338
|
+
} else {
|
|
339
|
+
close();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function open() {
|
|
344
|
+
clearInterval(timerId);
|
|
345
|
+
// Always focus as a safety
|
|
346
|
+
focus();
|
|
347
|
+
// Only emit open if value changes
|
|
348
|
+
if (!opened.value) {
|
|
349
|
+
opened.value = true;
|
|
350
|
+
emit('open');
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function close() {
|
|
355
|
+
opened.value = false;
|
|
356
|
+
blur();
|
|
357
|
+
timerId = setTimeout(() => {
|
|
358
|
+
// If no valid modelValue is set on close, set the keywords to the original value
|
|
359
|
+
if (props.modelValueShow && normalizedModelValue.value) {
|
|
360
|
+
setKeywords(normalizedModelValue.value.label);
|
|
290
361
|
}
|
|
291
362
|
}, 10);
|
|
292
|
-
|
|
363
|
+
emit('close');
|
|
364
|
+
}
|
|
293
365
|
|
|
294
366
|
const onTextInput = (event: Event) => {
|
|
367
|
+
open();
|
|
368
|
+
shouldFilter.value = true;
|
|
295
369
|
selectionIndex.value = 0;
|
|
296
370
|
setKeywords(get(event, 'target.value') + '');
|
|
371
|
+
emit('typing', keywords.value);
|
|
297
372
|
dropdown.value?.scrollTo({
|
|
298
373
|
top: 0,
|
|
299
374
|
});
|
|
@@ -311,6 +386,7 @@ const onTextKeydown = (event: KeyboardEvent) => {
|
|
|
311
386
|
}
|
|
312
387
|
|
|
313
388
|
if (key === 'ArrowDown') {
|
|
389
|
+
event.preventDefault();
|
|
314
390
|
if (selectionIndex.value < filteredNormalizedOptions.value.length - 1) {
|
|
315
391
|
selectionIndex.value++;
|
|
316
392
|
} else {
|
|
@@ -320,6 +396,7 @@ const onTextKeydown = (event: KeyboardEvent) => {
|
|
|
320
396
|
}
|
|
321
397
|
|
|
322
398
|
if (key === 'ArrowUp') {
|
|
399
|
+
event.preventDefault();
|
|
323
400
|
if (selectionIndex.value > 0) {
|
|
324
401
|
selectionIndex.value--;
|
|
325
402
|
} else {
|
|
@@ -340,74 +417,112 @@ const onTextKeydown = (event: KeyboardEvent) => {
|
|
|
340
417
|
}
|
|
341
418
|
};
|
|
342
419
|
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if (selected) {
|
|
348
|
-
if (active) {
|
|
349
|
-
return 'bg-blue-600 hover:bg-blue-700 text-white';
|
|
350
|
-
}
|
|
420
|
+
const clear = () => {
|
|
421
|
+
update(null);
|
|
422
|
+
emit('clear');
|
|
423
|
+
};
|
|
351
424
|
|
|
352
|
-
|
|
425
|
+
function onSelect(normalizedModelValue: Option | null | undefined) {
|
|
426
|
+
focus();
|
|
427
|
+
update(normalizedModelValue);
|
|
428
|
+
if (props.dropdownShow == 'focus') {
|
|
429
|
+
close();
|
|
353
430
|
}
|
|
431
|
+
}
|
|
354
432
|
|
|
355
|
-
|
|
356
|
-
|
|
433
|
+
function update(normalizedSelection: Option | null | undefined) {
|
|
434
|
+
const selection = normalizedSelection ? normalizedSelection.option : null;
|
|
435
|
+
if (selection) {
|
|
436
|
+
const index = filteredNormalizedOptions.value.findIndex(
|
|
437
|
+
(option) => option.value == selection.value
|
|
438
|
+
);
|
|
439
|
+
selectionIndex.value = index;
|
|
357
440
|
}
|
|
441
|
+
shouldFilter.value = false;
|
|
442
|
+
emitUpdate(selection);
|
|
443
|
+
}
|
|
358
444
|
|
|
359
|
-
|
|
360
|
-
|
|
445
|
+
function setKeywords(input: string) {
|
|
446
|
+
keywords.value = input;
|
|
447
|
+
}
|
|
361
448
|
|
|
362
|
-
|
|
363
|
-
emit('clear');
|
|
364
|
-
setKeywordsWithoutEvent('');
|
|
365
|
-
update(null);
|
|
449
|
+
function focus() {
|
|
366
450
|
inputElement.value?.focus();
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const onSelect = (normalizedModelValue: Option | null | undefined) => {
|
|
370
|
-
update(normalizedModelValue);
|
|
371
|
-
};
|
|
451
|
+
}
|
|
372
452
|
|
|
373
|
-
function
|
|
453
|
+
function blur() {
|
|
374
454
|
inputElement.value?.blur();
|
|
375
455
|
}
|
|
376
456
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
457
|
+
// Element Classes
|
|
458
|
+
|
|
459
|
+
const optionClass = (option: NormalizedOption) => {
|
|
460
|
+
const active = optionActive.value && optionActive.value.value == option.value;
|
|
461
|
+
|
|
462
|
+
if (active) {
|
|
463
|
+
return 'bg-slate-200';
|
|
381
464
|
}
|
|
382
|
-
emitUpdate(selection);
|
|
383
|
-
};
|
|
384
465
|
|
|
385
|
-
|
|
386
|
-
keywords.value = input;
|
|
466
|
+
return 'bg-white';
|
|
387
467
|
};
|
|
388
468
|
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
}
|
|
469
|
+
const optionSizeClass = computed(() => {
|
|
470
|
+
if (props.size == 'xs') {
|
|
471
|
+
return 'text-xs';
|
|
472
|
+
}
|
|
473
|
+
if (props.size == 'sm') {
|
|
474
|
+
return 'text-sm';
|
|
475
|
+
}
|
|
476
|
+
return 'text-sm';
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const inputClass = computed(() => {
|
|
480
|
+
if (props.size == 'xs') {
|
|
481
|
+
return 'xs:text-xs xs:pl-7 text-base pl-9';
|
|
482
|
+
}
|
|
483
|
+
if (props.size == 'sm') {
|
|
484
|
+
return 'xs:text-sm xs:pl-8 text-base pl-9';
|
|
485
|
+
}
|
|
486
|
+
return 'text-base pl-9';
|
|
487
|
+
});
|
|
393
488
|
|
|
394
|
-
const
|
|
489
|
+
const iconClass = computed(() => {
|
|
490
|
+
if (props.size == 'xs') {
|
|
491
|
+
return 'xs:h-4 xs:w-4 h-5 w-5';
|
|
492
|
+
}
|
|
493
|
+
if (props.size == 'sm') {
|
|
494
|
+
return 'xs:h-5 xs:w-5 h-5 w-5';
|
|
495
|
+
}
|
|
496
|
+
return 'h-5 w-5';
|
|
497
|
+
});
|
|
395
498
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
499
|
+
const iconWrapClass = computed(() => {
|
|
500
|
+
if (props.size == 'xs') {
|
|
501
|
+
return 'xs:pl-2 pl-2.5';
|
|
502
|
+
}
|
|
503
|
+
if (props.size == 'sm') {
|
|
504
|
+
return 'xs:pl-2 pl-2.5';
|
|
505
|
+
}
|
|
506
|
+
return 'pl-2.5';
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// Infinite scroll
|
|
401
510
|
|
|
402
511
|
onMounted(() => {
|
|
403
|
-
|
|
512
|
+
useInfiniteScroll(
|
|
513
|
+
dropdown.value,
|
|
514
|
+
() => {
|
|
515
|
+
emit('scrollBottom');
|
|
516
|
+
},
|
|
517
|
+
{ distance: 60 }
|
|
518
|
+
);
|
|
404
519
|
});
|
|
405
520
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
);
|
|
521
|
+
defineExpose({
|
|
522
|
+
focus,
|
|
523
|
+
blur,
|
|
524
|
+
close,
|
|
525
|
+
open,
|
|
526
|
+
setKeywords,
|
|
527
|
+
});
|
|
413
528
|
</script>
|
|
@@ -3,9 +3,19 @@ import ShowValue from '@/../.storybook/components/ShowValue.vue';
|
|
|
3
3
|
import { options } from '@/../.storybook/utils';
|
|
4
4
|
import { createFieldStory } from '../../.storybook/utils';
|
|
5
5
|
|
|
6
|
+
const sizes = ['xs', 'sm', 'base'];
|
|
7
|
+
|
|
6
8
|
export default {
|
|
7
9
|
title: 'Form/BaseAutocompleteFetch',
|
|
8
10
|
component: BaseAutocompleteFetch,
|
|
11
|
+
argTypes: {
|
|
12
|
+
size: {
|
|
13
|
+
control: {
|
|
14
|
+
type: 'select',
|
|
15
|
+
options: sizes,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
9
19
|
args: {
|
|
10
20
|
url: 'https://effettandem.com/api/content/articles',
|
|
11
21
|
labelKey: 'title',
|
|
@@ -29,6 +39,17 @@ const Template = (args) => ({
|
|
|
29
39
|
export const Demo = Template.bind({});
|
|
30
40
|
Demo.args = {};
|
|
31
41
|
|
|
42
|
+
export const AlwaysShowDropdown = Template.bind({});
|
|
43
|
+
AlwaysShowDropdown.args = {
|
|
44
|
+
inline: true,
|
|
45
|
+
dropdownShow: 'always',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const NoFocus = Template.bind({});
|
|
49
|
+
NoFocus.args = {
|
|
50
|
+
visibleFocus: false,
|
|
51
|
+
};
|
|
52
|
+
|
|
32
53
|
export const Disabled = Template.bind({});
|
|
33
54
|
Disabled.args = {
|
|
34
55
|
labelKey: 'label',
|
|
@@ -37,6 +58,25 @@ Disabled.args = {
|
|
|
37
58
|
disabled: true,
|
|
38
59
|
};
|
|
39
60
|
|
|
61
|
+
export const Inline = Template.bind({});
|
|
62
|
+
Inline.args = {
|
|
63
|
+
inline: true,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const Sizes = (args) => ({
|
|
67
|
+
components: { BaseAutocompleteFetch },
|
|
68
|
+
setup() {
|
|
69
|
+
const value = ref(null);
|
|
70
|
+
return { args, sizes, value };
|
|
71
|
+
},
|
|
72
|
+
template: `
|
|
73
|
+
<div v-for="size in sizes" class="mb-1">
|
|
74
|
+
<p class="text-xs text-slate-600 leading-tight">{{ size }}</p>
|
|
75
|
+
<BaseAutocompleteFetch v-model="value" v-bind="args" :size="size"></BaseAutocompleteFetch>
|
|
76
|
+
</div>
|
|
77
|
+
`,
|
|
78
|
+
});
|
|
79
|
+
|
|
40
80
|
export const SlotOption = (args) => {
|
|
41
81
|
return {
|
|
42
82
|
components: { BaseAutocompleteFetch },
|
|
@@ -10,9 +10,15 @@
|
|
|
10
10
|
:value-key="valueKey"
|
|
11
11
|
:label-key="labelKey"
|
|
12
12
|
:has-error="hasError"
|
|
13
|
+
:required="required"
|
|
14
|
+
:size="size"
|
|
15
|
+
:inline="inline"
|
|
16
|
+
:dropdown-show="dropdownShow"
|
|
17
|
+
:model-value-show="modelValueShow"
|
|
18
|
+
:visible-focus="visibleFocus"
|
|
13
19
|
:filter="() => true"
|
|
14
20
|
@clear="onClear"
|
|
15
|
-
@
|
|
21
|
+
@open="onOpen"
|
|
16
22
|
@typing="onTyping"
|
|
17
23
|
@scroll-bottom="scrollBottom"
|
|
18
24
|
@update:model-value="$emit('update:modelValue', $event)"
|
|
@@ -45,6 +51,15 @@ import { PropType, Ref } from 'vue';
|
|
|
45
51
|
import { Option } from '@/types';
|
|
46
52
|
import BaseAutocomplete from './BaseAutocomplete.vue';
|
|
47
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Behavior notes
|
|
56
|
+
*
|
|
57
|
+
* - When the user types, the component will fetch the data from the API.
|
|
58
|
+
* - When the user scrolls to the bottom, the component will fetch the next page.
|
|
59
|
+
* - When the user clears the input, the component will NOT re-fetch the data if the current query is already empty.
|
|
60
|
+
* - When a value is selected, the component will NOT re-fetch the data.
|
|
61
|
+
*/
|
|
62
|
+
|
|
48
63
|
const props = defineProps({
|
|
49
64
|
modelValue: {
|
|
50
65
|
default: undefined,
|
|
@@ -86,6 +101,26 @@ const props = defineProps({
|
|
|
86
101
|
default: false,
|
|
87
102
|
type: Boolean,
|
|
88
103
|
},
|
|
104
|
+
inline: {
|
|
105
|
+
default: false,
|
|
106
|
+
type: Boolean,
|
|
107
|
+
},
|
|
108
|
+
size: {
|
|
109
|
+
default: 'base',
|
|
110
|
+
type: String as PropType<'xs' | 'sm' | 'base'>,
|
|
111
|
+
},
|
|
112
|
+
dropdownShow: {
|
|
113
|
+
default: 'focus',
|
|
114
|
+
type: String as PropType<'focus' | 'always'>,
|
|
115
|
+
},
|
|
116
|
+
modelValueShow: {
|
|
117
|
+
default: true,
|
|
118
|
+
type: Boolean,
|
|
119
|
+
},
|
|
120
|
+
visibleFocus: {
|
|
121
|
+
default: true,
|
|
122
|
+
type: Boolean,
|
|
123
|
+
},
|
|
89
124
|
});
|
|
90
125
|
|
|
91
126
|
const emit = defineEmits([
|
|
@@ -112,15 +147,17 @@ const onTyping = (query: string) => {
|
|
|
112
147
|
debouncedSearch();
|
|
113
148
|
};
|
|
114
149
|
|
|
115
|
-
const
|
|
150
|
+
const onOpen = () => {
|
|
116
151
|
if (!firstSearch.value) {
|
|
117
152
|
search();
|
|
118
153
|
}
|
|
119
154
|
};
|
|
120
155
|
|
|
121
156
|
const onClear = () => {
|
|
122
|
-
keywords.value
|
|
123
|
-
|
|
157
|
+
if (keywords.value != '') {
|
|
158
|
+
keywords.value = '';
|
|
159
|
+
search();
|
|
160
|
+
}
|
|
124
161
|
emit('clear');
|
|
125
162
|
};
|
|
126
163
|
|
|
@@ -131,7 +168,7 @@ const scrollBottom = () => {
|
|
|
131
168
|
}
|
|
132
169
|
};
|
|
133
170
|
|
|
134
|
-
|
|
171
|
+
function search() {
|
|
135
172
|
if (fetching.value) {
|
|
136
173
|
return;
|
|
137
174
|
}
|
|
@@ -165,7 +202,7 @@ const search = () => {
|
|
|
165
202
|
fetching.value = false;
|
|
166
203
|
showLoading.value = false;
|
|
167
204
|
});
|
|
168
|
-
}
|
|
205
|
+
}
|
|
169
206
|
|
|
170
207
|
const debouncedSearch = debounce(() => {
|
|
171
208
|
search();
|