diff --git a/docs/pages/examples.vue b/docs/pages/examples.vue index 3ceb42d691..0c7a4a47a9 100644 --- a/docs/pages/examples.vue +++ b/docs/pages/examples.vue @@ -149,18 +149,13 @@ @@ -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 diff --git a/src/runtime/components/navigation/CommandPalette.vue b/src/runtime/components/navigation/CommandPalette.vue index de658dfb02..07af7a87d8 100644 --- a/src/runtime/components/navigation/CommandPalette.vue +++ b/src/runtime/components/navigation/CommandPalette.vue @@ -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" > - + @@ -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>> = 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 diff --git a/src/runtime/components/navigation/CommandPaletteGroup.vue b/src/runtime/components/navigation/CommandPaletteGroup.vue index 3e55fee73a..b598613d29 100644 --- a/src/runtime/components/navigation/CommandPaletteGroup.vue +++ b/src/runtime/components/navigation/CommandPaletteGroup.vue @@ -1,7 +1,7 @@