@alaarab/ogrid-core 1.2.2 → 1.3.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.
Files changed (35) hide show
  1. package/dist/esm/components/GridContextMenu.js +12 -3
  2. package/dist/esm/components/MarchingAntsOverlay.js +109 -0
  3. package/dist/esm/hooks/useCellSelection.js +4 -0
  4. package/dist/esm/hooks/useClipboard.js +41 -8
  5. package/dist/esm/hooks/useColumnHeaderFilterState.js +11 -12
  6. package/dist/esm/hooks/useContextMenu.js +1 -0
  7. package/dist/esm/hooks/useDataGridState.js +74 -30
  8. package/dist/esm/hooks/useFillHandle.js +16 -3
  9. package/dist/esm/hooks/useInlineCellEditorState.js +2 -0
  10. package/dist/esm/hooks/useKeyboardNavigation.js +22 -2
  11. package/dist/esm/hooks/useOGrid.js +4 -1
  12. package/dist/esm/hooks/useUndoRedo.js +44 -14
  13. package/dist/esm/index.js +2 -1
  14. package/dist/esm/utils/dataGridViewModel.js +7 -1
  15. package/dist/esm/utils/gridContextMenuHelpers.js +20 -5
  16. package/dist/esm/utils/index.js +2 -1
  17. package/dist/esm/utils/valueParsers.js +107 -0
  18. package/dist/types/components/GridContextMenu.d.ts +4 -0
  19. package/dist/types/components/MarchingAntsOverlay.d.ts +15 -0
  20. package/dist/types/hooks/useCellSelection.d.ts +2 -0
  21. package/dist/types/hooks/useClipboard.d.ts +7 -0
  22. package/dist/types/hooks/useContextMenu.d.ts +1 -0
  23. package/dist/types/hooks/useDataGridState.d.ts +12 -0
  24. package/dist/types/hooks/useFillHandle.d.ts +3 -0
  25. package/dist/types/hooks/useKeyboardNavigation.d.ts +2 -1
  26. package/dist/types/hooks/useUndoRedo.d.ts +5 -1
  27. package/dist/types/index.d.ts +5 -3
  28. package/dist/types/types/columnTypes.d.ts +17 -0
  29. package/dist/types/types/dataGridTypes.d.ts +8 -0
  30. package/dist/types/types/index.d.ts +1 -1
  31. package/dist/types/utils/dataGridViewModel.d.ts +9 -0
  32. package/dist/types/utils/gridContextMenuHelpers.d.ts +8 -0
  33. package/dist/types/utils/index.d.ts +3 -1
  34. package/dist/types/utils/valueParsers.d.ts +37 -0
  35. package/package.json +1 -1
@@ -1,8 +1,9 @@
1
1
  import { useCallback } from 'react';
2
2
  import { normalizeSelectionRange } from '../types';
3
3
  import { getCellValue } from '../utils';
