feat: search in command panel (#576)

This commit is contained in:
Ayaka Rizumu 2022-12-29 00:29:07 +08:00 committed by GitHub
parent d585d4eeb7
commit a775203bdc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 181 additions and 64 deletions

View file

@ -0,0 +1,58 @@
<script lang="ts" setup>
import type { ResolvedCommand } from '@/composables/command'
const emits = defineEmits<{
(event: 'activate'): void
}>()
const {
cmd,
index,
active = false,
} = $defineProps<{
cmd: ResolvedCommand
index: number
active?: boolean
}>()
</script>
<template>
<div
class="flex px-3 py-2 my-1 items-center rounded-lg hover:bg-active transition-all duration-65 ease-in-out cursor-pointer scroll-m-10"
:class="{ 'bg-active': active }"
:data-index="index"
@click="emits('activate')"
>
<div v-if="cmd.icon" mr-2 :class="cmd.icon" />
<div class="flex-1 flex items-baseline gap-2">
<div :class="{ 'font-medium': active }">
{{ cmd.name }}
</div>
<div v-if="cmd.description" class="text-xs text-secondary">
{{ cmd.description }}
</div>
</div>
<div
v-if="cmd.onComplete"
class="flex items-center gap-1 transition-all duration-65 ease-in-out"
:class="active ? 'opacity-100' : 'opacity-0'"
>
<div class="text-xs text-secondary">
{{ $t('command.complete') }}
</div>
<CommandKey name="Tab" />
</div>
<div
v-if="cmd.onActivate"
class="flex items-center gap-1 transition-all duration-65 ease-in-out"
:class="active ? 'opacity-100' : 'opacity-0'"
>
<div class="text-xs text-secondary">
{{ $t('command.activate') }}
</div>
<CommandKey name="Enter" />
</div>
</div>
</template>

View file

@ -1,5 +1,6 @@
<script setup lang="ts">
import type { CommandScope, QueryIndexedCommand } from '@/composables/command'
import type { SearchResult as SearchResultType } from '@/components/search/types'
import type { CommandScope, QueryResult, QueryResultItem } from '@/composables/command'
const emit = defineEmits<{
(event: 'close'): void
@ -7,6 +8,8 @@ const emit = defineEmits<{
const registry = useCommandRegistry()
const router = useRouter()
const inputEl = $ref<HTMLInputElement>()
const resultEl = $ref<HTMLDivElement>()
@ -18,9 +21,51 @@ onMounted(() => {
})
const commandMode = $computed(() => input.startsWith('>'))
const result = $computed(() => commandMode
const query = $computed(() => commandMode ? '' : input.trim())
const { accounts, hashtags, loading } = useSearch($$(query))
const toSearchQueryResultItem = (search: SearchResultType): QueryResultItem => ({
index: 0,
type: 'search',
search,
onActivate: () => router.push(search.to),
})
const searchResult = $computed<QueryResult>(() => {
if (query.length === 0 || loading.value)
return { length: 0, items: [], grouped: {} as any }
const hashtagList = hashtags.value.slice(0, 3)
.map<SearchResultType>(hashtag => ({ type: 'hashtag', hashtag, to: `/tags/${hashtag.name}` }))
.map(toSearchQueryResultItem)
const accountList = accounts.value
.map<SearchResultType>(account => ({ type: 'account', account, to: `/@${account.acct}` }))
.map(toSearchQueryResultItem)
const grouped: QueryResult['grouped'] = new Map()
grouped.set('Hashtags', hashtagList)
grouped.set('Users', accountList)
let index = 0
for (const items of grouped.values()) {
for (const item of items)
item.index = index++
}
return {
grouped,
items: [...hashtagList, ...accountList],
length: hashtagList.length + accountList.length,
}
})
const result = $computed<QueryResult>(() => commandMode
? registry.query(scopes.map(s => s.id).join('.'), input.slice(1))
: { length: 0, items: [], grouped: {} })
: searchResult,
)
let active = $ref(0)
watch($$(result), (n, o) => {
if (n.length !== o.length || !n.items.every((i, idx) => i === o.items[idx]))
@ -29,7 +74,7 @@ watch($$(result), (n, o) => {
const findItemEl = (index: number) =>
resultEl?.querySelector(`[data-index="${index}"]`) as HTMLDivElement | null
const onCommandActivate = (item: QueryIndexedCommand) => {
const onCommandActivate = (item: QueryResultItem) => {
if (item.onActivate) {
item.onActivate()
emit('close')
@ -39,13 +84,14 @@ const onCommandActivate = (item: QueryIndexedCommand) => {
input = '>'
}
}
const onCommandComplete = (item: QueryIndexedCommand) => {
const onCommandComplete = (item: QueryResultItem) => {
if (item.onComplete) {
scopes.push(item.onComplete())
input = '>'
}
else if (item.onActivate) {
item.onActivate()
emit('close')
}
}
const intoView = (index: number) => {
@ -158,52 +204,30 @@ const onKeyDown = (e: KeyboardEvent) => {
<!-- Results -->
<div ref="resultEl" class="flex-1 mx-1 overflow-y-auto">
<template v-for="[scope, group] in result.grouped" :key="scope">
<div class="mt-2 px-2 py-1 text-sm text-secondary">
{{ scope }}
</div>
<template v-for="cmd in group" :key="cmd.index">
<div
class="flex px-3 py-2 my-1 items-center rounded-lg hover:bg-active transition-all duration-65 ease-in-out cursor-pointer scroll-m-10"
:class="{ 'bg-active': active === cmd.index }"
:data-index="cmd.index"
@click="onCommandActivate(cmd)"
>
<div v-if="cmd.icon" mr-2 :class="cmd.icon" />
<div class="flex-1 flex items-baseline gap-2">
<div :class="{ 'font-medium': active === cmd.index }">
{{ cmd.name }}
</div>
<div v-if="cmd.description" class="text-xs text-secondary">
{{ cmd.description }}
</div>
</div>
<div
v-if="cmd.onComplete"
class="flex items-center gap-1 transition-all duration-65 ease-in-out"
:class="active === cmd.index ? 'opacity-100' : 'opacity-0'"
>
<div class="text-xs text-secondary">
{{ $t('command.complete') }}
</div>
<CommandKey name="Tab" />
</div>
<div
v-if="cmd.onActivate"
class="flex items-center gap-1 transition-all duration-65 ease-in-out"
:class="active === cmd.index ? 'opacity-100' : 'opacity-0'"
>
<div class="text-xs text-secondary">
{{ $t('command.activate') }}
</div>
<CommandKey name="Enter" />
</div>
<template v-if="loading">
<SearchResultSkeleton />
<SearchResultSkeleton />
<SearchResultSkeleton />
</template>
<template v-else-if="result.length">
<template v-for="[scope, group] in result.grouped" :key="scope">
<div class="mt-2 px-2 py-1 text-sm text-secondary">
{{ scope }}
</div>
<template v-for="item in group" :key="item.index">
<SearchResult v-if="item.type === 'search'" :active="active === item.index" :result="item.search" />
<CommandItem v-else :index="item.index" :cmd="item.cmd" :active="active === item.index" @activate="onCommandActivate(item)" />
</template>
</template>
</template>
<div v-else p5 text-center text-secondary italic>
{{
input.length
? $t('common.not_found')
: $t('search.search_desc')
}}
</div>
</div>
<div class="w-full border-b-1 border-base" />