UNPKG

nhb-toolbox

Version:

A versatile collection of smart, efficient, and reusable utility functions and classes for everyday development needs.

232 lines (231 loc) 8.4 kB
/** * Generates pagination logic, offsets, metadata and other pagination logic(s) for APIs and UIs. */ export class Paginator { #totalItems; #perPage; #currentPage; /** * * Creates an instance of the Paginator. * @param options - The options for pagination. */ constructor(options) { const { totalItems, itemsPerPage = 10, currentPage = 1, } = options ?? {}; this.#totalItems = Math.max(0, Number(totalItems)); this.#perPage = Math.max(1, Number(itemsPerPage)); this.#currentPage = Math.max(1, Number(currentPage)); } /** * @instance Returns a new Paginator instance with the given page number. * * This method does not modify the current instance. * - **N.B.** *If the provided page is out of range, it will be clamped between 1 and the last page.* * @param page - The new current page to use. * @returns A new Paginator instance with the updated (clamped) page. */ withPage(page) { const safePage = Math.min(Math.max(1, page), this.totalPages()); return new Paginator({ totalItems: this.#totalItems, itemsPerPage: this.#perPage, currentPage: safePage, }); } /** * @instance Returns a new Paginator instance with the given items per page. * - **N.B.** *If the value is less than `1`, it will be clamped to 1.* * @param perPage - New items per page value. * @returns A new Paginator instance with updated items per page. */ withPerPage(perPage) { const safePerPage = Math.max(1, perPage); return new Paginator({ totalItems: this.#totalItems, itemsPerPage: safePerPage, currentPage: this.#currentPage, }); } /** * @instance Returns a new Paginator instance with the given total items. * - **N.B.** *If the value is `negative`, it will be clamped to `0`.* * @param totalItems - New total items value. * @returns A new Paginator instance with updated total items. */ withTotalItems(totalItems) { const safeTotalItems = Math.max(0, totalItems); return new Paginator({ totalItems: safeTotalItems, itemsPerPage: this.#perPage, currentPage: this.#currentPage, }); } /** * @instance Returns a new Paginator instance with updated pagination options. * - **N.B.** *Any value provided will override the existing one, with clamping applied for safety.* * @param options - Partial pagination options to override the current instance. * @returns A new Paginator instance with merged and clamped options. */ withOptions(options) { const newTotalItems = Math.max(0, options.totalItems ? Number(options.totalItems) : this.#totalItems); const newItemsPerPage = Math.max(1, options.itemsPerPage ? Number(options.itemsPerPage) : this.#perPage); const totalPages = Math.ceil(newTotalItems / newItemsPerPage); const newCurrentPage = Math.min(Math.max(1, options.currentPage ? Number(options.currentPage) : this.#currentPage), totalPages); return new Paginator({ totalItems: newTotalItems, itemsPerPage: newItemsPerPage, currentPage: newCurrentPage, }); } /** * @instance Calculates the offset (number of items to skip) based on the `current page` and `items per page`. * @returns The number of items to skip. */ offset() { return (this.#currentPage - 1) * this.#perPage; } /** * @instance Calculates the offset (number of items to skip) based on the `current page` and `items per page`. * @alias of {@link offset} * @returns The number of items to skip. */ getOffset() { return this.offset(); } /** * @instance Calculates the number of items to skip based on the `current page` and `items per page`. * @alias of {@link offset} * @returns The number of items to skip. */ skipCount() { return this.offset(); } /** * @instance Calculates the total number of pages based on `total items` and `items per page`. * @returns The total number of pages. */ totalPages() { return Math.ceil(this.#totalItems / this.#perPage); } /** * * Returns pagination metadata useful for API responses or UI rendering. * @returns An object with pagination metadata. */ getMeta() { const totalPages = this.totalPages(); return { totalItems: this.#totalItems, currentPage: this.#currentPage, itemsPerPage: this.#perPage, totalPages, hasPrev: this.hasPrevPage(), hasNext: this.hasNextPage(), isFirst: this.isFirstPage(), isLast: this.isLastPage(), offset: this.offset(), }; } /** * @instance Returns the next page number if it exists. * @returns The next page number or null if it's the last page. */ nextPage() { return this.#currentPage < this.totalPages() ? this.#currentPage + 1 : null; } /** * @instance Returns the previous page number if it exists. * @returns The previous page number or null if it's the first page. */ prevPage() { return this.#currentPage > 1 ? this.#currentPage - 1 : null; } /** * @instance Checks if the current page is the first page. * @returns Whether the current page is the first page. */ isFirstPage() { return this.#currentPage === 1; } /** * @instance Checks if the current page is the last page. * @returns Whether the current page is the last page. */ isLastPage() { return this.#currentPage === this.totalPages(); } /** * @instance Checks if a previous page exists. * @returns Whether a previous page exists. */ hasPrevPage() { return this.#currentPage > 1; } /** * @instance Checks if a next page exists. * @returns Whether a next page exists. */ hasNextPage() { return this.#currentPage < this.totalPages(); } /** * @instance Creates an array of page numbers for UI pagination display. * @param options Options for customizing the page list. * @returns An array of visible page numbers. */ pageList(options = {}) { const total = this.totalPages(); const edgeCount = Math.max(0, options.edgeCount ?? 1); const siblingCount = Math.max(0, options.siblingCount ?? 1); const start = Math.max(this.#currentPage - siblingCount, edgeCount + 1); const end = Math.min(this.#currentPage + siblingCount, total - edgeCount); const _getRange = (from, to) => { return from > to ? [] : Array.from({ length: to - from + 1 }, (_, i) => from + i); }; const startPages = _getRange(1, edgeCount); const middlePages = _getRange(start, end); const endPages = _getRange(total - edgeCount + 1, total); const pages = new Set([ ...startPages, ...middlePages, ...endPages, ]); return Array.from(pages).sort((a, b) => a - b); } /** * @instance Returns the first page number. * @returns Always returns 1. */ firstPage() { return 1; } /** * @instance Returns the last page number based on total items and per page count. * @returns The last page number. */ lastPage() { return this.totalPages(); } /** * @instance Checks if a page number is valid within the pagination range. * @param page - The page number to validate. * @returns Whether the page number is within range. */ isPageValid(page) { const p = Math.floor(page); return p >= 1 && p <= this.totalPages(); } /** * @static Creates a new Paginator instance from a meta object. * @param meta - A pagination metadata object. * @returns A new Paginator instance. */ static fromMeta(meta) { return new Paginator({ totalItems: meta.totalItems, itemsPerPage: meta.itemsPerPage, currentPage: meta.currentPage, }); } }