Added multiple field to SearchSelect and ModalForm
This commit is contained in:
1 parent
01c40cb2f4
commit
e952dd7b7a
2 files changed
+99
-19
No files matched your search
@@ -18,10 +18,19 @@ interface FileField<T> extends BaseField<T> {
|
|||||||
|
|
||||||
interface SearchSelectField<T> extends BaseField<T> {
|
interface SearchSelectField<T> extends BaseField<T> {
|
||||||
type: "search-select"
|
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> {
|
interface ModalFormProps<T> {
|
||||||
item?: T
|
item?: T
|
||||||
@@ -65,11 +74,15 @@ const save = async () => {
|
|||||||
<form class="form" @submit.prevent="save">
|
<form class="form" @submit.prevent="save">
|
||||||
<template v-for="field in props.fields">
|
<template v-for="field in props.fields">
|
||||||
<SearchSelect
|
<SearchSelect
|
||||||
v-if="field.type == 'search-select'"
|
v-if="
|
||||||
|
field.type == 'search-select' ||
|
||||||
|
field.type == 'search-select-array'
|
||||||
|
"
|
||||||
v-model:value="localItem[field.key]"
|
v-model:value="localItem[field.key]"
|
||||||
:fetch-options="field.fetchOptions"
|
:fetch-options="field.fetchOptions"
|
||||||
:label="field.label || field.key"
|
:label="field.label || field.key"
|
||||||
:predefined="props.item?.[field.key]"
|
:predefined="props.item?.[field.key]"
|
||||||
|
:multiple="field.type == 'search-select-array'"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
v-else
|
v-else
|
||||||
|
|||||||
+83
-16
@@ -6,52 +6,109 @@ export interface Option {
|
|||||||
|
|
||||||
interface SearchSelectProps {
|
interface SearchSelectProps {
|
||||||
label?: string
|
label?: string
|
||||||
fetchOptions: (q: string) => Promise<Option[]>
|
fetchOptions: (q?: string) => Promise<Option[]>
|
||||||
predefined?: Option
|
predefined?: Option
|
||||||
|
multiple?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<SearchSelectProps>()
|
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 query = ref<string>("")
|
||||||
const options = ref<Option[]>([])
|
const options = ref<Option[]>([])
|
||||||
const show = ref<boolean>(false)
|
const show = ref<boolean>(false)
|
||||||
|
|
||||||
|
const isMultiple = computed(() => !!props.multiple)
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
if (!props.predefined) return
|
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 = []))
|
nextTick(() => (options.value = []))
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(query, async (q) => {
|
watch(query, async (q) => {
|
||||||
options.value = q ? await props.fetchOptions(q) : []
|
options.value = await (await props.fetchOptions(q)).slice(0, 10)
|
||||||
show.value = true
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isSelected = (opt: Option) => {
|
||||||
|
if (isMultiple.value && Array.isArray(value.value)) {
|
||||||
|
return value.value.includes(opt.value)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const select = (item: Option) => {
|
const select = (item: Option) => {
|
||||||
value.value = item.value
|
if (isMultiple.value) {
|
||||||
query.value = item.label
|
const current = [...selectedOptions.value]
|
||||||
nextTick(() => (options.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
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="search-select" @blur="show = false">
|
<div class="search-select" tabindex="0" @focusout="onFocusOut">
|
||||||
<Input
|
<Input
|
||||||
v-model:value="query"
|
v-model:value="query"
|
||||||
@focus="show = true"
|
@focus="onFocus"
|
||||||
|
@focusout="onFocusOut"
|
||||||
:label="props.label"
|
:label="props.label"
|
||||||
type="text"
|
type="text"
|
||||||
full
|
full
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ul v-if="options.length && show">
|
<ul v-if="options.length && show" @mousedown.prevent>
|
||||||
<li v-for="opt in options" :key="opt.value" @click="select(opt)">
|
<li
|
||||||
{{ opt.label }}
|
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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,13 +140,23 @@ const select = (item: Option) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
& li {
|
& 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;
|
transition: background-color 0.1s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $color-button-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>
|
</style>
|
||||||
Reference in new issue
Block a user