Fix bug where on long click, blur value was added in the list of chips.

This commit is contained in:
Konstantinos Triantafyllou 2023-05-03 12:34:17 +03:00
parent 5d5ebe7a55
commit 0d9b6f9622
2 changed files with 164 additions and 77 deletions

View File

@ -17,7 +17,6 @@
[placeholder]="placeholder" [matAutocomplete]="autocomplete" [value]="inputValue" [placeholder]="placeholder" [matAutocomplete]="autocomplete" [value]="inputValue"
(keyup)="onKeyUp($event)" (keydown)="onKeyDown($event)" [disabled]="disabled" (focus)="_onInputFocus()" (keyup)="onKeyUp($event)" (keydown)="onKeyDown($event)" [disabled]="disabled" (focus)="_onInputFocus()"
(blur)="onBlur($event)" [matChipInputFor]="chipList" [matChipInputSeparatorKeyCodes]="separatorKeysCodes" (blur)="onBlur($event)" [matChipInputFor]="chipList" [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="autoSelectFirstOptionOnBlur"
(matChipInputTokenEnd)="_addItem($event)" (matChipInputTokenEnd)="_addItem($event)"
[matAutocompleteConnectedTo]="origin"> [matAutocompleteConnectedTo]="origin">
<!-- The attribute autocomplete="nope", set by downshift, is ignored in Chrome 67 and Opera 54 (latest at the time of writing) <!-- The attribute autocomplete="nope", set by downshift, is ignored in Chrome 67 and Opera 54 (latest at the time of writing)
@ -29,7 +28,7 @@
<mat-autocomplete #autocomplete="matAutocomplete" [displayWith]="_displayFn.bind(this)" (optionSelected)="_optionSelected($event)"> <mat-autocomplete #autocomplete="matAutocomplete" [displayWith]="_displayFn.bind(this)" (optionSelected)="_optionSelected($event)">
<span *ngIf="_groupedItems"> <span *ngIf="_groupedItems">
<mat-optgroup *ngFor="let group of _groupedItems | async" [label]="group.title"> <mat-optgroup *ngFor="let group of _groupedItems | async" [label]="group.title">
<mat-option *ngFor="let item of group.items" [value]="item" [class.two-line-mat-option]="_subtitleFn(item) && !_optionTemplate(item)"> <mat-option *ngFor="let item of group.items" [value]="item" (mousedown)="selected = true;" (mouseup)="selected = false;" [class.two-line-mat-option]="_subtitleFn(item) && !_optionTemplate(item)">
<!-- <img style="vertical-align:middle;" aria-hidden src="{{state.flag}}" height="25" /> --> <!-- <img style="vertical-align:middle;" aria-hidden src="{{state.flag}}" height="25" /> -->
<ng-template #cellTemplate *ngIf="_optionTemplate(item)" [ngTemplateOutlet]="_optionTemplate(item)" [ngTemplateOutletContext]="{ <ng-template #cellTemplate *ngIf="_optionTemplate(item)" [ngTemplateOutlet]="_optionTemplate(item)" [ngTemplateOutletContext]="{
item: item item: item
@ -55,7 +54,7 @@
<span *ngIf="!_groupedItems"> <span *ngIf="!_groupedItems">
<ng-container *ngIf="_items | async as autocompleteItems; else loading"> <ng-container *ngIf="_items | async as autocompleteItems; else loading">
<ng-container *ngIf="autocompleteItems.length"> <ng-container *ngIf="autocompleteItems.length">
<mat-option *ngFor="let item of autocompleteItems" [value]="item" [class.two-line-mat-option]="_subtitleFn(item) && !_optionTemplate(item)"> <mat-option *ngFor="let item of autocompleteItems" [value]="item" (mousedown)="selected = true;" (mouseup)="selected = false;" [class.two-line-mat-option]="_subtitleFn(item) && !_optionTemplate(item)">
<!-- <img style="vertical-align:middle;" aria-hidden src="{{state.flag}}" height="25" /> --> <!-- <img style="vertical-align:middle;" aria-hidden src="{{state.flag}}" height="25" /> -->
<ng-template #cellTemplate *ngIf="_optionTemplate(item)" [ngTemplateOutlet]="_optionTemplate(item)" [ngTemplateOutletContext]="{ <ng-template #cellTemplate *ngIf="_optionTemplate(item)" [ngTemplateOutlet]="_optionTemplate(item)" [ngTemplateOutletContext]="{
item: item item: item

View File

@ -1,13 +1,30 @@
import {FocusMonitor} from '@angular/cdk/a11y'; import {FocusMonitor} from '@angular/cdk/a11y';
import {BACKSPACE, ENTER} from '@angular/cdk/keycodes'; 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 {
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 {ControlValueAccessor, FormGroupDirective, NgControl, NgForm} from '@angular/forms';
import {MatChipInputEvent} from '@angular/material/chips'; import {MatChipInputEvent} from '@angular/material/chips';
import {ErrorStateMatcher, mixinErrorState} from '@angular/material/core'; import {ErrorStateMatcher, mixinErrorState} from '@angular/material/core';
import {MatAutocomplete, MatAutocompleteSelectedEvent, MatAutocompleteTrigger} from '@angular/material/autocomplete'; import {MatAutocomplete, MatAutocompleteSelectedEvent, MatAutocompleteTrigger} from '@angular/material/autocomplete';
import {MatFormFieldControl} from '@angular/material/form-field'; import {MatFormFieldControl} from '@angular/material/form-field';
import {AutoCompleteGroup} from '@app/library/auto-complete/auto-complete-group'; import {AutoCompleteGroup} from '@app/library/auto-complete/auto-complete-group';
import { MultipleAutoCompleteConfiguration } from '@app/library/auto-complete/multiple/multiple-auto-complete-configuration'; import {
MultipleAutoCompleteConfiguration
} from '@app/library/auto-complete/multiple/multiple-auto-complete-configuration';
import {BaseComponent} from '@common/base/base.component'; import {BaseComponent} from '@common/base/base.component';
import {isNullOrUndefined} from '@swimlane/ngx-datatable'; import {isNullOrUndefined} from '@swimlane/ngx-datatable';
import {BehaviorSubject, combineLatest, Observable, of as observableOf, Subject, Subscription} from 'rxjs'; import {BehaviorSubject, combineLatest, Observable, of as observableOf, Subject, Subscription} from 'rxjs';
@ -19,8 +36,11 @@ export class CustomComponentBase extends BaseComponent {
public _parentForm: NgForm, public _parentForm: NgForm,
public _parentFormGroup: FormGroupDirective, public _parentFormGroup: FormGroupDirective,
public ngControl: NgControl public ngControl: NgControl
) { super(); } ) {
super();
} }
}
export const _CustomComponentMixinBase = mixinErrorState(CustomComponentBase); export const _CustomComponentMixinBase = mixinErrorState(CustomComponentBase);
@Component({ @Component({
@ -37,10 +57,14 @@ export class MultipleAutoCompleteComponent extends _CustomComponentMixinBase imp
@ViewChild('autocompleteInput', {static: true}) autocompleteInput: ElementRef; @ViewChild('autocompleteInput', {static: true}) autocompleteInput: ElementRef;
@Input() @Input()
get configuration(): MultipleAutoCompleteConfiguration { return this._configuration; } get configuration(): MultipleAutoCompleteConfiguration {
return this._configuration;
}
set configuration(configuration: MultipleAutoCompleteConfiguration) { set configuration(configuration: MultipleAutoCompleteConfiguration) {
this._configuration = configuration; this._configuration = configuration;
} }
private _configuration: MultipleAutoCompleteConfiguration; private _configuration: MultipleAutoCompleteConfiguration;
@Input() @Input()
@ -72,46 +96,66 @@ export class MultipleAutoCompleteComponent extends _CustomComponentMixinBase imp
_groupedItems: Observable<AutoCompleteGroup[]>; _groupedItems: Observable<AutoCompleteGroup[]>;
selectable = false; selectable = false;
removable = true; removable = true;
selected: boolean = false;
get empty() { return (!this.value || this.value.length === 0) && (!this.inputValue || this.inputValue.length === 0); } get empty() {
return (!this.value || this.value.length === 0) && (!this.inputValue || this.inputValue.length === 0);
}
get shouldLabelFloat() { return this.focused || !this.empty; } get shouldLabelFloat() {
return this.focused || !this.empty;
}
@Input() minLength: number = 0; @Input() minLength: number = 0;
@Input() showNoResultsLabel: boolean = true; @Input() showNoResultsLabel: boolean = true;
@Input() hidePlaceholder: boolean = false; @Input() hidePlaceholder: boolean = false;
@Input() @Input()
get placeholder() { return this._placeholder; } get placeholder() {
return this._placeholder;
}
set placeholder(placeholder) { set placeholder(placeholder) {
this._placeholder = placeholder; this._placeholder = placeholder;
this.stateChanges.next(); this.stateChanges.next();
} }
private _placeholder: string; private _placeholder: string;
@Input() @Input()
get required() { return this._required; } get required() {
return this._required;
}
set required(req) { set required(req) {
this._required = !!(req); this._required = !!(req);
this.stateChanges.next(); this.stateChanges.next();
} }
private _required = false; private _required = false;
@Input() @Input()
get disabled() { return this._disabled; } get disabled() {
return this._disabled;
}
set disabled(dis) { set disabled(dis) {
this._disabled = !!(dis); this._disabled = !!(dis);
this.stateChanges.next(); this.stateChanges.next();
} }
private _disabled = false; private _disabled = false;
@Input() @Input()
get value(): any | null { get value(): any | null {
return this._selectedValue; return this._selectedValue;
} }
set value(value: any | null) { set value(value: any | null) {
this._selectedValue = (value != null && value.length === 0) ? null : value; this._selectedValue = (value != null && value.length === 0) ? null : value;
this.stateChanges.next(); this.stateChanges.next();
} }
private _selectedValue; private _selectedValue;
constructor( constructor(
@ -138,7 +182,7 @@ export class MultipleAutoCompleteComponent extends _CustomComponentMixinBase imp
ngOnInit() { ngOnInit() {
this.valueAssignSubscription = combineLatest(this.valueOnBlur.asObservable(), this.onSelectAutoCompleteValue.asObservable()) this.valueAssignSubscription = combineLatest(this.valueOnBlur.asObservable(), this.onSelectAutoCompleteValue.asObservable())
.pipe(debounceTime(200)) .pipe(debounceTime(100))
.subscribe(latest => { .subscribe(latest => {
const fromBlur = latest[0]; const fromBlur = latest[0];
const fromAutoComplete = latest[1]; const fromAutoComplete = latest[1];
@ -158,7 +202,7 @@ export class MultipleAutoCompleteComponent extends _CustomComponentMixinBase imp
if (!isNullOrUndefined(fromBlur)) { if (!isNullOrUndefined(fromBlur)) {
this.optionSelectedInternal(fromBlur); this.optionSelectedInternal(fromBlur);
this.autocompleteTrigger.closePanel();
// consumed and flush // consumed and flush
this.onSelectAutoCompleteValue.next(null); this.onSelectAutoCompleteValue.next(null);
this.valueOnBlur.next(null); this.valueOnBlur.next(null);
@ -273,14 +317,20 @@ export class MultipleAutoCompleteComponent extends _CustomComponentMixinBase imp
tap(query => this.queryValue = query), tap(query => this.queryValue = query),
switchMap(query => (!this.minLength || (query && query.length >= this.minLength)) ? this.filter(query) : observableOf([]))); 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))); } if (this.configuration.groupingFn) {
this._groupedItems = this._items.pipe(map(items => this.configuration.groupingFn(items)));
}
} }
} }
public onBlur($event: FocusEvent) { public onBlur($event: FocusEvent) {
if (!this.selected) {
if (this.inputValue && this.inputValue.length > 1 && this.autocomplete.options && this.autocomplete.options.length > 0 && this.autoSelectFirstOptionOnBlur) { 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.optionSelectedInternal(this.autocomplete.options.first.value);
this.valueOnBlur.next(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 // Clear text if not an option
@ -291,22 +341,42 @@ export class MultipleAutoCompleteComponent extends _CustomComponentMixinBase imp
} }
onChange = (_: any) => { }; onChange = (_: any) => {
private _onTouched = () => { }; };
private _onTouched = () => {
};
writeValue(value: any): void { writeValue(value: any): void {
this.value = Array.isArray(value) ? value : null; this.value = Array.isArray(value) ? value : null;
// Update chips observable // Update chips observable
this.getSelectedItems(value); this.getSelectedItems(value);
} }
pushChanges(value: any) { this.onChange(value); }
registerOnChange(fn: (_: any) => {}): void { this.onChange = fn; } pushChanges(value: any) {
registerOnTouched(fn: () => {}): void { this._onTouched = fn; } this.onChange(value);
setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; } }
setDescribedByIds(ids: string[]) { this.describedBy = ids.join(' '); }
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) { onContainerClick(event: MouseEvent) {
event.stopPropagation(); event.stopPropagation();
if (this.disabled) { return; } if (this.disabled) {
return;
}
this._onInputFocus(); this._onInputFocus();
if (!this.autocomplete.isOpen) { if (!this.autocomplete.isOpen) {
this.autocompleteTrigger.openPanel(); this.autocompleteTrigger.openPanel();
@ -325,32 +395,44 @@ export class MultipleAutoCompleteComponent extends _CustomComponentMixinBase imp
//Configuration getters //Configuration getters
_displayFn(item: any): string { _displayFn(item: any): string {
// console.log('_displayFn' + item); // console.log('_displayFn' + item);
if (this.configuration.displayFn && item) { return this.configuration.displayFn(item); } if (this.configuration.displayFn && item) {
return this.configuration.displayFn(item);
}
return item; return item;
} }
_titleFn(item: any): string { _titleFn(item: any): string {
if (this.configuration.titleFn && item) { return this.configuration.titleFn(item); } if (this.configuration.titleFn && item) {
return this.configuration.titleFn(item);
}
return item; return item;
} }
_optionTemplate(item: any): TemplateRef<any> { _optionTemplate(item: any): TemplateRef<any> {
if (this.configuration.optionTemplate && item) { return this.configuration.optionTemplate; } if (this.configuration.optionTemplate && item) {
return this.configuration.optionTemplate;
}
return null; return null;
} }
_selectedValueTemplate(item: any): TemplateRef<any> { _selectedValueTemplate(item: any): TemplateRef<any> {
if (this.configuration.selectedValueTemplate && item) { return this.configuration.selectedValueTemplate; } if (this.configuration.selectedValueTemplate && item) {
return this.configuration.selectedValueTemplate;
}
return null; return null;
} }
_subtitleFn(item: any): string { _subtitleFn(item: any): string {
if (this.configuration.subtitleFn && item) { return this.configuration.subtitleFn(item); } if (this.configuration.subtitleFn && item) {
return this.configuration.subtitleFn(item);
}
return null; return null;
} }
_valueToAssign(item: any): any { _valueToAssign(item: any): any {
if (this.configuration.valueAssign && item) { return this.configuration.valueAssign(item); } if (this.configuration.valueAssign && item) {
return this.configuration.valueAssign(item);
}
return item; return item;
} }
@ -373,6 +455,7 @@ export class MultipleAutoCompleteComponent extends _CustomComponentMixinBase imp
get popupItemActionIcon(): string { get popupItemActionIcon(): string {
return this.configuration.popupItemActionIcon != null ? this.configuration.popupItemActionIcon : ''; return this.configuration.popupItemActionIcon != null ? this.configuration.popupItemActionIcon : '';
} }
get appendClassToItem(): { class: string, applyFunc: (item: any) => boolean }[] { get appendClassToItem(): { class: string, applyFunc: (item: any) => boolean }[] {
return this.configuration.appendClassToItem !== null ? this.configuration.appendClassToItem : null; return this.configuration.appendClassToItem !== null ? this.configuration.appendClassToItem : null;
} }
@ -394,7 +477,9 @@ export class MultipleAutoCompleteComponent extends _CustomComponentMixinBase imp
} }
_removeSelectedItem(item: any, event: MouseEvent): void { _removeSelectedItem(item: any, event: MouseEvent): void {
if (event != null) { event.stopPropagation(); } if (event != null) {
event.stopPropagation();
}
const valueToDelete = this._valueToAssign(item); 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.value = this.value.filter(x => this.stringify(x) !== this.stringify(valueToDelete)); //TODO, maybe we need to implement equality here differently.
this.optionRemoved.emit(item); this.optionRemoved.emit(item);
@ -407,12 +492,15 @@ export class MultipleAutoCompleteComponent extends _CustomComponentMixinBase imp
} }
_optionActionClick(item: any, event: MouseEvent): void { _optionActionClick(item: any, event: MouseEvent): void {
if (event != null) { event.stopPropagation(); } if (event != null) {
event.stopPropagation();
}
this.optionActionClicked.emit(item); this.optionActionClicked.emit(item);
} }
private readonly empyObj = {}; private readonly empyObj = {};
computeClass(value: any) { computeClass(value: any) {
if (!(this.appendClassToItem && this.appendClassToItem.length)) { if (!(this.appendClassToItem && this.appendClassToItem.length)) {
return this.empyObj; return this.empyObj;