fix(webui): keyboard navigation in searchbox results

closes #250
This commit is contained in:
Gauthier Roebroeck 2021-03-03 16:20:38 +08:00
parent a5c7b17829
commit 604ccf1192
2 changed files with 124 additions and 96 deletions

View file

@ -1,113 +1,94 @@
<template>
<div id="searchbox">
<v-text-field v-model="search"
<div>
<v-autocomplete
v-model="selectedItem"
:placeholder="$t('search.search')"
:no-data-text="$t('searchbox.no_results')"
:loading="loading"
:items="results"
:hide-no-data="!showResults"
clearable
solo
hide-details
clearable
no-filter
return-object
prepend-inner-icon="mdi-magnify"
:label="$t('search.search')"
:loading="loading"
@click:clear="clear"
append-icon=""
item-text="id"
auto-select-first
:search-input.sync="search"
:menu-props="{maxHeight: $vuetify.breakpoint.height * .9, minWidth: $vuetify.breakpoint.mdAndUp ? $vuetify.breakpoint.width * .4 : $vuetify.breakpoint.width * .8}"
@keydown.esc="clear"
@keydown.enter="searchDetails"
/>
<v-menu nudge-bottom="57"
nudge-right="52"
attach="#searchbox"
v-model="showResults"
:max-height="$vuetify.breakpoint.height * .9"
:min-width="$vuetify.breakpoint.mdAndUp ? $vuetify.breakpoint.width * .4 : $vuetify.breakpoint.width * .8"
ref="searchbox"
>
<v-list>
<v-list-item
v-if="series.length === 0 && books.length === 0 && collections.length === 0 && readLists.length === 0">
{{ $t('searchbox.no_results') }}
</v-list-item>
<template v-slot:selection>
</template>
<template v-if="series.length !== 0">
<v-subheader class="text-uppercase">{{ $t('common.series') }}</v-subheader>
<v-list-item v-for="item in series"
:key="item.id"
link
:to="{name: 'browse-series', params: {seriesId: item.id}}"
>
<v-img :src="seriesThumbnailUrl(item.id)"
<template v-slot:item="data">
<template v-if="typeof data.item !== 'object'">
<v-list-item-content v-text="data.item"></v-list-item-content>
</template>
<template v-if="data.item.type === 'search'">
<v-list-item-content>{{ $t('searchbox.search_all') }}</v-list-item-content>
</template>
<template v-if="data.item.type === 'series'">
<v-img :src="seriesThumbnailUrl(data.item.id)"
height="50"
max-width="35"
class="my-1 mx-3"
>
<span v-if="item.booksUnreadCount !== 0"
<span v-if="data.item.booksUnreadCount !== 0"
class="white--text pa-0 px-1 text-caption"
:style="{background: 'orange', position: 'absolute', right: 0}"
>
{{ item.booksUnreadCount }}
{{ data.item.booksUnreadCount }}
</span>
</v-img>
<v-list-item-content>
<v-list-item-title v-text="item.metadata.title"/>
<v-list-item-title v-text="data.item.metadata.title"/>
</v-list-item-content>
</v-list-item>
</template>
<template v-if="books.length !== 0">
<v-subheader class="text-uppercase">{{ $t('common.books') }}</v-subheader>
<v-list-item v-for="item in books"
:key="item.id"
link
:to="{name: 'browse-book', params: {bookId: item.id}}"
>
<v-img :src="bookThumbnailUrl(item.id)"
<template v-if="data.item.type === 'book'">
<v-img :src="bookThumbnailUrl(data.item.id)"
height="50"
max-width="35"
class="my-1 mx-3"
>
<div class="unread" v-if="isUnread(item)"/>
<div class="unread" v-if="isUnread(data.item)"/>
</v-img>
<v-list-item-content>
<v-list-item-title v-text="item.metadata.title"/>
<v-list-item-title v-text="data.item.metadata.title"/>
</v-list-item-content>
</v-list-item>
</template>
<template v-if="collections.length !== 0">
<v-subheader class="text-uppercase">{{ $t('common.collections') }}</v-subheader>
<v-list-item v-for="item in collections"
:key="item.id"
link
:to="{name: 'browse-collection', params: {collectionId: item.id}}"
>
<v-img :src="collectionThumbnailUrl(item.id)"
<template v-if="data.item.type === 'collection'">
<v-img :src="collectionThumbnailUrl(data.item.id)"
height="50"
max-width="35"
class="my-1 mx-3"
/>
<v-list-item-content>
<v-list-item-title v-text="item.name"/>
<v-list-item-title v-text="data.item.name"/>
</v-list-item-content>
</v-list-item>
</template>
<template v-if="readLists.length !== 0">
<v-subheader class="text-uppercase">{{ $t('common.readlists') }}</v-subheader>
<v-list-item v-for="item in readLists"
:key="item.id"
link
:to="{name: 'browse-readlist', params: {readListId: item.id}}"
>
<v-img :src="readListThumbnailUrl(item.id)"
<template v-if="data.item.type === 'readlist'">
<v-img :src="readListThumbnailUrl(data.item.id)"
height="50"
max-width="35"
class="my-1 mx-3"
/>
<v-list-item-content>
<v-list-item-title v-text="item.name"/>
<v-list-item-title v-text="data.item.name"/>
</v-list-item-content>
</v-list-item>
</template>
</v-list>
</v-menu>
</template>
</v-autocomplete>
</div>
</template>
@ -124,6 +105,7 @@ export default Vue.extend({
name: 'SearchBox',
data: function () {
return {
selectedItem: null as unknown as any,
search: null,
showResults: false,
loading: false,
@ -135,6 +117,25 @@ export default Vue.extend({
}
},
watch: {
selectedItem(val, old) {
if (val && val.hasOwnProperty('type')) {
this.$nextTick(() => {
this.selectedItem = undefined
})
if (val.type === 'series') this.$router.push({name: 'browse-series', params: {seriesId: val.id}})
else if (val.type === 'book') this.$router.push({name: 'browse-book', params: {bookId: val.id}})
else if (val.type === 'collection') this.$router.push({
name: 'browse-collection',
params: {collectionId: val.id},
})
else if (val.type === 'readlist') this.$router.push({name: 'browse-readlist', params: {readListId: val.id}})
else if (val.type === 'search') this.searchDetails()
//@ts-ignore
this.$refs.searchbox.blur()
}
},
search(val) {
this.searchItems(val)
},
@ -142,6 +143,31 @@ export default Vue.extend({
!val && this.clear()
},
},
computed: {
results(): object[] {
const results = []
if (this.search) {
results.push({type: 'search'})
if (this.series.length > 0) {
results.push({header: this.$t('common.series').toString().toUpperCase()})
results.push(...this.series.map(o => ({...o, type: 'series'})))
}
if (this.books.length > 0) {
results.push({header: this.$t('common.books').toString().toUpperCase()})
results.push(...this.books.map(o => ({...o, type: 'book'})))
}
if (this.collections.length > 0) {
results.push({header: this.$t('common.collections').toString().toUpperCase()})
results.push(...this.collections.map(o => ({...o, type: 'collection'})))
}
if (this.readLists.length > 0) {
results.push({header: this.$t('common.readlists').toString().toUpperCase()})
results.push(...this.readLists.map(o => ({...o, type: 'readlist'})))
}
}
return results
},
},
methods: {
searchItems: debounce(async function (this: any, query: string) {
if (query) {
@ -158,6 +184,7 @@ export default Vue.extend({
}, 500),
clear() {
this.search = null
// this.selectedItem = null
this.showResults = false
this.series = []
this.books = []

View file

@ -121,6 +121,7 @@
"collections": "Collections",
"create": "Create",
"delete": "Delete",
"download": "Download",
"email": "Email",
"filter_no_matches": "The active filter has no matches",
"genre": "Genre",
@ -133,15 +134,14 @@
"pages_n": "No pages | 1 page | {count} pages",
"password": "Password",
"publisher": "Publisher",
"read": "Read",
"readlists": "Read Lists",
"required": "Required",
"roles": "Roles",
"series": "Series",
"tags": "Tags",
"use_filter_panel_to_change_filter": "Use the filter panel to change the active filter",
"year": "year",
"download": "Download",
"read": "Read"
"year": "year"
},
"dashboard": {
"keep_reading": "Keep Reading",
@ -423,7 +423,8 @@
"search_results_for": "Search results for \"{name}\""
},
"searchbox": {
"no_results": "No results"
"no_results": "No results",
"search_all": "Search all…"
},
"server": {
"server_management": {