232 lines
7.0 KiB
TypeScript
232 lines
7.0 KiB
TypeScript
import { FocusMonitor } from '@angular/cdk/a11y';
|
|
import { COMMA, ENTER } from '@angular/cdk/keycodes';
|
|
import { Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Optional, Output, Self } from '@angular/core';
|
|
import { ControlValueAccessor, NgControl } from '@angular/forms';
|
|
import { MatAutocompleteSelectedEvent, MatFormFieldControl } from '@angular/material';
|
|
import { Observable, of as observableOf, Subject } from 'rxjs';
|
|
import { debounceTime, distinctUntilChanged, map, mergeMap, startWith, tap } from 'rxjs/operators';
|
|
import { AutoCompleteGroup } from '../auto-complete-group';
|
|
import { SingleAutoCompleteConfiguration } from './single-auto-complete-configuration';
|
|
import { switchMap } from 'rxjs/internal/operators/switchMap';
|
|
|
|
|
|
@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 implements OnInit, MatFormFieldControl<string>, ControlValueAccessor, OnDestroy {
|
|
|
|
static nextId = 0;
|
|
|
|
@Input() configuration: SingleAutoCompleteConfiguration;
|
|
// Selected Option Event
|
|
@Output() optionSelected: EventEmitter<any> = new EventEmitter();
|
|
|
|
id = `single-autocomplete-${SingleAutoCompleteComponent.nextId++}`;
|
|
stateChanges = new Subject<void>();
|
|
focused = false;
|
|
errorState = false;
|
|
controlType = 'single-autocomplete';
|
|
describedBy = '';
|
|
_inputValue: string;
|
|
_inputSubject = new Subject<string>();
|
|
loading = false;
|
|
_items: Observable<any[]>;
|
|
_groupedItems: Observable<AutoCompleteGroup[]>;
|
|
private requestDelay = 200; //ms
|
|
private minFilteringChars = 1;
|
|
private loadDataOnStart = true;
|
|
separatorKeysCodes: number[] = [ENTER, COMMA];
|
|
|
|
get empty() {
|
|
return !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._inputValue = (value && value != "") ? " " : null;
|
|
this.stateChanges.next();
|
|
}
|
|
private _selectedValue;
|
|
|
|
constructor(
|
|
private fm: FocusMonitor,
|
|
private elRef: ElementRef,
|
|
@Optional() @Self() public ngControl: NgControl) {
|
|
|
|
fm.monitor(elRef.nativeElement, true).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() {
|
|
|
|
}
|
|
|
|
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([]);
|
|
}
|
|
}
|
|
|
|
isNullOrEmpty(query: string): boolean {
|
|
return typeof query !== 'string' || query === null || query.length === 0;
|
|
}
|
|
|
|
_displayFn(item: any): string {
|
|
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;
|
|
}
|
|
|
|
_subtitleFn(item: any): string {
|
|
if (this.configuration.subtitleFn && item) { return this.configuration.subtitleFn(item); }
|
|
return null;
|
|
}
|
|
|
|
_requestDelay(): number {
|
|
return this.configuration.requestDelay || this.requestDelay;
|
|
}
|
|
|
|
_minFilteringChars(): number {
|
|
return this.configuration.minFilteringChars || this.minFilteringChars;
|
|
}
|
|
|
|
_loadDataOnStart(): boolean {
|
|
return this.configuration.loadDataOnStart || this.loadDataOnStart;
|
|
}
|
|
|
|
_optionSelected(event: MatAutocompleteSelectedEvent) {
|
|
this._setValue(this.configuration.valueAssign ? this.configuration.valueAssign(event.option.value) : event.option.value);
|
|
//this._inputValue = " ";
|
|
this.stateChanges.next();
|
|
this.optionSelected.emit(event.option.value);
|
|
}
|
|
|
|
private _setValue(value: any) {
|
|
this.value = value;
|
|
this.pushChanges(this.value);
|
|
}
|
|
|
|
_onInputFocus() {
|
|
// We set the items observable on focus to avoid the request being executed on component load.
|
|
if (!this._items) {
|
|
this._items = this._inputSubject.pipe(
|
|
startWith(null),
|
|
debounceTime(this.requestDelay),
|
|
distinctUntilChanged(),
|
|
tap(() => { this.loading = true; }),
|
|
switchMap(query => {
|
|
// If its a valid object, a selection just made and the object is set as the value of the form control. That means we should fire an extra request to the server.
|
|
if (this._isValidObject(query)) { return observableOf([]); }
|
|
|
|
// Since the object is changed we need to clear any existing selections, except for the first time.
|
|
if (query !== null) { this.pushChanges(null); }
|
|
return this.filter(query);
|
|
}),
|
|
tap(() => { this.loading = false; }));
|
|
|
|
if (this.configuration.groupingFn) { this._groupedItems = this._items.pipe(map(items => this.configuration.groupingFn(items))); }
|
|
}
|
|
}
|
|
|
|
_inputValueChange(value: string) {
|
|
//this._inputValue = value;
|
|
this._inputSubject.next(value);
|
|
this.stateChanges.next();
|
|
}
|
|
|
|
_isValidObject(value: any): boolean {
|
|
try {
|
|
if (!value) { return false; }
|
|
if (typeof value !== 'object') { JSON.parse(value); }
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
onChange = (_: any) => { };
|
|
onTouched = () => { };
|
|
writeValue(value: any): void { this.value = value || ''; }
|
|
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) {
|
|
if ((event.target as Element).tagName.toLowerCase() !== 'input') {
|
|
this.elRef.nativeElement.querySelector('input').focus();
|
|
}
|
|
}
|
|
|
|
chipRemove(): void {
|
|
this._setValue(null);
|
|
}
|
|
|
|
autoCompleteDisplayFn() {
|
|
return (val) => "";
|
|
}
|
|
|
|
ngOnDestroy() {
|
|
this.stateChanges.complete();
|
|
this.fm.stopMonitoring(this.elRef.nativeElement);
|
|
}
|
|
}
|