Something went wrong on our end
-
Alex ORLUC authoredAlex ORLUC authored
select-search.component.ts 14.45 KiB
import {
AfterViewInit, ChangeDetectorRef,
Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, QueryList,
ViewChild,
Renderer2,
Output
} from '@angular/core';
import { ControlValueAccessor, FormControl } from '@angular/forms';
import { MatOption } from '@angular/material/core';
import { MatSelect } from '@angular/material/select';
import { take, takeUntil, startWith, map, tap } from 'rxjs/operators';
import { Subject, ReplaySubject, Observable } from 'rxjs';
import { LatinisePipe } from 'ngx-pipes';
import { TranslateService } from '@ngx-translate/core';
import { AppService } from '@service/app.service';
import { SortPipe } from '../../plugins/sorting.pipe';
import { FunctionsService } from '@service/functions.service';
@Component({
selector: 'plugin-select-search',
templateUrl: 'select-search.component.html',
styleUrls: ['select-search.component.scss', '../../app/indexation/indexing-form/indexing-form.component.scss'],
providers: [SortPipe]
})
export class PluginSelectSearchComponent implements OnInit, OnDestroy, AfterViewInit, ControlValueAccessor {
/** Label of the search placeholder */
@Input() placeholderLabel = this.translate.instant('lang.chooseValue');
@Input() formControlSelect: FormControl = new FormControl();
@Input() datas: any = [];
@Input() returnValue: 'id' | 'object' = 'id';
@Input() label: string;
@Input() id: string = '';
@Input() showResetOption: boolean;
@Input() showLabel: boolean = false;
@Input() required: boolean = false;
@Input() hideErrorDesc: boolean = true;
@Input() multiple: boolean = false;
@Input() optGroupTarget: string = null;
/**
* ex : [ { id : 'group1' , label: 'Group 1'} ]
*/
@Input() optGroupList: any = null;
/**
* ex : {class:'fa-circle', color:'#fffff', title: 'foo'}
*/
@Input() suffixIcon: any = null;
@Input() class: string = 'input-form';
/**
* Catch external event after select an element in autocomplete
*/
@Output() afterSelected = new EventEmitter();
@Output() afterOpened = new EventEmitter();
/** Reference to the search input field */
@ViewChild('searchSelectInput', { read: ElementRef, static: true }) searchSelectInput: ElementRef;
@ViewChild('test', { static: true }) matSelect: MatSelect;
public filteredDatas: Observable<string[]>;
public filteredDatasMulti: ReplaySubject<any[]> = new ReplaySubject<any[]>(1);
/** Reference to the MatSelect options */
public _options: QueryList<MatOption>;
/** Previously selected values when using <mat-select [multiple]="true">*/
private previousSelectedValues: any[];
/** Whether the backdrop class has been set */
private overlayClassSet = false;
/** Event that emits when the current value changes */
private change = new EventEmitter<string>();
/** Subject that emits when the component has been destroyed. */
private _onDestroy = new Subject<void>();
formControlSearch = new FormControl();
selected: any[] = [];
/** Current search value */
get value(): string {
return this._value;
}
private _value: string;
onChange: Function = (_: any) => { };
onTouched: Function = (_: any) => { };
constructor(
public translate: TranslateService,
private latinisePipe: LatinisePipe,
private changeDetectorRef: ChangeDetectorRef,
private renderer: Renderer2,
public appService: AppService,
public functions: FunctionsService,
private sortPipe: SortPipe) { }
ngOnInit() {
if (this.multiple) {
this.matSelect.compareWith = (item1: any, item2: any) => item1 && item2 ? item1.id === item2.id : item1 === item2;
}
if (this.optGroupList !== null) {
this.initOptGroups();
}
// set custom panel class
const panelClass = 'mat-select-search-panel';
if (this.matSelect.panelClass) {
if (Array.isArray(this.matSelect.panelClass)) {
this.matSelect.panelClass.push(panelClass);
} else if (typeof this.matSelect.panelClass === 'string') {
this.matSelect.panelClass = [this.matSelect.panelClass, panelClass];
} else if (typeof this.matSelect.panelClass === 'object') {
this.matSelect.panelClass[panelClass] = true;
}
} else {
this.matSelect.panelClass = panelClass;
}
// when the select dropdown panel is opened or closed
this.matSelect.openedChange
.pipe(takeUntil(this._onDestroy))
.subscribe((opened) => {
if (opened) {
// focus the search field when opening
if (!this.appService.getViewMode()) {
this._focus();
}
} else {
// clear it when closing
// this._reset();
this.formControlSearch.reset();
}
});
// set the first item active after the options changed
this.matSelect.openedChange
.pipe(take(1))
.pipe(takeUntil(this._onDestroy))
.subscribe(() => {
this._options = this.matSelect.options;
this._options.changes
.pipe(takeUntil(this._onDestroy))
.subscribe(() => {
const keyManager = this.matSelect._keyManager;
if (keyManager && this.matSelect.panelOpen) {
// avoid "expression has been changed" error
setTimeout(() => {
keyManager.setFirstItemActive();
});
}
});
});
setTimeout(() => {
let group = '';
let index = 1;
this.datas = JSON.parse(JSON.stringify(this.datas));
this.datas.forEach((element: any) => {
if (element.isTitle) {
group = `group_${index}`;
element.id = group;
index++;
} else {
element.group = group;
}
});
this.filteredDatas = this.formControlSearch.valueChanges
.pipe(
startWith(''),
map(value => this._filter(value))
);
}, 200);
// this.initMultipleHandling();
}
initOptGroups() {
this.datas.unshift({ id: 0, label: 'toto', disabled: true });
let tmpArr = [];
this.optGroupList = this.sortPipe.transform(this.optGroupList, 'label');
this.optGroupList.forEach(group => {
tmpArr.push({ id: group.id, label: group.label, disabled: true });
tmpArr = tmpArr.concat(this.datas.filter(data => data[this.optGroupTarget] === group.id).map(data => {
return {
...data,
title: data.label,
label: '  ' + data.label
};
}));
});
this.datas = tmpArr;
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
ngAfterViewInit() {
if (this.datas.length > 5) {
this.setOverlayClass();
}
}
/**
* Handles the key down event with MatSelect.
* Allows e.g. selecting with enter key, navigation with arrow keys, etc.
* @param {KeyboardEvent} event
* @private
*/
_handleKeydown(event: KeyboardEvent) {
if (event.keyCode === 32) {
// do not propagate spaces to MatSelect, as this would select the currently active option
event.stopPropagation();
}
}
writeValue(value: string) {
const valueChanged = value !== this._value;
if (valueChanged) {
this._value = value;
this.change.emit(value);
}
}
onInputChange(value: any) {
const valueChanged = value !== this._value;
if (valueChanged) {
this._value = value;
this.onChange(value);
this.change.emit(value);
}
}
onBlur(value: string) {
this.writeValue(value);
this.onTouched();
}
registerOnChange(fn: Function) {
this.onChange = fn;
}
registerOnTouched(fn: Function) {
this.onTouched = fn;
}
/**
* Focuses the search input field
* @private
*/
public _focus() {
// save and restore scrollTop of panel, since it will be reset by focus()
// note: this is hacky
const panel = this.matSelect.panel.nativeElement;
const scrollTop = panel.scrollTop;
// focus
if (this.datas.length > 5) {
this.renderer.selectRootElement('#searchSelectInput').focus();
}
panel.scrollTop = scrollTop;
}
/**
* Resets the current search value
* @param {boolean} focus whether to focus after resetting
* @private
*/
public _reset(focus?: boolean) {
this.formControlSearch.reset();
this.renderer.selectRootElement('#searchSelectInput').focus();
}
/**
* Sets the overlay class to correct offsetY
* so that the selected option is at the position of the select box when opening
*/
private setOverlayClass() {
if (this.overlayClassSet) {
return;
}
const overlayClass = 'cdk-overlay-pane-select-search';
this.matSelect.overlayDir.attach
.pipe(takeUntil(this._onDestroy))
.subscribe(() => {
// note: this is hacky, but currently there is no better way to do this
if (this.searchSelectInput !== undefined) {
this.searchSelectInput.nativeElement.parentElement.parentElement
.parentElement.parentElement.parentElement.classList.add(overlayClass);
}
});
this.overlayClassSet = true;
}
/**
* Initializes handling <mat-select [multiple]="true">
* Note: to improve this code, mat-select should be extended to allow disabling resetting the selection while filtering.
*/
private initMultipleHandling() {
// if <mat-select [multiple]="true">
// store previously selected values and restore them when they are deselected
// because the option is not available while we are currently filtering
this.matSelect.valueChange
.pipe(takeUntil(this._onDestroy))
.subscribe((values) => {
if (this.matSelect.multiple) {
let restoreSelectedValues = false;
if (this._value && this._value.length
&& this.previousSelectedValues && Array.isArray(this.previousSelectedValues)) {
if (!values || !Array.isArray(values)) {
values = [];
}
const optionValues = this.matSelect.options.map(option => option.value);
this.previousSelectedValues.forEach(previousValue => {
if (values.indexOf(previousValue) === -1 && optionValues.indexOf(previousValue) === -1) {
// if a value that was selected before is deselected and not found in the options, it was deselected
// due to the filtering, so we restore it.
values.push(previousValue);
restoreSelectedValues = true;
}
});
}
if (restoreSelectedValues) {
this.matSelect._onChange(values);
}
this.previousSelectedValues = values;
}
});
}
private _filter(value: string, showSelectedValues: boolean = false): string[] {
if (value === '__SELECTED') {
return this.returnValue === 'id' ? this.datas.filter((option: any) => this.formControlSelect.value.indexOf(option['id']) > -1) : this.datas.filter((option: any) => this.formControlSelect.value.map((val: any) => val.id).indexOf(option['id']) > -1);
} else if (typeof value === 'string' && value !== '') {
const filterValue = this.latinisePipe.transform(value.toLowerCase());
const group = this.datas.filter((option: any) => option['isTitle'] && this.latinisePipe.transform(option['label'].toLowerCase()).includes(filterValue)).map((opt: any) => opt.id);
return this.datas.filter((option: any) => (option['isTitle'] && group.indexOf(option['id']) > -1) || (group.indexOf(option['group']) > -1 || this.latinisePipe.transform(option['label'].toLowerCase()).includes(filterValue)));
} else {
return this.datas;
}
}
launchEvent(ev: any) {
if (this.selected.length > 0) {
const ids = new Set(this.formControlSelect.value.map(d => d.id));
const merged = [...this.formControlSelect.value, ...this.selected.filter(d => !ids.has(d.id))];
this.formControlSelect.setValue(merged);
}
if (this.afterSelected !== undefined) {
this.afterSelected.emit(ev.value);
}
}
selectChange(ev: any) {
if (this.multiple && ev.isUserInput) {
if (ev.source._selected) {
this.selected = this.formControlSelect.value;
} else {
this.selected = this.selected.length > 0 ? this.selected.filter((val: any) => val.id !== ev.source.value.id) : [];
}
}
}
getErrorMsg(error: any) {
if (error.required !== undefined) {
return this.translate.instant('lang.requiredField');
} else if (error.pattern !== undefined || error.email !== undefined) {
return this.translate.instant('lang.badFormat');
} else {
return 'unknow validator';
}
}
getFirstDataLabel() {
return this.formControlSelect.value[0].label.replace(/ /g, '');
// return this.returnValue === 'id' ? this.formControlSelect.value[0].label.replace(/\u00a0/g, '') : this.formControlSelect.value.map((item: any) => item !== null ? item.label : this.translate.instant('lang.emptyValue'))[0];
}
emptyData() {
return this.returnValue === 'id' ? null : {id: null, label : this.translate.instant('lang.emptyValue')};
}
}