@rowakit/table 0.2.2 → 0.4.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 +187 -15
- package/dist/index.cjs +675 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -5
- package/dist/index.d.ts +22 -5
- package/dist/index.js +676 -13
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
- package/src/styles/table.css +154 -1
package/dist/index.cjs
CHANGED
|
@@ -120,6 +120,158 @@ function getRowKey(row, rowKey) {
|
|
|
120
120
|
function getHeaderLabel(column) {
|
|
121
121
|
return column.header ?? column.id;
|
|
122
122
|
}
|
|
123
|
+
function validateViewName(name) {
|
|
124
|
+
const trimmed = name.trim();
|
|
125
|
+
if (trimmed.length === 0) {
|
|
126
|
+
return { valid: false, error: "Name cannot be empty" };
|
|
127
|
+
}
|
|
128
|
+
if (trimmed.length > 40) {
|
|
129
|
+
return { valid: false, error: "Name cannot exceed 40 characters" };
|
|
130
|
+
}
|
|
131
|
+
const invalidChars = /[/\\?%*:|"<>\x00-\x1f\x7f]/;
|
|
132
|
+
if (invalidChars.test(trimmed)) {
|
|
133
|
+
return { valid: false, error: "Name contains invalid characters" };
|
|
134
|
+
}
|
|
135
|
+
return { valid: true };
|
|
136
|
+
}
|
|
137
|
+
function getSavedViewsIndex() {
|
|
138
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const indexStr = localStorage.getItem("rowakit-views-index");
|
|
143
|
+
if (indexStr) {
|
|
144
|
+
const index = JSON.parse(indexStr);
|
|
145
|
+
if (Array.isArray(index)) {
|
|
146
|
+
return index;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
}
|
|
151
|
+
const rebuilt = [];
|
|
152
|
+
try {
|
|
153
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
154
|
+
const key = localStorage.key(i);
|
|
155
|
+
if (key?.startsWith("rowakit-view-")) {
|
|
156
|
+
const name = key.substring("rowakit-view-".length);
|
|
157
|
+
rebuilt.push({
|
|
158
|
+
name,
|
|
159
|
+
updatedAt: Date.now()
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
}
|
|
165
|
+
return rebuilt;
|
|
166
|
+
}
|
|
167
|
+
function setSavedViewsIndex(index) {
|
|
168
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
localStorage.setItem("rowakit-views-index", JSON.stringify(index));
|
|
173
|
+
} catch {
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function loadSavedViewsFromStorage() {
|
|
177
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
const index = getSavedViewsIndex();
|
|
181
|
+
const views = [];
|
|
182
|
+
for (const entry of index) {
|
|
183
|
+
try {
|
|
184
|
+
const viewStr = localStorage.getItem(`rowakit-view-${entry.name}`);
|
|
185
|
+
if (viewStr) {
|
|
186
|
+
const state = JSON.parse(viewStr);
|
|
187
|
+
views.push({
|
|
188
|
+
name: entry.name,
|
|
189
|
+
state
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return views;
|
|
196
|
+
}
|
|
197
|
+
function parseUrlState(params, defaultPageSize, pageSizeOptions) {
|
|
198
|
+
const pageStr = params.get("page");
|
|
199
|
+
let page = 1;
|
|
200
|
+
if (pageStr) {
|
|
201
|
+
const parsed = parseInt(pageStr, 10);
|
|
202
|
+
page = !isNaN(parsed) && parsed >= 1 ? parsed : 1;
|
|
203
|
+
}
|
|
204
|
+
const pageSizeStr = params.get("pageSize");
|
|
205
|
+
let pageSize = defaultPageSize;
|
|
206
|
+
if (pageSizeStr) {
|
|
207
|
+
const parsed = parseInt(pageSizeStr, 10);
|
|
208
|
+
if (!isNaN(parsed) && parsed >= 1) {
|
|
209
|
+
if (pageSizeOptions && pageSizeOptions.length > 0) {
|
|
210
|
+
pageSize = pageSizeOptions.includes(parsed) ? parsed : defaultPageSize;
|
|
211
|
+
} else {
|
|
212
|
+
pageSize = parsed;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const result = { page, pageSize };
|
|
217
|
+
const sortField = params.get("sortField");
|
|
218
|
+
const sortDir = params.get("sortDirection");
|
|
219
|
+
if (sortField && (sortDir === "asc" || sortDir === "desc")) {
|
|
220
|
+
result.sort = { field: sortField, direction: sortDir };
|
|
221
|
+
}
|
|
222
|
+
const filtersStr = params.get("filters");
|
|
223
|
+
if (filtersStr) {
|
|
224
|
+
try {
|
|
225
|
+
const parsed = JSON.parse(filtersStr);
|
|
226
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
227
|
+
result.filters = parsed;
|
|
228
|
+
}
|
|
229
|
+
} catch {
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const widthsStr = params.get("columnWidths");
|
|
233
|
+
if (widthsStr) {
|
|
234
|
+
try {
|
|
235
|
+
const parsed = JSON.parse(widthsStr);
|
|
236
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
237
|
+
const widths = {};
|
|
238
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
239
|
+
if (typeof value === "number" && value > 0) {
|
|
240
|
+
widths[key] = value;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (Object.keys(widths).length > 0) {
|
|
244
|
+
result.columnWidths = widths;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
function serializeUrlState(query, filters, columnWidths, defaultPageSize, enableColumnResizing) {
|
|
253
|
+
const params = new URLSearchParams();
|
|
254
|
+
params.set("page", String(query.page));
|
|
255
|
+
if (query.pageSize !== defaultPageSize) {
|
|
256
|
+
params.set("pageSize", String(query.pageSize));
|
|
257
|
+
}
|
|
258
|
+
if (query.sort) {
|
|
259
|
+
params.set("sortField", query.sort.field);
|
|
260
|
+
params.set("sortDirection", query.sort.direction);
|
|
261
|
+
}
|
|
262
|
+
if (filters && Object.keys(filters).length > 0) {
|
|
263
|
+
const nonEmptyFilters = Object.fromEntries(
|
|
264
|
+
Object.entries(filters).filter(([, v]) => v !== void 0)
|
|
265
|
+
);
|
|
266
|
+
if (Object.keys(nonEmptyFilters).length > 0) {
|
|
267
|
+
params.set("filters", JSON.stringify(nonEmptyFilters));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (enableColumnResizing && Object.keys(columnWidths).length > 0) {
|
|
271
|
+
params.set("columnWidths", JSON.stringify(columnWidths));
|
|
272
|
+
}
|
|
273
|
+
return params.toString();
|
|
274
|
+
}
|
|
123
275
|
function renderCell(column, row, isLoading, setConfirmState) {
|
|
124
276
|
switch (column.kind) {
|
|
125
277
|
case "text": {
|
|
@@ -169,7 +321,11 @@ function renderCell(column, row, isLoading, setConfirmState) {
|
|
|
169
321
|
return numValue.toLocaleString();
|
|
170
322
|
}
|
|
171
323
|
case "actions": {
|
|
172
|
-
|
|
324
|
+
const columnWithActions = column;
|
|
325
|
+
if (!Array.isArray(columnWithActions.actions)) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rowakit-table-actions", children: columnWithActions.actions.map((action) => {
|
|
173
329
|
const isDisabled = isLoading || action.disabled === true || typeof action.disabled === "function" && action.disabled(row);
|
|
174
330
|
const handleClick = () => {
|
|
175
331
|
if (isDisabled || action.loading) {
|
|
@@ -213,7 +369,10 @@ function RowaKitTable({
|
|
|
213
369
|
pageSizeOptions = [10, 20, 50],
|
|
214
370
|
rowKey,
|
|
215
371
|
className = "",
|
|
216
|
-
enableFilters = false
|
|
372
|
+
enableFilters = false,
|
|
373
|
+
enableColumnResizing = false,
|
|
374
|
+
syncToUrl = false,
|
|
375
|
+
enableSavedViews = false
|
|
217
376
|
}) {
|
|
218
377
|
const [dataState, setDataState] = react.useState({
|
|
219
378
|
state: "idle",
|
|
@@ -225,8 +384,100 @@ function RowaKitTable({
|
|
|
225
384
|
pageSize: defaultPageSize
|
|
226
385
|
});
|
|
227
386
|
const [filters, setFilters] = react.useState({});
|
|
387
|
+
const [columnWidths, setColumnWidths] = react.useState({});
|
|
388
|
+
const resizeRafRef = react.useRef(null);
|
|
389
|
+
const resizePendingRef = react.useRef(null);
|
|
390
|
+
const tableRef = react.useRef(null);
|
|
391
|
+
const isResizingRef = react.useRef(false);
|
|
392
|
+
const lastResizeEndTsRef = react.useRef(0);
|
|
393
|
+
const resizingColIdRef = react.useRef(null);
|
|
394
|
+
const didHydrateUrlRef = react.useRef(false);
|
|
395
|
+
const didSkipInitialUrlSyncRef = react.useRef(false);
|
|
396
|
+
const urlSyncDebounceRef = react.useRef(null);
|
|
397
|
+
const [savedViews, setSavedViews] = react.useState([]);
|
|
398
|
+
const [showSaveViewForm, setShowSaveViewForm] = react.useState(false);
|
|
399
|
+
const [saveViewInput, setSaveViewInput] = react.useState("");
|
|
400
|
+
const [saveViewError, setSaveViewError] = react.useState("");
|
|
401
|
+
const [overwriteConfirmName, setOverwriteConfirmName] = react.useState(null);
|
|
402
|
+
react.useEffect(() => {
|
|
403
|
+
if (!enableSavedViews) return;
|
|
404
|
+
const views = loadSavedViewsFromStorage();
|
|
405
|
+
setSavedViews(views);
|
|
406
|
+
}, [enableSavedViews]);
|
|
228
407
|
const [confirmState, setConfirmState] = react.useState(null);
|
|
229
408
|
const requestIdRef = react.useRef(0);
|
|
409
|
+
react.useEffect(() => {
|
|
410
|
+
if (!syncToUrl) {
|
|
411
|
+
didSkipInitialUrlSyncRef.current = false;
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (!didSkipInitialUrlSyncRef.current) {
|
|
415
|
+
didSkipInitialUrlSyncRef.current = true;
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (urlSyncDebounceRef.current) {
|
|
419
|
+
clearTimeout(urlSyncDebounceRef.current);
|
|
420
|
+
urlSyncDebounceRef.current = null;
|
|
421
|
+
}
|
|
422
|
+
const urlStr = serializeUrlState(query, filters, columnWidths, defaultPageSize, enableColumnResizing);
|
|
423
|
+
const qs = urlStr ? `?${urlStr}` : "";
|
|
424
|
+
window.history.replaceState(null, "", `${window.location.pathname}${qs}${window.location.hash}`);
|
|
425
|
+
}, [query, filters, syncToUrl, enableColumnResizing, defaultPageSize, columnWidths]);
|
|
426
|
+
react.useEffect(() => {
|
|
427
|
+
if (!syncToUrl || !enableColumnResizing) return;
|
|
428
|
+
if (!didSkipInitialUrlSyncRef.current) return;
|
|
429
|
+
if (urlSyncDebounceRef.current) {
|
|
430
|
+
clearTimeout(urlSyncDebounceRef.current);
|
|
431
|
+
}
|
|
432
|
+
urlSyncDebounceRef.current = setTimeout(() => {
|
|
433
|
+
const urlStr = serializeUrlState(query, filters, columnWidths, defaultPageSize, enableColumnResizing);
|
|
434
|
+
const qs = urlStr ? `?${urlStr}` : "";
|
|
435
|
+
window.history.replaceState(null, "", `${window.location.pathname}${qs}${window.location.hash}`);
|
|
436
|
+
urlSyncDebounceRef.current = null;
|
|
437
|
+
}, 150);
|
|
438
|
+
return () => {
|
|
439
|
+
if (urlSyncDebounceRef.current) {
|
|
440
|
+
clearTimeout(urlSyncDebounceRef.current);
|
|
441
|
+
urlSyncDebounceRef.current = null;
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
}, [columnWidths, syncToUrl, enableColumnResizing, query, filters, defaultPageSize]);
|
|
445
|
+
react.useEffect(() => {
|
|
446
|
+
if (!syncToUrl) {
|
|
447
|
+
didHydrateUrlRef.current = false;
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (didHydrateUrlRef.current) return;
|
|
451
|
+
didHydrateUrlRef.current = true;
|
|
452
|
+
const params = new URLSearchParams(window.location.search);
|
|
453
|
+
const parsed = parseUrlState(params, defaultPageSize, pageSizeOptions);
|
|
454
|
+
setQuery({
|
|
455
|
+
page: parsed.page,
|
|
456
|
+
pageSize: parsed.pageSize,
|
|
457
|
+
sort: parsed.sort,
|
|
458
|
+
filters: parsed.filters
|
|
459
|
+
});
|
|
460
|
+
if (parsed.filters) {
|
|
461
|
+
setFilters(parsed.filters);
|
|
462
|
+
}
|
|
463
|
+
if (parsed.columnWidths && enableColumnResizing) {
|
|
464
|
+
const clamped = {};
|
|
465
|
+
for (const [colId, rawWidth] of Object.entries(parsed.columnWidths)) {
|
|
466
|
+
const widthNum = typeof rawWidth === "number" ? rawWidth : Number(rawWidth);
|
|
467
|
+
if (!Number.isFinite(widthNum)) continue;
|
|
468
|
+
const colDef = columns.find((c) => c.id === colId);
|
|
469
|
+
if (!colDef) continue;
|
|
470
|
+
const minW = colDef.minWidth ?? 80;
|
|
471
|
+
const maxW = colDef.maxWidth;
|
|
472
|
+
let finalW = Math.max(minW, widthNum);
|
|
473
|
+
if (maxW != null) {
|
|
474
|
+
finalW = Math.min(finalW, maxW);
|
|
475
|
+
}
|
|
476
|
+
clamped[colId] = finalW;
|
|
477
|
+
}
|
|
478
|
+
setColumnWidths(clamped);
|
|
479
|
+
}
|
|
480
|
+
}, [syncToUrl, defaultPageSize, enableColumnResizing, pageSizeOptions, columns]);
|
|
230
481
|
react.useEffect(() => {
|
|
231
482
|
if (!enableFilters) return;
|
|
232
483
|
const activeFilters = {};
|
|
@@ -312,10 +563,203 @@ function RowaKitTable({
|
|
|
312
563
|
}
|
|
313
564
|
return query.sort.direction === "asc" ? " \u2191" : " \u2193";
|
|
314
565
|
};
|
|
566
|
+
const scheduleColumnWidthUpdate = (colId, width) => {
|
|
567
|
+
resizePendingRef.current = { colId, width };
|
|
568
|
+
if (resizeRafRef.current != null) return;
|
|
569
|
+
resizeRafRef.current = requestAnimationFrame(() => {
|
|
570
|
+
resizeRafRef.current = null;
|
|
571
|
+
const pending = resizePendingRef.current;
|
|
572
|
+
if (!pending) return;
|
|
573
|
+
handleColumnResize(pending.colId, pending.width);
|
|
574
|
+
});
|
|
575
|
+
};
|
|
576
|
+
const handleColumnResize = (columnId, newWidth) => {
|
|
577
|
+
const minWidth = columns.find((c) => c.id === columnId)?.minWidth ?? 80;
|
|
578
|
+
const maxWidth = columns.find((c) => c.id === columnId)?.maxWidth;
|
|
579
|
+
let finalWidth = Math.max(minWidth, newWidth);
|
|
580
|
+
if (maxWidth) {
|
|
581
|
+
finalWidth = Math.min(finalWidth, maxWidth);
|
|
582
|
+
}
|
|
583
|
+
if (columnWidths[columnId] === finalWidth) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
setColumnWidths((prev) => ({
|
|
587
|
+
...prev,
|
|
588
|
+
[columnId]: finalWidth
|
|
589
|
+
}));
|
|
590
|
+
};
|
|
591
|
+
const autoFitColumnWidth = (columnId) => {
|
|
592
|
+
const tableEl = tableRef.current;
|
|
593
|
+
if (!tableEl) return;
|
|
594
|
+
const th = tableEl.querySelector(`th[data-col-id="${columnId}"]`);
|
|
595
|
+
if (!th) return;
|
|
596
|
+
const tds = Array.from(
|
|
597
|
+
tableEl.querySelectorAll(`td[data-col-id="${columnId}"]`)
|
|
598
|
+
);
|
|
599
|
+
const headerW = th.scrollWidth;
|
|
600
|
+
const cellsMaxW = tds.reduce((max, td) => Math.max(max, td.scrollWidth), 0);
|
|
601
|
+
const padding = 24;
|
|
602
|
+
const raw = Math.max(headerW, cellsMaxW) + padding;
|
|
603
|
+
const colDef = columns.find((c) => c.id === columnId);
|
|
604
|
+
const minW = colDef?.minWidth ?? 80;
|
|
605
|
+
const maxW = colDef?.maxWidth ?? 600;
|
|
606
|
+
const finalW = Math.max(minW, Math.min(raw, maxW));
|
|
607
|
+
setColumnWidths((prev) => ({ ...prev, [columnId]: finalW }));
|
|
608
|
+
};
|
|
609
|
+
const startColumnResize = (e, columnId) => {
|
|
610
|
+
e.preventDefault();
|
|
611
|
+
e.stopPropagation();
|
|
612
|
+
if (e.detail === 2) {
|
|
613
|
+
autoFitColumnWidth(columnId);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (e.pointerType === "mouse" && e.buttons !== 1) {
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const target = e.currentTarget;
|
|
620
|
+
const pointerId = e.pointerId;
|
|
621
|
+
try {
|
|
622
|
+
target.setPointerCapture(pointerId);
|
|
623
|
+
} catch {
|
|
624
|
+
}
|
|
625
|
+
isResizingRef.current = true;
|
|
626
|
+
resizingColIdRef.current = columnId;
|
|
627
|
+
const startX = e.clientX;
|
|
628
|
+
const th = target.parentElement;
|
|
629
|
+
let startWidth = columnWidths[columnId] ?? th.offsetWidth;
|
|
630
|
+
const MIN_DRAG_WIDTH = 80;
|
|
631
|
+
if (startWidth < MIN_DRAG_WIDTH) {
|
|
632
|
+
const nextTh = th.nextElementSibling;
|
|
633
|
+
if (nextTh && nextTh.offsetWidth >= 50) {
|
|
634
|
+
startWidth = nextTh.offsetWidth;
|
|
635
|
+
} else {
|
|
636
|
+
startWidth = 100;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
document.body.classList.add("rowakit-resizing");
|
|
640
|
+
const handlePointerMove = (moveEvent) => {
|
|
641
|
+
const delta = moveEvent.clientX - startX;
|
|
642
|
+
const newWidth = startWidth + delta;
|
|
643
|
+
scheduleColumnWidthUpdate(columnId, newWidth);
|
|
644
|
+
};
|
|
645
|
+
const cleanupResize = () => {
|
|
646
|
+
target.removeEventListener("pointermove", handlePointerMove);
|
|
647
|
+
target.removeEventListener("pointerup", handlePointerUp);
|
|
648
|
+
target.removeEventListener("pointercancel", handlePointerCancel);
|
|
649
|
+
document.body.classList.remove("rowakit-resizing");
|
|
650
|
+
isResizingRef.current = false;
|
|
651
|
+
resizingColIdRef.current = null;
|
|
652
|
+
lastResizeEndTsRef.current = Date.now();
|
|
653
|
+
try {
|
|
654
|
+
target.releasePointerCapture(pointerId);
|
|
655
|
+
} catch {
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
const handlePointerUp = () => {
|
|
659
|
+
cleanupResize();
|
|
660
|
+
};
|
|
661
|
+
const handlePointerCancel = () => {
|
|
662
|
+
cleanupResize();
|
|
663
|
+
};
|
|
664
|
+
target.addEventListener("pointermove", handlePointerMove);
|
|
665
|
+
target.addEventListener("pointerup", handlePointerUp);
|
|
666
|
+
target.addEventListener("pointercancel", handlePointerCancel);
|
|
667
|
+
};
|
|
668
|
+
const handleColumnResizeDoubleClick = (e, columnId) => {
|
|
669
|
+
e.preventDefault();
|
|
670
|
+
e.stopPropagation();
|
|
671
|
+
autoFitColumnWidth(columnId);
|
|
672
|
+
};
|
|
673
|
+
const saveCurrentView = (name) => {
|
|
674
|
+
const viewState = {
|
|
675
|
+
page: query.page,
|
|
676
|
+
pageSize: query.pageSize,
|
|
677
|
+
sort: query.sort,
|
|
678
|
+
filters: query.filters,
|
|
679
|
+
columnWidths: enableColumnResizing ? columnWidths : void 0
|
|
680
|
+
};
|
|
681
|
+
setSavedViews((prev) => {
|
|
682
|
+
const filtered = prev.filter((v) => v.name !== name);
|
|
683
|
+
return [...filtered, { name, state: viewState }];
|
|
684
|
+
});
|
|
685
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
686
|
+
try {
|
|
687
|
+
localStorage.setItem(`rowakit-view-${name}`, JSON.stringify(viewState));
|
|
688
|
+
const index = getSavedViewsIndex();
|
|
689
|
+
const filtered = index.filter((v) => v.name !== name);
|
|
690
|
+
filtered.push({ name, updatedAt: Date.now() });
|
|
691
|
+
setSavedViewsIndex(filtered);
|
|
692
|
+
} catch {
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
const loadSavedView = (name) => {
|
|
697
|
+
const view = savedViews.find((v) => v.name === name);
|
|
698
|
+
if (!view) return;
|
|
699
|
+
const { state } = view;
|
|
700
|
+
setQuery({
|
|
701
|
+
page: state.page,
|
|
702
|
+
pageSize: state.pageSize,
|
|
703
|
+
sort: state.sort,
|
|
704
|
+
filters: state.filters
|
|
705
|
+
});
|
|
706
|
+
setFilters(state.filters ?? {});
|
|
707
|
+
if (state.columnWidths && enableColumnResizing) {
|
|
708
|
+
setColumnWidths(state.columnWidths);
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
const deleteSavedView = (name) => {
|
|
712
|
+
setSavedViews((prev) => prev.filter((v) => v.name !== name));
|
|
713
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
714
|
+
try {
|
|
715
|
+
localStorage.removeItem(`rowakit-view-${name}`);
|
|
716
|
+
const index = getSavedViewsIndex();
|
|
717
|
+
const filtered = index.filter((v) => v.name !== name);
|
|
718
|
+
setSavedViewsIndex(filtered);
|
|
719
|
+
} catch {
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
const resetTableState = () => {
|
|
724
|
+
setQuery({
|
|
725
|
+
page: 1,
|
|
726
|
+
pageSize: defaultPageSize
|
|
727
|
+
});
|
|
728
|
+
setFilters({});
|
|
729
|
+
setColumnWidths({});
|
|
730
|
+
};
|
|
731
|
+
const transformFilterValueForColumn = (column, value) => {
|
|
732
|
+
if (!value || column?.kind !== "number") {
|
|
733
|
+
return value;
|
|
734
|
+
}
|
|
735
|
+
const numberColumn = column;
|
|
736
|
+
if (!numberColumn.filterTransform) {
|
|
737
|
+
return value;
|
|
738
|
+
}
|
|
739
|
+
if (value.op === "equals" && typeof value.value === "number") {
|
|
740
|
+
return {
|
|
741
|
+
...value,
|
|
742
|
+
value: numberColumn.filterTransform(value.value)
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
if (value.op === "range" && typeof value.value === "object") {
|
|
746
|
+
const { from, to } = value.value;
|
|
747
|
+
return {
|
|
748
|
+
op: "range",
|
|
749
|
+
value: {
|
|
750
|
+
from: from !== void 0 && typeof from === "number" ? numberColumn.filterTransform(from) : from,
|
|
751
|
+
to: to !== void 0 && typeof to === "number" ? numberColumn.filterTransform(to) : to
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
return value;
|
|
756
|
+
};
|
|
315
757
|
const handleFilterChange = (field, value) => {
|
|
758
|
+
const column = columns.find((c) => c.id === field);
|
|
759
|
+
const transformedValue = transformFilterValueForColumn(column, value);
|
|
316
760
|
setFilters((prev) => ({
|
|
317
761
|
...prev,
|
|
318
|
-
[field]:
|
|
762
|
+
[field]: transformedValue
|
|
319
763
|
}));
|
|
320
764
|
};
|
|
321
765
|
const handleClearFilter = (field) => {
|
|
@@ -335,7 +779,158 @@ function RowaKitTable({
|
|
|
335
779
|
const canGoPrevious = query.page > 1 && !isLoading;
|
|
336
780
|
const canGoNext = query.page < totalPages && !isLoading;
|
|
337
781
|
const hasActiveFilters = enableFilters && Object.values(filters).some((v) => v !== void 0);
|
|
338
|
-
|
|
782
|
+
const containerClass = [
|
|
783
|
+
"rowakit-table",
|
|
784
|
+
enableColumnResizing ? "rowakit-layout-fixed" : "",
|
|
785
|
+
className
|
|
786
|
+
].filter(Boolean).join(" ");
|
|
787
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: containerClass, children: [
|
|
788
|
+
enableSavedViews && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rowakit-saved-views-group", children: [
|
|
789
|
+
!showSaveViewForm ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
790
|
+
"button",
|
|
791
|
+
{
|
|
792
|
+
onClick: () => {
|
|
793
|
+
setShowSaveViewForm(true);
|
|
794
|
+
setSaveViewInput("");
|
|
795
|
+
setSaveViewError("");
|
|
796
|
+
setOverwriteConfirmName(null);
|
|
797
|
+
},
|
|
798
|
+
className: "rowakit-saved-view-button",
|
|
799
|
+
type: "button",
|
|
800
|
+
children: "Save View"
|
|
801
|
+
}
|
|
802
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rowakit-save-view-form", children: overwriteConfirmName ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rowakit-save-view-confirm", children: [
|
|
803
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
|
|
804
|
+
'View "',
|
|
805
|
+
overwriteConfirmName,
|
|
806
|
+
'" already exists. Overwrite?'
|
|
807
|
+
] }),
|
|
808
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
809
|
+
"button",
|
|
810
|
+
{
|
|
811
|
+
onClick: () => {
|
|
812
|
+
saveCurrentView(overwriteConfirmName);
|
|
813
|
+
setShowSaveViewForm(false);
|
|
814
|
+
setSaveViewInput("");
|
|
815
|
+
setSaveViewError("");
|
|
816
|
+
setOverwriteConfirmName(null);
|
|
817
|
+
},
|
|
818
|
+
className: "rowakit-saved-view-button",
|
|
819
|
+
type: "button",
|
|
820
|
+
children: "Overwrite"
|
|
821
|
+
}
|
|
822
|
+
),
|
|
823
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
824
|
+
"button",
|
|
825
|
+
{
|
|
826
|
+
onClick: () => {
|
|
827
|
+
setOverwriteConfirmName(null);
|
|
828
|
+
},
|
|
829
|
+
className: "rowakit-saved-view-button",
|
|
830
|
+
type: "button",
|
|
831
|
+
children: "Cancel"
|
|
832
|
+
}
|
|
833
|
+
)
|
|
834
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
835
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
836
|
+
"input",
|
|
837
|
+
{
|
|
838
|
+
type: "text",
|
|
839
|
+
value: saveViewInput,
|
|
840
|
+
onChange: (e) => {
|
|
841
|
+
setSaveViewInput(e.target.value);
|
|
842
|
+
setSaveViewError("");
|
|
843
|
+
},
|
|
844
|
+
onKeyDown: (e) => {
|
|
845
|
+
if (e.key === "Enter") {
|
|
846
|
+
const validation = validateViewName(saveViewInput);
|
|
847
|
+
if (!validation.valid) {
|
|
848
|
+
setSaveViewError(validation.error || "Invalid name");
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
if (savedViews.some((v) => v.name === saveViewInput.trim())) {
|
|
852
|
+
setOverwriteConfirmName(saveViewInput.trim());
|
|
853
|
+
} else {
|
|
854
|
+
saveCurrentView(saveViewInput.trim());
|
|
855
|
+
setShowSaveViewForm(false);
|
|
856
|
+
setSaveViewInput("");
|
|
857
|
+
setSaveViewError("");
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
},
|
|
861
|
+
placeholder: "Enter view name...",
|
|
862
|
+
className: "rowakit-save-view-input"
|
|
863
|
+
}
|
|
864
|
+
),
|
|
865
|
+
saveViewError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rowakit-save-view-error", children: saveViewError }),
|
|
866
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
867
|
+
"button",
|
|
868
|
+
{
|
|
869
|
+
onClick: () => {
|
|
870
|
+
const validation = validateViewName(saveViewInput);
|
|
871
|
+
if (!validation.valid) {
|
|
872
|
+
setSaveViewError(validation.error || "Invalid name");
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
if (savedViews.some((v) => v.name === saveViewInput.trim())) {
|
|
876
|
+
setOverwriteConfirmName(saveViewInput.trim());
|
|
877
|
+
} else {
|
|
878
|
+
saveCurrentView(saveViewInput.trim());
|
|
879
|
+
setShowSaveViewForm(false);
|
|
880
|
+
setSaveViewInput("");
|
|
881
|
+
setSaveViewError("");
|
|
882
|
+
}
|
|
883
|
+
},
|
|
884
|
+
className: "rowakit-saved-view-button",
|
|
885
|
+
type: "button",
|
|
886
|
+
children: "Save"
|
|
887
|
+
}
|
|
888
|
+
),
|
|
889
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
890
|
+
"button",
|
|
891
|
+
{
|
|
892
|
+
onClick: () => {
|
|
893
|
+
setShowSaveViewForm(false);
|
|
894
|
+
setSaveViewInput("");
|
|
895
|
+
setSaveViewError("");
|
|
896
|
+
},
|
|
897
|
+
className: "rowakit-saved-view-button",
|
|
898
|
+
type: "button",
|
|
899
|
+
children: "Cancel"
|
|
900
|
+
}
|
|
901
|
+
)
|
|
902
|
+
] }) }),
|
|
903
|
+
savedViews.map((view) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rowakit-saved-view-item", children: [
|
|
904
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
905
|
+
"button",
|
|
906
|
+
{
|
|
907
|
+
onClick: () => loadSavedView(view.name),
|
|
908
|
+
className: "rowakit-saved-view-button",
|
|
909
|
+
type: "button",
|
|
910
|
+
children: view.name
|
|
911
|
+
}
|
|
912
|
+
),
|
|
913
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
914
|
+
"button",
|
|
915
|
+
{
|
|
916
|
+
onClick: () => deleteSavedView(view.name),
|
|
917
|
+
className: "rowakit-saved-view-button rowakit-saved-view-button-delete",
|
|
918
|
+
type: "button",
|
|
919
|
+
title: "Delete this view",
|
|
920
|
+
children: "\xD7"
|
|
921
|
+
}
|
|
922
|
+
)
|
|
923
|
+
] }, view.name)),
|
|
924
|
+
(hasActiveFilters || query.page > 1 || query.sort) && /* @__PURE__ */ jsxRuntime.jsx(
|
|
925
|
+
"button",
|
|
926
|
+
{
|
|
927
|
+
onClick: resetTableState,
|
|
928
|
+
className: "rowakit-saved-view-button",
|
|
929
|
+
type: "button",
|
|
930
|
+
children: "Reset"
|
|
931
|
+
}
|
|
932
|
+
)
|
|
933
|
+
] }),
|
|
339
934
|
hasActiveFilters && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rowakit-table-filter-controls", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
340
935
|
"button",
|
|
341
936
|
{
|
|
@@ -345,15 +940,22 @@ function RowaKitTable({
|
|
|
345
940
|
children: "Clear all filters"
|
|
346
941
|
}
|
|
347
942
|
) }),
|
|
348
|
-
/* @__PURE__ */ jsxRuntime.jsxs("table", { children: [
|
|
943
|
+
/* @__PURE__ */ jsxRuntime.jsxs("table", { ref: tableRef, children: [
|
|
349
944
|
/* @__PURE__ */ jsxRuntime.jsxs("thead", { children: [
|
|
350
945
|
/* @__PURE__ */ jsxRuntime.jsx("tr", { children: columns.map((column) => {
|
|
351
946
|
const isSortable = column.kind !== "actions" && (column.kind === "custom" ? false : column.sortable === true);
|
|
352
947
|
const field = column.kind === "actions" ? "" : column.kind === "custom" ? column.field : column.field;
|
|
948
|
+
const isResizable = enableColumnResizing && column.kind !== "actions";
|
|
949
|
+
const actualWidth = columnWidths[column.id] ?? column.width;
|
|
353
950
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
354
951
|
"th",
|
|
355
952
|
{
|
|
356
|
-
|
|
953
|
+
"data-col-id": column.id,
|
|
954
|
+
onClick: isSortable ? () => {
|
|
955
|
+
if (isResizingRef.current) return;
|
|
956
|
+
if (Date.now() - lastResizeEndTsRef.current < 150) return;
|
|
957
|
+
handleSort(String(field));
|
|
958
|
+
} : void 0,
|
|
357
959
|
role: isSortable ? "button" : void 0,
|
|
358
960
|
tabIndex: isSortable ? 0 : void 0,
|
|
359
961
|
onKeyDown: isSortable ? (e) => {
|
|
@@ -364,13 +966,27 @@ function RowaKitTable({
|
|
|
364
966
|
} : void 0,
|
|
365
967
|
"aria-sort": isSortable && query.sort?.field === String(field) ? query.sort.direction === "asc" ? "ascending" : "descending" : void 0,
|
|
366
968
|
style: {
|
|
367
|
-
width:
|
|
368
|
-
textAlign: column.align
|
|
969
|
+
width: actualWidth != null ? `${actualWidth}px` : void 0,
|
|
970
|
+
textAlign: column.align,
|
|
971
|
+
position: isResizable ? "relative" : void 0
|
|
369
972
|
},
|
|
370
|
-
className:
|
|
973
|
+
className: [
|
|
974
|
+
column.truncate ? "rowakit-cell-truncate" : "",
|
|
975
|
+
resizingColIdRef.current === column.id ? "resizing" : ""
|
|
976
|
+
// PRD-01
|
|
977
|
+
].filter(Boolean).join(" ") || void 0,
|
|
371
978
|
children: [
|
|
372
979
|
getHeaderLabel(column),
|
|
373
|
-
isSortable && getSortIndicator(String(field))
|
|
980
|
+
isSortable && getSortIndicator(String(field)),
|
|
981
|
+
isResizable && /* @__PURE__ */ jsxRuntime.jsx(
|
|
982
|
+
"div",
|
|
983
|
+
{
|
|
984
|
+
className: "rowakit-column-resize-handle",
|
|
985
|
+
onPointerDown: (e) => startColumnResize(e, column.id),
|
|
986
|
+
onDoubleClick: (e) => handleColumnResizeDoubleClick(e, column.id),
|
|
987
|
+
title: "Drag to resize | Double-click to auto-fit content"
|
|
988
|
+
}
|
|
989
|
+
)
|
|
374
990
|
]
|
|
375
991
|
},
|
|
376
992
|
column.id
|
|
@@ -470,6 +1086,51 @@ function RowaKitTable({
|
|
|
470
1086
|
] }) }, column.id);
|
|
471
1087
|
}
|
|
472
1088
|
const isNumberColumn = column.kind === "number";
|
|
1089
|
+
if (isNumberColumn) {
|
|
1090
|
+
const fromValue = filterValue?.op === "range" ? String(filterValue.value.from ?? "") : filterValue?.op === "equals" && typeof filterValue.value === "number" ? String(filterValue.value) : "";
|
|
1091
|
+
const toValue = filterValue?.op === "range" ? String(filterValue.value.to ?? "") : "";
|
|
1092
|
+
const showRangeUI = !filterValue || filterValue.op === "range";
|
|
1093
|
+
if (showRangeUI) {
|
|
1094
|
+
return /* @__PURE__ */ jsxRuntime.jsx("th", { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rowakit-filter-number-range", children: [
|
|
1095
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1096
|
+
"input",
|
|
1097
|
+
{
|
|
1098
|
+
type: "number",
|
|
1099
|
+
className: "rowakit-filter-input",
|
|
1100
|
+
placeholder: "Min",
|
|
1101
|
+
value: fromValue,
|
|
1102
|
+
onChange: (e) => {
|
|
1103
|
+
const from = e.target.value ? Number(e.target.value) : void 0;
|
|
1104
|
+
const to = toValue ? Number(toValue) : void 0;
|
|
1105
|
+
if (from === void 0 && to === void 0) {
|
|
1106
|
+
handleClearFilter(field);
|
|
1107
|
+
} else {
|
|
1108
|
+
handleFilterChange(field, { op: "range", value: { from, to } });
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
),
|
|
1113
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1114
|
+
"input",
|
|
1115
|
+
{
|
|
1116
|
+
type: "number",
|
|
1117
|
+
className: "rowakit-filter-input",
|
|
1118
|
+
placeholder: "Max",
|
|
1119
|
+
value: toValue,
|
|
1120
|
+
onChange: (e) => {
|
|
1121
|
+
const to = e.target.value ? Number(e.target.value) : void 0;
|
|
1122
|
+
const from = fromValue ? Number(fromValue) : void 0;
|
|
1123
|
+
if (from === void 0 && to === void 0) {
|
|
1124
|
+
handleClearFilter(field);
|
|
1125
|
+
} else {
|
|
1126
|
+
handleFilterChange(field, { op: "range", value: { from, to } });
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
)
|
|
1131
|
+
] }) }, column.id);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
473
1134
|
return /* @__PURE__ */ jsxRuntime.jsx("th", { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
474
1135
|
"input",
|
|
475
1136
|
{
|
|
@@ -521,12 +1182,14 @@ function RowaKitTable({
|
|
|
521
1182
|
column.kind === "number" ? "rowakit-cell-number" : "",
|
|
522
1183
|
column.truncate ? "rowakit-cell-truncate" : ""
|
|
523
1184
|
].filter(Boolean).join(" ") || void 0;
|
|
1185
|
+
const actualWidth = columnWidths[column.id] ?? column.width;
|
|
524
1186
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
525
1187
|
"td",
|
|
526
1188
|
{
|
|
1189
|
+
"data-col-id": column.id,
|
|
527
1190
|
className: cellClass,
|
|
528
1191
|
style: {
|
|
529
|
-
width:
|
|
1192
|
+
width: actualWidth != null ? `${actualWidth}px` : void 0,
|
|
530
1193
|
textAlign: column.align || (column.kind === "number" ? "right" : void 0)
|
|
531
1194
|
},
|
|
532
1195
|
children: renderCell(column, row, isLoading, setConfirmState)
|
|
@@ -631,7 +1294,7 @@ function RowaKitTable({
|
|
|
631
1294
|
var SmartTable = RowaKitTable;
|
|
632
1295
|
|
|
633
1296
|
// src/index.ts
|
|
634
|
-
var VERSION = "0.
|
|
1297
|
+
var VERSION = "0.4.0";
|
|
635
1298
|
|
|
636
1299
|
exports.RowaKitTable = RowaKitTable;
|
|
637
1300
|
exports.SmartTable = SmartTable;
|