Added multiple field to SearchSelect and ModalForm

This commit is contained in:
lucast committed 2026-02-03 15:54:42 +01:00
1 parent 01c40cb2f4
commit e952dd7b7a
2 files changed
+97 -17

No files matched your search

+16 -3
View File
@@ -18,10 +18,19 @@ interface FileField<T> extends BaseField<T> {
interface SearchSelectField<T> extends BaseField<T> {
type: "search-select"
fetchOptions: (q: string) => Promise<Option[]>
fetchOptions: (q?: string) => Promise<Option[]>
}
export type Field<T = any> = TextField<T> | FileField<T> | SearchSelectField<T>
interface SearchSelectArrayField<T> extends BaseField<T> {
type: "search-select-array"
fetchOptions: (q?: string) => Promise<Option[]>
}
export type Field<T = any> =
| TextField<T>
| FileField<T>
| SearchSelectField<T>
| SearchSelectArrayField<T>
interface ModalFormProps<T> {
item?: T
@@ -65,11 +74,15 @@ const save = async () => {
<form class="form" @submit.prevent="save">
<template v-for="field in props.fields">
<SearchSelect
v-if="field.type == 'search-select'"
v-if="
field.type == 'search-select' ||
field.type == 'search-select-array'
"
v-model:value="localItem[field.key]"
:fetch-options="field.fetchOptions"
:label="field.label || field.key"
:predefined="props.item?.[field.key]"
:multiple="field.type == 'search-select-array'"
/>
<Input
v-else
+81 -14
View File
@@ -6,52 +6,109 @@ export interface Option {
interface SearchSelectProps {
label?: string
fetchOptions: (q: string) => Promise<Option[]>
fetchOptions: (q?: string) => Promise<Option[]>
predefined?: Option
multiple?: boolean
}
const props = defineProps<SearchSelectProps>()
const value = defineModel<string>("value")
const value = defineModel<string | string[] | Option | Option[]>("value")
const selectedOptions = ref<Option[]>([])
const query = ref<string>("")
const options = ref<Option[]>([])
const show = ref<boolean>(false)
const isMultiple = computed(() => !!props.multiple)
watchEffect(() => {
if (!props.predefined) return
console.log(props.predefined)
if (isMultiple.value) {
const items = Array.isArray(props.predefined)
? props.predefined
: [props.predefined]
value.value = items.map((i) => i.value)
if (Array.isArray(items)) {
query.value = items.map((o) => o.label).join(", ")
selectedOptions.value = items
}
} else {
const item = Array.isArray(props.predefined)
? props.predefined[0]
: props.predefined
query.value = item.label
value.value = item.value
}
query.value = props.predefined.label
value.value = props.predefined.value
nextTick(() => (options.value = []))
})
watch(query, async (q) => {
options.value = q ? await props.fetchOptions(q) : []
show.value = true
options.value = await (await props.fetchOptions(q)).slice(0, 10)
})
const isSelected = (opt: Option) => {
if (isMultiple.value && Array.isArray(value.value)) {
return value.value.includes(opt.value)
}
return false
}
const select = (item: Option) => {
if (isMultiple.value) {
const current = [...selectedOptions.value]
const idx = current.findIndex((o) => o.value === item.value)
if (idx === -1) current.push(item)
else current.splice(idx, 1)
selectedOptions.value = current
value.value = current.map((o) => o.value)
} else {
value.value = item.value
query.value = item.label
nextTick(() => (options.value = []))
show.value = false
}
}
const onFocus = async () => {
if (isMultiple.value) query.value = ""
show.value = true
options.value = await (await props.fetchOptions()).slice(0, 10)
}
const onFocusOut = () => {
show.value = false
if (Array.isArray(selectedOptions.value) && isMultiple.value) {
query.value = selectedOptions.value.map((o) => o.label).join(", ")
}
}
</script>
<template>
<div class="search-select" @blur="show = false">
<div class="search-select" tabindex="0" @focusout="onFocusOut">
<Input
v-model:value="query"
@focus="show = true"
@focus="onFocus"
@focusout="onFocusOut"
:label="props.label"
type="text"
full
/>
<ul v-if="options.length && show">
<li v-for="opt in options" :key="opt.value" @click="select(opt)">
{{ opt.label }}
<ul v-if="options.length && show" @mousedown.prevent>
<li
v-for="opt in options"
:key="opt.value"
@mousedown.prevent="select(opt)"
>
<span>{{ opt.label }}</span>
<span v-if="isSelected(opt)" class="li-chip">✓</span>
</li>
</ul>
</div>
@@ -83,13 +140,23 @@ const select = (item: Option) => {
}
& li {
padding: 0.35rem 0.85rem;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.35rem 0.85rem;
transition: background-color 0.1s ease;
&:hover {
background-color: $color-button-hover;
}
}
.li-chip {
font-size: 0.7rem;
padding: 0.15rem 0.45rem;
border-radius: 999px;
background: $color-button-hover;
}
}
</style>