@alaarab/ogrid-react 2.1.14 → 2.2.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/dist/esm/index.js CHANGED
@@ -507,23 +507,39 @@ function processClientSideData(data, columns, filters, sortBy, sortDirection) {
507
507
  const trimmed = val.value.trim();
508
508
  if (trimmed) {
509
509
  const lower = trimmed.toLowerCase();
510
- predicates.push((r) => String(getCellValue(r, col) ?? "").toLowerCase().includes(lower));
510
+ const textCache = /* @__PURE__ */ new Map();
511
+ for (let j = 0; j < data.length; j++) {
512
+ textCache.set(data[j], String(getCellValue(data[j], col) ?? "").toLowerCase());
513
+ }
514
+ predicates.push((r) => (textCache.get(r) ?? "").includes(lower));
511
515
  }
512
516
  break;
513
517
  }
514
518
  case "people": {
515
519
  const email = val.value.email.toLowerCase();
516
- predicates.push((r) => String(getCellValue(r, col) ?? "").toLowerCase() === email);
520
+ const peopleCache = /* @__PURE__ */ new Map();
521
+ for (let j = 0; j < data.length; j++) {
522
+ peopleCache.set(data[j], String(getCellValue(data[j], col) ?? "").toLowerCase());
523
+ }
524
+ predicates.push((r) => (peopleCache.get(r) ?? "") === email);
517
525
  break;
518
526
  }
