Skip to content
Snippets Groups Projects
Verified Commit 1dfc78f3 authored by Alex ORLUC's avatar Alex ORLUC
Browse files

FEAT #11771 add filter select plugin

parent f9415e0b
No related branches found
No related tags found
No related merge requests found
<mat-form-field class="input-form" floatLabel="never">
<mat-select [formControl]="formControlSelect" [placeholder]="placeholderLabel" #test>
<input *ngIf="datas.length > 5" matInput class="mat-select-search-input mat-select-search-hidden" />
<div *ngIf="datas.length > 5" class="mat-select-search-inner" [ngClass]="{'mat-select-search-inner-multiple': matSelect.multiple}">
<input matInput id="searchSelectInput" [formControl]="formControlSearch" class="mat-select-search-input" #searchSelectInput (keydown)="_handleKeydown($event)"
(input)="onInputChange($event.target.value)" (blur)="onBlur($event.target.value)"
[placeholder]="lang.filterBy" />
<button mat-button *ngIf="formControlSearch.value" mat-icon-button aria-label="Clear" (click)="_reset(true)"
class="mat-select-search-clear">
<mat-icon class="fa fa-times"></mat-icon>
</button>
</div>
<div *ngIf="noEntriesFoundLabel && value && _options?.length === 0 && datas.length > 5" class="mat-select-search-no-entries-found">
{{lang.noResult}}
</div>
<mat-option *ngIf="adminMode"></mat-option>
<mat-option *ngFor="let value of filteredDatas | async" [value]="value.id"
[title]="value.title !== undefined ? value.title : value.label" [disabled]="value.disabled"
[class.opt-group]="value.isTitle" [style.color]="value.color" [innerHTML]="value.label">
</mat-option>
</mat-select>
</mat-form-field>
\ No newline at end of file
@import '../../css/vars.scss';
$mat-menu-side-padding: 16px !default;
$scrollbar-width: 17px;
$clear-button-width: 20px;
$multiple-check-width: 33px;
.mat-select-search-hidden {
visibility: hidden;
}
.mat-select-search-inner {
position: absolute;
top: 0;
width: calc(100% + #{2 * $mat-menu-side-padding - $scrollbar-width});
border-bottom: 1px solid #cccccc;
background: white;
z-index: 100;
&.mat-select-search-inner-multiple {
width: calc(100% + #{2 * $mat-menu-side-padding - $scrollbar-width + $multiple-check-width});
}
}
::ng-deep.mat-select-search-panel {
/* allow absolute positioning relative to outer options container */
transform: none !important;
max-height: 350px;
}
.mat-select-search-input {
padding: $mat-menu-side-padding;
padding-right: $mat-menu-side-padding + $clear-button-width;
box-sizing: border-box;
}
.mat-select-search-no-entries-found {
padding: $mat-menu-side-padding;
}
.mat-select-search-clear {
position: absolute;
right: 0;
top: 4px;
}
::ng-deep.cdk-overlay-pane-select-search {
/* correct offsetY so that the selected option is at the position of the select box when opening */
margin-top: -50px;
}
import {
AfterViewInit, ChangeDetectorRef,
Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, QueryList,
ViewChild,
Renderer2
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormControl } from '@angular/forms';
import { MatOption, MatSelect } from '@angular/material';
import { take, takeUntil, startWith, map } from 'rxjs/operators';
import { Subject, ReplaySubject, Observable } from 'rxjs';
import { LatinisePipe } from 'ngx-pipes';
import { LANG } from '../../app/translate.component';
@Component({
selector: 'plugin-select-search',
templateUrl: 'select-search.component.html',
styleUrls: ['select-search.component.scss', '../../app/indexation/indexing-form/indexing-form.component.scss']
})
export class PluginSelectSearchComponent implements OnInit, OnDestroy, AfterViewInit, ControlValueAccessor {
lang: any = LANG;
/** Label of the search placeholder */
@Input() placeholderLabel = this.lang.chooseValue;
/** Label to be shown when no entries are found. Set to null if no message should be shown. */
@Input() noEntriesFoundLabel = 'Aucun résultat';
@Input('formControlSelect') formControlSelect: FormControl;
@Input('datas') datas: any;
/** Reference to the search input field */
@ViewChild('searchSelectInput', { read: ElementRef, static: true }) searchSelectInput: ElementRef;
@ViewChild('test', { static: true }) matSelect: MatSelect;
/** Current search value */
get value(): string {
return this._value;
}
private _value: string;
onChange: Function = (_: any) => { };
onTouched: Function = (_: any) => { };
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();
constructor(private latinisePipe: LatinisePipe, private changeDetectorRef: ChangeDetectorRef, private renderer: Renderer2) {
}
ngOnInit() {
// 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
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();
});
}
});
});
// detect changes when the input changes
/*this.change
.pipe(takeUntil(this._onDestroy))
.subscribe(() => {
this.changeDetectorRef.detectChanges();
});*/
setTimeout(() => {
this.filteredDatas = this.formControlSearch.valueChanges
.pipe(
startWith(''),
map(value => this._filter(value))
);
}, 800);
// this.initMultipleHandling();
// load the initial bank list
//this.filteredDatas.next(this.datas.slice());
//this.filteredDatasMulti.next(this.datas.slice());
// listen for search field value changes
/*this.formControlSelect.valueChanges
.pipe(takeUntil(this._onDestroy))
.subscribe(() => {
this.filterDatas();
});*/
/*this.formControlSelect.valueChanges
.pipe(takeUntil(this._onDestroy))
.subscribe(() => {
this.filterDatasMulti();
});*/
}
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
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): string[] {
if (typeof value === 'string') {
const filterValue = this.latinisePipe.transform(value.toLowerCase());
return this.datas.filter((option: any) => this.latinisePipe.transform(option['label'].toLowerCase()).includes(filterValue));
} else {
return this.datas;
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment