mirror of
https://github.com/xuthus83/pigallery2.git
synced 2024-11-03 21:04:03 +08:00
adding minimal autocomplete
This commit is contained in:
parent
9f9dbe0c51
commit
8afd49c588
@ -1,7 +1,7 @@
|
|||||||
import {SearchQueryTypes} from './SearchQueryDTO';
|
import {SearchQueryTypes} from './SearchQueryDTO';
|
||||||
|
|
||||||
export class AutoCompleteItem {
|
export class AutoCompleteItem {
|
||||||
constructor(public text: string, public type: SearchQueryTypes) {
|
constructor(public text: string, public type: SearchQueryTypes = null) {
|
||||||
}
|
}
|
||||||
|
|
||||||
equals(other: AutoCompleteItem) {
|
equals(other: AutoCompleteItem) {
|
||||||
|
@ -95,6 +95,8 @@ import {CSRFInterceptor} from './model/network/helper/csrf.interceptor';
|
|||||||
import {SettingsEntryComponent} from './ui/settings/_abstract/settings-entry/settings-entry.component';
|
import {SettingsEntryComponent} from './ui/settings/_abstract/settings-entry/settings-entry.component';
|
||||||
import {GallerySearchQueryEntryComponent} from './ui/gallery/search/query-enrty/query-entry.search.gallery.component';
|
import {GallerySearchQueryEntryComponent} from './ui/gallery/search/query-enrty/query-entry.search.gallery.component';
|
||||||
import {StringifySearchQuery} from './pipes/StringifySearchQuery';
|
import {StringifySearchQuery} from './pipes/StringifySearchQuery';
|
||||||
|
import {AutoCompleteService} from './ui/gallery/search/autocomplete.service';
|
||||||
|
import {SearchQueryParserService} from './ui/gallery/search/search-query-parser.service';
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -231,6 +233,8 @@ export function translationsFactory(locale: string) {
|
|||||||
GalleryCacheService,
|
GalleryCacheService,
|
||||||
GalleryService,
|
GalleryService,
|
||||||
MapService,
|
MapService,
|
||||||
|
SearchQueryParserService,
|
||||||
|
AutoCompleteService,
|
||||||
AuthenticationService,
|
AuthenticationService,
|
||||||
ThumbnailLoaderService,
|
ThumbnailLoaderService,
|
||||||
ThumbnailManagerService,
|
ThumbnailManagerService,
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
import {Pipe, PipeTransform} from '@angular/core';
|
import {Pipe, PipeTransform} from '@angular/core';
|
||||||
import {SearchQueryDTO} from '../../../common/entities/SearchQueryDTO';
|
import {SearchQueryDTO} from '../../../common/entities/SearchQueryDTO';
|
||||||
|
import {SearchQueryParserService} from '../ui/gallery/search/search-query-parser.service';
|
||||||
|
|
||||||
|
|
||||||
@Pipe({name: 'searchQuery'})
|
@Pipe({name: 'searchQuery'})
|
||||||
export class StringifySearchQuery implements PipeTransform {
|
export class StringifySearchQuery implements PipeTransform {
|
||||||
|
constructor(
|
||||||
|
private _searchQueryParserService: SearchQueryParserService) {
|
||||||
|
}
|
||||||
|
|
||||||
transform(query: SearchQueryDTO): string {
|
transform(query: SearchQueryDTO): string {
|
||||||
return SearchQueryDTO.stringify(query);
|
return this._searchQueryParserService.stringify(query);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ import {Subscription} from 'rxjs';
|
|||||||
import {SearchQueryDTO, SearchQueryTypes, TextSearch} from '../../../../../common/entities/SearchQueryDTO';
|
import {SearchQueryDTO, SearchQueryTypes, TextSearch} from '../../../../../common/entities/SearchQueryDTO';
|
||||||
import {ActivatedRoute, Params} from '@angular/router';
|
import {ActivatedRoute, Params} from '@angular/router';
|
||||||
import {QueryParams} from '../../../../../common/QueryParams';
|
import {QueryParams} from '../../../../../common/QueryParams';
|
||||||
|
import {SearchQueryParserService} from '../search/search-query-parser.service';
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -34,6 +35,7 @@ export class RandomQueryBuilderGalleryComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
constructor(public _galleryService: GalleryService,
|
constructor(public _galleryService: GalleryService,
|
||||||
private _notification: NotificationService,
|
private _notification: NotificationService,
|
||||||
|
private _searchQueryParserService: SearchQueryParserService,
|
||||||
public i18n: I18n,
|
public i18n: I18n,
|
||||||
private _route: ActivatedRoute,
|
private _route: ActivatedRoute,
|
||||||
private modalService: BsModalService) {
|
private modalService: BsModalService) {
|
||||||
@ -57,7 +59,7 @@ export class RandomQueryBuilderGalleryComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
validateRawSearchText() {
|
validateRawSearchText() {
|
||||||
try {
|
try {
|
||||||
this.searchQueryDTO = SearchQueryDTO.parse(this.rawSearchText);
|
this.searchQueryDTO = this._searchQueryParserService.parse(this.rawSearchText);
|
||||||
this.url = NetworkService.buildUrl(Config.Client.publicUrl + '/api/gallery/random/' + this.HTMLSearchQuery);
|
this.url = NetworkService.buildUrl(Config.Client.publicUrl + '/api/gallery/random/' + this.HTMLSearchQuery);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@ -65,7 +67,7 @@ export class RandomQueryBuilderGalleryComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onQueryChange() {
|
onQueryChange() {
|
||||||
this.rawSearchText = SearchQueryDTO.stringify(this.searchQueryDTO);
|
this.rawSearchText = this._searchQueryParserService.stringify(this.searchQueryDTO);
|
||||||
this.url = NetworkService.buildUrl(Config.Client.publicUrl + '/api/gallery/random/' + this.HTMLSearchQuery);
|
this.url = NetworkService.buildUrl(Config.Client.publicUrl + '/api/gallery/random/' + this.HTMLSearchQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,23 +2,47 @@ import {Injectable} from '@angular/core';
|
|||||||
import {NetworkService} from '../../../model/network/network.service';
|
import {NetworkService} from '../../../model/network/network.service';
|
||||||
import {AutoCompleteItem} from '../../../../../common/entities/AutoCompleteItem';
|
import {AutoCompleteItem} from '../../../../../common/entities/AutoCompleteItem';
|
||||||
import {GalleryCacheService} from '../cache.gallery.service';
|
import {GalleryCacheService} from '../cache.gallery.service';
|
||||||
|
import {SearchQueryParserService} from './search-query-parser.service';
|
||||||
|
import {BehaviorSubject} from 'rxjs';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AutoCompleteService {
|
export class AutoCompleteService {
|
||||||
|
|
||||||
|
|
||||||
constructor(private _networkService: NetworkService,
|
constructor(private _networkService: NetworkService,
|
||||||
private galleryCacheService: GalleryCacheService) {
|
private _searchQueryParserService: SearchQueryParserService,
|
||||||
|
private _galleryCacheService: GalleryCacheService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async autoComplete(text: string): Promise<Array<AutoCompleteItem>> {
|
public autoComplete(text: string): BehaviorSubject<AutoCompleteItem[]> {
|
||||||
let items: Array<AutoCompleteItem> = this.galleryCacheService.getAutoComplete(text);
|
const items: BehaviorSubject<AutoCompleteItem[]> = new BehaviorSubject(
|
||||||
if (items == null) {
|
this.sortResults(text, this.getQueryKeywords(text)));
|
||||||
items = await this._networkService.getJson<Array<AutoCompleteItem>>('/autocomplete/' + text);
|
const cached = this._galleryCacheService.getAutoComplete(text);
|
||||||
this.galleryCacheService.setAutoComplete(text, items);
|
if (cached == null) {
|
||||||
|
this._networkService.getJson<AutoCompleteItem[]>('/autocomplete/' + text).then(ret => {
|
||||||
|
this._galleryCacheService.setAutoComplete(text, ret);
|
||||||
|
items.next(this.sortResults(text, ret.concat(items.value)));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sortResults(text: string, items: AutoCompleteItem[]) {
|
||||||
|
return items.sort((a, b) => {
|
||||||
|
if ((a.text.startsWith(text) && b.text.startsWith(text)) ||
|
||||||
|
(!a.text.startsWith(text) && !b.text.startsWith(text))) {
|
||||||
|
return a.text.localeCompare(b.text);
|
||||||
|
} else if (a.text.startsWith(text)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private getQueryKeywords(text: string) {
|
||||||
|
return Object.values(this._searchQueryParserService.keywords)
|
||||||
|
.filter(key => key.startsWith(text))
|
||||||
|
.map(key => new AutoCompleteItem(key + ':'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,46 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {QueryKeywords, SearchQueryParser} from '../../../../../common/SearchQueryParser';
|
||||||
|
import {SearchQueryDTO} from '../../../../../common/entities/SearchQueryDTO';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SearchQueryParserService {
|
||||||
|
|
||||||
|
public readonly keywords: QueryKeywords = {
|
||||||
|
NSomeOf: '-of',
|
||||||
|
and: 'and',
|
||||||
|
caption: 'caption',
|
||||||
|
directory: 'directory',
|
||||||
|
file_name: 'file-name',
|
||||||
|
from: 'from',
|
||||||
|
keyword: 'keyword',
|
||||||
|
landscape: 'landscape',
|
||||||
|
maxRating: 'max-rating',
|
||||||
|
maxResolution: 'max-resolution',
|
||||||
|
minRating: 'min-rating',
|
||||||
|
minResolution: 'min-resolution',
|
||||||
|
or: 'or',
|
||||||
|
orientation: 'orientation',
|
||||||
|
person: 'person',
|
||||||
|
portrait: 'portrait',
|
||||||
|
position: 'position',
|
||||||
|
someOf: 'some-of',
|
||||||
|
to: 'to',
|
||||||
|
kmFrom: 'km-from'
|
||||||
|
};
|
||||||
|
private readonly parser: SearchQueryParser;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.parser = new SearchQueryParser(this.keywords);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public parse(str: string): SearchQueryDTO {
|
||||||
|
return this.parser.parse(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
stringify(query: SearchQueryDTO): string {
|
||||||
|
return this.parser.stringify(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -36,9 +36,19 @@
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#srch-term {
|
.search-text {
|
||||||
border-bottom-left-radius: 0;
|
border: 0;
|
||||||
border-bottom-width: 0;
|
z-index: 6;
|
||||||
|
width: 500px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-hint {
|
||||||
|
width: 500px;
|
||||||
|
background: white;
|
||||||
|
z-index: 1;
|
||||||
|
position: absolute;
|
||||||
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
form {
|
form {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<form class="navbar-form" role="search" #SearchForm="ngForm">
|
<form class="navbar-form" role="search" #SearchForm="ngForm">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text"
|
<input type="text"
|
||||||
class="form-control "
|
class="form-control search-text"
|
||||||
i18n-placeholder
|
i18n-placeholder
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
(keyup)="onSearchChange($event)"
|
(keyup)="onSearchChange($event)"
|
||||||
@ -10,12 +10,24 @@
|
|||||||
[(ngModel)]="rawSearchText"
|
[(ngModel)]="rawSearchText"
|
||||||
(ngModelChange)="validateRawSearchText()"
|
(ngModelChange)="validateRawSearchText()"
|
||||||
(keydown.enter)="Search()"
|
(keydown.enter)="Search()"
|
||||||
|
(keydown.arrowRight)="applyHint($event)"
|
||||||
#name="ngModel"
|
#name="ngModel"
|
||||||
size="30"
|
size="30"
|
||||||
ngControl="search"
|
ngControl="search"
|
||||||
name="srch-term"
|
name="srch-term"
|
||||||
id="srch-term"
|
id="srch-term"
|
||||||
autocomplete="off">
|
autocomplete="off">
|
||||||
|
<input type="text"
|
||||||
|
class="form-control search-hint"
|
||||||
|
[ngModel]="SearchHint"
|
||||||
|
i18n-placeholder
|
||||||
|
placeholder="Search"
|
||||||
|
disabled
|
||||||
|
value="test hint"
|
||||||
|
size="30"
|
||||||
|
name="srch-term-hint"
|
||||||
|
id="srch-term-hint"
|
||||||
|
autocomplete="off">
|
||||||
|
|
||||||
|
|
||||||
<div class="autocomplete-list" *ngIf="autoCompleteItems.length > 0"
|
<div class="autocomplete-list" *ngIf="autoCompleteItems.length > 0"
|
||||||
@ -62,7 +74,7 @@
|
|||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form #searchPanelForm="ngForm" class="form-horizontal">
|
<form #searchPanelForm="ngForm" class="form-horizontal">
|
||||||
<input type="text"
|
<input type="text"
|
||||||
class="form-control"
|
class="form-control search-text"
|
||||||
i18n-placeholder
|
i18n-placeholder
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
[(ngModel)]="rawSearchText"
|
[(ngModel)]="rawSearchText"
|
||||||
|
@ -3,13 +3,14 @@ import {AutoCompleteService} from './autocomplete.service';
|
|||||||
import {AutoCompleteItem} from '../../../../../common/entities/AutoCompleteItem';
|
import {AutoCompleteItem} from '../../../../../common/entities/AutoCompleteItem';
|
||||||
import {ActivatedRoute, Params, Router, RouterLink} from '@angular/router';
|
import {ActivatedRoute, Params, Router, RouterLink} from '@angular/router';
|
||||||
import {GalleryService} from '../gallery.service';
|
import {GalleryService} from '../gallery.service';
|
||||||
import {Subscription} from 'rxjs';
|
import {BehaviorSubject, Subscription} from 'rxjs';
|
||||||
import {Config} from '../../../../../common/config/public/Config';
|
import {Config} from '../../../../../common/config/public/Config';
|
||||||
import {NavigationService} from '../../../model/navigation.service';
|
import {NavigationService} from '../../../model/navigation.service';
|
||||||
import {QueryParams} from '../../../../../common/QueryParams';
|
import {QueryParams} from '../../../../../common/QueryParams';
|
||||||
import {MetadataSearchQueryTypes, SearchQueryDTO, SearchQueryTypes, TextSearch} from '../../../../../common/entities/SearchQueryDTO';
|
import {MetadataSearchQueryTypes, SearchQueryDTO, SearchQueryTypes, TextSearch} from '../../../../../common/entities/SearchQueryDTO';
|
||||||
import {BsModalService} from 'ngx-bootstrap/modal';
|
import {BsModalService} from 'ngx-bootstrap/modal';
|
||||||
import {BsModalRef} from 'ngx-bootstrap/modal/bs-modal-ref.service';
|
import {BsModalRef} from 'ngx-bootstrap/modal/bs-modal-ref.service';
|
||||||
|
import {SearchQueryParserService} from './search-query-parser.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-gallery-search',
|
selector: 'app-gallery-search',
|
||||||
@ -31,8 +32,10 @@ export class GallerySearchComponent implements OnDestroy {
|
|||||||
lastInstantSearch: ''
|
lastInstantSearch: ''
|
||||||
};
|
};
|
||||||
private readonly subscription: Subscription = null;
|
private readonly subscription: Subscription = null;
|
||||||
|
private autocompleteItems: BehaviorSubject<AutoCompleteItem[]>;
|
||||||
|
|
||||||
constructor(private _autoCompleteService: AutoCompleteService,
|
constructor(private _autoCompleteService: AutoCompleteService,
|
||||||
|
private _searchQueryParserService: SearchQueryParserService,
|
||||||
private _galleryService: GalleryService,
|
private _galleryService: GalleryService,
|
||||||
private navigationService: NavigationService,
|
private navigationService: NavigationService,
|
||||||
private _route: ActivatedRoute,
|
private _route: ActivatedRoute,
|
||||||
@ -54,6 +57,17 @@ export class GallerySearchComponent implements OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get SearchHint() {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
return this.rawSearchText;
|
||||||
|
}
|
||||||
|
|
||||||
get HTMLSearchQuery() {
|
get HTMLSearchQuery() {
|
||||||
return JSON.stringify(this.searchQueryDTO);
|
return JSON.stringify(this.searchQueryDTO);
|
||||||
@ -66,15 +80,21 @@ export class GallerySearchComponent implements OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAutocompleteToken(): string {
|
||||||
|
if (this.rawSearchText.trim().length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const tokens = this.rawSearchText.split(' ');
|
||||||
|
return tokens[tokens.length - 1];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
onSearchChange(event: KeyboardEvent) {
|
onSearchChange(event: KeyboardEvent) {
|
||||||
|
const searchText = this.getAutocompleteToken();
|
||||||
|
|
||||||
const searchText = (<HTMLInputElement>event.target).value.trim();
|
|
||||||
|
|
||||||
if (Config.Client.Search.AutoComplete.enabled &&
|
if (Config.Client.Search.AutoComplete.enabled &&
|
||||||
this.cache.lastAutocomplete !== searchText) {
|
this.cache.lastAutocomplete !== searchText) {
|
||||||
this.cache.lastAutocomplete = searchText;
|
this.cache.lastAutocomplete = searchText;
|
||||||
// this.autocomplete(searchText).catch(console.error);
|
this.autocomplete(searchText).catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -109,12 +129,12 @@ export class GallerySearchComponent implements OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onQueryChange() {
|
onQueryChange() {
|
||||||
this.rawSearchText = SearchQueryDTO.stringify(this.searchQueryDTO);
|
this.rawSearchText = this._searchQueryParserService.stringify(this.searchQueryDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
validateRawSearchText() {
|
validateRawSearchText() {
|
||||||
try {
|
try {
|
||||||
this.searchQueryDTO = SearchQueryDTO.parse(this.rawSearchText);
|
this.searchQueryDTO = this._searchQueryParserService.parse(this.rawSearchText);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
@ -124,6 +144,13 @@ export class GallerySearchComponent implements OnDestroy {
|
|||||||
this.router.navigate(['/search', this.HTMLSearchQuery]).catch(console.error);
|
this.router.navigate(['/search', this.HTMLSearchQuery]).catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyHint($event: any) {
|
||||||
|
if ($event.target.selectionStart !== this.rawSearchText.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.rawSearchText = this.SearchHint;
|
||||||
|
}
|
||||||
|
|
||||||
private emptyAutoComplete() {
|
private emptyAutoComplete() {
|
||||||
this.autoCompleteItems = [];
|
this.autoCompleteItems = [];
|
||||||
}
|
}
|
||||||
@ -132,15 +159,16 @@ export class GallerySearchComponent implements OnDestroy {
|
|||||||
if (!Config.Client.Search.AutoComplete.enabled) {
|
if (!Config.Client.Search.AutoComplete.enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (searchText.trim() === '.') {
|
|
||||||
|
if (searchText.trim().length === 0 ||
|
||||||
|
searchText.trim() === '.') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (searchText.trim().length > 0) {
|
if (searchText.trim().length > 0) {
|
||||||
try {
|
try {
|
||||||
const items = await this._autoCompleteService.autoComplete(searchText);
|
this.autocompleteItems = this._autoCompleteService.autoComplete(searchText);
|
||||||
this.showSuggestions(items, searchText);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user