519
527
  case "date": {
520
528
  const dv = val.value;
521
529
  const fromTs = dv.from ? (/* @__PURE__ */ new Date(dv.from + "T00:00:00")).getTime() : NaN;
522
530
  const toTs = dv.to ? (/* @__PURE__ */ new Date(dv.to + "T23:59:59.999")).getTime() : NaN;
531
+ const dateCache = /* @__PURE__ */ new Map();
532
+ for (let j = 0; j < data.length; j++) {
533
+ const cellVal = getCellValue(data[j], col);
534
+ if (cellVal == null) {
535
+ dateCache.set(data[j], NaN);
536
+ } else {
537
+ const t = new Date(String(cellVal)).getTime();
538
+ dateCache.set(data[j], Number.isNaN(t) ? NaN : t);
539
+ }
540
+ }
523
541
  predicates.push((r) => {
524
- const cellVal = getCellValue(r, col);
525
- if (cellVal == null) return false;
526
- const cellTs = new Date(String(cellVal)).getTime();
542
+ const cellTs = dateCache.get(r) ?? NaN;
527
543
  if (Number.isNaN(cellTs)) return false;
528
544
  if (!Number.isNaN(fromTs) && cellTs < fromTs) return false;
529
545
  if (!Number.isNaN(toTs) && cellTs > toTs) return false;
@@ -699,6 +715,285 @@ function calculateDropTarget(params) {
699
715
  }
700
716
  return { targetIndex, indicatorX };
701
717
  }
718
+ function computeVisibleColumnRange(scrollLeft, columnWidths, containerWidth, overscan = 2) {
719
+ if (columnWidths.length === 0 || containerWidth <= 0) {
720
+ return { startIndex: 0, endIndex: -1, leftOffset: 0, rightOffset: 0 };
721
+ }
722
+ let cumWidth = 0;
723
+ let rawStart = columnWidths.length;
724
+ let rawEnd = -1;
725
+ for (let i = 0; i < columnWidths.length; i++) {
726
+ const colStart = cumWidth;
727
+ cumWidth += columnWidths[i];
728
+ if (cumWidth > scrollLeft && rawStart === columnWidths.length) {
729
+ rawStart = i;
730
+ }
731
+ if (colStart < scrollLeft + containerWidth) {
732
+ rawEnd = i;
733
+ }
734
+ }
735
+ if (rawStart > rawEnd) {
736
+ return { startIndex: 0, endIndex: -1, leftOffset: 0, rightOffset: 0 };
737
+ }
738
+ const startIndex = Math.max(0, rawStart - overscan);
739
+ const endIndex = Math.min(columnWidths.length - 1, rawEnd + overscan);
740
+ let leftOffset = 0;
741
+ for (let i = 0; i < startIndex; i++) {
742
+ leftOffset += columnWidths[i];
743
+ }
744
+ let rightOffset = 0;
745
+ for (let i = endIndex + 1; i < columnWidths.length; i++) {
746
+ rightOffset += columnWidths[i];
747
+ }
748
+ return { startIndex, endIndex, leftOffset, rightOffset };
749
+ }
750
+ function partitionColumnsForVirtualization(visibleCols, columnRange, pinnedColumns) {
751
+ const pinnedLeft = [];
752
+ const pinnedRight = [];
753
+ const unpinned = [];
754
+ for (const col of visibleCols) {
755
+ const pin = pinnedColumns?.[col.columnId];
756
+ if (pin === "left") pinnedLeft.push(col);
757
+ else if (pin === "right") pinnedRight.push(col);
758
+ else unpinned.push(col);
759
+ }
760
+ if (!columnRange || columnRange.endIndex < 0) {
761
+ return {
762
+ pinnedLeft,
763
+ virtualizedUnpinned: unpinned,
764
+ pinnedRight,
765
+ leftSpacerWidth: 0,
766
+ rightSpacerWidth: 0
767
+ };
768
+ }
769
+ const virtualizedUnpinned = unpinned.slice(columnRange.startIndex, columnRange.endIndex + 1);
770
+ return {
771
+ pinnedLeft,
772
+ virtualizedUnpinned,
773
+ pinnedRight,
774
+ leftSpacerWidth: columnRange.leftOffset,
775
+ rightSpacerWidth: columnRange.rightOffset
776
+ };
777
+ }
778
+ function workerBody() {
779
+ const ctx = self;
780
+ ctx.onmessage = (e) => {
781
+ const msg = e.data;
782
+ if (msg.type !== "sort-filter") return;
783
+ const { requestId, values, filters, sort } = msg;
784
+ const rowCount = values.length;
785
+ let indices = [];
786
+ const filterEntries = Object.entries(filters);
787
+ if (filterEntries.length === 0) {
788
+ indices = new Array(rowCount);
789
+ for (let i = 0; i < rowCount; i++) indices[i] = i;
790
+ } else {
791
+ for (let r = 0; r < rowCount; r++) {
792
+ let pass = true;
793
+ for (let f = 0; f < filterEntries.length; f++) {
794
+ const colIdx = Number(filterEntries[f][0]);
795
+ const filter = filterEntries[f][1];
796
+ const cellVal = values[r][colIdx];
797
+ switch (filter.type) {
798
+ case "text": {
799
+ const trimmed = filter.value.trim().toLowerCase();
800
+ if (trimmed && !String(cellVal ?? "").toLowerCase().includes(trimmed)) {
801
+ pass = false;
802
+ }
803
+ break;
804
+ }
805
+ case "multiSelect": {
806
+ if (filter.value.length > 0) {
807
+ const set = new Set(filter.value);
808
+ if (!set.has(String(cellVal ?? ""))) {
809
+ pass = false;
810
+ }
811
+ }
812
+ break;
813
+ }
814
+ case "date": {
815
+ if (cellVal == null) {
816
+ pass = false;
817
+ break;
818
+ }
819
+ const ts = new Date(String(cellVal)).getTime();
820
+ if (isNaN(ts)) {
821
+ pass = false;
822
+ break;
823
+ }
824
+ if (filter.value.from) {
825
+ const fromTs = (/* @__PURE__ */ new Date(filter.value.from + "T00:00:00")).getTime();
826
+ if (ts < fromTs) {
827
+ pass = false;
828
+ break;
829
+ }
830
+ }
831
+ if (filter.value.to) {
832
+ const toTs = (/* @__PURE__ */ new Date(filter.value.to + "T23:59:59.999")).getTime();
833
+ if (ts > toTs) {
834
+ pass = false;
835
+ break;
836
+ }
837
+ }
838
+ break;
839
+ }
840
+ }
841
+ if (!pass) break;
842
+ }
843
+ if (pass) indices.push(r);
844
+ }
845
+ }
846
+ if (sort) {
847
+ const { columnIndex, direction } = sort;
848
+ const dir = direction === "asc" ? 1 : -1;
849
+ indices.sort((a, b) => {
850
+ const av = values[a][columnIndex];
851
+ const bv = values[b][columnIndex];
852
+ if (av == null && bv == null) return 0;
853
+ if (av == null) return -1 * dir;
854
+ if (bv == null) return 1 * dir;
855
+ if (typeof av === "number" && typeof bv === "number") {
856
+ return av === bv ? 0 : av > bv ? dir : -dir;
857
+ }
858
+ const sa = String(av).toLowerCase();
859
+ const sb = String(bv).toLowerCase();
860
+ return sa === sb ? 0 : sa > sb ? dir : -dir;
861
+ });
862
+ }
863
+ const response = {
864
+ type: "sort-filter-result",
865
+ requestId,
866
+ indices
867
+ };
868
+ ctx.postMessage(response);
869
+ };
870
+ }
871
+ var workerInstance = null;
872
+ var requestCounter = 0;
873
+ var pendingRequests = /* @__PURE__ */ new Map();
874
+ function createSortFilterWorker() {
875
+ if (workerInstance) return workerInstance;
876
+ if (typeof Worker === "undefined" || typeof Blob === "undefined" || typeof URL === "undefined") {
877
+ return null;
878
+ }
879
+ try {
880
+ const fnStr = workerBody.toString();
881
+ const blob = new Blob(
882
+ [`(${fnStr})()`],
883
+ { type: "application/javascript" }
884
+ );
885
+ const url = URL.createObjectURL(blob);
886
+ workerInstance = new Worker(url);
887
+ URL.revokeObjectURL(url);
888
+ workerInstance.onmessage = (e) => {
889
+ const { requestId, indices } = e.data;
890
+ const pending = pendingRequests.get(requestId);
891
+ if (pending) {
892
+ pendingRequests.delete(requestId);
893
+ pending.resolve(indices);
894
+ }
895
+ };
896
+ workerInstance.onerror = (err) => {
897
+ for (const [id, pending] of pendingRequests) {
898
+ pending.reject(new Error(err.message || "Worker error"));
899
+ pendingRequests.delete(id);
900
+ }
901
+ };
902
+ return workerInstance;
903
+ } catch {
904
+ return null;
905
+ }
906
+ }
907
+ function extractValueMatrix(data, columns) {
908
+ const matrix = new Array(data.length);
909
+ for (let r = 0; r < data.length; r++) {
910
+ const row = new Array(columns.length);
911
+ for (let c = 0; c < columns.length; c++) {
912
+ const val = getCellValue(data[r], columns[c]);
913
+ if (val == null) {
914
+ row[c] = null;
915
+ } else if (typeof val === "string" || typeof val === "number" || typeof val === "boolean") {
916
+ row[c] = val;
917
+ } else {
918
+ row[c] = String(val);
919
+ }
920
+ }
921
+ matrix[r] = row;
922
+ }
923
+ return matrix;
924
+ }
925
+ function processClientSideDataAsync(data, columns, filters, sortBy, sortDirection) {
926
+ if (sortBy) {
927
+ const sortCol = columns.find((c) => c.columnId === sortBy);
928
+ if (sortCol?.compare) {
929
+ return Promise.resolve(processClientSideData(data, columns, filters, sortBy, sortDirection));
930
+ }
931
+ }
932
+ const worker = createSortFilterWorker();
933
+ if (!worker) {
934
+ return Promise.resolve(processClientSideData(data, columns, filters, sortBy, sortDirection));
935
+ }
936
+ const columnIndexMap = /* @__PURE__ */ new Map();
937
+ for (let i = 0; i < columns.length; i++) {
938
+ columnIndexMap.set(columns[i].columnId, i);
939
+ }
940
+ const values = extractValueMatrix(data, columns);
941
+ const columnMeta = columns.map((col, idx) => ({
942
+ type: col.type ?? "text",
943
+ index: idx
944
+ }));
945
+ const workerFilters = {};
946
+ for (const col of columns) {
947
+ const filterKey = getFilterField(col);
948
+ const val = filters[filterKey];
949
+ if (!val) continue;
950
+ const colIdx = columnIndexMap.get(col.columnId);
951
+ if (colIdx === void 0) continue;
952
+ switch (val.type) {
953
+ case "text":
954
+ workerFilters[colIdx] = { type: "text", value: val.value };
955
+ break;
956
+ case "multiSelect":
957
+ workerFilters[colIdx] = { type: "multiSelect", value: val.value };
958
+ break;
959
+ case "date":
960
+ workerFilters[colIdx] = { type: "date", value: { from: val.value.from, to: val.value.to } };
961
+ break;
962
+ // 'people' filter has a UserLike object — fall back to sync
963
+ case "people":
964
+ return Promise.resolve(processClientSideData(data, columns, filters, sortBy, sortDirection));
965
+ }
966
+ }
967
+ let sort;
968
+ if (sortBy) {
969
+ const sortColIdx = columnIndexMap.get(sortBy);
970
+ if (sortColIdx !== void 0) {
971
+ sort = { columnIndex: sortColIdx, direction: sortDirection ?? "asc" };
972
+ }
973
+ }
974
+ const requestId = ++requestCounter;
975
+ return new Promise((resolve, reject) => {
976
+ pendingRequests.set(requestId, {
977
+ resolve: (indices) => {
978
+ const result = new Array(indices.length);
979
+ for (let i = 0; i < indices.length; i++) {
980
+ result[i] = data[indices[i]];
981
+ }
982
+ resolve(result);
983
+ },
984
+ reject
985
+ });
986
+ const request = {
987
+ type: "sort-filter",
988
+ requestId,
989
+ values,
990
+ columnMeta,
991
+ filters: workerFilters,
992
+ sort
993
+ };
994
+ worker.postMessage(request);
995
+ });
996
+ }
702
997
  function getHeaderFilterConfig(col, input) {
703
998
  const filterable = isFilterConfig(col.filterable) ? col.filterable : null;
704
999
  const filterType = filterable?.type ?? "none";
@@ -1220,7 +1515,7 @@ function computeRowSelectionState(selectedIds, items, getRowId) {
1220
1515
  if (selectedIds.size === 0 || items.length === 0) {
1221
1516
  return { allSelected: false, someSelected: false };
1222
1517
  }
1223
- const allSelected = items.every((item) => selectedIds.has(getRowId(item)));
1518
+ const allSelected = selectedIds.size >= items.length && items.every((item) => selectedIds.has(getRowId(item)));
1224
1519
  const someSelected = !allSelected && selectedIds.size > 0;
1225
1520
  return { allSelected, someSelected };
1226
1521
  }
@@ -1541,7 +1836,7 @@ function useFilterOptions(dataSource, fields) {
1541
1836
  );
1542
1837
  setFilterOptions(results);
1543
1838
  setLoadingOptions(EMPTY_LOADING);
1544
- }, [stableFields]);
1839
+ }, [stableFields, dataSourceRef]);
1545
1840
  useEffect(() => {
1546
1841
  load().catch(() => {
1547
1842
  });
@@ -1703,11 +1998,13 @@ function useOGridDataFetching(params) {
1703
1998
  page,
1704
1999
  pageSize,
1705
2000
  onError,
1706
- onFirstDataRendered
2001
+ onFirstDataRendered,
2002
+ workerSort
1707
2003
  } = params;
1708
2004
  const isClientSide = !isServerSide;
2005
+ const useWorker = workerSort === true || workerSort === "auto" && displayData.length > 5e3;
1709
2006
  const clientItemsAndTotal = useMemo(() => {
1710
- if (!isClientSide) return null;
2007
+ if (!isClientSide || useWorker) return null;
1711
2008
  const rows = processClientSideData(
1712
2009
  displayData,
1713
2010
  columns,
@@ -1719,7 +2016,29 @@ function useOGridDataFetching(params) {
1719
2016
  const start = (page - 1) * pageSize;
1720
2017
  const paged = rows.slice(start, start + pageSize);
1721
2018
  return { items: paged, totalCount: total };
1722
- }, [isClientSide, displayData, columns, stableFilters, sort.field, sort.direction, page, pageSize]);
2019
+ }, [isClientSide, useWorker, displayData, columns, stableFilters, sort.field, sort.direction, page, pageSize]);
2020
+ const [asyncItems, setAsyncItems] = useState(null);
2021
+ const asyncIdRef = useRef(0);
2022
+ useEffect(() => {
2023
+ if (!isClientSide || !useWorker) {
2024
+ setAsyncItems(null);
2025
+ return;
2026
+ }
2027
+ const id = ++asyncIdRef.current;
2028
+ processClientSideDataAsync(
2029
+ displayData,
2030
+ columns,
2031
+ stableFilters,
2032
+ sort.field,
2033
+ sort.direction
2034
+ ).then((rows) => {
2035
+ if (id !== asyncIdRef.current) return;
2036
+ const total = rows.length;
2037
+ const start = (page - 1) * pageSize;
2038
+ const paged = rows.slice(start, start + pageSize);
2039
+ setAsyncItems({ items: paged, totalCount: total });
2040
+ });
2041
+ }, [isClientSide, useWorker, displayData, columns, stableFilters, sort.field, sort.direction, page, pageSize]);
1723
2042
  const [serverItems, setServerItems] = useState([]);
1724
2043
  const [serverTotalCount, setServerTotalCount] = useState(0);
1725
2044
  const [serverLoading, setServerLoading] = useState(true);
@@ -1752,9 +2071,10 @@ function useOGridDataFetching(params) {
1752
2071
  }).finally(() => {
1753
2072
  if (id === fetchIdRef.current) setServerLoading(false);
1754
2073
  });
1755
- }, [isServerSide, page, pageSize, sort.field, sort.direction, stableFilters, refreshCounter]);
1756
- const displayItems = isClientSide && clientItemsAndTotal ? clientItemsAndTotal.items : serverItems;
1757
- const displayTotalCount = isClientSide && clientItemsAndTotal ? clientItemsAndTotal.totalCount : serverTotalCount;
2074
+ }, [isServerSide, page, pageSize, sort.field, sort.direction, stableFilters, refreshCounter, dataSourceRef, onErrorRef]);
2075
+ const clientResult = clientItemsAndTotal ?? asyncItems;
2076
+ const displayItems = isClientSide && clientResult ? clientResult.items : serverItems;
2077
+ const displayTotalCount = isClientSide && clientResult ? clientResult.totalCount : serverTotalCount;
1758
2078
  const onFirstDataRenderedRef = useLatestRef(onFirstDataRendered);
