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 / sorting.ts

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

305 lines 8.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 export const TABLEBERG_ROWS_CHANGED_EVENT = "tableberg:rows-changed";
2
3 type SortableType = "text" | "number" | "date";
4 type SortOrder = "asc" | "desc" | null;
5
6 interface TableSortState {
7 table: HTMLTableElement;
8 tbody: HTMLTableSectionElement;
9 headerEnabled: boolean;
10 footerEnabled: boolean;
11 sortableColumns: Map<number, SortableType>;
12 originalRows: HTMLTableRowElement[];
13 currentColumn: number | null;
14 currentOrder: SortOrder;
15 }
16
17 interface TableRows {
18 headerRow: HTMLTableRowElement | null;
19 footerRow: HTMLTableRowElement | null;
20 dataRows: HTMLTableRowElement[];
21 }
22
23 function isSortableType(value: string | null): value is SortableType {
24 return value === "text" || value === "number" || value === "date";
25 }
26
27 function getColumnIndex(cell: HTMLTableCellElement): number | null {
28 const rawColumn = cell.getAttribute("data-cell-col");
29 if (!rawColumn) {
30 return null;
31 }
32
33 const column = Number.parseInt(rawColumn, 10);
34 return Number.isNaN(column) ? null : column;
35 }
36
37 function getCellText(row: HTMLTableRowElement, column: number): string {
38 const cell = row.querySelector<HTMLElement>(`[data-cell-col="${column}"]`);
39 return (cell?.textContent || "").trim();
40 }
41
42 function parseSortValue(
43 rawValue: string,
44 sortType: SortableType
45 ): string | number {
46 if (sortType === "number") {
47 const cleaned = rawValue.replace(/[^0-9.-]/g, "");
48 const numberValue = Number.parseFloat(cleaned);
49 return Number.isNaN(numberValue) ? 0 : numberValue;
50 }
51
52 if (sortType === "date") {
53 const timestamp = Date.parse(rawValue);
54 return Number.isNaN(timestamp) ? 0 : timestamp;
55 }
56
57 return rawValue.toLowerCase();
58 }
59
60 function compareRows(
61 rowA: HTMLTableRowElement,
62 rowB: HTMLTableRowElement,
63 column: number,
64 sortType: SortableType,
65 order: Exclude<SortOrder, null>
66 ): number {
67 const valueA = parseSortValue(getCellText(rowA, column), sortType);
68 const valueB = parseSortValue(getCellText(rowB, column), sortType);
69
70 let result = 0;
71 if (typeof valueA === "number" && typeof valueB === "number") {
72 result = valueA - valueB;
73 } else {
74 result = String(valueA).localeCompare(String(valueB));
75 }
76
77 return order === "asc" ? result : -result;
78 }
79
80 function getTableRows(state: TableSortState): TableRows {
81 const rows = Array.from(state.tbody.rows);
82 const headerRow = state.headerEnabled && rows.length > 0 ? rows[0] : null;
83 const footerRow =
84 state.footerEnabled && rows.length > (headerRow ? 1 : 0)
85 ? rows[rows.length - 1]
86 : null;
87
88 const dataStart = headerRow ? 1 : 0;
89 const dataEnd = footerRow ? rows.length - 1 : rows.length;
90
91 return {
92 headerRow,
93 footerRow,
94 dataRows: rows.slice(dataStart, Math.max(dataStart, dataEnd)),
95 };
96 }
97
98 function ensureSortIndicator(cell: HTMLTableCellElement): HTMLSpanElement {
99 const existingIndicator = cell.querySelector<HTMLSpanElement>(
100 ".tableberg-sort-indicator"
101 );
102 if (existingIndicator) {
103 return existingIndicator;
104 }
105
106 const indicator = document.createElement("span");
107 indicator.className = "tableberg-sort-indicator";
108 indicator.setAttribute("aria-hidden", "true");
109 indicator.textContent = "\u25B2\u25BC";
110 cell.appendChild(indicator);
111
112 return indicator;
113 }
114
115 function updateSortIndicators(state: TableSortState) {
116 const { headerRow } = getTableRows(state);
117 if (!headerRow) {
118 return;
119 }
120
121 const sortableHeaders = Array.from(
122 headerRow.querySelectorAll<HTMLTableCellElement>("th[data-sortable]")
123 );
124
125 for (const headerCell of sortableHeaders) {
126 const column = getColumnIndex(headerCell);
127 if (column === null) {
128 continue;
129 }
130
131 const indicator = ensureSortIndicator(headerCell);
132 const isActive =
133 state.currentColumn === column && state.currentOrder !== null;
134
135 if (!isActive) {
136 indicator.textContent = "\u25B2\u25BC";
137 indicator.classList.remove("tableberg-sort-indicator--active");
138 headerCell.setAttribute("aria-sort", "none");
139 continue;
140 }
141
142 indicator.textContent =
143 state.currentOrder === "asc" ? "\u25B2" : "\u25BC";
144 indicator.classList.add("tableberg-sort-indicator--active");
145 headerCell.setAttribute(
146 "aria-sort",
147 state.currentOrder === "asc" ? "ascending" : "descending"
148 );
149 }
150 }
151
152 function renderRows(state: TableSortState, dataRows: HTMLTableRowElement[]) {
153 const { headerRow, footerRow } = getTableRows(state);
154
155 const rowsToRender: HTMLTableRowElement[] = [];
156 if (headerRow) {
157 rowsToRender.push(headerRow);
158 }
159
160 rowsToRender.push(...dataRows);
161
162 if (footerRow) {
163 rowsToRender.push(footerRow);
164 }
165
166 for (const row of rowsToRender) {
167 state.tbody.appendChild(row);
168 }
169
170 updateSortIndicators(state);
171
172 state.table.dispatchEvent(new CustomEvent(TABLEBERG_ROWS_CHANGED_EVENT));
173 }
174
175 function applySort(state: TableSortState) {
176 const { currentColumn, currentOrder } = state;
177
178 if (currentColumn === null || currentOrder === null) {
179 renderRows(state, state.originalRows.slice());
180 return;
181 }
182
183 const sortType = state.sortableColumns.get(currentColumn);
184 if (!sortType) {
185 renderRows(state, state.originalRows.slice());
186 return;
187 }
188
189 const sortedRows = state.originalRows
190 .slice()
191 .sort((rowA, rowB) =>
192 compareRows(rowA, rowB, currentColumn, sortType, currentOrder)
193 );
194
195 renderRows(state, sortedRows);
196 }
197
198 function toggleSort(state: TableSortState, column: number) {
199 if (state.currentColumn !== column) {
200 state.currentColumn = column;
201 state.currentOrder = "asc";
202 applySort(state);
203 return;
204 }
205
206 if (state.currentOrder === "asc") {
207 state.currentOrder = "desc";
208 applySort(state);
209 return;
210 }
211
212 state.currentColumn = null;
213 state.currentOrder = null;
214 applySort(state);
215 }
216
217 function setupSortableTable(table: HTMLTableElement) {
218 if (table.dataset.tablebergSortingInitialized === "true") {
219 return;
220 }
221
222 const tbody = table.tBodies.item(0);
223 if (!tbody) {
224 return;
225 }
226
227 const headerEnabled = table.dataset.tablebergHeader === "true";
228 if (!headerEnabled || tbody.rows.length === 0) {
229 return;
230 }
231
232 const headerRow = tbody.rows[0];
233 const sortableHeaders = Array.from(
234 headerRow.querySelectorAll<HTMLTableCellElement>("th[data-sortable]")
235 );
236
237 if (sortableHeaders.length === 0) {
238 return;
239 }
240
241 const state: TableSortState = {
242 table,
243 tbody,
244 headerEnabled,
245 footerEnabled: table.dataset.tablebergFooter === "true",
246 sortableColumns: new Map<number, SortableType>(),
247 originalRows: [],
248 currentColumn: null,
249 currentOrder: null,
250 };
251
252 for (const headerCell of sortableHeaders) {
253 const sortTypeAttr = headerCell.getAttribute("data-sortable");
254 if (!isSortableType(sortTypeAttr)) {
255 continue;
256 }
257
258 const column = getColumnIndex(headerCell);
259 if (column === null) {
260 continue;
261 }
262
263 state.sortableColumns.set(column, sortTypeAttr);
264
265 headerCell.setAttribute("role", "button");
266 headerCell.setAttribute("tabindex", "0");
267 headerCell.setAttribute("aria-sort", "none");
268
269 const handleToggleSort = () => {
270 toggleSort(state, column);
271 };
272
273 headerCell.addEventListener("click", handleToggleSort);
274 headerCell.addEventListener("keydown", event => {
275 if (
276 event.key !== "Enter" &&
277 event.key !== " " &&
278 event.key !== "Spacebar"
279 ) {
280 return;
281 }
282
283 event.preventDefault();
284 handleToggleSort();
285 });
286 }
287
288 const { dataRows } = getTableRows(state);
289 state.originalRows = dataRows.slice();
290 updateSortIndicators(state);
291 table.dataset.tablebergSortingInitialized = "true";
292 }
293
294 export function initializeSorting() {
295 const tables = Array.from(
296 document.querySelectorAll<HTMLTableElement>(
297 ".wp-block-tableberg[data-tableberg-sortable='true']"
298 )
299 );
300
301 for (const table of tables) {
302 setupSortableTable(table);
303 }
304 }
305