1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2024-12-04 10:34:45 +02:00

improving autocomplete #237

This commit is contained in:
Patrik J. Braun 2021-03-21 09:46:25 +01:00
parent 042b744a48
commit 24942b2ee1
5 changed files with 121 additions and 41 deletions

View File

@ -2,12 +2,12 @@ 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, IAutoCompleteItem} from '../../../../common/entities/AutoCompleteItem';
import {IAutoCompleteItem} from '../../../../common/entities/AutoCompleteItem';
import {SearchResultDTO} from '../../../../common/entities/SearchResultDTO';
import {MediaDTO} from '../../../../common/entities/MediaDTO';
import {SortingMethods} from '../../../../common/entities/SortingMethods';
import {VersionService} from '../../model/version.service';
import {SearchQueryDTO} from '../../../../common/entities/SearchQueryDTO';
import {SearchQueryDTO, SearchQueryTypes} from '../../../../common/entities/SearchQueryDTO';
interface CacheItem<T> {
timestamp: number;
@ -101,11 +101,11 @@ export class GalleryCacheService {
return null;
}
public getAutoComplete(text: string): IAutoCompleteItem[] {
public getAutoComplete(text: string, type: SearchQueryTypes): IAutoCompleteItem[] {
if (Config.Client.Other.enableCache === false) {
return null;
}
const key = GalleryCacheService.AUTO_COMPLETE_PREFIX + text;
const key = GalleryCacheService.AUTO_COMPLETE_PREFIX + text + (type ? '_' + type : '');
const tmp = localStorage.getItem(key);
if (tmp != null) {
const value: CacheItem<IAutoCompleteItem[]> = JSON.parse(tmp);
@ -118,16 +118,17 @@ export class GalleryCacheService {
return null;
}
public setAutoComplete(text: string, items: Array<IAutoCompleteItem>): void {
public setAutoComplete(text: string, type: SearchQueryTypes, items: Array<IAutoCompleteItem>): void {
if (Config.Client.Other.enableCache === false) {
return;
}
const key = GalleryCacheService.AUTO_COMPLETE_PREFIX + text + (type ? '_' + type : '');
const tmp: CacheItem<Array<IAutoCompleteItem>> = {
timestamp: Date.now(),
item: items
};
try {
localStorage.setItem(GalleryCacheService.AUTO_COMPLETE_PREFIX + text, JSON.stringify(tmp));
localStorage.setItem(key, JSON.stringify(tmp));
} catch (e) {
this.reset();
console.error(e);

View File

@ -5,11 +5,14 @@ import {GalleryCacheService} from '../cache.gallery.service';
import {SearchQueryParserService} from './search-query-parser.service';
import {BehaviorSubject} from 'rxjs';
import {SearchQueryTypes, TextSearchQueryTypes} from '../../../../../common/entities/SearchQueryDTO';
import {QueryParams} from '../../../../../common/QueryParams';
@Injectable()
export class AutoCompleteService {
private keywords: string[] = [];
private relationKeywords: string[] = [];
private textSearchKeywordsMap: { [key: string]: SearchQueryTypes } = {};
constructor(private _networkService: NetworkService,
private _searchQueryParserService: SearchQueryParserService,
@ -27,15 +30,37 @@ export class AutoCompleteService {
for (let i = 0; i < 10; i++) {
this.keywords.push(i + this._searchQueryParserService.keywords.NSomeOf);
}
TextSearchQueryTypes.forEach(t => {
this.textSearchKeywordsMap[(<any>this._searchQueryParserService.keywords)[SearchQueryTypes[t]]] = t;
});
}
public autoComplete(text: string): BehaviorSubject<RenderableAutoCompleteItem[]> {
public autoComplete(text: { current: string, prev: string }): BehaviorSubject<RenderableAutoCompleteItem[]> {
const items: BehaviorSubject<RenderableAutoCompleteItem[]> = new BehaviorSubject(
this.sortResults(text, this.getQueryKeywords(text)));
const cached = this._galleryCacheService.getAutoComplete(text);
this.sortResults(text.current, this.getQueryKeywords(text)));
const type = this.getTypeFromPrefix(text.current);
const searchText = this.getPrefixLessSearchText(text.current);
if (searchText === '') {
return items;
}
this.typedAutoComplete(searchText, type, items);
return items;
}
public typedAutoComplete(text: string, type: SearchQueryTypes,
items?: BehaviorSubject<RenderableAutoCompleteItem[]>): BehaviorSubject<RenderableAutoCompleteItem[]> {
items = items || new BehaviorSubject([]);
const cached = this._galleryCacheService.getAutoComplete(text, type);
if (cached == null) {
this._networkService.getJson<IAutoCompleteItem[]>('/autocomplete/' + text).then(ret => {
this._galleryCacheService.setAutoComplete(text, ret);
const acParams: any = {};
if (type) {
acParams[QueryParams.gallery.search.type] = type;
}
this._networkService.getJson<IAutoCompleteItem[]>('/autocomplete/' + text, acParams).then(ret => {
this._galleryCacheService.setAutoComplete(text, type, ret);
items.next(this.sortResults(text, ret.map(i => this.ACItemToRenderable(i)).concat(items.value)));
});
} else {
@ -44,6 +69,22 @@ export class AutoCompleteService {
return items;
}
private getTypeFromPrefix(text: string): SearchQueryTypes {
const tokens = text.split(':');
if (tokens.length !== 2) {
return null;
}
return this.textSearchKeywordsMap[tokens[0]] || null;
}
private getPrefixLessSearchText(text: string): string {
const tokens = text.split(':');
if (tokens.length !== 2) {
return text;
}
return tokens[1];
}
private ACItemToRenderable(item: IAutoCompleteItem): RenderableAutoCompleteItem {
if (!item.type) {
return {text: item.text, queryHint: item.text};
@ -52,7 +93,7 @@ export class AutoCompleteService {
return {
text: item.text, type: item.type,
queryHint:
(<any>this._searchQueryParserService.keywords)[SearchQueryTypes[item.type]] + ':(' + item.text + ')'
(<any>this._searchQueryParserService.keywords)[SearchQueryTypes[item.type]] + ':"' + item.text + '"'
};
}
return {
@ -73,9 +114,20 @@ export class AutoCompleteService {
}
private getQueryKeywords(text: string): RenderableAutoCompleteItem[] {
private getQueryKeywords(text: { current: string, prev: string }): RenderableAutoCompleteItem[] {
// if empty, recommend "and"
if (text.current === '') {
if (text.prev !== this._searchQueryParserService.keywords.and) {
return [{
text: this._searchQueryParserService.keywords.and,
queryHint: this._searchQueryParserService.keywords.and
}];
} else {
return [];
}
}
return this.keywords
.filter(key => key.startsWith(text))
.filter(key => key.startsWith(text.current))
.map(key => ({
text: key,
queryHint: key

View File

@ -20,7 +20,7 @@
font-size: 17px;
}
.autocomplete-item a {
.autocomplete-item {
color: #333;
padding: 0 20px;
line-height: 1.42857143;
@ -28,14 +28,11 @@
display: block;
}
.autocomplete-item:hover {
.autocomplete-item-selected {
background-color: #007bff;
color: #FFF;
}
.autocomplete-item:hover a {
color: #FFF;
text-decoration: none;
}
.search-text {
border: 0;

View File

@ -9,8 +9,10 @@
(focus)="onFocus()"
[(ngModel)]="rawSearchText"
(ngModelChange)="validateRawSearchText()"
(keydown.enter)="Search()"
(keydown.enter)="OnEnter($event)"
(keydown.arrowRight)="applyHint($event)"
(keydown.arrowUp)="selectAutocompleteUp()"
(keydown.arrowDown)="selectAutocompleteDown()"
#name="ngModel"
size="30"
ngControl="search"
@ -32,7 +34,11 @@
<div class="autocomplete-list" *ngIf="autoCompleteRenders.length > 0"
(mouseover)="setMouseOverAutoComplete(true)" (mouseout)="setMouseOverAutoComplete(false)">
<div class="autocomplete-item" (click)="applyAutoComplete(item)" *ngFor="let item of autoCompleteRenders">
<div class="autocomplete-item"
[ngClass]="{'autocomplete-item-selected':highlightedAutoCompleteItem == i}"
(mouseover)="setMouseOverAutoCompleteItem(i)"
(click)="applyAutoComplete(item)"
*ngFor="let item of autoCompleteRenders; let i = index">
<div>
<span [ngSwitch]="item.type">
<span *ngSwitchCase="SearchQueryTypes.caption" class="oi oi-image"></span>

View File

@ -26,6 +26,7 @@ export class GallerySearchComponent implements OnDestroy {
readonly SearchQueryTypes: typeof SearchQueryTypes;
modalRef: BsModalRef;
public readonly MetadataSearchQueryTypes: { value: string; key: SearchQueryTypes }[];
public highlightedAutoCompleteItem = 0;
private cache = {
lastAutocomplete: '',
lastInstantSearch: ''
@ -62,11 +63,11 @@ export class GallerySearchComponent implements OnDestroy {
return this.rawSearchText;
}
const searchText = this.getAutocompleteToken();
if (searchText === '') {
return this.rawSearchText;
if (searchText.current === '') {
return this.rawSearchText + this.autoCompleteItems.value[this.highlightedAutoCompleteItem].queryHint;
}
if (this.autoCompleteItems.value[0].queryHint.startsWith(searchText)) {
return this.rawSearchText + this.autoCompleteItems.value[0].queryHint.substr(searchText.length);
if (this.autoCompleteItems.value[0].queryHint.startsWith(searchText.current)) {
return this.rawSearchText + this.autoCompleteItems.value[this.highlightedAutoCompleteItem].queryHint.substr(searchText.current.length);
}
return this.rawSearchText;
}
@ -82,20 +83,23 @@ export class GallerySearchComponent implements OnDestroy {
}
}
getAutocompleteToken(): string {
getAutocompleteToken(): { current: string, prev: string } {
if (this.rawSearchText.trim().length === 0) {
return '';
return {current: '', prev: ''};
}
const tokens = this.rawSearchText.split(' ');
return tokens[tokens.length - 1];
return {
current: tokens[tokens.length - 1],
prev: (tokens.length > 2 ? tokens[tokens.length - 2] : '')
};
}
onSearchChange(event: KeyboardEvent) {
const searchText = this.getAutocompleteToken();
if (Config.Client.Search.AutoComplete.enabled &&
this.cache.lastAutocomplete !== searchText) {
this.cache.lastAutocomplete = searchText;
this.cache.lastAutocomplete !== searchText.current) {
this.cache.lastAutocomplete = searchText.current;
this.autocomplete(searchText).catch(console.error);
}
@ -157,35 +161,55 @@ export class GallerySearchComponent implements OnDestroy {
applyAutoComplete(item: AutoCompleteRenderItem) {
const token = this.getAutocompleteToken();
this.rawSearchText = this.rawSearchText.substr(0, this.rawSearchText.length - token.length)
this.rawSearchText = this.rawSearchText.substr(0, this.rawSearchText.length - token.current.length)
+ item.queryHint;
this.emptyAutoComplete();
this.validateRawSearchText();
}
setMouseOverAutoCompleteItem(i: number) {
this.highlightedAutoCompleteItem = i;
}
selectAutocompleteUp() {
if (this.highlightedAutoCompleteItem > 0) {
this.highlightedAutoCompleteItem--;
}
}
selectAutocompleteDown() {
if (this.autoCompleteItems &&
this.highlightedAutoCompleteItem < this.autoCompleteItems.value.length - 1) {
this.highlightedAutoCompleteItem++;
}
}
OnEnter($event: any) {
if (this.autoCompleteRenders.length === 0) {
this.Search();
return;
}
this.applyAutoComplete(this.autoCompleteRenders[this.highlightedAutoCompleteItem]);
}
private emptyAutoComplete() {
this.highlightedAutoCompleteItem = 0;
this.autoCompleteRenders = [];
}
private async autocomplete(searchText: string) {
private async autocomplete(searchText: { current: string, prev: string }) {
if (!Config.Client.Search.AutoComplete.enabled) {
return;
}
if (searchText.trim().length === 0 ||
searchText.trim() === '.') {
return;
}
if (searchText.trim().length > 0) {
if (this.rawSearchText.trim().length > 0) { // are we searching for anything?
try {
if (this.autoCompleteItems) {
this.autoCompleteItems.unsubscribe();
}
this.autoCompleteItems = this._autoCompleteService.autoComplete(searchText);
this.autoCompleteItems.subscribe(() => {
this.showSuggestions(this.autoCompleteItems.value, searchText);
this.showSuggestions(this.autoCompleteItems.value, searchText.current);
});
} catch (error) {
console.error(error);