mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-14 12:14:41 +01:00
feat(CommandPalette): handle async search for specific groups
This commit is contained in:
@@ -149,18 +149,13 @@
|
||||
|
||||
<UCard body-class="">
|
||||
<UCommandPalette
|
||||
v-model="form.persons"
|
||||
multiple
|
||||
:placeholder="false"
|
||||
:options="{
|
||||
fuseOptions: {
|
||||
includeMatches: true
|
||||
}
|
||||
}"
|
||||
:groups="[{
|
||||
key: 'persons',
|
||||
commands: people
|
||||
}]"
|
||||
:groups="commandPaletteGroups"
|
||||
command-attribute="name"
|
||||
/>
|
||||
</UCard>
|
||||
@@ -263,6 +258,18 @@ const y = ref(0)
|
||||
const isContextMenuOpen = ref(false)
|
||||
const contextMenuRef = ref(null)
|
||||
|
||||
const commandPaletteGroups = computed(() => ([{
|
||||
key: 'people',
|
||||
commands: people.value
|
||||
}, {
|
||||
key: 'search',
|
||||
label: q => q && `Search results for "${q}"...`,
|
||||
search: async (q) => {
|
||||
if (!q) { return [] }
|
||||
return await $fetch(`https://jsonplaceholder.typicode.com/users?q=${q}`)
|
||||
}
|
||||
}]))
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('mousemove', ({ clientX, clientY }) => {
|
||||
x.value = clientX
|
||||
|
||||
@@ -38,7 +38,14 @@
|
||||
aria-label="Commands"
|
||||
class="relative flex-1 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-800 scroll-py-2"
|
||||
>
|
||||
<CommandPaletteGroup v-for="group of groups" :key="group.key" :group="group" :group-attribute="groupAttribute" :command-attribute="commandAttribute">
|
||||
<CommandPaletteGroup
|
||||
v-for="group of groups"
|
||||
:key="group.key"
|
||||
:query="query"
|
||||
:group="group"
|
||||
:group-attribute="groupAttribute"
|
||||
:command-attribute="commandAttribute"
|
||||
>
|
||||
<template v-for="(_, name) in $slots" #[name]="slotData">
|
||||
<slot :name="name" v-bind="slotData" />
|
||||
</template>
|
||||
@@ -59,6 +66,7 @@
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { Combobox, ComboboxInput, ComboboxOptions } from '@headlessui/vue'
|
||||
import type { ComputedRef, PropType, ComponentPublicInstance } from 'vue'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { useFuse } from '@vueuse/integrations/useFuse'
|
||||
import { groupBy, map } from 'lodash-es'
|
||||
import { defu } from 'defu'
|
||||
@@ -133,6 +141,10 @@ const props = defineProps({
|
||||
placeholder: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
debounce: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
@@ -168,29 +180,44 @@ const options: ComputedRef<Partial<UseFuseOptions<Command>>> = computed(() => de
|
||||
matchAllWhenSearchEmpty: true
|
||||
}))
|
||||
|
||||
const commands = computed(() => props.groups.reduce((acc, group) => {
|
||||
const commands = computed(() => props.groups.filter(group => !group.search).reduce((acc, group) => {
|
||||
return acc.concat(group.commands.map(command => ({ ...command, group: group.key })))
|
||||
}, [] as Command[]))
|
||||
|
||||
const searchResults = ref({})
|
||||
|
||||
const { results } = useFuse(query, commands, options)
|
||||
|
||||
const groups = computed(() => map(groupBy(results.value, command => command.item.group), (results, key) => {
|
||||
const commands = results.map((result) => {
|
||||
const { item, ...data } = result
|
||||
const groups = computed(() => ([
|
||||
...map(groupBy(results.value, command => command.item.group), (results, key) => {
|
||||
const commands = results.map((result) => {
|
||||
const { item, ...data } = result
|
||||
|
||||
return {
|
||||
...item,
|
||||
...data
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
...item,
|
||||
...data
|
||||
}
|
||||
})
|
||||
...props.groups.find(group => group.key === key),
|
||||
commands: commands.slice(0, options.value.resultLimit)
|
||||
} as Group
|
||||
}),
|
||||
...props.groups.filter(group => !!group.search).map(group => ({ ...group, commands: (searchResults.value[group.key] || []).slice(0, options.value.resultLimit) })).filter(group => group.commands.length)
|
||||
]))
|
||||
|
||||
return {
|
||||
...props.groups.find(group => group.key === key),
|
||||
commands: commands.slice(0, options.value.resultLimit)
|
||||
} as Group
|
||||
}))
|
||||
const debouncedSearch = useDebounceFn(async () => {
|
||||
const searchableGroups = props.groups.filter(group => !!group.search)
|
||||
|
||||
await Promise.all(searchableGroups.map(async (group) => {
|
||||
searchResults.value[group.key] = await group.search(query.value)
|
||||
}))
|
||||
}, props.debounce)
|
||||
|
||||
watch(query, () => {
|
||||
debouncedSearch()
|
||||
|
||||
// Select first item on search changes
|
||||
setTimeout(() => {
|
||||
// https://github.com/tailwindlabs/headlessui/blob/6fa6074cd5d3a96f78a2d965392aa44101f5eede/packages/%40headlessui-vue/src/components/combobox/combobox.ts#L804
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="p-2" role="option">
|
||||
<h2 v-if="group[groupAttribute]" class="px-3 my-2 text-xs font-semibold u-text-gray-900">
|
||||
{{ group[groupAttribute] }}
|
||||
<h2 v-if="label" class="px-3 my-2 text-xs font-semibold u-text-gray-900">
|
||||
{{ label }}
|
||||
</h2>
|
||||
|
||||
<div class="text-sm u-text-gray-700" role="listbox" :aria-label="group[groupAttribute]">
|
||||
@@ -52,6 +52,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ComboboxOption } from '@headlessui/vue'
|
||||
import type { PropType } from 'vue'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
@@ -59,11 +60,15 @@ import Avatar from '../elements/Avatar.vue'
|
||||
import type { Group } from '../../types/command-palette'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
group: {
|
||||
type: Object as PropType<Group>,
|
||||
required: true
|
||||
},
|
||||
query: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
groupAttribute: {
|
||||
type: String,
|
||||
required: true
|
||||
@@ -74,6 +79,12 @@ defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const label = computed(() => {
|
||||
const label = props.group[props.groupAttribute]
|
||||
|
||||
return typeof label === 'function' ? label(props.query) : label
|
||||
})
|
||||
|
||||
function highlight ({ indices, value }: { indices: number[][], value:string }, i = 1): string {
|
||||
const pair = indices[indices.length - i]
|
||||
if (!pair) {
|
||||
|
||||
Reference in New Issue
Block a user