1759
2079
  const firstDataRenderedRef = useRef(false);
1760
2080
  useEffect(() => {
@@ -1762,7 +2082,7 @@ function useOGridDataFetching(params) {
1762
2082
  firstDataRenderedRef.current = true;
1763
2083
  onFirstDataRenderedRef.current?.();
1764
2084
  }
1765
- }, [displayItems.length]);
2085
+ }, [displayItems.length, onFirstDataRenderedRef]);
1766
2086
  return {
1767
2087
  displayItems,
1768
2088
  displayTotalCount,
@@ -1861,34 +2181,35 @@ function useOGrid(props, ref) {
1861
2181
  virtualScroll,
1862
2182
  rowHeight,
1863
2183
  density = "normal",
2184
+ workerSort,
1864
2185
  "aria-label": ariaLabel,
1865
2186
  "aria-labelledby": ariaLabelledBy
1866
2187
  } = props;
1867
2188
  const getRowIdStableRef = useLatestRef(getRowIdProp);
1868
2189
  const getRowId = useCallback((item) => getRowIdStableRef.current(item), [getRowIdStableRef]);
1869
2190
  const onColumnOrderChangeRef = useLatestRef(onColumnOrderChangeProp);
2191
+ const hasColumnOrderChange = onColumnOrderChangeProp != null;
1870
2192
  const onColumnOrderChange = useMemo(
1871
- () => onColumnOrderChangeProp ? (order) => onColumnOrderChangeRef.current?.(order) : void 0,
1872
- // eslint-disable-next-line react-hooks/exhaustive-deps
1873
- [!!onColumnOrderChangeProp]
2193
+ () => hasColumnOrderChange ? (order) => onColumnOrderChangeRef.current?.(order) : void 0,
2194
+ [hasColumnOrderChange, onColumnOrderChangeRef]
1874
2195
  );
1875
2196
  const onCellValueChangedRef = useLatestRef(onCellValueChangedProp);
2197
+ const hasCellValueChanged = onCellValueChangedProp != null;
1876
2198
  const onCellValueChanged = useMemo(
1877
- () => onCellValueChangedProp ? (event) => onCellValueChangedRef.current?.(event) : void 0,
1878
- // eslint-disable-next-line react-hooks/exhaustive-deps
1879
- [!!onCellValueChangedProp]
2199
+ () => hasCellValueChanged ? (event) => onCellValueChangedRef.current?.(event) : void 0,
2200
+ [hasCellValueChanged, onCellValueChangedRef]
1880
2201
  );
1881
2202
  const onUndoRef = useLatestRef(onUndoProp);
2203
+ const hasUndo = onUndoProp != null;
1882
2204
  const onUndo = useMemo(
1883
- () => onUndoProp ? () => onUndoRef.current?.() : void 0,
1884
- // eslint-disable-next-line react-hooks/exhaustive-deps
1885
- [!!onUndoProp]
2205
+ () => hasUndo ? () => onUndoRef.current?.() : void 0,
2206
+ [hasUndo, onUndoRef]
1886
2207
  );
1887
2208
  const onRedoRef = useLatestRef(onRedoProp);
2209
+ const hasRedo = onRedoProp != null;
1888
2210
  const onRedo = useMemo(
1889
- () => onRedoProp ? () => onRedoRef.current?.() : void 0,
1890
- // eslint-disable-next-line react-hooks/exhaustive-deps
1891
- [!!onRedoProp]
2211
+ () => hasRedo ? () => onRedoRef.current?.() : void 0,
2212
+ [hasRedo, onRedoRef]
1892
2213
  );
1893
2214
  const columnChooserPlacement = columnChooserProp === false ? "none" : columnChooserProp === "sidebar" ? "sidebar" : "toolbar";
1894
2215
  const columns = useMemo(() => flattenColumns(columnsProp), [columnsProp]);
@@ -1896,7 +2217,7 @@ function useOGrid(props, ref) {
1896
2217
  const rowIdsValidatedRef = useRef(false);
1897
2218
  useEffect(() => {
1898
2219
  validateColumns(columns);
1899
- }, []);
2220
+ }, [columns]);
1900
2221
  const defaultSortField = defaultSortBy ?? columns[0]?.columnId ?? "";
