@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.
- package/dist/esm/constants/index.js +3 -3
- package/dist/esm/constants/timing.js +2 -2
- package/dist/esm/index.js +54 -10
- package/dist/esm/utils/cellValue.js +6 -0
- package/dist/esm/utils/clientSideData.js +19 -9
- package/dist/esm/utils/clipboardHelpers.js +86 -1
- package/dist/esm/utils/columnReorder.js +4 -7
- package/dist/esm/utils/dataGridViewModel.js +6 -9
- package/dist/esm/utils/exportToCsv.js +21 -10
- package/dist/esm/utils/fillHelpers.js +47 -0
- package/dist/esm/utils/gridRowComparator.js +3 -3
- package/dist/esm/utils/index.js +6 -4
- package/dist/esm/utils/keyboardNavigation.js +116 -7
- package/dist/esm/utils/ogridHelpers.js +17 -14
- package/dist/esm/utils/selectionHelpers.js +48 -0
- package/dist/esm/utils/undoRedoStack.js +17 -12
- package/dist/esm/utils/validation.js +43 -0
- package/dist/esm/utils/virtualScroll.js +2 -2
- package/dist/types/constants/index.d.ts +4 -3
- package/dist/types/constants/timing.d.ts +2 -2
- package/dist/types/index.d.ts +45 -6
- package/dist/types/utils/cellValue.d.ts +6 -0
- package/dist/types/utils/clipboardHelpers.d.ts +24 -1
- package/dist/types/utils/columnReorder.d.ts +21 -10
- package/dist/types/utils/exportToCsv.d.ts +9 -0
- package/dist/types/utils/fillHelpers.d.ts +18 -0
- package/dist/types/utils/index.d.ts +8 -5
- package/dist/types/utils/keyboardNavigation.d.ts +50 -3
- package/dist/types/utils/ogridHelpers.d.ts +3 -1
- package/dist/types/utils/selectionHelpers.d.ts +27 -0
- package/dist/types/utils/validation.d.ts +13 -0
- package/package.json +9 -2
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export
|
|
7
|
-
//
|
|
8
|
-
export {
|
|
9
|
-
|
|
10
|
-
export {
|
|
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
|
|
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
|
|
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 <
|
|
98
|
-
const row =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
32
|
-
*
|
|
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(
|
|
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
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
|
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
|
|
58
|
-
prev.selectionRange
|
|
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
|
package/dist/esm/utils/index.js
CHANGED
|
@@ -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
|
-
|
|
3
|
-
|
|
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
|
+
}
|