@alaarab/ogrid-core 2.0.22 → 2.1.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.
Files changed (32) hide show
  1. package/dist/esm/constants/index.js +3 -3
  2. package/dist/esm/constants/timing.js +2 -2
  3. package/dist/esm/index.js +54 -10
  4. package/dist/esm/utils/cellValue.js +6 -0
  5. package/dist/esm/utils/clientSideData.js +19 -9
  6. package/dist/esm/utils/clipboardHelpers.js +86 -1
  7. package/dist/esm/utils/columnReorder.js +4 -7
  8. package/dist/esm/utils/dataGridViewModel.js +6 -9
  9. package/dist/esm/utils/exportToCsv.js +21 -10
  10. package/dist/esm/utils/fillHelpers.js +47 -0
  11. package/dist/esm/utils/gridRowComparator.js +3 -3
  12. package/dist/esm/utils/index.js +6 -4
  13. package/dist/esm/utils/keyboardNavigation.js +116 -7
  14. package/dist/esm/utils/ogridHelpers.js +17 -14
  15. package/dist/esm/utils/selectionHelpers.js +48 -0
  16. package/dist/esm/utils/undoRedoStack.js +17 -12
  17. package/dist/esm/utils/validation.js +43 -0
  18. package/dist/esm/utils/virtualScroll.js +2 -2
  19. package/dist/types/constants/index.d.ts +4 -3
  20. package/dist/types/constants/timing.d.ts +2 -2
  21. package/dist/types/index.d.ts +45 -6
  22. package/dist/types/utils/cellValue.d.ts +6 -0
  23. package/dist/types/utils/clipboardHelpers.d.ts +24 -1
  24. package/dist/types/utils/columnReorder.d.ts +21 -10
  25. package/dist/types/utils/exportToCsv.d.ts +9 -0
  26. package/dist/types/utils/fillHelpers.d.ts +18 -0
  27. package/dist/types/utils/index.d.ts +8 -5
  28. package/dist/types/utils/keyboardNavigation.d.ts +50 -3
  29. package/dist/types/utils/ogridHelpers.d.ts +3 -1
  30. package/dist/types/utils/selectionHelpers.d.ts +27 -0
  31. package/dist/types/utils/validation.d.ts +13 -0
  32. package/package.json +9 -2
@@ -1,3 +1,3 @@
1
- export * from './layout';
2
- export * from './timing';
3
- export * from './zIndex';
1
+ export { CHECKBOX_COLUMN_WIDTH, ROW_NUMBER_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, GRID_BORDER_RADIUS, } from './layout';
2
+ export { DEFAULT_DEBOUNCE_MS, PEOPLE_SEARCH_DEBOUNCE_MS, SIDEBAR_TRANSITION_MS, } from './timing';
3
+ export { Z_INDEX } from './zIndex';
@@ -2,9 +2,9 @@
2
2
  * Timing constants used across OGrid.
3
3
  * Centralizes magic numbers for consistency and easier tuning.
4
4
  */
5
- /** Debounce delay for people/user search inputs (milliseconds) */
6
- export const PEOPLE_SEARCH_DEBOUNCE_MS = 300;
7
5
  /** Default debounce delay for generic inputs (milliseconds) */
8
6
  export const DEFAULT_DEBOUNCE_MS = 300;
7
+ /** Debounce delay for people/user search inputs (milliseconds) */
8
+ export const PEOPLE_SEARCH_DEBOUNCE_MS = DEFAULT_DEBOUNCE_MS;
9
9
  /** Sidebar panel transition duration (milliseconds) */
10
10
  export const SIDEBAR_TRANSITION_MS = 300;
