argos/dmp-frontend/src/app/library/auto-complete/multiple/multiple-auto-complete.comp...

520 lines
15 KiB
TypeScript

import {FocusMonitor} from '@angular/cdk/a11y';
import {BACKSPACE, 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 {MatChipInputEvent} from '@angular/material/chips';
import {ErrorStateMatcher, mixinErrorState} from '@angular/material/core';
import {MatAutocomplete, MatAutocompleteSelectedEvent, MatAutocompleteTrigger} from '@angular/material/autocomplete';
import {MatFormFieldControl} from '@angular/material/form-field';
import {AutoCompleteGroup} from '@app/library/auto-complete/auto-complete-group';
import {
MultipleAutoCompleteConfiguration
} from '@app/library/auto-complete/multiple/multiple-auto-complete-configuration';
import {BaseComponent} from '@common/base/base.component';
import {isNullOrUndefined} from '@swimlane/ngx-datatable';
import {BehaviorSubject, combineLatest, Observable, of as observableOf, Subject, Subscription} from 'rxjs';
import {debounceTime, distinctUntilChanged, map, startWith, takeUntil, switchMap, tap} 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-multiple-auto-complete',
templateUrl: './multiple-auto-complete.component.html',
styleUrls: ['./multiple-auto-complete.component.scss'],
providers: [{provide: MatFormFieldControl, useExisting: MultipleAutoCompleteComponent}]
})
export class MultipleAutoCompleteComponent 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()
get configuration(): MultipleAutoCompleteConfiguration {
return this._configuration;
}
set configuration(configuration: MultipleAutoCompleteConfiguration) {
this._configuration = configuration;
}
private _configuration: MultipleAutoCompleteConfiguration;
@Input()
separatorKeysCodes = [];
// Selected Option Event
@Output() optionSelected: EventEmitter<any> = new EventEmitter();
@Output() optionRemoved: EventEmitter<any> = new EventEmitter();
@Output() optionActionClicked: EventEmitter<any> = new EventEmitter();
id = `multiple-autocomplete-${MultipleAutoCompleteComponent.nextId++}`;
stateChanges = new Subject<void>();
valueOnBlur = new BehaviorSubject<any>(null);
onSelectAutoCompleteValue = new BehaviorSubject<any>(null);
valueAssignSubscription: Subscription;
queryValue: string = "";
focused = false;
controlType = 'multiple-autocomplete';
describedBy = '';
inputValue = '';
_inputSubject = new Subject<string>();
_items: Observable<any[]>;
_selectedItems: Map<string, any> = new Map<any, any>();
_groupedItems: Observable<AutoCompleteGroup[]>;
selectable = false;
removable = true;
selected: boolean = false;
get empty() {
return (!this.value || this.value.length === 0) && (!this.inputValue || this.inputValue.length === 0);
}
get shouldLabelFloat() {
return this.focused || !this.empty;
}
@Input() minLength: number = 0;
@Input() showNoResultsLabel: boolean = true;
@Input() hidePlaceholder: boolean = false;
@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 != null && value.length === 0) ? null : 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() {
this.valueAssignSubscription = combineLatest(this.valueOnBlur.asObservable(), this.onSelectAutoCompleteValue.asObservable())
.pipe(debounceTime(100))
.subscribe(latest => {
const fromBlur = latest[0];
const fromAutoComplete = latest[1];
if (isNullOrUndefined(fromBlur) && isNullOrUndefined(fromAutoComplete)) {
return;
}
//higher precedence
if (!isNullOrUndefined(fromAutoComplete)) {
this.optionSelectedInternal(fromAutoComplete);
// consumed and flush
this.onSelectAutoCompleteValue.next(null);
this.valueOnBlur.next(null);
return;
}
if (!isNullOrUndefined(fromBlur)) {
this.optionSelectedInternal(fromBlur);
// consumed and flush
this.onSelectAutoCompleteValue.next(null);
this.valueOnBlur.next(null);
return;
}
});
}
ngDoCheck(): void {
if (this.ngControl) {
this.updateErrorState();
}
}
ngOnChanges(changes: SimpleChanges) {
if (changes['configuration'] && changes['configuration'].isFirstChange) {
this.getSelectedItems(this.value);
}
if (changes['value'] && !changes['value'].isFirstChange()) {
this.getSelectedItems(this.value);
}
}
getSelectedItems(value: any) {
if (value != null && Array.isArray(value) && this.configuration) {
const newSelections = value.filter(x => !this._selectedItems.has(this.stringify(x)));
if (newSelections.length > 0 && this.configuration.getSelectedItems != null) {
this.configuration.getSelectedItems(newSelections).pipe(takeUntil(this._destroyed)).subscribe(x => {
x.forEach(element => {
this._selectedItems.set(this.stringify(this.configuration.valueAssign != null ? this.configuration.valueAssign(element) : element), element);
});
});
} else {
newSelections.forEach(element => {
this._selectedItems.set(this.stringify(this.configuration.valueAssign != null ? this.configuration.valueAssign(element) : element), element);
});
}
}
}
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 typeof (value) == 'string' ? value : JSON.stringify(value);
}
isNullOrEmpty(query: string): boolean {
return typeof query !== 'string' || query === null || query.length === 0;
}
_optionSelected(event: MatAutocompleteSelectedEvent) {
// this.optionSelectedInternal(event.option.value);
this.onSelectAutoCompleteValue.next(event.option.value);
this.autocompleteInput.nativeElement.value = '';
}
private optionSelectedInternal(item: any) {
const newValue = this._valueToAssign(item);
//Update selected items
this._selectedItems.set(this.stringify(newValue), item);
const newValueArray = (Array.isArray(this.value) ? this.value : []);
newValueArray.push(newValue);
this._setValue(newValueArray);
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)) {
this._inputSubject.next(this.inputValue);
}
// if (event.keyCode !== ENTER && (event.keyCode < 37 || event.keyCode > 40) && event.keyCode !== COMMA) {
// this._inputSubject.next(this.inputValue);
// }
}
public onKeyDown(event) {
if (event.keyCode === BACKSPACE && event.target.value.length === 0 && this._selectedItems.size > 0) {
this._removeSelectedItem(Array.from(this._selectedItems.values()).pop(), event);
}
}
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(),
distinctUntilChanged(),
tap(query => this.queryValue = query),
switchMap(query => (!this.minLength || (query && query.length >= this.minLength)) ? this.filter(query) : observableOf([])));
if (this.configuration.groupingFn) {
this._groupedItems = this._items.pipe(map(items => this.configuration.groupingFn(items)));
}
}
}
public onBlur($event: FocusEvent) {
if (!this.selected) {
if (this.inputValue && this.inputValue.length > 1 && this.autocomplete.options && this.autocomplete.options.length > 0 && this.autoSelectFirstOptionOnBlur) {
// this.optionSelectedInternal(this.autocomplete.options.first.value);
this.valueOnBlur.next(this.autocomplete.options.first.value);
} else if (this.inputValue && this.inputValue.length > 1 && this.autoSelectFirstOptionOnBlur) {
this.valueOnBlur.next(this.inputValue);
}
}
// Clear text if not an option
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 = Array.isArray(value) ? value : null;
// Update chips observable
this.getSelectedItems(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) {
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);
if (this.valueAssignSubscription) {
this.valueAssignSubscription.unsubscribe();
this.valueAssignSubscription = null;
}
}
//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 : 600;
}
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 popupItemActionIcon(): string {
return this.configuration.popupItemActionIcon != null ? this.configuration.popupItemActionIcon : '';
}
get appendClassToItem(): { class: string, applyFunc: (item: any) => boolean }[] {
return this.configuration.appendClassToItem !== null ? this.configuration.appendClassToItem : null;
}
//Chip Functions
_addItem(event: MatChipInputEvent): void {
const input = event.input;
const value = event.value;
// Add our fruit
if (value.trim()) {
// this.optionSelectedInternal(value);
this.valueOnBlur.next(value);
}
// Reset the input value
if (input) {
this.inputValue = '';
}
//this.inputFormControl.setValue(null);
}
_removeSelectedItem(item: any, event: MouseEvent): void {
if (event != null) {
event.stopPropagation();
}
const valueToDelete = this._valueToAssign(item);
this.value = this.value.filter(x => this.stringify(x) !== this.stringify(valueToDelete)); //TODO, maybe we need to implement equality here differently.
this.optionRemoved.emit(item);
//Update chips
this._selectedItems.delete(this.stringify(valueToDelete));
this.autocompleteInput.nativeElement.focus();
this.pushChanges(this.value);
}
_optionActionClick(item: any, event: MouseEvent): void {
if (event != null) {
event.stopPropagation();
}
this.optionActionClicked.emit(item);
}
private readonly empyObj = {};
computeClass(value: any) {
if (!(this.appendClassToItem && this.appendClassToItem.length)) {
return this.empyObj;
}
const classes = this.appendClassToItem.filter(e => e.applyFunc(value));
return classes.reduce((r, current) => {
r[current.class] = true;
return r;
}, {});
}
}