diff --git a/src/common/entities/AutoCompleteItem.ts b/src/common/entities/AutoCompleteItem.ts index 29bf9995..046c220b 100644 --- a/src/common/entities/AutoCompleteItem.ts +++ b/src/common/entities/AutoCompleteItem.ts @@ -1,6 +1,11 @@ import {SearchQueryTypes} from './SearchQueryDTO'; -export class AutoCompleteItem { +export interface IAutoCompleteItem { + text: string; + type?: SearchQueryTypes; +} + +export class AutoCompleteItem implements IAutoCompleteItem { constructor(public text: string, public type: SearchQueryTypes = null) { } diff --git a/src/frontend/app/ui/gallery/cache.gallery.service.ts b/src/frontend/app/ui/gallery/cache.gallery.service.ts index 1d80228f..ef73a5d1 100644 --- a/src/frontend/app/ui/gallery/cache.gallery.service.ts +++ b/src/frontend/app/ui/gallery/cache.gallery.service.ts @@ -2,7 +2,7 @@ import {Injectable} from '@angular/core'; import {DirectoryDTO} from '../../../../common/entities/DirectoryDTO'; import {Utils} from '../../../../common/Utils'; import {Config} from '../../../../common/config/public/Config'; -import {AutoCompleteItem} from '../../../../common/entities/AutoCompleteItem'; +import {AutoCompleteItem, IAutoCompleteItem} from '../../../../common/entities/AutoCompleteItem'; import {SearchResultDTO} from '../../../../common/entities/SearchResultDTO'; import {MediaDTO} from '../../../../common/entities/MediaDTO'; import {SortingMethods} from '../../../../common/entities/SortingMethods'; @@ -101,14 +101,14 @@ export class GalleryCacheService { return null; } - public getAutoComplete(text: string): AutoCompleteItem[] { + public getAutoComplete(text: string): IAutoCompleteItem[] { if (Config.Client.Other.enableCache === false) { return null; } const key = GalleryCacheService.AUTO_COMPLETE_PREFIX + text; const tmp = localStorage.getItem(key); if (tmp != null) { - const value: CacheItem = JSON.parse(tmp); + const value: CacheItem = JSON.parse(tmp); if (value.timestamp < Date.now() - Config.Client.Search.AutoComplete.cacheTimeout) { localStorage.removeItem(key); return null; @@ -118,11 +118,11 @@ export class GalleryCacheService { return null; } - public setAutoComplete(text: string, items: Array): void { + public setAutoComplete(text: string, items: Array): void { if (Config.Client.Other.enableCache === false) { return; } - const tmp: CacheItem> = { + const tmp: CacheItem> = { timestamp: Date.now(), item: items }; diff --git a/src/frontend/app/ui/gallery/search/autocomplete.service.ts b/src/frontend/app/ui/gallery/search/autocomplete.service.ts index 9aa45213..9f9021ff 100644 --- a/src/frontend/app/ui/gallery/search/autocomplete.service.ts +++ b/src/frontend/app/ui/gallery/search/autocomplete.service.ts @@ -1,33 +1,66 @@ import {Injectable} from '@angular/core'; import {NetworkService} from '../../../model/network/network.service'; -import {AutoCompleteItem} from '../../../../../common/entities/AutoCompleteItem'; +import {IAutoCompleteItem} from '../../../../../common/entities/AutoCompleteItem'; import {GalleryCacheService} from '../cache.gallery.service'; import {SearchQueryParserService} from './search-query-parser.service'; import {BehaviorSubject} from 'rxjs'; +import {SearchQueryTypes, TextSearchQueryTypes} from '../../../../../common/entities/SearchQueryDTO'; @Injectable() export class AutoCompleteService { + private keywords: string[] = []; constructor(private _networkService: NetworkService, private _searchQueryParserService: SearchQueryParserService, private _galleryCacheService: GalleryCacheService) { + this.keywords = Object.values(this._searchQueryParserService.keywords) + .filter(k => k !== this._searchQueryParserService.keywords.or && + k !== this._searchQueryParserService.keywords.and && + k !== this._searchQueryParserService.keywords.portrait && + k !== this._searchQueryParserService.keywords.kmFrom && + k !== this._searchQueryParserService.keywords.NSomeOf) + .map(k => k + ':'); + + this.keywords.push(this._searchQueryParserService.keywords.and); + this.keywords.push(this._searchQueryParserService.keywords.or); + for (let i = 0; i < 10; i++) { + this.keywords.push(i + this._searchQueryParserService.keywords.NSomeOf); + } } - public autoComplete(text: string): BehaviorSubject { - const items: BehaviorSubject = new BehaviorSubject( + public autoComplete(text: string): BehaviorSubject { + const items: BehaviorSubject = new BehaviorSubject( this.sortResults(text, this.getQueryKeywords(text))); const cached = this._galleryCacheService.getAutoComplete(text); if (cached == null) { - this._networkService.getJson('/autocomplete/' + text).then(ret => { + this._networkService.getJson('/autocomplete/' + text).then(ret => { this._galleryCacheService.setAutoComplete(text, ret); - items.next(this.sortResults(text, ret.concat(items.value))); + items.next(this.sortResults(text, ret.map(i => this.ACItemToRenderable(i)).concat(items.value))); }); + } else { + items.next(this.sortResults(text, cached.map(i => this.ACItemToRenderable(i)).concat(items.value))); } return items; } - private sortResults(text: string, items: AutoCompleteItem[]) { + private ACItemToRenderable(item: IAutoCompleteItem): RenderableAutoCompleteItem { + if (!item.type) { + return {text: item.text, queryHint: item.text}; + } + if (TextSearchQueryTypes.includes(item.type) && item.type !== SearchQueryTypes.any_text) { + return { + text: item.text, type: item.type, + queryHint: + (this._searchQueryParserService.keywords)[SearchQueryTypes[item.type]] + ':(' + item.text + ')' + }; + } + return { + text: item.text, type: item.type, queryHint: item.text + }; + } + + private sortResults(text: string, items: RenderableAutoCompleteItem[]) { return items.sort((a, b) => { if ((a.text.startsWith(text) && b.text.startsWith(text)) || (!a.text.startsWith(text) && !b.text.startsWith(text))) { @@ -40,9 +73,16 @@ export class AutoCompleteService { } - private getQueryKeywords(text: string) { - return Object.values(this._searchQueryParserService.keywords) + private getQueryKeywords(text: string): RenderableAutoCompleteItem[] { + return this.keywords .filter(key => key.startsWith(text)) - .map(key => new AutoCompleteItem(key + ':')); + .map(key => ({ + text: key, + queryHint: key + })); } } + +export interface RenderableAutoCompleteItem extends IAutoCompleteItem { + queryHint: string; +} diff --git a/src/frontend/app/ui/gallery/search/search.gallery.component.css b/src/frontend/app/ui/gallery/search/search.gallery.component.css index bf67691c..fd9966f7 100644 --- a/src/frontend/app/ui/gallery/search/search.gallery.component.css +++ b/src/frontend/app/ui/gallery/search/search.gallery.component.css @@ -10,6 +10,7 @@ padding: 5px 0; -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175); box-shadow: 0 6px 12px rgba(0, 0, 0, .175); + z-index: 7; } .autocomplete-item { diff --git a/src/frontend/app/ui/gallery/search/search.gallery.component.html b/src/frontend/app/ui/gallery/search/search.gallery.component.html index 2b2c2008..ec56bb38 100644 --- a/src/frontend/app/ui/gallery/search/search.gallery.component.html +++ b/src/frontend/app/ui/gallery/search/search.gallery.component.html @@ -30,9 +30,9 @@ autocomplete="off"> -
-
+
diff --git a/src/frontend/app/ui/gallery/search/search.gallery.component.ts b/src/frontend/app/ui/gallery/search/search.gallery.component.ts index 91681d1d..a2992fd5 100644 --- a/src/frontend/app/ui/gallery/search/search.gallery.component.ts +++ b/src/frontend/app/ui/gallery/search/search.gallery.component.ts @@ -1,6 +1,5 @@ import {Component, OnDestroy, TemplateRef} from '@angular/core'; -import {AutoCompleteService} from './autocomplete.service'; -import {AutoCompleteItem} from '../../../../../common/entities/AutoCompleteItem'; +import {AutoCompleteService, RenderableAutoCompleteItem} from './autocomplete.service'; import {ActivatedRoute, Params, Router, RouterLink} from '@angular/router'; import {GalleryService} from '../gallery.service'; import {BehaviorSubject, Subscription} from 'rxjs'; @@ -20,9 +19,9 @@ import {SearchQueryParserService} from './search-query-parser.service'; }) export class GallerySearchComponent implements OnDestroy { - autoCompleteItems: AutoCompleteRenderItem[] = []; + autoCompleteRenders: AutoCompleteRenderItem[] = []; public searchQueryDTO: SearchQueryDTO = {type: SearchQueryTypes.any_text, text: ''}; - public rawSearchText: string; + public rawSearchText = ''; mouseOverAutoComplete = false; readonly SearchQueryTypes: typeof SearchQueryTypes; modalRef: BsModalRef; @@ -32,7 +31,7 @@ export class GallerySearchComponent implements OnDestroy { lastInstantSearch: '' }; private readonly subscription: Subscription = null; - private autocompleteItems: BehaviorSubject; + private autoCompleteItems: BehaviorSubject; constructor(private _autoCompleteService: AutoCompleteService, private _searchQueryParserService: SearchQueryParserService, @@ -58,13 +57,16 @@ export class GallerySearchComponent implements OnDestroy { } get SearchHint() { - if (!this.autocompleteItems || - !this.autocompleteItems.value || this.autocompleteItems.value.length === 0) { + if (!this.autoCompleteItems || + !this.autoCompleteItems.value || this.autoCompleteItems.value.length === 0) { return this.rawSearchText; } const searchText = this.getAutocompleteToken(); - if (this.autocompleteItems.value[0].text.startsWith(searchText)) { - return this.rawSearchText + this.autocompleteItems.value[0].text.substr(searchText.length); + if (searchText === '') { + return this.rawSearchText; + } + if (this.autoCompleteItems.value[0].queryHint.startsWith(searchText)) { + return this.rawSearchText + this.autoCompleteItems.value[0].queryHint.substr(searchText.length); } return this.rawSearchText; } @@ -105,7 +107,7 @@ export class GallerySearchComponent implements OnDestroy { public onFocusLost() { if (this.mouseOverAutoComplete === false) { - this.autoCompleteItems = []; + this.autoCompleteRenders = []; } } @@ -130,6 +132,7 @@ export class GallerySearchComponent implements OnDestroy { onQueryChange() { this.rawSearchText = this._searchQueryParserService.stringify(this.searchQueryDTO); + this.validateRawSearchText(); } validateRawSearchText() { @@ -149,10 +152,19 @@ export class GallerySearchComponent implements OnDestroy { return; } this.rawSearchText = this.SearchHint; + this.validateRawSearchText(); + } + + applyAutoComplete(item: AutoCompleteRenderItem) { + const token = this.getAutocompleteToken(); + this.rawSearchText = this.rawSearchText.substr(0, this.rawSearchText.length - token.length) + + item.queryHint; + this.emptyAutoComplete(); + this.validateRawSearchText(); } private emptyAutoComplete() { - this.autoCompleteItems = []; + this.autoCompleteRenders = []; } private async autocomplete(searchText: string) { @@ -168,7 +180,13 @@ export class GallerySearchComponent implements OnDestroy { if (searchText.trim().length > 0) { try { - this.autocompleteItems = this._autoCompleteService.autoComplete(searchText); + if (this.autoCompleteItems) { + this.autoCompleteItems.unsubscribe(); + } + this.autoCompleteItems = this._autoCompleteService.autoComplete(searchText); + this.autoCompleteItems.subscribe(() => { + this.showSuggestions(this.autoCompleteItems.value, searchText); + }); } catch (error) { console.error(error); } @@ -178,11 +196,11 @@ export class GallerySearchComponent implements OnDestroy { } } - private showSuggestions(suggestions: AutoCompleteItem[], searchText: string) { + private showSuggestions(suggestions: RenderableAutoCompleteItem[], searchText: string) { this.emptyAutoComplete(); - suggestions.forEach((item: AutoCompleteItem) => { - const renderItem = new AutoCompleteRenderItem(item.text, searchText, item.type); - this.autoCompleteItems.push(renderItem); + suggestions.forEach((item: RenderableAutoCompleteItem) => { + const renderItem = new AutoCompleteRenderItem(item.text, searchText, item.type, item.queryHint); + this.autoCompleteRenders.push(renderItem); }); } } @@ -192,8 +210,9 @@ class AutoCompleteRenderItem { public highLightText = ''; public postText = ''; public type: SearchQueryTypes; + public queryHint: string; - constructor(public text: string, searchText: string, type: SearchQueryTypes) { + constructor(public text: string, searchText: string, type: SearchQueryTypes, queryHint: string) { const preIndex = text.toLowerCase().indexOf(searchText.toLowerCase()); if (preIndex > -1) { this.preText = text.substring(0, preIndex); @@ -203,6 +222,7 @@ class AutoCompleteRenderItem { this.postText = text; } this.type = type; + this.queryHint = queryHint; } }