Compare commits

...

7 Commits

Author SHA1 Message Date
Benjamin Canac
8ace629ff8 chore(release): 1.1.1 2023-02-20 18:06:15 +01:00
Benjamin Canac
0d35b82ecb chore(CommandPalette): expose query 2023-02-20 18:05:52 +01:00
Benjamin Canac
5f37077835 types(CommandPalette): options no longer exists 2023-02-20 18:05:39 +01:00
Benjamin Canac
948f4b89b1 chore(release): 1.1.0 2023-02-17 19:08:09 +01:00
Benjamin Canac
e6d0dd5898 chore(CommandPalette): set debounce to 200 2023-02-17 19:07:38 +01:00
Benjamin Canac
4702a4f103 fix(CommandPalette): types 2023-02-17 18:14:11 +01:00
Benjamin Canac
efa9674815 feat(CommandPalette): handle async search for specific groups 2023-02-17 18:03:59 +01:00
6 changed files with 85 additions and 25 deletions

View File

@@ -2,6 +2,20 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [1.1.1](https://github.com/nuxtlabs/ui/compare/v1.1.0...v1.1.1) (2023-02-20)
## [1.1.0](https://github.com/nuxtlabs/ui/compare/v1.0.0...v1.1.0) (2023-02-17)
### Features
* **CommandPalette:** handle async search for specific groups ([efa9674](https://github.com/nuxtlabs/ui/commit/efa9674815ab4de756079690da0a381c3703d564))
### Bug Fixes
* **CommandPalette:** types ([4702a4f](https://github.com/nuxtlabs/ui/commit/4702a4f10379201c167cc52099519778756a5780))
## [1.0.0](https://github.com/nuxtlabs/ui/compare/v0.2.1...v1.0.0) (2023-02-17) ## [1.0.0](https://github.com/nuxtlabs/ui/compare/v0.2.1...v1.0.0) (2023-02-17)

View File

@@ -149,18 +149,13 @@
<UCard body-class=""> <UCard body-class="">
<UCommandPalette <UCommandPalette
v-model="form.persons"
multiple
:placeholder="false" :placeholder="false"
:options="{ :options="{
fuseOptions: { fuseOptions: {
includeMatches: true includeMatches: true
} }
}" }"
:groups="[{ :groups="commandPaletteGroups"
key: 'persons',
commands: people
}]"
command-attribute="name" command-attribute="name"
/> />
</UCard> </UCard>
@@ -263,6 +258,18 @@ const y = ref(0)
const isContextMenuOpen = ref(false) const isContextMenuOpen = ref(false)
const contextMenuRef = ref(null) 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(() => { onMounted(() => {
document.addEventListener('mousemove', ({ clientX, clientY }) => { document.addEventListener('mousemove', ({ clientX, clientY }) => {
x.value = clientX x.value = clientX

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nuxthq/ui", "name": "@nuxthq/ui",
"version": "1.0.0", "version": "1.1.1",
"repository": "https://github.com/nuxtlabs/ui", "repository": "https://github.com/nuxtlabs/ui",
"license": "MIT", "license": "MIT",
"exports": { "exports": {

View File

@@ -38,7 +38,14 @@
aria-label="Commands" aria-label="Commands"
class="relative flex-1 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-800 scroll-py-2" 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"> <template v-for="(_, name) in $slots" #[name]="slotData">
<slot :name="name" v-bind="slotData" /> <slot :name="name" v-bind="slotData" />
</template> </template>
@@ -59,6 +66,7 @@
import { ref, computed, watch, onMounted } from 'vue' import { ref, computed, watch, onMounted } from 'vue'
import { Combobox, ComboboxInput, ComboboxOptions } from '@headlessui/vue' import { Combobox, ComboboxInput, ComboboxOptions } from '@headlessui/vue'
import type { ComputedRef, PropType, ComponentPublicInstance } from 'vue' import type { ComputedRef, PropType, ComponentPublicInstance } from 'vue'
import { useDebounceFn } from '@vueuse/core'
import { useFuse } from '@vueuse/integrations/useFuse' import { useFuse } from '@vueuse/integrations/useFuse'
import { groupBy, map } from 'lodash-es' import { groupBy, map } from 'lodash-es'
import { defu } from 'defu' import { defu } from 'defu'
@@ -133,6 +141,10 @@ const props = defineProps({
placeholder: { placeholder: {
type: Boolean, type: Boolean,
default: true default: true
},
debounce: {
type: Number,
default: 200
} }
}) })
@@ -168,29 +180,44 @@ const options: ComputedRef<Partial<UseFuseOptions<Command>>> = computed(() => de
matchAllWhenSearchEmpty: true 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 }))) return acc.concat(group.commands.map(command => ({ ...command, group: group.key })))
}, [] as Command[])) }, [] as Command[]))
const searchResults = ref<{ [key: string]: any }>({})
const { results } = useFuse(query, commands, options) const { results } = useFuse(query, commands, options)
const groups = computed(() => map(groupBy(results.value, command => command.item.group), (results, key) => { const groups = computed(() => ([
const commands = results.map((result) => { ...map(groupBy(results.value, command => command.item.group), (results, key) => {
const { item, ...data } = result const commands = results.map((result) => {
const { item, ...data } = result
return {
...item,
...data
}
})
return { return {
...item, ...props.groups.find(group => group.key === key),
...data 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 { const debouncedSearch = useDebounceFn(async () => {
...props.groups.find(group => group.key === key), const searchableGroups = props.groups.filter(group => !!group.search)
commands: commands.slice(0, options.value.resultLimit)
} as Group await Promise.all(searchableGroups.map(async (group) => {
})) searchResults.value[group.key] = await group.search(query.value)
}))
}, props.debounce)
watch(query, () => { watch(query, () => {
debouncedSearch()
// Select first item on search changes // Select first item on search changes
setTimeout(() => { setTimeout(() => {
// https://github.com/tailwindlabs/headlessui/blob/6fa6074cd5d3a96f78a2d965392aa44101f5eede/packages/%40headlessui-vue/src/components/combobox/combobox.ts#L804 // https://github.com/tailwindlabs/headlessui/blob/6fa6074cd5d3a96f78a2d965392aa44101f5eede/packages/%40headlessui-vue/src/components/combobox/combobox.ts#L804
@@ -228,6 +255,7 @@ function onClear () {
} }
defineExpose({ defineExpose({
query,
updateQuery: (q: string) => { updateQuery: (q: string) => {
query.value = q query.value = q
}, },

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="p-2" role="option"> <div class="p-2" role="option">
<h2 v-if="group[groupAttribute]" class="px-3 my-2 text-xs font-semibold u-text-gray-900"> <h2 v-if="label" class="px-3 my-2 text-xs font-semibold u-text-gray-900">
{{ group[groupAttribute] }} {{ label }}
</h2> </h2>
<div class="text-sm u-text-gray-700" role="listbox" :aria-label="group[groupAttribute]"> <div class="text-sm u-text-gray-700" role="listbox" :aria-label="group[groupAttribute]">
@@ -52,6 +52,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { ComboboxOption } from '@headlessui/vue' import { ComboboxOption } from '@headlessui/vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import Icon from '../elements/Icon.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 type { Group } from '../../types/command-palette'
import $ui from '#build/ui' import $ui from '#build/ui'
defineProps({ const props = defineProps({
group: { group: {
type: Object as PropType<Group>, type: Object as PropType<Group>,
required: true required: true
}, },
query: {
type: String,
default: ''
},
groupAttribute: { groupAttribute: {
type: String, type: String,
required: true 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 { function highlight ({ indices, value }: { indices: number[][], value:string }, i = 1): string {
const pair = indices[indices.length - i] const pair = indices[indices.length - i]
if (!pair) { if (!pair) {

View File

@@ -1,6 +1,7 @@
import type { UseFuseOptions } from '@vueuse/integrations/useFuse' import type { UseFuseOptions } from '@vueuse/integrations/useFuse'
import type { FuseSortFunctionMatch, FuseSortFunctionMatchList } from 'fuse.js' import type { FuseSortFunctionMatch, FuseSortFunctionMatchList } from 'fuse.js'
import type { Avatar } from './avatar' import type { Avatar } from './avatar'
export interface Command { export interface Command {
id: string | number id: string | number
prefix?: string prefix?: string
@@ -22,6 +23,5 @@ export interface Group {
active?: string active?: string
inactive?: string inactive?: string
commands: Command[] commands: Command[]
options?: Partial<UseFuseOptions<Command>>
[key: string]: any [key: string]: any
} }