mirror of
https://github.com/xuthus83/pigallery2.git
synced 2024-11-03 21:04:03 +08:00
Improve Search field
This commit is contained in:
parent
24942b2ee1
commit
5e2b8a44dd
@ -97,6 +97,7 @@ import {GallerySearchQueryEntryComponent} from './ui/gallery/search/query-enrty/
|
||||
import {StringifySearchQuery} from './pipes/StringifySearchQuery';
|
||||
import {AutoCompleteService} from './ui/gallery/search/autocomplete.service';
|
||||
import {SearchQueryParserService} from './ui/gallery/search/search-query-parser.service';
|
||||
import {GallerySearchFieldComponent} from './ui/gallery/search/search-field/search-field.gallery.component';
|
||||
|
||||
|
||||
@Injectable()
|
||||
@ -179,6 +180,7 @@ export function translationsFactory(locale: string) {
|
||||
FrameComponent,
|
||||
GallerySearchComponent,
|
||||
GallerySearchQueryEntryComponent,
|
||||
GallerySearchFieldComponent,
|
||||
GalleryShareComponent,
|
||||
GalleryNavigatorComponent,
|
||||
GalleryPhotoComponent,
|
||||
|
@ -32,16 +32,11 @@
|
||||
</div>
|
||||
<hr/>
|
||||
<form #searchPanelForm="ngForm" class="form-horizontal">
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
i18n-placeholder
|
||||
placeholder="Search"
|
||||
[(ngModel)]="rawSearchText"
|
||||
(ngModelChange)="validateRawSearchText()"
|
||||
size="30"
|
||||
name="srch-term-preview"
|
||||
id="srch-term-preview"
|
||||
autocomplete="off">
|
||||
<app-gallery-search-field [(ngModel)]="rawSearchText"
|
||||
(ngModelChange)="validateRawSearchText()"
|
||||
name="form-search-field">
|
||||
|
||||
</app-gallery-search-field>
|
||||
|
||||
<app-gallery-search-query-entry
|
||||
[(ngModel)]="searchQueryDTO"
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {Component, EventEmitter, forwardRef, OnChanges, Output} from '@angular/core';
|
||||
import {Component, EventEmitter, forwardRef, Output} from '@angular/core';
|
||||
import {
|
||||
DistanceSearch,
|
||||
ListSearchQueryTypes,
|
||||
@ -32,7 +32,7 @@ import {ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Val
|
||||
}
|
||||
]
|
||||
})
|
||||
export class GallerySearchQueryEntryComponent implements ControlValueAccessor, Validator, OnChanges {
|
||||
export class GallerySearchQueryEntryComponent implements ControlValueAccessor, Validator {
|
||||
public queryEntry: SearchQueryDTO;
|
||||
public SearchQueryTypesEnum: { value: string; key: SearchQueryTypes }[];
|
||||
public SearchQueryTypes = SearchQueryTypes;
|
||||
@ -114,7 +114,7 @@ export class GallerySearchQueryEntryComponent implements ControlValueAccessor, V
|
||||
} else {
|
||||
delete this.AsOrientationQuery.landscape;
|
||||
}
|
||||
this.onChange(this.queryEntry);
|
||||
this.onChange();
|
||||
}
|
||||
|
||||
deleteItem() {
|
||||
@ -125,18 +125,12 @@ export class GallerySearchQueryEntryComponent implements ControlValueAccessor, V
|
||||
this.AsListQuery.list.splice(i, 1);
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
// console.log('ngOnChanges', this.queryEntry);
|
||||
|
||||
}
|
||||
|
||||
public onTouched(): void {
|
||||
}
|
||||
|
||||
public writeValue(obj: any): void {
|
||||
this.queryEntry = obj;
|
||||
// console.log('write value', this.queryEntry);
|
||||
this.ngOnChanges();
|
||||
}
|
||||
|
||||
registerOnChange(fn: (_: any) => void): void {
|
||||
@ -147,7 +141,7 @@ export class GallerySearchQueryEntryComponent implements ControlValueAccessor, V
|
||||
this.propagateTouch = fn;
|
||||
}
|
||||
|
||||
public onChange(event: any) {
|
||||
public onChange() {
|
||||
this.propagateChange(this.queryEntry);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,49 @@
|
||||
.autocomplete-list {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 34px;
|
||||
background-color: white;
|
||||
width: 100%;
|
||||
border: 1px solid #ccc;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
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 {
|
||||
cursor: pointer;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.autocomplete-item {
|
||||
color: #333;
|
||||
padding: 0 20px;
|
||||
line-height: 1.42857143;
|
||||
font-weight: 400;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.autocomplete-item-selected {
|
||||
background-color: #007bff;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
|
||||
.search-text {
|
||||
z-index: 6;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.search-hint {
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
@ -0,0 +1,58 @@
|
||||
<div class="input-group">
|
||||
<input type="text"
|
||||
class="form-control search-text"
|
||||
i18n-placeholder
|
||||
placeholder="Search"
|
||||
(keyup)="onSearchChange($event)"
|
||||
(blur)="onFocusLost()"
|
||||
(focus)="onFocus()"
|
||||
[(ngModel)]="rawSearchText"
|
||||
(ngModelChange)="onChange()"
|
||||
(keydown.enter)="OnEnter($event)"
|
||||
(keydown.arrowRight)="applyHint($event)"
|
||||
(keydown.arrowUp)="selectAutocompleteUp()"
|
||||
(keydown.arrowDown)="selectAutocompleteDown()"
|
||||
(scroll)="Scrolled()"
|
||||
(selectionchange)="Scrolled()"
|
||||
#name="ngModel"
|
||||
size="30"
|
||||
ngControl="search"
|
||||
name="srch-term"
|
||||
id="srch-term"
|
||||
#SearchField
|
||||
autocomplete="off">
|
||||
<input type="text"
|
||||
class="form-control search-hint"
|
||||
[ngModel]="SearchHint"
|
||||
size="30"
|
||||
name="srch-term-hint"
|
||||
id="srch-term-hint"
|
||||
#SearchHintField
|
||||
autocomplete="off">
|
||||
|
||||
|
||||
|
||||
<div class="autocomplete-list" *ngIf="autoCompleteRenders.length > 0"
|
||||
(mouseover)="setMouseOverAutoComplete(true)" (mouseout)="setMouseOverAutoComplete(false)">
|
||||
<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>
|
||||
<span *ngSwitchCase="SearchQueryTypes.directory" class="oi oi-folder"></span>
|
||||
<span *ngSwitchCase="SearchQueryTypes.file_name" class="oi oi-image"></span>
|
||||
<span *ngSwitchCase="SearchQueryTypes.keyword" class="oi oi-tag"></span>
|
||||
<span *ngSwitchCase="SearchQueryTypes.person" class="oi oi-person"></span>
|
||||
<span *ngSwitchCase="SearchQueryTypes.position" class="oi oi-map-marker"></span>
|
||||
</span>
|
||||
{{item.preText}}<strong>{{item.highLightText}}</strong>{{item.postText}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
@ -0,0 +1,254 @@
|
||||
import {Component, ElementRef, EventEmitter, forwardRef, OnDestroy, Output, ViewChild} from '@angular/core';
|
||||
import {ActivatedRoute, Router, RouterLink} from '@angular/router';
|
||||
import {BehaviorSubject, Subscription} from 'rxjs';
|
||||
import {AutoCompleteService, RenderableAutoCompleteItem} from '../autocomplete.service';
|
||||
import {MetadataSearchQueryTypes, SearchQueryTypes} from '../../../../../../common/entities/SearchQueryDTO';
|
||||
import {SearchQueryParserService} from '../search-query-parser.service';
|
||||
import {GalleryService} from '../../gallery.service';
|
||||
import {NavigationService} from '../../../../model/navigation.service';
|
||||
import {Config} from '../../../../../../common/config/public/Config';
|
||||
import {ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator} from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-gallery-search-field',
|
||||
templateUrl: './search-field.gallery.component.html',
|
||||
styleUrls: ['./search-field.gallery.component.css'],
|
||||
providers: [
|
||||
AutoCompleteService, RouterLink,
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => GallerySearchFieldComponent),
|
||||
multi: true
|
||||
},
|
||||
{
|
||||
provide: NG_VALIDATORS,
|
||||
useExisting: forwardRef(() => GallerySearchFieldComponent),
|
||||
multi: true
|
||||
}
|
||||
]
|
||||
})
|
||||
export class GallerySearchFieldComponent implements ControlValueAccessor, Validator, OnDestroy {
|
||||
|
||||
@ViewChild('SearchField', {static: false}) searchField: ElementRef;
|
||||
@ViewChild('SearchHintField', {static: false}) searchHintField: ElementRef;
|
||||
@Output() search = new EventEmitter<void>();
|
||||
|
||||
autoCompleteRenders: AutoCompleteRenderItem[] = [];
|
||||
public rawSearchText = '';
|
||||
mouseOverAutoComplete = false;
|
||||
readonly SearchQueryTypes: typeof SearchQueryTypes;
|
||||
public readonly MetadataSearchQueryTypes: { value: string; key: SearchQueryTypes }[];
|
||||
public highlightedAutoCompleteItem = 0;
|
||||
private cache = {
|
||||
lastAutocomplete: '',
|
||||
lastInstantSearch: ''
|
||||
};
|
||||
private autoCompleteItemsSubscription: Subscription = null;
|
||||
private autoCompleteItems: BehaviorSubject<RenderableAutoCompleteItem[]>;
|
||||
|
||||
constructor(private _autoCompleteService: AutoCompleteService,
|
||||
private _searchQueryParserService: SearchQueryParserService,
|
||||
private _galleryService: GalleryService,
|
||||
private navigationService: NavigationService,
|
||||
private _route: ActivatedRoute,
|
||||
public router: Router) {
|
||||
|
||||
this.SearchQueryTypes = SearchQueryTypes;
|
||||
this.MetadataSearchQueryTypes = MetadataSearchQueryTypes.map(v => ({key: v, value: SearchQueryTypes[v]}));
|
||||
|
||||
}
|
||||
|
||||
get SearchHint() {
|
||||
if (!this.autoCompleteItems ||
|
||||
!this.autoCompleteItems.value || this.autoCompleteItems.value.length === 0) {
|
||||
return this.rawSearchText;
|
||||
}
|
||||
const searchText = this.getAutocompleteToken();
|
||||
if (searchText.current === '') {
|
||||
return this.rawSearchText + this.autoCompleteItems.value[this.highlightedAutoCompleteItem].queryHint;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.autoCompleteItemsSubscription) {
|
||||
this.autoCompleteItemsSubscription.unsubscribe();
|
||||
this.autoCompleteItemsSubscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
getAutocompleteToken(): { current: string, prev: string } {
|
||||
if (this.rawSearchText.trim().length === 0) {
|
||||
return {current: '', prev: ''};
|
||||
}
|
||||
const tokens = this.rawSearchText.split(' ');
|
||||
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.current) {
|
||||
this.cache.lastAutocomplete = searchText.current;
|
||||
this.autocomplete(searchText).catch(console.error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public setMouseOverAutoComplete(value: boolean) {
|
||||
this.mouseOverAutoComplete = value;
|
||||
}
|
||||
|
||||
public onFocusLost() {
|
||||
if (this.mouseOverAutoComplete === false) {
|
||||
this.autoCompleteRenders = [];
|
||||
}
|
||||
}
|
||||
|
||||
public onFocus() {
|
||||
// TODO: implement autocomplete
|
||||
// this.autocomplete(this.searchText).catch(console.error);
|
||||
}
|
||||
|
||||
|
||||
applyHint($event: any) {
|
||||
if ($event.target.selectionStart !== this.rawSearchText.length) {
|
||||
return;
|
||||
}
|
||||
this.rawSearchText = this.SearchHint;
|
||||
this.onChange();
|
||||
}
|
||||
|
||||
applyAutoComplete(item: AutoCompleteRenderItem) {
|
||||
const token = this.getAutocompleteToken();
|
||||
this.rawSearchText = this.rawSearchText.substr(0, this.rawSearchText.length - token.current.length)
|
||||
+ item.queryHint;
|
||||
this.onChange();
|
||||
this.emptyAutoComplete();
|
||||
}
|
||||
|
||||
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.emit();
|
||||
return;
|
||||
}
|
||||
this.applyAutoComplete(this.autoCompleteRenders[this.highlightedAutoCompleteItem]);
|
||||
}
|
||||
|
||||
public onTouched(): void {
|
||||
}
|
||||
|
||||
public writeValue(obj: any): void {
|
||||
this.rawSearchText = obj;
|
||||
}
|
||||
|
||||
registerOnChange(fn: (_: any) => void): void {
|
||||
this.propagateChange = fn;
|
||||
}
|
||||
|
||||
registerOnTouched(fn: () => void): void {
|
||||
this.propagateTouch = fn;
|
||||
}
|
||||
|
||||
public onChange() {
|
||||
this.propagateChange(this.rawSearchText);
|
||||
}
|
||||
|
||||
validate(control: FormControl): ValidationErrors {
|
||||
return {required: true};
|
||||
}
|
||||
|
||||
Scrolled() {
|
||||
this.searchHintField.nativeElement.scrollLeft = this.searchField.nativeElement.scrollLeft;
|
||||
}
|
||||
|
||||
private emptyAutoComplete() {
|
||||
this.highlightedAutoCompleteItem = 0;
|
||||
this.autoCompleteRenders = [];
|
||||
}
|
||||
|
||||
private async autocomplete(searchText: { current: string, prev: string }) {
|
||||
if (!Config.Client.Search.AutoComplete.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
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.autoCompleteItemsSubscription = this.autoCompleteItems.subscribe(() => {
|
||||
this.showSuggestions(this.autoCompleteItems.value, searchText.current);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
} else {
|
||||
this.emptyAutoComplete();
|
||||
}
|
||||
}
|
||||
|
||||
private showSuggestions(suggestions: RenderableAutoCompleteItem[], searchText: string) {
|
||||
this.emptyAutoComplete();
|
||||
suggestions.forEach((item: RenderableAutoCompleteItem) => {
|
||||
const renderItem = new AutoCompleteRenderItem(item.text, searchText, item.type, item.queryHint);
|
||||
this.autoCompleteRenders.push(renderItem);
|
||||
});
|
||||
}
|
||||
|
||||
private propagateChange = (_: any) => {
|
||||
};
|
||||
|
||||
private propagateTouch = (_: any) => {
|
||||
};
|
||||
}
|
||||
|
||||
class AutoCompleteRenderItem {
|
||||
public preText = '';
|
||||
public highLightText = '';
|
||||
public postText = '';
|
||||
public type: SearchQueryTypes;
|
||||
public queryHint: string;
|
||||
|
||||
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);
|
||||
this.highLightText = text.substring(preIndex, preIndex + searchText.length);
|
||||
this.postText = text.substring(preIndex + searchText.length);
|
||||
} else {
|
||||
this.postText = text;
|
||||
}
|
||||
this.type = type;
|
||||
this.queryHint = queryHint;
|
||||
}
|
||||
}
|
||||
|
@ -1,54 +1,15 @@
|
||||
.autocomplete-list {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 34px;
|
||||
background-color: white;
|
||||
width: 100%;
|
||||
border: 1px solid #ccc;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
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 {
|
||||
cursor: pointer;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.autocomplete-item {
|
||||
color: #333;
|
||||
padding: 0 20px;
|
||||
line-height: 1.42857143;
|
||||
font-weight: 400;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.autocomplete-item-selected {
|
||||
background-color: #007bff;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
|
||||
.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 {
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
@media screen and ( min-width: 1200px) {
|
||||
.search-field {
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and ( min-width: 1400px) {
|
||||
.search-field {
|
||||
width: 500px;
|
||||
}
|
||||
}
|
||||
|
@ -1,57 +1,13 @@
|
||||
<form class="navbar-form" role="search" #SearchForm="ngForm">
|
||||
<div class="input-group">
|
||||
<input type="text"
|
||||
class="form-control search-text"
|
||||
i18n-placeholder
|
||||
placeholder="Search"
|
||||
(keyup)="onSearchChange($event)"
|
||||
(blur)="onFocusLost()"
|
||||
(focus)="onFocus()"
|
||||
[(ngModel)]="rawSearchText"
|
||||
(ngModelChange)="validateRawSearchText()"
|
||||
(keydown.enter)="OnEnter($event)"
|
||||
(keydown.arrowRight)="applyHint($event)"
|
||||
(keydown.arrowUp)="selectAutocompleteUp()"
|
||||
(keydown.arrowDown)="selectAutocompleteDown()"
|
||||
#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">
|
||||
|
||||
<app-gallery-search-field [(ngModel)]="rawSearchText"
|
||||
(ngModelChange)="validateRawSearchText()"
|
||||
class="search-field"
|
||||
(search)="Search()"
|
||||
name="search-field">
|
||||
|
||||
<div class="autocomplete-list" *ngIf="autoCompleteRenders.length > 0"
|
||||
(mouseover)="setMouseOverAutoComplete(true)" (mouseout)="setMouseOverAutoComplete(false)">
|
||||
<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>
|
||||
<span *ngSwitchCase="SearchQueryTypes.directory" class="oi oi-folder"></span>
|
||||
<span *ngSwitchCase="SearchQueryTypes.file_name" class="oi oi-image"></span>
|
||||
<span *ngSwitchCase="SearchQueryTypes.keyword" class="oi oi-tag"></span>
|
||||
<span *ngSwitchCase="SearchQueryTypes.person" class="oi oi-person"></span>
|
||||
<span *ngSwitchCase="SearchQueryTypes.position" class="oi oi-map-marker"></span>
|
||||
</span>
|
||||
{{item.preText}}<strong>{{item.highLightText}}</strong>{{item.postText}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</app-gallery-search-field>
|
||||
|
||||
<div class="input-group-btn" style="display: block">
|
||||
<button class="btn btn-light" type="button"
|
||||
@ -79,16 +35,12 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form #searchPanelForm="ngForm" class="form-horizontal">
|
||||
<input type="text"
|
||||
class="form-control search-text"
|
||||
i18n-placeholder
|
||||
placeholder="Search"
|
||||
[(ngModel)]="rawSearchText"
|
||||
(ngModelChange)="validateRawSearchText()"
|
||||
size="30"
|
||||
name="srch-term-preview"
|
||||
id="srch-term-preview"
|
||||
autocomplete="off">
|
||||
<app-gallery-search-field [(ngModel)]="rawSearchText"
|
||||
(ngModelChange)="validateRawSearchText()"
|
||||
(search)="Search()"
|
||||
name="form-search-field">
|
||||
|
||||
</app-gallery-search-field>
|
||||
|
||||
<app-gallery-search-query-entry
|
||||
[(ngModel)]="searchQueryDTO"
|
||||
|
@ -1,9 +1,8 @@
|
||||
import {Component, OnDestroy, TemplateRef} from '@angular/core';
|
||||
import {AutoCompleteService, RenderableAutoCompleteItem} from './autocomplete.service';
|
||||
import {AutoCompleteService} from './autocomplete.service';
|
||||
import {ActivatedRoute, Params, Router, RouterLink} from '@angular/router';
|
||||
import {GalleryService} from '../gallery.service';
|
||||
import {BehaviorSubject, Subscription} from 'rxjs';
|
||||
import {Config} from '../../../../../common/config/public/Config';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {NavigationService} from '../../../model/navigation.service';
|
||||
import {QueryParams} from '../../../../../common/QueryParams';
|
||||
import {MetadataSearchQueryTypes, SearchQueryDTO, SearchQueryTypes, TextSearch} from '../../../../../common/entities/SearchQueryDTO';
|
||||
@ -19,20 +18,13 @@ import {SearchQueryParserService} from './search-query-parser.service';
|
||||
})
|
||||
export class GallerySearchComponent implements OnDestroy {
|
||||
|
||||
autoCompleteRenders: AutoCompleteRenderItem[] = [];
|
||||
public searchQueryDTO: SearchQueryDTO = <TextSearch>{type: SearchQueryTypes.any_text, text: ''};
|
||||
public rawSearchText = '';
|
||||
mouseOverAutoComplete = false;
|
||||
readonly SearchQueryTypes: typeof SearchQueryTypes;
|
||||
modalRef: BsModalRef;
|
||||
public readonly MetadataSearchQueryTypes: { value: string; key: SearchQueryTypes }[];
|
||||
public highlightedAutoCompleteItem = 0;
|
||||
private cache = {
|
||||
lastAutocomplete: '',
|
||||
lastInstantSearch: ''
|
||||
};
|
||||
private readonly subscription: Subscription = null;
|
||||
private autoCompleteItems: BehaviorSubject<RenderableAutoCompleteItem[]>;
|
||||
|
||||
constructor(private _autoCompleteService: AutoCompleteService,
|
||||
private _searchQueryParserService: SearchQueryParserService,
|
||||
@ -57,21 +49,6 @@ 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 (searchText.current === '') {
|
||||
return this.rawSearchText + this.autoCompleteItems.value[this.highlightedAutoCompleteItem].queryHint;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
get HTMLSearchQuery() {
|
||||
return JSON.stringify(this.searchQueryDTO);
|
||||
}
|
||||
@ -83,43 +60,6 @@ export class GallerySearchComponent implements OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
getAutocompleteToken(): { current: string, prev: string } {
|
||||
if (this.rawSearchText.trim().length === 0) {
|
||||
return {current: '', prev: ''};
|
||||
}
|
||||
const tokens = this.rawSearchText.split(' ');
|
||||
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.current) {
|
||||
this.cache.lastAutocomplete = searchText.current;
|
||||
this.autocomplete(searchText).catch(console.error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public setMouseOverAutoComplete(value: boolean) {
|
||||
this.mouseOverAutoComplete = value;
|
||||
}
|
||||
|
||||
public onFocusLost() {
|
||||
if (this.mouseOverAutoComplete === false) {
|
||||
this.autoCompleteRenders = [];
|
||||
}
|
||||
}
|
||||
|
||||
public onFocus() {
|
||||
// TODO: implement autocomplete
|
||||
// this.autocomplete(this.searchText).catch(console.error);
|
||||
}
|
||||
|
||||
public async openModal(template: TemplateRef<any>) {
|
||||
this.modalRef = this.modalService.show(template, {class: 'modal-lg'});
|
||||
document.body.style.paddingRight = '0px';
|
||||
@ -136,7 +76,7 @@ export class GallerySearchComponent implements OnDestroy {
|
||||
|
||||
onQueryChange() {
|
||||
this.rawSearchText = this._searchQueryParserService.stringify(this.searchQueryDTO);
|
||||
this.validateRawSearchText();
|
||||
// this.validateRawSearchText();
|
||||
}
|
||||
|
||||
validateRawSearchText() {
|
||||
@ -151,102 +91,7 @@ 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;
|
||||
this.validateRawSearchText();
|
||||
}
|
||||
|
||||
applyAutoComplete(item: AutoCompleteRenderItem) {
|
||||
const token = this.getAutocompleteToken();
|
||||
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: { current: string, prev: string }) {
|
||||
if (!Config.Client.Search.AutoComplete.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
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.current);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
} else {
|
||||
this.emptyAutoComplete();
|
||||
}
|
||||
}
|
||||
|
||||
private showSuggestions(suggestions: RenderableAutoCompleteItem[], searchText: string) {
|
||||
this.emptyAutoComplete();
|
||||
suggestions.forEach((item: RenderableAutoCompleteItem) => {
|
||||
const renderItem = new AutoCompleteRenderItem(item.text, searchText, item.type, item.queryHint);
|
||||
this.autoCompleteRenders.push(renderItem);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class AutoCompleteRenderItem {
|
||||
public preText = '';
|
||||
public highLightText = '';
|
||||
public postText = '';
|
||||
public type: SearchQueryTypes;
|
||||
public queryHint: string;
|
||||
|
||||
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);
|
||||
this.highLightText = text.substring(preIndex, preIndex + searchText.length);
|
||||
this.postText = text.substring(preIndex + searchText.length);
|
||||
} else {
|
||||
this.postText = text;
|
||||
}
|
||||
this.type = type;
|
||||
this.queryHint = queryHint;
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user