import { Component, Input, NgZone, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild } from '@angular/core'; import { FormGroup, FormControl } from '@angular/forms'; import { SelectOption } from '../../models'; import { MatAutocompleteActivatedEvent, MatAutocompleteSelectedEvent, MatAutocompleteTrigger } from '@angular/material/autocomplete'; import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; import { BehaviorSubject, Observable, Subject, map, startWith, take } from 'rxjs'; import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'fss-form-select', templateUrl: './form-select.component.html', styleUrls: ['./form-select.component.scss'] }) export class FormSelectComponent implements OnInit, OnChanges, OnDestroy { @Input() form: FormGroup | undefined; @Input() control: FormControl | undefined; @Input() hasPermission = true; @Input() label!: string; @Input() showLabel: boolean = true; @Input() toolTipText = 'fss.universal.tooltip.missingPermission'; @Input() placeholder: string = ''; @Input() options: SelectOption[] = []; @Input() appearance: string = ''; @Input() clearValue?: any; @Input() allowClearButton: boolean = true; @ViewChild(MatAutocompleteTrigger, { static: false, read: MatAutocompleteTrigger }) inputAutoComplete?: MatAutocompleteTrigger; @ViewChild('scrollViewport', { static: false }) cdkVirtualScrollViewport?: CdkVirtualScrollViewport; viewportMaxHeight = '240px'; viewportHeight = '48px'; filteredOptions!: Observable; optionsSubject: BehaviorSubject = new BehaviorSubject([]); filterValueSubject: Subject = new Subject(); ngUnsubscribe: Subject = new Subject(); noOptionsAvailable: SelectOption = { value: null, display: 'fss.general.display.noOptionsAvailable', translateDisplay: true, disabled: true }; private scrollEventListener!: EventListener; constructor(private ngZone: NgZone, private translator: TranslateService) { } ngOnChanges(changes: SimpleChanges): void { for (const propName in changes) { if (changes.hasOwnProperty(propName)) { switch (propName) { case 'options': this.validateAndFilterOptions(); } } } } ngOnDestroy(): void { this.ngUnsubscribe.next(undefined); this.ngUnsubscribe.complete(); this.optionsSubject.next([]); this.optionsSubject.complete(); this.filterValueSubject.next(''); this.filterValueSubject.complete(); this.ngZone.runOutsideAngular(() => { window.removeEventListener('scroll', this.scrollEventListener, true); }); } ngOnInit(): void { this.setFilteredOptions(); this.scrollEventListener = $event => this.onScroll($event); this.ngZone.runOutsideAngular(() => { window.addEventListener('scroll', this.scrollEventListener, true); }) } validateAndFilterOptions() { this.filterValueSubject.next(''); if (!this.options) { this.control?.setValue(null); return; } this.optionsSubject.next(this.options); if (this.control) { if (!this.options.some(x => x.value === this.control?.value)) { this.control.setValue(null); } this.inputAutoComplete?.writeValue(this.displayFn(this.control?.value)); } } displayFn(value: any): string { let option: SelectOption | undefined; if (!this.filteredOptions) { return ''; } this.filteredOptions.pipe(take(1)).subscribe(options => { if (options && options.length > 0 && value != null) { option = options.find(x => x.value == value); } }); return option != null ? option.translateDisplay ? this.translator.instant(option.display) : option.display : ''; } setFilteredOptions() { this.filteredOptions = this.filterValueSubject.asObservable() .pipe( startWith(''), map(value => { const filteredOptions = this.getFilteredOptions(value.toLowerCase()); if (filteredOptions?.length === 0) { filteredOptions.push(this.noOptionsAvailable); } this.calculateViewportHeight(filteredOptions); return filteredOptions; }) ); } getFilteredOptions(filterValue: string): SelectOption[] { if (!this.options) { return []; } return this.options.filter(option => { if (option.translateDisplay) { return this.translator.instant(option.display).toLowerCase().indexOf(filterValue) >= 0; } return option.display.toLowerCase().indexOf(filterValue) >= 0; }); } calculateViewportHeight(filteredOptions: SelectOption[]) { if (this.cdkVirtualScrollViewport) { this.cdkVirtualScrollViewport.checkViewportSize(); } if (!filteredOptions || filteredOptions.length === 0) { this.viewportHeight = '48px'; } else if (filteredOptions.length < 5) { this.viewportHeight = `${filteredOptions.length * 48}px`; } else { this.viewportHeight = this.viewportMaxHeight; } } onScroll(event: any) { if (this.inputAutoComplete?.panelOpen && !event.srcElement.classList.contains('cdk-virtual-scroll-viewport')) { this.inputAutoComplete?.closePanel(); } } openPanel(event: any) { event.stopPropagation(); if (this.control?.disabled) { return; } if (this.inputAutoComplete && !this.inputAutoComplete.panelOpen) { this.filterValueSubject.next(''); this.inputAutoComplete.openPanel(); } } clear(event: any) { event.stopPropagation(); this.setControlValue(this.clearValue, true, false); this.displayFn(this.clearValue); } onInput(event: any) { this.filterValueSubject.next(event.srcElement.value); } onInputChange(event: any) { if (event.srcElement.value === '') { this.clear(event); return; } if (this.inputAutoComplete?.autocomplete.options.length === 1) { const option = this.inputAutoComplete.autocomplete.options.first; if (option) { this.setControlValue(option.value); } } } onSelectionChanged(event: MatAutocompleteSelectedEvent) { if (event.option && event.option.value) { this.setControlValue(event.option.value, true, false); } } onSelectionActivated(event: MatAutocompleteActivatedEvent) { if (event.option && event.option.value) { this.setControlValue(event.option.value, false, false); } } setControlValue(value: any, clearFilter: boolean = true, closePanel: boolean = true) { this.control?.setValue(value); this.control?.updateValueAndValidity(); if (clearFilter) { this.filterValueSubject.next(''); } if (closePanel) { this.inputAutoComplete?.closePanel(); } } }