feat(CommandPalette): handle async search for specific groups

This commit is contained in:
Benjamin Canac
2023-02-17 18:03:59 +01:00
parent 97d40395d3
commit efa9674815
3 changed files with 68 additions and 23 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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) {