@usefy/use-pagination 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +201 -0
- package/dist/index.d.mts +253 -0
- package/dist/index.d.ts +253 -0
- package/dist/index.js +190 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +160 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/mirunamu00/usefy/master/assets/logo.png" alt="usefy logo" width="120" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">@usefy/use-pagination</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>A headless pagination state machine — current page, page count, a slice-ready range, and an ellipsis-aware pager model</strong>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/@usefy/use-pagination"><img src="https://img.shields.io/npm/v/@usefy/use-pagination.svg?style=flat-square&color=007acc" alt="npm version" /></a>
|
|
13
|
+
<a href="https://www.npmjs.com/package/@usefy/use-pagination"><img src="https://img.shields.io/npm/dm/@usefy/use-pagination.svg?style=flat-square&color=007acc" alt="npm downloads" /></a>
|
|
14
|
+
<a href="https://bundlephobia.com/package/@usefy/use-pagination"><img src="https://img.shields.io/bundlephobia/minzip/@usefy/use-pagination?style=flat-square&color=007acc" alt="bundle size" /></a>
|
|
15
|
+
<a href="https://github.com/mirunamu00/usefy/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/@usefy/use-pagination.svg?style=flat-square&color=007acc" alt="license" /></a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<a href="#installation">Installation</a> •
|
|
20
|
+
<a href="#quick-start">Quick Start</a> •
|
|
21
|
+
<a href="#api">API</a> •
|
|
22
|
+
<a href="#testing">Testing</a> •
|
|
23
|
+
<a href="#license">License</a>
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
<p align="center">
|
|
27
|
+
<a href="https://mirunamu00.github.io/usefy/?path=/docs/hooks-usepagination--docs" target="_blank" rel="noopener noreferrer">
|
|
28
|
+
<strong>📚 View Storybook Demo</strong>
|
|
29
|
+
</a>
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Overview
|
|
35
|
+
|
|
36
|
+
`usePagination` is part of the [@usefy](https://www.npmjs.com/org/usefy) ecosystem — a collection of production-ready, TypeScript-first, SSR-safe React hooks. It owns **pagination state** — nothing else — so you can render the pager however you like: a table footer, a numbered button row, a "load more" counter, an API cursor.
|
|
37
|
+
|
|
38
|
+
From `total` and `pageSize` it derives the page count, keeps the current page clamped in range, hands you a **slice-ready index window** (`range`) for your data array, and builds the **ellipsis-aware page-number model** (`items`) you need for a `1 … 24 25 26 … 50` pager — the same model as MUI/Mantine's `usePagination`. The current page works in both **controlled** and **uncontrolled** modes, composing [`@usefy/use-controllable-state`](https://www.npmjs.com/package/@usefy/use-controllable-state).
|
|
39
|
+
|
|
40
|
+
## Features
|
|
41
|
+
|
|
42
|
+
- **Controlled & uncontrolled** — pass `page` + `onChange`, or let the hook own the page from `defaultPage`
|
|
43
|
+
- **Slice-ready `range`** — a 0-based `{ start, end }` window (end **exclusive**, clamped to `total`) so `data.slice(start, end)` just works
|
|
44
|
+
- **Ellipsis-aware `items`** — page numbers + `"ellipsis"` tokens driven by `siblingCount` / `boundaryCount`, each flagged `selected` for the current page
|
|
45
|
+
- **Always in range** — the page is clamped into `[1, pageCount]`; when the dataset shrinks the current page follows it, no dangling out-of-range page
|
|
46
|
+
- **No wasted renders** — `next`/`prev` at a bound and `setPage` to the current page are no-ops (no re-render, no `onChange`)
|
|
47
|
+
- **Stable controls** — `setPage`/`next`/`prev`/`first`/`last` keep their identity across renders, safe as effect deps
|
|
48
|
+
- **Robust guards** — non-positive `pageSize` is coerced to `1`, negative/`NaN` `total` to `0`, fractional pages floored
|
|
49
|
+
- **TypeScript-first** — full type inference and exported types
|
|
50
|
+
- **Tiny & tree-shakeable** — one tiny dependency, published as its own package
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# npm
|
|
56
|
+
npm install @usefy/use-pagination
|
|
57
|
+
|
|
58
|
+
# yarn
|
|
59
|
+
yarn add @usefy/use-pagination
|
|
60
|
+
|
|
61
|
+
# pnpm
|
|
62
|
+
pnpm add @usefy/use-pagination
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Requires React 18 or 19 (`peerDependencies: "react": "^18.0.0 || ^19.0.0"`).
|
|
66
|
+
|
|
67
|
+
## Quick Start
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { usePagination } from "@usefy/use-pagination";
|
|
71
|
+
|
|
72
|
+
function UsersTable({ users }: { users: User[] }) {
|
|
73
|
+
const { page, pageCount, range, items, setPage, next, prev, canNext, canPrev } =
|
|
74
|
+
usePagination({ total: users.length, pageSize: 10 });
|
|
75
|
+
|
|
76
|
+
const visible = users.slice(range.start, range.end);
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<>
|
|
80
|
+
<ul>
|
|
81
|
+
{visible.map((u) => (
|
|
82
|
+
<li key={u.id}>{u.name}</li>
|
|
83
|
+
))}
|
|
84
|
+
</ul>
|
|
85
|
+
|
|
86
|
+
<nav>
|
|
87
|
+
<button onClick={prev} disabled={!canPrev}>‹ Prev</button>
|
|
88
|
+
{items.map((item, i) =>
|
|
89
|
+
item.type === "ellipsis" ? (
|
|
90
|
+
<span key={`gap-${i}`}>…</span>
|
|
91
|
+
) : (
|
|
92
|
+
<button
|
|
93
|
+
key={item.page}
|
|
94
|
+
aria-current={item.selected ? "page" : undefined}
|
|
95
|
+
onClick={() => setPage(item.page!)}
|
|
96
|
+
>
|
|
97
|
+
{item.page}
|
|
98
|
+
</button>
|
|
99
|
+
)
|
|
100
|
+
)}
|
|
101
|
+
<button onClick={next} disabled={!canNext}>Next ›</button>
|
|
102
|
+
</nav>
|
|
103
|
+
|
|
104
|
+
<p>Page {page} of {pageCount}</p>
|
|
105
|
+
</>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## API
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const pagination = usePagination(options);
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Options — `UsePaginationOptions`
|
|
117
|
+
|
|
118
|
+
| Option | Type | Default | Description |
|
|
119
|
+
| --- | --- | --- | --- |
|
|
120
|
+
| `total` | `number` | — (required) | Total number of items across all pages. Values below `0` and non-finite values are treated as `0`; fractions are floored. |
|
|
121
|
+
| `pageSize` | `number` | `10` | Items per page. Clamped to a minimum of `1`. |
|
|
122
|
+
| `page` | `number` | `undefined` | **Controlled** current page (1-based). When provided, the returned `page` mirrors this prop (clamped) and the controls only call `onChange`. |
|
|
123
|
+
| `defaultPage` | `number` | `1` | Initial page in **uncontrolled** mode. Ignored while controlled. |
|
|
124
|
+
| `siblingCount` | `number` | `1` | Pages shown on each side of the current page in `items`. |
|
|
125
|
+
| `boundaryCount` | `number` | `1` | Pages shown at the start and end in `items`. |
|
|
126
|
+
| `onChange` | `(page: number) => void` | `undefined` | Called with the next page whenever the page changes. Its identity may change between renders freely. |
|
|
127
|
+
|
|
128
|
+
### Returns — `UsePaginationReturn`
|
|
129
|
+
|
|
130
|
+
| Property | Type | Description |
|
|
131
|
+
| --- | --- | --- |
|
|
132
|
+
| `page` | `number` | Current page, 1-based, always within `[1, pageCount]`. |
|
|
133
|
+
| `pageCount` | `number` | Number of pages, always `>= 1` (an empty dataset still has one empty page). |
|
|
134
|
+
| `pageSize` | `number` | The effective page size (clamped to `>= 1`). |
|
|
135
|
+
| `setPage` | `(page: number) => void` | Go to a specific page. Argument is clamped and floored; going to the current page is a no-op. Stable identity. |
|
|
136
|
+
| `next` | `() => void` | Go to the next page (no-op on the last). Stable identity. |
|
|
137
|
+
| `prev` | `() => void` | Go to the previous page (no-op on the first). Stable identity. |
|
|
138
|
+
| `first` | `() => void` | Go to the first page. Stable identity. |
|
|
139
|
+
| `last` | `() => void` | Go to the last page. Stable identity. |
|
|
140
|
+
| `canNext` | `boolean` | `true` when `page < pageCount`. |
|
|
141
|
+
| `canPrev` | `boolean` | `true` when `page > 1`. |
|
|
142
|
+
| `range` | `PaginationRange` | 0-based `{ start, end }` window of the current page — see below. |
|
|
143
|
+
| `items` | `PaginationItem[]` | Ellipsis-aware pager model — see below. |
|
|
144
|
+
|
|
145
|
+
### `range` — `PaginationRange`
|
|
146
|
+
|
|
147
|
+
The 0-based index window for slicing your data array:
|
|
148
|
+
|
|
149
|
+
- `start` — index of the first item on the current page (**inclusive**).
|
|
150
|
+
- `end` — index **one past** the last item (**exclusive**), clamped to `total`.
|
|
151
|
+
|
|
152
|
+
So `array.slice(range.start, range.end)` yields exactly the current page's items. For an empty dataset, `start === end === 0`.
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
// total: 25, pageSize: 10, page: 3 → { start: 20, end: 25 }
|
|
156
|
+
data.slice(20, 25); // the last 5 items
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### `items` — `PaginationItem[]`
|
|
160
|
+
|
|
161
|
+
The ordered model for rendering a pager, MUI/Mantine-style. Each entry is either a page button or an `"ellipsis"` gap:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
interface PaginationItem {
|
|
165
|
+
type: "page" | "ellipsis";
|
|
166
|
+
page: number | null; // 1-based page for "page", null for "ellipsis"
|
|
167
|
+
selected: boolean; // true only for the current page
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`siblingCount` (pages around the current page) and `boundaryCount` (pages pinned at each end) control how the middle collapses. When a gap would hide exactly one page, that page is shown instead of an ellipsis:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
// pageCount 50, page 25, siblingCount 1, boundaryCount 1:
|
|
175
|
+
// [1, "ellipsis", 24, 25, 26, "ellipsis", 50]
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The pure builder is also exported for non-React use:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import { buildPaginationRange, getPageCount } from "@usefy/use-pagination";
|
|
182
|
+
|
|
183
|
+
getPageCount(95, 10); // 10
|
|
184
|
+
buildPaginationRange({ page: 1, pageCount: 10 }); // [1, 2, 3, 4, 5, "ellipsis", 10]
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Behavior notes
|
|
188
|
+
|
|
189
|
+
- **Clamping is derived.** The current page is clamped **presentationally** into `[1, pageCount]` — it is never written back into state, so shrinking `total` never fires a surprise `onChange`. In **controlled** mode this means a `page` prop that is out of range is displayed clamped but not corrected via `onChange`; pass a valid `page`. Any navigation interaction (`next`/`prev`/`setPage`) commits a fresh in-range value.
|
|
190
|
+
- **No-op skipping.** Moving to the page you're already on — including `next`/`prev` at a bound — does not re-render or call `onChange`.
|
|
191
|
+
- **SSR-safe.** Pure state and math; there is no `window`/`document` access, so it renders identically on the server and client.
|
|
192
|
+
|
|
193
|
+
## Testing
|
|
194
|
+
|
|
195
|
+
📊 <a href="https://mirunamu00.github.io/usefy/coverage/use-pagination/src/index.html" target="_blank" rel="noopener noreferrer"><strong>View Detailed Coverage Report</strong></a> (GitHub Pages) — **48 tests**, 100% statement coverage.
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
MIT © [mirunamu](https://github.com/mirunamu00)
|
|
200
|
+
|
|
201
|
+
This package is part of the [usefy](https://github.com/mirunamu00/usefy) monorepo.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for the {@link usePagination} hook.
|
|
3
|
+
*/
|
|
4
|
+
interface UsePaginationOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Total number of items across all pages. Values below `0` (and non-finite
|
|
7
|
+
* values) are treated as `0`. Fractional values are floored.
|
|
8
|
+
*/
|
|
9
|
+
total: number;
|
|
10
|
+
/**
|
|
11
|
+
* Number of items per page.
|
|
12
|
+
*
|
|
13
|
+
* @defaultValue 10
|
|
14
|
+
* @remarks Clamped to a minimum of `1` — a page size of `0` or a negative
|
|
15
|
+
* value would make `pageCount` meaningless, so it is coerced to `1`.
|
|
16
|
+
*/
|
|
17
|
+
pageSize?: number;
|
|
18
|
+
/**
|
|
19
|
+
* The **controlled** current page (1-based). When provided (not `undefined`),
|
|
20
|
+
* the hook runs in controlled mode: the returned `page` mirrors this prop
|
|
21
|
+
* (clamped into `[1, pageCount]`) and `setPage`/`next`/`prev`/… only call
|
|
22
|
+
* `onChange` — the parent owns the value.
|
|
23
|
+
*
|
|
24
|
+
* Pass `undefined` to run uncontrolled, seeded from {@link defaultPage}.
|
|
25
|
+
*/
|
|
26
|
+
page?: number;
|
|
27
|
+
/**
|
|
28
|
+
* The initial current page (1-based) in **uncontrolled** mode. Ignored while
|
|
29
|
+
* controlled.
|
|
30
|
+
*
|
|
31
|
+
* @defaultValue 1
|
|
32
|
+
*/
|
|
33
|
+
defaultPage?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Number of always-visible page buttons on each side of the current page in
|
|
36
|
+
* the {@link UsePaginationReturn.items} model.
|
|
37
|
+
*
|
|
38
|
+
* @defaultValue 1
|
|
39
|
+
*/
|
|
40
|
+
siblingCount?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Number of always-visible page buttons at the start and end (the boundaries)
|
|
43
|
+
* in the {@link UsePaginationReturn.items} model.
|
|
44
|
+
*
|
|
45
|
+
* @defaultValue 1
|
|
46
|
+
*/
|
|
47
|
+
boundaryCount?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Called with the next page whenever the current page changes — via any of the
|
|
50
|
+
* navigation controls in uncontrolled mode, or when a control requests a
|
|
51
|
+
* *different* page in controlled mode. Its identity may change between renders
|
|
52
|
+
* without any effect on the hook (the latest callback is always used).
|
|
53
|
+
*/
|
|
54
|
+
onChange?: (page: number) => void;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The kind of a {@link PaginationItem}: a real page button, or an `"ellipsis"`
|
|
58
|
+
* gap token used to collapse a long run of pages.
|
|
59
|
+
*/
|
|
60
|
+
type PaginationItemType = "page" | "ellipsis";
|
|
61
|
+
/**
|
|
62
|
+
* A single entry in the ellipsis-aware page-number model
|
|
63
|
+
* ({@link UsePaginationReturn.items}) used to render a pager UI.
|
|
64
|
+
*/
|
|
65
|
+
interface PaginationItem {
|
|
66
|
+
/** Whether this entry is a clickable page or an `"ellipsis"` gap. */
|
|
67
|
+
type: PaginationItemType;
|
|
68
|
+
/**
|
|
69
|
+
* The 1-based page number for a `"page"` entry, or `null` for an `"ellipsis"`.
|
|
70
|
+
*/
|
|
71
|
+
page: number | null;
|
|
72
|
+
/** `true` when this is the current page (always `false` for `"ellipsis"`). */
|
|
73
|
+
selected: boolean;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The 0-based index window of the current page, ready to slice an array with
|
|
77
|
+
* `array.slice(range.start, range.end)`.
|
|
78
|
+
*/
|
|
79
|
+
interface PaginationRange {
|
|
80
|
+
/** 0-based index of the first item on the current page (**inclusive**). */
|
|
81
|
+
readonly start: number;
|
|
82
|
+
/**
|
|
83
|
+
* 0-based index **one past** the last item on the current page
|
|
84
|
+
* (**exclusive**), clamped to `total`. `array.slice(start, end)` yields the
|
|
85
|
+
* current page's items. For an empty dataset, `start === end === 0`.
|
|
86
|
+
*/
|
|
87
|
+
readonly end: number;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Return value of {@link usePagination}.
|
|
91
|
+
*/
|
|
92
|
+
interface UsePaginationReturn {
|
|
93
|
+
/** The current page, 1-based, always within `[1, pageCount]`. */
|
|
94
|
+
page: number;
|
|
95
|
+
/**
|
|
96
|
+
* Total number of pages, always `>= 1` (an empty dataset still has one empty
|
|
97
|
+
* page). Equals `Math.max(1, Math.ceil(total / pageSize))`.
|
|
98
|
+
*/
|
|
99
|
+
pageCount: number;
|
|
100
|
+
/** The effective page size (clamped to a minimum of `1`). */
|
|
101
|
+
pageSize: number;
|
|
102
|
+
/**
|
|
103
|
+
* Go to a specific page. The argument is clamped into `[1, pageCount]` and
|
|
104
|
+
* floored; going to the page you are already on is a no-op (no re-render, no
|
|
105
|
+
* `onChange`). Stable identity.
|
|
106
|
+
*/
|
|
107
|
+
setPage: (page: number) => void;
|
|
108
|
+
/** Go to the next page (no-op on the last page). Stable identity. */
|
|
109
|
+
next: () => void;
|
|
110
|
+
/** Go to the previous page (no-op on the first page). Stable identity. */
|
|
111
|
+
prev: () => void;
|
|
112
|
+
/** Go to the first page. Stable identity. */
|
|
113
|
+
first: () => void;
|
|
114
|
+
/** Go to the last page. Stable identity. */
|
|
115
|
+
last: () => void;
|
|
116
|
+
/** `true` when there is a next page (`page < pageCount`). */
|
|
117
|
+
canNext: boolean;
|
|
118
|
+
/** `true` when there is a previous page (`page > 1`). */
|
|
119
|
+
canPrev: boolean;
|
|
120
|
+
/**
|
|
121
|
+
* The 0-based index window of the current page for slicing your data array.
|
|
122
|
+
* See {@link PaginationRange}.
|
|
123
|
+
*/
|
|
124
|
+
range: PaginationRange;
|
|
125
|
+
/**
|
|
126
|
+
* The ellipsis-aware ordered list of page buttons and gap tokens to render a
|
|
127
|
+
* pager UI, driven by `siblingCount`/`boundaryCount`. Read-only — it is a
|
|
128
|
+
* memoized derived model, so treat it as immutable. See {@link PaginationItem}.
|
|
129
|
+
*/
|
|
130
|
+
items: readonly PaginationItem[];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A headless pagination state machine — the current page (controlled or
|
|
135
|
+
* uncontrolled), the derived page count, a slice-ready index range, and an
|
|
136
|
+
* ellipsis-aware page-number model for rendering a pager UI. It owns *state*
|
|
137
|
+
* only; you render however you like.
|
|
138
|
+
*
|
|
139
|
+
* The current page is always kept within `[1, pageCount]`: it is clamped
|
|
140
|
+
* **derived** from `total`/`pageSize`, so when the dataset shrinks the returned
|
|
141
|
+
* `page` never points past the last page and the navigation controls stay
|
|
142
|
+
* correct (a plain re-clamp, no surprise `onChange`). All controls
|
|
143
|
+
* (`setPage`/`next`/`prev`/`first`/`last`) have stable identities and skip
|
|
144
|
+
* no-op moves (staying on the current page does not re-render or fire
|
|
145
|
+
* `onChange`).
|
|
146
|
+
*
|
|
147
|
+
* Works in both modes, mirroring `useControllableState`:
|
|
148
|
+
* - **Uncontrolled** (`page` omitted): the hook owns the page, seeded from
|
|
149
|
+
* `defaultPage`; `onChange` fires after each committed change.
|
|
150
|
+
* - **Controlled** (`page` provided): the returned `page` mirrors the prop
|
|
151
|
+
* (clamped) and the controls only call `onChange` — the parent owns the value.
|
|
152
|
+
*
|
|
153
|
+
* @param options - See {@link UsePaginationOptions}.
|
|
154
|
+
* @returns The pagination state and controls — see {@link UsePaginationReturn}.
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```tsx
|
|
158
|
+
* function UsersTable({ users }: { users: User[] }) {
|
|
159
|
+
* const { page, pageCount, range, items, setPage, next, prev, canNext, canPrev } =
|
|
160
|
+
* usePagination({ total: users.length, pageSize: 10 });
|
|
161
|
+
*
|
|
162
|
+
* const visible = users.slice(range.start, range.end);
|
|
163
|
+
*
|
|
164
|
+
* return (
|
|
165
|
+
* <>
|
|
166
|
+
* <ul>{visible.map((u) => <li key={u.id}>{u.name}</li>)}</ul>
|
|
167
|
+
* <nav>
|
|
168
|
+
* <button onClick={prev} disabled={!canPrev}>Prev</button>
|
|
169
|
+
* {items.map((item, i) =>
|
|
170
|
+
* item.type === "ellipsis" ? (
|
|
171
|
+
* <span key={`gap-${i}`}>…</span>
|
|
172
|
+
* ) : (
|
|
173
|
+
* <button
|
|
174
|
+
* key={item.page}
|
|
175
|
+
* aria-current={item.selected}
|
|
176
|
+
* onClick={() => setPage(item.page!)}
|
|
177
|
+
* >
|
|
178
|
+
* {item.page}
|
|
179
|
+
* </button>
|
|
180
|
+
* )
|
|
181
|
+
* )}
|
|
182
|
+
* <button onClick={next} disabled={!canNext}>Next</button>
|
|
183
|
+
* </nav>
|
|
184
|
+
* <p>Page {page} of {pageCount}</p>
|
|
185
|
+
* </>
|
|
186
|
+
* );
|
|
187
|
+
* }
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
declare function usePagination(options: UsePaginationOptions): UsePaginationReturn;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Compute the number of pages for a dataset. Always returns at least `1` — an
|
|
194
|
+
* empty dataset still has a single (empty) page — so it is safe to use directly
|
|
195
|
+
* as the upper clamp bound. `total` is treated as `>= 0` and `pageSize` as
|
|
196
|
+
* `>= 1` (both floored; non-finite inputs are normalized).
|
|
197
|
+
*
|
|
198
|
+
* @param total - Total number of items.
|
|
199
|
+
* @param pageSize - Items per page.
|
|
200
|
+
* @returns The page count, `>= 1`.
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```ts
|
|
204
|
+
* getPageCount(0, 10); // 1
|
|
205
|
+
* getPageCount(25, 10); // 3
|
|
206
|
+
* getPageCount(30, 10); // 3
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
declare function getPageCount(total: number, pageSize: number): number;
|
|
210
|
+
/**
|
|
211
|
+
* Clamp a requested page into `[1, pageCount]`, flooring fractions. Non-finite
|
|
212
|
+
* requests fall back to page `1`. `pageCount` is assumed to be `>= 1` (as
|
|
213
|
+
* returned by {@link getPageCount}).
|
|
214
|
+
*
|
|
215
|
+
* @param page - The requested 1-based page.
|
|
216
|
+
* @param pageCount - The number of pages (upper bound).
|
|
217
|
+
* @returns The clamped page, within `[1, pageCount]`.
|
|
218
|
+
*/
|
|
219
|
+
declare function clampPage(page: number, pageCount: number): number;
|
|
220
|
+
/**
|
|
221
|
+
* Parameters for {@link buildPaginationRange}.
|
|
222
|
+
*/
|
|
223
|
+
interface BuildPaginationRangeParams {
|
|
224
|
+
/** The current (clamped) 1-based page. */
|
|
225
|
+
page: number;
|
|
226
|
+
/** Total number of pages (`>= 1`). */
|
|
227
|
+
pageCount: number;
|
|
228
|
+
/** Pages shown on each side of the current page. @defaultValue 1 */
|
|
229
|
+
siblingCount?: number;
|
|
230
|
+
/** Pages shown at the start and end. @defaultValue 1 */
|
|
231
|
+
boundaryCount?: number;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Build the ordered list of page numbers with `"ellipsis"` gap tokens for a
|
|
235
|
+
* pager UI — the well-known MUI/Mantine `usePagination` model. Always shows the
|
|
236
|
+
* `boundaryCount` first/last pages and the `siblingCount` pages around the
|
|
237
|
+
* current page, collapsing anything in between into a single `"ellipsis"`. When
|
|
238
|
+
* a gap would hide exactly one page, that page is shown instead of an ellipsis.
|
|
239
|
+
*
|
|
240
|
+
* @param params - See {@link BuildPaginationRangeParams}.
|
|
241
|
+
* @returns An ordered array of 1-based page numbers and `"ellipsis"` tokens.
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```ts
|
|
245
|
+
* buildPaginationRange({ page: 1, pageCount: 10 });
|
|
246
|
+
* // [1, 2, 3, 4, 5, "ellipsis", 10]
|
|
247
|
+
* buildPaginationRange({ page: 6, pageCount: 10 });
|
|
248
|
+
* // [1, "ellipsis", 5, 6, 7, "ellipsis", 10]
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
declare function buildPaginationRange(params: BuildPaginationRangeParams): (number | "ellipsis")[];
|
|
252
|
+
|
|
253
|
+
export { type BuildPaginationRangeParams, type PaginationItem, type PaginationItemType, type PaginationRange, type UsePaginationOptions, type UsePaginationReturn, buildPaginationRange, clampPage, getPageCount, usePagination };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for the {@link usePagination} hook.
|
|
3
|
+
*/
|
|
4
|
+
interface UsePaginationOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Total number of items across all pages. Values below `0` (and non-finite
|
|
7
|
+
* values) are treated as `0`. Fractional values are floored.
|
|
8
|
+
*/
|
|
9
|
+
total: number;
|
|
10
|
+
/**
|
|
11
|
+
* Number of items per page.
|
|
12
|
+
*
|
|
13
|
+
* @defaultValue 10
|
|
14
|
+
* @remarks Clamped to a minimum of `1` — a page size of `0` or a negative
|
|
15
|
+
* value would make `pageCount` meaningless, so it is coerced to `1`.
|
|
16
|
+
*/
|
|
17
|
+
pageSize?: number;
|
|
18
|
+
/**
|
|
19
|
+
* The **controlled** current page (1-based). When provided (not `undefined`),
|
|
20
|
+
* the hook runs in controlled mode: the returned `page` mirrors this prop
|
|
21
|
+
* (clamped into `[1, pageCount]`) and `setPage`/`next`/`prev`/… only call
|
|
22
|
+
* `onChange` — the parent owns the value.
|
|
23
|
+
*
|
|
24
|
+
* Pass `undefined` to run uncontrolled, seeded from {@link defaultPage}.
|
|
25
|
+
*/
|
|
26
|
+
page?: number;
|
|
27
|
+
/**
|
|
28
|
+
* The initial current page (1-based) in **uncontrolled** mode. Ignored while
|
|
29
|
+
* controlled.
|
|
30
|
+
*
|
|
31
|
+
* @defaultValue 1
|
|
32
|
+
*/
|
|
33
|
+
defaultPage?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Number of always-visible page buttons on each side of the current page in
|
|
36
|
+
* the {@link UsePaginationReturn.items} model.
|
|
37
|
+
*
|
|
38
|
+
* @defaultValue 1
|
|
39
|
+
*/
|
|
40
|
+
siblingCount?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Number of always-visible page buttons at the start and end (the boundaries)
|
|
43
|
+
* in the {@link UsePaginationReturn.items} model.
|
|
44
|
+
*
|
|
45
|
+
* @defaultValue 1
|
|
46
|
+
*/
|
|
47
|
+
boundaryCount?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Called with the next page whenever the current page changes — via any of the
|
|
50
|
+
* navigation controls in uncontrolled mode, or when a control requests a
|
|
51
|
+
* *different* page in controlled mode. Its identity may change between renders
|
|
52
|
+
* without any effect on the hook (the latest callback is always used).
|
|
53
|
+
*/
|
|
54
|
+
onChange?: (page: number) => void;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The kind of a {@link PaginationItem}: a real page button, or an `"ellipsis"`
|
|
58
|
+
* gap token used to collapse a long run of pages.
|
|
59
|
+
*/
|
|
60
|
+
type PaginationItemType = "page" | "ellipsis";
|
|
61
|
+
/**
|
|
62
|
+
* A single entry in the ellipsis-aware page-number model
|
|
63
|
+
* ({@link UsePaginationReturn.items}) used to render a pager UI.
|
|
64
|
+
*/
|
|
65
|
+
interface PaginationItem {
|
|
66
|
+
/** Whether this entry is a clickable page or an `"ellipsis"` gap. */
|
|
67
|
+
type: PaginationItemType;
|
|
68
|
+
/**
|
|
69
|
+
* The 1-based page number for a `"page"` entry, or `null` for an `"ellipsis"`.
|
|
70
|
+
*/
|
|
71
|
+
page: number | null;
|
|
72
|
+
/** `true` when this is the current page (always `false` for `"ellipsis"`). */
|
|
73
|
+
selected: boolean;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The 0-based index window of the current page, ready to slice an array with
|
|
77
|
+
* `array.slice(range.start, range.end)`.
|
|
78
|
+
*/
|
|
79
|
+
interface PaginationRange {
|
|
80
|
+
/** 0-based index of the first item on the current page (**inclusive**). */
|
|
81
|
+
readonly start: number;
|
|
82
|
+
/**
|
|
83
|
+
* 0-based index **one past** the last item on the current page
|
|
84
|
+
* (**exclusive**), clamped to `total`. `array.slice(start, end)` yields the
|
|
85
|
+
* current page's items. For an empty dataset, `start === end === 0`.
|
|
86
|
+
*/
|
|
87
|
+
readonly end: number;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Return value of {@link usePagination}.
|
|
91
|
+
*/
|
|
92
|
+
interface UsePaginationReturn {
|
|
93
|
+
/** The current page, 1-based, always within `[1, pageCount]`. */
|
|
94
|
+
page: number;
|
|
95
|
+
/**
|
|
96
|
+
* Total number of pages, always `>= 1` (an empty dataset still has one empty
|
|
97
|
+
* page). Equals `Math.max(1, Math.ceil(total / pageSize))`.
|
|
98
|
+
*/
|
|
99
|
+
pageCount: number;
|
|
100
|
+
/** The effective page size (clamped to a minimum of `1`). */
|
|
101
|
+
pageSize: number;
|
|
102
|
+
/**
|
|
103
|
+
* Go to a specific page. The argument is clamped into `[1, pageCount]` and
|
|
104
|
+
* floored; going to the page you are already on is a no-op (no re-render, no
|
|
105
|
+
* `onChange`). Stable identity.
|
|
106
|
+
*/
|
|
107
|
+
setPage: (page: number) => void;
|
|
108
|
+
/** Go to the next page (no-op on the last page). Stable identity. */
|
|
109
|
+
next: () => void;
|
|
110
|
+
/** Go to the previous page (no-op on the first page). Stable identity. */
|
|
111
|
+
prev: () => void;
|
|
112
|
+
/** Go to the first page. Stable identity. */
|
|
113
|
+
first: () => void;
|
|
114
|
+
/** Go to the last page. Stable identity. */
|
|
115
|
+
last: () => void;
|
|
116
|
+
/** `true` when there is a next page (`page < pageCount`). */
|
|
117
|
+
canNext: boolean;
|
|
118
|
+
/** `true` when there is a previous page (`page > 1`). */
|
|
119
|
+
canPrev: boolean;
|
|
120
|
+
/**
|
|
121
|
+
* The 0-based index window of the current page for slicing your data array.
|
|
122
|
+
* See {@link PaginationRange}.
|
|
123
|
+
*/
|
|
124
|
+
range: PaginationRange;
|
|
125
|
+
/**
|
|
126
|
+
* The ellipsis-aware ordered list of page buttons and gap tokens to render a
|
|
127
|
+
* pager UI, driven by `siblingCount`/`boundaryCount`. Read-only — it is a
|
|
128
|
+
* memoized derived model, so treat it as immutable. See {@link PaginationItem}.
|
|
129
|
+
*/
|
|
130
|
+
items: readonly PaginationItem[];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A headless pagination state machine — the current page (controlled or
|
|
135
|
+
* uncontrolled), the derived page count, a slice-ready index range, and an
|
|
136
|
+
* ellipsis-aware page-number model for rendering a pager UI. It owns *state*
|
|
137
|
+
* only; you render however you like.
|
|
138
|
+
*
|
|
139
|
+
* The current page is always kept within `[1, pageCount]`: it is clamped
|
|
140
|
+
* **derived** from `total`/`pageSize`, so when the dataset shrinks the returned
|
|
141
|
+
* `page` never points past the last page and the navigation controls stay
|
|
142
|
+
* correct (a plain re-clamp, no surprise `onChange`). All controls
|
|
143
|
+
* (`setPage`/`next`/`prev`/`first`/`last`) have stable identities and skip
|
|
144
|
+
* no-op moves (staying on the current page does not re-render or fire
|
|
145
|
+
* `onChange`).
|
|
146
|
+
*
|
|
147
|
+
* Works in both modes, mirroring `useControllableState`:
|
|
148
|
+
* - **Uncontrolled** (`page` omitted): the hook owns the page, seeded from
|
|
149
|
+
* `defaultPage`; `onChange` fires after each committed change.
|
|
150
|
+
* - **Controlled** (`page` provided): the returned `page` mirrors the prop
|
|
151
|
+
* (clamped) and the controls only call `onChange` — the parent owns the value.
|
|
152
|
+
*
|
|
153
|
+
* @param options - See {@link UsePaginationOptions}.
|
|
154
|
+
* @returns The pagination state and controls — see {@link UsePaginationReturn}.
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```tsx
|
|
158
|
+
* function UsersTable({ users }: { users: User[] }) {
|
|
159
|
+
* const { page, pageCount, range, items, setPage, next, prev, canNext, canPrev } =
|
|
160
|
+
* usePagination({ total: users.length, pageSize: 10 });
|
|
161
|
+
*
|
|
162
|
+
* const visible = users.slice(range.start, range.end);
|
|
163
|
+
*
|
|
164
|
+
* return (
|
|
165
|
+
* <>
|
|
166
|
+
* <ul>{visible.map((u) => <li key={u.id}>{u.name}</li>)}</ul>
|
|
167
|
+
* <nav>
|
|
168
|
+
* <button onClick={prev} disabled={!canPrev}>Prev</button>
|
|
169
|
+
* {items.map((item, i) =>
|
|
170
|
+
* item.type === "ellipsis" ? (
|
|
171
|
+
* <span key={`gap-${i}`}>…</span>
|
|
172
|
+
* ) : (
|
|
173
|
+
* <button
|
|
174
|
+
* key={item.page}
|
|
175
|
+
* aria-current={item.selected}
|
|
176
|
+
* onClick={() => setPage(item.page!)}
|
|
177
|
+
* >
|
|
178
|
+
* {item.page}
|
|
179
|
+
* </button>
|
|
180
|
+
* )
|
|
181
|
+
* )}
|
|
182
|
+
* <button onClick={next} disabled={!canNext}>Next</button>
|
|
183
|
+
* </nav>
|
|
184
|
+
* <p>Page {page} of {pageCount}</p>
|
|
185
|
+
* </>
|
|
186
|
+
* );
|
|
187
|
+
* }
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
declare function usePagination(options: UsePaginationOptions): UsePaginationReturn;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Compute the number of pages for a dataset. Always returns at least `1` — an
|
|
194
|
+
* empty dataset still has a single (empty) page — so it is safe to use directly
|
|
195
|
+
* as the upper clamp bound. `total` is treated as `>= 0` and `pageSize` as
|
|
196
|
+
* `>= 1` (both floored; non-finite inputs are normalized).
|
|
197
|
+
*
|
|
198
|
+
* @param total - Total number of items.
|
|
199
|
+
* @param pageSize - Items per page.
|
|
200
|
+
* @returns The page count, `>= 1`.
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```ts
|
|
204
|
+
* getPageCount(0, 10); // 1
|
|
205
|
+
* getPageCount(25, 10); // 3
|
|
206
|
+
* getPageCount(30, 10); // 3
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
declare function getPageCount(total: number, pageSize: number): number;
|
|
210
|
+
/**
|
|
211
|
+
* Clamp a requested page into `[1, pageCount]`, flooring fractions. Non-finite
|
|
212
|
+
* requests fall back to page `1`. `pageCount` is assumed to be `>= 1` (as
|
|
213
|
+
* returned by {@link getPageCount}).
|
|
214
|
+
*
|
|
215
|
+
* @param page - The requested 1-based page.
|
|
216
|
+
* @param pageCount - The number of pages (upper bound).
|
|
217
|
+
* @returns The clamped page, within `[1, pageCount]`.
|
|
218
|
+
*/
|
|
219
|
+
declare function clampPage(page: number, pageCount: number): number;
|
|
220
|
+
/**
|
|
221
|
+
* Parameters for {@link buildPaginationRange}.
|
|
222
|
+
*/
|
|
223
|
+
interface BuildPaginationRangeParams {
|
|
224
|
+
/** The current (clamped) 1-based page. */
|
|
225
|
+
page: number;
|
|
226
|
+
/** Total number of pages (`>= 1`). */
|
|
227
|
+
pageCount: number;
|
|
228
|
+
/** Pages shown on each side of the current page. @defaultValue 1 */
|
|
229
|
+
siblingCount?: number;
|
|
230
|
+
/** Pages shown at the start and end. @defaultValue 1 */
|
|
231
|
+
boundaryCount?: number;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Build the ordered list of page numbers with `"ellipsis"` gap tokens for a
|
|
235
|
+
* pager UI — the well-known MUI/Mantine `usePagination` model. Always shows the
|
|
236
|
+
* `boundaryCount` first/last pages and the `siblingCount` pages around the
|
|
237
|
+
* current page, collapsing anything in between into a single `"ellipsis"`. When
|
|
238
|
+
* a gap would hide exactly one page, that page is shown instead of an ellipsis.
|
|
239
|
+
*
|
|
240
|
+
* @param params - See {@link BuildPaginationRangeParams}.
|
|
241
|
+
* @returns An ordered array of 1-based page numbers and `"ellipsis"` tokens.
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```ts
|
|
245
|
+
* buildPaginationRange({ page: 1, pageCount: 10 });
|
|
246
|
+
* // [1, 2, 3, 4, 5, "ellipsis", 10]
|
|
247
|
+
* buildPaginationRange({ page: 6, pageCount: 10 });
|
|
248
|
+
* // [1, "ellipsis", 5, 6, 7, "ellipsis", 10]
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
declare function buildPaginationRange(params: BuildPaginationRangeParams): (number | "ellipsis")[];
|
|
252
|
+
|
|
253
|
+
export { type BuildPaginationRangeParams, type PaginationItem, type PaginationItemType, type PaginationRange, type UsePaginationOptions, type UsePaginationReturn, buildPaginationRange, clampPage, getPageCount, usePagination };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
buildPaginationRange: () => buildPaginationRange,
|
|
24
|
+
clampPage: () => clampPage,
|
|
25
|
+
getPageCount: () => getPageCount,
|
|
26
|
+
usePagination: () => usePagination
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/usePagination.ts
|
|
31
|
+
var import_react = require("react");
|
|
32
|
+
var import_use_controllable_state = require("@usefy/use-controllable-state");
|
|
33
|
+
|
|
34
|
+
// src/utils.ts
|
|
35
|
+
function toSafeInt(value, min, fallback) {
|
|
36
|
+
if (!Number.isFinite(value)) {
|
|
37
|
+
return fallback;
|
|
38
|
+
}
|
|
39
|
+
const floored = Math.floor(value);
|
|
40
|
+
return floored < min ? min : floored;
|
|
41
|
+
}
|
|
42
|
+
function getPageCount(total, pageSize) {
|
|
43
|
+
const safeTotal = toSafeInt(total, 0, 0);
|
|
44
|
+
const safeSize = toSafeInt(pageSize, 1, 1);
|
|
45
|
+
return Math.max(1, Math.ceil(safeTotal / safeSize));
|
|
46
|
+
}
|
|
47
|
+
function clampPage(page, pageCount) {
|
|
48
|
+
if (!Number.isFinite(page)) {
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
const floored = Math.floor(page);
|
|
52
|
+
if (floored < 1) {
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
return floored > pageCount ? pageCount : floored;
|
|
56
|
+
}
|
|
57
|
+
var inclusiveRange = (start, end) => start > end ? [] : Array.from({ length: end - start + 1 }, (_, index) => start + index);
|
|
58
|
+
function buildPaginationRange(params) {
|
|
59
|
+
const count = Math.max(1, Math.floor(params.pageCount));
|
|
60
|
+
const page = clampPage(params.page, count);
|
|
61
|
+
const siblingCount = Math.max(0, Math.floor(params.siblingCount ?? 1));
|
|
62
|
+
const boundaryCount = Math.max(0, Math.floor(params.boundaryCount ?? 1));
|
|
63
|
+
const startPages = inclusiveRange(1, Math.min(boundaryCount, count));
|
|
64
|
+
const endPages = inclusiveRange(
|
|
65
|
+
Math.max(count - boundaryCount + 1, boundaryCount + 1),
|
|
66
|
+
count
|
|
67
|
+
);
|
|
68
|
+
const siblingsStart = Math.max(
|
|
69
|
+
Math.min(page - siblingCount, count - boundaryCount - siblingCount * 2 - 1),
|
|
70
|
+
boundaryCount + 2
|
|
71
|
+
);
|
|
72
|
+
const siblingsEnd = Math.min(
|
|
73
|
+
Math.max(page + siblingCount, boundaryCount + siblingCount * 2 + 2),
|
|
74
|
+
endPages.length > 0 ? endPages[0] - 2 : count - 1
|
|
75
|
+
);
|
|
76
|
+
return [
|
|
77
|
+
...startPages,
|
|
78
|
+
// Start gap: an ellipsis, or the single page it would have hidden.
|
|
79
|
+
...siblingsStart > boundaryCount + 2 ? ["ellipsis"] : boundaryCount + 1 < count - boundaryCount ? [boundaryCount + 1] : [],
|
|
80
|
+
...inclusiveRange(siblingsStart, siblingsEnd),
|
|
81
|
+
// End gap: an ellipsis, or the single page it would have hidden.
|
|
82
|
+
...siblingsEnd < count - boundaryCount - 1 ? ["ellipsis"] : count - boundaryCount > boundaryCount ? [count - boundaryCount] : [],
|
|
83
|
+
...endPages
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/usePagination.ts
|
|
88
|
+
var DEFAULT_PAGE_SIZE = 10;
|
|
89
|
+
var DEFAULT_SIBLING_COUNT = 1;
|
|
90
|
+
var DEFAULT_BOUNDARY_COUNT = 1;
|
|
91
|
+
function usePagination(options) {
|
|
92
|
+
const {
|
|
93
|
+
total: rawTotal,
|
|
94
|
+
pageSize: rawPageSize = DEFAULT_PAGE_SIZE,
|
|
95
|
+
page: controlledPage,
|
|
96
|
+
defaultPage = 1,
|
|
97
|
+
siblingCount = DEFAULT_SIBLING_COUNT,
|
|
98
|
+
boundaryCount = DEFAULT_BOUNDARY_COUNT,
|
|
99
|
+
onChange
|
|
100
|
+
} = options;
|
|
101
|
+
const total = toSafeInt(rawTotal, 0, 0);
|
|
102
|
+
const pageSize = toSafeInt(rawPageSize, 1, 1);
|
|
103
|
+
const pageCount = getPageCount(rawTotal, rawPageSize);
|
|
104
|
+
const [rawPage, setRawPage] = (0, import_use_controllable_state.useControllableState)({
|
|
105
|
+
value: controlledPage,
|
|
106
|
+
defaultValue: defaultPage,
|
|
107
|
+
onChange
|
|
108
|
+
});
|
|
109
|
+
const page = clampPage(rawPage, pageCount);
|
|
110
|
+
const stateRef = (0, import_react.useRef)({ page, pageCount });
|
|
111
|
+
stateRef.current = { page, pageCount };
|
|
112
|
+
const setRawPageRef = (0, import_react.useRef)(setRawPage);
|
|
113
|
+
setRawPageRef.current = setRawPage;
|
|
114
|
+
const setPage = (0, import_react.useCallback)((next2) => {
|
|
115
|
+
const { page: current, pageCount: count } = stateRef.current;
|
|
116
|
+
const target = clampPage(next2, count);
|
|
117
|
+
if (target === current) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
setRawPageRef.current(target);
|
|
121
|
+
}, []);
|
|
122
|
+
const next = (0, import_react.useCallback)(() => {
|
|
123
|
+
setPage(stateRef.current.page + 1);
|
|
124
|
+
}, [setPage]);
|
|
125
|
+
const prev = (0, import_react.useCallback)(() => {
|
|
126
|
+
setPage(stateRef.current.page - 1);
|
|
127
|
+
}, [setPage]);
|
|
128
|
+
const first = (0, import_react.useCallback)(() => {
|
|
129
|
+
setPage(1);
|
|
130
|
+
}, [setPage]);
|
|
131
|
+
const last = (0, import_react.useCallback)(() => {
|
|
132
|
+
setPage(stateRef.current.pageCount);
|
|
133
|
+
}, [setPage]);
|
|
134
|
+
const canNext = page < pageCount;
|
|
135
|
+
const canPrev = page > 1;
|
|
136
|
+
const range = (0, import_react.useMemo)(() => {
|
|
137
|
+
const start = (page - 1) * pageSize;
|
|
138
|
+
const end = Math.min(start + pageSize, total);
|
|
139
|
+
return { start, end };
|
|
140
|
+
}, [page, pageSize, total]);
|
|
141
|
+
const items = (0, import_react.useMemo)(
|
|
142
|
+
() => buildPaginationRange({
|
|
143
|
+
page,
|
|
144
|
+
pageCount,
|
|
145
|
+
siblingCount,
|
|
146
|
+
boundaryCount
|
|
147
|
+
}).map(
|
|
148
|
+
(entry) => entry === "ellipsis" ? { type: "ellipsis", page: null, selected: false } : { type: "page", page: entry, selected: entry === page }
|
|
149
|
+
),
|
|
150
|
+
[page, pageCount, siblingCount, boundaryCount]
|
|
151
|
+
);
|
|
152
|
+
return (0, import_react.useMemo)(
|
|
153
|
+
() => ({
|
|
154
|
+
page,
|
|
155
|
+
pageCount,
|
|
156
|
+
pageSize,
|
|
157
|
+
setPage,
|
|
158
|
+
next,
|
|
159
|
+
prev,
|
|
160
|
+
first,
|
|
161
|
+
last,
|
|
162
|
+
canNext,
|
|
163
|
+
canPrev,
|
|
164
|
+
range,
|
|
165
|
+
items
|
|
166
|
+
}),
|
|
167
|
+
[
|
|
168
|
+
page,
|
|
169
|
+
pageCount,
|
|
170
|
+
pageSize,
|
|
171
|
+
setPage,
|
|
172
|
+
next,
|
|
173
|
+
prev,
|
|
174
|
+
first,
|
|
175
|
+
last,
|
|
176
|
+
canNext,
|
|
177
|
+
canPrev,
|
|
178
|
+
range,
|
|
179
|
+
items
|
|
180
|
+
]
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
184
|
+
0 && (module.exports = {
|
|
185
|
+
buildPaginationRange,
|
|
186
|
+
clampPage,
|
|
187
|
+
getPageCount,
|
|
188
|
+
usePagination
|
|
189
|
+
});
|
|
190
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/usePagination.ts","../src/utils.ts"],"sourcesContent":["export { usePagination } from \"./usePagination\";\nexport type {\n UsePaginationOptions,\n UsePaginationReturn,\n PaginationItem,\n PaginationItemType,\n PaginationRange,\n} from \"./types\";\nexport {\n getPageCount,\n clampPage,\n buildPaginationRange,\n type BuildPaginationRangeParams,\n} from \"./utils\";\n","import { useCallback, useMemo, useRef } from \"react\";\nimport { useControllableState } from \"@usefy/use-controllable-state\";\nimport type {\n PaginationItem,\n PaginationRange,\n UsePaginationOptions,\n UsePaginationReturn,\n} from \"./types\";\nimport { buildPaginationRange, clampPage, getPageCount, toSafeInt } from \"./utils\";\n\nconst DEFAULT_PAGE_SIZE = 10;\nconst DEFAULT_SIBLING_COUNT = 1;\nconst DEFAULT_BOUNDARY_COUNT = 1;\n\n/**\n * A headless pagination state machine — the current page (controlled or\n * uncontrolled), the derived page count, a slice-ready index range, and an\n * ellipsis-aware page-number model for rendering a pager UI. It owns *state*\n * only; you render however you like.\n *\n * The current page is always kept within `[1, pageCount]`: it is clamped\n * **derived** from `total`/`pageSize`, so when the dataset shrinks the returned\n * `page` never points past the last page and the navigation controls stay\n * correct (a plain re-clamp, no surprise `onChange`). All controls\n * (`setPage`/`next`/`prev`/`first`/`last`) have stable identities and skip\n * no-op moves (staying on the current page does not re-render or fire\n * `onChange`).\n *\n * Works in both modes, mirroring `useControllableState`:\n * - **Uncontrolled** (`page` omitted): the hook owns the page, seeded from\n * `defaultPage`; `onChange` fires after each committed change.\n * - **Controlled** (`page` provided): the returned `page` mirrors the prop\n * (clamped) and the controls only call `onChange` — the parent owns the value.\n *\n * @param options - See {@link UsePaginationOptions}.\n * @returns The pagination state and controls — see {@link UsePaginationReturn}.\n *\n * @example\n * ```tsx\n * function UsersTable({ users }: { users: User[] }) {\n * const { page, pageCount, range, items, setPage, next, prev, canNext, canPrev } =\n * usePagination({ total: users.length, pageSize: 10 });\n *\n * const visible = users.slice(range.start, range.end);\n *\n * return (\n * <>\n * <ul>{visible.map((u) => <li key={u.id}>{u.name}</li>)}</ul>\n * <nav>\n * <button onClick={prev} disabled={!canPrev}>Prev</button>\n * {items.map((item, i) =>\n * item.type === \"ellipsis\" ? (\n * <span key={`gap-${i}`}>…</span>\n * ) : (\n * <button\n * key={item.page}\n * aria-current={item.selected}\n * onClick={() => setPage(item.page!)}\n * >\n * {item.page}\n * </button>\n * )\n * )}\n * <button onClick={next} disabled={!canNext}>Next</button>\n * </nav>\n * <p>Page {page} of {pageCount}</p>\n * </>\n * );\n * }\n * ```\n */\nexport function usePagination(\n options: UsePaginationOptions\n): UsePaginationReturn {\n const {\n total: rawTotal,\n pageSize: rawPageSize = DEFAULT_PAGE_SIZE,\n page: controlledPage,\n defaultPage = 1,\n siblingCount = DEFAULT_SIBLING_COUNT,\n boundaryCount = DEFAULT_BOUNDARY_COUNT,\n onChange,\n } = options;\n\n const total = toSafeInt(rawTotal, 0, 0);\n const pageSize = toSafeInt(rawPageSize, 1, 1);\n const pageCount = getPageCount(rawTotal, rawPageSize);\n\n // Raw (unclamped) page state, controllable. We always write *concrete*\n // clamped values through this setter (never updater functions), so the fact\n // that its internal value may drift out of range while the dataset shrinks\n // never leaks — the `page` below is always the clamped, derived value.\n const [rawPage, setRawPage] = useControllableState<number>({\n value: controlledPage,\n defaultValue: defaultPage,\n onChange,\n });\n\n const page = clampPage(rawPage, pageCount);\n\n // Mirror the latest derived values + setter into refs so the controls can be\n // fully identity-stable (empty dep arrays) while still reading fresh state.\n const stateRef = useRef({ page, pageCount });\n stateRef.current = { page, pageCount };\n\n const setRawPageRef = useRef(setRawPage);\n setRawPageRef.current = setRawPage;\n\n const setPage = useCallback((next: number) => {\n const { page: current, pageCount: count } = stateRef.current;\n const target = clampPage(next, count);\n if (target === current) {\n return; // no-op: staying put must not re-render or fire onChange\n }\n setRawPageRef.current(target);\n }, []);\n\n const next = useCallback(() => {\n setPage(stateRef.current.page + 1);\n }, [setPage]);\n\n const prev = useCallback(() => {\n setPage(stateRef.current.page - 1);\n }, [setPage]);\n\n const first = useCallback(() => {\n setPage(1);\n }, [setPage]);\n\n const last = useCallback(() => {\n setPage(stateRef.current.pageCount);\n }, [setPage]);\n\n const canNext = page < pageCount;\n const canPrev = page > 1;\n\n const range = useMemo<PaginationRange>(() => {\n const start = (page - 1) * pageSize;\n const end = Math.min(start + pageSize, total);\n return { start, end };\n }, [page, pageSize, total]);\n\n const items = useMemo<readonly PaginationItem[]>(\n () =>\n buildPaginationRange({\n page,\n pageCount,\n siblingCount,\n boundaryCount,\n }).map((entry) =>\n entry === \"ellipsis\"\n ? { type: \"ellipsis\", page: null, selected: false }\n : { type: \"page\", page: entry, selected: entry === page }\n ),\n [page, pageCount, siblingCount, boundaryCount]\n );\n\n return useMemo<UsePaginationReturn>(\n () => ({\n page,\n pageCount,\n pageSize,\n setPage,\n next,\n prev,\n first,\n last,\n canNext,\n canPrev,\n range,\n items,\n }),\n [\n page,\n pageCount,\n pageSize,\n setPage,\n next,\n prev,\n first,\n last,\n canNext,\n canPrev,\n range,\n items,\n ]\n );\n}\n","/**\n * Coerce a value to a safe integer, flooring fractions and enforcing a minimum.\n * Non-finite values (`NaN`, `Infinity`) fall back to `fallback`.\n *\n * @internal\n */\nexport function toSafeInt(value: number, min: number, fallback: number): number {\n if (!Number.isFinite(value)) {\n return fallback;\n }\n const floored = Math.floor(value);\n return floored < min ? min : floored;\n}\n\n/**\n * Compute the number of pages for a dataset. Always returns at least `1` — an\n * empty dataset still has a single (empty) page — so it is safe to use directly\n * as the upper clamp bound. `total` is treated as `>= 0` and `pageSize` as\n * `>= 1` (both floored; non-finite inputs are normalized).\n *\n * @param total - Total number of items.\n * @param pageSize - Items per page.\n * @returns The page count, `>= 1`.\n *\n * @example\n * ```ts\n * getPageCount(0, 10); // 1\n * getPageCount(25, 10); // 3\n * getPageCount(30, 10); // 3\n * ```\n */\nexport function getPageCount(total: number, pageSize: number): number {\n const safeTotal = toSafeInt(total, 0, 0);\n const safeSize = toSafeInt(pageSize, 1, 1);\n return Math.max(1, Math.ceil(safeTotal / safeSize));\n}\n\n/**\n * Clamp a requested page into `[1, pageCount]`, flooring fractions. Non-finite\n * requests fall back to page `1`. `pageCount` is assumed to be `>= 1` (as\n * returned by {@link getPageCount}).\n *\n * @param page - The requested 1-based page.\n * @param pageCount - The number of pages (upper bound).\n * @returns The clamped page, within `[1, pageCount]`.\n */\nexport function clampPage(page: number, pageCount: number): number {\n if (!Number.isFinite(page)) {\n return 1;\n }\n const floored = Math.floor(page);\n if (floored < 1) {\n return 1;\n }\n return floored > pageCount ? pageCount : floored;\n}\n\n/**\n * Parameters for {@link buildPaginationRange}.\n */\nexport interface BuildPaginationRangeParams {\n /** The current (clamped) 1-based page. */\n page: number;\n /** Total number of pages (`>= 1`). */\n pageCount: number;\n /** Pages shown on each side of the current page. @defaultValue 1 */\n siblingCount?: number;\n /** Pages shown at the start and end. @defaultValue 1 */\n boundaryCount?: number;\n}\n\nconst inclusiveRange = (start: number, end: number): number[] =>\n start > end\n ? []\n : Array.from({ length: end - start + 1 }, (_, index) => start + index);\n\n/**\n * Build the ordered list of page numbers with `\"ellipsis\"` gap tokens for a\n * pager UI — the well-known MUI/Mantine `usePagination` model. Always shows the\n * `boundaryCount` first/last pages and the `siblingCount` pages around the\n * current page, collapsing anything in between into a single `\"ellipsis\"`. When\n * a gap would hide exactly one page, that page is shown instead of an ellipsis.\n *\n * @param params - See {@link BuildPaginationRangeParams}.\n * @returns An ordered array of 1-based page numbers and `\"ellipsis\"` tokens.\n *\n * @example\n * ```ts\n * buildPaginationRange({ page: 1, pageCount: 10 });\n * // [1, 2, 3, 4, 5, \"ellipsis\", 10]\n * buildPaginationRange({ page: 6, pageCount: 10 });\n * // [1, \"ellipsis\", 5, 6, 7, \"ellipsis\", 10]\n * ```\n */\nexport function buildPaginationRange(\n params: BuildPaginationRangeParams\n): (number | \"ellipsis\")[] {\n const count = Math.max(1, Math.floor(params.pageCount));\n const page = clampPage(params.page, count);\n const siblingCount = Math.max(0, Math.floor(params.siblingCount ?? 1));\n const boundaryCount = Math.max(0, Math.floor(params.boundaryCount ?? 1));\n\n const startPages = inclusiveRange(1, Math.min(boundaryCount, count));\n const endPages = inclusiveRange(\n Math.max(count - boundaryCount + 1, boundaryCount + 1),\n count\n );\n\n const siblingsStart = Math.max(\n Math.min(page - siblingCount, count - boundaryCount - siblingCount * 2 - 1),\n boundaryCount + 2\n );\n const siblingsEnd = Math.min(\n Math.max(page + siblingCount, boundaryCount + siblingCount * 2 + 2),\n endPages.length > 0 ? endPages[0] - 2 : count - 1\n );\n\n return [\n ...startPages,\n // Start gap: an ellipsis, or the single page it would have hidden.\n ...(siblingsStart > boundaryCount + 2\n ? ([\"ellipsis\"] as const)\n : boundaryCount + 1 < count - boundaryCount\n ? [boundaryCount + 1]\n : []),\n ...inclusiveRange(siblingsStart, siblingsEnd),\n // End gap: an ellipsis, or the single page it would have hidden.\n ...(siblingsEnd < count - boundaryCount - 1\n ? ([\"ellipsis\"] as const)\n : count - boundaryCount > boundaryCount\n ? [count - boundaryCount]\n : []),\n ...endPages,\n ];\n}\n"],"mappings":"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","names":["next"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// src/usePagination.ts
|
|
2
|
+
import { useCallback, useMemo, useRef } from "react";
|
|
3
|
+
import { useControllableState } from "@usefy/use-controllable-state";
|
|
4
|
+
|
|
5
|
+
// src/utils.ts
|
|
6
|
+
function toSafeInt(value, min, fallback) {
|
|
7
|
+
if (!Number.isFinite(value)) {
|
|
8
|
+
return fallback;
|
|
9
|
+
}
|
|
10
|
+
const floored = Math.floor(value);
|
|
11
|
+
return floored < min ? min : floored;
|
|
12
|
+
}
|
|
13
|
+
function getPageCount(total, pageSize) {
|
|
14
|
+
const safeTotal = toSafeInt(total, 0, 0);
|
|
15
|
+
const safeSize = toSafeInt(pageSize, 1, 1);
|
|
16
|
+
return Math.max(1, Math.ceil(safeTotal / safeSize));
|
|
17
|
+
}
|
|
18
|
+
function clampPage(page, pageCount) {
|
|
19
|
+
if (!Number.isFinite(page)) {
|
|
20
|
+
return 1;
|
|
21
|
+
}
|
|
22
|
+
const floored = Math.floor(page);
|
|
23
|
+
if (floored < 1) {
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
return floored > pageCount ? pageCount : floored;
|
|
27
|
+
}
|
|
28
|
+
var inclusiveRange = (start, end) => start > end ? [] : Array.from({ length: end - start + 1 }, (_, index) => start + index);
|
|
29
|
+
function buildPaginationRange(params) {
|
|
30
|
+
const count = Math.max(1, Math.floor(params.pageCount));
|
|
31
|
+
const page = clampPage(params.page, count);
|
|
32
|
+
const siblingCount = Math.max(0, Math.floor(params.siblingCount ?? 1));
|
|
33
|
+
const boundaryCount = Math.max(0, Math.floor(params.boundaryCount ?? 1));
|
|
34
|
+
const startPages = inclusiveRange(1, Math.min(boundaryCount, count));
|
|
35
|
+
const endPages = inclusiveRange(
|
|
36
|
+
Math.max(count - boundaryCount + 1, boundaryCount + 1),
|
|
37
|
+
count
|
|
38
|
+
);
|
|
39
|
+
const siblingsStart = Math.max(
|
|
40
|
+
Math.min(page - siblingCount, count - boundaryCount - siblingCount * 2 - 1),
|
|
41
|
+
boundaryCount + 2
|
|
42
|
+
);
|
|
43
|
+
const siblingsEnd = Math.min(
|
|
44
|
+
Math.max(page + siblingCount, boundaryCount + siblingCount * 2 + 2),
|
|
45
|
+
endPages.length > 0 ? endPages[0] - 2 : count - 1
|
|
46
|
+
);
|
|
47
|
+
return [
|
|
48
|
+
...startPages,
|
|
49
|
+
// Start gap: an ellipsis, or the single page it would have hidden.
|
|
50
|
+
...siblingsStart > boundaryCount + 2 ? ["ellipsis"] : boundaryCount + 1 < count - boundaryCount ? [boundaryCount + 1] : [],
|
|
51
|
+
...inclusiveRange(siblingsStart, siblingsEnd),
|
|
52
|
+
// End gap: an ellipsis, or the single page it would have hidden.
|
|
53
|
+
...siblingsEnd < count - boundaryCount - 1 ? ["ellipsis"] : count - boundaryCount > boundaryCount ? [count - boundaryCount] : [],
|
|
54
|
+
...endPages
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/usePagination.ts
|
|
59
|
+
var DEFAULT_PAGE_SIZE = 10;
|
|
60
|
+
var DEFAULT_SIBLING_COUNT = 1;
|
|
61
|
+
var DEFAULT_BOUNDARY_COUNT = 1;
|
|
62
|
+
function usePagination(options) {
|
|
63
|
+
const {
|
|
64
|
+
total: rawTotal,
|
|
65
|
+
pageSize: rawPageSize = DEFAULT_PAGE_SIZE,
|
|
66
|
+
page: controlledPage,
|
|
67
|
+
defaultPage = 1,
|
|
68
|
+
siblingCount = DEFAULT_SIBLING_COUNT,
|
|
69
|
+
boundaryCount = DEFAULT_BOUNDARY_COUNT,
|
|
70
|
+
onChange
|
|
71
|
+
} = options;
|
|
72
|
+
const total = toSafeInt(rawTotal, 0, 0);
|
|
73
|
+
const pageSize = toSafeInt(rawPageSize, 1, 1);
|
|
74
|
+
const pageCount = getPageCount(rawTotal, rawPageSize);
|
|
75
|
+
const [rawPage, setRawPage] = useControllableState({
|
|
76
|
+
value: controlledPage,
|
|
77
|
+
defaultValue: defaultPage,
|
|
78
|
+
onChange
|
|
79
|
+
});
|
|
80
|
+
const page = clampPage(rawPage, pageCount);
|
|
81
|
+
const stateRef = useRef({ page, pageCount });
|
|
82
|
+
stateRef.current = { page, pageCount };
|
|
83
|
+
const setRawPageRef = useRef(setRawPage);
|
|
84
|
+
setRawPageRef.current = setRawPage;
|
|
85
|
+
const setPage = useCallback((next2) => {
|
|
86
|
+
const { page: current, pageCount: count } = stateRef.current;
|
|
87
|
+
const target = clampPage(next2, count);
|
|
88
|
+
if (target === current) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
setRawPageRef.current(target);
|
|
92
|
+
}, []);
|
|
93
|
+
const next = useCallback(() => {
|
|
94
|
+
setPage(stateRef.current.page + 1);
|
|
95
|
+
}, [setPage]);
|
|
96
|
+
const prev = useCallback(() => {
|
|
97
|
+
setPage(stateRef.current.page - 1);
|
|
98
|
+
}, [setPage]);
|
|
99
|
+
const first = useCallback(() => {
|
|
100
|
+
setPage(1);
|
|
101
|
+
}, [setPage]);
|
|
102
|
+
const last = useCallback(() => {
|
|
103
|
+
setPage(stateRef.current.pageCount);
|
|
104
|
+
}, [setPage]);
|
|
105
|
+
const canNext = page < pageCount;
|
|
106
|
+
const canPrev = page > 1;
|
|
107
|
+
const range = useMemo(() => {
|
|
108
|
+
const start = (page - 1) * pageSize;
|
|
109
|
+
const end = Math.min(start + pageSize, total);
|
|
110
|
+
return { start, end };
|
|
111
|
+
}, [page, pageSize, total]);
|
|
112
|
+
const items = useMemo(
|
|
113
|
+
() => buildPaginationRange({
|
|
114
|
+
page,
|
|
115
|
+
pageCount,
|
|
116
|
+
siblingCount,
|
|
117
|
+
boundaryCount
|
|
118
|
+
}).map(
|
|
119
|
+
(entry) => entry === "ellipsis" ? { type: "ellipsis", page: null, selected: false } : { type: "page", page: entry, selected: entry === page }
|
|
120
|
+
),
|
|
121
|
+
[page, pageCount, siblingCount, boundaryCount]
|
|
122
|
+
);
|
|
123
|
+
return useMemo(
|
|
124
|
+
() => ({
|
|
125
|
+
page,
|
|
126
|
+
pageCount,
|
|
127
|
+
pageSize,
|
|
128
|
+
setPage,
|
|
129
|
+
next,
|
|
130
|
+
prev,
|
|
131
|
+
first,
|
|
132
|
+
last,
|
|
133
|
+
canNext,
|
|
134
|
+
canPrev,
|
|
135
|
+
range,
|
|
136
|
+
items
|
|
137
|
+
}),
|
|
138
|
+
[
|
|
139
|
+
page,
|
|
140
|
+
pageCount,
|
|
141
|
+
pageSize,
|
|
142
|
+
setPage,
|
|
143
|
+
next,
|
|
144
|
+
prev,
|
|
145
|
+
first,
|
|
146
|
+
last,
|
|
147
|
+
canNext,
|
|
148
|
+
canPrev,
|
|
149
|
+
range,
|
|
150
|
+
items
|
|
151
|
+
]
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
export {
|
|
155
|
+
buildPaginationRange,
|
|
156
|
+
clampPage,
|
|
157
|
+
getPageCount,
|
|
158
|
+
usePagination
|
|
159
|
+
};
|
|
160
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/usePagination.ts","../src/utils.ts"],"sourcesContent":["import { useCallback, useMemo, useRef } from \"react\";\nimport { useControllableState } from \"@usefy/use-controllable-state\";\nimport type {\n PaginationItem,\n PaginationRange,\n UsePaginationOptions,\n UsePaginationReturn,\n} from \"./types\";\nimport { buildPaginationRange, clampPage, getPageCount, toSafeInt } from \"./utils\";\n\nconst DEFAULT_PAGE_SIZE = 10;\nconst DEFAULT_SIBLING_COUNT = 1;\nconst DEFAULT_BOUNDARY_COUNT = 1;\n\n/**\n * A headless pagination state machine — the current page (controlled or\n * uncontrolled), the derived page count, a slice-ready index range, and an\n * ellipsis-aware page-number model for rendering a pager UI. It owns *state*\n * only; you render however you like.\n *\n * The current page is always kept within `[1, pageCount]`: it is clamped\n * **derived** from `total`/`pageSize`, so when the dataset shrinks the returned\n * `page` never points past the last page and the navigation controls stay\n * correct (a plain re-clamp, no surprise `onChange`). All controls\n * (`setPage`/`next`/`prev`/`first`/`last`) have stable identities and skip\n * no-op moves (staying on the current page does not re-render or fire\n * `onChange`).\n *\n * Works in both modes, mirroring `useControllableState`:\n * - **Uncontrolled** (`page` omitted): the hook owns the page, seeded from\n * `defaultPage`; `onChange` fires after each committed change.\n * - **Controlled** (`page` provided): the returned `page` mirrors the prop\n * (clamped) and the controls only call `onChange` — the parent owns the value.\n *\n * @param options - See {@link UsePaginationOptions}.\n * @returns The pagination state and controls — see {@link UsePaginationReturn}.\n *\n * @example\n * ```tsx\n * function UsersTable({ users }: { users: User[] }) {\n * const { page, pageCount, range, items, setPage, next, prev, canNext, canPrev } =\n * usePagination({ total: users.length, pageSize: 10 });\n *\n * const visible = users.slice(range.start, range.end);\n *\n * return (\n * <>\n * <ul>{visible.map((u) => <li key={u.id}>{u.name}</li>)}</ul>\n * <nav>\n * <button onClick={prev} disabled={!canPrev}>Prev</button>\n * {items.map((item, i) =>\n * item.type === \"ellipsis\" ? (\n * <span key={`gap-${i}`}>…</span>\n * ) : (\n * <button\n * key={item.page}\n * aria-current={item.selected}\n * onClick={() => setPage(item.page!)}\n * >\n * {item.page}\n * </button>\n * )\n * )}\n * <button onClick={next} disabled={!canNext}>Next</button>\n * </nav>\n * <p>Page {page} of {pageCount}</p>\n * </>\n * );\n * }\n * ```\n */\nexport function usePagination(\n options: UsePaginationOptions\n): UsePaginationReturn {\n const {\n total: rawTotal,\n pageSize: rawPageSize = DEFAULT_PAGE_SIZE,\n page: controlledPage,\n defaultPage = 1,\n siblingCount = DEFAULT_SIBLING_COUNT,\n boundaryCount = DEFAULT_BOUNDARY_COUNT,\n onChange,\n } = options;\n\n const total = toSafeInt(rawTotal, 0, 0);\n const pageSize = toSafeInt(rawPageSize, 1, 1);\n const pageCount = getPageCount(rawTotal, rawPageSize);\n\n // Raw (unclamped) page state, controllable. We always write *concrete*\n // clamped values through this setter (never updater functions), so the fact\n // that its internal value may drift out of range while the dataset shrinks\n // never leaks — the `page` below is always the clamped, derived value.\n const [rawPage, setRawPage] = useControllableState<number>({\n value: controlledPage,\n defaultValue: defaultPage,\n onChange,\n });\n\n const page = clampPage(rawPage, pageCount);\n\n // Mirror the latest derived values + setter into refs so the controls can be\n // fully identity-stable (empty dep arrays) while still reading fresh state.\n const stateRef = useRef({ page, pageCount });\n stateRef.current = { page, pageCount };\n\n const setRawPageRef = useRef(setRawPage);\n setRawPageRef.current = setRawPage;\n\n const setPage = useCallback((next: number) => {\n const { page: current, pageCount: count } = stateRef.current;\n const target = clampPage(next, count);\n if (target === current) {\n return; // no-op: staying put must not re-render or fire onChange\n }\n setRawPageRef.current(target);\n }, []);\n\n const next = useCallback(() => {\n setPage(stateRef.current.page + 1);\n }, [setPage]);\n\n const prev = useCallback(() => {\n setPage(stateRef.current.page - 1);\n }, [setPage]);\n\n const first = useCallback(() => {\n setPage(1);\n }, [setPage]);\n\n const last = useCallback(() => {\n setPage(stateRef.current.pageCount);\n }, [setPage]);\n\n const canNext = page < pageCount;\n const canPrev = page > 1;\n\n const range = useMemo<PaginationRange>(() => {\n const start = (page - 1) * pageSize;\n const end = Math.min(start + pageSize, total);\n return { start, end };\n }, [page, pageSize, total]);\n\n const items = useMemo<readonly PaginationItem[]>(\n () =>\n buildPaginationRange({\n page,\n pageCount,\n siblingCount,\n boundaryCount,\n }).map((entry) =>\n entry === \"ellipsis\"\n ? { type: \"ellipsis\", page: null, selected: false }\n : { type: \"page\", page: entry, selected: entry === page }\n ),\n [page, pageCount, siblingCount, boundaryCount]\n );\n\n return useMemo<UsePaginationReturn>(\n () => ({\n page,\n pageCount,\n pageSize,\n setPage,\n next,\n prev,\n first,\n last,\n canNext,\n canPrev,\n range,\n items,\n }),\n [\n page,\n pageCount,\n pageSize,\n setPage,\n next,\n prev,\n first,\n last,\n canNext,\n canPrev,\n range,\n items,\n ]\n );\n}\n","/**\n * Coerce a value to a safe integer, flooring fractions and enforcing a minimum.\n * Non-finite values (`NaN`, `Infinity`) fall back to `fallback`.\n *\n * @internal\n */\nexport function toSafeInt(value: number, min: number, fallback: number): number {\n if (!Number.isFinite(value)) {\n return fallback;\n }\n const floored = Math.floor(value);\n return floored < min ? min : floored;\n}\n\n/**\n * Compute the number of pages for a dataset. Always returns at least `1` — an\n * empty dataset still has a single (empty) page — so it is safe to use directly\n * as the upper clamp bound. `total` is treated as `>= 0` and `pageSize` as\n * `>= 1` (both floored; non-finite inputs are normalized).\n *\n * @param total - Total number of items.\n * @param pageSize - Items per page.\n * @returns The page count, `>= 1`.\n *\n * @example\n * ```ts\n * getPageCount(0, 10); // 1\n * getPageCount(25, 10); // 3\n * getPageCount(30, 10); // 3\n * ```\n */\nexport function getPageCount(total: number, pageSize: number): number {\n const safeTotal = toSafeInt(total, 0, 0);\n const safeSize = toSafeInt(pageSize, 1, 1);\n return Math.max(1, Math.ceil(safeTotal / safeSize));\n}\n\n/**\n * Clamp a requested page into `[1, pageCount]`, flooring fractions. Non-finite\n * requests fall back to page `1`. `pageCount` is assumed to be `>= 1` (as\n * returned by {@link getPageCount}).\n *\n * @param page - The requested 1-based page.\n * @param pageCount - The number of pages (upper bound).\n * @returns The clamped page, within `[1, pageCount]`.\n */\nexport function clampPage(page: number, pageCount: number): number {\n if (!Number.isFinite(page)) {\n return 1;\n }\n const floored = Math.floor(page);\n if (floored < 1) {\n return 1;\n }\n return floored > pageCount ? pageCount : floored;\n}\n\n/**\n * Parameters for {@link buildPaginationRange}.\n */\nexport interface BuildPaginationRangeParams {\n /** The current (clamped) 1-based page. */\n page: number;\n /** Total number of pages (`>= 1`). */\n pageCount: number;\n /** Pages shown on each side of the current page. @defaultValue 1 */\n siblingCount?: number;\n /** Pages shown at the start and end. @defaultValue 1 */\n boundaryCount?: number;\n}\n\nconst inclusiveRange = (start: number, end: number): number[] =>\n start > end\n ? []\n : Array.from({ length: end - start + 1 }, (_, index) => start + index);\n\n/**\n * Build the ordered list of page numbers with `\"ellipsis\"` gap tokens for a\n * pager UI — the well-known MUI/Mantine `usePagination` model. Always shows the\n * `boundaryCount` first/last pages and the `siblingCount` pages around the\n * current page, collapsing anything in between into a single `\"ellipsis\"`. When\n * a gap would hide exactly one page, that page is shown instead of an ellipsis.\n *\n * @param params - See {@link BuildPaginationRangeParams}.\n * @returns An ordered array of 1-based page numbers and `\"ellipsis\"` tokens.\n *\n * @example\n * ```ts\n * buildPaginationRange({ page: 1, pageCount: 10 });\n * // [1, 2, 3, 4, 5, \"ellipsis\", 10]\n * buildPaginationRange({ page: 6, pageCount: 10 });\n * // [1, \"ellipsis\", 5, 6, 7, \"ellipsis\", 10]\n * ```\n */\nexport function buildPaginationRange(\n params: BuildPaginationRangeParams\n): (number | \"ellipsis\")[] {\n const count = Math.max(1, Math.floor(params.pageCount));\n const page = clampPage(params.page, count);\n const siblingCount = Math.max(0, Math.floor(params.siblingCount ?? 1));\n const boundaryCount = Math.max(0, Math.floor(params.boundaryCount ?? 1));\n\n const startPages = inclusiveRange(1, Math.min(boundaryCount, count));\n const endPages = inclusiveRange(\n Math.max(count - boundaryCount + 1, boundaryCount + 1),\n count\n );\n\n const siblingsStart = Math.max(\n Math.min(page - siblingCount, count - boundaryCount - siblingCount * 2 - 1),\n boundaryCount + 2\n );\n const siblingsEnd = Math.min(\n Math.max(page + siblingCount, boundaryCount + siblingCount * 2 + 2),\n endPages.length > 0 ? endPages[0] - 2 : count - 1\n );\n\n return [\n ...startPages,\n // Start gap: an ellipsis, or the single page it would have hidden.\n ...(siblingsStart > boundaryCount + 2\n ? ([\"ellipsis\"] as const)\n : boundaryCount + 1 < count - boundaryCount\n ? [boundaryCount + 1]\n : []),\n ...inclusiveRange(siblingsStart, siblingsEnd),\n // End gap: an ellipsis, or the single page it would have hidden.\n ...(siblingsEnd < count - boundaryCount - 1\n ? ([\"ellipsis\"] as const)\n : count - boundaryCount > boundaryCount\n ? [count - boundaryCount]\n : []),\n ...endPages,\n ];\n}\n"],"mappings":"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","names":["next"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@usefy/use-pagination",
|
|
3
|
+
"version": "0.20.0",
|
|
4
|
+
"description": "A React hook for pagination state — controlled/uncontrolled current page, page count, range, and an ellipsis-aware page-number model",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"sideEffects": false,
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@usefy/use-controllable-state": "0.20.0"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
27
|
+
"@testing-library/react": "^16.3.1",
|
|
28
|
+
"@testing-library/user-event": "^14.6.1",
|
|
29
|
+
"@types/react": "^19.0.0",
|
|
30
|
+
"jsdom": "^27.3.0",
|
|
31
|
+
"react": "^19.0.0",
|
|
32
|
+
"rimraf": "^6.0.1",
|
|
33
|
+
"tsup": "^8.0.0",
|
|
34
|
+
"typescript": "^5.0.0",
|
|
35
|
+
"vitest": "^4.0.16"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/mirunamu00/usefy.git",
|
|
43
|
+
"directory": "packages/hooks/use-pagination"
|
|
44
|
+
},
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"keywords": [
|
|
47
|
+
"react",
|
|
48
|
+
"hooks",
|
|
49
|
+
"pagination",
|
|
50
|
+
"paginate",
|
|
51
|
+
"pager",
|
|
52
|
+
"page",
|
|
53
|
+
"controllable",
|
|
54
|
+
"ellipsis",
|
|
55
|
+
"usePagination"
|
|
56
|
+
],
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsup",
|
|
59
|
+
"dev": "tsup --watch",
|
|
60
|
+
"test": "vitest run",
|
|
61
|
+
"test:watch": "vitest",
|
|
62
|
+
"typecheck": "tsc --noEmit",
|
|
63
|
+
"clean": "rimraf dist"
|
|
64
|
+
}
|
|
65
|
+
}
|