All files / hooks/use-pagination/src utils.ts

100% Statements 25/25
100% Branches 26/26
100% Functions 6/6
100% Lines 25/25

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136              224x 5x   219x 219x                                         59x 59x 59x                         138x 3x   135x 135x 5x   130x                                 1x 183x   204x                                             61x 61x 61x 61x   61x 61x         61x       61x         61x                                    
/**
 * Coerce a value to a safe integer, flooring fractions and enforcing a minimum.
 * Non-finite values (`NaN`, `Infinity`) fall back to `fallback`.
 *
 * @internal
 */
export function toSafeInt(value: number, min: number, fallback: number): number {
  if (!Number.isFinite(value)) {
    return fallback;
  }
  const floored = Math.floor(value);
  return floored < min ? min : floored;
}
 
/**
 * Compute the number of pages for a dataset. Always returns at least `1` — an
 * empty dataset still has a single (empty) page — so it is safe to use directly
 * as the upper clamp bound. `total` is treated as `>= 0` and `pageSize` as
 * `>= 1` (both floored; non-finite inputs are normalized).
 *
 * @param total - Total number of items.
 * @param pageSize - Items per page.
 * @returns The page count, `>= 1`.
 *
 * @example
 * ```ts
 * getPageCount(0, 10);  // 1
 * getPageCount(25, 10); // 3
 * getPageCount(30, 10); // 3
 * ```
 */
export function getPageCount(total: number, pageSize: number): number {
  const safeTotal = toSafeInt(total, 0, 0);
  const safeSize = toSafeInt(pageSize, 1, 1);
  return Math.max(1, Math.ceil(safeTotal / safeSize));
}
 
/**
 * Clamp a requested page into `[1, pageCount]`, flooring fractions. Non-finite
 * requests fall back to page `1`. `pageCount` is assumed to be `>= 1` (as
 * returned by {@link getPageCount}).
 *
 * @param page - The requested 1-based page.
 * @param pageCount - The number of pages (upper bound).
 * @returns The clamped page, within `[1, pageCount]`.
 */
export function clampPage(page: number, pageCount: number): number {
  if (!Number.isFinite(page)) {
    return 1;
  }
  const floored = Math.floor(page);
  if (floored < 1) {
    return 1;
  }
  return floored > pageCount ? pageCount : floored;
}
 
/**
 * Parameters for {@link buildPaginationRange}.
 */
export interface BuildPaginationRangeParams {
  /** The current (clamped) 1-based page. */
  page: number;
  /** Total number of pages (`>= 1`). */
  pageCount: number;
  /** Pages shown on each side of the current page. @defaultValue 1 */
  siblingCount?: number;
  /** Pages shown at the start and end. @defaultValue 1 */
  boundaryCount?: number;
}
 
const inclusiveRange = (start: number, end: number): number[] =>
  start > end
    ? []
    : Array.from({ length: end - start + 1 }, (_, index) => start + index);
 
/**
 * Build the ordered list of page numbers with `"ellipsis"` gap tokens for a
 * pager UI — the well-known MUI/Mantine `usePagination` model. Always shows the
 * `boundaryCount` first/last pages and the `siblingCount` pages around the
 * current page, collapsing anything in between into a single `"ellipsis"`. When
 * a gap would hide exactly one page, that page is shown instead of an ellipsis.
 *
 * @param params - See {@link BuildPaginationRangeParams}.
 * @returns An ordered array of 1-based page numbers and `"ellipsis"` tokens.
 *
 * @example
 * ```ts
 * buildPaginationRange({ page: 1, pageCount: 10 });
 * // [1, 2, 3, 4, 5, "ellipsis", 10]
 * buildPaginationRange({ page: 6, pageCount: 10 });
 * // [1, "ellipsis", 5, 6, 7, "ellipsis", 10]
 * ```
 */
export function buildPaginationRange(
  params: BuildPaginationRangeParams
): (number | "ellipsis")[] {
  const count = Math.max(1, Math.floor(params.pageCount));
  const page = clampPage(params.page, count);
  const siblingCount = Math.max(0, Math.floor(params.siblingCount ?? 1));
  const boundaryCount = Math.max(0, Math.floor(params.boundaryCount ?? 1));
 
  const startPages = inclusiveRange(1, Math.min(boundaryCount, count));
  const endPages = inclusiveRange(
    Math.max(count - boundaryCount + 1, boundaryCount + 1),
    count
  );
 
  const siblingsStart = Math.max(
    Math.min(page - siblingCount, count - boundaryCount - siblingCount * 2 - 1),
    boundaryCount + 2
  );
  const siblingsEnd = Math.min(
    Math.max(page + siblingCount, boundaryCount + siblingCount * 2 + 2),
    endPages.length > 0 ? endPages[0] - 2 : count - 1
  );
 
  return [
    ...startPages,
    // Start gap: an ellipsis, or the single page it would have hidden.
    ...(siblingsStart > boundaryCount + 2
      ? (["ellipsis"] as const)
      : boundaryCount + 1 < count - boundaryCount
        ? [boundaryCount + 1]
        : []),
    ...inclusiveRange(siblingsStart, siblingsEnd),
    // End gap: an ellipsis, or the single page it would have hidden.
    ...(siblingsEnd < count - boundaryCount - 1
      ? (["ellipsis"] as const)
      : count - boundaryCount > boundaryCount
        ? [count - boundaryCount]
        : []),
    ...endPages,
  ];
}