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
|
<div
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="be-launch-bar-action-item be-launch-bar-suggest-item"
|
class="be-launch-bar-action-item be-launch-bar-suggest-item"
|
||||||
|
:class="{ focused }"
|
||||||
:title="action.displayName || action.name"
|
:title="action.displayName || action.name"
|
||||||
:data-indexer="action.indexer"
|
:data-indexer="action.indexer"
|
||||||
@click.self="performAction($event)"
|
@click.self="performAction($event)"
|
||||||
@ -51,6 +52,7 @@
|
|||||||
import { VIcon } from '@/ui'
|
import { VIcon } from '@/ui'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
focused?: boolean
|
||||||
action: {
|
action: {
|
||||||
name: string
|
name: string
|
||||||
displayName?: string
|
displayName?: string
|
||||||
@ -99,6 +101,7 @@ const performDelete = async (event: KeyboardEvent | MouseEvent) => {
|
|||||||
@include h-center();
|
@include h-center();
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
&:not(.disabled).focused,
|
||||||
&:not(.disabled):hover,
|
&:not(.disabled):hover,
|
||||||
&:not(.disabled):focus-within {
|
&:not(.disabled):focus-within {
|
||||||
background-color: #8882;
|
background-color: #8882;
|
||||||
|
|||||||
@ -4,11 +4,12 @@
|
|||||||
<div class="launch-bar-form">
|
<div class="launch-bar-form">
|
||||||
<input
|
<input
|
||||||
ref="input"
|
ref="input"
|
||||||
v-model="keyword"
|
|
||||||
class="input"
|
class="input"
|
||||||
type="text"
|
type="text"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:placeholder="recommended.word"
|
:placeholder="recommended.word"
|
||||||
|
:value="keyword"
|
||||||
|
@input="handleSearch($event)"
|
||||||
@keydown.enter.stop="handleEnter"
|
@keydown.enter.stop="handleEnter"
|
||||||
@keydown.up.stop="handleUp"
|
@keydown.up.stop="handleUp"
|
||||||
@keydown.down.stop="handleDown"
|
@keydown.down.stop="handleDown"
|
||||||
@ -19,7 +20,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- <div class="input-active-bar"></div> -->
|
<!-- <div class="input-active-bar"></div> -->
|
||||||
</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="isHistory" class="launch-bar-history-list">
|
||||||
<div
|
<div
|
||||||
v-if="actions.length === 0"
|
v-if="actions.length === 0"
|
||||||
@ -32,9 +33,8 @@
|
|||||||
v-for="(a, index) of actions"
|
v-for="(a, index) of actions"
|
||||||
:key="a.name"
|
:key="a.name"
|
||||||
:action="a"
|
:action="a"
|
||||||
@previous-item="previousItem()"
|
:focused="index === itemIndex"
|
||||||
@next-item="nextItem()"
|
@delete-item="onDeleteItem()"
|
||||||
@delete-item="onDeleteItem(index)"
|
|
||||||
@action="
|
@action="
|
||||||
index === actions.length - 1 && onClearHistory()
|
index === actions.length - 1 && onClearHistory()
|
||||||
onAction()
|
onAction()
|
||||||
@ -43,12 +43,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="!isHistory" class="launch-bar-action-list">
|
<div v-if="!isHistory" class="launch-bar-action-list">
|
||||||
<VEmpty
|
<VEmpty
|
||||||
v-if="actions.length === 0 && noActions"
|
v-if="actions.length === 0 && noOnlineActions"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="be-launch-bar-suggest-item disabled"
|
class="be-launch-bar-suggest-item disabled"
|
||||||
></VEmpty>
|
></VEmpty>
|
||||||
<VLoading
|
<VLoading
|
||||||
v-if="actions.length === 0 && !noActions"
|
v-if="actions.length === 0 && !noOnlineActions"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="be-launch-bar-suggest-item disabled"
|
class="be-launch-bar-suggest-item disabled"
|
||||||
></VLoading>
|
></VLoading>
|
||||||
@ -56,9 +56,8 @@
|
|||||||
v-for="(a, index) of actions"
|
v-for="(a, index) of actions"
|
||||||
:key="a.name"
|
:key="a.name"
|
||||||
:action="a"
|
:action="a"
|
||||||
@previous-item="previousItem()"
|
:focused="index === itemIndex"
|
||||||
@next-item="nextItem()"
|
@delete-item="onDeleteItem()"
|
||||||
@delete-item="onDeleteItem(index)"
|
|
||||||
@action="onAction()"
|
@action="onAction()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -66,7 +65,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<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 Fuse from 'fuse.js'
|
||||||
import { VIcon, VLoading, VEmpty } from '@/ui'
|
import { VIcon, VLoading, VEmpty } from '@/ui'
|
||||||
import { registerAndGetData } from '@/plugins/data'
|
import { registerAndGetData } from '@/plugins/data'
|
||||||
@ -82,14 +81,13 @@ import {
|
|||||||
} from './launch-bar-action'
|
} from './launch-bar-action'
|
||||||
import { searchProvider, search } from './search-provider'
|
import { searchProvider, search } from './search-provider'
|
||||||
import { historyProvider } from './history-provider'
|
import { historyProvider } from './history-provider'
|
||||||
import { FocusTarget } from './focus-target'
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: 'close'): void
|
(event: 'close'): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const input = ref<HTMLInputElement>()
|
const input = ref<HTMLInputElement>()
|
||||||
const list = ref<HTMLElement>()
|
const itemIndex = ref(-1)
|
||||||
|
|
||||||
const [actionProviders] = registerAndGetData(LaunchBarActionProviders, [
|
const [actionProviders] = registerAndGetData(LaunchBarActionProviders, [
|
||||||
searchProvider,
|
searchProvider,
|
||||||
@ -102,10 +100,35 @@ const [recommended] = registerAndGetData('launchBar.recommended', {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const actions = ref<LaunchBarAction[]>([])
|
const actions = ref<LaunchBarAction[]>([])
|
||||||
|
const lastKeyword = ref('')
|
||||||
const keyword = ref('')
|
const keyword = ref('')
|
||||||
const focusTarget = new FocusTarget(0)
|
const noOnlineActions = ref(false)
|
||||||
const noActions = 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 isHistory = computed(() => keyword.value.length === 0)
|
||||||
|
|
||||||
const sortActions = (actionsList: LaunchBarAction[]) => {
|
const sortActions = (actionsList: LaunchBarAction[]) => {
|
||||||
@ -147,13 +170,13 @@ const getOnlineActionsInternal = async () => {
|
|||||||
const fuseResult = fuse.search(keyword.value)
|
const fuseResult = fuse.search(keyword.value)
|
||||||
console.log(fuseResult)
|
console.log(fuseResult)
|
||||||
actions.value = sortActions(fuseResult.map(it => it.item).slice(0, 13))
|
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 getOnlineActions = lodash.debounce(getOnlineActionsInternal, 200)
|
||||||
|
|
||||||
const getActions = async () => {
|
const getActions = async () => {
|
||||||
noActions.value = false
|
noOnlineActions.value = false
|
||||||
if (isHistory.value) {
|
if (isHistory.value) {
|
||||||
actions.value = sortActions(
|
actions.value = sortActions(
|
||||||
generateKeys(historyProvider, await historyProvider.getActions(keyword.value)),
|
generateKeys(historyProvider, await historyProvider.getActions(keyword.value)),
|
||||||
@ -183,13 +206,18 @@ const setupSearchPageSync = async () => {
|
|||||||
|
|
||||||
const handleSelect = () => {
|
const handleSelect = () => {
|
||||||
emit('close')
|
emit('close')
|
||||||
getActions()
|
// getActions()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEnter = async (e?: KeyboardEvent | MouseEvent) => {
|
const handleEnter = async (e?: KeyboardEvent | MouseEvent) => {
|
||||||
if ((e as KeyboardEvent)?.isComposing) {
|
if ((e as KeyboardEvent)?.isComposing) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (hasFocus.value) {
|
||||||
|
await focusedItem.value?.action()
|
||||||
|
handleSelect()
|
||||||
|
return
|
||||||
|
}
|
||||||
if (actions.value.length > 0 && !isHistory.value) {
|
if (actions.value.length > 0 && !isHistory.value) {
|
||||||
const [first] = actions.value as LaunchBarAction[]
|
const [first] = actions.value as LaunchBarAction[]
|
||||||
if (first.explicitSelect !== true) {
|
if (first.explicitSelect !== true) {
|
||||||
@ -210,7 +238,7 @@ const handleUp = (e: KeyboardEvent) => {
|
|||||||
if (e.isComposing) {
|
if (e.isComposing) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
focusTarget.previous()
|
previousItem()
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,45 +246,24 @@ const handleDown = (e: KeyboardEvent) => {
|
|||||||
if (e.isComposing) {
|
if (e.isComposing) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
focusTarget.next()
|
nextItem()
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
const focusInput = () => {
|
const handleSearch = (e: Event) => {
|
||||||
input.value?.focus()
|
keyword.value = (e.target as HTMLInputElement).value
|
||||||
|
lastKeyword.value = keyword.value
|
||||||
|
resetFocus()
|
||||||
|
getActions()
|
||||||
}
|
}
|
||||||
|
|
||||||
const focusSuggestItem = (nth: number) => {
|
const onDeleteItem = () => {
|
||||||
;(
|
previousItem()
|
||||||
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()
|
|
||||||
getActions()
|
getActions()
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClearHistory = () => {
|
const onClearHistory = () => {
|
||||||
focusInput()
|
resetFocus()
|
||||||
getActions()
|
getActions()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,27 +271,16 @@ const onAction = () => {
|
|||||||
handleSelect()
|
handleSelect()
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(keyword, () => {
|
|
||||||
getActions()
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(actions, () => {
|
|
||||||
focusTarget.reset(actions.value.length)
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await getActions()
|
await getActions()
|
||||||
if (matchUrlPattern(/^https?:\/\/search\.bilibili\.com/)) {
|
if (matchUrlPattern(/^https?:\/\/search\.bilibili\.com/)) {
|
||||||
await setupSearchPageSync()
|
await setupSearchPageSync()
|
||||||
}
|
}
|
||||||
focusTarget.addEventListener('index-change', () => {
|
|
||||||
handleIndexUpdate()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
input,
|
input,
|
||||||
focusInput,
|
focusInput: resetFocus,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<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',
|
icon: 'mdi-history',
|
||||||
// description: `在 ${formatDate(new Date(it.timestamp))} 搜索过`,
|
// description: `在 ${formatDate(new Date(it.timestamp))} 搜索过`,
|
||||||
explicitSelect: true,
|
explicitSelect: true,
|
||||||
|
suggestName: it.value,
|
||||||
action: () => {
|
action: () => {
|
||||||
search(it.value)
|
search(it.value)
|
||||||
},
|
},
|
||||||
|
|||||||
@ -20,6 +20,8 @@ export interface LaunchBarAction {
|
|||||||
deleteAction?: Executable
|
deleteAction?: Executable
|
||||||
/** 显式选中模式: 开启后可以禁止在列表第一项时直接由 Enter 触发 */
|
/** 显式选中模式: 开启后可以禁止在列表第一项时直接由 Enter 触发 */
|
||||||
explicitSelect?: boolean
|
explicitSelect?: boolean
|
||||||
|
/** 用于搜索建议的名称, 在搜索结果中选中时会回填到输入框中 */
|
||||||
|
suggestName?: string
|
||||||
/** 手动指定在搜索结果中的顺序, 数字越小越排前面 */
|
/** 手动指定在搜索结果中的顺序, 数字越小越排前面 */
|
||||||
order?: number
|
order?: number
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,9 +47,11 @@ export const searchProvider: LaunchBarActionProvider = {
|
|||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
results.push(
|
results.push(
|
||||||
...suggests.map(result => ({
|
...suggests.map(
|
||||||
|
(result): LaunchBarAction => ({
|
||||||
name: `${input}.${result.value}`,
|
name: `${input}.${result.value}`,
|
||||||
icon: 'search',
|
icon: 'search',
|
||||||
|
suggestName: result.value,
|
||||||
content: async () =>
|
content: async () =>
|
||||||
Vue.extend({
|
Vue.extend({
|
||||||
render: h => {
|
render: h => {
|
||||||
@ -62,7 +64,8 @@ export const searchProvider: LaunchBarActionProvider = {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
action: () => search(result.value),
|
action: () => search(result.value),
|
||||||
})),
|
}),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return lodash.uniqBy(results, it => it.name)
|
return lodash.uniqBy(results, it => it.name)
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user