1901
2222
  const [internalData, setInternalData] = useState([]);
1902
2223
  const [internalLoading, setInternalLoading] = useState(false);
@@ -1934,7 +2255,8 @@ function useOGrid(props, ref) {
1934
2255
  page: paginationState.page,
1935
2256
  pageSize: paginationState.pageSize,
1936
2257
  onError,
1937
- onFirstDataRendered
2258
+ onFirstDataRendered,
2259
+ workerSort
1938
2260
  });
1939
2261
  useEffect(() => {
1940
2262
  const items = dataFetchingState.displayItems;
@@ -1992,7 +2314,13 @@ function useOGrid(props, ref) {
1992
2314
  [selectedRows, onSelectionChange]
1993
2315
  );
1994
2316
  const [columnWidthOverrides, setColumnWidthOverrides] = useState({});
1995
- const [pinnedOverrides, setPinnedOverrides] = useState({});
2317
+ const [pinnedOverrides, setPinnedOverrides] = useState(() => {
2318
+ const initial = {};
2319
+ for (const col of flattenColumns(columnsProp)) {
2320
+ if (col.pinned) initial[col.columnId] = col.pinned;
2321
+ }
2322
+ return initial;
2323
+ });
1996
2324
  const handleColumnResized = useCallback(
1997
2325
  (columnId, width) => {
1998
2326
  setColumnWidthOverrides((prev) => ({ ...prev, [columnId]: width }));
@@ -2518,15 +2846,17 @@ function useCellSelection(params) {
2518
2846
  }
2519
2847
  }
2520
2848
  if (!cellIndex) cellIndex = buildCellIndex(wrapperRef.current);
2849
+ let rebuilt = false;
2521
2850
  for (let r = minR; r <= maxR; r++) {
2522
2851
  for (let c = minC; c <= maxC; c++) {
2523
2852
  const key = `${r},${c + colOff}`;
2524
2853
  let el = cellIndex?.get(key);
2525
- if (el && !el.isConnected) {
2854
+ if (el && !el.isConnected && !rebuilt) {
2855
+ rebuilt = true;
2526
2856
  cellIndex = buildCellIndex(wrapperRef.current);
2527
2857
  el = cellIndex?.get(key);
2528
2858
  }
2529
- if (el) {
2859
+ if (el && el.isConnected) {
2530
2860
  styleCellInRange(el, r, c, minR, maxR, minC, maxC, anchor);
2531
2861
  }
2532
2862
  }
@@ -2661,10 +2991,13 @@ function useCellSelection(params) {
2661
2991
  const finalRange = liveDragRangeRef.current;
2662
2992
  if (finalRange) {
2663
2993
  setSelectionRange(finalRange);
2664
- setActiveCell({
2665
- rowIndex: finalRange.endRow,
2666
- columnIndex: finalRange.endCol + colOffsetRef.current
2667
- });
2994
+ const anchor = dragStartRef.current;
2995
+ if (anchor) {
2996
+ setActiveCell({
2997
+ rowIndex: anchor.row,
2998
+ columnIndex: anchor.col + colOffsetRef.current
2999
+ });
3000
+ }
2668
3001
  }
2669
3002
  }
2670
3003
  clearDragAttrs();
@@ -3129,8 +3462,7 @@ function useUndoRedo(params) {
3129
3462
  }
3130
3463
  onCellValueChangedRef.current(event);
3131
3464
  },
3132
- // eslint-disable-next-line react-hooks/exhaustive-deps
3133
- [getStack]
3465
+ [getStack, onCellValueChangedRef]
3134
3466
  );
3135
3467
  const beginBatch = useCallback(() => {
3136
3468
  getStack().beginBatch();
@@ -3156,7 +3488,7 @@ function useUndoRedo(params) {
3156
3488
  newValue: ev.oldValue
3157
3489
  });
3158
3490
  }
3159
- }, [getStack]);
3491
+ }, [getStack, onCellValueChangedRef]);
3160
3492
  const redo = useCallback(() => {
3161
3493
  if (!onCellValueChangedRef.current) return;
3162
3494
  const stack = getStack();
@@ -3167,7 +3499,7 @@ function useUndoRedo(params) {
3167
3499
  for (const ev of nextBatch) {
3168
3500
  onCellValueChangedRef.current(ev);
3169
3501
  }
3170
- }, [getStack]);
3502
+ }, [getStack, onCellValueChangedRef]);
3171
3503
  return {
3172
3504
  onCellValueChanged: onCellValueChanged ? wrapped : void 0,
3173
3505
  undo,
@@ -3307,7 +3639,7 @@ function useFillHandle(params) {
3307
3639
  endCol: end.endCol
3308
3640
  });
3309
3641
  setSelectionRange(norm);
3310
- setActiveCell({ rowIndex: end.endRow, columnIndex: end.endCol + colOffsetRef.current });
3642
+ setActiveCell({ rowIndex: fillDrag.startRow, columnIndex: fillDrag.startCol + colOffsetRef.current });
3311
3643
  const fillEvents = applyFillValues(norm, fillDrag.startRow, fillDrag.startCol, items, visibleCols);
