import { AsyncPipe, NgFor, NgIf, } from '@angular/common'; import { ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output, } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { BehaviorSubject, Observable, Subscription, } from 'rxjs'; import { reduce, startWith, switchMap, } from 'rxjs/operators'; import { EntityTypeDataService } from '../../core/data/entity-type-data.service'; import { FindListOptions } from '../../core/data/find-list-options.model'; import { PaginatedList } from '../../core/data/paginated-list.model'; import { RemoteData } from '../../core/data/remote-data'; import { ItemType } from '../../core/shared/item-relationships/item-type.model'; import { getFirstSucceededRemoteWithNotEmptyData } from '../../core/shared/operators'; import { hasValue } from '../empty.util'; import { ThemedLoadingComponent } from '../loading/themed-loading.component'; @Component({ selector: 'ds-entity-dropdown', templateUrl: './entity-dropdown.component.html', styleUrls: ['./entity-dropdown.component.scss'], standalone: true, imports: [InfiniteScrollModule, NgIf, NgFor, ThemedLoadingComponent, AsyncPipe, TranslateModule], }) export class EntityDropdownComponent implements OnInit, OnDestroy { /** * The entity list obtained from a search * @type {Observable<ItemType[]>} */ public searchListEntity$: Observable<ItemType[]>; /** * A boolean representing if dropdown list is scrollable to the bottom * @type {boolean} */ private scrollableBottom = false; /** * A boolean representing if dropdown list is scrollable to the top * @type {boolean} */ private scrollableTop = false; /** * The list of entity to render */ public searchListEntity: ItemType[] = []; /** * TRUE if the parent operation is a 'new submission' operation, FALSE otherwise (eg.: is an 'Import metadata from an external source' operation). */ @Input() isSubmission: boolean; /** * The entity to output to the parent component */ @Output() selectionChange = new EventEmitter<ItemType>(); /** * A boolean representing if the loader is visible or not */ public isLoadingList: BehaviorSubject<boolean> = new BehaviorSubject(false); /** * A numeric representig current page */ public currentPage: number; /** * A boolean representing if exist another page to render */ public hasNextPage: boolean; /** * Array to track all subscriptions and unsubscribe them onDestroy * @type {Array} */ public subs: Subscription[] = []; /** * Initialize instance variables * * @param {ChangeDetectorRef} changeDetectorRef * @param {EntityTypeDataService} entityTypeService * @param el */ constructor( private changeDetectorRef: ChangeDetectorRef, private entityTypeService: EntityTypeDataService, private el: ElementRef, ) { } /** * Method called on mousewheel event, it prevent the page scroll * when arriving at the top/bottom of dropdown menu * * @param event * mousewheel event */ @HostListener('mousewheel', ['$event']) onMousewheel(event) { if (event.wheelDelta > 0 && this.scrollableTop) { event.preventDefault(); } if (event.wheelDelta < 0 && this.scrollableBottom) { event.preventDefault(); } } /** * Initialize entity list */ ngOnInit() { this.resetPagination(); this.populateEntityList(this.currentPage); } /** * Check if dropdown scrollbar is at the top or bottom of the dropdown list * * @param event */ public onScroll(event) { this.scrollableBottom = (event.target.scrollTop + event.target.clientHeight === event.target.scrollHeight); this.scrollableTop = (event.target.scrollTop === 0); } /** * Method used from infitity scroll for retrive more data on scroll down */ public onScrollDown() { if ( this.hasNextPage ) { this.populateEntityList(++this.currentPage); } } /** * Emit a [selectionChange] event when a new entity is selected from list * * @param event * the selected [ItemType] */ public onSelect(event: ItemType) { this.selectionChange.emit(event); } /** * Method called for populate the entity list * @param page page number */ public populateEntityList(page: number) { this.isLoadingList.next(true); // Set the pagination info const findOptions: FindListOptions = { elementsPerPage: 10, currentPage: page, }; let searchListEntity$; if (this.isSubmission) { searchListEntity$ = this.entityTypeService.getAllAuthorizedRelationshipType(findOptions); } else { searchListEntity$ = this.entityTypeService.getAllAuthorizedRelationshipTypeImport(findOptions); } this.searchListEntity$ = searchListEntity$.pipe( getFirstSucceededRemoteWithNotEmptyData(), switchMap((entityType: RemoteData<PaginatedList<ItemType>>) => { if ( (this.searchListEntity.length + findOptions.elementsPerPage) >= entityType.payload.totalElements ) { this.hasNextPage = false; } return entityType.payload.page; }), reduce((acc: any, value: any) => [...acc, value], []), startWith([]), ); this.subs.push( this.searchListEntity$.subscribe( (next) => { this.searchListEntity.push(...next); }, undefined, () => { this.hideShowLoader(false); this.changeDetectorRef.detectChanges(); }, ), ); } /** * Reset pagination values */ public resetPagination() { this.currentPage = 1; this.hasNextPage = true; this.searchListEntity = []; } /** * Hide/Show the entity list loader * @param hideShow true for show, false otherwise */ public hideShowLoader(hideShow: boolean) { this.isLoadingList.next(hideShow); } /** * Unsubscribe from all subscriptions */ ngOnDestroy(): void { this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); } }