import {OverlayRef} from "@angular/cdk/overlay"; import { AfterViewInit, Directive, ElementRef, HostBinding, HostListener, Input, OnInit, ViewChild, } from "@angular/core"; import { FormBuilder, FormGroup, } from "@angular/forms"; import {environment} from "@environments/environment"; import {Store} from "@ngxs/store"; import {SharedAbstractContentPresentational} from "@shared/components/presentationals/shared-abstract-content/shared-abstract-content.presentational"; import {SharedSearchableMultiSelectPresentational} from "@shared/components/presentationals/shared-searchable-multi-select/shared-searchable-multi-select.presentational"; import {SharedSearchableSingleSelectPresentational} from "@shared/components/presentationals/shared-searchable-single-select/shared-searchable-single-select.presentational"; import {Observable} from "rxjs"; import { debounceTime, distinctUntilChanged, tap, } from "rxjs/operators"; import { BaseResourceType, isEmptyString, isNotNullNorUndefined, isNullOrUndefined, MappingObjectUtil, MemoizedUtil, QueryParameters, ResourceState, ResourceStateModel, } from "solidify-frontend"; @Directive() export abstract class SharedSearchableAbstractContentPresentational<TResource extends BaseResourceType> extends SharedAbstractContentPresentational<TResource> implements OnInit, AfterViewInit { listObs: Observable<TResource[]>; totalObs: Observable<number>; isLoadingObs: Observable<boolean>; navigationResId: string; form: FormGroup; protected _overlayRefs: Set<OverlayRef> = new Set<OverlayRef>(); @HostBinding("class.is-loading") isLoading: boolean; list: TResource[]; @Input() abstract host: SharedSearchableMultiSelectPresentational<ResourceStateModel<TResource>, TResource> | SharedSearchableSingleSelectPresentational<ResourceStateModel<TResource>, TResource>; @ViewChild("input") input: ElementRef; protected readonly _debounceTime: number = 250; protected readonly _pageSize: number = environment.defaultEnumValuePageSizeLazyLoad; constructor(protected _store: Store, protected _fb: FormBuilder) { super(); } ngOnInit(): void { super.ngOnInit(); this._initSearchForm(); this._dispatchInitDataLoad(); this._retrieveSelector(); this._observeSearchInput(); this._observeIsLoading(); this._observeList(); } private _initSearchForm(): void { this.form = this._fb.group({ [this.host.formDefinition.search]: [""], }); } getFocusedResourceAndIndex(): { value: TResource | undefined, index: number } { const list = this.list; for (let index = 0; index < list.length; index++) { if (this.navigationResId === list[index].resId) { return {value: list[index], index}; } } return {value: undefined, index: -1}; } getFocusedResource(): TResource | undefined { return this.getFocusedResourceAndIndex().value; } getFocusedIndex(): number { return this.getFocusedResourceAndIndex().index; } selectNextResource(): void { const list = this.list; const focusedIndex = this.getFocusedIndex(); if (focusedIndex !== list.length - 1) { this.navigationResId = list[focusedIndex + 1].resId; } } selectPreviousResource(): void { const list = this.list; const focusedIndex = this.getFocusedIndex(); if (focusedIndex > 0) { this.navigationResId = list[focusedIndex - 1].resId; } else { this.navigationResId = undefined; } } @HostListener("keydown.arrowDown", ["$event"]) onArrowDown(keyboardEvent: KeyboardEvent): void { if (keyboardEvent && !keyboardEvent.defaultPrevented) { keyboardEvent.preventDefault(); this.selectNextResource(); } } @HostListener("keydown.arrowUp", ["$event"]) onArrowUp(keyboardEvent: KeyboardEvent): void { if (keyboardEvent && !keyboardEvent.defaultPrevented) { keyboardEvent.preventDefault(); this.selectPreviousResource(); } } @HostListener("keydown.enter", ["$event"]) onSelectValue(keyboardEvent: KeyboardEvent): void { if (keyboardEvent && !keyboardEvent.defaultPrevented) { keyboardEvent.preventDefault(); this.select(this.getFocusedResource()); } } private _dispatchInitDataLoad(): void { const queryParameters = new QueryParameters(this._pageSize); if (isNotNullNorUndefined(this.host.extraSearchQueryParam)) { queryParameters.search.searchItems = MappingObjectUtil.copy(this.host.extraSearchQueryParam); } this._store.dispatch(new this.host.resourceNameSpace.GetAll(queryParameters)); } private _retrieveSelector(): void { this.listObs = ResourceState.list(this._store, this.host.state); this.totalObs = ResourceState.total(this._store, this.host.state); this.isLoadingObs = MemoizedUtil.isLoading(this._store, this.host.state); } private _observeSearchInput(): void { this.subscribe(this.form.get(this.host.formDefinition.search).valueChanges.pipe( debounceTime(this._debounceTime), distinctUntilChanged(), tap(v => { const queryParameters = new QueryParameters(); queryParameters.paging.pageSize = this._pageSize; if (isNotNullNorUndefined(this.host.extraSearchQueryParam)) { queryParameters.search.searchItems = MappingObjectUtil.copy(this.host.extraSearchQueryParam); } MappingObjectUtil.set(queryParameters.search.searchItems, (isNotNullNorUndefined(this.host.searchKey) ? this.host.searchKey : this.host.labelKey), v); this._store.dispatch(new this.host.resourceNameSpace.GetAll(queryParameters, true)); }), )); } ngAfterViewInit(): void { this.setFocusOnInput(); } private setFocusOnInput(): void { setTimeout(() => { this.input.nativeElement.focus(); }, 0); } private _observeIsLoading(): void { this.subscribe(this.isLoadingObs.pipe( distinctUntilChanged(), tap(isLoading => this.isLoading = isLoading), )); } private _observeList(): void { this.subscribe(this.listObs.pipe( distinctUntilChanged(), tap(list => this.list = list), )); } computeHighlightText(valueElement: string): string { const fc = this.form.get(this.host.formDefinition.search); if (isNullOrUndefined(fc)) { return valueElement; } const value = fc.value; if (isNullOrUndefined(value) || isEmptyString(value)) { return valueElement; } return valueElement.replace(new RegExp(value, "gi"), match => `<span class="highlight-text">${match}</span>`); } }