package/dist/esm/index.js CHANGED
@@ -1,10 +1,54 @@
1
- // Types
2
- export * from './types';
3
- // Utils
4
- export * from './utils';
5
- // Constants
6
- export * from './constants';
7
- // Explicit constant exports for better test resolution
8
- export { CHECKBOX_COLUMN_WIDTH, ROW_NUMBER_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, GRID_BORDER_RADIUS, } from './constants/layout';
9
- export { PEOPLE_SEARCH_DEBOUNCE_MS, DEFAULT_DEBOUNCE_MS, SIDEBAR_TRANSITION_MS, } from './constants/timing';
10
- export { Z_INDEX } from './constants/zIndex';
1
+ export { toUserLike, isInSelectionRange, normalizeSelectionRange, } from './types';
2
+ // Utils exportToCsv
3
+ export { escapeCsvValue, buildCsvHeader, buildCsvRows, exportToCsv, triggerCsvDownload, } from './utils';
4
+ // Utils cellValue, columnUtils
5
+ export { getCellValue } from './utils';
6
+ export { flattenColumns, buildHeaderRows } from './utils';
7
+ // Utils ogridHelpers
8
+ export { isFilterConfig, getFilterField, mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, } from './utils';
9
+ // Utils statusBarHelpers, dataGridStatusBar
10
+ export { getStatusBarParts } from './utils';
11
+ export { getDataGridStatusBarConfig } from './utils';
12
+ // Utils — paginationHelpers
13
+ export { getPaginationViewModel, PAGE_SIZE_OPTIONS, MAX_PAGE_BUTTONS, } from './utils';
14
+ // Utils — gridContextMenuHelpers
15
+ export { GRID_CONTEXT_MENU_ITEMS, COLUMN_HEADER_MENU_ITEMS, getContextMenuHandlers, getColumnHeaderMenuItems, formatShortcut, } from './utils';
16
+ // Utils — valueParsers
17
+ export { parseValue, numberParser, currencyParser, dateParser, emailParser, booleanParser, } from './utils';
18
+ // Utils — aggregationUtils
19
+ export { computeAggregations } from './utils';
20
+ // Utils — clientSideData
21
+ export { processClientSideData } from './utils';
22
+ // Utils — gridRowComparator
23
+ export { areGridRowPropsEqual, isRowInRange } from './utils';
24
+ // Utils — columnReorder
25
+ export { getPinStateForColumn, reorderColumnArray, calculateDropTarget, } from './utils';
26
+ // Utils — virtualScroll
27
+ export { computeVisibleRange, computeTotalHeight, getScrollTopForRow, } from './utils';
28
+ // Utils — dataGridViewModel
29
+ export { getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, } from './utils';
30
+ // Utils — debounce, dom
31
+ export { debounce } from './utils';
32
+ export { measureRange, injectGlobalStyles } from './utils';
33
+ // Utils — sortHelpers
34
+ export { computeNextSortState } from './utils';
35
+ // Utils — columnAutosize
36
+ export { measureColumnContentWidth, AUTOSIZE_EXTRA_PX, AUTOSIZE_MAX_PX } from './utils';
37
+ // Utils — keyboardNavigation
38
+ export { findCtrlArrowTarget, computeTabNavigation, computeArrowNavigation, applyCellDeletion } from './utils';
39
+ // Utils — selectionHelpers
40
+ export { rangesEqual, clampSelectionToBounds, computeAutoScrollSpeed, applyRangeRowSelection, computeRowSelectionState } from './utils';
41
+ // Utils — clipboardHelpers
42
+ export { formatCellValueForTsv, formatSelectionAsTsv, parseTsvClipboard, applyPastedValues, applyCutClear, } from './utils';
43
+ // Utils — fillHelpers
44
+ export { applyFillValues } from './utils';
45
+ // Utils — undoRedoStack
46
+ export { UndoRedoStack } from './utils';
47
+ // Utils — validation
48
+ export { validateColumns, validateRowIds } from './utils';
49
+ // Constants — layout
50
+ export { CHECKBOX_COLUMN_WIDTH, ROW_NUMBER_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, GRID_BORDER_RADIUS, } from './constants';
51
+ // Constants — timing
52
+ export { DEFAULT_DEBOUNCE_MS, PEOPLE_SEARCH_DEBOUNCE_MS, SIDEBAR_TRANSITION_MS, } from './constants';
53
+ // Constants — zIndex
54
+ export { Z_INDEX } from './constants';
@@ -1,5 +1,11 @@
1
1
  /**
2
2
  * Get the cell value for a row/column, using valueGetter when defined otherwise item[columnId].
3
+ *
4
+ * @param item - The row data object.
5
+ * @param col - Column definition. If `valueGetter` is defined it takes priority;
6
+ * otherwise the value is read via `item[col.columnId]`.
7
+ * Assumes `columnId` is a valid key on the item when no `valueGetter` is provided.
8
+ * @returns The raw cell value (`unknown`). May be `undefined` if the key does not exist on the item.
3
9
  */
