@nexgrid/react 0.2.0 → 0.2.1

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/dist/index.js CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  isFilterable,
21
21
  isHideable,
22
22
  isPageSize,
23
+ isPinned,
23
24
  isSortable,
24
25
  primarySort,
25
26
  resolveLocale,
@@ -34,7 +35,16 @@ import {
34
35
  withSearch,
35
36
  withToggledSort,
36
37
  withToggledMultiSort,
37
- copyToClipboard
38
+ copyToClipboard,
39
+ clearGridState,
40
+ loadGridState,
41
+ saveGridState,
42
+ getCellValue as getCellValue2,
43
+ getCellText as getCellText2,
44
+ queryClientData,
45
+ defaultQuery,
46
+ flattenColumns,
47
+ buildHeaderRows
38
48
  } from "@nexgrid/core";
39
49
 
40
50
  // src/icons.tsx
@@ -269,7 +279,7 @@ function useDropdown() {
269
279
  }
270
280
 
271
281
  // src/table-x.tsx
272
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
282
+ import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
273
283
  function defaultRowId(row) {
274
284
  const record = row;
275
285
  const id = record === null || record === void 0 ? void 0 : record["id"];
@@ -284,9 +294,11 @@ function TableX(props) {
284
294
  const {
285
295
  columns,
286
296
  data,
287
- total,
288
- query,
289
- onQueryChange,
297
+ total: totalProp,
298
+ query: queryProp,
299
+ onQueryChange: onQueryChangeProp,
300
+ clientSidePagination,
301
+ paginationMode,
290
302
  caption,
291
303
  density: initialDensity = "default",
292
304
  isLoading = false,
@@ -321,6 +333,8 @@ function TableX(props) {
321
333
  enableColumnReorder = false,
322
334
  onColumnOrderChange,
323
335
  onCellEdit,
336
+ storageKey,
337
+ showFilterPills = true,
324
338
  toolbarActions,
325
339
  onRowClick,
326
340
  getRowId = defaultRowId,
@@ -334,6 +348,22 @@ function TableX(props) {
334
348
  onNotify,
335
349
  theme = "light"
336
350
  } = props;
351
+ const isClientSide = clientSidePagination === true || paginationMode === "client" || onQueryChangeProp === void 0 && queryProp === void 0;
352
+ const [internalQuery, setInternalQuery] = React4.useState(() => queryProp ?? defaultQuery());
353
+ const query = queryProp ?? internalQuery;
354
+ const onQueryChange = React4.useCallback(
355
+ (next) => {
356
+ setInternalQuery(next);
357
+ onQueryChangeProp?.(next);
358
+ },
359
+ [onQueryChangeProp]
360
+ );
361
+ const clientPaged = React4.useMemo(() => {
362
+ if (!isClientSide) return null;
363
+ return queryClientData(data, query);
364
+ }, [isClientSide, data, query]);
365
+ const rows = isClientSide ? clientPaged?.items ?? data : data;
366
+ const total = isClientSide ? clientPaged?.total ?? data.length : totalProp ?? data.length;
337
367
  const locale = resolveLocale(localeOverrides);
338
368
  const boolLabels = { yes: locale.booleanYes, no: locale.booleanNo };
339
369
  const isColumnsVisible = enableColumns && showColumnsButton !== false;
@@ -348,7 +378,7 @@ function TableX(props) {
348
378
  }, [columns]);
349
379
  const [density, setDensity] = React4.useState(initialDensity);
350
380
  const [hiddenCols, setHiddenCols] = React4.useState(
351
- () => initialHiddenColumns(columns)
381
+ () => initialHiddenColumns(flattenColumns(columns))
352
382
  );
353
383
  const [selectedIds, setSelectedIds] = React4.useState(
354
384
  () => /* @__PURE__ */ new Set()
@@ -361,9 +391,82 @@ function TableX(props) {
361
391
  const [isExporting, setIsExporting] = React4.useState(false);
362
392
  const [openFilterCol, setOpenFilterCol] = React4.useState(null);
363
393
  const [colWidths, setColWidths] = React4.useState({});
394
+ React4.useEffect(() => {
395
+ if (!storageKey) return;
396
+ const persisted = loadGridState(storageKey);
397
+ if (persisted) {
398
+ if (persisted.density) setDensity(persisted.density);
399
+ if (persisted.columnWidths) setColWidths(persisted.columnWidths);
400
+ if (persisted.hiddenColumns && Array.isArray(persisted.hiddenColumns)) {
401
+ const hiddenMap = {};
402
+ for (const col of columns) {
403
+ const id = getColumnId2(col);
404
+ if (id) hiddenMap[id] = persisted.hiddenColumns.includes(id);
405
+ }
406
+ setHiddenCols(hiddenMap);
407
+ }
408
+ if (persisted.columnOrder && Array.isArray(persisted.columnOrder)) {
409
+ const colMap = new Map(columns.map((c) => [getColumnId2(c), c]));
410
+ const reordered = [];
411
+ for (const id of persisted.columnOrder) {
412
+ const col = colMap.get(id);
413
+ if (col) {
414
+ reordered.push(col);
415
+ colMap.delete(id);
416
+ }
417
+ }
418
+ for (const remaining of colMap.values()) {
419
+ reordered.push(remaining);
420
+ }
421
+ if (reordered.length === columns.length) {
422
+ setColList(reordered);
423
+ }
424
+ }
425
+ }
426
+ }, [storageKey, columns]);
427
+ React4.useEffect(() => {
428
+ if (!storageKey) return;
429
+ const hiddenList = Object.entries(hiddenCols).filter(([_, isHidden]) => isHidden).map(([id]) => id);
430
+ saveGridState(storageKey, {
431
+ density,
432
+ columnWidths: colWidths,
433
+ columnOrder: colList.map(getColumnId2).filter(Boolean),
434
+ hiddenColumns: hiddenList
435
+ });
436
+ }, [storageKey, density, hiddenCols, colList, colWidths]);
364
437
  const columnsMenu = useDropdown();
365
438
  const densityMenu = useDropdown();
366
439
  const exportMenu = useDropdown();
440
+ const handleResetView = React4.useCallback(() => {
441
+ if (storageKey) {
442
+ clearGridState(storageKey);
443
+ }
444
+ setDensity(initialDensity);
445
+ setColWidths({});
446
+ setColList(columns);
447
+ setHiddenCols(initialHiddenColumns(flattenColumns(columns)));
448
+ columnsMenu.close();
449
+ }, [storageKey, initialDensity, columns, columnsMenu]);
450
+ const leafCols = React4.useMemo(() => flattenColumns(colList), [colList]);
451
+ const autoFitColumn = React4.useCallback((id) => {
452
+ const col = leafCols.find((c) => getColumnId2(c) === id);
453
+ if (!col) return;
454
+ const meta = col.meta ?? {};
455
+ let maxContentWidth = 0;
456
+ const headerTitle = getColumnTitle(col) || id;
457
+ maxContentWidth = Math.max(maxContentWidth, headerTitle.length * 8.5 + 50);
458
+ for (const row of rows) {
459
+ const rawVal = getCellValue2(col, row);
460
+ const val = getCellText2(rawVal);
461
+ if (val) {
462
+ maxContentWidth = Math.max(maxContentWidth, String(val).length * 8 + 26);
463
+ }
464
+ }
465
+ const minW = meta.minWidth ?? 60;
466
+ const maxW = 550;
467
+ const finalWidth = Math.min(maxW, Math.max(minW, Math.round(maxContentWidth)));
468
+ setColWidths((prev) => ({ ...prev, [id]: finalWidth }));
469
+ }, [leafCols, rows]);
367
470
  const instanceId = React4.useId();
368
471
  const columnsButtonId = `${instanceId}-columns`;
369
472
  const densityButtonId = `${instanceId}-density`;
@@ -378,16 +481,20 @@ function TableX(props) {
378
481
  const totalPages = totalPagesFor(total, pageSize);
379
482
  const range = getRecordRange(currentPage, pageSize, total);
380
483
  const sort = primarySort(query);
381
- const visible = React4.useMemo(
382
- () => visibleColumns(colList, hiddenCols),
484
+ const headerRows = React4.useMemo(
485
+ () => buildHeaderRows(colList, hiddenCols),
383
486
  [colList, hiddenCols]
384
487
  );
385
- const hideable = React4.useMemo(() => colList.filter(isHideable), [colList]);
488
+ const visible = React4.useMemo(
489
+ () => visibleColumns(leafCols, hiddenCols),
490
+ [leafCols, hiddenCols]
491
+ );
492
+ const hideable = React4.useMemo(() => leafCols.filter(isHideable), [leafCols]);
386
493
  const pageItems = React4.useMemo(
387
494
  () => getPageNumbers(currentPage, totalPages),
388
495
  [currentPage, totalPages]
389
496
  );
390
- const pageRowIds = React4.useMemo(() => data.map((row) => getRowId(row)), [data, getRowId]);
497
+ const pageRowIds = React4.useMemo(() => rows.map((row) => getRowId(row)), [rows, getRowId]);
391
498
  const columnCount = visible.length + (renderExpandedRow ? 1 : 0) + (showSerialNumber ? 1 : 0) + (enableSelection ? 1 : 0);
392
499
  const getPinnedOffsets = (visibleCols) => {
393
500
  const leftOffsets2 = /* @__PURE__ */ new Map();
@@ -506,7 +613,7 @@ function TableX(props) {
506
613
  setIsExporting(true);
507
614
  notify("info", "Exporting...");
508
615
  try {
509
- let exportRows = data;
616
+ let exportRows = rows;
510
617
  if (fetchEndpoint) {
511
618
  const full = await fetchAllPages(async (page, size) => {
512
619
  const url = buildQueryUrl(fetchEndpoint, {
@@ -519,6 +626,9 @@ function TableX(props) {
519
626
  return await response.json();
520
627
  });
521
628
  exportRows = full.items;
629
+ } else if (isClientSide) {
630
+ const full = queryClientData(data, query, { paginate: false });
631
+ exportRows = full.items;
522
632
  }
523
633
  const exportColumns = toExportColumns(visible, boolLabels);
524
634
  const prefix = exportFileName ?? filePrefixFromCaption(caption);
@@ -570,6 +680,205 @@ function TableX(props) {
570
680
  onRowClick?.(row);
571
681
  };
572
682
  const hasSummary = enableSummaryRow === true || visible.some((col) => col.meta?.aggregation !== void 0);
683
+ const renderLeafTh = (col, rowSpan = 1, isGroupChild = false) => {
684
+ const id = getColumnId2(col);
685
+ const sortable = isSortable(col) && enableSorting !== false;
686
+ const sortIndex = query.sort.findIndex((s) => s.field === id);
687
+ const sortItem = sortIndex >= 0 ? query.sort[sortIndex] : void 0;
688
+ const sorted = sortable && sortItem !== void 0;
689
+ const title = getColumnTitle(col) || id;
690
+ const meta = col.meta;
691
+ const activeFilter = query.filter?.[id];
692
+ const isFilterActive = activeFilter !== void 0 && activeFilter !== "";
693
+ const customWidth = colWidths[id];
694
+ const baseStyle = headerCellStyle(col);
695
+ const thStyle = {
696
+ ...baseStyle,
697
+ ...customWidth !== void 0 ? { width: `${customWidth}px` } : {},
698
+ ...leftOffsets.has(id) ? { left: leftOffsets.get(id) } : {},
699
+ ...rightOffsets.has(id) ? { right: rightOffsets.get(id) } : {}
700
+ };
701
+ const startResize = (e) => {
702
+ e.preventDefault();
703
+ e.stopPropagation();
704
+ const startX = e.clientX;
705
+ const targetTh = e.currentTarget.parentElement || null;
706
+ const startWidth = targetTh ? targetTh.getBoundingClientRect().width : customWidth ?? meta?.width ?? 120;
707
+ const onMove = (moveEvent) => {
708
+ const nextW = Math.max(meta?.minWidth ?? 60, Math.round(startWidth + (moveEvent.clientX - startX)));
709
+ setColWidths((prev) => ({ ...prev, [id]: nextW }));
710
+ };
711
+ const onUp = () => {
712
+ document.removeEventListener("pointermove", onMove);
713
+ document.removeEventListener("pointerup", onUp);
714
+ document.body?.classList.remove("tbx-resizing");
715
+ };
716
+ document.body?.classList.add("tbx-resizing");
717
+ document.addEventListener("pointermove", onMove);
718
+ document.addEventListener("pointerup", onUp);
719
+ };
720
+ return /* @__PURE__ */ jsxs2(
721
+ "th",
722
+ {
723
+ scope: "col",
724
+ rowSpan: rowSpan > 1 ? rowSpan : void 0,
725
+ "aria-sort": !sortable ? void 0 : sorted ? sortItem?.dir === "asc" ? "ascending" : "descending" : "none",
726
+ tabIndex: sortable ? 0 : void 0,
727
+ "data-column-id": id,
728
+ "data-tbx-focus": sortable ? `sort:${id}` : void 0,
729
+ draggable: enableColumnReorder && !isGroupChild,
730
+ onDragStart: enableColumnReorder && !isGroupChild ? (e) => {
731
+ setDraggedColId(id);
732
+ e.dataTransfer.effectAllowed = "move";
733
+ e.dataTransfer.setData("text/plain", id);
734
+ } : void 0,
735
+ onDragOver: enableColumnReorder && !isGroupChild ? (e) => {
736
+ e.preventDefault();
737
+ } : void 0,
738
+ onDrop: enableColumnReorder && !isGroupChild ? (e) => {
739
+ e.preventDefault();
740
+ if (!draggedColId || draggedColId === id) return;
741
+ const fromIdx = colList.findIndex((c) => getColumnId2(c) === draggedColId);
742
+ const toIdx = colList.findIndex((c) => getColumnId2(c) === id);
743
+ if (fromIdx >= 0 && toIdx >= 0) {
744
+ const nextCols = [...colList];
745
+ const moved = nextCols[fromIdx];
746
+ if (!moved) return;
747
+ nextCols.splice(fromIdx, 1);
748
+ nextCols.splice(toIdx, 0, moved);
749
+ setColList(nextCols);
750
+ onColumnOrderChange?.(nextCols.map(getColumnId2));
751
+ }
752
+ } : void 0,
753
+ onDragEnd: enableColumnReorder && !isGroupChild ? () => {
754
+ setDraggedColId(null);
755
+ } : void 0,
756
+ className: [
757
+ "tbx-th",
758
+ sortable ? "tbx-th--sortable" : void 0,
759
+ isGroupChild ? "tbx-th--grouped-child" : void 0,
760
+ enableColumnReorder && !isGroupChild ? "tbx-th--draggable" : void 0,
761
+ draggedColId === id ? "tbx-th--dragging" : void 0,
762
+ isPinned(col) === "left" ? "tbx-th--pinned-left" : void 0,
763
+ isPinned(col) === "right" ? "tbx-th--pinned-right" : void 0,
764
+ lastLeftPinnedId === id ? "tbx-pinned-border-left" : void 0,
765
+ firstRightPinnedId === id ? "tbx-pinned-border-right" : void 0
766
+ ].filter(Boolean).join(" "),
767
+ style: thStyle,
768
+ onClick: sortable ? (event) => {
769
+ const t = event.target;
770
+ if (t?.closest(".tbx-col-filter-wrap") || t?.closest(".tbx-resize-handle")) return;
771
+ toggleSort(id, event.shiftKey);
772
+ } : void 0,
773
+ onKeyDown: sortable ? (event) => {
774
+ if (event.key !== "Enter" && event.key !== " ") return;
775
+ const t = event.target;
776
+ if (t?.closest(".tbx-col-filter-wrap")) return;
777
+ event.preventDefault();
778
+ toggleSort(id, event.shiftKey);
779
+ } : void 0,
780
+ children: [
781
+ /* @__PURE__ */ jsxs2("div", { className: headerInnerClass(col), children: [
782
+ /* @__PURE__ */ jsx3("span", { children: renderColumnHeader(col) }),
783
+ sortable ? /* @__PURE__ */ jsxs2("span", { className: "tbx-sort-icon-wrap", children: [
784
+ sorted ? sortItem?.dir === "asc" ? /* @__PURE__ */ jsx3(ArrowUpIcon, { className: "tbx-sort-icon" }) : /* @__PURE__ */ jsx3(ArrowDownIcon, { className: "tbx-sort-icon" }) : /* @__PURE__ */ jsx3(ArrowUpDownIcon, { className: "tbx-sort-icon tbx-sort-icon--idle" }),
785
+ query.sort.length > 1 && sortIndex >= 0 ? /* @__PURE__ */ jsx3("span", { className: "tbx-sort-order", children: sortIndex + 1 }) : null
786
+ ] }) : null,
787
+ isFilterable(col, enableColumnFilters) ? /* @__PURE__ */ jsxs2("div", { className: "tbx-col-filter-wrap", children: [
788
+ /* @__PURE__ */ jsx3(
789
+ "button",
790
+ {
791
+ type: "button",
792
+ className: isFilterActive ? "tbx-col-filter-btn tbx-col-filter-btn--active" : "tbx-col-filter-btn",
793
+ "aria-label": `Filter ${title}`,
794
+ onClick: (e) => {
795
+ e.stopPropagation();
796
+ setOpenFilterCol(openFilterCol === id ? null : id);
797
+ },
798
+ children: /* @__PURE__ */ jsx3(DotsVerticalIcon, { className: "tbx-icon" })
799
+ }
800
+ ),
801
+ openFilterCol === id ? /* @__PURE__ */ jsxs2(
802
+ "div",
803
+ {
804
+ className: "tbx-filter-popover",
805
+ onClick: (e) => e.stopPropagation(),
806
+ children: [
807
+ /* @__PURE__ */ jsx3(
808
+ "input",
809
+ {
810
+ autoFocus: true,
811
+ type: "text",
812
+ className: "tbx-filter-popover-input",
813
+ defaultValue: activeFilter ?? "",
814
+ placeholder: `Filter by ${title}...`,
815
+ onKeyDown: (e) => {
816
+ if (e.key === "Enter") {
817
+ e.preventDefault();
818
+ const val = e.currentTarget.value.trim();
819
+ setOpenFilterCol(null);
820
+ onQueryChange(withFilter(query, id, val || void 0));
821
+ } else if (e.key === "Escape") {
822
+ setOpenFilterCol(null);
823
+ }
824
+ }
825
+ }
826
+ ),
827
+ /* @__PURE__ */ jsxs2("div", { className: "tbx-filter-popover-actions", children: [
828
+ /* @__PURE__ */ jsxs2(
829
+ "button",
830
+ {
831
+ type: "button",
832
+ className: "tbx-filter-popover-btn",
833
+ onClick: (e) => {
834
+ e.stopPropagation();
835
+ setOpenFilterCol(null);
836
+ onQueryChange(withFilter(query, id, void 0));
837
+ },
838
+ children: [
839
+ /* @__PURE__ */ jsx3(RotateCcwIcon, { className: "tbx-icon" }),
840
+ /* @__PURE__ */ jsx3("span", { children: locale.clearFilter })
841
+ ]
842
+ }
843
+ ),
844
+ /* @__PURE__ */ jsxs2(
845
+ "button",
846
+ {
847
+ type: "button",
848
+ className: "tbx-filter-popover-btn tbx-filter-popover-btn--primary",
849
+ onClick: (e) => {
850
+ e.stopPropagation();
851
+ const parent = e.currentTarget.closest(".tbx-filter-popover");
852
+ const inputEl = parent?.querySelector("input");
853
+ const val = inputEl?.value.trim();
854
+ setOpenFilterCol(null);
855
+ onQueryChange(withFilter(query, id, val || void 0));
856
+ },
857
+ children: [
858
+ /* @__PURE__ */ jsx3(CheckIcon, { className: "tbx-icon" }),
859
+ /* @__PURE__ */ jsx3("span", { children: locale.applyFilter })
860
+ ]
861
+ }
862
+ )
863
+ ] })
864
+ ]
865
+ }
866
+ ) : null
867
+ ] }) : null
868
+ ] }),
869
+ enableColumnResize ? /* @__PURE__ */ jsx3(
870
+ "div",
871
+ {
872
+ className: "tbx-resize-handle",
873
+ onPointerDown: startResize,
874
+ onDoubleClick: () => autoFitColumn(id)
875
+ }
876
+ ) : null
877
+ ]
878
+ },
879
+ id
880
+ );
881
+ };
573
882
  return /* @__PURE__ */ jsxs2("div", { className: rootClasses.join(" "), "data-density": density, ref: rootRef, children: [
574
883
  showToolbar ? /* @__PURE__ */ jsxs2("div", { className: "tbx-toolbar", children: [
575
884
  /* @__PURE__ */ jsx3("div", { className: "tbx-toolbar-group", children: enableSearch ? /* @__PURE__ */ jsxs2("div", { className: "tbx-search", children: [
@@ -638,7 +947,20 @@ function TableX(props) {
638
947
  },
639
948
  id
640
949
  );
641
- })
950
+ }),
951
+ storageKey ? /* @__PURE__ */ jsxs2(Fragment3, { children: [
952
+ /* @__PURE__ */ jsx3("div", { className: "tbx-menu-separator" }),
953
+ /* @__PURE__ */ jsx3(
954
+ "button",
955
+ {
956
+ type: "button",
957
+ className: "tbx-menu-item tbx-menu-item--reset",
958
+ role: "menuitem",
959
+ onClick: handleResetView,
960
+ children: /* @__PURE__ */ jsx3("span", { children: "Reset to default view" })
961
+ }
962
+ )
963
+ ] }) : null
642
964
  ] }) : null
643
965
  ] }) : null,
644
966
  isDensityVisible ? /* @__PURE__ */ jsxs2("div", { className: "tbx-menu-wrap", ref: densityMenu.containerRef, children: [
@@ -758,291 +1080,152 @@ function TableX(props) {
758
1080
  toolbarActions
759
1081
  ] })
760
1082
  ] }) : null,
761
- /* @__PURE__ */ jsx3("div", { className: "tbx-table-wrap", children: /* @__PURE__ */ jsxs2("table", { className: "tbx-table", "aria-label": caption, children: [
762
- /* @__PURE__ */ jsx3("thead", { children: /* @__PURE__ */ jsxs2("tr", { children: [
763
- renderExpandedRow ? /* @__PURE__ */ jsx3(
764
- "th",
765
- {
766
- className: [
767
- "tbx-th tbx-th--expand",
768
- leftOffsets.has("__expand") ? "tbx-th--pinned-left" : void 0,
769
- lastLeftPinnedId === "__expand" ? "tbx-pinned-border-left" : void 0
770
- ].filter(Boolean).join(" "),
771
- style: {
772
- width: "40px",
773
- left: leftOffsets.get("__expand")
774
- },
775
- scope: "col"
776
- }
777
- ) : null,
778
- enableSelection ? /* @__PURE__ */ jsx3(
779
- "th",
780
- {
781
- className: [
782
- "tbx-th tbx-th--select",
783
- leftOffsets.has("__select") ? "tbx-th--pinned-left" : void 0,
784
- lastLeftPinnedId === "__select" ? "tbx-pinned-border-left" : void 0
785
- ].filter(Boolean).join(" "),
786
- style: {
787
- left: leftOffsets.get("__select")
788
- },
789
- scope: "col",
790
- children: !isSingleSelect ? /* @__PURE__ */ jsx3(
791
- "input",
792
- {
793
- type: "checkbox",
794
- className: "tbx-checkbox",
795
- checked: allPageSelected,
796
- ref: (el) => {
797
- if (el) el.indeterminate = somePageSelected;
798
- },
799
- onChange: toggleSelectAll,
800
- "aria-label": locale.selectAllLabel
801
- }
802
- ) : null
803
- }
804
- ) : null,
805
- showSerialNumber ? /* @__PURE__ */ jsx3(
806
- "th",
1083
+ showFilterPills !== false && (Boolean(query.q?.trim()) || Object.keys(query.filter ?? {}).some((k) => query.filter?.[k])) ? /* @__PURE__ */ jsxs2("div", { className: "tbx-filter-pills-bar", children: [
1084
+ /* @__PURE__ */ jsx3("span", { className: "tbx-filter-pills-title", children: "Active filters:" }),
1085
+ query.q?.trim() ? /* @__PURE__ */ jsxs2("div", { className: "tbx-filter-pill", children: [
1086
+ /* @__PURE__ */ jsxs2("span", { className: "tbx-filter-pill-label", children: [
1087
+ searchPlaceholder || locale.searchPlaceholder || "Search",
1088
+ ":"
1089
+ ] }),
1090
+ /* @__PURE__ */ jsxs2("span", { className: "tbx-filter-pill-val", children: [
1091
+ '"',
1092
+ query.q,
1093
+ '"'
1094
+ ] }),
1095
+ /* @__PURE__ */ jsx3(
1096
+ "button",
807
1097
  {
808
- className: [
809
- "tbx-th tbx-th--serial",
810
- leftOffsets.has("__serial") ? "tbx-th--pinned-left" : void 0,
811
- lastLeftPinnedId === "__serial" ? "tbx-pinned-border-left" : void 0
812
- ].filter(Boolean).join(" "),
813
- style: {
814
- left: leftOffsets.get("__serial")
815
- },
816
- scope: "col",
817
- children: locale.serialHeader
1098
+ type: "button",
1099
+ className: "tbx-filter-pill-remove",
1100
+ title: "Clear search",
1101
+ "aria-label": "Clear search",
1102
+ onClick: () => onQueryChange(withSearch(query, "")),
1103
+ children: "\u2715"
818
1104
  }
819
- ) : null,
820
- visible.map((col, index) => {
821
- const id = getColumnId2(col);
822
- const sortable = isSortable(col) && enableSorting !== false;
823
- const sortIndex = query.sort.findIndex((s) => s.field === id);
824
- const sortItem = sortIndex >= 0 ? query.sort[sortIndex] : void 0;
825
- const sorted = sortable && sortItem !== void 0;
826
- const title = getColumnTitle(col) || id;
827
- const meta = col.meta;
828
- const activeFilter = query.filter?.[id];
829
- const isFilterActive = activeFilter !== void 0 && activeFilter !== "";
830
- const customWidth = colWidths[id];
831
- const baseStyle = headerCellStyle(col);
832
- const thStyle = {
833
- ...baseStyle,
834
- ...customWidth !== void 0 ? { width: `${customWidth}px` } : {},
835
- ...leftOffsets.has(id) ? { left: leftOffsets.get(id) } : {},
836
- ...rightOffsets.has(id) ? { right: rightOffsets.get(id) } : {}
837
- };
838
- const startResize = (e) => {
839
- e.preventDefault();
840
- e.stopPropagation();
841
- const startX = e.clientX;
842
- const targetTh = e.currentTarget.parentElement || null;
843
- const startWidth = targetTh ? targetTh.getBoundingClientRect().width : customWidth ?? meta?.width ?? 120;
844
- const onMove = (moveEvent) => {
845
- const nextW = Math.max(meta?.minWidth ?? 60, Math.round(startWidth + (moveEvent.clientX - startX)));
846
- setColWidths((prev) => ({ ...prev, [id]: nextW }));
847
- };
848
- const onUp = () => {
849
- document.removeEventListener("pointermove", onMove);
850
- document.removeEventListener("pointerup", onUp);
851
- document.body?.classList.remove("tbx-resizing");
852
- };
853
- document.body?.classList.add("tbx-resizing");
854
- document.addEventListener("pointermove", onMove);
855
- document.addEventListener("pointerup", onUp);
856
- };
857
- const isPinnedLeft = leftOffsets.has(id);
858
- const isPinnedRight = rightOffsets.has(id);
859
- const isLastLeft = lastLeftPinnedId === id;
860
- const isFirstRight = firstRightPinnedId === id;
861
- return /* @__PURE__ */ jsxs2(
1105
+ )
1106
+ ] }) : null,
1107
+ Object.entries(query.filter ?? {}).map(([key, val]) => {
1108
+ if (val === void 0 || val === "") return null;
1109
+ const col = leafCols.find((c) => getColumnId2(c) === key);
1110
+ const title = col ? getColumnTitle(col) || key : key;
1111
+ return /* @__PURE__ */ jsxs2("div", { className: "tbx-filter-pill", children: [
1112
+ /* @__PURE__ */ jsxs2("span", { className: "tbx-filter-pill-label", children: [
1113
+ title,
1114
+ ":"
1115
+ ] }),
1116
+ /* @__PURE__ */ jsx3("span", { className: "tbx-filter-pill-val", children: String(val) }),
1117
+ /* @__PURE__ */ jsx3(
1118
+ "button",
1119
+ {
1120
+ type: "button",
1121
+ className: "tbx-filter-pill-remove",
1122
+ title: `Remove ${title} filter`,
1123
+ "aria-label": `Remove filter for ${title}`,
1124
+ onClick: () => onQueryChange(withFilter(query, key, void 0)),
1125
+ children: "\u2715"
1126
+ }
1127
+ )
1128
+ ] }, key);
1129
+ }),
1130
+ /* @__PURE__ */ jsx3(
1131
+ "button",
1132
+ {
1133
+ type: "button",
1134
+ className: "tbx-filter-pill-clear-all",
1135
+ onClick: () => onQueryChange({ ...query, page: 1, q: void 0, filter: {} }),
1136
+ children: "Clear all"
1137
+ }
1138
+ )
1139
+ ] }) : null,
1140
+ /* @__PURE__ */ jsx3("div", { className: "tbx-table-wrap", children: /* @__PURE__ */ jsxs2("table", { className: "tbx-table", "aria-label": caption, children: [
1141
+ /* @__PURE__ */ jsxs2("thead", { children: [
1142
+ /* @__PURE__ */ jsxs2("tr", { children: [
1143
+ renderExpandedRow ? /* @__PURE__ */ jsx3(
862
1144
  "th",
863
1145
  {
864
- scope: "col",
1146
+ rowSpan: headerRows.hasGroups ? 2 : void 0,
865
1147
  className: [
866
- "tbx-th",
867
- sortable ? "tbx-th--sortable" : void 0,
868
- enableColumnReorder ? "tbx-th--draggable" : void 0,
869
- isPinnedLeft ? "tbx-th--pinned-left" : void 0,
870
- isPinnedRight ? "tbx-th--pinned-right" : void 0,
871
- isLastLeft ? "tbx-pinned-border-left" : void 0,
872
- isFirstRight ? "tbx-pinned-border-right" : void 0
1148
+ "tbx-th tbx-th--expand",
1149
+ leftOffsets.has("__expand") ? "tbx-th--pinned-left" : void 0,
1150
+ lastLeftPinnedId === "__expand" ? "tbx-pinned-border-left" : void 0
873
1151
  ].filter(Boolean).join(" "),
874
- style: thStyle,
875
- "aria-sort": sorted ? sortItem?.dir === "asc" ? "ascending" : "descending" : sortable ? "none" : void 0,
876
- "aria-label": title || void 0,
877
- tabIndex: sortable ? 0 : void 0,
878
- draggable: enableColumnReorder,
879
- onDragStart: enableColumnReorder ? (e) => {
880
- setDraggedColId(id);
881
- e.dataTransfer.setData("text/plain", id);
882
- } : void 0,
883
- onDragOver: enableColumnReorder ? (e) => {
884
- e.preventDefault();
885
- } : void 0,
886
- onDrop: enableColumnReorder ? (e) => {
887
- e.preventDefault();
888
- if (!draggedColId || draggedColId === id) return;
889
- const fromIdx = colList.findIndex((c) => getColumnId2(c) === draggedColId);
890
- const toIdx = colList.findIndex((c) => getColumnId2(c) === id);
891
- if (fromIdx >= 0 && toIdx >= 0) {
892
- const nextCols = [...colList];
893
- const moved = nextCols[fromIdx];
894
- if (!moved) return;
895
- nextCols.splice(fromIdx, 1);
896
- nextCols.splice(toIdx, 0, moved);
897
- setColList(nextCols);
898
- onColumnOrderChange?.(nextCols.map(getColumnId2));
1152
+ style: {
1153
+ width: "40px",
1154
+ left: leftOffsets.get("__expand")
1155
+ },
1156
+ scope: "col"
1157
+ }
1158
+ ) : null,
1159
+ enableSelection ? /* @__PURE__ */ jsx3(
1160
+ "th",
1161
+ {
1162
+ rowSpan: headerRows.hasGroups ? 2 : void 0,
1163
+ className: [
1164
+ "tbx-th tbx-th--select",
1165
+ leftOffsets.has("__select") ? "tbx-th--pinned-left" : void 0,
1166
+ lastLeftPinnedId === "__select" ? "tbx-pinned-border-left" : void 0
1167
+ ].filter(Boolean).join(" "),
1168
+ style: {
1169
+ left: leftOffsets.get("__select")
1170
+ },
1171
+ scope: "col",
1172
+ children: !isSingleSelect ? /* @__PURE__ */ jsx3(
1173
+ "input",
1174
+ {
1175
+ type: "checkbox",
1176
+ className: "tbx-checkbox",
1177
+ checked: allPageSelected,
1178
+ ref: (el) => {
1179
+ if (el) el.indeterminate = somePageSelected;
1180
+ },
1181
+ onChange: toggleSelectAll,
1182
+ "aria-label": locale.selectAllLabel
899
1183
  }
900
- setDraggedColId(null);
901
- } : void 0,
902
- onClick: sortable ? (event) => {
903
- const t = event.target;
904
- if (t?.closest(".tbx-col-filter-wrap") || t?.closest(".tbx-resize-handle")) return;
905
- toggleSort(id, event.shiftKey);
906
- } : void 0,
907
- onKeyDown: sortable ? (event) => {
908
- if (event.key !== "Enter" && event.key !== " ") return;
909
- const t = event.target;
910
- if (t?.closest(".tbx-col-filter-wrap")) return;
911
- event.preventDefault();
912
- toggleSort(id, event.shiftKey);
913
- } : void 0,
914
- children: [
915
- /* @__PURE__ */ jsxs2("div", { className: headerInnerClass(col), children: [
916
- /* @__PURE__ */ jsx3("span", { children: renderColumnHeader(col) }),
917
- sortable ? /* @__PURE__ */ jsxs2("span", { className: "tbx-sort-icon-wrap", children: [
918
- sorted ? sortItem?.dir === "asc" ? /* @__PURE__ */ jsx3(ArrowUpIcon, { className: "tbx-sort-icon" }) : /* @__PURE__ */ jsx3(ArrowDownIcon, { className: "tbx-sort-icon" }) : /* @__PURE__ */ jsx3(ArrowUpDownIcon, { className: "tbx-sort-icon tbx-sort-icon--idle" }),
919
- query.sort.length > 1 && sortIndex >= 0 ? /* @__PURE__ */ jsx3("span", { className: "tbx-sort-order", children: sortIndex + 1 }) : null
920
- ] }) : null,
921
- isFilterable(col, enableColumnFilters) ? /* @__PURE__ */ jsxs2("div", { className: "tbx-col-filter-wrap", children: [
922
- /* @__PURE__ */ jsx3(
923
- "button",
924
- {
925
- type: "button",
926
- className: isFilterActive ? "tbx-col-filter-btn tbx-col-filter-btn--active" : "tbx-col-filter-btn",
927
- "aria-label": `Filter ${title}`,
928
- onClick: (e) => {
929
- e.stopPropagation();
930
- setOpenFilterCol(openFilterCol === id ? null : id);
931
- },
932
- children: /* @__PURE__ */ jsx3(DotsVerticalIcon, { className: "tbx-icon" })
933
- }
934
- ),
935
- openFilterCol === id ? /* @__PURE__ */ jsxs2(
936
- "div",
937
- {
938
- className: "tbx-filter-popover",
939
- onClick: (e) => e.stopPropagation(),
940
- children: [
941
- /* @__PURE__ */ jsx3(
942
- "input",
943
- {
944
- autoFocus: true,
945
- type: "text",
946
- className: "tbx-filter-popover-input",
947
- defaultValue: activeFilter ?? "",
948
- placeholder: meta?.filterPlaceholder || formatMessage(locale.filterColumnPlaceholder, { column: title }),
949
- "aria-label": `Filter ${title}`,
950
- onChange: (e) => {
951
- const val = e.currentTarget.value.toLowerCase().trim();
952
- const pop = e.currentTarget.closest(".tbx-filter-popover");
953
- const opts = pop?.querySelectorAll(".tbx-filter-option:not(:first-child)");
954
- opts?.forEach((opt) => {
955
- const text = opt.textContent?.toLowerCase() ?? "";
956
- opt.style.display = !val || text.includes(val) ? "" : "none";
957
- });
958
- },
959
- onKeyDown: (e) => {
960
- if (e.key === "Enter") {
961
- e.preventDefault();
962
- const val = e.currentTarget.value.trim();
963
- setOpenFilterCol(null);
964
- onQueryChange(withFilter(query, id, val || void 0));
965
- } else if (e.key === "Escape") {
966
- e.preventDefault();
967
- setOpenFilterCol(null);
968
- }
969
- }
970
- }
971
- ),
972
- meta?.filterOptions && meta.filterOptions.length > 0 ? /* @__PURE__ */ jsxs2("div", { className: "tbx-filter-popover-options", children: [
973
- /* @__PURE__ */ jsx3(
974
- "div",
975
- {
976
- className: !activeFilter ? "tbx-filter-option tbx-filter-option--selected" : "tbx-filter-option",
977
- onClick: () => {
978
- setOpenFilterCol(null);
979
- onQueryChange(withFilter(query, id, void 0));
980
- },
981
- children: locale.filterAll
982
- }
983
- ),
984
- meta.filterOptions.map((opt) => /* @__PURE__ */ jsx3(
985
- "div",
986
- {
987
- className: activeFilter === opt ? "tbx-filter-option tbx-filter-option--selected" : "tbx-filter-option",
988
- onClick: () => {
989
- setOpenFilterCol(null);
990
- onQueryChange(withFilter(query, id, opt));
991
- },
992
- children: opt
993
- },
994
- opt
995
- ))
996
- ] }) : null,
997
- /* @__PURE__ */ jsxs2("div", { className: "tbx-filter-popover-actions", children: [
998
- /* @__PURE__ */ jsxs2(
999
- "button",
1000
- {
1001
- type: "button",
1002
- className: "tbx-filter-popover-btn",
1003
- onClick: (e) => {
1004
- e.stopPropagation();
1005
- setOpenFilterCol(null);
1006
- onQueryChange(withFilter(query, id, void 0));
1007
- },
1008
- children: [
1009
- /* @__PURE__ */ jsx3(RotateCcwIcon, { className: "tbx-icon" }),
1010
- /* @__PURE__ */ jsx3("span", { children: locale.clearFilter })
1011
- ]
1012
- }
1013
- ),
1014
- /* @__PURE__ */ jsxs2(
1015
- "button",
1016
- {
1017
- type: "button",
1018
- className: "tbx-filter-popover-btn tbx-filter-popover-btn--primary",
1019
- onClick: (e) => {
1020
- e.stopPropagation();
1021
- const parent = e.currentTarget.closest(".tbx-filter-popover");
1022
- const inputEl = parent?.querySelector("input");
1023
- const val = inputEl?.value.trim();
1024
- setOpenFilterCol(null);
1025
- onQueryChange(withFilter(query, id, val || void 0));
1026
- },
1027
- children: [
1028
- /* @__PURE__ */ jsx3(CheckIcon, { className: "tbx-icon" }),
1029
- /* @__PURE__ */ jsx3("span", { children: locale.applyFilter })
1030
- ]
1031
- }
1032
- )
1033
- ] })
1034
- ]
1035
- }
1036
- ) : null
1037
- ] }) : null
1038
- ] }),
1039
- enableColumnResize ? /* @__PURE__ */ jsx3("div", { className: "tbx-resize-handle", onPointerDown: startResize }) : null
1040
- ]
1041
- },
1042
- id || `col-${index}`
1043
- );
1044
- })
1045
- ] }) }),
1184
+ ) : null
1185
+ }
1186
+ ) : null,
1187
+ showSerialNumber ? /* @__PURE__ */ jsx3(
1188
+ "th",
1189
+ {
1190
+ rowSpan: headerRows.hasGroups ? 2 : void 0,
1191
+ className: [
1192
+ "tbx-th tbx-th--serial",
1193
+ leftOffsets.has("__serial") ? "tbx-th--pinned-left" : void 0,
1194
+ lastLeftPinnedId === "__serial" ? "tbx-pinned-border-left" : void 0
1195
+ ].filter(Boolean).join(" "),
1196
+ style: {
1197
+ left: leftOffsets.get("__serial")
1198
+ },
1199
+ scope: "col",
1200
+ children: locale.serialHeader
1201
+ }
1202
+ ) : null,
1203
+ !headerRows.hasGroups ? visible.map((col) => renderLeafTh(col, 1, false)) : headerRows.topRow.map((cell, idx) => {
1204
+ if (cell.isGroup) {
1205
+ return /* @__PURE__ */ jsx3(
1206
+ "th",
1207
+ {
1208
+ colSpan: cell.colSpan,
1209
+ className: "tbx-th tbx-th--group",
1210
+ scope: "colgroup",
1211
+ children: /* @__PURE__ */ jsx3("span", { className: "tbx-th-group-title", children: cell.title })
1212
+ },
1213
+ `grp-${cell.id}-${idx}`
1214
+ );
1215
+ }
1216
+ if (cell.leafColumn) {
1217
+ return renderLeafTh(cell.leafColumn, cell.rowSpan, false);
1218
+ }
1219
+ return null;
1220
+ })
1221
+ ] }),
1222
+ headerRows.hasGroups ? /* @__PURE__ */ jsx3("tr", { children: headerRows.bottomRow.map((cell) => {
1223
+ if (cell.leafColumn) {
1224
+ return renderLeafTh(cell.leafColumn, 1, true);
1225
+ }
1226
+ return null;
1227
+ }) }) : null
1228
+ ] }),
1046
1229
  /* @__PURE__ */ jsx3("tbody", { children: isLoading ? /* @__PURE__ */ jsx3("tr", { children: /* @__PURE__ */ jsxs2("td", { className: "tbx-state", colSpan: Math.max(1, columnCount), children: [
1047
1230
  /* @__PURE__ */ jsxs2("div", { className: "tbx-dotted-loader", "aria-hidden": "true", children: [
1048
1231
  /* @__PURE__ */ jsx3("span", { className: "tbx-dot" }),
@@ -1051,7 +1234,7 @@ function TableX(props) {
1051
1234
  /* @__PURE__ */ jsx3("span", { className: "tbx-dot" })
1052
1235
  ] }),
1053
1236
  /* @__PURE__ */ jsx3("div", { className: "tbx-loading-text", "aria-live": "polite", children: locale.loadingText })
1054
- ] }) }) : data.length === 0 ? /* @__PURE__ */ jsx3("tr", { children: /* @__PURE__ */ jsx3("td", { className: "tbx-state", colSpan: Math.max(1, columnCount), children: locale.emptyText }) }) : data.map((row, index) => {
1237
+ ] }) }) : rows.length === 0 ? /* @__PURE__ */ jsx3("tr", { children: /* @__PURE__ */ jsx3("td", { className: "tbx-state", colSpan: Math.max(1, columnCount), children: locale.emptyText }) }) : rows.map((row, index) => {
1055
1238
  const id = pageRowIds[index] ?? String(index);
1056
1239
  const isSelected = selectedIds.has(id);
1057
1240
  const isExpanded = expandedRows.has(id);
@@ -1260,7 +1443,7 @@ function TableX(props) {
1260
1443
  /* @__PURE__ */ jsx3("span", { className: "tbx-dot" })
1261
1444
  ] }),
1262
1445
  /* @__PURE__ */ jsx3("div", { className: "tbx-loading-text", "aria-live": "polite", children: locale.loadingText })
1263
- ] }) }) : data.length === 0 ? /* @__PURE__ */ jsx3("div", { className: "tbx-card", children: /* @__PURE__ */ jsx3("div", { className: "tbx-state", children: locale.emptyText }) }) : data.map((row, index) => {
1446
+ ] }) }) : rows.length === 0 ? /* @__PURE__ */ jsx3("div", { className: "tbx-card", children: /* @__PURE__ */ jsx3("div", { className: "tbx-state", children: locale.emptyText }) }) : rows.map((row, index) => {
1264
1447
  const id = pageRowIds[index] ?? String(index);
1265
1448
  const isSelected = selectedIds.has(id);
1266
1449
  return /* @__PURE__ */ jsxs2(
@@ -1419,16 +1602,16 @@ var NexGrid = TableX;
1419
1602
  // src/use-client-table-x.ts
1420
1603
  import { useMemo as useMemo2, useState as useState4 } from "react";
1421
1604
  import {
1422
- defaultQuery,
1423
- queryClientData
1605
+ defaultQuery as defaultQuery2,
1606
+ queryClientData as queryClientData2
1424
1607
  } from "@nexgrid/core";
1425
1608
  function useClientTableX(allData, options) {
1426
1609
  const [query, setQuery] = useState4(() => ({
1427
- ...defaultQuery(),
1610
+ ...defaultQuery2(),
1428
1611
  ...options?.initialQuery
1429
1612
  }));
1430
1613
  const page = useMemo2(
1431
- () => queryClientData(allData, query, options),
1614
+ () => queryClientData2(allData, query, options),
1432
1615
  [allData, query, options]
1433
1616
  );
1434
1617
  return {
@@ -1446,8 +1629,8 @@ var useClientNexGrid = useClientTableX;
1446
1629
 
1447
1630
  // src/index.ts
1448
1631
  import {
1449
- queryClientData as queryClientData2,
1450
- defaultQuery as defaultQuery2,
1632
+ queryClientData as queryClientData3,
1633
+ defaultQuery as defaultQuery3,
1451
1634
  parseQuery,
1452
1635
  serializeQuery,
1453
1636
  buildQueryUrl as buildQueryUrl2,
@@ -1477,12 +1660,12 @@ export {
1477
1660
  TableX,
1478
1661
  buildODataUrl,
1479
1662
  buildQueryUrl2 as buildQueryUrl,
1480
- defaultQuery2 as defaultQuery,
1663
+ defaultQuery3 as defaultQuery,
1481
1664
  fromODataResponse,
1482
1665
  isPageSize2 as isPageSize,
1483
1666
  parseQuery,
1484
1667
  primarySort2 as primarySort,
1485
- queryClientData2 as queryClientData,
1668
+ queryClientData3 as queryClientData,
1486
1669
  resolveLocale2 as resolveLocale,
1487
1670
  serializeQuery,
1488
1671
  toODataParams,