1
0
mirror of https://github.com/xuthus83/pigallery2.git synced 2024-11-03 21:04:03 +08:00

Implementing basic autocomplete for advanced search #58 #237

This commit is contained in:
Patrik J. Braun 2021-03-20 20:15:49 +01:00
parent 8afd49c588
commit 6b24436c22
6 changed files with 100 additions and 34 deletions

View File

@ -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) {
}

View File

@ -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<AutoCompleteItem[]> = JSON.parse(tmp);
const value: CacheItem<IAutoCompleteItem[]> = 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<AutoCompleteItem>): void {
public setAutoComplete(text: string, items: Array<IAutoCompleteItem>): void {
if (Config.Client.Other.enableCache === false) {
return;
}
const tmp: CacheItem<Array<AutoCompleteItem>> = {
const tmp: CacheItem<Array<IAutoCompleteItem>> = {
timestamp: Date.now(),
item: items
};

View File

@ -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<AutoCompleteItem[]> {
const items: BehaviorSubject<AutoCompleteItem[]> = new BehaviorSubject(
public autoComplete(text: string): BehaviorSubject<RenderableAutoCompleteItem[]> {
const items: BehaviorSubject<RenderableAutoCompleteItem[]> = new BehaviorSubject(
this.sortResults(text, this.getQueryKeywords(text)));
const cached = this._galleryCacheService.getAutoComplete(text);
if (cached == null) {
this._networkService.getJson<AutoCompleteItem[]>('/autocomplete/' + text).then(ret => {
this._networkService.getJson<IAutoCompleteItem[]>('/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:
(<any>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;
}

View File

@ -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 {

View File

@ -30,9 +30,9 @@
autocomplete="off">
<div class="autocomplete-list" *ngIf="autoCompleteItems.length > 0"
<div class="autocomplete-list" *ngIf="autoCompleteRenders.length > 0"
(mouseover)="setMouseOverAutoComplete(true)" (mouseout)="setMouseOverAutoComplete(false)">
<div class="autocomplete-item" *ngFor="let item of autoCompleteItems">
<div class="autocomplete-item" (click)="applyAutoComplete(item)" *ngFor="let item of autoCompleteRenders">
<div>
<span [ngSwitch]="item.type">
<span *ngSwitchCase="SearchQueryTypes.caption" class="oi oi-image"></span>

View File

@ -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 = <TextSearch>{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<AutoCompleteItem[]>;
private autoCompleteItems: BehaviorSubject<RenderableAutoCompleteItem[]>;
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;
}
}