4
10
  export function getCellValue(item, col) {
5
11
  if (col.valueGetter)
@@ -36,6 +36,9 @@ export function processClientSideData(data, columns, filters, sortBy, sortDirect
36
36
  continue;
37
37
  switch (val.type) {
38
38
  case 'multiSelect':
39
+ // NOTE: Cell values are coerced to string via String() for set membership checks.
40
+ // Object-typed column values will produce "[object Object]" — use valueGetter or
41
+ // valueFormatter on the column def to ensure meaningful string representation.
39
42
  if (val.value.length > 0) {
40
43
  const allowedSet = new Set(val.value);
41
44
  predicates.push((r) => allowedSet.has(String(getCellValue(r, col))));
@@ -76,7 +79,8 @@ export function processClientSideData(data, columns, filters, sortBy, sortDirect
76
79
  }
77
80
  }
78
81
  }
79
- const rows = predicates.length > 0
82
+ const filtered = predicates.length > 0;
83
+ const rows = filtered
80
84
  ? data.filter((row) => {
81
85
  for (let i = 0; i < predicates.length; i++) {
82
86
  if (!predicates[i](row))
@@ -84,18 +88,23 @@ export function processClientSideData(data, columns, filters, sortBy, sortDirect
84
88
  }
85
89
  return true;
86
90
  })
87
- : data.slice();
91
+ : data;
88
92
  // --- Sorting ---
89
93
  if (sortBy) {
94
+ // Copy before sorting if we didn't filter (filter already creates a new array).
95
+ // This avoids mutating the caller's original data array.
96
+ const sortable = filtered ? rows : rows.slice();
90
97
  const sortCol = columnMap.get(sortBy);
91
98
  const compare = sortCol?.compare;
92
99
  const dir = sortDirection === 'asc' ? 1 : -1;
93
100
  const isDateSort = sortCol?.type === 'date';
94
- // For date columns, pre-compute timestamps to avoid repeated new Date() in O(n log n) comparisons
101
+ // For date columns, pre-compute timestamps to avoid repeated new Date() in O(n log n) comparisons.
102
+ // NOTE: The timestamp cache is scoped to this single sort invocation. It is rebuilt on every call,
103
+ // so mutating row objects between calls is safe — stale timestamps cannot persist across invocations.
95
104
  if (isDateSort && !compare) {
96
105
  const timestampCache = new Map();
97
- for (let i = 0; i < rows.length; i++) {
98
- const row = rows[i];
106
+ for (let i = 0; i < sortable.length; i++) {
107
+ const row = sortable[i];
99
108
  const val = sortCol ? getCellValue(row, sortCol) : row[sortBy];
100
109
  if (val == null) {
101
110
  timestampCache.set(row, NaN);
@@ -105,9 +114,9 @@ export function processClientSideData(data, columns, filters, sortBy, sortDirect
105
114
  timestampCache.set(row, Number.isNaN(t) ? 0 : t);
106
115
  }
107
116
  }
108
- rows.sort((a, b) => {
109
- const at = timestampCache.get(a);
110
- const bt = timestampCache.get(b);
117
+ sortable.sort((a, b) => {
118
+ const at = timestampCache.get(a) ?? NaN;
119
+ const bt = timestampCache.get(b) ?? NaN;
111
120
  if (Number.isNaN(at) && Number.isNaN(bt))
112
121
  return 0;
113
122
  if (Number.isNaN(at))
@@ -118,7 +127,7 @@ export function processClientSideData(data, columns, filters, sortBy, sortDirect
118
127
  });
119
128
  }
120
129
  else {
121
- rows.sort((a, b) => {
130
+ sortable.sort((a, b) => {
122
131
  if (compare)
123
132
  return compare(a, b) * dir;
124
133
  const av = sortCol
@@ -140,6 +149,7 @@ export function processClientSideData(data, columns, filters, sortBy, sortDirect
140
149
  return as === bs ? 0 : as > bs ? dir : -dir;
141
150
  });
142
151
  }
152
+ return sortable;
143
153
  }
144
154
  return rows;
145
155
  }
@@ -1,4 +1,5 @@
1
1
  import { getCellValue } from './cellValue';
2
+ import { parseValue } from './valueParsers';
2
3
  import { normalizeSelectionRange } from '../types';
3
4
  /**
4
5
  * Format a single cell value for inclusion in a TSV clipboard string.
@@ -12,7 +13,12 @@ export function formatCellValueForTsv(raw, formatted) {
12
13
  const val = formatted != null && formatted !== '' ? formatted : raw;
13
14
  if (val == null || val === '')
14
15
  return '';
15
- return String(val).replace(/[\t\n]/g, ' ');
16
+ try {
17
+ return String(val).replace(/[\t\n]/g, ' ');
18
+ }
19
+ catch {
20
+ return '[Object]';
21
+ }
16
22
  }
17
23
  /**
18
24
  * Serialize a rectangular cell range to a TSV (tab-separated values) string
@@ -55,3 +61,82 @@ export function parseTsvClipboard(text) {
55
61
  const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
56
62
  return lines.map((line) => line.split('\t'));
57
63
  }
64
+ /**
65
+ * Apply parsed clipboard rows to the grid starting at anchor position.
66
+ * For each cell in the parsed rows, validates editability, parses the value,
67
+ * and produces a cell value changed event.
68
+ *
69
+ * @param parsedRows 2D array of string values (from parseTsvClipboard).
70
+ * @param anchorRow Target starting row index.
71
+ * @param anchorCol Target starting column index (data column, not absolute).
72
+ * @param items Array of all row data objects.
73
+ * @param visibleCols Visible column definitions.
74
+ * @returns Array of cell value changed events to apply.
75
+ */
76
+ export function applyPastedValues(parsedRows, anchorRow, anchorCol, items, visibleCols) {
77
+ const events = [];
78
+ for (let r = 0; r < parsedRows.length; r++) {
79
+ const cells = parsedRows[r];
80
+ for (let c = 0; c < cells.length; c++) {
81
+ const targetRow = anchorRow + r;
82
+ const targetCol = anchorCol + c;
83
+ if (targetRow >= items.length || targetCol >= visibleCols.length)
84
+ continue;
85
+ const item = items[targetRow];
86
+ const col = visibleCols[targetCol];
87
+ const colEditable = col.editable === true ||
88
+ (typeof col.editable === 'function' && col.editable(item));
89
+ if (!colEditable)
90
+ continue;
91
+ const rawValue = cells[c] ?? '';
92
+ const oldValue = getCellValue(item, col);
93
+ const result = parseValue(rawValue, oldValue, item, col);
94
+ if (!result.valid)
95
+ continue;
96
+ events.push({
97
+ item,
98
+ columnId: col.columnId,
99
+ oldValue,
100
+ newValue: result.value,
101
+ rowIndex: targetRow,
102
+ });
103
+ }
104
+ }
105
+ return events;
106
+ }
107
+ /**
108
+ * Clear cells in a cut range by setting each editable cell to an empty-string-parsed value.
109
+ * Used after pasting cut content to clear the original cells.
110
+ *
111
+ * @param cutRange The normalized range of cells to clear.
112
+ * @param items Array of all row data objects.
113
+ * @param visibleCols Visible column definitions.
114
+ * @returns Array of cell value changed events to apply.
115
+ */
116
+ export function applyCutClear(cutRange, items, visibleCols) {
117
+ const events = [];
118
+ for (let r = cutRange.startRow; r <= cutRange.endRow; r++) {
119
+ for (let c = cutRange.startCol; c <= cutRange.endCol; c++) {
120
+ if (r >= items.length || c >= visibleCols.length)
121
+ continue;
122
+ const item = items[r];
123
+ const col = visibleCols[c];
124
+ const colEditable = col.editable === true ||
125
+ (typeof col.editable === 'function' && col.editable(item));
126
+ if (!colEditable)
127
+ continue;
128
+ const oldValue = getCellValue(item, col);
129
+ const result = parseValue('', oldValue, item, col);
130
+ if (!result.valid)
131
+ continue;
132
+ events.push({
133
+ item,
134
+ columnId: col.columnId,
135
+ oldValue,
136
+ newValue: result.value,
137
+ rowIndex: r,
138
+ });
139
+ }
140
+ }
141
+ return events;
142
+ }
@@ -28,15 +28,12 @@ export function reorderColumnArray(order, columnId, targetIndex) {
28
28
  * finds the midpoint of each header cell, and determines insertion side.
29
29
  * Respects pinning zones: a left-pinned column can only drop among left-pinned, etc.
30
30
  *
31
- * @param mouseX - Current mouse X position (client coordinates)
32
- * @param columnOrder - Current column display order (array of column ids)
33
- * @param draggedColumnId - The column being dragged
34
- * @param draggedPinState - Pin state of the dragged column
35
- * @param tableElement - The table (or grid container) DOM element to query headers from
36
- * @param pinnedColumns - Pinned column configuration
31
+ * @param params - Options object containing mouseX, columnOrder, draggedColumnId,
32
+ * draggedPinState, tableElement, and optional pinnedColumns.
37
33
  * @returns Drop target with insertion index and indicator X, or null if no valid target.
38
34
  */
39
- export function calculateDropTarget(mouseX, columnOrder, draggedColumnId, draggedPinState, tableElement, pinnedColumns) {
35
+ export function calculateDropTarget(params) {
36
+ const { mouseX, columnOrder, draggedColumnId, draggedPinState, tableElement, pinnedColumns } = params;
40
37
  const headerCells = tableElement.querySelectorAll('[data-column-id]');
41
38
  if (headerCells.length === 0)
42
39
  return null;
@@ -5,11 +5,12 @@
5
5
  */
6
6
  import { getCellValue } from './cellValue';
7
7
  import { isInSelectionRange } from '../types/dataGridTypes';
8
+ import { isFilterConfig } from './ogridHelpers';
8
9
  /**
9
10
  * Returns ColumnHeaderFilter props from column def and grid filter/sort state.
10
11
  */
11
12
  export function getHeaderFilterConfig(col, input) {
12
- const filterable = col.filterable && typeof col.filterable === 'object' ? col.filterable : null;
13
+ const filterable = isFilterConfig(col.filterable) ? col.filterable : null;
13
14
  const filterType = (filterable?.type ?? 'none');
14
15
  const filterField = filterable?.filterField ?? col.columnId;
15
16
  const sortable = col.sortable !== false;
@@ -93,9 +94,10 @@ export function getCellRenderDescriptor(item, col, rowIndex, colIdx, input) {
93
94
  colIdx === input.selectionRange.endCol;
94
95
  const isPinned = col.pinned != null;
95
96
  const pinnedSide = col.pinned ?? undefined;
97
+ // Compute cell value once — used in editing and display branches
98
+ const cellValue = getCellValue(item, col);
96
99
  let mode = 'display';
97
100
  let editorType;
98
- let value;
99
101
  if (isEditing && canEditInline) {
100
102
  mode = 'editing-inline';
101
103
  if (col.cellEditor === 'text' ||
@@ -114,19 +116,14 @@ export function getCellRenderDescriptor(item, col, rowIndex, colIdx, input) {
114
116
  else {
115
117
  editorType = 'text';
116
118
  }
117
- value = getCellValue(item, col);
118
119
  }
119
120
  else if (isEditing && canEditPopup && typeof col.cellEditor === 'function') {
120
121
  mode = 'editing-popover';
121
- value = getCellValue(item, col);
122
- }
123
- else {
124
- value = getCellValue(item, col);
125
122
  }
126
123
  return {
127
124
  mode,
128
125
  editorType,
129
- value,
126
+ value: cellValue,
130
127
  isActive,
131
128
  isInRange,
132
129
  isInCutRange,
@@ -138,7 +135,7 @@ export function getCellRenderDescriptor(item, col, rowIndex, colIdx, input) {
138
135
  globalColIndex,
139
136
  rowId,
140
137
  rowIndex,
141
- displayValue: value,
138
+ displayValue: cellValue,
142
139
  };
143
140
  }
144
141
  /**
@@ -20,20 +20,31 @@ export function exportToCsv(items, columns, getValue, filename) {
20
20
  const csv = [header, ...rows].join('\n');
21
21
  triggerCsvDownload(csv, filename ?? `export_${new Date().toISOString().slice(0, 10)}.csv`);
22
22
  }
23
+ /**
24
+ * Triggers a browser CSV file download.
25
+ *
26
+ * NOTE: This function uses DOM APIs (document.createElement, document.body) and therefore
27
+ * requires a browser environment. It is intentionally kept in the core package because all
28
+ * framework packages (React, Angular, Vue, JS) need CSV export, and duplicating it would be
29
+ * worse than the DOM dependency. In server-side rendering (SSR) contexts, call exportToCsv
30
+ * only from browser-side code (e.g. event handlers), not during server rendering.
31
+ */
23
32
  export function triggerCsvDownload(csvContent, filename) {
24
33
  const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
25
- const link = document.createElement('a');
26
34
  const url = URL.createObjectURL(blob);
27
- link.setAttribute('href', url);
28
- link.setAttribute('download', filename);
29
- link.style.visibility = 'hidden';
30
- document.body.appendChild(link);
31
- link.click();
35
+ const link = document.createElement('a');
32
36
  try {
33
- document.body.removeChild(link);
37
+ link.setAttribute('href', url);
38
+ link.setAttribute('download', filename);
39
+ link.style.visibility = 'hidden';
40
+ document.body.appendChild(link);
41
+ link.click();
34
42
  }
35
- catch {
36
- // Ignore if removeChild fails (e.g. link was not actually appended in test env)
43
+ finally {
44
+ try {
45
+ document.body.removeChild(link);
46
+ }
47
+ catch { /* noop */ }
48
+ URL.revokeObjectURL(url);
37
49
  }
38
- URL.revokeObjectURL(url);
39
50
  }
@@ -0,0 +1,47 @@
1
+ import { getCellValue } from './cellValue';
2
+ import { parseValue } from './valueParsers';
3
+ /**
4
+ * Apply fill values from a source cell across a normalized selection range.
5
+ * Copies the value from the start cell of the range to every other editable cell.
6
+ *
7
+ * @param range The normalized fill range (startRow/startCol is the source).
8
+ * @param sourceRow The original source row index (skipped during fill).
9
+ * @param sourceCol The original source col index (skipped during fill).
10
+ * @param items Array of all row data objects.
11
+ * @param visibleCols Visible column definitions.
12
+ * @returns Array of cell value changed events to apply. Empty if source cell is out of bounds.
13
+ */
14
+ export function applyFillValues(range, sourceRow, sourceCol, items, visibleCols) {
15
+ const events = [];
16
+ const startItem = items[range.startRow];
17
+ const startColDef = visibleCols[range.startCol];
18
+ if (!startItem || !startColDef)
19
+ return events;
20
+ const startValue = getCellValue(startItem, startColDef);
21
+ for (let row = range.startRow; row <= range.endRow; row++) {
22
+ for (let col = range.startCol; col <= range.endCol; col++) {
23
+ if (row === sourceRow && col === sourceCol)
24
+ continue;
25
+ if (row >= items.length || col >= visibleCols.length)
26
+ continue;
27
+ const item = items[row];
28
+ const colDef = visibleCols[col];
29
+ const colEditable = colDef.editable === true ||
30
+ (typeof colDef.editable === 'function' && colDef.editable(item));
31
+ if (!colEditable)
32
+ continue;
33
+ const oldValue = getCellValue(item, colDef);
34
+ const result = parseValue(startValue, oldValue, item, colDef);
35
+ if (!result.valid)
36
+ continue;
37
+ events.push({
38
+ item,
39
+ columnId: colDef.columnId,
40
+ oldValue,
41
+ newValue: result.value,
42
+ rowIndex: row,
43
+ });
44
+ }
45
+ }
46
+ return events;
47
+ }
@@ -46,7 +46,7 @@ export function areGridRowPropsEqual(prev, next) {
46
46
  const nextActive = next.activeCell?.rowIndex === ri;
47
47
  if (prevActive !== nextActive)
48
48
  return false;
49
- if (prevActive && nextActive && prev.activeCell.columnIndex !== next.activeCell.columnIndex)
49
+ if (prevActive && nextActive && prev.activeCell?.columnIndex !== next.activeCell?.columnIndex)
50
50
  return false;
51
51
  // Selection range touches this row?
52
52
  const prevInSel = isRowInRange(prev.selectionRange, ri);
@@ -54,8 +54,8 @@ export function areGridRowPropsEqual(prev, next) {
54
54
  if (prevInSel !== nextInSel)
55
55
  return false;
56
56
  if (prevInSel && nextInSel) {
57
- if (prev.selectionRange.startCol !== next.selectionRange.startCol ||
58
- prev.selectionRange.endCol !== next.selectionRange.endCol)
57
+ if (prev.selectionRange?.startCol !== next.selectionRange?.startCol ||
58
+ prev.selectionRange?.endCol !== next.selectionRange?.endCol)
59
59
  return false;
60
60
  }
61
61
  // Fill handle (selection end row) + isDragging
@@ -1,7 +1,7 @@
1
1
  export { escapeCsvValue, buildCsvHeader, buildCsvRows, exportToCsv, triggerCsvDownload, } from './exportToCsv';
2
2
  export { getCellValue } from './cellValue';
3
3
  export { flattenColumns, buildHeaderRows } from './columnUtils';
4
- export { getFilterField, mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, } from './ogridHelpers';
4
+ export { isFilterConfig, getFilterField, mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, } from './ogridHelpers';
5
5
  export { getStatusBarParts } from './statusBarHelpers';
6
6
  export { getDataGridStatusBarConfig } from './dataGridStatusBar';
7
7
  export { getPaginationViewModel, PAGE_SIZE_OPTIONS, MAX_PAGE_BUTTONS, } from './paginationHelpers';
@@ -17,7 +17,9 @@ export { debounce } from './debounce';
17
17
  export { measureRange, injectGlobalStyles } from './dom';
18
18
  export { computeNextSortState } from './sortHelpers';
19
19
  export { measureColumnContentWidth, AUTOSIZE_EXTRA_PX, AUTOSIZE_MAX_PX } from './columnAutosize';
20
- export { findCtrlArrowTarget, computeTabNavigation } from './keyboardNavigation';
21
- export { rangesEqual, clampSelectionToBounds, computeAutoScrollSpeed } from './selectionHelpers';
22
- export { formatCellValueForTsv, formatSelectionAsTsv, parseTsvClipboard, } from './clipboardHelpers';
20
+ export { findCtrlArrowTarget, computeTabNavigation, computeArrowNavigation, applyCellDeletion } from './keyboardNavigation';
21
+ export { rangesEqual, clampSelectionToBounds, computeAutoScrollSpeed, applyRangeRowSelection, computeRowSelectionState } from './selectionHelpers';
22
+ export { formatCellValueForTsv, formatSelectionAsTsv, parseTsvClipboard, applyPastedValues, applyCutClear, } from './clipboardHelpers';
23
+ export { applyFillValues } from './fillHelpers';
23
24
  export { UndoRedoStack } from './undoRedoStack';
25
+ export { validateColumns, validateRowIds } from './validation';
@@ -1,7 +1,6 @@
1
- /**
2
- * Pure keyboard navigation helpers shared across React, Vue, Angular, and JS.
3
- * No framework dependencies — takes plain values, returns plain values.
4
- */
1
+ import { normalizeSelectionRange } from '../types/dataGridTypes';
2
+ import { getCellValue } from './cellValue';
3
+ import { parseValue } from './valueParsers';
5
4
  /**
6
5
  * Excel-style Ctrl+Arrow: find the target position along a 1D axis.
7
6
  * - Non-empty current + non-empty next → scan through non-empties, stop at last before empty/edge.
@@ -41,11 +40,11 @@ export function findCtrlArrowTarget(pos, edge, step, isEmpty) {
41
40
  *
42
41
  * @param rowIndex Current row index.
43
42
  * @param columnIndex Current absolute column index (includes checkbox offset).
44
- * @param maxRowIndex Maximum row index (items.length - 1).
45
- * @param maxColIndex Maximum absolute column index.
43
+ * @param maxRowIndex Maximum row index (items.length - 1). Must be >= 0.
44
+ * @param maxColIndex Maximum absolute column index. Must be >= 0.
46
45
  * @param colOffset Number of non-data leading columns (checkbox column offset).
47
46
  * @param shiftKey True if Shift is held (backward tab).
48
- * @returns New { rowIndex, columnIndex } after tab.
47
+ * @returns New { rowIndex, columnIndex } after tab. Caller must ensure maxRowIndex and maxColIndex are non-negative.
49
48
  */
50
49
  export function computeTabNavigation(rowIndex, columnIndex, maxRowIndex, maxColIndex, colOffset, shiftKey) {
51
50
  let newRow = rowIndex;
@@ -70,3 +69,113 @@ export function computeTabNavigation(rowIndex, columnIndex, maxRowIndex, maxColI
70
69
  }
71
70
  return { rowIndex: newRow, columnIndex: newCol };
72
71
  }
72
+ /**
73
+ * Computes the next active cell position and selection range for a single arrow key press.
74
+ * Handles Ctrl+Arrow (jump to edge), Shift+Arrow (extend selection), and plain Arrow (move).
75
+ *
76
+ * Pure function — no framework dependencies.
77
+ *
78
+ * @param ctx Arrow navigation context with current position, direction, modifiers, and grid bounds.
79
+ * @returns The new row/column indices and selection range.
80
+ */
81
+ export function computeArrowNavigation(ctx) {
82
+ const { direction, rowIndex, columnIndex, dataColIndex, colOffset, maxRowIndex, maxColIndex, visibleColCount, isCtrl, isShift, selectionRange, isEmptyAt, } = ctx;
83
+ let newRowIndex = rowIndex;
84
+ let newColumnIndex = columnIndex;
85
+ if (direction === 'ArrowDown') {
86
+ newRowIndex = isCtrl
87
+ ? findCtrlArrowTarget(rowIndex, maxRowIndex, 1, (r) => isEmptyAt(r, Math.max(0, dataColIndex)))
88
+ : Math.min(rowIndex + 1, maxRowIndex);
89
+ }
90
+ else if (direction === 'ArrowUp') {
91
+ newRowIndex = isCtrl
92
+ ? findCtrlArrowTarget(rowIndex, 0, -1, (r) => isEmptyAt(r, Math.max(0, dataColIndex)))
93
+ : Math.max(rowIndex - 1, 0);
94
+ }
95
+ else if (direction === 'ArrowRight') {
96
+ if (isCtrl && dataColIndex >= 0) {
97
+ newColumnIndex = findCtrlArrowTarget(dataColIndex, visibleColCount - 1, 1, (c) => isEmptyAt(rowIndex, c)) + colOffset;
98
+ }
99
+ else {
100
+ newColumnIndex = Math.min(columnIndex + 1, maxColIndex);
101
+ }
102
+ }
103
+ else { // ArrowLeft
104
+ if (isCtrl && dataColIndex >= 0) {
105
+ newColumnIndex = findCtrlArrowTarget(dataColIndex, 0, -1, (c) => isEmptyAt(rowIndex, c)) + colOffset;
106
+ }
107
+ else {
108
+ newColumnIndex = Math.max(columnIndex - 1, colOffset);
109
+ }
110
+ }
111
+ const newDataColIndex = newColumnIndex - colOffset;
112
+ const isVertical = direction === 'ArrowDown' || direction === 'ArrowUp';
113
+ let newRange;
114
+ if (isShift) {
115
+ if (isVertical) {
116
+ newRange = normalizeSelectionRange({
117
+ startRow: selectionRange?.startRow ?? rowIndex,
118
+ startCol: selectionRange?.startCol ?? dataColIndex,
119
+ endRow: newRowIndex,
120
+ endCol: selectionRange?.endCol ?? dataColIndex,
121
+ });
122
+ }
123
+ else {
124
+ newRange = normalizeSelectionRange({
125
+ startRow: selectionRange?.startRow ?? rowIndex,
126
+ startCol: selectionRange?.startCol ?? dataColIndex,
127
+ endRow: selectionRange?.endRow ?? rowIndex,
128
+ endCol: newDataColIndex,
129
+ });
130
+ }
131
+ }
132
+ else {
133
+ newRange = {
134
+ startRow: newRowIndex,
135
+ startCol: newDataColIndex,
136
+ endRow: newRowIndex,
137
+ endCol: newDataColIndex,
138
+ };
139
+ }
140
+ return { newRowIndex, newColumnIndex, newDataColIndex, newRange };
141
+ }
142
+ /**
143
+ * Apply cell deletion (Delete/Backspace key) across a selection range.
144
+ * For each editable cell in the range, parses an empty string as the new value
145
+ * and emits a cell value changed event.
146
+ *
147
+ * Pure function — no framework dependencies.
148
+ *
149
+ * @param range The normalized selection range to clear.
150
+ * @param items Array of all row data objects.
151
+ * @param visibleCols Visible column definitions.
152
+ * @returns Array of cell value changed events to apply.
153
+ */
154
+ export function applyCellDeletion(range, items, visibleCols) {
155
+ const norm = normalizeSelectionRange(range);
156
+ const events = [];
157
+ for (let r = norm.startRow; r <= norm.endRow; r++) {
158
+ for (let c = norm.startCol; c <= norm.endCol; c++) {
159
+ if (r >= items.length || c >= visibleCols.length)
160
+ continue;
161
+ const item = items[r];
162
+ const col = visibleCols[c];
163
+ const colEditable = col.editable === true ||
164
+ (typeof col.editable === 'function' && col.editable(item));
165
+ if (!colEditable)
166
+ continue;
167
+ const oldValue = getCellValue(item, col);
168
+ const result = parseValue('', oldValue, item, col);
169
+ if (!result.valid)
170
+ continue;
171
+ events.push({
172
+ item,
173
+ columnId: col.columnId,
174
+ oldValue,
175
+ newValue: result.value,
176
+ rowIndex: r,
177
+ });
178
+ }
179
+ }
180
+ return events;
181
+ }