@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.
- package/dist/esm/components/GridContextMenu.js +12 -3
- package/dist/esm/components/MarchingAntsOverlay.js +109 -0
- package/dist/esm/hooks/useCellSelection.js +4 -0
- package/dist/esm/hooks/useClipboard.js +41 -8
- package/dist/esm/hooks/useColumnHeaderFilterState.js +11 -12
- package/dist/esm/hooks/useContextMenu.js +1 -0
- package/dist/esm/hooks/useDataGridState.js +74 -30
- package/dist/esm/hooks/useFillHandle.js +16 -3
- package/dist/esm/hooks/useInlineCellEditorState.js +2 -0
- package/dist/esm/hooks/useKeyboardNavigation.js +22 -2
- package/dist/esm/hooks/useOGrid.js +4 -1
- package/dist/esm/hooks/useUndoRedo.js +44 -14
- package/dist/esm/index.js +2 -1
- package/dist/esm/utils/dataGridViewModel.js +7 -1
- package/dist/esm/utils/gridContextMenuHelpers.js +20 -5
- package/dist/esm/utils/index.js +2 -1
- package/dist/esm/utils/valueParsers.js +107 -0
- package/dist/types/components/GridContextMenu.d.ts +4 -0
- package/dist/types/components/MarchingAntsOverlay.d.ts +15 -0
- package/dist/types/hooks/useCellSelection.d.ts +2 -0
- package/dist/types/hooks/useClipboard.d.ts +7 -0
- package/dist/types/hooks/useContextMenu.d.ts +1 -0
- package/dist/types/hooks/useDataGridState.d.ts +12 -0
- package/dist/types/hooks/useFillHandle.d.ts +3 -0
- package/dist/types/hooks/useKeyboardNavigation.d.ts +2 -1
- package/dist/types/hooks/useUndoRedo.d.ts +5 -1
- package/dist/types/index.d.ts +5 -3
- package/dist/types/types/columnTypes.d.ts +17 -0
- package/dist/types/types/dataGridTypes.d.ts +8 -0
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/utils/dataGridViewModel.d.ts +9 -0
- package/dist/types/utils/gridContextMenuHelpers.d.ts +8 -0
- package/dist/types/utils/index.d.ts +3 -1
- package/dist/types/utils/valueParsers.d.ts +37 -0
- 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 = '
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
}, [onCellValueChanged, maxHistory]);
|
|
41
|
+
}, [maxHistory]);
|
|
21
42
|
const undo = useCallback(() => {
|
|
22
43
|
if (!onCellValueChanged || historyRef.current.length === 0)
|
|
23
44
|
return;
|
|
24
|
-
const
|
|
45
|
+
const lastBatch = historyRef.current[historyRef.current.length - 1];
|
|
25
46
|
historyRef.current = historyRef.current.slice(0, -1);
|
|
26
|
-
redoStackRef.current = [...redoStackRef.current,
|
|
47
|
+
redoStackRef.current = [...redoStackRef.current, lastBatch];
|
|
27
48
|
setHistoryLength(historyRef.current.length);
|
|
28
49
|
setRedoLength(redoStackRef.current.length);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
63
|
+
const nextBatch = redoStackRef.current[redoStackRef.current.length - 1];
|
|
39
64
|
redoStackRef.current = redoStackRef.current.slice(0, -1);
|
|
40
|
-
historyRef.current = [...historyRef.current,
|
|
65
|
+
historyRef.current = [...historyRef.current, nextBatch];
|
|
41
66
|
setRedoLength(redoStackRef.current.length);
|
|
42
67
|
setHistoryLength(historyRef.current.length);
|
|
43
|
-
|
|
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
|
|
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: '
|
|
3
|
-
{ id: '
|
|
4
|
-
{ id: '
|
|
5
|
-
{ id: '
|
|
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();
|
package/dist/esm/utils/index.js
CHANGED
|
@@ -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;
|
|
@@ -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
|
|
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
|
-
*
|
|
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>;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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 {
|
|
12
|
-
export type {
|
|
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';
|