4
+ import { parseValue } from '../utils/valueParsers';
4
5
  export function useKeyboardNavigation(params) {
5
- const { items, visibleCols, colOffset, hasCheckboxCol, visibleColumnCount, activeCell, setActiveCell, selectionRange, setSelectionRange, editable, onCellValueChanged, getRowId, editingCell, setEditingCell, rowSelection, selectedRowIds, handleRowCheckboxChange, handleCopy, handleCut, handlePaste, setContextMenu, wrapperRef, onUndo, onRedo, } = params;
6
+ const { items, visibleCols, colOffset, hasCheckboxCol, visibleColumnCount, activeCell, setActiveCell, selectionRange, setSelectionRange, editable, onCellValueChanged, getRowId, editingCell, setEditingCell, rowSelection, selectedRowIds, handleRowCheckboxChange, handleCopy, handleCut, handlePaste, setContextMenu, wrapperRef, onUndo, onRedo, clearClipboardRanges, } = params;
6
7
  const maxRowIndex = items.length - 1;
7
8
  const maxColIndex = visibleColumnCount - 1 + colOffset;
8
9
  const handleGridKeyDown = useCallback((e) => {
@@ -30,18 +31,24 @@ export function useKeyboardNavigation(params) {
30
31
  switch (e.key) {
31
32
  case 'c':
32
33
  if (e.ctrlKey || e.metaKey) {
34
+ if (editingCell != null)
35
+ break; // let the input handle copy
33
36
  e.preventDefault();
34
37
  handleCopy();
35
38
  }
36
39
  break;
37
40
  case 'x':
38
41
  if (e.ctrlKey || e.metaKey) {
42
+ if (editingCell != null)
43
+ break; // let the input handle cut
39
44
  e.preventDefault();
40
45
  handleCut();
41
46
  }
42
47
  break;
43
48
  case 'v':
44
49
  if (e.ctrlKey || e.metaKey) {
50
+ if (editingCell != null)
51
+ break; // let the input handle paste
45
52
  e.preventDefault();
46
53
  void handlePaste();
47
54
  }
@@ -216,6 +223,7 @@ export function useKeyboardNavigation(params) {
216
223
  setEditingCell(null);
217
224
  }
218
225
  else {
226
+ clearClipboardRanges?.();
219
227
  setActiveCell(null);
220
228
  setSelectionRange(null);
221
229
  }
@@ -257,6 +265,8 @@ export function useKeyboardNavigation(params) {
257
265
  break;
258
266
  case 'a':
259
267
  if (e.ctrlKey || e.metaKey) {
268
+ if (editingCell != null)
269
+ break; // let the input handle select-all
260
270
  e.preventDefault();
261
271
  if (items.length > 0 && visibleColumnCount > 0) {
262
272
  setSelectionRange({
@@ -273,6 +283,8 @@ export function useKeyboardNavigation(params) {
273
283
  case 'Backspace': {
274
284
  if (editingCell != null)
275
285
  break;
286
+ if (editable === false)
287
+ break;
276
288
  if (onCellValueChanged == null)
277
289
  break;
278
290
  const range = selectionRange ??
@@ -294,13 +306,20 @@ export function useKeyboardNavigation(params) {
294
306
  continue;
295
307
  const item = items[r];
296
308
  const col = visibleCols[c];
309
+ const colEditable = col.editable === true ||
310
+ (typeof col.editable === 'function' && col.editable(item));
311
+ if (!colEditable)
312
+ continue;
297
313
  const oldValue = getCellValue(item, col);
314
+ const result = parseValue('', oldValue, item, col);
315
+ if (!result.valid)
316
+ continue;
298
317
  onCellValueChanged({
299
318
  item,
300
319
  columnId: col.columnId,
301
320
  field: col.columnId,
302
321
  oldValue,
303
- newValue: '',
322
+ newValue: result.value,
304
323
  rowIndex: r,
305
324
  });
306
325
  }
@@ -359,6 +378,7 @@ export function useKeyboardNavigation(params) {
359
378
  wrapperRef,
360
379
  onUndo,
361
380
  onRedo,
381
+ clearClipboardRanges,
362
382
  ]);
363
383
  return { handleGridKeyDown };
364
384
  }
@@ -5,7 +5,7 @@ import { toDataGridFilterProps } from '../types';
5
5
  import { useFilterOptions } from './useFilterOptions';
6
6
  const DEFAULT_PAGE_SIZE = 20;
7
7
  export function useOGrid(props, ref) {
8
- const { columns: columnsProp, getRowId, data, dataSource, page: controlledPage, pageSize: controlledPageSize, sort: controlledSort, filters: controlledFilters, visibleColumns: controlledVisibleColumns, isLoading: controlledLoading, onPageChange, onPageSizeChange, onSortChange, onFiltersChange, onVisibleColumnsChange, columnOrder, onColumnOrderChange, freezeRows, freezeCols, defaultPageSize = DEFAULT_PAGE_SIZE, defaultSortBy, defaultSortDirection = 'asc', toolbar, emptyState, entityLabelPlural = 'items', className, title, layoutMode = 'content', editable, onCellValueChanged, onUndo, onRedo, rowSelection = 'none', selectedRows, onSelectionChange, statusBar, onError, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, } = props;
8
+ const { columns: columnsProp, getRowId, data, dataSource, page: controlledPage, pageSize: controlledPageSize, sort: controlledSort, filters: controlledFilters, visibleColumns: controlledVisibleColumns, isLoading: controlledLoading, onPageChange, onPageSizeChange, onSortChange, onFiltersChange, onVisibleColumnsChange, columnOrder, onColumnOrderChange, freezeRows, freezeCols, defaultPageSize = DEFAULT_PAGE_SIZE, defaultSortBy, defaultSortDirection = 'asc', toolbar, emptyState, entityLabelPlural = 'items', className, title, layoutMode = 'fill', editable, cellSelection, onCellValueChanged, onUndo, onRedo, canUndo, canRedo, rowSelection = 'none', selectedRows, onSelectionChange, statusBar, onError, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, } = props;
9
9
  const columns = useMemo(() => flattenColumns(columnsProp), [columnsProp]);
10
10
  const isServerSide = dataSource != null;
11
11
  const isClientSide = !isServerSide;
@@ -307,9 +307,12 @@ export function useOGrid(props, ref) {
307
307
  freezeRows,
308
308
  freezeCols,
309
309
  editable,
310
+ cellSelection,
310
311
  onCellValueChanged,
311
312
  onUndo,
312
313
  onRedo,
314
+ canUndo,
315
+ canRedo,
313
316
  rowSelection,
314
317
  selectedRows: effectiveSelectedRows,
315
318
  onSelectionChange: handleSelectionChange,
@@ -1,46 +1,74 @@
1
1
  import { useCallback, useRef, useState } from 'react';
2
2
  /**
3
3
  * Wraps onCellValueChanged with an undo/redo history stack.
4
- * Undo reverts the last change; redo reapplies it.
4
+ * Supports batch operations: changes between beginBatch/endBatch are one undo step.
5
5
  */
6
6
  export function useUndoRedo(params) {
7
7
  const { onCellValueChanged, maxHistory = 50 } = params;
8
+ // Each history entry is an array of events (batch). Single edits are [event].
8
9
  const historyRef = useRef([]);
9
10
  const redoStackRef = useRef([]);
11
+ const batchRef = useRef(null);
10
12
  const [historyLength, setHistoryLength] = useState(0);
11
13
  const [redoLength, setRedoLength] = useState(0);
12
14
  const wrapped = useCallback((event) => {
13
15
  if (!onCellValueChanged)
14
16
  return;
15
- historyRef.current = [...historyRef.current, event].slice(-maxHistory);
17
+ if (batchRef.current !== null) {
18
+ // Accumulate into the current batch — don't push to history yet
19
+ batchRef.current.push(event);
20
+ }
21
+ else {
22
+ historyRef.current = [...historyRef.current, [event]].slice(-maxHistory);
23
+ redoStackRef.current = [];
24
+ setHistoryLength(historyRef.current.length);
25
+ setRedoLength(0);
26
+ }
27
+ onCellValueChanged(event);
28
+ }, [onCellValueChanged, maxHistory]);
29
+ const beginBatch = useCallback(() => {
30
+ batchRef.current = [];
31
+ }, []);
32
+ const endBatch = useCallback(() => {
33
+ const batch = batchRef.current;
34
+ batchRef.current = null;
35
+ if (!batch || batch.length === 0)
36
+ return;
37
+ historyRef.current = [...historyRef.current, batch].slice(-maxHistory);
16
38
  redoStackRef.current = [];
17
39
  setHistoryLength(historyRef.current.length);
18
40
  setRedoLength(0);
19
- onCellValueChanged(event);
20
- }, [onCellValueChanged, maxHistory]);
41
+ }, [maxHistory]);
21
42
  const undo = useCallback(() => {
22
43
  if (!onCellValueChanged || historyRef.current.length === 0)
23
44
  return;
24
- const last = historyRef.current[historyRef.current.length - 1];
45
+ const lastBatch = historyRef.current[historyRef.current.length - 1];
25
46
  historyRef.current = historyRef.current.slice(0, -1);
26
- redoStackRef.current = [...redoStackRef.current, last];
47
+ redoStackRef.current = [...redoStackRef.current, lastBatch];
27
48
  setHistoryLength(historyRef.current.length);
28
49
  setRedoLength(redoStackRef.current.length);
29
- onCellValueChanged({
30
- ...last,
31
- oldValue: last.newValue,
32
- newValue: last.oldValue,
33
- });
50
+ // Revert in reverse order so multi-cell undo is applied correctly
51
+ for (let i = lastBatch.length - 1; i >= 0; i--) {
52
+ const ev = lastBatch[i];
53
+ onCellValueChanged({
54
+ ...ev,
55
+ oldValue: ev.newValue,
56
+ newValue: ev.oldValue,
57
+ });
58
+ }
34
59
  }, [onCellValueChanged]);
35
60
  const redo = useCallback(() => {
36
61
  if (!onCellValueChanged || redoStackRef.current.length === 0)
37
62
  return;
38
- const next = redoStackRef.current[redoStackRef.current.length - 1];
63
+ const nextBatch = redoStackRef.current[redoStackRef.current.length - 1];
39
64
  redoStackRef.current = redoStackRef.current.slice(0, -1);
40
- historyRef.current = [...historyRef.current, next];
65
+ historyRef.current = [...historyRef.current, nextBatch];
41
66
  setRedoLength(redoStackRef.current.length);
42
67
  setHistoryLength(historyRef.current.length);
43
- onCellValueChanged(next);
68
+ // Replay in original order
69
+ for (const ev of nextBatch) {
70
+ onCellValueChanged(ev);
71
+ }
44
72
  }, [onCellValueChanged]);
45
73
  return {
46
74
  onCellValueChanged: onCellValueChanged ? wrapped : undefined,
@@ -48,5 +76,7 @@ export function useUndoRedo(params) {
48
76
  redo,
49
77
  canUndo: historyLength > 0,
50
78
  canRedo: redoLength > 0,
79
+ beginBatch,
80
+ endBatch,
51
81
  };
52
82
  }
package/dist/esm/index.js CHANGED
@@ -5,5 +5,6 @@ export { useFilterOptions, useOGrid, useActiveCell, useCellEditing, useContextMe
5
5
  export { OGridLayout } from './components/OGridLayout';
6
6
  export { StatusBar } from './components/StatusBar';
7
7
  export { GridContextMenu } from './components/GridContextMenu';
8
+ export { MarchingAntsOverlay } from './components/MarchingAntsOverlay';
8
9
  // Utilities
9
- export { escapeCsvValue, buildCsvHeader, buildCsvRows, exportToCsv, triggerCsvDownload, getCellValue, flattenColumns, getFilterField, mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, getStatusBarParts, getDataGridStatusBarConfig, GRID_CONTEXT_MENU_ITEMS, getContextMenuHandlers, getPaginationViewModel, PAGE_SIZE_OPTIONS, MAX_PAGE_BUTTONS, getHeaderFilterConfig, getCellRenderDescriptor, } from './utils';
10
+ export { escapeCsvValue, buildCsvHeader, buildCsvRows, exportToCsv, triggerCsvDownload, getCellValue, flattenColumns, getFilterField, mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, getStatusBarParts, getDataGridStatusBarConfig, GRID_CONTEXT_MENU_ITEMS, getContextMenuHandlers, formatShortcut, getPaginationViewModel, PAGE_SIZE_OPTIONS, MAX_PAGE_BUTTONS, getHeaderFilterConfig, getCellRenderDescriptor, parseValue, numberParser, currencyParser, dateParser, emailParser, booleanParser, } from './utils';
@@ -77,7 +77,12 @@ export function getCellRenderDescriptor(item, col, rowIndex, colIdx, input) {
77
77
  isInSelectionRange(input.selectionRange, rowIndex, colIdx);
78
78
  const isInCutRange = input.cutRange != null &&
79
79
  isInSelectionRange(input.cutRange, rowIndex, colIdx);
80
- const isSelectionEndCell = input.selectionRange != null &&
80
+ const isInCopyRange = input.copyRange != null &&
81
+ isInSelectionRange(input.copyRange, rowIndex, colIdx);
82
+ const isSelectionEndCell = !input.isDragging &&
83
+ input.copyRange == null &&
84
+ input.cutRange == null &&
85
+ input.selectionRange != null &&
81
86
  rowIndex === input.selectionRange.endRow &&
82
87
  colIdx === input.selectionRange.endCol;
83
88
  const isPinned = col.pinned != null;
@@ -109,6 +114,7 @@ export function getCellRenderDescriptor(item, col, rowIndex, colIdx, input) {
109
114
  isActive,
110
115
  isInRange,
111
116
  isInCutRange,
117
+ isInCopyRange,
112
118
  isSelectionEndCell,
113
119
  canEditAny,
114
120
  isPinned,
@@ -1,16 +1,31 @@
1
1
  export const GRID_CONTEXT_MENU_ITEMS = [
2
- { id: 'copy', label: 'Copy', disabledWhenNoSelection: true },
3
- { id: 'cut', label: 'Cut', disabledWhenNoSelection: true },
4
- { id: 'paste', label: 'Paste' },
5
- { id: 'selectAll', label: 'Select all' },
2
+ { id: 'undo', label: 'Undo', shortcut: 'Ctrl+Z' },
3
+ { id: 'redo', label: 'Redo', shortcut: 'Ctrl+Y' },
4
+ { id: 'copy', label: 'Copy', shortcut: 'Ctrl+C', disabledWhenNoSelection: true, dividerBefore: true },
5
+ { id: 'cut', label: 'Cut', shortcut: 'Ctrl+X', disabledWhenNoSelection: true },
6
+ { id: 'paste', label: 'Paste', shortcut: 'Ctrl+V' },
7
+ { id: 'selectAll', label: 'Select all', shortcut: 'Ctrl+A', dividerBefore: true },
6
8
  ];
9
+ /** Returns the shortcut string with Ctrl swapped to ⌘ on Mac. */
10
+ export function formatShortcut(shortcut) {
11
+ const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad|iPod/.test(navigator.userAgent);
12
+ return isMac ? shortcut.replace('Ctrl', '\u2318') : shortcut;
13
+ }
7
14
  /**
8
15
  * Returns a map of menu item id -> click handler. Each handler invokes the corresponding
9
16
  * action and then onClose. Used by Fluent, Material, and Radix GridContextMenu components.
10
17
  */
11
18
  export function getContextMenuHandlers(props) {
12
- const { onCopy, onCut, onPaste, onSelectAll, onClose } = props;
19
+ const { onCopy, onCut, onPaste, onSelectAll, onUndo, onRedo, onClose } = props;
13
20
  return {
21
+ undo: () => {
22
+ onUndo();
23
+ onClose();
24
+ },
25
+ redo: () => {
26
+ onRedo();
27
+ onClose();
28
+ },
14
29
  copy: () => {
15
30
  onCopy();
16
31
  onClose();
@@ -5,5 +5,6 @@ export { getFilterField, mergeFilter, deriveFilterOptionsFromData, getMultiSelec
5
5
  export { getStatusBarParts } from './statusBarHelpers';
6
6
  export { getDataGridStatusBarConfig } from './dataGridStatusBar';
7
7
  export { getPaginationViewModel, PAGE_SIZE_OPTIONS, MAX_PAGE_BUTTONS, } from './paginationHelpers';
8
- export { GRID_CONTEXT_MENU_ITEMS, getContextMenuHandlers } from './gridContextMenuHelpers';
8
+ export { GRID_CONTEXT_MENU_ITEMS, getContextMenuHandlers, formatShortcut } from './gridContextMenuHelpers';
9
9
  export { getHeaderFilterConfig, getCellRenderDescriptor, } from './dataGridViewModel';
10
+ export { parseValue, numberParser, currencyParser, dateParser, emailParser, booleanParser, } from './valueParsers';
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Run the column's valueParser (if any), or auto-validate select columns.
3
+ * Returns `{ valid: true, value }` with the parsed value, or `{ valid: false }` to reject.
4
+ */
5
+ export function parseValue(newValue, oldValue, item, col) {
6
+ // 1. Custom valueParser takes priority
7
+ if (col.valueParser) {
8
+ const params = {
9
+ newValue,
10
+ oldValue,
11
+ data: item,
12
+ column: col,
13
+ };
14
+ const parsed = col.valueParser(params);
15
+ if (parsed === undefined) {
16
+ return { valid: false, value: undefined };
17
+ }
18
+ return { valid: true, value: parsed };
19
+ }
20
+ // 2. Auto-validate select columns against allowed values
21
+ if (col.cellEditor === 'select' &&
22
+ col.cellEditorParams?.values != null &&
23
+ Array.isArray(col.cellEditorParams.values)) {
24
+ const allowedValues = col.cellEditorParams.values;
25
+ const strValue = typeof newValue === 'string' ? newValue : String(newValue ?? '');
26
+ // Allow clearing (empty string)
27
+ if (strValue === '') {
28
+ return { valid: true, value: '' };
29
+ }
30
+ // Case-insensitive match; return canonical value from the options list
31
+ const match = allowedValues.find((v) => String(v).toLowerCase() === strValue.toLowerCase());
32
+ if (match !== undefined) {
33
+ return { valid: true, value: match };
34
+ }
35
+ return { valid: false, value: undefined };
36
+ }
37
+ // 3. No parser, not a select column — pass through unchanged
38
+ return { valid: true, value: newValue };
39
+ }
40
+ // --- Built-in parser helpers ---
41
+ // Consumers assign these to columns: { valueParser: numberParser }
42
+ // Return `undefined` to reject; `null` to clear the cell.
43
+ /**
44
+ * Parses a value as a number. Strips whitespace and commas.
45
+ * Returns `undefined` (reject) if result is NaN.
46
+ */
47
+ export function numberParser(params) {
48
+ const { newValue } = params;
49
+ if (newValue === '' || newValue == null)
50
+ return null;
51
+ const str = String(newValue).replace(/[\s,]/g, '');
52
+ const num = Number(str);
53
+ return Number.isNaN(num) ? undefined : num;
54
+ }
55
+ /**
56
+ * Parses a currency string. Strips currency symbols ($, €, £, ¥), whitespace, commas.
57
+ * Returns `undefined` (reject) if result is NaN.
58
+ */
59
+ export function currencyParser(params) {
60
+ const { newValue } = params;
61
+ if (newValue === '' || newValue == null)
62
+ return null;
63
+ const str = String(newValue)
64
+ .replace(/[$\u20AC\u00A3\u00A5]/g, '') // $, €, £, ¥
65
+ .replace(/[\s,]/g, '');
66
+ const num = Number(str);
67
+ return Number.isNaN(num) ? undefined : num;
68
+ }
69
+ /**
70
+ * Parses a date string via `new Date()`. Returns ISO string or `undefined` if invalid.
71
+ */
72
+ export function dateParser(params) {
73
+ const { newValue } = params;
74
+ if (newValue === '' || newValue == null)
75
+ return null;
76
+ const str = String(newValue).trim();
77
+ const date = new Date(str);
78
+ if (Number.isNaN(date.getTime()))
79
+ return undefined;
80
+ return date.toISOString();
81
+ }
82
+ /**
83
+ * Validates an email address with a basic regex.
84
+ * Returns the trimmed string or `undefined` if invalid.
85
+ */
86
+ export function emailParser(params) {
87
+ const { newValue } = params;
88
+ if (newValue === '' || newValue == null)
89
+ return null;
90
+ const str = String(newValue).trim();
91
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str) ? str : undefined;
92
+ }
93
+ /**
94
+ * Parses boolean-like values: true/false/yes/no/1/0.
95
+ * Returns `undefined` if not recognized.
96
+ */
97
+ export function booleanParser(params) {
98
+ const { newValue } = params;
99
+ if (newValue === '' || newValue == null)
100
+ return null;
101
+ const str = String(newValue).trim().toLowerCase();
102
+ if (['true', 'yes', '1'].includes(str))
103
+ return true;
104
+ if (['false', 'no', '0'].includes(str))
105
+ return false;
106
+ return undefined;
107
+ }
@@ -3,12 +3,16 @@ import type { GridContextMenuHandlerProps } from '../utils/gridContextMenuHelper
3
3
  export interface GridContextMenuClassNames {
4
4
  contextMenu?: string;
5
5
  contextMenuItem?: string;
6
+ contextMenuItemLabel?: string;
7
+ contextMenuItemShortcut?: string;
6
8
  contextMenuDivider?: string;
7
9
  }
8
10
  export interface GridContextMenuProps extends GridContextMenuHandlerProps {
9
11
  x: number;
10
12
  y: number;
11
13
  hasSelection: boolean;
14
+ canUndo: boolean;
15
+ canRedo: boolean;
12
16
  classNames?: GridContextMenuClassNames;
13
17
  }
14
18
  export declare function GridContextMenu(props: GridContextMenuProps): React.ReactElement;
@@ -0,0 +1,15 @@
1
+ import * as React from 'react';
2
+ import type { ISelectionRange } from '../types';
3
+ export interface MarchingAntsOverlayProps {
4
+ /** Ref to the positioned container that wraps the table (must have position: relative) */
5
+ containerRef: React.RefObject<HTMLElement | null>;
6
+ /** Current selection range — solid green border */
7
+ selectionRange: ISelectionRange | null;
8
+ /** Copy range — animated dashed border */
9
+ copyRange: ISelectionRange | null;
10
+ /** Cut range — animated dashed border */
11
+ cutRange: ISelectionRange | null;
12
+ /** Column offset — 1 when checkbox column is present, else 0 */
13
+ colOffset: number;
14
+ }
15
+ export declare function MarchingAntsOverlay({ containerRef, selectionRange, copyRange, cutRange, colOffset, }: MarchingAntsOverlayProps): React.ReactElement | null;
@@ -10,5 +10,7 @@ export interface UseCellSelectionResult {
10
10
  setSelectionRange: (range: ISelectionRange | null) => void;
11
11
  handleCellMouseDown: (e: React.MouseEvent, rowIndex: number, globalColIndex: number) => void;
12
12
  handleSelectAllCells: () => void;
13
+ /** True while the user is drag-selecting cells (mousedown → mousemove → mouseup). */
14
+ isDragging: boolean;
13
15
  }
14
16
  export declare function useCellSelection(params: UseCellSelectionParams): UseCellSelectionResult;
@@ -5,7 +5,10 @@ export interface UseClipboardParams<T> {
5
5
  colOffset: number;
6
6
  selectionRange: ISelectionRange | null;
7
7
  activeCell: IActiveCell | null;
8
+ editable?: boolean;
8
9
  onCellValueChanged: ((event: ICellValueChangedEvent<T>) => void) | undefined;
10
+ beginBatch?: () => void;
11
+ endBatch?: () => void;
9
12
  }
10
13
  export interface UseClipboardResult {
11
14
  handleCopy: () => void;
@@ -14,5 +17,9 @@ export interface UseClipboardResult {
14
17
  cutRangeRef: React.MutableRefObject<ISelectionRange | null>;
15
18
  /** Current cut range for UI (marching ants). Null when no cut or after paste. */
16
19
  cutRange: ISelectionRange | null;
20
+ /** Current copy range for UI (marching ants). Null when no copy or after paste/cut. */
21
+ copyRange: ISelectionRange | null;
22
+ /** Clear both copy and cut ranges (dismisses marching ants). Called on Escape. */
23
+ clearClipboardRanges: () => void;
17
24
  }
18
25
  export declare function useClipboard<T>(params: UseClipboardParams<T>): UseClipboardResult;
@@ -8,6 +8,7 @@ export interface UseContextMenuResult {
8
8
  handleCellContextMenu: (e: {
9
9
  clientX: number;
10
10
  clientY: number;
11
+ preventDefault?: () => void;
11
12
  }) => void;
12
13
  closeContextMenu: () => void;
13
14
  }
@@ -57,8 +57,13 @@ export interface UseDataGridStateResult<T> {
57
57
  handleCellContextMenu: (e: {
58
58
  clientX: number;
59
59
  clientY: number;
60
+ preventDefault?: () => void;
60
61
  }) => void;
61
62
  closeContextMenu: () => void;
63
+ canUndo: boolean;
64
+ canRedo: boolean;
65
+ onUndo?: () => void;
66
+ onRedo?: () => void;
62
67
  handleCopy: () => void;
63
68
  handleCut: () => void;
64
69
  handlePaste: () => Promise<void>;
@@ -68,6 +73,12 @@ export interface UseDataGridStateResult<T> {
68
73
  endRow: number;
69
74
  endCol: number;
70
75
  } | null;
76
+ copyRange: {
77
+ startRow: number;
78
+ startCol: number;
79
+ endRow: number;
80
+ endCol: number;
81
+ } | null;
71
82
  handleGridKeyDown: (e: React.KeyboardEvent) => void;
72
83
  handleFillHandleMouseDown: (e: React.MouseEvent) => void;
73
84
  containerWidth: number;
@@ -84,6 +95,7 @@ export interface UseDataGridStateResult<T> {
84
95
  cancelPopoverEdit: () => void;
85
96
  popoverAnchorEl: HTMLElement | null;
86
97
  setPopoverAnchorEl: React.Dispatch<React.SetStateAction<HTMLElement | null>>;
98
+ clearClipboardRanges: () => void;
87
99
  statusBarConfig: IStatusBarProps | null;
88
100
  showEmptyInGrid: boolean;
89
101
  hasCellSelection: boolean;
@@ -4,12 +4,15 @@ import type { IColumnDef, ICellValueChangedEvent } from '../types/columnTypes';
4
4
  export interface UseFillHandleParams<T> {
5
5
  items: T[];
6
6
  visibleCols: IColumnDef<T>[];
7
+ editable?: boolean;
7
8
  onCellValueChanged?: (event: ICellValueChangedEvent<T>) => void;
8
9
  selectionRange: ISelectionRange | null;
9
10
  setSelectionRange: (range: ISelectionRange | null) => void;
10
11
  setActiveCell: (cell: IActiveCell | null) => void;
11
12
  colOffset: number;
12
13
  wrapperRef: RefObject<HTMLDivElement | null>;
14
+ beginBatch?: () => void;
15
+ endBatch?: () => void;
13
16
  }
14
17
  export interface UseFillHandleResult {
15
18
  fillDrag: {
@@ -11,7 +11,7 @@ export interface UseKeyboardNavigationParams<T> {
11
11
  setActiveCell: (cell: IActiveCell | null) => void;
12
12
  selectionRange: ISelectionRange | null;
13
13
  setSelectionRange: (range: ISelectionRange | null) => void;
14
- editable: boolean | undefined;
14
+ editable?: boolean;
15
15
  onCellValueChanged: ((event: ICellValueChangedEvent<T>) => void) | undefined;
16
16
  getRowId: (item: T) => RowId;
17
17
  editingCell: EditingCell | null;
@@ -26,6 +26,7 @@ export interface UseKeyboardNavigationParams<T> {
26
26
  wrapperRef: React.RefObject<HTMLElement | null>;
27
27
  onUndo?: () => void;
28
28
  onRedo?: () => void;
29
+ clearClipboardRanges?: () => void;
29
30
  }
30
31
  export interface UseKeyboardNavigationResult {
31
32
  handleGridKeyDown: (e: React.KeyboardEvent) => void;
@@ -9,9 +9,13 @@ export interface UseUndoRedoResult<T> {
9
9
  redo: () => void;
10
10
  canUndo: boolean;
11
11
  canRedo: boolean;
12
+ /** Start a batch — all changes until endBatch() are grouped as one undo step. */
13
+ beginBatch: () => void;
14
+ /** End a batch — commits the accumulated changes as a single undo entry. */
15
+ endBatch: () => void;
12
16
  }
13
17
  /**
14
18
  * Wraps onCellValueChanged with an undo/redo history stack.
15
- * Undo reverts the last change; redo reapplies it.
19
+ * Supports batch operations: changes between beginBatch/endBatch are one undo step.
16
20
  */
17
21
  export declare function useUndoRedo<T>(params: UseUndoRedoParams<T>): UseUndoRedoResult<T>;
@@ -1,4 +1,4 @@
1
- export type { ColumnFilterType, IColumnFilterDef, IColumnMeta, IColumnDef, IColumnGroupDef, IColumnDefinition, ICellValueChangedEvent, ICellEditorProps, CellEditorParams, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, IOGridApi, IOGridProps, IOGridDataGridProps, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, } from './types';
1
+ export type { ColumnFilterType, IColumnFilterDef, IColumnMeta, IColumnDef, IColumnGroupDef, IColumnDefinition, ICellValueChangedEvent, ICellEditorProps, CellEditorParams, IValueParserParams, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, IOGridApi, IOGridProps, IOGridDataGridProps, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, } from './types';
2
2
  export { toUserLike, toDataGridFilterProps, isInSelectionRange, normalizeSelectionRange } from './types';
3
3
  export { useFilterOptions, useOGrid, useActiveCell, useCellEditing, useContextMenu, useCellSelection, useClipboard, useRowSelection, useKeyboardNavigation, useUndoRedo, useDebounce, useFillHandle, useDataGridState, useColumnHeaderFilterState, useColumnChooserState, useInlineCellEditorState, useColumnResize, } from './hooks';
4
4
  export type { UseFilterOptionsResult, UseOGridResult, UseActiveCellResult, UseCellEditingResult, EditingCell, UseContextMenuResult, ContextMenuPosition, UseCellSelectionResult, UseCellSelectionParams, UseClipboardResult, UseClipboardParams, UseRowSelectionResult, UseRowSelectionParams, UseKeyboardNavigationResult, UseKeyboardNavigationParams, UseUndoRedoResult, UseUndoRedoParams, UseFillHandleResult, UseFillHandleParams, UseDataGridStateParams, UseDataGridStateResult, UseColumnHeaderFilterStateParams, UseColumnHeaderFilterStateResult, UseColumnChooserStateParams, UseColumnChooserStateResult, UseInlineCellEditorStateParams, UseInlineCellEditorStateResult, InlineCellEditorType, UseColumnResizeParams, UseColumnResizeResult, } from './hooks';
@@ -8,5 +8,7 @@ export { StatusBar } from './components/StatusBar';
8
8
  export type { StatusBarProps, StatusBarClassNames } from './components/StatusBar';
9
9
  export { GridContextMenu } from './components/GridContextMenu';
10
10
  export type { GridContextMenuProps, GridContextMenuClassNames } from './components/GridContextMenu';
11
- export { escapeCsvValue, buildCsvHeader, buildCsvRows, exportToCsv, triggerCsvDownload, getCellValue, flattenColumns, getFilterField, mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, getStatusBarParts, getDataGridStatusBarConfig, GRID_CONTEXT_MENU_ITEMS, getContextMenuHandlers, getPaginationViewModel, PAGE_SIZE_OPTIONS, MAX_PAGE_BUTTONS, getHeaderFilterConfig, getCellRenderDescriptor, } from './utils';
12
- export type { CsvColumn, StatusBarPart, StatusBarPartsInput, GridContextMenuItem, GridContextMenuHandlerProps, PaginationViewModel, HeaderFilterConfigInput, HeaderFilterConfig, CellRenderDescriptorInput, CellRenderDescriptor, CellRenderMode, } from './utils';
11
+ export { MarchingAntsOverlay } from './components/MarchingAntsOverlay';
12
+ export type { MarchingAntsOverlayProps } from './components/MarchingAntsOverlay';
13
+ export { escapeCsvValue, buildCsvHeader, buildCsvRows, exportToCsv, triggerCsvDownload, getCellValue, flattenColumns, getFilterField, mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, getStatusBarParts, getDataGridStatusBarConfig, GRID_CONTEXT_MENU_ITEMS, getContextMenuHandlers, formatShortcut, getPaginationViewModel, PAGE_SIZE_OPTIONS, MAX_PAGE_BUTTONS, getHeaderFilterConfig, getCellRenderDescriptor, parseValue, numberParser, currencyParser, dateParser, emailParser, booleanParser, } from './utils';
14
+ export type { CsvColumn, StatusBarPart, StatusBarPartsInput, GridContextMenuItem, GridContextMenuHandlerProps, PaginationViewModel, HeaderFilterConfigInput, HeaderFilterConfig, CellRenderDescriptorInput, CellRenderDescriptor, CellRenderMode, ParseValueResult, } from './utils';
@@ -21,6 +21,17 @@ export interface IColumnMeta {
21
21
  /** Pin column to left or right edge (sticky during horizontal scroll). */
22
22
  pinned?: 'left' | 'right';
23
23
  }
24
+ /** Parameters passed to the valueParser function. */
25
+ export interface IValueParserParams<T = unknown> {
26
+ /** The new value to parse (typically a string from paste or editor). */
27
+ newValue: unknown;
28
+ /** The current value of the cell before the edit. */
29
+ oldValue: unknown;
30
+ /** The row data item. */
31
+ data: T;
32
+ /** The column definition. */
33
+ column: IColumnDef<T>;
34
+ }
24
35
  export interface IColumnDef<T = unknown> extends IColumnMeta {
25
36
  renderCell?: (item: T) => React.ReactNode;
26
37
  compare?: (a: T, b: T) => number;
@@ -28,6 +39,12 @@ export interface IColumnDef<T = unknown> extends IColumnMeta {
28
39
  valueGetter?: (item: T) => unknown;
29
40
  /** Format the cell value for display (used when no renderCell). */
30
41
  valueFormatter?: (value: unknown, item: T) => string;
42
+ /**
43
+ * Parse/validate a new value before it is committed to the cell.
44
+ * Called on paste, inline edit commit, fill handle, and delete.
45
+ * Return the parsed value to use, or `undefined` to reject (skip) the change.
46
+ */
47
+ valueParser?: (params: IValueParserParams<T>) => unknown;
31
48
  /** Static or per-row cell inline styles. */
32
49
  cellStyle?: React.CSSProperties | ((item: T) => React.CSSProperties);
33
50
  /** Whether the cell is editable (per-column or per-row). */
@@ -141,9 +141,13 @@ export interface IOGridProps<T> {
141
141
  freezeRows?: number;
142
142
  freezeCols?: number;
143
143
  editable?: boolean;
144
+ /** Enable spreadsheet-like cell selection (active cell, range, fill handle, clipboard, context menu). Default: true. */
145
+ cellSelection?: boolean;
144
146
  onCellValueChanged?: (event: ICellValueChangedEvent<T>) => void;
145
147
  onUndo?: () => void;
146
148
  onRedo?: () => void;
149
+ canUndo?: boolean;
150
+ canRedo?: boolean;
147
151
  rowSelection?: RowSelectionMode;
148
152
  selectedRows?: Set<RowId>;
149
153
  onSelectionChange?: (event: IRowSelectionChangeEvent<T>) => void;
@@ -185,9 +189,13 @@ export interface IOGridDataGridProps<T> {
185
189
  isLoading?: boolean;
186
190
  loadingMessage?: string;
187
191
  editable?: boolean;
192
+ /** Enable spreadsheet-like cell selection. Default: true. */
193
+ cellSelection?: boolean;
188
194
  onCellValueChanged?: (event: ICellValueChangedEvent<T>) => void;
189
195
  onUndo?: () => void;
190
196
  onRedo?: () => void;
197
+ canUndo?: boolean;
198
+ canRedo?: boolean;
191
199
  rowSelection?: RowSelectionMode;
192
200
  selectedRows?: Set<RowId>;
193
201
  onSelectionChange?: (event: IRowSelectionChangeEvent<T>) => void;
@@ -1,3 +1,3 @@
1
- export type { ColumnFilterType, IColumnFilterDef, IColumnMeta, IColumnDef, IColumnGroupDef, IColumnDefinition, ICellValueChangedEvent, ICellEditorProps, CellEditorParams, } from './columnTypes';
1
+ export type { ColumnFilterType, IColumnFilterDef, IColumnMeta, IColumnDef, IColumnGroupDef, IColumnDefinition, ICellValueChangedEvent, ICellEditorProps, CellEditorParams, IValueParserParams, } from './columnTypes';
2
2
  export type { RowId, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, IOGridApi, IOGridProps, IOGridDataGridProps, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, } from './dataGridTypes';
3
3
  export { toUserLike, toDataGridFilterProps, isInSelectionRange, normalizeSelectionRange } from './dataGridTypes';