335 lines
12 KiB
TypeScript
335 lines
12 KiB
TypeScript
import { FocusMonitor } from '@angular/cdk/a11y';
|
|
import { ENTER } from '@angular/cdk/keycodes';
|
|
import { Component, DoCheck, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Optional, Output, Self, SimpleChanges, TemplateRef, ViewChild } from '@angular/core';
|
|
import { ControlValueAccessor, FormGroupDirective, NgControl, NgForm } from '@angular/forms';
|
|
import { MatAutocomplete, MatAutocompleteSelectedEvent, MatAutocompleteTrigger } from '@angular/material/autocomplete';
|
|
import { ErrorStateMatcher, mixinErrorState } from '@angular/material/core';
|
|
import { MatFormFieldControl } from '@angular/material/form-field';
|
|
import { AutoCompleteGroup } from '@app/library/auto-complete/auto-complete-group';
|
|
import { SingleAutoCompleteConfiguration } from '@app/library/auto-complete/single/single-auto-complete-configuration';
|
|
import { BaseComponent } from '@common/base/base.component';
|
|
import { Observable, of as observableOf, Subject } from 'rxjs';
|
|
import { debounceTime, distinctUntilChanged, map, mergeMap, startWith, takeUntil, switchMap } from 'rxjs/operators';
|
|
|
|
|
|
export class CustomComponentBase extends BaseComponent {
|
|
constructor(
|
|
public _defaultErrorStateMatcher: ErrorStateMatcher,
|
|
public _parentForm: NgForm,
|
|
public _parentFormGroup: FormGroupDirective,
|
|
public ngControl: NgControl
|
|
) { super(); }
|
|
}
|
|
export const _CustomComponentMixinBase = mixinErrorState(CustomComponentBase);
|
|
|
|
@Component({
|
|
selector: 'app-single-auto-complete',
|
|
templateUrl: './single-auto-complete.component.html',
|
|
styleUrls: ['./single-auto-complete.component.scss'],
|
|
providers: [{ provide: MatFormFieldControl, useExisting: SingleAutoCompleteComponent }],
|
|
})
|
|
export class SingleAutoCompleteComponent extends _CustomComponentMixinBase implements OnInit, MatFormFieldControl<string>, ControlValueAccessor, OnDestroy, DoCheck, OnChanges {
|
|
|
|
static nextId = 0;
|
|
@ViewChild('autocomplete', { static: true }) autocomplete: MatAutocomplete;
|
|
@ViewChild('autocompleteTrigger', { static: true }) autocompleteTrigger: MatAutocompleteTrigger;
|
|
@ViewChild('autocompleteInput', { static: true }) autocompleteInput: ElementRef;
|
|
|
|
@Input() initialSelectedData: any;
|
|
@Input()
|
|
get configuration(): SingleAutoCompleteConfiguration { return this._configuration; }
|
|
set configuration(configuration: SingleAutoCompleteConfiguration) {
|
|
this._configuration = configuration;
|
|
//On startup, fill the input with the existing value
|
|
if (this.autocompleteInput && this.value) {
|
|
this.inputValue = this._displayFn(this.value);
|
|
}
|
|
}
|
|
private _configuration: SingleAutoCompleteConfiguration;
|
|
|
|
// Selected Option Event
|
|
@Output() optionSelected: EventEmitter<any> = new EventEmitter();
|
|
|
|
id = `single-autocomplete-${SingleAutoCompleteComponent.nextId++}`;
|
|
stateChanges = new Subject<void>();
|
|
focused = false;
|
|
controlType = 'single-autocomplete';
|
|
describedBy = '';
|
|
inputValue = '';
|
|
_inputSubject = new Subject<string>();
|
|
loading = false;
|
|
_items: Observable<any[]>;
|
|
_groupedItems: Observable<AutoCompleteGroup[]>;
|
|
_selectedItems: Map<string, any> = new Map<any, any>();
|
|
|
|
|
|
get empty() { return (this.value == null) && (!this.inputValue || this.inputValue.length === 0); }
|
|
|
|
get shouldLabelFloat() { return this.focused || !this.empty; }
|
|
|
|
@Input()
|
|
get placeholder() { return this._placeholder; }
|
|
set placeholder(placeholder) {
|
|
this._placeholder = placeholder;
|
|
this.stateChanges.next();
|
|
}
|
|
private _placeholder: string;
|
|
|
|
@Input()
|
|
get required() { return this._required; }
|
|
set required(req) {
|
|
this._required = !!(req);
|
|
this.stateChanges.next();
|
|
}
|
|
private _required = false;
|
|
|
|
@Input()
|
|
get disabled() { return this._disabled; }
|
|
set disabled(dis) {
|
|
this._disabled = !!(dis);
|
|
this.stateChanges.next();
|
|
}
|
|
private _disabled = false;
|
|
|
|
@Input()
|
|
get value(): any | null {
|
|
return this._selectedValue;
|
|
}
|
|
set value(value: any | null) {
|
|
this._selectedValue = value;
|
|
this.stateChanges.next();
|
|
}
|
|
private _selectedValue;
|
|
|
|
constructor(
|
|
private fm: FocusMonitor,
|
|
private elRef: ElementRef,
|
|
@Optional() @Self() public ngControl: NgControl,
|
|
@Optional() _parentForm: NgForm,
|
|
@Optional() _parentFormGroup: FormGroupDirective,
|
|
_defaultErrorStateMatcher: ErrorStateMatcher
|
|
) {
|
|
super(_defaultErrorStateMatcher, _parentForm, _parentFormGroup, ngControl);
|
|
|
|
fm.monitor(elRef.nativeElement, true).pipe(takeUntil(this._destroyed)).subscribe((origin) => {
|
|
this.focused = !!origin;
|
|
this.stateChanges.next();
|
|
});
|
|
|
|
if (this.ngControl != null) {
|
|
// Setting the value accessor directly (instead of using
|
|
// the providers) to avoid running into a circular import.
|
|
this.ngControl.valueAccessor = this;
|
|
}
|
|
}
|
|
|
|
ngOnInit() { }
|
|
|
|
ngDoCheck(): void {
|
|
if (this.ngControl) {
|
|
this.updateErrorState();
|
|
}
|
|
}
|
|
|
|
ngOnChanges(changes: SimpleChanges) {
|
|
if (changes['initialSelectedData'] && changes['initialSelectedData'].currentValue && changes['initialSelectedData'].isFirstChange && this.configuration != null) {
|
|
this._selectedItems.set(JSON.stringify(this.configuration.valueAssign != null ? this.configuration.valueAssign(this.initialSelectedData) : this.initialSelectedData), this.initialSelectedData);
|
|
this.inputValue = this._displayFn(this.initialSelectedData);
|
|
}
|
|
|
|
if (changes['configuration'] && changes['configuration'].isFirstChange) {
|
|
if (changes['initialSelectedData'] != null && this.value != null) {
|
|
if (this._valueToAssign(this.initialSelectedData) != this.value) { this.getSelectedItems(this.value); }
|
|
} else {
|
|
this.getSelectedItems(this.value);
|
|
}
|
|
}
|
|
}
|
|
|
|
getSelectedItems(value: any) {
|
|
if (value != null && !this._selectedItems.has(JSON.stringify(value)) && this.configuration) {
|
|
if (this.configuration.getSelectedItem != null) {
|
|
this.configuration.getSelectedItem(value).pipe(takeUntil(this._destroyed)).subscribe(x => {
|
|
if (x != null) {
|
|
this._selectedItems.set(JSON.stringify(this.configuration.valueAssign != null ? this.configuration.valueAssign(x) : x), x);
|
|
this.inputValue = this._displayFn(x);
|
|
}
|
|
});
|
|
} else {
|
|
this._selectedItems.set(JSON.stringify(this.configuration.valueAssign != null ? this.configuration.valueAssign(value) : value), value);
|
|
this.inputValue = this._displayFn(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
filter(query: string): Observable<any[]> {
|
|
// If loadDataOnStart is enabled and query is empty we return the initial items.
|
|
if (this.isNullOrEmpty(query) && this.loadDataOnStart) {
|
|
return this.configuration.initialItems(this.configuration.extraData) || observableOf([]);
|
|
} else if (query && query.length >= this.minFilteringChars) {
|
|
if (this.configuration.filterFn) {
|
|
return this.configuration.filterFn(query, this.configuration.extraData);
|
|
} else {
|
|
return this.configuration.initialItems(this.configuration.extraData) || observableOf([]);
|
|
}
|
|
} else {
|
|
return observableOf([]);
|
|
}
|
|
}
|
|
|
|
stringify(value: any): string {
|
|
return JSON.stringify(value);
|
|
}
|
|
|
|
isNullOrEmpty(query: string): boolean {
|
|
return typeof query !== 'string' || query === null || query.length === 0;
|
|
}
|
|
|
|
_optionSelected(event: MatAutocompleteSelectedEvent) {
|
|
this.inputValue = this._displayFn(event.option.value);
|
|
this.optionSelectedInternal(event.option.value);
|
|
}
|
|
|
|
private optionSelectedInternal(item: any) {
|
|
const newValue = this._valueToAssign(item);
|
|
|
|
//Update selected items
|
|
this._selectedItems.set(JSON.stringify(newValue), item);
|
|
|
|
this._setValue(newValue);
|
|
|
|
this.stateChanges.next();
|
|
this.optionSelected.emit(item);
|
|
}
|
|
|
|
public onKeyUp(event) {
|
|
this.inputValue = event.target.value;
|
|
// prevent filtering results if arrow were pressed
|
|
if (event.keyCode !== ENTER && (event.keyCode < 37 || event.keyCode > 40)) {
|
|
if (this.inputValue.length === 0 && this.value != null) {
|
|
this.optionSelectedInternal(null);
|
|
}
|
|
this._inputSubject.next(this.inputValue);
|
|
}
|
|
}
|
|
|
|
private _setValue(value: any) {
|
|
this.value = value;
|
|
this.pushChanges(this.value);
|
|
}
|
|
|
|
_onInputFocus() {
|
|
if (!this._items) {
|
|
this._items = this._inputSubject.pipe(
|
|
startWith(null),
|
|
debounceTime(this.requestDelay),
|
|
distinctUntilChanged(),
|
|
switchMap(query => this.filter(query)));
|
|
|
|
if (this.configuration.groupingFn) { this._groupedItems = this._items.pipe(map(items => this.configuration.groupingFn(items))); }
|
|
}
|
|
}
|
|
|
|
public onBlur($event: MouseEvent) {
|
|
if (this.value != null) {
|
|
const inputLabel = this.inputValue;
|
|
const selectedLabel = this._displayFn(this._selectedItems.get(JSON.stringify(this.value)));
|
|
if (inputLabel && selectedLabel !== inputLabel) {
|
|
this.inputValue = selectedLabel;
|
|
}
|
|
} else if (this.inputValue && this.inputValue.length > 1 && this.autocomplete.options && this.autocomplete.options.length > 0 && this.autoSelectFirstOptionOnBlur) {
|
|
this.inputValue = this._displayFn(this.autocomplete.options.first.value);
|
|
this.optionSelectedInternal(this.autocomplete.options.first.value);
|
|
}
|
|
|
|
// Clear text if not an option
|
|
else if (this.inputValue && this.inputValue.length > 1) {
|
|
this.inputValue = '';
|
|
document.getElementById((<HTMLInputElement>$event.target).id).focus();
|
|
}
|
|
}
|
|
|
|
onChange = (_: any) => { };
|
|
private _onTouched = () => { };
|
|
writeValue(value: any): void {
|
|
this.value = value;
|
|
// Update chips observable
|
|
if (value != null) { this.getSelectedItems(value); } else {
|
|
if (this.autocompleteInput && this.autocompleteInput.nativeElement && this.autocompleteInput.nativeElement.value) { this.autocompleteInput.nativeElement.value = ''; }
|
|
this.inputValue = null;
|
|
this._items = null;
|
|
}
|
|
}
|
|
pushChanges(value: any) { this.onChange(value); }
|
|
registerOnChange(fn: (_: any) => {}): void { this.onChange = fn; }
|
|
registerOnTouched(fn: () => {}): void { this._onTouched = fn; }
|
|
setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; }
|
|
setDescribedByIds(ids: string[]) { this.describedBy = ids.join(' '); }
|
|
|
|
onContainerClick(event: MouseEvent) {
|
|
event.stopPropagation();
|
|
if (this.disabled) { return; }
|
|
this._onInputFocus();
|
|
if (!this.autocomplete.isOpen) {
|
|
this.autocompleteTrigger.openPanel();
|
|
}
|
|
}
|
|
|
|
ngOnDestroy() {
|
|
this.stateChanges.complete();
|
|
this.fm.stopMonitoring(this.elRef.nativeElement);
|
|
}
|
|
|
|
//Configuration getters
|
|
_displayFn(item: any): string {
|
|
// console.log('_displayFn' + item);
|
|
if (this.configuration.displayFn && item) { return this.configuration.displayFn(item); }
|
|
return item;
|
|
}
|
|
|
|
_titleFn(item: any): string {
|
|
if (this.configuration.titleFn && item) { return this.configuration.titleFn(item); }
|
|
return item;
|
|
}
|
|
|
|
_optionTemplate(item: any): TemplateRef<any> {
|
|
if (this.configuration.optionTemplate && item) { return this.configuration.optionTemplate; }
|
|
return null;
|
|
}
|
|
|
|
_selectedValueTemplate(item: any): TemplateRef<any> {
|
|
if (this.configuration.selectedValueTemplate && item) { return this.configuration.selectedValueTemplate; }
|
|
return null;
|
|
}
|
|
|
|
_subtitleFn(item: any): string {
|
|
if (this.configuration.subtitleFn && item) { return this.configuration.subtitleFn(item); }
|
|
return null;
|
|
}
|
|
|
|
_valueToAssign(item: any): any {
|
|
if (this.configuration.valueAssign && item) { return this.configuration.valueAssign(item); }
|
|
return item;
|
|
}
|
|
|
|
get requestDelay(): number {
|
|
return this.configuration.requestDelay != null ? this.configuration.requestDelay : 400;
|
|
}
|
|
|
|
get minFilteringChars(): number {
|
|
return this.configuration.minFilteringChars != null ? this.configuration.minFilteringChars : 1;
|
|
}
|
|
|
|
get loadDataOnStart(): boolean {
|
|
return this.configuration.loadDataOnStart != null ? this.configuration.loadDataOnStart : true;
|
|
}
|
|
|
|
get autoSelectFirstOptionOnBlur(): boolean {
|
|
return this.configuration.autoSelectFirstOptionOnBlur != null ? this.configuration.autoSelectFirstOptionOnBlur : false;
|
|
}
|
|
|
|
// get forceFocus(): boolean {
|
|
// return this.configuration.forceFocus != null ? this.configuration.forceFocus : false;
|
|
// }
|
|
}
|