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';
|
||||
|
||||
export class AutoCompleteItem {
|
||||
constructor(public text: string, public type: SearchQueryTypes) {
|
||||
constructor(public text: string, public type: SearchQueryTypes = null) {
|
||||
}
|
||||
|
||||
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 {GallerySearchQueryEntryComponent} from './ui/gallery/search/query-enrty/query-entry.search.gallery.component';
|
||||
import {StringifySearchQuery} from './pipes/StringifySearchQuery';
|
||||
import {AutoCompleteService} from './ui/gallery/search/autocomplete.service';
|
||||
import {SearchQueryParserService} from './ui/gallery/search/search-query-parser.service';
|
||||
|
||||
|
||||
@Injectable()
|
||||
@ -231,6 +233,8 @@ export function translationsFactory(locale: string) {
|
||||
GalleryCacheService,
|
||||
GalleryService,
|
||||
MapService,
|
||||
SearchQueryParserService,
|
||||
AutoCompleteService,
|
||||
AuthenticationService,
|
||||
ThumbnailLoaderService,
|
||||
ThumbnailManagerService,
|
||||
|
@ -1,11 +1,16 @@
|
||||
import {Pipe, PipeTransform} from '@angular/core';
|
||||
import {SearchQueryDTO} from '../../../common/entities/SearchQueryDTO';
|
||||
import {SearchQueryParserService} from '../ui/gallery/search/search-query-parser.service';
|
||||
|
||||
|
||||
@Pipe({name: 'searchQuery'})
|
||||
export class StringifySearchQuery implements PipeTransform {
|
||||
constructor(
|
||||
private _searchQueryParserService: SearchQueryParserService) {
|
||||
}
|
||||
|
||||
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 {ActivatedRoute, Params} from '@angular/router';
|
||||
import {QueryParams} from '../../../../../common/QueryParams';
|
||||
import {SearchQueryParserService} from '../search/search-query-parser.service';
|
||||
|
||||
|
||||
@Component({
|
||||
@ -34,6 +35,7 @@ export class RandomQueryBuilderGalleryComponent implements OnInit, OnDestroy {
|
||||
|
||||
constructor(public _galleryService: GalleryService,
|
||||
private _notification: NotificationService,
|
||||
private _searchQueryParserService: SearchQueryParserService,
|
||||
public i18n: I18n,
|
||||
private _route: ActivatedRoute,
|
||||
private modalService: BsModalService) {
|
||||
@ -57,7 +59,7 @@ export class RandomQueryBuilderGalleryComponent implements OnInit, OnDestroy {
|
||||
|
||||
validateRawSearchText() {
|
||||
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);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@ -65,7 +67,7 @@ export class RandomQueryBuilderGalleryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -2,23 +2,47 @@ import {Injectable} from '@angular/core';
|
||||
import {NetworkService} from '../../../model/network/network.service';
|
||||
import {AutoCompleteItem} from '../../../../../common/entities/AutoCompleteItem';
|
||||
import {GalleryCacheService} from '../cache.gallery.service';
|
||||
import {SearchQueryParserService} from './search-query-parser.service';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
|
||||
@Injectable()
|
||||
export class AutoCompleteService {
|
||||
|
||||
|
||||
constructor(private _networkService: NetworkService,
|
||||
private galleryCacheService: GalleryCacheService) {
|
||||
private _searchQueryParserService: SearchQueryParserService,
|
||||
private _galleryCacheService: GalleryCacheService) {
|
||||
}
|
||||
|
||||
public async autoComplete(text: string): Promise<Array<AutoCompleteItem>> {
|
||||
let items: Array<AutoCompleteItem> = this.galleryCacheService.getAutoComplete(text);
|
||||
if (items == null) {
|
||||
items = await this._networkService.getJson<Array<AutoCompleteItem>>('/autocomplete/' + text);
|
||||
this.galleryCacheService.setAutoComplete(text, items);
|
||||
public autoComplete(text: string): BehaviorSubject<AutoCompleteItem[]> {
|
||||
const items: BehaviorSubject<AutoCompleteItem[]> = 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._galleryCacheService.setAutoComplete(text, ret);
|
||||
items.next(this.sortResults(text, ret.concat(items.value)));
|
||||
});
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
#srch-term {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-width: 0;
|
||||
.search-text {
|
||||
border: 0;
|
||||
z-index: 6;
|
||||
width: 500px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.search-hint {
|
||||
width: 500px;
|
||||
background: white;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
form {
|
||||
|
@ -1,7 +1,7 @@
|
||||
<form class="navbar-form" role="search" #SearchForm="ngForm">
|
||||
<div class="input-group">
|
||||
<input type="text"
|
||||
class="form-control "
|
||||
class="form-control search-text"
|
||||
i18n-placeholder
|
||||
placeholder="Search"
|
||||
(keyup)="onSearchChange($event)"
|
||||
@ -10,12 +10,24 @@
|
||||
[(ngModel)]="rawSearchText"
|
||||
(ngModelChange)="validateRawSearchText()"
|
||||
(keydown.enter)="Search()"
|
||||
(keydown.arrowRight)="applyHint($event)"
|
||||
#name="ngModel"
|
||||
size="30"
|
||||
ngControl="search"
|
||||
name="srch-term"
|
||||
id="srch-term"
|
||||
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"
|
||||
@ -62,7 +74,7 @@
|
||||
<div class="modal-body">
|
||||
<form #searchPanelForm="ngForm" class="form-horizontal">
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
class="form-control search-text"
|
||||
i18n-placeholder
|
||||
placeholder="Search"
|
||||
[(ngModel)]="rawSearchText"
|
||||
|
@ -3,13 +3,14 @@ import {AutoCompleteService} from './autocomplete.service';
|
||||
import {AutoCompleteItem} from '../../../../../common/entities/AutoCompleteItem';
|
||||
import {ActivatedRoute, Params, Router, RouterLink} from '@angular/router';
|
||||
import {GalleryService} from '../gallery.service';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {BehaviorSubject, Subscription} from 'rxjs';
|
||||
import {Config} from '../../../../../common/config/public/Config';
|
||||
import {NavigationService} from '../../../model/navigation.service';
|
||||
import {QueryParams} from '../../../../../common/QueryParams';
|
||||
import {MetadataSearchQueryTypes, SearchQueryDTO, SearchQueryTypes, TextSearch} from '../../../../../common/entities/SearchQueryDTO';
|
||||
import {BsModalService} from 'ngx-bootstrap/modal';
|
||||
import {BsModalRef} from 'ngx-bootstrap/modal/bs-modal-ref.service';
|
||||
import {SearchQueryParserService} from './search-query-parser.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-gallery-search',
|
||||
@ -31,8 +32,10 @@ export class GallerySearchComponent implements OnDestroy {
|
||||
lastInstantSearch: ''
|
||||
};
|
||||
private readonly subscription: Subscription = null;
|
||||
private autocompleteItems: BehaviorSubject<AutoCompleteItem[]>;
|
||||
|
||||
constructor(private _autoCompleteService: AutoCompleteService,
|
||||
private _searchQueryParserService: SearchQueryParserService,
|
||||
private _galleryService: GalleryService,
|
||||
private navigationService: NavigationService,
|
||||
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() {
|
||||
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) {
|
||||
|
||||
|
||||
const searchText = (<HTMLInputElement>event.target).value.trim();
|
||||
|
||||
const searchText = this.getAutocompleteToken();
|
||||
if (Config.Client.Search.AutoComplete.enabled &&
|
||||
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() {
|
||||
this.rawSearchText = SearchQueryDTO.stringify(this.searchQueryDTO);
|
||||
this.rawSearchText = this._searchQueryParserService.stringify(this.searchQueryDTO);
|
||||
}
|
||||
|
||||
validateRawSearchText() {
|
||||
try {
|
||||
this.searchQueryDTO = SearchQueryDTO.parse(this.rawSearchText);
|
||||
this.searchQueryDTO = this._searchQueryParserService.parse(this.rawSearchText);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
@ -124,6 +144,13 @@ export class GallerySearchComponent implements OnDestroy {
|
||||
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() {
|
||||
this.autoCompleteItems = [];
|
||||
}
|
||||
@ -132,15 +159,16 @@ export class GallerySearchComponent implements OnDestroy {
|
||||
if (!Config.Client.Search.AutoComplete.enabled) {
|
||||
return;
|
||||
}
|
||||
if (searchText.trim() === '.') {
|
||||
|
||||
if (searchText.trim().length === 0 ||
|
||||
searchText.trim() === '.') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (searchText.trim().length > 0) {
|
||||
try {
|
||||
const items = await this._autoCompleteService.autoComplete(searchText);
|
||||
this.showSuggestions(items, searchText);
|
||||
this.autocompleteItems = this._autoCompleteService.autoComplete(searchText);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user