@c2n/pagination 0.0.7

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Nguyen Thai Vinh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # @c2n/pagination
2
+
3
+ Pagination built with Lit: page navigation for a list, a table or search results, in three layouts.
4
+
5
+ ```bash
6
+ npm install @c2n/pagination
7
+ ```
8
+
9
+ ```html
10
+ <script type="module">
11
+ import '@c2n/pagination'
12
+ </script>
13
+
14
+ <!-- Numbered: previous/next around the page numbers -->
15
+ <c2-pagination total-items="240" page-size="10" page="4"></c2-pagination>
16
+
17
+ <!-- Simple: previous/next around a page status -->
18
+ <c2-pagination variant="simple" total-pages="12" page="3"></c2-pagination>
19
+
20
+ <!-- Compact: the table-footer row -->
21
+ <c2-pagination variant="compact" total-items="100" page-size="10" page="3"></c2-pagination>
22
+ ```
23
+
24
+ ```js
25
+ document.querySelector('c2-pagination').addEventListener('page-change', (event) => {
26
+ const { startIndex, endIndex } = event.detail
27
+ render(rows.slice(startIndex, endIndex))
28
+ })
29
+ ```
30
+
31
+ - **Variants**: `numbered` (default), `simple` and `compact`. `hide-nav-labels` keeps the chevrons and drops the visible "Previous"/"Next" text, and `hide-page-size` / `hide-range` strip the compact row down.
32
+ - **Range**: `total-items` + `page-size`, or `total-pages` when the item count is unknown. The element owns `page`, reflects it and clamps it, so listening to `page-change` is enough.
33
+ - **Numbers shown**: `boundary-count` (default `1`) pins numbers at each end, `sibling-count` (default `1`) surrounds the current page; the rest collapses into an ellipsis, and an ellipsis standing for a single page is replaced by that page, so the row keeps the same number of slots as the current page moves.
34
+ - **Ellipsis**: a button, not decoration — it jumps into the middle of the pages it hides (`1 … 9` jumps to 5) and shows the direction on hover and focus. `jump-label-template` names it.
35
+ - **One row**: the controls never wrap. Numbers that do not fit the host's width are shed (siblings first, then boundaries) and come back when the container grows; only the three groups of the `compact` variant may drop onto a second line. Because `boundary-count="1"` keeps the first and last page one click away, `show-first-last` is for `simple` and `compact` — or for `boundary-count="0"`.
36
+ - **Rows per page**: the `compact` variant uses a `c2-select` (registered by this package) fed by `page-size-options`. Picking another size keeps the first item of the current page on screen and fires `page-size-change`.
37
+ - **Inside a `c2-table`**: slotted into the table's `footer` the pager becomes a controlled view — the table owns `page`, `page-size` and `total-items` and feeds them through the `@c2n/core` pager context, so nothing needs wiring up. Anywhere else it keeps managing its own state.
38
+ - **Events**: `page-change` (`page`, `previousPage`, `pageSize`, `pageCount`, `startIndex`, `endIndex`) and `page-size-change` (`pageSize`, `previousPageSize`, `page`).
39
+ - **Text**: every label is an attribute — `previous-label`, `next-label`, `first-label`, `last-label`, `page-size-label` — plus `range-template` (`{start}`, `{end}`, `{total}`), `page-template` (`{page}`, `{pageCount}`), `page-label-template` (`{page}`) and `jump-label-template` (`{page}`, `{from}`, `{to}`, `{count}`).
40
+ - **Accessibility**: a `nav` landmark named "Pagination" (`aria-label` overrides), `aria-current="page"` on the current number, polite live regions for the status text, and focus handed to the opposite control when the one just pressed reaches an end.
41
+
42
+ `--c2-pagination__item--*` styles the page numbers (`__selected` the current one), `--c2-pagination__nav--*` the previous/next/first/last controls, `--c2-pagination__label--*` the status text and rows-per-page label, and `--c2-pagination--justify-content` places the row in the host's width. The rows-per-page select wears those same variables. The full list is in `custom-elements.json` and on the docs site.
@@ -0,0 +1,549 @@
1
+ import { LitElement, html, isServer, nothing, unsafeCSS } from "lit";
2
+ import { property, query, state } from "lit/decorators.js";
3
+ import { customElement } from "@c2n/core/element-helper.js";
4
+ import { classMap } from "lit/directives/class-map.js";
5
+ import { repeat } from "lit/directives/repeat.js";
6
+ import { consume } from "@lit/context";
7
+ import { PAGER_CONNECT_EVENT, pagerContext } from "@c2n/core/contexts/pager.js";
8
+ import "@c2n/select";
9
+ import "@c2n/list-item";
10
+ //#region src/pagination.scss?inline
11
+ var pagination_default = "/* ex : var((width: 24px), width, c2-checkbox) returns var(--c2-checkbox-width, 24px) */\n:host {\n display: block;\n}\n\n:host([hidden]) {\n display: none;\n}\n\n.c2-pagination {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: var(--c2-pagination--justify-content,flex-start);\n gap: var(--c2-pagination--section-gap,24px);\n min-width: 0;\n}\n\n.c2-pagination-list {\n display: flex;\n flex-wrap: nowrap;\n align-items: center;\n gap: var(--c2-pagination--gap,4px);\n margin: 0;\n padding: 0;\n list-style: none;\n min-width: 0;\n overflow: hidden;\n}\n.c2-pagination-list.is-measuring {\n visibility: hidden;\n}\n.c2-pagination-list > li {\n display: inline-flex;\n flex: none;\n}\n\n.c2-pagination-arrows {\n display: flex;\n flex: none;\n align-items: center;\n gap: var(--c2-pagination--gap,4px);\n}\n\n.c2-pagination-control {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n flex: none;\n height: var(--c2-pagination__item--height,36px);\n margin: 0;\n font-family: inherit;\n line-height: 1;\n white-space: nowrap;\n cursor: pointer;\n transition: background-color 150ms ease, color 150ms ease, border-color 150ms ease;\n}\n.c2-pagination-control:focus-visible {\n outline: var(--c2-pagination__item__focus--outline,2px solid rgba(2, 101, 220, 0.4));\n outline-offset: var(--c2-pagination__item__focus--outline-offset,1px);\n}\n.c2-pagination-control:disabled {\n opacity: var(--c2-pagination__item__disabled--opacity,0.38);\n cursor: default;\n}\n\n.c2-pagination-item {\n min-width: var(--c2-pagination__item--min-width,36px);\n padding-left: var(--c2-pagination__item--padding-left,8px);\n padding-right: var(--c2-pagination__item--padding-right,8px);\n color: var(--c2-pagination__item--color,#18181b);\n background-color: var(--c2-pagination__item--background-color,transparent);\n border: var(--c2-pagination__item--border,1px solid transparent);\n border-radius: var(--c2-pagination__item--border-radius,8px);\n font-size: var(--c2-pagination__item--font-size,14px);\n font-weight: var(--c2-pagination__item--font-weight,500);\n}\n.c2-pagination-item:hover:not(:disabled):not([aria-current]) {\n color: var(--c2-pagination__item__hover--color,#18181b);\n background-color: var(--c2-pagination__item__hover--background-color,#f4f4f5);\n border: var(--c2-pagination__item__hover--border,1px solid transparent);\n}\n.c2-pagination-item[aria-current] {\n color: var(--c2-pagination__item__selected--color,#18181b);\n background-color: var(--c2-pagination__item__selected--background-color,#ffffff);\n border: var(--c2-pagination__item__selected--border,1px solid #e4e4e7);\n font-weight: var(--c2-pagination__item__selected--font-weight,500);\n cursor: default;\n}\n\n.c2-pagination-nav {\n gap: var(--c2-pagination__nav--gap,6px);\n min-width: var(--c2-pagination__item--min-width,36px);\n padding-left: var(--c2-pagination__nav--padding-left,10px);\n padding-right: var(--c2-pagination__nav--padding-right,10px);\n color: var(--c2-pagination__nav--color,#18181b);\n background-color: var(--c2-pagination__nav--background-color,transparent);\n border: var(--c2-pagination__nav--border,1px solid transparent);\n border-radius: var(--c2-pagination__nav--border-radius,8px);\n font-size: var(--c2-pagination__nav--font-size,14px);\n font-weight: var(--c2-pagination__nav--font-weight,500);\n}\n.c2-pagination-nav:hover:not(:disabled) {\n color: var(--c2-pagination__nav__hover--color,#18181b);\n background-color: var(--c2-pagination__nav__hover--background-color,#f4f4f5);\n border: var(--c2-pagination__nav__hover--border,1px solid transparent);\n}\n\n:host([variant=compact]) .c2-pagination-nav {\n min-width: var(--c2-pagination__item--height,36px);\n padding-left: 0;\n padding-right: 0;\n}\n\n.c2-pagination-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n color: var(--c2-pagination__icon--color);\n}\n.c2-pagination-icon svg,\n.c2-pagination-icon ::slotted(*) {\n width: var(--c2-pagination__icon--size,16px);\n height: var(--c2-pagination__icon--size,16px);\n display: block;\n}\n.c2-pagination-icon {\n --c2-feather-icon--size: var(--c2-pagination__icon--size,16px);\n --c2-mat-icon--font-size: var(--c2-pagination__icon--size,16px);\n}\n\n.c2-pagination-ellipsis {\n min-width: var(--c2-pagination__ellipsis--min-width,36px);\n padding: 0;\n color: var(--c2-pagination__ellipsis--color,#71717a);\n background-color: var(--c2-pagination__item--background-color,transparent);\n border: var(--c2-pagination__item--border,1px solid transparent);\n border-radius: var(--c2-pagination__item--border-radius,8px);\n font-size: var(--c2-pagination__item--font-size,14px);\n user-select: none;\n}\n.c2-pagination-ellipsis .c2-pagination-ellipsis-jump {\n display: none;\n}\n.c2-pagination-ellipsis:hover:not(:disabled), .c2-pagination-ellipsis:focus-visible {\n color: var(--c2-pagination__item__hover--color,#18181b);\n background-color: var(--c2-pagination__item__hover--background-color,#f4f4f5);\n border: var(--c2-pagination__item__hover--border,1px solid transparent);\n}\n.c2-pagination-ellipsis:hover:not(:disabled) .c2-pagination-ellipsis-dots, .c2-pagination-ellipsis:focus-visible .c2-pagination-ellipsis-dots {\n display: none;\n}\n.c2-pagination-ellipsis:hover:not(:disabled) .c2-pagination-ellipsis-jump, .c2-pagination-ellipsis:focus-visible .c2-pagination-ellipsis-jump {\n display: inline-flex;\n}\n\n.c2-pagination-label,\n.c2-pagination-status {\n display: inline-flex;\n align-items: center;\n color: var(--c2-pagination__label--color,#71717a);\n font-size: var(--c2-pagination__label--font-size,14px);\n font-weight: var(--c2-pagination__label--font-weight,500);\n white-space: nowrap;\n}\n\n.c2-pagination-page-size {\n display: inline-flex;\n align-items: center;\n gap: var(--c2-pagination__label--gap,8px);\n}\n\n.c2-pagination-page-size-select {\n --c2-select__button--background: var(--c2-pagination__item--background-color,transparent);\n --c2-select__button--color: var(--c2-pagination__item--color,#18181b);\n --c2-select__button--font-size: var(--c2-pagination__label--font-size,14px);\n --c2-select__button--font-weight: var(--c2-pagination__label--font-weight,500);\n --c2-select__button--min-height: 32px;\n --c2-select__button--padding: 4px 4px 4px 8px;\n --c2-select__button--gap: 2px;\n --c2-select__button--border-top: none;\n --c2-select__button--border-right: none;\n --c2-select__button--border-bottom: none;\n --c2-select__button--border-left: none;\n --c2-select__button__hover--border-top: none;\n --c2-select__button__hover--border-right: none;\n --c2-select__button__hover--border-bottom: none;\n --c2-select__button__hover--border-left: none;\n --c2-select__button__open--border-top: none;\n --c2-select__button__open--border-right: none;\n --c2-select__button__open--border-bottom: none;\n --c2-select__button__open--border-left: none;\n --c2-select__button__hover--background: var(--c2-pagination__item__hover--background-color,#f4f4f5);\n --c2-select__button--border-top-left-radius: var(--c2-pagination__item--border-radius,8px);\n --c2-select__button--border-top-right-radius: var(--c2-pagination__item--border-radius,8px);\n --c2-select__button--border-bottom-left-radius: var(--c2-pagination__item--border-radius,8px);\n --c2-select__button--border-bottom-right-radius: var(--c2-pagination__item--border-radius,8px);\n --c2-select__button__focus--outline: var(--c2-pagination__item__focus--outline,2px solid rgba(2, 101, 220, 0.4));\n --c2-select__button__suffix-icon--color: var(--c2-pagination__label--color,#71717a);\n --c2-select__button__suffix-icon--width: 14px;\n --c2-select__button__suffix-icon--height: 14px;\n --c2-select__list--min-width: var(--c2-pagination__page-size-list--min-width,56px);\n --c2-list-item--font-size: var(--c2-pagination__label--font-size,14px);\n}";
12
+ //#endregion
13
+ //#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorate.js
14
+ function __decorate(decorators, target, key, desc) {
15
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
16
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
17
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
18
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
19
+ }
20
+ //#endregion
21
+ //#region src/pagination.ts
22
+ var DEFAULT_PAGE_SIZE_OPTIONS = [
23
+ 10,
24
+ 25,
25
+ 50,
26
+ 100
27
+ ];
28
+ /** `page-size-options="10,25,50"`; invalid and non-positive entries are dropped. */
29
+ var numberListConverter = {
30
+ toAttribute: (value) => Array.isArray(value) && value.length > 0 ? value.join(",") : null,
31
+ fromAttribute: (value) => (value ?? "").split(",").map((entry) => Number(entry.trim())).filter((entry) => Number.isFinite(entry) && entry > 0)
32
+ };
33
+ /** Substitutes `{page}`, `{pageCount}`, `{start}`, `{end}` and `{total}` in a label template. */
34
+ function format(template, values) {
35
+ return template.replace(/\{(\w+)\}/g, (match, key) => key in values ? String(values[key]) : match);
36
+ }
37
+ function range(start, end) {
38
+ return Array.from({ length: Math.max(0, end - start + 1) }, (_, index) => start + index);
39
+ }
40
+ var chevronLeft = html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
41
+ <polyline points="15 18 9 12 15 6"></polyline>
42
+ </svg>`;
43
+ var chevronRight = html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
44
+ <polyline points="9 18 15 12 9 6"></polyline>
45
+ </svg>`;
46
+ var chevronsLeft = html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
47
+ <polyline points="11 17 6 12 11 7"></polyline>
48
+ <polyline points="18 17 13 12 18 7"></polyline>
49
+ </svg>`;
50
+ var chevronsRight = html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
51
+ <polyline points="13 17 18 12 13 7"></polyline>
52
+ <polyline points="6 17 11 12 6 7"></polyline>
53
+ </svg>`;
54
+ var Pagination = class Pagination extends LitElement {
55
+ constructor(..._args) {
56
+ super(..._args);
57
+ this.variant = "numbered";
58
+ this.page = 1;
59
+ this.pageSize = 10;
60
+ this.totalItems = 0;
61
+ this.totalPages = 0;
62
+ this.siblingCount = 1;
63
+ this.boundaryCount = 1;
64
+ this.pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS;
65
+ this.hidePageSize = false;
66
+ this.hideRange = false;
67
+ this.showFirstLast = false;
68
+ this.hideNavLabels = false;
69
+ this.disabled = false;
70
+ this.previousLabel = "Previous";
71
+ this.nextLabel = "Next";
72
+ this.firstLabel = "First";
73
+ this.lastLabel = "Last";
74
+ this.pageSizeLabel = "Rows per page:";
75
+ this.rangeTemplate = "{start}–{end} of {total}";
76
+ this.pageTemplate = "Page {page} of {pageCount}";
77
+ this.pageLabelTemplate = "Go to page {page}";
78
+ this.jumpLabelTemplate = "Jump to page {page}, skipping {count} pages";
79
+ this.lastPressedNav = null;
80
+ this.maxNumbers = null;
81
+ this.measuring = false;
82
+ this.measuredWidth = -1;
83
+ }
84
+ static {
85
+ this.styles = unsafeCSS(pagination_default);
86
+ }
87
+ /** Number of pages, from `total-pages` when given, otherwise from `total-items` and `page-size`. At least 1. */
88
+ get pageCount() {
89
+ if (this.totalPages > 0) return Math.max(1, Math.floor(this.totalPages));
90
+ const size = Math.max(1, Math.floor(this.pageSize));
91
+ if (this.totalItems > 0) return Math.max(1, Math.ceil(this.totalItems / size));
92
+ return 1;
93
+ }
94
+ /** `page` clamped to `1..pageCount`; this is the page the element actually shows. */
95
+ get currentPage() {
96
+ return Math.min(Math.max(1, Math.floor(this.page) || 1), this.pageCount);
97
+ }
98
+ /** Zero-based index of the first item of the current page. */
99
+ get startIndex() {
100
+ return (this.currentPage - 1) * Math.max(1, Math.floor(this.pageSize));
101
+ }
102
+ /** Zero-based index after the last item of the current page; equals `startIndex` when the total is unknown. */
103
+ get endIndex() {
104
+ if (this.totalItems <= 0) return this.startIndex;
105
+ return Math.min(this.totalItems, this.startIndex + Math.max(1, Math.floor(this.pageSize)));
106
+ }
107
+ /**
108
+ * The page numbers and ellipses actually rendered: the requested `sibling-count` / `boundary-count`, narrowed by
109
+ * whatever the measured width allows (see `measure`). Reading it tells you what the row shows right now.
110
+ */
111
+ get items() {
112
+ let sibling = Math.max(0, Math.floor(this.siblingCount));
113
+ let boundary = Math.max(0, Math.floor(this.boundaryCount));
114
+ let items = this.itemsFor(sibling, boundary);
115
+ if (this.maxNumbers === null) return items;
116
+ while (items.filter((item) => item.kind === "page").length > this.maxNumbers && (sibling > 0 || boundary > 0)) {
117
+ if (sibling > 0) sibling--;
118
+ else boundary--;
119
+ items = this.itemsFor(sibling, boundary);
120
+ }
121
+ return items;
122
+ }
123
+ /**
124
+ * The page numbers and ellipses for one pair of counts: both ends show `boundary` numbers, the current page shows
125
+ * `sibling` numbers on each side, and everything left over collapses into an ellipsis. An ellipsis standing for a
126
+ * single page is replaced by that page, so the row never grows a gap it did not need, and the number of slots stays
127
+ * the same as the current page moves.
128
+ */
129
+ itemsFor(sibling, boundary) {
130
+ const count = this.pageCount;
131
+ const page = this.currentPage;
132
+ const startPages = range(1, Math.min(boundary, count));
133
+ const endPages = range(Math.max(count - boundary + 1, boundary + 1), count);
134
+ const siblingsStart = Math.max(Math.min(page - sibling, count - boundary - sibling * 2 - 1), boundary + 2);
135
+ const siblingsEnd = Math.min(Math.max(page + sibling, boundary + sibling * 2 + 2), endPages.length > 0 ? endPages[0] - 2 : count - 1);
136
+ const pages = [
137
+ ...startPages,
138
+ ...siblingsStart > boundary + 2 ? ["start-ellipsis"] : boundary + 1 < count - boundary ? [boundary + 1] : [],
139
+ ...range(siblingsStart, siblingsEnd),
140
+ ...siblingsEnd < count - boundary - 1 ? ["end-ellipsis"] : count - boundary > boundary ? [count - boundary] : [],
141
+ ...endPages
142
+ ];
143
+ return pages.map((entry, index) => {
144
+ if (typeof entry === "number") return {
145
+ kind: "page",
146
+ page: entry
147
+ };
148
+ const before = pages[index - 1];
149
+ const after = pages[index + 1];
150
+ const from = typeof before === "number" ? before + 1 : 1;
151
+ const to = typeof after === "number" ? after - 1 : count;
152
+ return {
153
+ kind: "ellipsis",
154
+ key: entry,
155
+ from,
156
+ to,
157
+ target: Math.floor((from + to) / 2)
158
+ };
159
+ });
160
+ }
161
+ /** Show `page`, clamped to the available range. Fires `page-change` when the page actually moves. */
162
+ goToPage(page) {
163
+ const previousPage = this.currentPage;
164
+ const next = Math.min(Math.max(1, Math.floor(page) || 1), this.pageCount);
165
+ if (next === previousPage && next === this.page) return;
166
+ this.page = next;
167
+ this.pagerHost?.pageChanged(next);
168
+ if (next === previousPage) return;
169
+ this.dispatchEvent(new CustomEvent("page-change", {
170
+ bubbles: true,
171
+ composed: true,
172
+ detail: {
173
+ page: next,
174
+ previousPage,
175
+ pageSize: this.pageSize,
176
+ pageCount: this.pageCount,
177
+ startIndex: this.startIndex,
178
+ endIndex: this.endIndex
179
+ }
180
+ }));
181
+ }
182
+ handleNav(target, control) {
183
+ if (this.disabled) return;
184
+ this.lastPressedNav = control;
185
+ this.goToPage(target);
186
+ }
187
+ /** Picking another page size keeps the first item of the current page on screen, the way a table footer does. */
188
+ handlePageSizeChange(event) {
189
+ const detail = event.detail;
190
+ const next = Number(detail.value[0]);
191
+ const previousPageSize = this.pageSize;
192
+ if (!Number.isFinite(next) || next <= 0 || next === previousPageSize) return;
193
+ const firstItem = this.startIndex;
194
+ const previousPage = this.currentPage;
195
+ this.pageSize = next;
196
+ this.page = Math.floor(firstItem / next) + 1;
197
+ this.pagerHost?.pageSizeChanged(next);
198
+ const page = this.currentPage;
199
+ this.dispatchEvent(new CustomEvent("page-size-change", {
200
+ bubbles: true,
201
+ composed: true,
202
+ detail: {
203
+ pageSize: next,
204
+ previousPageSize,
205
+ page
206
+ }
207
+ }));
208
+ if (page !== previousPage) this.dispatchEvent(new CustomEvent("page-change", {
209
+ bubbles: true,
210
+ composed: true,
211
+ detail: {
212
+ page,
213
+ previousPage,
214
+ pageSize: next,
215
+ pageCount: this.pageCount,
216
+ startIndex: this.startIndex,
217
+ endIndex: this.endIndex
218
+ }
219
+ }));
220
+ }
221
+ connectedCallback() {
222
+ super.connectedCallback();
223
+ this.announce();
224
+ if (isServer || typeof ResizeObserver === "undefined") return;
225
+ this.resizeObserver ??= new ResizeObserver((entries) => {
226
+ const width = entries[0]?.contentRect.width ?? 0;
227
+ if (Math.abs(width - this.measuredWidth) > .5) this.requestMeasure();
228
+ });
229
+ this.resizeObserver.observe(this);
230
+ this.retryAnnounce();
231
+ }
232
+ disconnectedCallback() {
233
+ super.disconnectedCallback();
234
+ this.resizeObserver?.disconnect();
235
+ this.pagerHost = void 0;
236
+ }
237
+ announce() {
238
+ this.dispatchEvent(new CustomEvent(PAGER_CONNECT_EVENT, {
239
+ bubbles: true,
240
+ composed: true,
241
+ detail: { pageSize: this.pageSize }
242
+ }));
243
+ }
244
+ /**
245
+ * A host that has not been upgraded yet hears nothing: both the connect event and the context request reach the
246
+ * document and die. That happens whenever this module is evaluated before the host's, which is a bundler's choice,
247
+ * not the author's — so wait for every custom-element ancestor to be defined and then ask once more.
248
+ */
249
+ async retryAnnounce() {
250
+ if (isServer) return;
251
+ const pending = [];
252
+ for (let node = this.parentOf(this); node; node = this.parentOf(node)) {
253
+ const tag = node.localName;
254
+ if (tag.includes("-") && !customElements.get(tag)) pending.push(customElements.whenDefined(tag));
255
+ }
256
+ if (pending.length === 0) return;
257
+ await Promise.all(pending);
258
+ if (this.isConnected && !this.pagerHost) this.announce();
259
+ }
260
+ parentOf(node) {
261
+ const root = node.parentNode;
262
+ return node.parentElement ?? (root instanceof ShadowRoot ? root.host : null);
263
+ }
264
+ /** A hosted pager shows the host's paging state, not its own; its own properties are only the standalone fallback. */
265
+ willUpdate(_changed) {
266
+ const host = this.pagerHost;
267
+ if (!host) return;
268
+ this.page = host.page;
269
+ this.pageSize = host.pageSize;
270
+ this.totalItems = host.totalItems;
271
+ this.disabled = host.busy;
272
+ }
273
+ /** Start over from the requested counts, so a container that grew back gets its page numbers back. */
274
+ requestMeasure() {
275
+ if (isServer || !this.isConnected || this.variant === "compact") return;
276
+ this.maxNumbers = null;
277
+ this.measuring = true;
278
+ }
279
+ updated(changed) {
280
+ if (changed.has("variant") || changed.has("siblingCount") || changed.has("boundaryCount") || changed.has("showFirstLast") || changed.has("hideNavLabels")) this.requestMeasure();
281
+ if (this.measuring) this.measure();
282
+ this.restoreFocus();
283
+ }
284
+ /**
285
+ * The row never wraps, so when the requested page numbers do not fit the host, work out how many do and let `items`
286
+ * shed siblings and boundaries until they do. One pass: the widest rendered number plus a gap is the cost of a slot,
287
+ * and the overflow divided by that is how many slots have to go. Runs before paint, so there is no flicker.
288
+ */
289
+ measure() {
290
+ const list = this.list;
291
+ this.measuring = false;
292
+ if (!list) return;
293
+ this.measuredWidth = this.getBoundingClientRect().width;
294
+ const available = list.clientWidth + .5;
295
+ const total = list.scrollWidth;
296
+ const numbers = [...list.querySelectorAll(".c2-pagination-item")];
297
+ if (total <= available || numbers.length === 0) return;
298
+ const gap = parseFloat(getComputedStyle(list).columnGap) || 0;
299
+ const slot = Math.max(...numbers.map((number) => number.getBoundingClientRect().width)) + gap;
300
+ const drop = slot > 0 ? Math.ceil((total - available) / slot) : numbers.length;
301
+ this.maxNumbers = Math.max(1, numbers.length - drop);
302
+ }
303
+ /**
304
+ * Reaching the first or the last page disables the control that was just pressed, which would otherwise drop focus
305
+ * to the document. Hand it to the opposite control so the keyboard user stays in the pagination.
306
+ */
307
+ restoreFocus() {
308
+ const pressed = this.lastPressedNav;
309
+ if (!pressed) return;
310
+ this.lastPressedNav = null;
311
+ const active = this.shadowRoot?.activeElement;
312
+ const button = this.shadowRoot?.querySelector(`[data-nav="${pressed}"]`);
313
+ if (!button || button !== active || !button.disabled) return;
314
+ this.shadowRoot?.querySelector(`[data-nav="${pressed === "previous" ? "next" : "previous"}"]`)?.focus();
315
+ }
316
+ renderNav(control, target, disabled, icon, label) {
317
+ const isPrevNext = control === "previous" || control === "next";
318
+ const showLabel = isPrevNext && !this.hideNavLabels && this.variant !== "compact";
319
+ return html`<button
320
+ class="c2-pagination-control c2-pagination-nav"
321
+ part="nav"
322
+ type="button"
323
+ data-nav=${control}
324
+ aria-label=${label}
325
+ ?disabled=${disabled || this.disabled}
326
+ @click=${() => this.handleNav(target, isPrevNext ? control : null)}
327
+ >
328
+ ${control === "next" || control === "last" ? nothing : html`<span class="c2-pagination-icon"><slot name="${control}-icon">${icon}</slot></span>`}
329
+ ${showLabel ? html`<span class="c2-pagination-nav-label">${label}</span>` : nothing}
330
+ ${control === "next" || control === "last" ? html`<span class="c2-pagination-icon"><slot name="${control}-icon">${icon}</slot></span>` : nothing}
331
+ </button>`;
332
+ }
333
+ renderPrevious(page) {
334
+ return this.renderNav("previous", page - 1, page <= 1, chevronLeft, this.previousLabel);
335
+ }
336
+ renderNext(page, count) {
337
+ return this.renderNav("next", page + 1, page >= count, chevronRight, this.nextLabel);
338
+ }
339
+ renderPages(page) {
340
+ return repeat(this.items, (item) => item.kind === "page" ? `page-${item.page}` : item.key, (item) => item.kind === "ellipsis" ? html`<li>${this.renderEllipsis(item)}</li>` : html`<li>
341
+ <button
342
+ class="c2-pagination-control c2-pagination-item"
343
+ part="item"
344
+ type="button"
345
+ aria-label=${format(this.pageLabelTemplate, { page: item.page })}
346
+ aria-current=${item.page === page ? "page" : nothing}
347
+ ?disabled=${this.disabled}
348
+ @click=${() => this.handleNav(item.page, null)}
349
+ >
350
+ ${item.page}
351
+ </button>
352
+ </li>`);
353
+ }
354
+ /**
355
+ * The ellipsis is a control, not decoration: it jumps into the middle of the pages it hides, which is the only way
356
+ * to reach them in one move once the row has shed its numbers. It reads as "…" until it is hovered or focused, and
357
+ * then shows the direction it will take you, so the affordance is visible before the click.
358
+ */
359
+ renderEllipsis(item) {
360
+ const backwards = item.key === "start-ellipsis";
361
+ return html`<button
362
+ class="c2-pagination-control c2-pagination-ellipsis"
363
+ part="ellipsis"
364
+ type="button"
365
+ aria-label=${format(this.jumpLabelTemplate, {
366
+ page: item.target,
367
+ from: item.from,
368
+ to: item.to,
369
+ count: item.to - item.from + 1
370
+ })}
371
+ ?disabled=${this.disabled}
372
+ @click=${() => this.handleNav(item.target, null)}
373
+ >
374
+ <span class="c2-pagination-ellipsis-dots" aria-hidden="true">…</span>
375
+ <span class="c2-pagination-ellipsis-jump c2-pagination-icon" aria-hidden="true">${backwards ? chevronsLeft : chevronsRight}</span>
376
+ </button>`;
377
+ }
378
+ renderNumbered(page, count) {
379
+ return html`<ul class=${classMap({
380
+ "c2-pagination-list": true,
381
+ "is-measuring": this.measuring
382
+ })}>
383
+ ${this.showFirstLast ? html`<li>${this.renderNav("first", 1, page <= 1, chevronsLeft, this.firstLabel)}</li>` : nothing}
384
+ <li>${this.renderPrevious(page)}</li>
385
+ ${this.renderPages(page)}
386
+ <li>${this.renderNext(page, count)}</li>
387
+ ${this.showFirstLast ? html`<li>${this.renderNav("last", count, page >= count, chevronsRight, this.lastLabel)}</li>` : nothing}
388
+ </ul>`;
389
+ }
390
+ renderSimple(page, count) {
391
+ return html`<ul class=${classMap({
392
+ "c2-pagination-list": true,
393
+ "is-measuring": this.measuring
394
+ })}>
395
+ ${this.showFirstLast ? html`<li>${this.renderNav("first", 1, page <= 1, chevronsLeft, this.firstLabel)}</li>` : nothing}
396
+ <li>${this.renderPrevious(page)}</li>
397
+ <li class="c2-pagination-status" part="label" aria-live="polite">${format(this.pageTemplate, {
398
+ page,
399
+ pageCount: count
400
+ })}</li>
401
+ <li>${this.renderNext(page, count)}</li>
402
+ ${this.showFirstLast ? html`<li>${this.renderNav("last", count, page >= count, chevronsRight, this.lastLabel)}</li>` : nothing}
403
+ </ul>`;
404
+ }
405
+ renderPageSize() {
406
+ if (this.hidePageSize || this.pageSizeOptions.length === 0) return nothing;
407
+ const options = this.pageSizeOptions.includes(this.pageSize) ? this.pageSizeOptions : [...this.pageSizeOptions, this.pageSize].sort((a, b) => a - b);
408
+ return html`<div class="c2-pagination-page-size">
409
+ <span class="c2-pagination-label" part="label">${this.pageSizeLabel}</span>
410
+ <c2-select
411
+ class="c2-pagination-page-size-select"
412
+ part="page-size"
413
+ aria-label=${this.pageSizeLabel.replace(/\s*:\s*$/, "")}
414
+ ?disabled=${this.disabled}
415
+ .value=${[String(this.pageSize)]}
416
+ @selection-change=${this.handlePageSizeChange}
417
+ >
418
+ ${options.map((option) => html`<c2-list-item value=${String(option)}>${option}</c2-list-item>`)}
419
+ </c2-select>
420
+ </div>`;
421
+ }
422
+ renderCompact(page, count) {
423
+ const total = this.totalItems;
424
+ const start = total > 0 ? this.startIndex + 1 : 0;
425
+ return html`
426
+ ${this.renderPageSize()}
427
+ ${this.hideRange ? nothing : html`<span class="c2-pagination-label c2-pagination-status" part="label" aria-live="polite"
428
+ >${format(this.rangeTemplate, {
429
+ start,
430
+ end: this.endIndex,
431
+ total
432
+ })}</span
433
+ >`}
434
+ <div class="c2-pagination-arrows">
435
+ ${this.showFirstLast ? this.renderNav("first", 1, page <= 1, chevronsLeft, this.firstLabel) : nothing} ${this.renderPrevious(page)}
436
+ ${this.renderNext(page, count)} ${this.showFirstLast ? this.renderNav("last", count, page >= count, chevronsRight, this.lastLabel) : nothing}
437
+ </div>
438
+ `;
439
+ }
440
+ render() {
441
+ const count = this.pageCount;
442
+ const page = this.currentPage;
443
+ return html`<nav class="c2-pagination" part="pagination" aria-label=${this.ariaLabel ?? "Pagination"}>
444
+ ${this.variant === "compact" ? this.renderCompact(page, count) : this.variant === "simple" ? this.renderSimple(page, count) : this.renderNumbered(page, count)}
445
+ </nav>`;
446
+ }
447
+ };
448
+ __decorate([property({
449
+ type: String,
450
+ reflect: true
451
+ })], Pagination.prototype, "variant", void 0);
452
+ __decorate([property({
453
+ type: Number,
454
+ reflect: true
455
+ })], Pagination.prototype, "page", void 0);
456
+ __decorate([property({
457
+ type: Number,
458
+ attribute: "page-size"
459
+ })], Pagination.prototype, "pageSize", void 0);
460
+ __decorate([property({
461
+ type: Number,
462
+ attribute: "total-items"
463
+ })], Pagination.prototype, "totalItems", void 0);
464
+ __decorate([property({
465
+ type: Number,
466
+ attribute: "total-pages"
467
+ })], Pagination.prototype, "totalPages", void 0);
468
+ __decorate([property({
469
+ type: Number,
470
+ attribute: "sibling-count"
471
+ })], Pagination.prototype, "siblingCount", void 0);
472
+ __decorate([property({
473
+ type: Number,
474
+ attribute: "boundary-count"
475
+ })], Pagination.prototype, "boundaryCount", void 0);
476
+ __decorate([property({
477
+ attribute: "page-size-options",
478
+ converter: numberListConverter
479
+ })], Pagination.prototype, "pageSizeOptions", void 0);
480
+ __decorate([property({
481
+ type: Boolean,
482
+ attribute: "hide-page-size"
483
+ })], Pagination.prototype, "hidePageSize", void 0);
484
+ __decorate([property({
485
+ type: Boolean,
486
+ attribute: "hide-range"
487
+ })], Pagination.prototype, "hideRange", void 0);
488
+ __decorate([property({
489
+ type: Boolean,
490
+ attribute: "show-first-last"
491
+ })], Pagination.prototype, "showFirstLast", void 0);
492
+ __decorate([property({
493
+ type: Boolean,
494
+ attribute: "hide-nav-labels"
495
+ })], Pagination.prototype, "hideNavLabels", void 0);
496
+ __decorate([property({
497
+ type: Boolean,
498
+ reflect: true
499
+ })], Pagination.prototype, "disabled", void 0);
500
+ __decorate([property({
501
+ type: String,
502
+ attribute: "previous-label"
503
+ })], Pagination.prototype, "previousLabel", void 0);
504
+ __decorate([property({
505
+ type: String,
506
+ attribute: "next-label"
507
+ })], Pagination.prototype, "nextLabel", void 0);
508
+ __decorate([property({
509
+ type: String,
510
+ attribute: "first-label"
511
+ })], Pagination.prototype, "firstLabel", void 0);
512
+ __decorate([property({
513
+ type: String,
514
+ attribute: "last-label"
515
+ })], Pagination.prototype, "lastLabel", void 0);
516
+ __decorate([property({
517
+ type: String,
518
+ attribute: "page-size-label"
519
+ })], Pagination.prototype, "pageSizeLabel", void 0);
520
+ __decorate([property({
521
+ type: String,
522
+ attribute: "range-template"
523
+ })], Pagination.prototype, "rangeTemplate", void 0);
524
+ __decorate([property({
525
+ type: String,
526
+ attribute: "page-template"
527
+ })], Pagination.prototype, "pageTemplate", void 0);
528
+ __decorate([property({
529
+ type: String,
530
+ attribute: "page-label-template"
531
+ })], Pagination.prototype, "pageLabelTemplate", void 0);
532
+ __decorate([property({
533
+ type: String,
534
+ attribute: "jump-label-template"
535
+ })], Pagination.prototype, "jumpLabelTemplate", void 0);
536
+ __decorate([property({
537
+ type: String,
538
+ attribute: "aria-label"
539
+ })], Pagination.prototype, "ariaLabel", void 0);
540
+ __decorate([consume({
541
+ context: pagerContext,
542
+ subscribe: true
543
+ }), state()], Pagination.prototype, "pagerHost", void 0);
544
+ __decorate([state()], Pagination.prototype, "maxNumbers", void 0);
545
+ __decorate([state()], Pagination.prototype, "measuring", void 0);
546
+ __decorate([query(".c2-pagination-list")], Pagination.prototype, "list", void 0);
547
+ Pagination = __decorate([customElement("c2-pagination")], Pagination);
548
+ //#endregion
549
+ export { Pagination };
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@c2n/pagination",
3
+ "version": "0.0.7",
4
+ "type": "module",
5
+ "main": "dist/pagination.js",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./types/src/pagination.d.ts",
9
+ "default": "./dist/pagination.js"
10
+ },
11
+ "./react": {
12
+ "types": "./react.d.ts",
13
+ "default": "./react.js"
14
+ },
15
+ "./vue": {
16
+ "types": "./vue.d.ts",
17
+ "default": "./vue.js"
18
+ },
19
+ "./custom-elements.json": "./custom-elements.json"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "types",
24
+ "react.d.ts",
25
+ "react.js",
26
+ "vue.d.ts",
27
+ "vue.js"
28
+ ],
29
+ "keywords": [
30
+ "pagination",
31
+ "pager",
32
+ "navigation",
33
+ "web component",
34
+ "lit"
35
+ ],
36
+ "license": "MIT",
37
+ "author": "code2nguyen@gmail.com",
38
+ "publishConfig": {
39
+ "registry": "https://registry.npmjs.org",
40
+ "access": "public"
41
+ },
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/code2nguyen/web-components.git"
45
+ },
46
+ "scripts": {
47
+ "dev": "vite",
48
+ "build": "wireit",
49
+ "build:only": "vite build",
50
+ "type-check": "wireit"
51
+ },
52
+ "wireit": {
53
+ "type-check": {
54
+ "dependencies": [
55
+ "../../core:build",
56
+ "../select:build",
57
+ "../list-item:build"
58
+ ],
59
+ "command": "tsc -p tsconfig.lib.json --composite false"
60
+ },
61
+ "build": {
62
+ "dependencies": [
63
+ "type-check"
64
+ ],
65
+ "command": "vite build"
66
+ }
67
+ },
68
+ "dependencies": {
69
+ "@c2n/core": "0.0.7",
70
+ "@c2n/list-item": "0.0.7",
71
+ "@c2n/select": "0.0.7",
72
+ "@lit/context": "1.1.6",
73
+ "lit": "3.3.3"
74
+ },
75
+ "devDependencies": {
76
+ "@c2n/config": "*"
77
+ },
78
+ "customElements": "custom-elements.json",
79
+ "gitHead": "c8db398a27f7cd417d665fdf5da455fee2d4e910"
80
+ }
package/react.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ // GENERATED by @c2n/framework-types. Do not edit by hand.
2
+ //
3
+ // JSX types for the custom elements of @c2n/pagination. Import once, anywhere in the program:
4
+ //
5
+ // import '@c2n/pagination/react'
6
+ //
7
+ // Register the elements at module scope before React renders, or React writes an object prop as an
8
+ // attribute and it stringifies.
9
+
10
+ import type { DetailedHTMLProps, HTMLAttributes } from 'react'
11
+ import type { Pagination } from '@c2n/pagination'
12
+
13
+ /** Standard React host-element attributes plus the element's own public properties. */
14
+ type C2Props<T> = DetailedHTMLProps<HTMLAttributes<T>, T> & Partial<Omit<T, keyof HTMLElement>>
15
+
16
+ declare module 'react' {
17
+ namespace JSX {
18
+ interface IntrinsicElements {
19
+ 'c2-pagination': C2Props<Pagination>
20
+ }
21
+ }
22
+ }
23
+
24
+ export {}
package/react.js ADDED
@@ -0,0 +1,3 @@
1
+ // GENERATED by @c2n/framework-types. Do not edit by hand.
2
+ // Types only — see the matching .d.ts.
3
+ export {}
@@ -0,0 +1,278 @@
1
+ import { LitElement, type PropertyValues, type TemplateResult } from 'lit';
2
+ import type { TypedAddEventListener, TypedRemoveEventListener } from '@c2n/core/event-helper.js';
3
+ import '@c2n/select';
4
+ import '@c2n/list-item';
5
+ /** Which layout the pagination renders. */
6
+ export type PaginationVariant = 'numbered' | 'simple' | 'compact';
7
+ export interface PageChangeEventDetail {
8
+ /** The page now shown, 1-based. */
9
+ page: number;
10
+ /** The page that was shown before, 1-based. */
11
+ previousPage: number;
12
+ /** Items per page at the moment of the change. */
13
+ pageSize: number;
14
+ /** Total number of pages. */
15
+ pageCount: number;
16
+ /** Zero-based index of the first item of the page: `items.slice(startIndex, endIndex)`. */
17
+ startIndex: number;
18
+ /** Zero-based index after the last item of the page. Equals `startIndex` when the total is unknown. */
19
+ endIndex: number;
20
+ }
21
+ export interface PageSizeChangeEventDetail {
22
+ /** Items per page now. */
23
+ pageSize: number;
24
+ /** Items per page before the change. */
25
+ previousPageSize: number;
26
+ /** The page shown after the change; the pagination keeps the first item of the old page visible. */
27
+ page: number;
28
+ }
29
+ type PaginationItem = {
30
+ kind: 'page';
31
+ page: number;
32
+ }
33
+ /** `from`..`to` are the pages this ellipsis hides; `target` is the one clicking it jumps to. */
34
+ | {
35
+ kind: 'ellipsis';
36
+ key: 'start-ellipsis' | 'end-ellipsis';
37
+ from: number;
38
+ to: number;
39
+ target: number;
40
+ };
41
+ /** Events fired by {@link Pagination}, keyed for `addEventListener`. */
42
+ export interface PaginationEventMap {
43
+ 'page-change': CustomEvent<PageChangeEventDetail>;
44
+ 'page-size-change': CustomEvent<PageSizeChangeEventDetail>;
45
+ }
46
+ export interface Pagination {
47
+ addEventListener: TypedAddEventListener<Pagination, PaginationEventMap>;
48
+ removeEventListener: TypedRemoveEventListener<Pagination, PaginationEventMap>;
49
+ }
50
+ /**
51
+ * Page navigation for a list, a table or search results, in three layouts chosen with `variant`:
52
+ *
53
+ * - `numbered` (default) — previous/next controls around the page numbers, with an ellipsis standing in for the pages
54
+ * that do not fit. `sibling-count` and `boundary-count` decide how many numbers surround the current page and each
55
+ * end of the range.
56
+ * - `simple` — previous/next around a "Page 2 of 10" status, for narrow toolbars.
57
+ * - `compact` — the table-footer layout: a rows-per-page select, the range of items on show ("21–30 of 100") and two
58
+ * arrow buttons.
59
+ *
60
+ * `simple` and `compact` show no page numbers, so `show-first-last` adds the jumps to either end that the numbers
61
+ * would otherwise provide.
62
+ *
63
+ * The row of controls never wraps. When the requested page numbers do not fit the host's width, the component sheds
64
+ * them — siblings first, then boundaries — until they do, and puts them back when the container grows again; the
65
+ * current page always survives. Only the three groups of the `compact` variant may drop onto a second line.
66
+ *
67
+ * The ellipsis is a control, not decoration: it jumps into the middle of the pages it hides, which is what keeps
68
+ * those pages one click away once the row has shed its numbers. It shows the direction of that jump on hover and
69
+ * keyboard focus.
70
+ *
71
+ * How many pages there are comes from `total-items` and `page-size`, or from `total-pages` when the total item count
72
+ * is unknown. The element keeps `page` itself and reflects it, so listening to `page-change` is enough; setting `page`
73
+ * back from the outside works the same way. Every label is an attribute, and `range-template` / `page-template` /
74
+ * `page-label-template` hold the text around the numbers, so the whole element can be translated without a slot.
75
+ *
76
+ * @tag c2-pagination
77
+ *
78
+ * @slot previous-icon - Icon of the previous-page control. Defaults to a chevron.
79
+ * @slot next-icon - Icon of the next-page control. Defaults to a chevron.
80
+ * @slot first-icon - Icon of the first-page control (`show-first-last`). Defaults to a double chevron.
81
+ * @slot last-icon - Icon of the last-page control (`show-first-last`). Defaults to a double chevron.
82
+ *
83
+ * @event {CustomEvent<PageChangeEventDetail>} page-change - Fired after the shown page changes, by a control or by a page-size change. `detail.startIndex` / `detail.endIndex` slice the items of the new page.
84
+ * @event {CustomEvent<PageSizeChangeEventDetail>} page-size-change - Fired after the user picks another rows-per-page value.
85
+ *
86
+ * @cssproperty {pixel} [--c2-pagination--gap=4px] - Space between the controls.
87
+ * @cssproperty {pixel} [--c2-pagination--section-gap=24px] - Space between the groups of the `compact` variant.
88
+ * @cssproperty {justify-content} [--c2-pagination--justify-content=flex-start] - Where the controls sit in the host's width.
89
+ *
90
+ * @cssproperty {pixel} [--c2-pagination__item--min-width=36px]
91
+ * @cssproperty {pixel} [--c2-pagination__item--height=36px]
92
+ * @cssproperty {padding} [--c2-pagination__item--padding-left=8px]
93
+ * @cssproperty {padding} [--c2-pagination__item--padding-right=8px]
94
+ * @cssproperty {color} [--c2-pagination__item--color=#18181b]
95
+ * @cssproperty {color} [--c2-pagination__item--background-color=transparent]
96
+ * @cssproperty {border} [--c2-pagination__item--border=1px solid transparent]
97
+ * @cssproperty {border-radius} [--c2-pagination__item--border-radius=8px]
98
+ * @cssproperty {font-size} [--c2-pagination__item--font-size=14px]
99
+ * @cssproperty {font-weight} [--c2-pagination__item--font-weight=500]
100
+ * @cssproperty {color} [--c2-pagination__item__hover--color=#18181b]
101
+ * @cssproperty {color} [--c2-pagination__item__hover--background-color=#f4f4f5]
102
+ * @cssproperty {border} [--c2-pagination__item__hover--border=1px solid transparent]
103
+ * @cssproperty {color} [--c2-pagination__item__selected--color=#18181b]
104
+ * @cssproperty {color} [--c2-pagination__item__selected--background-color=#ffffff]
105
+ * @cssproperty {border} [--c2-pagination__item__selected--border=1px solid #e4e4e7]
106
+ * @cssproperty {font-weight} [--c2-pagination__item__selected--font-weight=500]
107
+ * @cssproperty {outline} [--c2-pagination__item__focus--outline=2px solid rgba(2, 101, 220, 0.4)]
108
+ * @cssproperty {pixel} [--c2-pagination__item__focus--outline-offset=1px]
109
+ * @cssproperty {opacity} [--c2-pagination__item__disabled--opacity=0.38]
110
+ *
111
+ * @cssproperty {pixel} [--c2-pagination__nav--gap=6px] - Space between a previous/next icon and its label.
112
+ * @cssproperty {padding} [--c2-pagination__nav--padding-left=10px]
113
+ * @cssproperty {padding} [--c2-pagination__nav--padding-right=10px]
114
+ * @cssproperty {color} [--c2-pagination__nav--color=#18181b]
115
+ * @cssproperty {color} [--c2-pagination__nav--background-color=transparent]
116
+ * @cssproperty {border} [--c2-pagination__nav--border=1px solid transparent]
117
+ * @cssproperty {border-radius} [--c2-pagination__nav--border-radius=8px]
118
+ * @cssproperty {font-size} [--c2-pagination__nav--font-size=14px]
119
+ * @cssproperty {font-weight} [--c2-pagination__nav--font-weight=500]
120
+ * @cssproperty {color} [--c2-pagination__nav__hover--color=#18181b]
121
+ * @cssproperty {color} [--c2-pagination__nav__hover--background-color=#f4f4f5]
122
+ * @cssproperty {border} [--c2-pagination__nav__hover--border=1px solid transparent]
123
+ *
124
+ * @cssproperty {pixel} [--c2-pagination__icon--size=16px]
125
+ * @cssproperty {color} --c2-pagination__icon--color - Defaults to the colour of the control it sits in.
126
+ *
127
+ * @cssproperty {color} [--c2-pagination__ellipsis--color=#71717a]
128
+ * @cssproperty {pixel} [--c2-pagination__ellipsis--min-width=36px]
129
+ *
130
+ * @cssproperty {color} [--c2-pagination__label--color=#71717a]
131
+ * @cssproperty {font-size} [--c2-pagination__label--font-size=14px]
132
+ * @cssproperty {font-weight} [--c2-pagination__label--font-weight=500]
133
+ * @cssproperty {pixel} [--c2-pagination__label--gap=8px] - Space between the rows-per-page label and its select.
134
+ * @cssproperty {pixel} [--c2-pagination__page-size-list--min-width=56px] - Floor for the rows-per-page dropdown: room for a three-digit option, so a one-digit one does not open a sliver.
135
+ *
136
+ * @internalcomponent c2-select
137
+ * @internalcomponent c2-list-item
138
+ */
139
+ export declare class Pagination extends LitElement {
140
+ static styles: import("lit").CSSResult;
141
+ /** Layout of the controls: page numbers, a "Page 2 of 10" status, or the table-footer row. */
142
+ variant: PaginationVariant;
143
+ /** The page on show, 1-based. Reflected, and clamped to the available range. */
144
+ page: number;
145
+ /** Items per page. Together with `total-items` it decides how many pages there are. */
146
+ pageSize: number;
147
+ /** Total number of items to page through. */
148
+ totalItems: number;
149
+ /** Number of pages, when the item count is unknown. Takes precedence over `total-items`. */
150
+ totalPages: number;
151
+ /** How many page numbers are shown on each side of the current page. */
152
+ siblingCount: number;
153
+ /** How many page numbers are always shown at each end of the range. */
154
+ boundaryCount: number;
155
+ /** Rows-per-page choices of the `compact` variant. Empty hides the select. */
156
+ pageSizeOptions: number[];
157
+ /** Hide the rows-per-page select of the `compact` variant. */
158
+ hidePageSize: boolean;
159
+ /** Hide the range status ("21–30 of 100") of the `compact` variant. */
160
+ hideRange: boolean;
161
+ /**
162
+ * Add first-page and last-page controls around the previous/next pair. Meant for `simple` and `compact`, which show
163
+ * no page numbers; in the `numbered` variant the pinned boundary numbers already reach both ends, unless
164
+ * `boundary-count` is `0`.
165
+ */
166
+ showFirstLast: boolean;
167
+ /** Keep the previous/next icons but drop their visible text; the accessible name is unchanged. */
168
+ hideNavLabels: boolean;
169
+ /** Dim every control and ignore interaction. */
170
+ disabled: boolean;
171
+ /** Visible and accessible label of the previous-page control. */
172
+ previousLabel: string;
173
+ /** Visible and accessible label of the next-page control. */
174
+ nextLabel: string;
175
+ /** Visible and accessible label of the first-page control. */
176
+ firstLabel: string;
177
+ /** Visible and accessible label of the last-page control. */
178
+ lastLabel: string;
179
+ /** Text before the rows-per-page select. A trailing colon is dropped from the select's accessible name. */
180
+ pageSizeLabel: string;
181
+ /** Range status of the `compact` variant. Placeholders: `{start}`, `{end}`, `{total}`. */
182
+ rangeTemplate: string;
183
+ /** Status of the `simple` variant. Placeholders: `{page}`, `{pageCount}`. */
184
+ pageTemplate: string;
185
+ /** Accessible name of a page number. Placeholder: `{page}`. */
186
+ pageLabelTemplate: string;
187
+ /** Accessible name of an ellipsis. Placeholders: `{page}` (where it jumps), `{from}`, `{to}`, `{count}` (pages hidden). */
188
+ jumpLabelTemplate: string;
189
+ /** Accessible name of the navigation landmark. */
190
+ ariaLabel: string;
191
+ /** The nav control the user last pressed, so focus can follow it when the press disables it. */
192
+ private lastPressedNav;
193
+ /**
194
+ * The paging host this pager sits inside — a `c2-table` — or `undefined` when it stands alone. While it is set the
195
+ * pager is a controlled view: it reports every request upward and renders whatever the host pushes back.
196
+ */
197
+ private pagerHost;
198
+ /** Upper bound on page numbers that the measured width allows; `null` means the requested counts fit. */
199
+ private maxNumbers;
200
+ /** Render the requested counts invisibly for one frame so `measure` can read their natural width. */
201
+ private measuring;
202
+ private list?;
203
+ private resizeObserver?;
204
+ private measuredWidth;
205
+ /** Number of pages, from `total-pages` when given, otherwise from `total-items` and `page-size`. At least 1. */
206
+ get pageCount(): number;
207
+ /** `page` clamped to `1..pageCount`; this is the page the element actually shows. */
208
+ get currentPage(): number;
209
+ /** Zero-based index of the first item of the current page. */
210
+ get startIndex(): number;
211
+ /** Zero-based index after the last item of the current page; equals `startIndex` when the total is unknown. */
212
+ get endIndex(): number;
213
+ /**
214
+ * The page numbers and ellipses actually rendered: the requested `sibling-count` / `boundary-count`, narrowed by
215
+ * whatever the measured width allows (see `measure`). Reading it tells you what the row shows right now.
216
+ */
217
+ get items(): PaginationItem[];
218
+ /**
219
+ * The page numbers and ellipses for one pair of counts: both ends show `boundary` numbers, the current page shows
220
+ * `sibling` numbers on each side, and everything left over collapses into an ellipsis. An ellipsis standing for a
221
+ * single page is replaced by that page, so the row never grows a gap it did not need, and the number of slots stays
222
+ * the same as the current page moves.
223
+ */
224
+ private itemsFor;
225
+ /** Show `page`, clamped to the available range. Fires `page-change` when the page actually moves. */
226
+ goToPage(page: number): void;
227
+ private handleNav;
228
+ /** Picking another page size keeps the first item of the current page on screen, the way a table footer does. */
229
+ private handlePageSizeChange;
230
+ connectedCallback(): void;
231
+ disconnectedCallback(): void;
232
+ private announce;
233
+ /**
234
+ * A host that has not been upgraded yet hears nothing: both the connect event and the context request reach the
235
+ * document and die. That happens whenever this module is evaluated before the host's, which is a bundler's choice,
236
+ * not the author's — so wait for every custom-element ancestor to be defined and then ask once more.
237
+ */
238
+ private retryAnnounce;
239
+ private parentOf;
240
+ /** A hosted pager shows the host's paging state, not its own; its own properties are only the standalone fallback. */
241
+ protected willUpdate(_changed: PropertyValues): void;
242
+ /** Start over from the requested counts, so a container that grew back gets its page numbers back. */
243
+ private requestMeasure;
244
+ updated(changed: PropertyValues): void;
245
+ /**
246
+ * The row never wraps, so when the requested page numbers do not fit the host, work out how many do and let `items`
247
+ * shed siblings and boundaries until they do. One pass: the widest rendered number plus a gap is the cost of a slot,
248
+ * and the overflow divided by that is how many slots have to go. Runs before paint, so there is no flicker.
249
+ */
250
+ private measure;
251
+ /**
252
+ * Reaching the first or the last page disables the control that was just pressed, which would otherwise drop focus
253
+ * to the document. Hand it to the opposite control so the keyboard user stays in the pagination.
254
+ */
255
+ private restoreFocus;
256
+ private renderNav;
257
+ private renderPrevious;
258
+ private renderNext;
259
+ private renderPages;
260
+ /**
261
+ * The ellipsis is a control, not decoration: it jumps into the middle of the pages it hides, which is the only way
262
+ * to reach them in one move once the row has shed its numbers. It reads as "…" until it is hovered or focused, and
263
+ * then shows the direction it will take you, so the affordance is visible before the click.
264
+ */
265
+ private renderEllipsis;
266
+ private renderNumbered;
267
+ private renderSimple;
268
+ private renderPageSize;
269
+ private renderCompact;
270
+ render(): TemplateResult<1>;
271
+ }
272
+ declare global {
273
+ interface HTMLElementTagNameMap {
274
+ 'c2-pagination': Pagination;
275
+ }
276
+ }
277
+ export {};
278
+ //# sourceMappingURL=pagination.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pagination.d.ts","sourceRoot":"","sources":["../../src/pagination.ts"],"names":[],"mappings":"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"}
package/vue.d.ts ADDED
@@ -0,0 +1,55 @@
1
+ // GENERATED by @c2n/framework-types. Do not edit by hand.
2
+ //
3
+ // Vue template types for the custom elements of @c2n/pagination. Import once, anywhere in the program:
4
+ //
5
+ // import '@c2n/pagination/vue'
6
+ //
7
+ // Tell the compiler about the tags as well, or every c2-* tag is resolved as a Vue component and renders
8
+ // nothing: template.compilerOptions.isCustomElement = (tag) => tag.startsWith('c2-') in vite.config.ts.
9
+
10
+ import type { DefineComponent, HTMLAttributes } from 'vue'
11
+ import type { Pagination, PaginationEventMap } from '@c2n/pagination'
12
+
13
+ /** The element's own public properties, plus every attribute Vue understands on a host element. */
14
+ type C2Props<T> = Partial<Omit<T, keyof HTMLElement>> & HTMLAttributes
15
+
16
+ declare module 'vue' {
17
+ interface GlobalComponents {
18
+ 'c2-pagination': DefineComponent<
19
+ C2Props<Pagination> & {
20
+ 'page-size'?: unknown
21
+ 'total-items'?: unknown
22
+ 'total-pages'?: unknown
23
+ 'sibling-count'?: unknown
24
+ 'boundary-count'?: unknown
25
+ 'page-size-options'?: unknown
26
+ 'hide-page-size'?: unknown
27
+ 'hide-range'?: unknown
28
+ 'show-first-last'?: unknown
29
+ 'hide-nav-labels'?: unknown
30
+ 'previous-label'?: unknown
31
+ 'next-label'?: unknown
32
+ 'first-label'?: unknown
33
+ 'last-label'?: unknown
34
+ 'page-size-label'?: unknown
35
+ 'range-template'?: unknown
36
+ 'page-template'?: unknown
37
+ 'page-label-template'?: unknown
38
+ 'jump-label-template'?: unknown
39
+ 'aria-label'?: unknown
40
+ onPageChange?: (event: PaginationEventMap['page-change']) => void
41
+ onPageSizeChange?: (event: PaginationEventMap['page-size-change']) => void
42
+ onPAGER_CONNECT_EVENT?: (event: PaginationEventMap['PAGER_CONNECT_EVENT']) => void
43
+ }
44
+ >
45
+ }
46
+ }
47
+
48
+ declare module '@vue/runtime-dom' {
49
+ interface HTMLAttributes {
50
+ /** Vue's own definition omits it, and slotting a plain element into a component needs it. */
51
+ slot?: string
52
+ }
53
+ }
54
+
55
+ export {}
package/vue.js ADDED
@@ -0,0 +1,3 @@
1
+ // GENERATED by @c2n/framework-types. Do not edit by hand.
2
+ // Types only — see the matching .d.ts.
3
+ export {}