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:
parent
042b744a48
commit
24942b2ee1
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user