@rowakit/table 0.2.2 → 0.3.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 +183 -14
- package/dist/index.cjs +322 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -4
- package/dist/index.d.ts +21 -4
- package/dist/index.js +322 -9
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
- package/src/styles/table.css +98 -0
package/dist/index.js
CHANGED
|
@@ -167,7 +167,11 @@ function renderCell(column, row, isLoading, setConfirmState) {
|
|
|
167
167
|
return numValue.toLocaleString();
|
|
168
168
|
}
|
|
169
169
|
case "actions": {
|
|
170
|
-
|
|
170
|
+
const columnWithActions = column;
|
|
171
|
+
if (!Array.isArray(columnWithActions.actions)) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
return /* @__PURE__ */ jsx("div", { className: "rowakit-table-actions", children: columnWithActions.actions.map((action) => {
|
|
171
175
|
const isDisabled = isLoading || action.disabled === true || typeof action.disabled === "function" && action.disabled(row);
|
|
172
176
|
const handleClick = () => {
|
|
173
177
|
if (isDisabled || action.loading) {
|
|
@@ -211,7 +215,10 @@ function RowaKitTable({
|
|
|
211
215
|
pageSizeOptions = [10, 20, 50],
|
|
212
216
|
rowKey,
|
|
213
217
|
className = "",
|
|
214
|
-
enableFilters = false
|
|
218
|
+
enableFilters = false,
|
|
219
|
+
enableColumnResizing = false,
|
|
220
|
+
syncToUrl = false,
|
|
221
|
+
enableSavedViews = false
|
|
215
222
|
}) {
|
|
216
223
|
const [dataState, setDataState] = useState({
|
|
217
224
|
state: "idle",
|
|
@@ -223,8 +230,64 @@ function RowaKitTable({
|
|
|
223
230
|
pageSize: defaultPageSize
|
|
224
231
|
});
|
|
225
232
|
const [filters, setFilters] = useState({});
|
|
233
|
+
const [columnWidths, setColumnWidths] = useState({});
|
|
234
|
+
const resizeRafRef = useRef(null);
|
|
235
|
+
const resizePendingRef = useRef(null);
|
|
236
|
+
const tableRef = useRef(null);
|
|
237
|
+
const [savedViews, setSavedViews] = useState([]);
|
|
226
238
|
const [confirmState, setConfirmState] = useState(null);
|
|
227
239
|
const requestIdRef = useRef(0);
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
if (!syncToUrl) return;
|
|
242
|
+
const params = new URLSearchParams();
|
|
243
|
+
params.set("page", String(query.page));
|
|
244
|
+
params.set("pageSize", String(query.pageSize));
|
|
245
|
+
if (query.sort) {
|
|
246
|
+
params.set("sortField", query.sort.field);
|
|
247
|
+
params.set("sortDirection", query.sort.direction);
|
|
248
|
+
}
|
|
249
|
+
if (query.filters && Object.keys(query.filters).length > 0) {
|
|
250
|
+
params.set("filters", JSON.stringify(query.filters));
|
|
251
|
+
}
|
|
252
|
+
if (enableColumnResizing && Object.keys(columnWidths).length > 0) {
|
|
253
|
+
params.set("columnWidths", JSON.stringify(columnWidths));
|
|
254
|
+
}
|
|
255
|
+
window.history.replaceState(null, "", `?${params.toString()}`);
|
|
256
|
+
}, [query, columnWidths, syncToUrl, enableColumnResizing]);
|
|
257
|
+
useEffect(() => {
|
|
258
|
+
if (!syncToUrl) return;
|
|
259
|
+
const params = new URLSearchParams(window.location.search);
|
|
260
|
+
const page = parseInt(params.get("page") ?? "1", 10);
|
|
261
|
+
const pageSize = parseInt(params.get("pageSize") ?? String(defaultPageSize), 10);
|
|
262
|
+
const sortField = params.get("sortField");
|
|
263
|
+
const sortDirection = params.get("sortDirection");
|
|
264
|
+
const filtersStr = params.get("filters");
|
|
265
|
+
const columnWidthsStr = params.get("columnWidths");
|
|
266
|
+
const newQuery = {
|
|
267
|
+
page: Math.max(1, page),
|
|
268
|
+
pageSize: Math.max(1, pageSize)
|
|
269
|
+
};
|
|
270
|
+
if (sortField && sortDirection) {
|
|
271
|
+
newQuery.sort = { field: sortField, direction: sortDirection };
|
|
272
|
+
}
|
|
273
|
+
if (filtersStr) {
|
|
274
|
+
try {
|
|
275
|
+
const parsedFilters = JSON.parse(filtersStr);
|
|
276
|
+
if (parsedFilters && typeof parsedFilters === "object") {
|
|
277
|
+
setFilters(parsedFilters);
|
|
278
|
+
newQuery.filters = parsedFilters;
|
|
279
|
+
}
|
|
280
|
+
} catch {
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (enableColumnResizing && columnWidthsStr) {
|
|
284
|
+
try {
|
|
285
|
+
setColumnWidths(JSON.parse(columnWidthsStr));
|
|
286
|
+
} catch {
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
setQuery(newQuery);
|
|
290
|
+
}, [syncToUrl, defaultPageSize, enableColumnResizing]);
|
|
228
291
|
useEffect(() => {
|
|
229
292
|
if (!enableFilters) return;
|
|
230
293
|
const activeFilters = {};
|
|
@@ -310,10 +373,154 @@ function RowaKitTable({
|
|
|
310
373
|
}
|
|
311
374
|
return query.sort.direction === "asc" ? " \u2191" : " \u2193";
|
|
312
375
|
};
|
|
376
|
+
const scheduleColumnWidthUpdate = (colId, width) => {
|
|
377
|
+
resizePendingRef.current = { colId, width };
|
|
378
|
+
if (resizeRafRef.current != null) return;
|
|
379
|
+
resizeRafRef.current = requestAnimationFrame(() => {
|
|
380
|
+
resizeRafRef.current = null;
|
|
381
|
+
const pending = resizePendingRef.current;
|
|
382
|
+
if (!pending) return;
|
|
383
|
+
handleColumnResize(pending.colId, pending.width);
|
|
384
|
+
});
|
|
385
|
+
};
|
|
386
|
+
const handleColumnResize = (columnId, newWidth) => {
|
|
387
|
+
const minWidth = columns.find((c) => c.id === columnId)?.minWidth ?? 80;
|
|
388
|
+
const maxWidth = columns.find((c) => c.id === columnId)?.maxWidth;
|
|
389
|
+
let finalWidth = Math.max(minWidth, newWidth);
|
|
390
|
+
if (maxWidth) {
|
|
391
|
+
finalWidth = Math.min(finalWidth, maxWidth);
|
|
392
|
+
}
|
|
393
|
+
setColumnWidths((prev) => ({
|
|
394
|
+
...prev,
|
|
395
|
+
[columnId]: finalWidth
|
|
396
|
+
}));
|
|
397
|
+
};
|
|
398
|
+
const startColumnResize = (e, columnId) => {
|
|
399
|
+
e.preventDefault();
|
|
400
|
+
const startX = e.clientX;
|
|
401
|
+
const th = e.currentTarget.parentElement;
|
|
402
|
+
let startWidth = columnWidths[columnId] ?? th.offsetWidth;
|
|
403
|
+
const MIN_DRAG_WIDTH = 80;
|
|
404
|
+
if (startWidth < MIN_DRAG_WIDTH) {
|
|
405
|
+
const nextTh = th.nextElementSibling;
|
|
406
|
+
if (nextTh && nextTh.offsetWidth >= 50) {
|
|
407
|
+
startWidth = nextTh.offsetWidth;
|
|
408
|
+
} else {
|
|
409
|
+
startWidth = 100;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
document.body.classList.add("rowakit-resizing");
|
|
413
|
+
const handleMouseMove = (moveEvent) => {
|
|
414
|
+
const delta = moveEvent.clientX - startX;
|
|
415
|
+
const newWidth = startWidth + delta;
|
|
416
|
+
scheduleColumnWidthUpdate(columnId, newWidth);
|
|
417
|
+
};
|
|
418
|
+
const handleMouseUp = () => {
|
|
419
|
+
document.removeEventListener("mousemove", handleMouseMove);
|
|
420
|
+
document.removeEventListener("mouseup", handleMouseUp);
|
|
421
|
+
document.body.classList.remove("rowakit-resizing");
|
|
422
|
+
};
|
|
423
|
+
document.addEventListener("mousemove", handleMouseMove);
|
|
424
|
+
document.addEventListener("mouseup", handleMouseUp);
|
|
425
|
+
};
|
|
426
|
+
const handleColumnResizeDoubleClick = (columnId) => {
|
|
427
|
+
const tableEl = tableRef.current;
|
|
428
|
+
if (!tableEl) return;
|
|
429
|
+
const th = tableEl.querySelector(`th[data-col-id="${columnId}"]`);
|
|
430
|
+
if (!th) return;
|
|
431
|
+
const tds = Array.from(tableEl.querySelectorAll(`td[data-col-id="${columnId}"]`));
|
|
432
|
+
const headerW = th.scrollWidth;
|
|
433
|
+
const cellsMaxW = tds.reduce((max, td) => Math.max(max, td.scrollWidth), 0);
|
|
434
|
+
const padding = 24;
|
|
435
|
+
const raw = Math.max(headerW, cellsMaxW) + padding;
|
|
436
|
+
const minW = columns.find((c) => c.id === columnId)?.minWidth ?? 80;
|
|
437
|
+
const maxW = columns.find((c) => c.id === columnId)?.maxWidth ?? 600;
|
|
438
|
+
const finalW = Math.max(minW, Math.min(raw, maxW));
|
|
439
|
+
setColumnWidths((prev) => ({ ...prev, [columnId]: finalW }));
|
|
440
|
+
};
|
|
441
|
+
const saveCurrentView = (name) => {
|
|
442
|
+
const viewState = {
|
|
443
|
+
page: query.page,
|
|
444
|
+
pageSize: query.pageSize,
|
|
445
|
+
sort: query.sort,
|
|
446
|
+
filters: query.filters,
|
|
447
|
+
columnWidths: enableColumnResizing ? columnWidths : void 0
|
|
448
|
+
};
|
|
449
|
+
setSavedViews((prev) => {
|
|
450
|
+
const filtered = prev.filter((v) => v.name !== name);
|
|
451
|
+
return [...filtered, { name, state: viewState }];
|
|
452
|
+
});
|
|
453
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
454
|
+
try {
|
|
455
|
+
localStorage.setItem(`rowakit-view-${name}`, JSON.stringify(viewState));
|
|
456
|
+
} catch {
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
const loadSavedView = (name) => {
|
|
461
|
+
const view = savedViews.find((v) => v.name === name);
|
|
462
|
+
if (!view) return;
|
|
463
|
+
const { state } = view;
|
|
464
|
+
setQuery({
|
|
465
|
+
page: state.page,
|
|
466
|
+
pageSize: state.pageSize,
|
|
467
|
+
sort: state.sort,
|
|
468
|
+
filters: state.filters
|
|
469
|
+
});
|
|
470
|
+
setFilters(state.filters ?? {});
|
|
471
|
+
if (state.columnWidths && enableColumnResizing) {
|
|
472
|
+
setColumnWidths(state.columnWidths);
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
const deleteSavedView = (name) => {
|
|
476
|
+
setSavedViews((prev) => prev.filter((v) => v.name !== name));
|
|
477
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
478
|
+
try {
|
|
479
|
+
localStorage.removeItem(`rowakit-view-${name}`);
|
|
480
|
+
} catch {
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
const resetTableState = () => {
|
|
485
|
+
setQuery({
|
|
486
|
+
page: 1,
|
|
487
|
+
pageSize: defaultPageSize
|
|
488
|
+
});
|
|
489
|
+
setFilters({});
|
|
490
|
+
setColumnWidths({});
|
|
491
|
+
};
|
|
492
|
+
const transformFilterValueForColumn = (column, value) => {
|
|
493
|
+
if (!value || column?.kind !== "number") {
|
|
494
|
+
return value;
|
|
495
|
+
}
|
|
496
|
+
const numberColumn = column;
|
|
497
|
+
if (!numberColumn.filterTransform) {
|
|
498
|
+
return value;
|
|
499
|
+
}
|
|
500
|
+
if (value.op === "equals" && typeof value.value === "number") {
|
|
501
|
+
return {
|
|
502
|
+
...value,
|
|
503
|
+
value: numberColumn.filterTransform(value.value)
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
if (value.op === "range" && typeof value.value === "object") {
|
|
507
|
+
const { from, to } = value.value;
|
|
508
|
+
return {
|
|
509
|
+
op: "range",
|
|
510
|
+
value: {
|
|
511
|
+
from: from !== void 0 && typeof from === "number" ? numberColumn.filterTransform(from) : from,
|
|
512
|
+
to: to !== void 0 && typeof to === "number" ? numberColumn.filterTransform(to) : to
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
return value;
|
|
517
|
+
};
|
|
313
518
|
const handleFilterChange = (field, value) => {
|
|
519
|
+
const column = columns.find((c) => c.id === field);
|
|
520
|
+
const transformedValue = transformFilterValueForColumn(column, value);
|
|
314
521
|
setFilters((prev) => ({
|
|
315
522
|
...prev,
|
|
316
|
-
[field]:
|
|
523
|
+
[field]: transformedValue
|
|
317
524
|
}));
|
|
318
525
|
};
|
|
319
526
|
const handleClearFilter = (field) => {
|
|
@@ -334,6 +541,52 @@ function RowaKitTable({
|
|
|
334
541
|
const canGoNext = query.page < totalPages && !isLoading;
|
|
335
542
|
const hasActiveFilters = enableFilters && Object.values(filters).some((v) => v !== void 0);
|
|
336
543
|
return /* @__PURE__ */ jsxs("div", { className: `rowakit-table${className ? ` ${className}` : ""}`, children: [
|
|
544
|
+
enableSavedViews && /* @__PURE__ */ jsxs("div", { className: "rowakit-saved-views-group", children: [
|
|
545
|
+
/* @__PURE__ */ jsx(
|
|
546
|
+
"button",
|
|
547
|
+
{
|
|
548
|
+
onClick: () => {
|
|
549
|
+
const name = typeof window !== "undefined" ? window.prompt("Enter view name:") : null;
|
|
550
|
+
if (name) {
|
|
551
|
+
saveCurrentView(name);
|
|
552
|
+
}
|
|
553
|
+
},
|
|
554
|
+
className: "rowakit-saved-view-button",
|
|
555
|
+
type: "button",
|
|
556
|
+
children: "Save View"
|
|
557
|
+
}
|
|
558
|
+
),
|
|
559
|
+
savedViews.map((view) => /* @__PURE__ */ jsxs("div", { className: "rowakit-saved-view-item", children: [
|
|
560
|
+
/* @__PURE__ */ jsx(
|
|
561
|
+
"button",
|
|
562
|
+
{
|
|
563
|
+
onClick: () => loadSavedView(view.name),
|
|
564
|
+
className: "rowakit-saved-view-button",
|
|
565
|
+
type: "button",
|
|
566
|
+
children: view.name
|
|
567
|
+
}
|
|
568
|
+
),
|
|
569
|
+
/* @__PURE__ */ jsx(
|
|
570
|
+
"button",
|
|
571
|
+
{
|
|
572
|
+
onClick: () => deleteSavedView(view.name),
|
|
573
|
+
className: "rowakit-saved-view-button rowakit-saved-view-button-delete",
|
|
574
|
+
type: "button",
|
|
575
|
+
title: "Delete this view",
|
|
576
|
+
children: "\xD7"
|
|
577
|
+
}
|
|
578
|
+
)
|
|
579
|
+
] }, view.name)),
|
|
580
|
+
(hasActiveFilters || query.page > 1 || query.sort) && /* @__PURE__ */ jsx(
|
|
581
|
+
"button",
|
|
582
|
+
{
|
|
583
|
+
onClick: resetTableState,
|
|
584
|
+
className: "rowakit-saved-view-button",
|
|
585
|
+
type: "button",
|
|
586
|
+
children: "Reset"
|
|
587
|
+
}
|
|
588
|
+
)
|
|
589
|
+
] }),
|
|
337
590
|
hasActiveFilters && /* @__PURE__ */ jsx("div", { className: "rowakit-table-filter-controls", children: /* @__PURE__ */ jsx(
|
|
338
591
|
"button",
|
|
339
592
|
{
|
|
@@ -343,14 +596,17 @@ function RowaKitTable({
|
|
|
343
596
|
children: "Clear all filters"
|
|
344
597
|
}
|
|
345
598
|
) }),
|
|
346
|
-
/* @__PURE__ */ jsxs("table", { children: [
|
|
599
|
+
/* @__PURE__ */ jsxs("table", { ref: tableRef, children: [
|
|
347
600
|
/* @__PURE__ */ jsxs("thead", { children: [
|
|
348
601
|
/* @__PURE__ */ jsx("tr", { children: columns.map((column) => {
|
|
349
602
|
const isSortable = column.kind !== "actions" && (column.kind === "custom" ? false : column.sortable === true);
|
|
350
603
|
const field = column.kind === "actions" ? "" : column.kind === "custom" ? column.field : column.field;
|
|
604
|
+
const isResizable = enableColumnResizing && column.kind !== "actions";
|
|
605
|
+
const actualWidth = columnWidths[column.id] ?? column.width;
|
|
351
606
|
return /* @__PURE__ */ jsxs(
|
|
352
607
|
"th",
|
|
353
608
|
{
|
|
609
|
+
"data-col-id": column.id,
|
|
354
610
|
onClick: isSortable ? () => handleSort(String(field)) : void 0,
|
|
355
611
|
role: isSortable ? "button" : void 0,
|
|
356
612
|
tabIndex: isSortable ? 0 : void 0,
|
|
@@ -362,13 +618,23 @@ function RowaKitTable({
|
|
|
362
618
|
} : void 0,
|
|
363
619
|
"aria-sort": isSortable && query.sort?.field === String(field) ? query.sort.direction === "asc" ? "ascending" : "descending" : void 0,
|
|
364
620
|
style: {
|
|
365
|
-
width:
|
|
366
|
-
textAlign: column.align
|
|
621
|
+
width: actualWidth ? `${actualWidth}px` : void 0,
|
|
622
|
+
textAlign: column.align,
|
|
623
|
+
position: isResizable ? "relative" : void 0
|
|
367
624
|
},
|
|
368
|
-
className: column.truncate ? "rowakit-cell-truncate" : void 0,
|
|
625
|
+
className: column.truncate && !isResizable ? "rowakit-cell-truncate" : void 0,
|
|
369
626
|
children: [
|
|
370
627
|
getHeaderLabel(column),
|
|
371
|
-
isSortable && getSortIndicator(String(field))
|
|
628
|
+
isSortable && getSortIndicator(String(field)),
|
|
629
|
+
isResizable && /* @__PURE__ */ jsx(
|
|
630
|
+
"div",
|
|
631
|
+
{
|
|
632
|
+
className: "rowakit-column-resize-handle",
|
|
633
|
+
onMouseDown: (e) => startColumnResize(e, column.id),
|
|
634
|
+
onDoubleClick: () => handleColumnResizeDoubleClick(column.id),
|
|
635
|
+
title: "Drag to resize | Double-click to auto-fit content"
|
|
636
|
+
}
|
|
637
|
+
)
|
|
372
638
|
]
|
|
373
639
|
},
|
|
374
640
|
column.id
|
|
@@ -468,6 +734,51 @@ function RowaKitTable({
|
|
|
468
734
|
] }) }, column.id);
|
|
469
735
|
}
|
|
470
736
|
const isNumberColumn = column.kind === "number";
|
|
737
|
+
if (isNumberColumn) {
|
|
738
|
+
const fromValue = filterValue?.op === "range" ? String(filterValue.value.from ?? "") : filterValue?.op === "equals" && typeof filterValue.value === "number" ? String(filterValue.value) : "";
|
|
739
|
+
const toValue = filterValue?.op === "range" ? String(filterValue.value.to ?? "") : "";
|
|
740
|
+
const showRangeUI = !filterValue || filterValue.op === "range";
|
|
741
|
+
if (showRangeUI) {
|
|
742
|
+
return /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsxs("div", { className: "rowakit-filter-number-range", children: [
|
|
743
|
+
/* @__PURE__ */ jsx(
|
|
744
|
+
"input",
|
|
745
|
+
{
|
|
746
|
+
type: "number",
|
|
747
|
+
className: "rowakit-filter-input",
|
|
748
|
+
placeholder: "Min",
|
|
749
|
+
value: fromValue,
|
|
750
|
+
onChange: (e) => {
|
|
751
|
+
const from = e.target.value ? Number(e.target.value) : void 0;
|
|
752
|
+
const to = toValue ? Number(toValue) : void 0;
|
|
753
|
+
if (from === void 0 && to === void 0) {
|
|
754
|
+
handleClearFilter(field);
|
|
755
|
+
} else {
|
|
756
|
+
handleFilterChange(field, { op: "range", value: { from, to } });
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
),
|
|
761
|
+
/* @__PURE__ */ jsx(
|
|
762
|
+
"input",
|
|
763
|
+
{
|
|
764
|
+
type: "number",
|
|
765
|
+
className: "rowakit-filter-input",
|
|
766
|
+
placeholder: "Max",
|
|
767
|
+
value: toValue,
|
|
768
|
+
onChange: (e) => {
|
|
769
|
+
const to = e.target.value ? Number(e.target.value) : void 0;
|
|
770
|
+
const from = fromValue ? Number(fromValue) : void 0;
|
|
771
|
+
if (from === void 0 && to === void 0) {
|
|
772
|
+
handleClearFilter(field);
|
|
773
|
+
} else {
|
|
774
|
+
handleFilterChange(field, { op: "range", value: { from, to } });
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
)
|
|
779
|
+
] }) }, column.id);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
471
782
|
return /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx(
|
|
472
783
|
"input",
|
|
473
784
|
{
|
|
@@ -519,12 +830,14 @@ function RowaKitTable({
|
|
|
519
830
|
column.kind === "number" ? "rowakit-cell-number" : "",
|
|
520
831
|
column.truncate ? "rowakit-cell-truncate" : ""
|
|
521
832
|
].filter(Boolean).join(" ") || void 0;
|
|
833
|
+
const actualWidth = columnWidths[column.id];
|
|
522
834
|
return /* @__PURE__ */ jsx(
|
|
523
835
|
"td",
|
|
524
836
|
{
|
|
837
|
+
"data-col-id": column.id,
|
|
525
838
|
className: cellClass,
|
|
526
839
|
style: {
|
|
527
|
-
width:
|
|
840
|
+
width: actualWidth ? `${actualWidth}px` : void 0,
|
|
528
841
|
textAlign: column.align || (column.kind === "number" ? "right" : void 0)
|
|
529
842
|
},
|
|
530
843
|
children: renderCell(column, row, isLoading, setConfirmState)
|