3312
3644
  if (fillEvents.length > 0) {
3313
3645
  beginBatch?.();
@@ -3334,7 +3666,8 @@ function useFillHandle(params) {
3334
3666
  beginBatch,
3335
3667
  endBatch,
3336
3668
  colOffsetRef,
3337
- wrapperRef
3669
+ wrapperRef,
3670
+ onCellValueChangedRef
3338
3671
  ]);
3339
3672
  const selectionRangeRef = useRef(selectionRange);
3340
3673
  selectionRangeRef.current = selectionRange;
@@ -3369,7 +3702,7 @@ function useFillHandle(params) {
3369
3702
  for (const evt of fillEvents) onCellValueChangedRef.current(evt);
3370
3703
  endBatch?.();
3371
3704
  }
3372
- }, [editable, beginBatch, endBatch]);
3705
+ }, [editable, beginBatch, endBatch, onCellValueChangedRef, itemsRef, visibleColsRef]);
3373
3706
  return { fillDrag, setFillDrag, handleFillHandleMouseDown, fillDown };
3374
3707
  }
3375
3708
  function useTableLayout(params) {
@@ -3869,8 +4202,7 @@ function useDataGridEditing(params) {
3869
4202
  setSelectionRange({ startRow: newRow, startCol: localCol, endRow: newRow, endCol: localCol });
3870
4203
  }
3871
4204
  },
3872
- // eslint-disable-next-line react-hooks/exhaustive-deps
3873
- [setEditingCell, setPendingEditorValue, setActiveCell, setSelectionRange, colOffset, visibleColsRef, itemsLengthRef]
4205
+ [setEditingCell, setPendingEditorValue, setActiveCell, setSelectionRange, colOffset, visibleColsRef, itemsLengthRef, onCellValueChangedRef]
3874
4206
  );
3875
4207
  const cancelPopoverEdit = useCallback(() => {
3876
4208
  setEditingCell(null);
@@ -6003,11 +6335,14 @@ function useVirtualScroll(params) {
6003
6335
  enabled,
6004
6336
  overscan = 5,
6005
6337
  threshold = DEFAULT_PASSTHROUGH_THRESHOLD,
6006
- containerRef
6338
+ containerRef,
6339
+ columnVirtualization = false,
6340
+ columnWidths,
6341
+ columnOverscan = 2
6007
6342
  } = params;
6008
6343
  useEffect(() => {
6009
6344
  validateVirtualScrollConfig({ enabled, rowHeight });
6010
- }, []);
6345
+ }, [enabled, rowHeight]);
6011
6346
  const isActive = enabled && totalRows >= threshold;
6012
6347
  const getScrollElement = useCallback(
6013
6348
  () => containerRef.current,
@@ -6062,11 +6397,51 @@ function useVirtualScroll(params) {
6062
6397
  },
6063
6398
  [isActive, containerRef, rowHeight]
6064
6399
  );
6400
+ const [scrollLeft, setScrollLeft] = useState(0);
6401
+ const scrollLeftRaf = useRef(0);
6402
+ const onHorizontalScroll = useCallback(
6403
+ (sl) => {
6404
+ if (scrollLeftRaf.current) cancelAnimationFrame(scrollLeftRaf.current);
6405
+ scrollLeftRaf.current = requestAnimationFrame(() => {
6406
+ scrollLeftRaf.current = 0;
6407
+ setScrollLeft(sl);
6408
+ });
6409
+ },
6410
+ []
6411
+ );
6412
+ useEffect(() => {
6413
+ return () => {
6414
+ if (scrollLeftRaf.current) cancelAnimationFrame(scrollLeftRaf.current);
6415
+ };
6416
+ }, []);
6417
+ const [containerWidth, setContainerWidth] = useState(0);
6418
+ useEffect(() => {
6419
+ if (!columnVirtualization) return;
6420
+ const el = containerRef.current;
6421
+ if (!el) return;
6422
+ setContainerWidth(el.clientWidth);
6423
+ if (typeof ResizeObserver === "undefined") return;
6424
+ const ro = new ResizeObserver((entries) => {
6425
+ if (entries.length > 0) {
6426
+ setContainerWidth(entries[0].contentRect.width);
6427
+ }
6428
+ });
6429
+ ro.observe(el);
6430
+ return () => ro.disconnect();
6431
+ }, [columnVirtualization, containerRef]);
6432
+ const columnRange = useMemo(() => {
6433
+ if (!columnVirtualization || !columnWidths || columnWidths.length === 0 || containerWidth <= 0) {
6434
+ return null;
6435
+ }
6436
+ return computeVisibleColumnRange(scrollLeft, columnWidths, containerWidth, columnOverscan);
6437
+ }, [columnVirtualization, columnWidths, containerWidth, scrollLeft, columnOverscan]);
6065
6438
  return {
6066
6439
  virtualizer: isActive ? virtualizer : null,
6067
6440
  totalHeight,
6068
6441
  visibleRange: activeRange,
6069
- scrollToIndex
6442
+ scrollToIndex,
6443
+ columnRange,
6444
+ onHorizontalScroll: columnVirtualization ? onHorizontalScroll : void 0
6070
6445
  };
6071
6446
  }
6072
6447
  function useListVirtualizer(opts) {
@@ -6177,13 +6552,28 @@ function useDataGridTableOrchestration(params) {
6177
6552
  });
6178
6553
  const virtualScrollEnabled = virtualScroll?.enabled === true;
6179
6554
  const virtualRowHeight = virtualScroll?.rowHeight ?? 36;
