Newer
Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<template>
<ComboBox
v-model="modelValue"
v-bind="props"
:choices="filteredChoices"
:close-on-select="props.closeOnSelect ?? !Array.isArray(modelValue)"
input-container-class="tw-flex tw-flex-wrap tw-p-2 tw-gap-2 tw-items-baseline tw-w-full form-control tw-h-auto tw-min-h-[46px]"
input-class="tw-border-none tw-px-1 tw-w-[100px] focus:tw-shadow-none focus:tw-outline-none focus:tw-ring-0"
@search="searchQuery = $event"
>
<template #default="{ choice, ...attributes }">
<slot :choice="choice" v-bind="attributes">
<li v-bind="attributes">
{{ choice.name }}
</li>
</slot>
</template>
<template #selected="{ value, deselect, isOpen }">
<slot name="selected" :value="value" :deselect="deselect" :is-open="isOpen">
<template v-if="Array.isArray(value)">
<Tag
v-for="(item, index) in value"
:key="index"
:label="item.name"
removable
@remove="deselect(item)"
/>
</template>
<template v-else>
<p v-if="value">{{ value.name }}</p>
</template>
</slot>
</template>
</ComboBox>
</template>
<script lang="ts">
import type { ComboBoxProps } from '@/components/ComboBox.vue'
export type ComboBoxSimpleProps<T> = Omit<ComboBoxProps<T>, 'choices'> & {
choices?: T[]
searchProvider?: (query: string, signal: AbortSignal) => Promise<T[]>
}
</script>
<script setup lang="ts" generic="T extends { id: ID, name?: string }">
import { ID } from '@rokoli/bnb/drf'
import { computedAsync } from '@vueuse/core'
import { computed, ref, toValue } from 'vue'
import { computedDebounced, matchesSearch } from '@/util'
import Tag from './generic/Tag.vue'
import ComboBox from './ComboBox.vue'
defineOptions({
compatConfig: { MODE: 3 },
})
defineSlots<{
default(
props: Record<string, unknown> & {
id: string
choice: T
index: number
activeIndex: number
},
): unknown
selected(props: {
value: null | T | T[]
deselect: (choice: null | T) => void
isOpen: boolean
}): unknown
}>()
const modelValue = defineModel<null | T | T[]>({ required: true })
const props = defineProps<ComboBoxSimpleProps<T>>()
const searchQuery = ref('')
const debouncedSearchQuery = computedDebounced(searchQuery, (t) => (t.trim() ? 0.3 : 0))
const selectedIds = computed(() => {
if (Array.isArray(modelValue.value)) {
return modelValue.value.map((item) => item.id)
} else if (modelValue.value !== null) {
return [modelValue.value.id]
} else {
return []
}
})
let abortController: AbortController | null = null
const searchedChoices = computedAsync(
async () => {
if (props.choices) {
const query = toValue(searchQuery)
return props.choices.filter(({ name }) => matchesSearch(name ?? '', query))
} else if (props.searchProvider) {
const query = toValue(debouncedSearchQuery)
if (abortController) abortController.abort()
abortController = new AbortController()
try {
return await props.searchProvider(query, abortController.signal)
} finally {
abortController = null
}
} else {
return []
}
},
props.choices ?? [],
{ lazy: false },
)
const filteredChoices = computed(() =>
searchedChoices.value.filter((choice) => !selectedIds.value.includes(choice.id)),