`,
})
export class HlmNumberedPagination {
/**
* The current (active) page.
*/
public readonly currentPage = model.required();
/**
* The number of items per paginated page.
*/
public readonly itemsPerPage = model.required();
/**
* The total number of items in the collection. Only useful when
* doing server-side paging, where the collection size is limited
* to a single page returned by the server API.
*/
public readonly totalItems = input.required({
transform: numberAttribute,
});
/**
* The number of page links to show.
*/
public readonly maxSize = input(7, {
transform: numberAttribute,
});
/**
* Show the first and last page buttons.
*/
public readonly showEdges = input(true, {
transform: booleanAttribute,
});
/**
* The page sizes to show.
* Defaults to [10, 20, 50, 100]
*/
public readonly pageSizes = input([10, 20, 50, 100]);
protected readonly _pageSizesWithCurrent = computed(() => {
const pageSizes = this.pageSizes();
return pageSizes.includes(this.itemsPerPage())
? pageSizes // if current page size is included, return the same array
: [...pageSizes, this.itemsPerPage()].sort((a, b) => a - b); // otherwise, add current page size and sort the array
});
protected readonly _isFirstPageActive = computed(() => this.currentPage() === 1);
protected readonly _isLastPageActive = computed(
() => this.currentPage() === this._lastPageNumber(),
);
protected readonly _lastPageNumber = computed(() => {
if (this.totalItems() < 1) {
// when there are 0 or fewer (an error case) items, there are no "pages" as such,
// but it makes sense to consider a single, empty page as the last page.
return 1;
}
return Math.ceil(this.totalItems() / this.itemsPerPage());
});
protected readonly _pages = computed(() => {
const correctedCurrentPage = outOfBoundCorrection(
this.totalItems(),
this.itemsPerPage(),
this.currentPage(),
);
if (correctedCurrentPage !== this.currentPage()) {
// update the current page
untracked(() => this.currentPage.set(correctedCurrentPage));
}
return createPageArray(
correctedCurrentPage,
this.itemsPerPage(),
this.totalItems(),
this.maxSize(),
);
});
protected goToPrevious(): void {
this.currentPage.set(this.currentPage() - 1);
}
protected goToNext(): void {
this.currentPage.set(this.currentPage() + 1);
}
protected goToFirst(): void {
this.currentPage.set(1);
}
protected goToLast(): void {
this.currentPage.set(this._lastPageNumber());
}
}
type Page = number | '...';
/**
* Checks that the instance.currentPage property is within bounds for the current page range.
* If not, return a correct value for currentPage, or the current value if OK.
*
* Copied from 'ngx-pagination' package
*/
export function outOfBoundCorrection(
totalItems: number,
itemsPerPage: number,
currentPage: number,
): number {
const totalPages = Math.ceil(totalItems / itemsPerPage);
if (totalPages < currentPage && 0 < totalPages) {
return totalPages;
}
if (currentPage < 1) {
return 1;
}
return currentPage;
}
/**
* Returns an array of Page objects to use in the pagination controls.
*
* Copied from 'ngx-pagination' package
*/
export function createPageArray(
currentPage: number,
itemsPerPage: number,
totalItems: number,
paginationRange: number,
): Page[] {
// paginationRange could be a string if passed from attribute, so cast to number.
paginationRange = +paginationRange;
const pages: Page[] = [];
// Return 1 as default page number
// Make sense to show 1 instead of empty when there are no items
const totalPages = Math.max(Math.ceil(totalItems / itemsPerPage), 1);
const halfWay = Math.ceil(paginationRange / 2);
const isStart = currentPage <= halfWay;
const isEnd = totalPages - halfWay < currentPage;
const isMiddle = !isStart && !isEnd;
const ellipsesNeeded = paginationRange < totalPages;
let i = 1;
while (i <= totalPages && i <= paginationRange) {
let label: number | '...';
const pageNumber = calculatePageNumber(i, currentPage, paginationRange, totalPages);
const openingEllipsesNeeded = i === 2 && (isMiddle || isEnd);
const closingEllipsesNeeded = i === paginationRange - 1 && (isMiddle || isStart);
if (ellipsesNeeded && (openingEllipsesNeeded || closingEllipsesNeeded)) {
label = '...';
} else {
label = pageNumber;
}
pages.push(label);
i++;
}
return pages;
}
/**
* Given the position in the sequence of pagination links [i],
* figure out what page number corresponds to that position.
*
* Copied from 'ngx-pagination' package
*/
function calculatePageNumber(
i: number,
currentPage: number,
paginationRange: number,
totalPages: number,
) {
const halfWay = Math.ceil(paginationRange / 2);
if (i === paginationRange) {
return totalPages;
}
if (i === 1) {
return i;
}
if (paginationRange < totalPages) {
if (totalPages - halfWay < currentPage) {
return totalPages - paginationRange + i;
}
if (halfWay < currentPage) {
return currentPage - halfWay + i;
}
return i;
}
return i;
}