Bilibili-Evolved/src/components/launch-bar/LaunchBar.vue
timongh 87189cc05e Fix event handlers
Fix event handlers with multiple lines of code in the Vue template.
2023-03-09 17:34:51 +08:00

350 lines
9.0 KiB
Vue

<template>
<div class="launch-bar">
<div class="input-area">
<div class="launch-bar-form">
<input
ref="input"
v-model="keyword"
class="input"
type="text"
autocomplete="off"
:placeholder="recommended.word"
@keydown.enter.stop="handleEnter"
@keydown.up.stop="handleUp"
@keydown.down.stop="handleDown"
/>
<button class="submit" title="执行" tabindex="-1" @click="handleEnter">
<VIcon icon="right-arrow" :size="20"></VIcon>
</button>
</div>
<!-- <div class="input-active-bar"></div> -->
</div>
<div ref="list" class="launch-bar-suggest-list">
<div v-if="isHistory" class="launch-bar-history-list">
<div v-if="actions.length === 0" class="history-empty suggest-item disabled" tabindex="0">
暂无搜索历史
</div>
<ActionItem
v-for="(a, index) of actions"
:key="a.key"
:action="a"
@previous-item="previousItem($event, index)"
@next-item="nextItem($event, index)"
@delete-item="onDeleteItem($event, index)"
@action="
() => {
index === actions.length - 1 && onClearHistory()
onAction()
}
"
/>
</div>
<div v-if="!isHistory" class="launch-bar-action-list">
<VEmpty
v-if="actions.length === 0 && noActions"
tabindex="0"
class="suggest-item disabled"
></VEmpty>
<VLoading
v-if="actions.length === 0 && !noActions"
tabindex="0"
class="suggest-item disabled"
></VLoading>
<ActionItem
v-for="(a, index) of actions"
:key="a.key"
:action="a"
@previous-item="previousItem($event, index)"
@next-item="nextItem($event, index)"
@delete-item="onDeleteItem($event, index)"
@action="onAction"
/>
</div>
</div>
</div>
</template>
<script lang="ts">
import Fuse from 'fuse.js'
import type { Ref } from 'vue'
import { defineComponent, ref } from 'vue'
import { select } from '@/core/spin-query'
import { matchUrlPattern } from '@/core/utils'
import { registerAndGetData } from '@/plugins/data'
import { VEmpty, VIcon, VLoading } from '@/ui'
import ActionItem from './ActionItem.vue'
import { historyProvider } from './history-provider'
import type { LaunchBarAction, LaunchBarActionProvider } from './launch-bar-action'
import { LaunchBarActionProviders } from './launch-bar-action'
import { search, searchProvider } from './search-provider'
const [actionProviders] = registerAndGetData(LaunchBarActionProviders, [
searchProvider,
historyProvider,
]) as [LaunchBarActionProvider[]]
const generateKeys = (
provider: LaunchBarActionProvider,
actions: LaunchBarAction[],
): ({
key: string
provider: LaunchBarActionProvider
} & LaunchBarAction)[] =>
actions.map(a => {
const key = `${provider.name}.${a.name}`
return {
...a,
key,
provider,
}
})
async function getOnlineActions(this: InstanceType<typeof ThisComponent>) {
const onlineActions = (
await Promise.all(
actionProviders.map(async provider =>
generateKeys(provider, await provider.getActions(this.keyword)),
),
)
).flat()
if (this.isHistory) {
return
}
const fuse = new Fuse(onlineActions, {
keys: ['indexer', 'displayName', 'name', 'description', 'key'],
})
const fuseResult = fuse.search(this.keyword)
console.log(fuseResult)
this.actions = fuseResult.map(it => it.item).slice(0, 12)
this.noActions = this.actions.length === 0
}
async function getActions(this: InstanceType<typeof ThisComponent>) {
this.noActions = false
if (this.isHistory) {
this.actions = generateKeys(historyProvider, await historyProvider.getActions(this.keyword))
return
}
this.actions = []
this.getOnlineActions().then()
}
const [recommended] = registerAndGetData('launchBar.recommended', {
word: '搜索',
href: 'https://search.bilibili.com/',
})
const ThisComponent = defineComponent({
components: {
VIcon,
VLoading,
VEmpty,
ActionItem,
},
emits: ['close'],
setup: () => ({
input: ref(null) as Ref<HTMLInputElement | null>,
list: ref(null) as Ref<HTMLDivElement | null>,
}),
data() {
return {
recommended,
actions: [] as ({
key: string
provider: LaunchBarActionProvider
} & LaunchBarAction)[],
keyword: '',
noActions: false,
}
},
computed: {
isHistory(): boolean {
return this.keyword.length === 0
},
},
watch: {
keyword() {
this.getActions()
},
},
async mounted() {
this.getActions().then()
if (!matchUrlPattern(/^https?:\/\/search\.bilibili\.com/)) {
return
}
select('#search-keyword').then((input: HTMLInputElement) => {
if (!input) {
return
}
this.keyword = input.value
document.addEventListener('change', e => {
if (!(e.target instanceof HTMLInputElement)) {
return
}
if (e.target.id === 'search-keyword') {
this.keyword = e.target.value
}
})
})
},
methods: {
getOnlineActions: lodash.debounce(getOnlineActions, 200) as unknown as () => Promise<void>,
getActions,
handleSelect() {
this.$emit('close')
this.getActions()
},
async handleEnter(e: KeyboardEvent | MouseEvent) {
if ('isComposing' in e && e.isComposing) {
return
}
if (this.actions.length > 0 && !this.isHistory) {
const [first] = this.actions
if (first.explicitSelect === false) {
first.action()
return
}
}
if (this.keyword) {
search(this.keyword)
this.handleSelect()
return
}
window.open(this.recommended.href, '_blank')
this.handleSelect()
},
handleUp(e: KeyboardEvent) {
if (e.isComposing) {
return
}
;(this.list.querySelector('.suggest-item:last-child') as HTMLElement).focus()
e.preventDefault()
},
handleDown(e: KeyboardEvent) {
if (e.isComposing) {
return
}
;(this.list.querySelector('.suggest-item') as HTMLElement).focus()
e.preventDefault()
},
previousItem(e: KeyboardEvent | MouseEvent, index: number) {
if (index === 0) {
this.focus()
} else {
;((e.currentTarget as HTMLElement).previousElementSibling as HTMLElement).focus()
}
},
nextItem(e: KeyboardEvent, index: number) {
const lastItemIndex = this.actions.length - 1
if (index !== lastItemIndex) {
;((e.currentTarget as HTMLElement).nextElementSibling as HTMLElement).focus()
} else {
this.focus()
}
},
search,
onDeleteItem(e: KeyboardEvent | MouseEvent, index: number) {
this.previousItem(e, index)
this.getActions()
},
onClearHistory() {
this.focus()
this.getActions()
},
onAction() {
// this.focus()
this.handleSelect()
},
focus() {
this.input.focus()
},
},
})
export default ThisComponent
</script>
<style lang="scss">
@import 'common';
.launch-bar {
--color: black;
color: var(--color);
position: relative;
@include h-center();
.input-area {
display: flex;
flex-direction: column;
flex: 1;
.recommended-target {
display: none;
}
.launch-bar-form {
flex: 1;
display: flex;
.input {
flex: 1;
padding: 8px;
background-color: transparent;
border: none;
outline: none !important;
color: inherit;
box-sizing: border-box;
width: 15vw;
font-size: inherit;
&::placeholder {
color: inherit !important;
opacity: 0.8;
}
}
.submit {
color: inherit;
padding: 4px;
background: transparent;
border: none;
outline: none !important;
cursor: pointer;
}
}
.input-active-bar {
flex: 0 0 auto;
align-self: flex-start;
height: 2px;
width: 0;
border-radius: 1px;
background-color: var(--theme-color);
transition: 0.3s ease-in-out;
}
}
.launch-bar-suggest-list {
position: absolute;
top: 100%;
left: 50%;
opacity: 0;
width: 100%;
transform: translateX(-50%) translateY(-4px);
pointer-events: none;
// transition: 0.2s ease-out;
border: 1px solid #8882;
white-space: nowrap;
border-radius: 8px;
@include shadow();
color: black;
background-color: #fff;
body.dark & {
color: #eee;
background-color: #222;
}
.suggest-highlight {
color: var(--theme-color);
font-style: normal;
}
}
&:focus-within {
.input-active-bar {
width: 100%;
}
}
&:focus-within .launch-bar-suggest-list,
.launch-bar-suggest-list:focus-within {
opacity: 1;
transform: translateX(-50%);
pointer-events: initial;
}
}
</style>