Support suggest item auto-fill (fix #822)

This commit is contained in:
the1812 2025-09-09 21:30:56 +08:00
parent 726b2ef873
commit bbdb70d5d6
6 changed files with 82 additions and 121 deletions

View File

@ -2,6 +2,7 @@
<div
tabindex="0"
class="be-launch-bar-action-item be-launch-bar-suggest-item"
:class="{ focused }"
:title="action.displayName || action.name"
:data-indexer="action.indexer"
@click.self="performAction($event)"
@ -51,6 +52,7 @@
import { VIcon } from '@/ui'
interface Props {
focused?: boolean
action: {
name: string
displayName?: string
@ -99,6 +101,7 @@ const performDelete = async (event: KeyboardEvent | MouseEvent) => {
@include h-center();
justify-content: center;
}
&:not(.disabled).focused,
&:not(.disabled):hover,
&:not(.disabled):focus-within {
background-color: #8882;

View File

@ -4,11 +4,12 @@
<div class="launch-bar-form">
<input
ref="input"
v-model="keyword"
class="input"
type="text"
autocomplete="off"
:placeholder="recommended.word"
:value="keyword"
@input="handleSearch($event)"
@keydown.enter.stop="handleEnter"
@keydown.up.stop="handleUp"
@keydown.down.stop="handleDown"
@ -19,7 +20,7 @@
</div>
<!-- <div class="input-active-bar"></div> -->
</div>
<div ref="list" class="launch-bar-suggest-list">
<div class="launch-bar-suggest-list">
<div v-if="isHistory" class="launch-bar-history-list">
<div
v-if="actions.length === 0"
@ -32,9 +33,8 @@
v-for="(a, index) of actions"
:key="a.name"
:action="a"
@previous-item="previousItem()"
@next-item="nextItem()"
@delete-item="onDeleteItem(index)"
:focused="index === itemIndex"
@delete-item="onDeleteItem()"
@action="
index === actions.length - 1 && onClearHistory()
onAction()
@ -43,12 +43,12 @@
</div>
<div v-if="!isHistory" class="launch-bar-action-list">
<VEmpty
v-if="actions.length === 0 && noActions"
v-if="actions.length === 0 && noOnlineActions"
tabindex="0"
class="be-launch-bar-suggest-item disabled"
></VEmpty>
<VLoading
v-if="actions.length === 0 && !noActions"
v-if="actions.length === 0 && !noOnlineActions"
tabindex="0"
class="be-launch-bar-suggest-item disabled"
></VLoading>
@ -56,9 +56,8 @@
v-for="(a, index) of actions"
:key="a.name"
:action="a"
@previous-item="previousItem()"
@next-item="nextItem()"
@delete-item="onDeleteItem(index)"
:focused="index === itemIndex"
@delete-item="onDeleteItem()"
@action="onAction()"
/>
</div>
@ -66,7 +65,7 @@
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, nextTick, onMounted } from 'vue'
import { ref, computed, nextTick, onMounted } from 'vue'
import Fuse from 'fuse.js'
import { VIcon, VLoading, VEmpty } from '@/ui'
import { registerAndGetData } from '@/plugins/data'
@ -82,14 +81,13 @@ import {
} from './launch-bar-action'
import { searchProvider, search } from './search-provider'
import { historyProvider } from './history-provider'
import { FocusTarget } from './focus-target'
const emit = defineEmits<{
(event: 'close'): void
}>()
const input = ref<HTMLInputElement>()
const list = ref<HTMLElement>()
const itemIndex = ref(-1)
const [actionProviders] = registerAndGetData(LaunchBarActionProviders, [
searchProvider,
@ -102,10 +100,35 @@ const [recommended] = registerAndGetData('launchBar.recommended', {
})
const actions = ref<LaunchBarAction[]>([])
const lastKeyword = ref('')
const keyword = ref('')
const focusTarget = new FocusTarget(0)
const noActions = ref(false)
const noOnlineActions = ref(false)
const setItemIndex = (index: number) => {
const newIndex = lodash.clamp(index, -1, actions.value.length - 1)
if (itemIndex.value !== newIndex) {
itemIndex.value = newIndex
if (newIndex > -1 && actions.value[newIndex].suggestName) {
keyword.value = actions.value[newIndex].suggestName
if (
lastKeyword.value !== '' &&
keyword.value.toLowerCase().startsWith(lastKeyword.value.toLowerCase())
) {
nextTick(() => {
input.value?.setSelectionRange(lastKeyword.value.length, keyword.value.length)
})
}
} else {
keyword.value = lastKeyword.value
}
}
}
const resetFocus = () => setItemIndex(-1)
const nextItem = () => setItemIndex(itemIndex.value + 1)
const previousItem = () => setItemIndex(itemIndex.value - 1)
const hasFocus = computed(() => itemIndex.value > -1)
const focusedItem = computed(() => actions.value[itemIndex.value])
const isHistory = computed(() => keyword.value.length === 0)
const sortActions = (actionsList: LaunchBarAction[]) => {
@ -147,13 +170,13 @@ const getOnlineActionsInternal = async () => {
const fuseResult = fuse.search(keyword.value)
console.log(fuseResult)
actions.value = sortActions(fuseResult.map(it => it.item).slice(0, 13))
noActions.value = actions.value.length === 0
noOnlineActions.value = actions.value.length === 0
}
const getOnlineActions = lodash.debounce(getOnlineActionsInternal, 200)
const getActions = async () => {
noActions.value = false
noOnlineActions.value = false
if (isHistory.value) {
actions.value = sortActions(
generateKeys(historyProvider, await historyProvider.getActions(keyword.value)),
@ -183,13 +206,18 @@ const setupSearchPageSync = async () => {
const handleSelect = () => {
emit('close')
getActions()
// getActions()
}
const handleEnter = async (e?: KeyboardEvent | MouseEvent) => {
if ((e as KeyboardEvent)?.isComposing) {
return
}
if (hasFocus.value) {
await focusedItem.value?.action()
handleSelect()
return
}
if (actions.value.length > 0 && !isHistory.value) {
const [first] = actions.value as LaunchBarAction[]
if (first.explicitSelect !== true) {
@ -210,7 +238,7 @@ const handleUp = (e: KeyboardEvent) => {
if (e.isComposing) {
return
}
focusTarget.previous()
previousItem()
e.preventDefault()
}
@ -218,45 +246,24 @@ const handleDown = (e: KeyboardEvent) => {
if (e.isComposing) {
return
}
focusTarget.next()
nextItem()
e.preventDefault()
}
const focusInput = () => {
input.value?.focus()
const handleSearch = (e: Event) => {
keyword.value = (e.target as HTMLInputElement).value
lastKeyword.value = keyword.value
resetFocus()
getActions()
}
const focusSuggestItem = (nth: number) => {
;(
list.value?.querySelector(`.be-launch-bar-suggest-item:nth-child(${nth})`) as HTMLElement
)?.focus()
}
const handleIndexUpdate = async () => {
await nextTick()
if (!focusTarget.hasFocus) {
focusInput()
return
}
focusSuggestItem(focusTarget.index + 1)
}
const previousItem = () => {
focusTarget.previous()
}
const nextItem = () => {
focusTarget.next()
}
const onDeleteItem = (index: number) => {
focusTarget.setFocus(index)
focusTarget.previous()
const onDeleteItem = () => {
previousItem()
getActions()
}
const onClearHistory = () => {
focusInput()
resetFocus()
getActions()
}
@ -264,27 +271,16 @@ const onAction = () => {
handleSelect()
}
watch(keyword, () => {
getActions()
})
watch(actions, () => {
focusTarget.reset(actions.value.length)
})
onMounted(async () => {
await getActions()
if (matchUrlPattern(/^https?:\/\/search\.bilibili\.com/)) {
await setupSearchPageSync()
}
focusTarget.addEventListener('index-change', () => {
handleIndexUpdate()
})
})
defineExpose({
input,
focusInput,
focusInput: resetFocus,
})
</script>
<style lang="scss">

View File

@ -1,44 +0,0 @@
export class FocusTarget extends EventTarget {
/**
* -1: Input Focus
* > -1: Item Focus
*/
private itemIndex = -1
private itemLength = 0
constructor(length: number, index = -1) {
super()
this.itemLength = length
this.index = index
}
get index() {
return this.itemIndex
}
private set index(value: number) {
const newIndex = lodash.clamp(value, -1, this.itemLength - 1)
if (this.itemIndex !== newIndex) {
this.itemIndex = newIndex
this.dispatchEvent(new CustomEvent('index-change', { detail: this }))
}
}
get hasFocus() {
return this.itemIndex > -1
}
setFocus(index: number) {
this.index = index
}
reset(length: number, index = this.index) {
this.itemLength = length
this.index = index
}
next() {
this.index += 1
console.log(this.index)
}
previous() {
this.index -= 1
console.log(this.index)
}
}

View File

@ -68,6 +68,7 @@ export const historyProvider: LaunchBarActionProvider = {
icon: 'mdi-history',
// description: `在 ${formatDate(new Date(it.timestamp))} 搜索过`,
explicitSelect: true,
suggestName: it.value,
action: () => {
search(it.value)
},

View File

@ -20,6 +20,8 @@ export interface LaunchBarAction {
deleteAction?: Executable
/** 显式选中模式: 开启后可以禁止在列表第一项时直接由 Enter 触发 */
explicitSelect?: boolean
/** 用于搜索建议的名称, 在搜索结果中选中时会回填到输入框中 */
suggestName?: string
/** 手动指定在搜索结果中的顺序, 数字越小越排前面 */
order?: number
}

View File

@ -47,22 +47,25 @@ export const searchProvider: LaunchBarActionProvider = {
return results
}
results.push(
...suggests.map(result => ({
name: `${input}.${result.value}`,
icon: 'search',
content: async () =>
Vue.extend({
render: h => {
const content = h('div', {
domProps: {
innerHTML: result.name.replace(/suggest_high_light/g, 'suggest-highlight'),
},
})
return content
},
}),
action: () => search(result.value),
})),
...suggests.map(
(result): LaunchBarAction => ({
name: `${input}.${result.value}`,
icon: 'search',
suggestName: result.value,
content: async () =>
Vue.extend({
render: h => {
const content = h('div', {
domProps: {
innerHTML: result.name.replace(/suggest_high_light/g, 'suggest-highlight'),
},
})
return content
},
}),
action: () => search(result.value),
}),
),
)
return lodash.uniqBy(results, it => it.name)
},