/*
* © 2017 Stratio Big Data Inc., Sucursal en España.
*
* This software is licensed under the Apache License, Version 2.0.
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the terms of the License for more details.
*
* SPDX-License-Identifier: Apache-2.0.
*/
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
Renderer2,
SimpleChanges,
ViewChild
} from '@angular/core';
import { FormControl } from '@angular/forms';
import { Subscription } from 'rxjs';
import { StDropDownMenuItem } from '../st-dropdown-menu/st-dropdown-menu.interface';
import { EventWindowManager } from '../utils/event-window-manager';
import { debounceTime } from 'rxjs/operators';
import { StSearchEvent, StSearchEventOrigin } from './st-search.model';
/**
* @description {Component} [Search]
*
* The search component has been designed to allow user to find a specific content according to his needs.
*
* @example
*
* {html}
*
* ```
* Search without filters
*
*
*
* Search with filters
*
* ```
*
*/
@Component({
selector: 'st-search',
templateUrl: './st-search.component.html',
styleUrls: ['./st-search.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class StSearchComponent extends EventWindowManager implements OnChanges, OnDestroy, OnInit {
/** @Input {number} [debounce=0] Time elapsed in milliseconds before displaying the autocomplete list */
@Input() debounce: number = 0;
/** @Input {boolean} [liveSearch=true] Boolean to enable or disable the automatic search while the user is typing */
@Input() liveSearch: boolean = true;
/** @Input {number} [minLength=0] Minimum of characters typed by the user before launching the search */
@Input() minLength: number = 0;
/** @Input {string} [placeholder='Search'] Text displayed in the search input */
@Input() placeholder: string = 'Search';
/** @Input {string} [qaTag=''] Label used as id */
@Input() qaTag: string;
/** @Input {string} [value=''] Initial value of the search text */
@Input() value: string;
/** @Input {boolean} [disabled=false] Boolean to enable or disable the search */
@Input() disabled: boolean = false;
/** @Input {boolean} [withAutocomplete=false] Enable or disable the autocomplete list when user is typing */
@Input() withAutocomplete: boolean = false;
/** @Input {StDropDownMenuItem[]} [autocompleteList=''] List of items displayed in the autocomplete list when user is typing */
@Input() autocompleteList: StDropDownMenuItem[] = [];
/** @Input {boolean} [showIcon=true] Boolean to hide/show the loupe icon */
@Input() showIcon: boolean = true;
/** @Input {string} [emptyAutocompleteListMessage=''] Message displayed when the autocomplete list is enabled but
* there are not any item with the typed text
*/
@Input() emptyAutocompleteListMessage: string = '';
/** @Input {StDropDownMenuItem[]} [filterOptions=''] Options displayed at the filter select. If it is not introduced,
* filter will not be displayed
*/
@Input() filterOptions: StDropDownMenuItem[];
/** @Output { Object(filter?: string, text: string)} [search=''] Event emitted when search is launched. It contains
* the text typed by the user and the filter value selected (only if filter is displayed)
*/
@Output() search: EventEmitter = new EventEmitter();
/** @Output {any} [value=''] Event emitted when filter is changed. It contains
* the filter value selected
*/
@Output() selectFilter: EventEmitter = new EventEmitter();
/** @Input {boolean} [keyBoardMove=false] It is needed to activate navigation through options using the keyboard
*/
@Input() keyBoardMove: boolean = false;
public searchBox: FormControl = new FormControl();
public showClear: boolean;
public filter: string;
@ViewChild('buttonId', {static: false}) public _eventElementRef: ElementRef;
private subscriptionSearch: Subscription | undefined = undefined;
private subscriptionSearchClearButton: Subscription | undefined = undefined;
private lastEmittedText: string | undefined = undefined;
constructor(private _render: Renderer2,
private cd: ChangeDetectorRef) {
super(_render, cd);
}
get menuId(): string {
return this.qaTag ? this.qaTag + '-autocomplete-menu' : null;
}
public ngOnInit(): void {
if (this.value) {
this.searchBox.setValue(this.value);
}
// Show clear button if have text
this.subscriptionSearchClearButton = this.searchBox.valueChanges.subscribe((val) => this.showClear = (val && val.length > 0));
this.checkDisabled();
this.manageSubscription();
if (this.filterOptions) {
this.filter = this.filterOptions[0].value;
}
}
public ngOnChanges(changes: SimpleChanges): void {
this.checkDebounceChange(changes);
this.checkValueChange(changes);
this.checkDisableChange(changes);
this.checkAutoCompleteMenuChange(changes);
}
public onChangeFilter(value: any): void {
this.selectFilter.emit(value);
this.emitValue(false, StSearchEventOrigin.FILTER);
}
public ngOnDestroy(): void {
if (this.subscriptionSearch !== undefined) {
this.subscriptionSearch.unsubscribe();
}
if (this.subscriptionSearchClearButton !== undefined) {
this.subscriptionSearchClearButton.unsubscribe();
}
this.closeElement();
}
public launchSearch(force: boolean, origin: StSearchEventOrigin): void {
if (this.canSearch()) {
this.showAutoCompleteMenu();
this.emitValue(force, origin);
} else {
this.closeElement();
}
}
public onKeyPress(event: KeyboardEvent): void {
let key: number = event.keyCode || event.which;
if (key === 13) {
this.launchSearch(true, StSearchEventOrigin.ENTER);
}
}
public changeOption(item: StDropDownMenuItem): void {
if (item && item.label) {
this.subscriptionSearch.unsubscribe();
this.searchBox.setValue(item.value);
this.cd.markForCheck();
this.closeElement();
this.emitValue(true, StSearchEventOrigin.LIST);
this.manageSubscription();
}
}
public clearInput(): void {
this.searchBox.setValue('');
this.closeElement();
this.emitValue(true, StSearchEventOrigin.INPUT);
}
public showAutoCompleteMenu(): void {
if (this.withAutocomplete && !this.isActive) {
this.openElement();
}
if (this.searchBox.value === '') {
this.closeElement();
}
this.cd.markForCheck();
}
private emitValue(force: boolean, origin: StSearchEventOrigin): void {
if (this.isEqualPrevious(force)) {
this.lastEmittedText = this.searchBox.value;
let newSearch: StSearchEvent = {text: this.lastEmittedText || '', origin: origin};
if (this.filter) {
newSearch.filter = this.filter;
}
this.search.emit(newSearch);
}
}
private checkDisabled(): void {
if (this.disabled) {
this.searchBox.disable();
} else {
this.searchBox.enable();
}
}
private canSearch(): boolean {
return this.isDefined() && !this.disabled && this.checkMins();
}
private isDefined(): boolean {
return this.searchBox && this.searchBox.value !== null && this.searchBox.value !== undefined;
}
private checkMins(): boolean {
return this.minLength <= (this.searchBox && this.searchBox.value && this.searchBox.value.length) ||
this.searchBox.value.trim().length === 0;
}
private isEqualPrevious(force: boolean): boolean {
return this.lastEmittedText !== this.searchBox.value || force;
}
private checkValueChange(changes: SimpleChanges): void {
if (changes && changes.value) {
if (this.subscriptionSearch) {
this.subscriptionSearch.unsubscribe();
}
this.searchBox.setValue(changes.value.currentValue);
this.manageSubscription();
}
}
private checkDebounceChange(changes: SimpleChanges): void {
if (changes && changes.debounce) {
this.manageSubscription();
}
}
private checkDisableChange(changes: SimpleChanges): void {
if (changes && changes.disabled) {
this.checkDisabled();
}
}
private checkAutoCompleteMenuChange(changes: SimpleChanges): void {
if (changes && changes.autocompleteList) {
this.cd.markForCheck();
}
}
private manageSubscription(): void {
if (this.subscriptionSearch !== undefined) {
this.subscriptionSearch.unsubscribe();
}
if (this.liveSearch) {
this.subscriptionSearch = this.searchBox
.valueChanges.pipe(
debounceTime(this.debounce))
.subscribe((event) => this.launchSearch(false, StSearchEventOrigin.INPUT));
}
}
}