PluginProbe
Tableberg – Simple Gutenberg Table Block / 1.1.5
Tableberg – Simple Gutenberg Table Block v1.1.5
1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.5 1.0.4 1.0.3 1.0.2 1.0.1 trunk 0.0.2 0.2.1 0.3.2 0.3.3 0.4.1 0.5.0 0.5.1 0.5.2 0.5.3 0.5.4 0.5.5 0.5.6 0.5.7 All 42 releases
tableberg / src / frontend / pagination.ts

pagination.ts in Tableberg – Simple Gutenberg Table Block 1.1.5, at src/frontend/pagination.ts

379 lines 9.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { TABLEBERG_ROWS_CHANGED_EVENT } from "./sorting";
2 import { TABLEBERG_SEARCH_CHANGED_EVENT } from "./search";
3
4 interface PaginationConfig {
5 enabled: boolean;
6 pageSize: number;
7 showPageNumbers: boolean;
8 showPrevNext: boolean;
9 }
10
11 interface PaginationState {
12 table: HTMLTableElement;
13 tbody: HTMLTableSectionElement;
14 headerEnabled: boolean;
15 footerEnabled: boolean;
16 config: PaginationConfig;
17 currentPage: number;
18 controlsEl: HTMLDivElement;
19 }
20
21 interface TableRows {
22 headerRow: HTMLTableRowElement | null;
23 footerRow: HTMLTableRowElement | null;
24 dataRows: HTMLTableRowElement[];
25 }
26
27 type PageItem = number | "ellipsis";
28
29 function getTableRows(state: PaginationState): TableRows {
30 const rows = Array.from(state.tbody.rows);
31 const headerRow = state.headerEnabled && rows.length > 0 ? rows[0] : null;
32 const footerRow =
33 state.footerEnabled && rows.length > (headerRow ? 1 : 0)
34 ? rows[rows.length - 1]
35 : null;
36
37 const dataStart = headerRow ? 1 : 0;
38 const dataEnd = footerRow ? rows.length - 1 : rows.length;
39
40 return {
41 headerRow,
42 footerRow,
43 dataRows: rows.slice(dataStart, Math.max(dataStart, dataEnd)),
44 };
45 }
46
47 function getTotalPages(dataRowCount: number, pageSize: number): number {
48 if (dataRowCount <= 0 || pageSize <= 0) {
49 return 1;
50 }
51
52 return Math.ceil(dataRowCount / pageSize);
53 }
54
55 function clampPage(page: number, totalPages: number): number {
56 if (totalPages <= 1) {
57 return 0;
58 }
59
60 if (page < 0) {
61 return 0;
62 }
63
64 if (page > totalPages - 1) {
65 return totalPages - 1;
66 }
67
68 return page;
69 }
70
71 function getPageItems(currentPage: number, totalPages: number): PageItem[] {
72 const pages: PageItem[] = [];
73 const maxVisiblePages = 5;
74
75 if (totalPages <= maxVisiblePages) {
76 for (let page = 0; page < totalPages; page++) {
77 pages.push(page);
78 }
79 return pages;
80 }
81
82 pages.push(0);
83
84 if (currentPage > 2) {
85 pages.push("ellipsis");
86 }
87
88 const start = Math.max(1, currentPage - 1);
89 const end = Math.min(totalPages - 2, currentPage + 1);
90
91 for (let page = start; page <= end; page++) {
92 if (!pages.includes(page)) {
93 pages.push(page);
94 }
95 }
96
97 if (currentPage < totalPages - 3) {
98 pages.push("ellipsis");
99 }
100
101 if (!pages.includes(totalPages - 1)) {
102 pages.push(totalPages - 1);
103 }
104
105 return pages;
106 }
107
108 function setDataRowsVisibility(
109 dataRows: HTMLTableRowElement[],
110 matchingRows: HTMLTableRowElement[],
111 pageSize: number,
112 currentPage: number
113 ) {
114 const matchingRowSet = new Set(matchingRows);
115 const start = currentPage * pageSize;
116 const end = start + pageSize;
117
118 let matchingIndex = 0;
119
120 dataRows.forEach(row => {
121 if (!matchingRowSet.has(row)) {
122 row.hidden = true;
123 return;
124 }
125
126 row.hidden = matchingIndex < start || matchingIndex >= end;
127 matchingIndex++;
128 });
129 }
130
131 function getSearchMatchedRows(
132 dataRows: HTMLTableRowElement[]
133 ): HTMLTableRowElement[] {
134 return dataRows.filter(row => row.dataset.tablebergSearchMatch !== "false");
135 }
136
137 function createButton(
138 label: string,
139 className: string,
140 onClick: () => void,
141 options?: {
142 disabled?: boolean;
143 ariaLabel?: string;
144 }
145 ): HTMLButtonElement {
146 const button = document.createElement("button");
147 button.type = "button";
148 button.className = className;
149
150 if (options?.disabled) {
151 button.disabled = true;
152 }
153
154 if (options?.ariaLabel) {
155 button.setAttribute("aria-label", options.ariaLabel);
156 }
157
158 button.textContent = label;
159 button.addEventListener("click", onClick);
160
161 return button;
162 }
163
164 function renderControls(state: PaginationState, totalPages: number) {
165 const { controlsEl, config, currentPage } = state;
166 const { showPageNumbers, showPrevNext } = config;
167
168 controlsEl.replaceChildren();
169
170 if (totalPages <= 1 || (!showPageNumbers && !showPrevNext)) {
171 controlsEl.hidden = true;
172 return;
173 }
174
175 controlsEl.hidden = false;
176
177 const canGoPrev = currentPage > 0;
178 const canGoNext = currentPage < totalPages - 1;
179 const buttonClass = "tableberg-pagination__button";
180
181 if (showPrevNext) {
182 controlsEl.appendChild(
183 createButton(
184 "\u2039",
185 `${buttonClass} ${buttonClass}--arrow`,
186 () => {
187 goToPage(state, currentPage - 1);
188 },
189 {
190 disabled: !canGoPrev,
191 ariaLabel: "Previous page",
192 }
193 )
194 );
195 }
196
197 if (showPageNumbers) {
198 const pageItems = getPageItems(currentPage, totalPages);
199
200 pageItems.forEach(item => {
201 if (item === "ellipsis") {
202 const ellipsis = document.createElement("span");
203 ellipsis.className = "tableberg-pagination__ellipsis";
204 ellipsis.textContent = "...";
205 ellipsis.setAttribute("aria-hidden", "true");
206 controlsEl.appendChild(ellipsis);
207 return;
208 }
209
210 const isActive = currentPage === item;
211 controlsEl.appendChild(
212 createButton(
213 String(item + 1),
214 isActive
215 ? `${buttonClass} ${buttonClass}--active`
216 : buttonClass,
217 () => {
218 goToPage(state, item);
219 }
220 )
221 );
222 });
223 }
224
225 if (showPrevNext) {
226 controlsEl.appendChild(
227 createButton(
228 "\u203A",
229 `${buttonClass} ${buttonClass}--arrow`,
230 () => {
231 goToPage(state, currentPage + 1);
232 },
233 {
234 disabled: !canGoNext,
235 ariaLabel: "Next page",
236 }
237 )
238 );
239 }
240 }
241
242 function renderPagination(state: PaginationState) {
243 const { headerRow, footerRow, dataRows } = getTableRows(state);
244 const matchingRows = getSearchMatchedRows(dataRows);
245 const totalPages = getTotalPages(
246 matchingRows.length,
247 state.config.pageSize
248 );
249
250 state.currentPage = clampPage(state.currentPage, totalPages);
251
252 if (headerRow) {
253 headerRow.hidden = false;
254 }
255
256 if (footerRow) {
257 footerRow.hidden = false;
258 }
259
260 setDataRowsVisibility(
261 dataRows,
262 matchingRows,
263 state.config.pageSize,
264 state.currentPage
265 );
266 renderControls(state, totalPages);
267 }
268
269 function goToPage(state: PaginationState, page: number) {
270 const { dataRows } = getTableRows(state);
271 const matchingRows = getSearchMatchedRows(dataRows);
272 const totalPages = getTotalPages(
273 matchingRows.length,
274 state.config.pageSize
275 );
276 const nextPage = clampPage(page, totalPages);
277
278 if (nextPage === state.currentPage) {
279 return;
280 }
281
282 state.currentPage = nextPage;
283 renderPagination(state);
284 }
285
286 function parsePaginationConfig(
287 table: HTMLTableElement
288 ): PaginationConfig | null {
289 const rawConfig = table.dataset.tablebergPagination;
290 if (!rawConfig) {
291 return null;
292 }
293
294 let parsedConfig: unknown;
295
296 try {
297 parsedConfig = JSON.parse(rawConfig);
298 } catch {
299 return null;
300 }
301
302 if (!parsedConfig || typeof parsedConfig !== "object") {
303 return null;
304 }
305
306 const config = parsedConfig as Partial<PaginationConfig>;
307
308 const pageSizeValue = Number(config.pageSize);
309 const pageSize =
310 Number.isFinite(pageSizeValue) && pageSizeValue > 0
311 ? Math.floor(pageSizeValue)
312 : 10;
313
314 return {
315 enabled: config.enabled === true,
316 pageSize,
317 showPageNumbers: config.showPageNumbers !== false,
318 showPrevNext: config.showPrevNext !== false,
319 };
320 }
321
322 function hasRowSpanningCells(tbody: HTMLTableSectionElement): boolean {
323 return Array.from(tbody.rows).some(row =>
324 Array.from(row.cells).some(cell => cell.rowSpan > 1)
325 );
326 }
327
328 function setupPagination(table: HTMLTableElement) {
329 if (table.dataset.tablebergPaginationInitialized === "true") {
330 return;
331 }
332
333 const config = parsePaginationConfig(table);
334 if (!config || !config.enabled) {
335 return;
336 }
337
338 const tbody = table.tBodies.item(0);
339 if (!tbody || hasRowSpanningCells(tbody)) {
340 return;
341 }
342
343 const controlsEl = document.createElement("div");
344 controlsEl.className = "tableberg-pagination";
345 table.insertAdjacentElement("afterend", controlsEl);
346
347 const state: PaginationState = {
348 table,
349 tbody,
350 headerEnabled: table.dataset.tablebergHeader === "true",
351 footerEnabled: table.dataset.tablebergFooter === "true",
352 config,
353 currentPage: 0,
354 controlsEl,
355 };
356
357 table.addEventListener(TABLEBERG_ROWS_CHANGED_EVENT, () => {
358 renderPagination(state);
359 });
360
361 table.addEventListener(TABLEBERG_SEARCH_CHANGED_EVENT, () => {
362 state.currentPage = 0;
363 renderPagination(state);
364 });
365
366 renderPagination(state);
367 table.dataset.tablebergPaginationInitialized = "true";
368 }
369
370 export function initializePagination() {
371 const tables = Array.from(
372 document.querySelectorAll<HTMLTableElement>(
373 ".wp-block-tableberg[data-tableberg-pagination]"
374 )
375 );
376
377 tables.forEach(setupPagination);
378 }
379