6180
- const { visibleRange } = useVirtualScroll({
6555
+ const columnVirtualization = virtualScroll?.columns === true;
6556
+ const unpinnedColumnWidths = useMemo(() => {
6557
+ if (!columnVirtualization) return void 0;
6558
+ const widths = [];
6559
+ for (const col of visibleCols) {
6560
+ const pin = pinnedColumns?.[col.columnId];
6561
+ if (!pin) {
6562
+ widths.push(getColumnWidth(col));
6563
+ }
6564
+ }
6565
+ return widths;
6566
+ }, [columnVirtualization, visibleCols, pinnedColumns, getColumnWidth]);
6567
+ const { visibleRange, columnRange, onHorizontalScroll } = useVirtualScroll({
6181
6568
  totalRows: items.length,
6182
6569
  rowHeight: virtualRowHeight,
6183
6570
  enabled: virtualScrollEnabled,
6184
6571
  overscan: virtualScroll?.overscan,
6185
6572
  threshold: virtualScroll?.threshold,
6186
- containerRef: wrapperRef
6573
+ containerRef: wrapperRef,
6574
+ columnVirtualization,
6575
+ columnWidths: unpinnedColumnWidths,
6576
+ columnOverscan: virtualScroll?.columnOverscan
6187
6577
  });
6188
6578
  const editCallbacks = useMemo(
6189
6579
  () => ({ commitCellEdit, setEditingCell, setPendingEditorValue, cancelPopoverEdit }),
@@ -6201,8 +6591,10 @@ function useDataGridTableOrchestration(params) {
6201
6591
  const currentVersion = CellDescriptorCache.computeVersion(cellDescriptorInput);
6202
6592
  cellDescriptorCacheRef.current.updateVersion(currentVersion);
6203
6593
  const prevItemsRef = useRef(items);
6204
- if (prevItemsRef.current !== items) {
6594
+ const prevVisibleColsRef = useRef(visibleCols);
6595
+ if (prevItemsRef.current !== items || prevVisibleColsRef.current !== visibleCols) {
6205
6596
  prevItemsRef.current = items;
6597
+ prevVisibleColsRef.current = visibleCols;
6206
6598
  cellDescriptorCacheRef.current.clear();
6207
6599
  }
6208
6600
  const handleSingleRowClick = useCallback((e) => {
@@ -6237,6 +6629,8 @@ function useDataGridTableOrchestration(params) {
6237
6629
  virtualScrollEnabled,
6238
6630
  virtualRowHeight,
6239
6631
  visibleRange,
6632
+ columnRange,
6633
+ onHorizontalScroll,
6240
6634
  // Derived from props
6241
6635
  items,
6242
6636
  columns,
@@ -7137,7 +7531,7 @@ function MarchingAntsOverlay({
7137
7531
  const selR = selRect ? roundRect(selRect) : null;
7138
7532
  const clipR = clipRect ? roundRect(clipRect) : null;
7139
7533
  return /* @__PURE__ */ jsxs(Fragment, { children: [
7140
- selR && !isDragging && !clipRangeMatchesSel && /* @__PURE__ */ jsx(
7534
+ selR && !isDragging && !clipRangeMatchesSel && !(selectionRange && selectionRange.startRow === selectionRange.endRow && selectionRange.startCol === selectionRange.endCol) && /* @__PURE__ */ jsx(
7141
7535
  "svg",
7142
7536
  {
7143
7537
  style: {
@@ -7539,4 +7933,4 @@ function renderFilterContent(filterType, state, options, isLoadingOptions, selec
7539
7933
  return null;
7540
7934
  }
7541
7935
 
7542
- export { BaseColumnHeaderMenu, BaseDropIndicator, BaseEmptyState, BaseInlineCellEditor, BaseLoadingOverlay, CELL_PADDING, CHECKBOX_COLUMN_WIDTH, COLUMN_HEADER_MENU_ITEMS, CURSOR_CELL_STYLE, CellDescriptorCache, CellErrorBoundary, DEFAULT_MIN_COLUMN_WIDTH, DateFilterContent, EmptyState, GRID_BORDER_RADIUS, GRID_CONTEXT_MENU_ITEMS, GRID_ROOT_STYLE, GridContextMenu, MAX_PAGE_BUTTONS, MarchingAntsOverlay, NOOP3 as NOOP, OGridLayout, PAGE_SIZE_OPTIONS, POPOVER_ANCHOR_STYLE, PREVENT_DEFAULT, ROW_NUMBER_COLUMN_WIDTH, STOP_PROPAGATION, SideBar, StatusBar, UndoRedoStack, areGridRowPropsEqual, booleanParser, buildCsvHeader, buildCsvRows, buildHeaderRows, buildInlineEditorProps2 as buildInlineEditorProps, buildPopoverEditorProps2 as buildPopoverEditorProps, clampSelectionToBounds, computeAggregations, computeAutoScrollSpeed, computeTabNavigation, createOGrid, currencyParser, dateParser, deriveFilterOptionsFromData, editorInputStyle, editorWrapperStyle, emailParser, escapeCsvValue, exportToCsv, findCtrlArrowTarget, flattenColumns, formatCellValueForTsv, formatSelectionAsTsv, formatShortcut, getCellInteractionProps, getCellRenderDescriptor, getCellValue, getColumnHeaderFilterStateParams, getColumnHeaderMenuItems, getContextMenuHandlers, getDataGridStatusBarConfig, getDateFilterContentProps, getFilterField, getHeaderFilterConfig, getMultiSelectFilterFields, getPaginationViewModel, getStatusBarParts, isInSelectionRange, isRowInRange, mergeFilter, normalizeSelectionRange, numberParser, parseTsvClipboard, parseValue, processClientSideData, rangesEqual, renderFilterContent, resolveCellDisplayContent2 as resolveCellDisplayContent, resolveCellStyle2 as resolveCellStyle, richSelectDropdownStyle, richSelectNoMatchesStyle, richSelectOptionHighlightedStyle, richSelectOptionStyle, richSelectWrapperStyle, selectChevronStyle, selectDisplayStyle, selectEditorStyle, toUserLike, triggerCsvDownload, useActiveCell, useCellEditing, useCellSelection, useClipboard, useColumnChooserState, useColumnHeaderFilterState, useColumnMeta, useColumnReorder, useColumnResize, useContextMenu, useDataGridState, useDataGridTableOrchestration, useDateFilterState, useDebounce, useFillHandle, useFilterOptions, useInlineCellEditorState, useKeyboardNavigation, useLatestRef, useListVirtualizer, useMultiSelectFilterState, useOGrid, usePaginationControls, usePeopleFilterState, useRichSelectState, useRowSelection, useSelectState, useSideBarState, useTableLayout, useTextFilterState, useUndoRedo, useVirtualScroll };
7936
+ export { BaseColumnHeaderMenu, BaseDropIndicator, BaseEmptyState, BaseInlineCellEditor, BaseLoadingOverlay, CELL_PADDING, CHECKBOX_COLUMN_WIDTH, COLUMN_HEADER_MENU_ITEMS, CURSOR_CELL_STYLE, CellDescriptorCache, CellErrorBoundary, DEFAULT_MIN_COLUMN_WIDTH, DateFilterContent, EmptyState, GRID_BORDER_RADIUS, GRID_CONTEXT_MENU_ITEMS, GRID_ROOT_STYLE, GridContextMenu, MAX_PAGE_BUTTONS, MarchingAntsOverlay, NOOP3 as NOOP, OGridLayout, PAGE_SIZE_OPTIONS, POPOVER_ANCHOR_STYLE, PREVENT_DEFAULT, ROW_NUMBER_COLUMN_WIDTH, STOP_PROPAGATION, SideBar, StatusBar, UndoRedoStack, areGridRowPropsEqual, booleanParser, buildCsvHeader, buildCsvRows, buildHeaderRows, buildInlineEditorProps2 as buildInlineEditorProps, buildPopoverEditorProps2 as buildPopoverEditorProps, clampSelectionToBounds, computeAggregations, computeAutoScrollSpeed, computeTabNavigation, createOGrid, currencyParser, dateParser, deriveFilterOptionsFromData, editorInputStyle, editorWrapperStyle, emailParser, escapeCsvValue, exportToCsv, findCtrlArrowTarget, flattenColumns, formatCellValueForTsv, formatSelectionAsTsv, formatShortcut, getCellInteractionProps, getCellRenderDescriptor, getCellValue, getColumnHeaderFilterStateParams, getColumnHeaderMenuItems, getContextMenuHandlers, getDataGridStatusBarConfig, getDateFilterContentProps, getFilterField, getHeaderFilterConfig, getMultiSelectFilterFields, getPaginationViewModel, getStatusBarParts, isInSelectionRange, isRowInRange, mergeFilter, normalizeSelectionRange, numberParser, parseTsvClipboard, parseValue, partitionColumnsForVirtualization, processClientSideData, rangesEqual, renderFilterContent, resolveCellDisplayContent2 as resolveCellDisplayContent, resolveCellStyle2 as resolveCellStyle, richSelectDropdownStyle, richSelectNoMatchesStyle, richSelectOptionHighlightedStyle, richSelectOptionStyle, richSelectWrapperStyle, selectChevronStyle, selectDisplayStyle, selectEditorStyle, toUserLike, triggerCsvDownload, useActiveCell, useCellEditing, useCellSelection, useClipboard, useColumnChooserState, useColumnHeaderFilterState, useColumnMeta, useColumnReorder, useColumnResize, useContextMenu, useDataGridState, useDataGridTableOrchestration, useDateFilterState, useDebounce, useFillHandle, useFilterOptions, useInlineCellEditorState, useKeyboardNavigation, useLatestRef, useListVirtualizer, useMultiSelectFilterState, useOGrid, usePaginationControls, usePeopleFilterState, useRichSelectState, useRowSelection, useSelectState, useSideBarState, useTableLayout, useTextFilterState, useUndoRedo, useVirtualScroll };
@@ -4,6 +4,7 @@ import type { DataGridLayoutState, DataGridRowSelectionState, DataGridEditingSta
4
4
  import type { UseColumnResizeResult } from './useColumnResize';
5
5
  import type { UseColumnReorderResult } from './useColumnReorder';
6
6
  import type { UseVirtualScrollResult } from './useVirtualScroll';
7
+ import type { IVisibleColumnRange } from '@alaarab/ogrid-core';
7
8
  import type { HeaderFilterConfigInput, CellRenderDescriptorInput } from '../utils';
8
9
  import type { IStatusBarProps, RowId, HeaderRow } from '../types';
9
10
  import { CellDescriptorCache } from '@alaarab/ogrid-core';
@@ -32,6 +33,10 @@ export interface UseDataGridTableOrchestrationResult<T> {
32
33
  virtualScrollEnabled: boolean;
33
34
  virtualRowHeight: number;
34
35
  visibleRange: UseVirtualScrollResult['visibleRange'];
36
+ /** Visible column range for horizontal virtualization (null when disabled). */
37
+ columnRange: IVisibleColumnRange | null;
38
+ /** Callback for horizontal scroll events (column virtualization). */
39
+ onHorizontalScroll?: (scrollLeft: number) => void;
35
40
  items: T[];
36
41
  columns: IOGridDataGridProps<T>['columns'];
37
42
  getRowId: IOGridDataGridProps<T>['getRowId'];
@@ -14,6 +14,8 @@ export interface UseOGridDataFetchingParams<T> {
14
14
  pageSize: number;
15
15
  onError?: (err: unknown) => void;
16
16
  onFirstDataRendered?: () => void;
17
+ /** Worker sort mode: true=always, 'auto'=when data > 5000 rows, false=sync. */
18
+ workerSort?: boolean | 'auto';
17
19
  }
18
20
  export interface UseOGridDataFetchingState<T> {
19
21
  displayItems: T[];
@@ -1,6 +1,6 @@
1
1
  import type { Virtualizer } from '@tanstack/react-virtual';
2
2
  import type { RefObject } from 'react';
3
- import type { IVisibleRange } from '@alaarab/ogrid-core';
3
+ import type { IVisibleRange, IVisibleColumnRange } from '@alaarab/ogrid-core';
4
4
  export type { IVirtualScrollConfig } from '@alaarab/ogrid-core';
5
5
  export interface UseVirtualScrollParams {
6
6
  /** Total number of rows in the data set. */
@@ -18,6 +18,12 @@ export interface UseVirtualScrollParams {
18
18
  threshold?: number;
19
19
  /** Ref to the scrollable container element. */
20
20
  containerRef: RefObject<HTMLElement | null>;
21
+ /** Enable column virtualization (only render visible columns). */
22
+ columnVirtualization?: boolean;
23
+ /** Column widths for horizontal virtualization (unpinned columns only). */
24
+ columnWidths?: number[];
25
+ /** Column overscan count. Default: 2. */
26
+ columnOverscan?: number;
21
27
  }
22
28
  export interface UseVirtualScrollResult {
23
29
  /** The TanStack virtualizer instance (null when disabled). */
@@ -28,11 +34,15 @@ export interface UseVirtualScrollResult {
28
34
  visibleRange: IVisibleRange;
29
35
  /** Scroll to a specific row index. */
30
36
  scrollToIndex: (index: number) => void;
37
+ /** Visible column range for horizontal virtualization (null when column virtualization disabled). */
38
+ columnRange: IVisibleColumnRange | null;
39
+ /** Callback to attach to scroll container's onScroll for horizontal tracking. */
40
+ onHorizontalScroll?: (scrollLeft: number) => void;
31
41
  }
32
42
  /**
33
- * Wraps TanStack Virtual for row virtualization.
43
+ * Wraps TanStack Virtual for row virtualization, with optional column virtualization.
34
44
  * When disabled or when totalRows < threshold, returns a pass-through (all rows visible).
35
- * @param params - Total rows, row height, enabled flag, overscan, threshold, and container ref.
36
- * @returns Virtualizer instance, total height, visible range, and scrollToIndex helper.
45
+ * @param params - Total rows, row height, enabled flag, overscan, threshold, container ref, and column virtualization params.
46
+ * @returns Virtualizer instance, total height, visible range, scrollToIndex, columnRange, and onHorizontalScroll.
37
47
  */
38
48
  export declare function useVirtualScroll(params: UseVirtualScrollParams): UseVirtualScrollResult;
@@ -32,7 +32,7 @@ export { BaseDropIndicator } from './components/BaseDropIndicator';
32
32
  export type { BaseDropIndicatorProps } from './components/BaseDropIndicator';
33
33
  export { DateFilterContent, getColumnHeaderFilterStateParams, getDateFilterContentProps, } from './components/ColumnHeaderFilterContent';
34
34
  export type { IColumnHeaderFilterProps, DateFilterContentProps, DateFilterClassNames, } from './components/ColumnHeaderFilterContent';
35
- export { escapeCsvValue, buildCsvHeader, buildCsvRows, exportToCsv, triggerCsvDownload, getCellValue, flattenColumns, buildHeaderRows, getFilterField, mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, getStatusBarParts, getDataGridStatusBarConfig, GRID_CONTEXT_MENU_ITEMS, COLUMN_HEADER_MENU_ITEMS, getContextMenuHandlers, getColumnHeaderMenuItems, formatShortcut, getPaginationViewModel, PAGE_SIZE_OPTIONS, MAX_PAGE_BUTTONS, getHeaderFilterConfig, getCellRenderDescriptor, CellDescriptorCache, isRowInRange, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, parseValue, numberParser, currencyParser, dateParser, emailParser, booleanParser, computeAggregations, processClientSideData, areGridRowPropsEqual, findCtrlArrowTarget, computeTabNavigation, rangesEqual, clampSelectionToBounds, computeAutoScrollSpeed, formatCellValueForTsv, formatSelectionAsTsv, parseTsvClipboard, UndoRedoStack, } from './utils';
35
+ export { escapeCsvValue, buildCsvHeader, buildCsvRows, exportToCsv, triggerCsvDownload, getCellValue, flattenColumns, buildHeaderRows, getFilterField, mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, getStatusBarParts, getDataGridStatusBarConfig, GRID_CONTEXT_MENU_ITEMS, COLUMN_HEADER_MENU_ITEMS, getContextMenuHandlers, getColumnHeaderMenuItems, formatShortcut, getPaginationViewModel, PAGE_SIZE_OPTIONS, MAX_PAGE_BUTTONS, getHeaderFilterConfig, getCellRenderDescriptor, CellDescriptorCache, isRowInRange, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, parseValue, numberParser, currencyParser, dateParser, emailParser, booleanParser, computeAggregations, processClientSideData, partitionColumnsForVirtualization, areGridRowPropsEqual, findCtrlArrowTarget, computeTabNavigation, rangesEqual, clampSelectionToBounds, computeAutoScrollSpeed, formatCellValueForTsv, formatSelectionAsTsv, parseTsvClipboard, UndoRedoStack, } from './utils';
36
36
  export type { CsvColumn, StatusBarPart, StatusBarPartsInput, GridContextMenuItem, GridContextMenuHandlerProps, PaginationViewModel, HeaderFilterConfigInput, HeaderFilterConfig, CellRenderDescriptorInput, CellRenderDescriptor, CellRenderMode, CellInteractionHandlers, ParseValueResult, AggregationResult, GridRowComparatorProps, IColumnHeaderMenuItem, ColumnHeaderMenuInput, ColumnHeaderMenuHandlers, } from './utils';
37
37
  export { renderFilterContent } from './components/ColumnHeaderFilterRenderers';
38
38
  export type { FilterContentRenderers, MultiSelectRendererProps, TextRendererProps, PeopleRendererProps, DateRendererProps, } from './components/ColumnHeaderFilterRenderers';
@@ -80,6 +80,14 @@ interface IOGridBaseProps<T> {
80
80
  rowHeight?: number;
81
81
  /** Cell spacing/density preset. Controls cell padding throughout the grid. Default: 'normal'. */
82
82
  density?: 'compact' | 'normal' | 'comfortable';
83
+ /**
84
+ * Offload sorting to a Web Worker to avoid blocking the main thread.
85
+ * - `true`: always use worker sort
86
+ * - `'auto'`: use worker sort when data.length > 5000
87
+ * - `false` (default): use synchronous sort
88
+ * Columns with custom `compare` functions fall back to synchronous sort.
89
+ */
90
+ workerSort?: boolean | 'auto';
83
91
  /** Fires once when the grid first renders with data (useful for restoring column state). */
84
92
  onFirstDataRendered?: () => void;
85
93
  /** Called when server-side fetchPage fails. */
@@ -1,4 +1,4 @@
1
- export { escapeCsvValue, buildCsvHeader, buildCsvRows, exportToCsv, triggerCsvDownload, getCellValue, flattenColumns, buildHeaderRows, getFilterField, mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, getStatusBarParts, getDataGridStatusBarConfig, getPaginationViewModel, PAGE_SIZE_OPTIONS, MAX_PAGE_BUTTONS, GRID_CONTEXT_MENU_ITEMS, COLUMN_HEADER_MENU_ITEMS, getContextMenuHandlers, getColumnHeaderMenuItems, formatShortcut, parseValue, numberParser, currencyParser, dateParser, emailParser, booleanParser, computeAggregations, processClientSideData, computeNextSortState, measureColumnContentWidth, AUTOSIZE_EXTRA_PX, AUTOSIZE_MAX_PX, findCtrlArrowTarget, computeTabNavigation, rangesEqual, clampSelectionToBounds, computeAutoScrollSpeed, formatCellValueForTsv, formatSelectionAsTsv, parseTsvClipboard, applyPastedValues, applyCutClear, applyFillValues, computeArrowNavigation, applyCellDeletion, applyRangeRowSelection, computeRowSelectionState, UndoRedoStack, buildCellIndex, } from '@alaarab/ogrid-core';
1
+ export { escapeCsvValue, buildCsvHeader, buildCsvRows, exportToCsv, triggerCsvDownload, getCellValue, flattenColumns, buildHeaderRows, getFilterField, mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, getStatusBarParts, getDataGridStatusBarConfig, getPaginationViewModel, PAGE_SIZE_OPTIONS, MAX_PAGE_BUTTONS, GRID_CONTEXT_MENU_ITEMS, COLUMN_HEADER_MENU_ITEMS, getContextMenuHandlers, getColumnHeaderMenuItems, formatShortcut, parseValue, numberParser, currencyParser, dateParser, emailParser, booleanParser, computeAggregations, processClientSideData, partitionColumnsForVirtualization, computeNextSortState, measureColumnContentWidth, AUTOSIZE_EXTRA_PX, AUTOSIZE_MAX_PX, findCtrlArrowTarget, computeTabNavigation, rangesEqual, clampSelectionToBounds, computeAutoScrollSpeed, formatCellValueForTsv, formatSelectionAsTsv, parseTsvClipboard, applyPastedValues, applyCutClear, applyFillValues, computeArrowNavigation, applyCellDeletion, applyRangeRowSelection, computeRowSelectionState, UndoRedoStack, buildCellIndex, } from '@alaarab/ogrid-core';
2
2
  export type { CsvColumn, StatusBarPart, StatusBarPartsInput, GridContextMenuItem, GridContextMenuHandlerProps, PaginationViewModel, ParseValueResult, AggregationResult, IColumnHeaderMenuItem, ColumnHeaderMenuInput, ColumnHeaderMenuHandlers, ArrowNavigationContext, ArrowNavigationResult, } from '@alaarab/ogrid-core';
3
3
  export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, } from './dataGridViewModel';
4
4
  export { CellDescriptorCache } from '@alaarab/ogrid-core';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-react",
3
- "version": "2.1.14",
3
+ "version": "2.2.0",
4
4
  "description": "OGrid React – React hooks, headless components, and utilities for OGrid data grids.",
5
5
  "main": "dist/esm/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -39,7 +39,7 @@
39
39
  "node": ">=18"
40
40
  },
41
41
  "dependencies": {
42
- "@alaarab/ogrid-core": "2.1.14",
42
+ "@alaarab/ogrid-core": "2.2.0",
43
43
  "@tanstack/react-virtual": "^3.0.0"
44
44
  },
45
45
  "peerDependencies": {