import { AsyncPipe, NgClass, NgFor, NgIf, } from '@angular/common'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, ViewEncapsulation, } from '@angular/core'; import { NgbDropdownModule, NgbPaginationModule, NgbTooltipModule, } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; import { Observable, of as observableOf, Subscription, } from 'rxjs'; import { map, startWith, switchMap, take, } from 'rxjs/operators'; import { SortDirection, SortOptions, } from '../../core/cache/models/sort-options.model'; import { PaginatedList } from '../../core/data/paginated-list.model'; import { RemoteData } from '../../core/data/remote-data'; import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationRouteParams } from '../../core/pagination/pagination-route-params.interface'; import { ViewMode } from '../../core/shared/view-mode.model'; import { BtnDisabledDirective } from '../btn-disabled.directive'; import { hasValue, hasValueOperator, } from '../empty.util'; import { HostWindowService } from '../host-window.service'; import { ListableObject } from '../object-collection/shared/listable-object.model'; import { RSSComponent } from '../rss-feed/rss.component'; import { EnumKeysPipe } from '../utils/enum-keys-pipe'; import { PaginationComponentOptions } from './pagination-component-options.model'; interface PaginationDetails { range: string; total: number; } /** * The default pagination controls component. */ @Component({ exportAs: 'paginationComponent', selector: 'ds-pagination', styleUrls: ['pagination.component.scss'], templateUrl: 'pagination.component.html', changeDetection: ChangeDetectionStrategy.Default, encapsulation: ViewEncapsulation.Emulated, standalone: true, imports: [NgIf, NgbDropdownModule, NgFor, NgClass, RSSComponent, NgbPaginationModule, NgbTooltipModule, AsyncPipe, TranslateModule, EnumKeysPipe, BtnDisabledDirective], }) export class PaginationComponent implements OnChanges, OnDestroy, OnInit { /** * ViewMode that should be passed to {@link ListableObjectComponentLoaderComponent}. */ viewMode: ViewMode = ViewMode.ListElement; /** * Number of items in collection. */ @Input() collectionSize: number; /** * Configuration for the NgbPagination component. */ @Input() paginationOptions: PaginationComponentOptions; /** * Sort configuration for this component. */ @Input() sortOptions: SortOptions; /** * Whether or not the pagination should be rendered as simple previous and next buttons instead of the normal pagination */ @Input() showPaginator = true; /** * The current pagination configuration */ @Input() config?: PaginationComponentOptions; /** * The list of listable objects to render in this component */ @Input() objects: RemoteData<PaginatedList<ListableObject>>; /** * The current sorting configuration */ @Input() sortConfig: SortOptions; /** * An event fired when the page is changed. * Event's payload equals to the newly selected page. */ @Output() pageChange: EventEmitter<number> = new EventEmitter<number>(); /** * An event fired when the page wsize is changed. * Event's payload equals to the newly selected page size. */ @Output() pageSizeChange: EventEmitter<number> = new EventEmitter<number>(); /** * An event fired when the sort direction is changed. * Event's payload equals to the newly selected sort direction. */ @Output() sortDirectionChange: EventEmitter<SortDirection> = new EventEmitter<SortDirection>(); /** * An event fired when the sort field is changed. * Event's payload equals to the newly selected sort field. */ @Output() sortFieldChange: EventEmitter<string> = new EventEmitter<string>(); /** * An event fired when the pagination is changed. * Event's payload equals to the newly selected sort field. */ @Output() paginationChange: EventEmitter<any> = new EventEmitter<any>(); /** * Option for hiding the pagination detail */ @Input() public hidePaginationDetail = false; /** * Option for hiding the gear */ @Input() public hideGear = false; /** * Option for hiding the gear */ @Input() public hideSortOptions = false; /** * Option for hiding the pager when there is less than 2 pages */ @Input() public hidePagerWhenSinglePage = true; /** * Option for retaining the scroll position upon navigating to an url with updated params. * After the page update the page will scroll back to the current pagination component. */ @Input() public retainScrollPosition = false; /** * Current page. */ public currentPage$: Observable<number>; /** * Current page in the state of a Remote paginated objects. */ public currentPageState: number = undefined; /** * ID for the pagination instance. This ID is used in the routing to retrieve the pagination options. * This ID needs to be unique between different pagination components when more than one will be displayed on the same page. */ public id: string; /** * A boolean that indicate if is an extra small devices viewport. */ public isXs: boolean; /** * Number of items per page. */ public pageSize$: Observable<number>; /** * Declare SortDirection enumeration to use it in the template */ public sortDirections = SortDirection; /** * A number array that represents options for a context pagination limit. */ public pageSizeOptions: number[]; /** * Direction in which to sort: ascending or descending */ public sortDirection$: Observable<SortDirection>; public defaultsortDirection: SortDirection = SortDirection.ASC; /** * Name of the field that's used to sort by */ public sortField$: Observable<string>; public defaultSortField = 'name'; public showingDetails$: Observable<PaginationDetails>; /** * Array to track all subscriptions and unsubscribe them onDestroy * @type {Array} */ private subs: Subscription[] = []; /** * If showPaginator is set to true, emit when the previous button is clicked */ @Output() prev = new EventEmitter<boolean>(); /** * If showPaginator is set to true, emit when the next button is clicked */ @Output() next = new EventEmitter<boolean>(); /** * Method provided by Angular. Invoked after the constructor. */ ngOnInit() { this.subs.push(this.hostWindowService.isXs() .subscribe((status: boolean) => { this.isXs = status; this.cdRef.markForCheck(); })); this.checkConfig(this.paginationOptions); this.initializeConfig(); } ngOnChanges(changes: SimpleChanges): void { if (changes.collectionSize.currentValue !== changes.collectionSize.previousValue) { this.showingDetails$ = this.getShowingDetails(this.collectionSize); } } /** * Method provided by Angular. Invoked when the instance is destroyed. */ ngOnDestroy() { this.subs .filter((sub) => hasValue(sub)) .forEach((sub) => sub.unsubscribe()); } /** * Initializes all default variables */ private initializeConfig() { // Set initial values this.id = this.paginationOptions.id || null; this.pageSizeOptions = this.paginationOptions.pageSizeOptions; this.currentPage$ = this.paginationService.getCurrentPagination(this.id, this.paginationOptions).pipe( map((currentPagination) => currentPagination.currentPage), ); this.pageSize$ = this.paginationService.getCurrentPagination(this.id, this.paginationOptions).pipe( map((currentPagination) => currentPagination.pageSize), ); let sortOptions: SortOptions; if (this.sortOptions) { sortOptions = this.sortOptions; } else { sortOptions = new SortOptions(this.defaultSortField, this.defaultsortDirection); } this.sortDirection$ = this.paginationService.getCurrentSort(this.id, sortOptions).pipe( map((currentSort) => currentSort.direction), ); this.sortField$ = this.paginationService.getCurrentSort(this.id, sortOptions).pipe( map((currentSort) => currentSort.field), ); } constructor( protected cdRef: ChangeDetectorRef, protected paginationService: PaginationService, public hostWindowService: HostWindowService, ) { } /** * Method to change the route to the given page * * @param page * The page being navigated to. */ public doPageChange(page: number) { this.updateParams({ page: page }); this.emitPaginationChange(); } /** * Method to change the route to the given page size * * @param pageSize * The page size being navigated to. */ public doPageSizeChange(pageSize: number) { this.updateParams({ page: 1, pageSize: pageSize }); this.emitPaginationChange(); } /** * Method to change the route to the given sort direction * * @param sortDirection * The sort direction being navigated to. */ public doSortDirectionChange(sortDirection: SortDirection) { this.updateParams({ page: 1, sortDirection: sortDirection }); this.emitPaginationChange(); } /** * Method to emit a general pagination change event */ private emitPaginationChange() { this.paginationChange.emit(); } /** * Update the current query params and optionally update the route * @param params */ private updateParams(params: PaginationRouteParams) { this.paginationService.updateRoute(this.id, params, {}, this.retainScrollPosition); } /** * Method to get pagination details of the current viewed page. */ public getShowingDetails(collectionSize: number): Observable<PaginationDetails> { return observableOf(collectionSize).pipe( hasValueOperator(), switchMap(() => this.paginationService.getCurrentPagination(this.id, this.paginationOptions)), map((currentPaginationOptions) => { let lastItem: number; const pageMax = currentPaginationOptions.pageSize * currentPaginationOptions.currentPage; const firstItem: number = currentPaginationOptions.pageSize * (currentPaginationOptions.currentPage - 1) + 1; if (collectionSize > pageMax) { lastItem = pageMax; } else { lastItem = collectionSize; } return { range: `${firstItem} - ${lastItem}`, total: collectionSize, }; }), startWith({ range: `${null} - ${null}`, total: null, }), ); } /** * Method to ensure options passed contains the required properties. * * @param paginateOptions * The paginate options object. */ private checkConfig(paginateOptions: any) { const required = ['id', 'currentPage', 'pageSize', 'pageSizeOptions']; const missing = required.filter((prop) => { return !(prop in paginateOptions); }); if (0 < missing.length) { throw new Error('Paginate: Argument is missing the following required properties: ' + missing.join(', ')); } } /** * Property to check whether the current pagination object has multiple pages * @returns true if there are multiple pages, else returns false */ get hasMultiplePages(): Observable<boolean> { return this.paginationService.getCurrentPagination(this.id, this.paginationOptions).pipe( map((currentPaginationOptions) => this.collectionSize > currentPaginationOptions.pageSize), ); } /** * Property to check whether the current pagination should show a bottom pages * @returns true if a bottom pages should be shown, else returns false */ get shouldShowBottomPager(): Observable<boolean> { return this.hasMultiplePages.pipe( map((hasMultiplePages) => hasMultiplePages || !this.hidePagerWhenSinglePage), ); } /** * Go to the previous page */ goPrev() { this.prev.emit(true); this.updatePagination(-1); } /** * Go to the next page */ goNext() { this.next.emit(true); this.updatePagination(1); } /** * Update page when next or prev button is clicked * @param value */ updatePagination(value: number) { this.paginationService.getCurrentPagination(this.id, this.paginationOptions).pipe(take(1)).subscribe((currentPaginationOptions) => { this.updateParams({ page: (currentPaginationOptions.currentPage + value) }); }); } }