| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| import {Component, OnInit, OnDestroy, HostListener, Input, ViewChild, ElementRef, forwardRef} from '@angular/core'; |
| import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; |
| import {Subject} from 'rxjs/Subject'; |
| import {SearchBoxParameter, SearchBoxParameterProcessed, SearchBoxParameterTriggered} from '@app/classes/filtering'; |
| import {ListItem} from '@app/classes/list-item'; |
| import {HomogeneousObject} from '@app/classes/object'; |
| import {UtilsService} from '@app/services/utils.service'; |
| import {Subscription} from 'rxjs/Subscription'; |
| |
| @Component({ |
| selector: 'search-box', |
| templateUrl: './search-box.component.html', |
| styleUrls: ['./search-box.component.less'], |
| providers: [ |
| { |
| provide: NG_VALUE_ACCESSOR, |
| useExisting: forwardRef(() => SearchBoxComponent), |
| multi: true |
| } |
| ] |
| }) |
| export class SearchBoxComponent implements OnInit, OnDestroy, ControlValueAccessor { |
| |
| private currentId: number = 0; |
| |
| private isExclude: boolean = false; |
| |
| /** |
| * Indicates whether search box is currently active |
| * @type {boolean} |
| */ |
| isActive: boolean = false; |
| |
| /** |
| * Indicates whether search query parameter value is currently typed |
| * @type {boolean} |
| */ |
| isValueInput: boolean = false; |
| |
| currentValue: string; |
| |
| /** |
| * Indicates whether there's no autocomplete matches in preset options for search query parameter name |
| * @type {boolean} |
| */ |
| private noMatchingParameterName: boolean = true; |
| |
| /** |
| * Indicates whether there's no autocomplete matches in preset options for search query parameter value |
| * @type {boolean} |
| */ |
| private noMatchingParameterValue: boolean = true; |
| |
| @Input() |
| items: ListItem[] = []; |
| |
| @Input() |
| itemsOptions: HomogeneousObject<ListItem[]> = {}; |
| |
| /** |
| * Name of parameter to be used if there are no matching values |
| * @type {string} |
| */ |
| @Input() |
| defaultParameterName?: string; |
| |
| @Input() |
| parameterNameChangeSubject: Subject<SearchBoxParameterTriggered> = new Subject(); |
| |
| @Input() |
| parameterAddSubject: Subject<SearchBoxParameter> = new Subject(); |
| |
| @Input() |
| updateValueSubject: Subject<void> = new Subject(); |
| |
| /** |
| * Indicates whether form should receive updated value immediately after user adds new search parameter, without |
| * explicit actions like pressing Submit button or Enter key |
| * @type {boolean} |
| */ |
| @Input() |
| updateValueImmediately = true; |
| |
| @ViewChild('parameterInput') |
| parameterInputRef: ElementRef; |
| |
| @ViewChild('valueInput') |
| valueInputRef: ElementRef; |
| |
| private parameterInput: HTMLInputElement; |
| |
| private valueInput: HTMLInputElement; |
| |
| /** |
| * Currently active search query parameter |
| * @type {ListItem | null} |
| */ |
| activeItem: ListItem | null = null; |
| |
| /** |
| * Search query parameters that are already specified by user |
| * @type {SearchBoxParameterProcessed[]} |
| */ |
| parameters: SearchBoxParameterProcessed[] = []; |
| |
| private onChange; |
| |
| private destroyed$ = new Subject(); |
| |
| constructor(private utils: UtilsService) {} |
| |
| ngOnInit(): void { |
| this.parameterInput = this.parameterInputRef.nativeElement; |
| this.valueInput = this.valueInputRef.nativeElement; |
| this.parameterNameChangeSubject.takeUntil(this.destroyed$).subscribe(this.onParameterNameChange); |
| this.parameterAddSubject.takeUntil(this.destroyed$).subscribe(this.onParameterAdd); |
| this.updateValueSubject.takeUntil(this.destroyed$).subscribe(this.updateValue); |
| } |
| |
| ngOnDestroy(): void { |
| this.destroyed$.next(true); |
| } |
| |
| /** |
| * Available options for value of currently active search query parameter |
| * @returns {ListItem[]} |
| */ |
| get activeItemValueOptions(): ListItem[] { |
| return this.itemsOptions && this.activeItem && this.itemsOptions[this.activeItem.value] ? |
| this.itemsOptions[this.activeItem.value] : []; |
| } |
| |
| @HostListener('click') |
| private onRootClick(): void { |
| if (!this.isActive) { |
| this.parameterInput.focus(); |
| } |
| } |
| |
| @HostListener('keydown', ['$event']) |
| private onRootKeyDown(event: KeyboardEvent): void { |
| if (this.utils.isEnterPressed(event)) { |
| event.preventDefault(); |
| } |
| }; |
| |
| @HostListener('blur') |
| private onRootBlur(): void { |
| this.clear(); |
| }; |
| |
| onParameterInputFocus(): void { |
| this.isActive = true; |
| } |
| |
| private switchToParameterInput = (): void => { |
| this.clear(); |
| this.isActive = true; |
| this.isValueInput = false; |
| setTimeout(() => this.parameterInput.focus(), 0); |
| } |
| |
| private getItemByValue(name: string): ListItem { |
| return this.items.find((field: ListItem): boolean => field.value === name); |
| } |
| |
| clear(): void { |
| this.isActive = false; |
| this.activeItem = null; |
| this.currentValue = ''; |
| this.parameterInput.value = ''; |
| this.valueInput.value = ''; |
| } |
| |
| onClearButtonClick = (event: MouseEvent): void => { |
| this.clear(); |
| this.parameters = []; |
| this.updateValueSubject.next(); |
| event.stopPropagation(); |
| event.preventDefault(); |
| } |
| |
| changeParameterName(options: SearchBoxParameterTriggered): void { |
| this.parameterNameChangeSubject.next(options); |
| } |
| |
| onParameterNameChange = (options: SearchBoxParameterTriggered): void => { |
| this.activeItem = options.item.label ? options.item : this.getItemByValue(options.item.value); |
| this.isExclude = options.isExclude; |
| this.isActive = true; |
| this.isValueInput = true; |
| this.currentValue = ''; |
| this.valueInput.focus(); |
| } |
| |
| onParameterValueKeyDown(event: KeyboardEvent): void { |
| if (this.utils.isBackSpacePressed(event) && !this.currentValue) { |
| this.switchToParameterInput(); |
| } |
| } |
| |
| onParameterValueKeyUp(event: KeyboardEvent): void { |
| if (this.utils.isEnterPressed(event) && this.currentValue && this.noMatchingParameterValue) { |
| this.onParameterValueChange(this.currentValue); |
| } |
| } |
| |
| onParameterValueChange(value: string): void { |
| this.parameters.push({ |
| id: this.currentId++, |
| name: this.activeItem.value, |
| label: this.activeItem.label, |
| value: value, |
| isExclude: this.isExclude |
| }); |
| if (this.updateValueImmediately) { |
| this.updateValueSubject.next(); |
| } |
| this.switchToParameterInput(); |
| } |
| |
| /** |
| * Adding the new parameter to search query |
| * @param parameter {SearchBoxParameter} |
| */ |
| onParameterAdd = (parameter: SearchBoxParameter): void => { |
| const item = this.getItemByValue(parameter.name); |
| this.parameters.push({ |
| id: this.currentId++, |
| name: parameter.name, |
| label: item.label, |
| value: parameter.value, |
| isExclude: parameter.isExclude |
| }); |
| if (this.updateValueImmediately) { |
| this.updateValueSubject.next(); |
| } |
| this.switchToParameterInput(); |
| } |
| |
| onParameterKeyUp(event: KeyboardEvent): void { |
| if (this.utils.isEnterPressed(event)) { |
| if (!this.currentValue && !this.updateValueImmediately) { |
| this.updateValueSubject.next(); |
| } else if (this.currentValue && this.noMatchingParameterName && this.defaultParameterName) { |
| this.parameterAddSubject.next({ |
| name: this.defaultParameterName, |
| value: this.currentValue, |
| isExclude: false |
| }); |
| } |
| } |
| } |
| |
| /** |
| * Toggle the parameter isExclude property value |
| * @param event {MouseEvent} - event that triggered this action |
| * @param id {number} - id of parameter |
| */ |
| toggleParameter(event: MouseEvent, id: number): void { |
| this.parameters = this.parameters.map((parameter: SearchBoxParameterProcessed): SearchBoxParameterProcessed => { |
| if (parameter.id === id) { |
| parameter.isExclude = !parameter.isExclude; |
| } |
| return parameter; |
| }); |
| if (this.updateValueImmediately) { |
| this.updateValueSubject.next(); |
| } |
| event.stopPropagation(); |
| } |
| |
| /** |
| * Removing parameter from search query |
| * @param event {MouseEvent} - event that triggered this action |
| * @param id {number} - id of parameter |
| */ |
| removeParameter(event: MouseEvent, id: number): void { |
| this.parameters = this.parameters.filter((parameter: SearchBoxParameterProcessed): boolean => parameter.id !== id); |
| if (this.updateValueImmediately) { |
| this.updateValueSubject.next(); |
| } |
| event.stopPropagation(); |
| } |
| |
| updateValue = (): void => { |
| this.currentValue = ''; |
| if (this.onChange) { |
| this.onChange([...this.parameters]); |
| } |
| } |
| |
| /** |
| * Update flag that indicates presence of autocomplete matches in preset options for search query parameter name |
| * @param hasNoMatches {boolean} |
| */ |
| setParameterNameMatchFlag(hasNoMatches: boolean): void { |
| this.noMatchingParameterName = hasNoMatches; |
| } |
| |
| /** |
| * Update flag that indicates presence of autocomplete matches in preset options for search query parameter value |
| * @param hasNoMatches {boolean} |
| */ |
| setParameterValueMatchFlag(hasNoMatches: boolean): void { |
| this.noMatchingParameterValue = hasNoMatches; |
| } |
| |
| writeValue(parameters: SearchBoxParameterProcessed[] = []): void { |
| this.currentValue = ''; |
| this.parameters = [...parameters]; |
| // this.updateValueSubject.next(); |
| } |
| |
| registerOnChange(callback: any): void { |
| this.onChange = callback; |
| } |
| |
| registerOnTouched(): void { |
| } |
| |
| } |