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

View file

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