mirror of
https://github.com/the1812/Bilibili-Evolved.git
synced 2025-11-04 21:22:45 +08:00
Support suggest item auto-fill (fix #822)
This commit is contained in:
parent
726b2ef873
commit
bbdb70d5d6
@ -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;
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
},
|
||||
|
||||
@ -20,6 +20,8 @@ export interface LaunchBarAction {
|
||||
deleteAction?: Executable
|
||||
/** 显式选中模式: 开启后可以禁止在列表第一项时直接由 Enter 触发 */
|
||||
explicitSelect?: boolean
|
||||
/** 用于搜索建议的名称, 在搜索结果中选中时会回填到输入框中 */
|
||||
suggestName?: string
|
||||
/** 手动指定在搜索结果中的顺序, 数字越小越排前面 */
|
||||
order?: number
|
||||
}
|
||||
|
||||
@ -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)
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user