@alaarab/ogrid-core 2.1.2 → 2.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/index.js +1426 -54
- package/package.json +3 -3
- package/dist/esm/constants/index.js +0 -3
- package/dist/esm/constants/layout.js +0 -13
- package/dist/esm/constants/timing.js +0 -10
- package/dist/esm/constants/zIndex.js +0 -16
- package/dist/esm/types/columnTypes.js +0 -1
- package/dist/esm/types/dataGridTypes.js +0 -27
- package/dist/esm/types/index.js +0 -2
- package/dist/esm/utils/aggregationUtils.js +0 -48
- package/dist/esm/utils/cellValue.js +0 -14
- package/dist/esm/utils/clientSideData.js +0 -155
- package/dist/esm/utils/clipboardHelpers.js +0 -142
- package/dist/esm/utils/columnAutosize.js +0 -38
- package/dist/esm/utils/columnReorder.js +0 -99
- package/dist/esm/utils/columnUtils.js +0 -122
- package/dist/esm/utils/dataGridStatusBar.js +0 -15
- package/dist/esm/utils/dataGridViewModel.js +0 -206
- package/dist/esm/utils/debounce.js +0 -40
- package/dist/esm/utils/dom.js +0 -53
- package/dist/esm/utils/exportToCsv.js +0 -50
- package/dist/esm/utils/fillHelpers.js +0 -47
- package/dist/esm/utils/gridContextMenuHelpers.js +0 -80
- package/dist/esm/utils/gridRowComparator.js +0 -78
- package/dist/esm/utils/index.js +0 -25
- package/dist/esm/utils/keyboardNavigation.js +0 -181
- package/dist/esm/utils/ogridHelpers.js +0 -67
- package/dist/esm/utils/paginationHelpers.js +0 -58
- package/dist/esm/utils/selectionHelpers.js +0 -94
- package/dist/esm/utils/sortHelpers.js +0 -28
- package/dist/esm/utils/statusBarHelpers.js +0 -27
- package/dist/esm/utils/undoRedoStack.js +0 -130
- package/dist/esm/utils/validation.js +0 -43
- package/dist/esm/utils/valueParsers.js +0 -121
- package/dist/esm/utils/virtualScroll.js +0 -46
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
function isColumnGroupDef(c) {
|
|
2
|
-
return 'children' in c && Array.isArray(c.children);
|
|
3
|
-
}
|
|
4
|
-
/**
|
|
5
|
-
* Flattens a tree of column groups and column definitions into a single array of leaf columns.
|
|
6
|
-
* Used for body rendering and when the grid accepts grouped columns.
|
|
7
|
-
*/
|
|
8
|
-
export function flattenColumns(columns) {
|
|
9
|
-
const result = [];
|
|
10
|
-
for (const c of columns) {
|
|
11
|
-
if (isColumnGroupDef(c)) {
|
|
12
|
-
result.push(...flattenColumns(c.children));
|
|
13
|
-
}
|
|
14
|
-
else {
|
|
15
|
-
result.push(c);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return result;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Builds an array of header rows from a column tree for multi-row <thead> rendering.
|
|
22
|
-
*
|
|
23
|
-
* - Flat columns (no groups) produce a single row of leaf cells.
|
|
24
|
-
* - Grouped columns produce N rows where N = max nesting depth + 1.
|
|
25
|
-
* - Group cells get colSpan = number of visible leaf descendants.
|
|
26
|
-
* - Leaf cells at a depth shallower than maxDepth are placed at their own depth
|
|
27
|
-
* (the rendering layer can use rowSpan to stretch them down to the bottom row).
|
|
28
|
-
* - If visibleColumns is provided, only visible leaf columns and their ancestors are included.
|
|
29
|
-
*
|
|
30
|
-
* @param columns - The column tree (mix of IColumnDef and IColumnGroupDef)
|
|
31
|
-
* @param visibleColumns - Optional set of visible column ids (filters out hidden leaves + empty groups)
|
|
32
|
-
* @returns Array of HeaderRow, from top (group headers) to bottom (leaf columns)
|
|
33
|
-
*/
|
|
34
|
-
export function buildHeaderRows(columns, visibleColumns) {
|
|
35
|
-
// Step 1: Compute max depth of the column tree
|
|
36
|
-
function getMaxDepth(cols, depth) {
|
|
37
|
-
let max = depth;
|
|
38
|
-
for (const c of cols) {
|
|
39
|
-
if (isColumnGroupDef(c)) {
|
|
40
|
-
max = Math.max(max, getMaxDepth(c.children, depth + 1));
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return max;
|
|
44
|
-
}
|
|
45
|
-
const maxDepth = getMaxDepth(columns, 0);
|
|
46
|
-
// If no groups at all, return a single row of leaf cells
|
|
47
|
-
if (maxDepth === 0) {
|
|
48
|
-
const row = [];
|
|
49
|
-
for (const c of columns) {
|
|
50
|
-
if (!isColumnGroupDef(c)) {
|
|
51
|
-
if (visibleColumns && !visibleColumns.has(c.columnId))
|
|
52
|
-
continue;
|
|
53
|
-
row.push({
|
|
54
|
-
label: c.name,
|
|
55
|
-
colSpan: 1,
|
|
56
|
-
isGroup: false,
|
|
57
|
-
columnDef: c,
|
|
58
|
-
depth: 0,
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return [row];
|
|
63
|
-
}
|
|
64
|
-
// Step 2: Build rows for depth 0..maxDepth
|
|
65
|
-
// Total rows = maxDepth + 1 (groups use rows 0..maxDepth-1, leaves use row maxDepth)
|
|
66
|
-
const totalRows = maxDepth + 1;
|
|
67
|
-
const rows = Array.from({ length: totalRows }, () => []);
|
|
68
|
-
// Step 3: Walk the tree and place cells
|
|
69
|
-
// Cache leaf counts by children array ref to avoid O(n²) repeated traversals
|
|
70
|
-
const leafCountCache = new Map();
|
|
71
|
-
function countVisibleLeaves(cols) {
|
|
72
|
-
const cached = leafCountCache.get(cols);
|
|
73
|
-
if (cached !== undefined)
|
|
74
|
-
return cached;
|
|
75
|
-
let count = 0;
|
|
76
|
-
for (const c of cols) {
|
|
77
|
-
if (isColumnGroupDef(c)) {
|
|
78
|
-
count += countVisibleLeaves(c.children);
|
|
79
|
-
}
|
|
80
|
-
else {
|
|
81
|
-
if (!visibleColumns || visibleColumns.has(c.columnId)) {
|
|
82
|
-
count++;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
leafCountCache.set(cols, count);
|
|
87
|
-
return count;
|
|
88
|
-
}
|
|
89
|
-
function walk(cols, depth) {
|
|
90
|
-
for (const c of cols) {
|
|
91
|
-
if (isColumnGroupDef(c)) {
|
|
92
|
-
const leafCount = countVisibleLeaves(c.children);
|
|
93
|
-
if (leafCount === 0)
|
|
94
|
-
continue; // Skip empty groups
|
|
95
|
-
rows[depth].push({
|
|
96
|
-
label: c.headerName,
|
|
97
|
-
colSpan: leafCount,
|
|
98
|
-
isGroup: true,
|
|
99
|
-
depth,
|
|
100
|
-
});
|
|
101
|
-
walk(c.children, depth + 1);
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
104
|
-
if (visibleColumns && !visibleColumns.has(c.columnId))
|
|
105
|
-
continue;
|
|
106
|
-
// Leaf column: place it at the current depth.
|
|
107
|
-
// If depth < maxDepth, the rendering layer should use rowSpan to stretch
|
|
108
|
-
// this cell down to the bottom row.
|
|
109
|
-
rows[depth].push({
|
|
110
|
-
label: c.name,
|
|
111
|
-
colSpan: 1,
|
|
112
|
-
isGroup: false,
|
|
113
|
-
columnDef: c,
|
|
114
|
-
depth,
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
walk(columns, 0);
|
|
120
|
-
// Remove any completely empty rows (can happen with certain structures)
|
|
121
|
-
return rows.filter(row => row.length > 0);
|
|
122
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Derives status bar config for DataGridTable from props + current items/selection.
|
|
3
|
-
* Use in Fluent, Material, and Radix DataGridTable so the same logic lives in one place.
|
|
4
|
-
*/
|
|
5
|
-
export function getDataGridStatusBarConfig(statusBar, itemsLength, selectedCount, filteredCount) {
|
|
6
|
-
if (!statusBar)
|
|
7
|
-
return null;
|
|
8
|
-
if (typeof statusBar === 'object')
|
|
9
|
-
return statusBar;
|
|
10
|
-
return {
|
|
11
|
-
totalCount: itemsLength,
|
|
12
|
-
selectedCount: selectedCount > 0 ? selectedCount : undefined,
|
|
13
|
-
filteredCount: filteredCount !== undefined && filteredCount !== itemsLength ? filteredCount : undefined,
|
|
14
|
-
};
|
|
15
|
-
}
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* View model helpers for DataGridTable.
|
|
3
|
-
* Pure TypeScript — no framework dependencies (React, Angular, Vue).
|
|
4
|
-
* Framework packages re-export these and may add thin framework-specific wrappers.
|
|
5
|
-
*/
|
|
6
|
-
import { getCellValue } from './cellValue';
|
|
7
|
-
import { isInSelectionRange } from '../types/dataGridTypes';
|
|
8
|
-
import { isFilterConfig } from './ogridHelpers';
|
|
9
|
-
/**
|
|
10
|
-
* Returns ColumnHeaderFilter props from column def and grid filter/sort state.
|
|
11
|
-
*/
|
|
12
|
-
export function getHeaderFilterConfig(col, input) {
|
|
13
|
-
const filterable = isFilterConfig(col.filterable) ? col.filterable : null;
|
|
14
|
-
const filterType = (filterable?.type ?? 'none');
|
|
15
|
-
const filterField = filterable?.filterField ?? col.columnId;
|
|
16
|
-
const sortable = col.sortable !== false;
|
|
17
|
-
const filterValue = input.filters[filterField];
|
|
18
|
-
const base = {
|
|
19
|
-
columnKey: col.columnId,
|
|
20
|
-
columnName: col.name,
|
|
21
|
-
filterType,
|
|
22
|
-
isSorted: input.sortBy === col.columnId,
|
|
23
|
-
isSortedDescending: input.sortBy === col.columnId && input.sortDirection === 'desc',
|
|
24
|
-
onSort: sortable ? () => input.onColumnSort(col.columnId) : undefined,
|
|
25
|
-
};
|
|
26
|
-
if (filterType === 'text') {
|
|
27
|
-
return {
|
|
28
|
-
...base,
|
|
29
|
-
textValue: filterValue?.type === 'text' ? filterValue.value : '',
|
|
30
|
-
onTextChange: (v) => input.onFilterChange(filterField, v.trim() ? { type: 'text', value: v } : undefined),
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
if (filterType === 'people') {
|
|
34
|
-
return {
|
|
35
|
-
...base,
|
|
36
|
-
selectedUser: filterValue?.type === 'people' ? filterValue.value : undefined,
|
|
37
|
-
onUserChange: (u) => input.onFilterChange(filterField, u ? { type: 'people', value: u } : undefined),
|
|
38
|
-
peopleSearch: input.peopleSearch,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
if (filterType === 'multiSelect') {
|
|
42
|
-
return {
|
|
43
|
-
...base,
|
|
44
|
-
options: input.filterOptions[filterField] ?? [],
|
|
45
|
-
isLoadingOptions: input.loadingFilterOptions[filterField] ?? false,
|
|
46
|
-
selectedValues: filterValue?.type === 'multiSelect' ? filterValue.value : [],
|
|
47
|
-
onFilterChange: (values) => input.onFilterChange(filterField, values.length ? { type: 'multiSelect', value: values } : undefined),
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
if (filterType === 'date') {
|
|
51
|
-
return {
|
|
52
|
-
...base,
|
|
53
|
-
dateValue: filterValue?.type === 'date' ? filterValue.value : undefined,
|
|
54
|
-
onDateChange: (v) => input.onFilterChange(filterField, v ? { type: 'date', value: v } : undefined),
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
return base;
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Returns a descriptor for rendering a cell. UI uses this to decide editing-inline vs editing-popover vs display
|
|
61
|
-
* and to apply isActive, isInRange, etc. without duplicating the boolean logic.
|
|
62
|
-
*/
|
|
63
|
-
export function getCellRenderDescriptor(item, col, rowIndex, colIdx, input) {
|
|
64
|
-
const rowId = input.getRowId(item);
|
|
65
|
-
const globalColIndex = colIdx + input.colOffset;
|
|
66
|
-
const colEditable = col.editable === true ||
|
|
67
|
-
(typeof col.editable === 'function' && col.editable(item));
|
|
68
|
-
const canEditInline = input.editable !== false &&
|
|
69
|
-
!!colEditable &&
|
|
70
|
-
!!input.onCellValueChanged &&
|
|
71
|
-
typeof col.cellEditor !== 'function';
|
|
72
|
-
const canEditPopup = input.editable !== false &&
|
|
73
|
-
!!colEditable &&
|
|
74
|
-
!!input.onCellValueChanged &&
|
|
75
|
-
typeof col.cellEditor === 'function' &&
|
|
76
|
-
col.cellEditorPopup !== false;
|
|
77
|
-
const canEditAny = canEditInline || canEditPopup;
|
|
78
|
-
const isEditing = input.editingCell?.rowId === rowId &&
|
|
79
|
-
input.editingCell?.columnId === col.columnId;
|
|
80
|
-
const isActive = !input.isDragging &&
|
|
81
|
-
input.activeCell?.rowIndex === rowIndex &&
|
|
82
|
-
input.activeCell?.columnIndex === globalColIndex;
|
|
83
|
-
const isInRange = input.selectionRange != null &&
|
|
84
|
-
isInSelectionRange(input.selectionRange, rowIndex, colIdx);
|
|
85
|
-
const isInCutRange = input.cutRange != null &&
|
|
86
|
-
isInSelectionRange(input.cutRange, rowIndex, colIdx);
|
|
87
|
-
const isInCopyRange = input.copyRange != null &&
|
|
88
|
-
isInSelectionRange(input.copyRange, rowIndex, colIdx);
|
|
89
|
-
const isSelectionEndCell = !input.isDragging &&
|
|
90
|
-
input.copyRange == null &&
|
|
91
|
-
input.cutRange == null &&
|
|
92
|
-
input.selectionRange != null &&
|
|
93
|
-
rowIndex === input.selectionRange.endRow &&
|
|
94
|
-
colIdx === input.selectionRange.endCol;
|
|
95
|
-
const isPinned = col.pinned != null;
|
|
96
|
-
const pinnedSide = col.pinned ?? undefined;
|
|
97
|
-
// Compute cell value once — used in editing and display branches
|
|
98
|
-
const cellValue = getCellValue(item, col);
|
|
99
|
-
let mode = 'display';
|
|
100
|
-
let editorType;
|
|
101
|
-
if (isEditing && canEditInline) {
|
|
102
|
-
mode = 'editing-inline';
|
|
103
|
-
if (col.cellEditor === 'text' ||
|
|
104
|
-
col.cellEditor === 'select' ||
|
|
105
|
-
col.cellEditor === 'checkbox' ||
|
|
106
|
-
col.cellEditor === 'richSelect' ||
|
|
107
|
-
col.cellEditor === 'date') {
|
|
108
|
-
editorType = col.cellEditor;
|
|
109
|
-
}
|
|
110
|
-
else if (col.type === 'date') {
|
|
111
|
-
editorType = 'date';
|
|
112
|
-
}
|
|
113
|
-
else if (col.type === 'boolean') {
|
|
114
|
-
editorType = 'checkbox';
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
editorType = 'text';
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
else if (isEditing && canEditPopup && typeof col.cellEditor === 'function') {
|
|
121
|
-
mode = 'editing-popover';
|
|
122
|
-
}
|
|
123
|
-
return {
|
|
124
|
-
mode,
|
|
125
|
-
editorType,
|
|
126
|
-
value: cellValue,
|
|
127
|
-
isActive,
|
|
128
|
-
isInRange,
|
|
129
|
-
isInCutRange,
|
|
130
|
-
isInCopyRange,
|
|
131
|
-
isSelectionEndCell,
|
|
132
|
-
canEditAny,
|
|
133
|
-
isPinned,
|
|
134
|
-
pinnedSide,
|
|
135
|
-
globalColIndex,
|
|
136
|
-
rowId,
|
|
137
|
-
rowIndex,
|
|
138
|
-
displayValue: cellValue,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Resolves display content for a cell in display mode.
|
|
143
|
-
* Handles the renderCell -> valueFormatter -> String() fallback chain.
|
|
144
|
-
* Returns `unknown` — framework packages may narrow to their own node type.
|
|
145
|
-
*/
|
|
146
|
-
export function resolveCellDisplayContent(col, item, displayValue) {
|
|
147
|
-
const c = col;
|
|
148
|
-
if (c.renderCell && typeof c.renderCell === 'function') {
|
|
149
|
-
return c.renderCell(item);
|
|
150
|
-
}
|
|
151
|
-
if (col.valueFormatter)
|
|
152
|
-
return col.valueFormatter(displayValue, item);
|
|
153
|
-
if (displayValue == null)
|
|
154
|
-
return null;
|
|
155
|
-
if (col.type === 'date') {
|
|
156
|
-
const d = new Date(String(displayValue));
|
|
157
|
-
if (!Number.isNaN(d.getTime()))
|
|
158
|
-
return d.toLocaleDateString();
|
|
159
|
-
}
|
|
160
|
-
if (col.type === 'boolean') {
|
|
161
|
-
return displayValue ? 'True' : 'False';
|
|
162
|
-
}
|
|
163
|
-
return String(displayValue);
|
|
164
|
-
}
|
|
165
|
-
/**
|
|
166
|
-
* Resolves the cellStyle from a column def, handling both function and static values.
|
|
167
|
-
*/
|
|
168
|
-
export function resolveCellStyle(col, item) {
|
|
169
|
-
const c = col;
|
|
170
|
-
if (!c.cellStyle)
|
|
171
|
-
return undefined;
|
|
172
|
-
return typeof c.cellStyle === 'function' ? c.cellStyle(item) : c.cellStyle;
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Builds props for InlineCellEditor. Shared across all UI packages.
|
|
176
|
-
*/
|
|
177
|
-
export function buildInlineEditorProps(item, col, descriptor, callbacks) {
|
|
178
|
-
return {
|
|
179
|
-
value: descriptor.value,
|
|
180
|
-
item,
|
|
181
|
-
column: col,
|
|
182
|
-
rowIndex: descriptor.rowIndex,
|
|
183
|
-
editorType: (descriptor.editorType ?? 'text'),
|
|
184
|
-
onCommit: (newValue) => callbacks.commitCellEdit(item, col.columnId, descriptor.value, newValue, descriptor.rowIndex, descriptor.globalColIndex),
|
|
185
|
-
onCancel: () => callbacks.setEditingCell(null),
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
/**
|
|
189
|
-
* Builds ICellEditorProps for custom popover editors. Shared across all UI packages.
|
|
190
|
-
*/
|
|
191
|
-
export function buildPopoverEditorProps(item, col, descriptor, pendingEditorValue, callbacks) {
|
|
192
|
-
const oldValue = descriptor.value;
|
|
193
|
-
const displayValue = pendingEditorValue !== undefined ? pendingEditorValue : oldValue;
|
|
194
|
-
return {
|
|
195
|
-
value: displayValue,
|
|
196
|
-
onValueChange: callbacks.setPendingEditorValue,
|
|
197
|
-
onCommit: () => {
|
|
198
|
-
const newValue = pendingEditorValue !== undefined ? pendingEditorValue : oldValue;
|
|
199
|
-
callbacks.commitCellEdit(item, col.columnId, oldValue, newValue, descriptor.rowIndex, descriptor.globalColIndex);
|
|
200
|
-
},
|
|
201
|
-
onCancel: callbacks.cancelPopoverEdit,
|
|
202
|
-
item,
|
|
203
|
-
column: col,
|
|
204
|
-
cellEditorParams: col.cellEditorParams,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Debounces a function call, delaying execution until after `delayMs` milliseconds
|
|
3
|
-
* have elapsed since the last invocation.
|
|
4
|
-
*
|
|
5
|
-
* @param fn - The function to debounce
|
|
6
|
-
* @param delayMs - Delay in milliseconds
|
|
7
|
-
* @returns Debounced function with a `cancel` method
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* ```typescript
|
|
11
|
-
* const search = debounce((query: string) => {
|
|
12
|
-
* console.log('Searching:', query);
|
|
13
|
-
* }, 300);
|
|
14
|
-
*
|
|
15
|
-
* search('a');
|
|
16
|
-
* search('ab');
|
|
17
|
-
* search('abc'); // Only this will execute after 300ms
|
|
18
|
-
*
|
|
19
|
-
* search.cancel(); // Cancel pending execution
|
|
20
|
-
* ```
|
|
21
|
-
*/
|
|
22
|
-
export function debounce(fn, delayMs) {
|
|
23
|
-
let timeoutId = null;
|
|
24
|
-
const debounced = ((...args) => {
|
|
25
|
-
if (timeoutId !== null) {
|
|
26
|
-
clearTimeout(timeoutId);
|
|
27
|
-
}
|
|
28
|
-
timeoutId = setTimeout(() => {
|
|
29
|
-
fn(...args);
|
|
30
|
-
timeoutId = null;
|
|
31
|
-
}, delayMs);
|
|
32
|
-
});
|
|
33
|
-
debounced.cancel = () => {
|
|
34
|
-
if (timeoutId !== null) {
|
|
35
|
-
clearTimeout(timeoutId);
|
|
36
|
-
timeoutId = null;
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
return debounced;
|
|
40
|
-
}
|
package/dist/esm/utils/dom.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DOM utility functions for OGrid components.
|
|
3
|
-
* These utilities are framework-agnostic and can be used across React, Angular, Vue, and vanilla JS implementations.
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* Measure the bounding rect of a cell range within a container.
|
|
7
|
-
*
|
|
8
|
-
* @param container - The grid container element with position: relative
|
|
9
|
-
* @param range - The selection range to measure
|
|
10
|
-
* @param colOffset - Column offset (1 when checkbox column is present, else 0)
|
|
11
|
-
* @returns Rectangle describing the range position and size, or null if cells not found
|
|
12
|
-
*/
|
|
13
|
-
export function measureRange(container, range, colOffset) {
|
|
14
|
-
const startGlobalCol = range.startCol + colOffset;
|
|
15
|
-
const endGlobalCol = range.endCol + colOffset;
|
|
16
|
-
const topLeft = container.querySelector(`[data-row-index="${range.startRow}"][data-col-index="${startGlobalCol}"]`);
|
|
17
|
-
const bottomRight = container.querySelector(`[data-row-index="${range.endRow}"][data-col-index="${endGlobalCol}"]`);
|
|
18
|
-
if (!topLeft || !bottomRight)
|
|
19
|
-
return null;
|
|
20
|
-
const cRect = container.getBoundingClientRect();
|
|
21
|
-
const tlRect = topLeft.getBoundingClientRect();
|
|
22
|
-
const brRect = bottomRight.getBoundingClientRect();
|
|
23
|
-
return {
|
|
24
|
-
top: tlRect.top - cRect.top,
|
|
25
|
-
left: tlRect.left - cRect.left,
|
|
26
|
-
width: brRect.right - tlRect.left,
|
|
27
|
-
height: brRect.bottom - tlRect.top,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Inject a global CSS rule into the document head (once per page, deduplicated by ID).
|
|
32
|
-
*
|
|
33
|
-
* @param id - Unique ID for the style element (prevents duplicates)
|
|
34
|
-
* @param css - CSS content to inject
|
|
35
|
-
*
|
|
36
|
-
* @example
|
|
37
|
-
* ```ts
|
|
38
|
-
* injectGlobalStyles(
|
|
39
|
-
* 'ogrid-marching-ants-keyframes',
|
|
40
|
-
* '@keyframes ogrid-marching-ants{to{stroke-dashoffset:-8}}'
|
|
41
|
-
* );
|
|
42
|
-
* ```
|
|
43
|
-
*/
|
|
44
|
-
export function injectGlobalStyles(id, css) {
|
|
45
|
-
if (typeof document === 'undefined')
|
|
46
|
-
return;
|
|
47
|
-
if (document.getElementById(id))
|
|
48
|
-
return;
|
|
49
|
-
const style = document.createElement('style');
|
|
50
|
-
style.id = id;
|
|
51
|
-
style.textContent = css;
|
|
52
|
-
document.head.appendChild(style);
|
|
53
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
export function escapeCsvValue(value) {
|
|
2
|
-
if (value === null || value === undefined) {
|
|
3
|
-
return '';
|
|
4
|
-
}
|
|
5
|
-
const s = String(value);
|
|
6
|
-
if (s.includes(',') || s.includes('"') || s.includes('\n')) {
|
|
7
|
-
return `"${s.replace(/"/g, '""')}"`;
|
|
8
|
-
}
|
|
9
|
-
return s;
|
|
10
|
-
}
|
|
11
|
-
export function buildCsvHeader(columns) {
|
|
12
|
-
return columns.map((c) => escapeCsvValue(c.name)).join(',');
|
|
13
|
-
}
|
|
14
|
-
export function buildCsvRows(items, columns, getValue) {
|
|
15
|
-
return items.map((item) => columns.map((c) => escapeCsvValue(getValue(item, c.columnId))).join(','));
|
|
16
|
-
}
|
|
17
|
-
export function exportToCsv(items, columns, getValue, filename) {
|
|
18
|
-
const header = buildCsvHeader(columns);
|
|
19
|
-
const rows = buildCsvRows(items, columns, getValue);
|
|
20
|
-
const csv = [header, ...rows].join('\n');
|
|
21
|
-
triggerCsvDownload(csv, filename ?? `export_${new Date().toISOString().slice(0, 10)}.csv`);
|
|
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
|
-
*/
|
|
32
|
-
export function triggerCsvDownload(csvContent, filename) {
|
|
33
|
-
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
34
|
-
const url = URL.createObjectURL(blob);
|
|
35
|
-
const link = document.createElement('a');
|
|
36
|
-
try {
|
|
37
|
-
link.setAttribute('href', url);
|
|
38
|
-
link.setAttribute('download', filename);
|
|
39
|
-
link.style.visibility = 'hidden';
|
|
40
|
-
document.body.appendChild(link);
|
|
41
|
-
link.click();
|
|
42
|
-
}
|
|
43
|
-
finally {
|
|
44
|
-
try {
|
|
45
|
-
document.body.removeChild(link);
|
|
46
|
-
}
|
|
47
|
-
catch { /* noop */ }
|
|
48
|
-
URL.revokeObjectURL(url);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
export const GRID_CONTEXT_MENU_ITEMS = [
|
|
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 },
|
|
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
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Returns a map of menu item id -> click handler. Each handler invokes the corresponding
|
|
16
|
-
* action and then onClose. Used by Fluent, Material, and Radix GridContextMenu components.
|
|
17
|
-
*/
|
|
18
|
-
export function getContextMenuHandlers(props) {
|
|
19
|
-
const { onCopy, onCut, onPaste, onSelectAll, onUndo, onRedo, onClose } = props;
|
|
20
|
-
return {
|
|
21
|
-
undo: () => {
|
|
22
|
-
onUndo();
|
|
23
|
-
onClose();
|
|
24
|
-
},
|
|
25
|
-
redo: () => {
|
|
26
|
-
onRedo();
|
|
27
|
-
onClose();
|
|
28
|
-
},
|
|
29
|
-
copy: () => {
|
|
30
|
-
onCopy();
|
|
31
|
-
onClose();
|
|
32
|
-
},
|
|
33
|
-
cut: () => {
|
|
34
|
-
onCut();
|
|
35
|
-
onClose();
|
|
36
|
-
},
|
|
37
|
-
paste: () => {
|
|
38
|
-
onPaste();
|
|
39
|
-
onClose();
|
|
40
|
-
},
|
|
41
|
-
selectAll: () => {
|
|
42
|
-
onSelectAll();
|
|
43
|
-
onClose();
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
/** Column header menu items for pin/unpin actions. */
|
|
48
|
-
export const COLUMN_HEADER_MENU_ITEMS = [
|
|
49
|
-
{ id: 'pinLeft', label: 'Pin left' },
|
|
50
|
-
{ id: 'pinRight', label: 'Pin right' },
|
|
51
|
-
{ id: 'unpin', label: 'Unpin' },
|
|
52
|
-
];
|
|
53
|
-
/**
|
|
54
|
-
* Builds the complete column header menu items based on current state.
|
|
55
|
-
* Returns pinning, sorting, and sizing options.
|
|
56
|
-
*/
|
|
57
|
-
export function getColumnHeaderMenuItems(input) {
|
|
58
|
-
const { canPinLeft, canPinRight, canUnpin, currentSort, isSortable = true, isResizable = true } = input;
|
|
59
|
-
const items = [];
|
|
60
|
-
// Pinning section
|
|
61
|
-
items.push({ id: 'pinLeft', label: 'Pin left', disabled: !canPinLeft }, { id: 'pinRight', label: 'Pin right', disabled: !canPinRight }, { id: 'unpin', label: 'Unpin', disabled: !canUnpin, divider: isSortable || isResizable });
|
|
62
|
-
// Sorting section
|
|
63
|
-
if (isSortable) {
|
|
64
|
-
if (!currentSort) {
|
|
65
|
-
// No sort applied - show both options
|
|
66
|
-
items.push({ id: 'sortAsc', label: 'Sort ascending' }, { id: 'sortDesc', label: 'Sort descending', divider: isResizable });
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
// Sort applied - show opposite + clear
|
|
70
|
-
const oppositeSort = currentSort === 'asc' ? 'desc' : 'asc';
|
|
71
|
-
const oppositeLabel = currentSort === 'asc' ? 'Sort descending' : 'Sort ascending';
|
|
72
|
-
items.push({ id: `sort${oppositeSort === 'asc' ? 'Asc' : 'Desc'}`, label: oppositeLabel }, { id: 'clearSort', label: 'Clear sort', divider: isResizable });
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
// Autosize section
|
|
76
|
-
if (isResizable) {
|
|
77
|
-
items.push({ id: 'autosizeThis', label: 'Autosize this column' }, { id: 'autosizeAll', label: 'Autosize all columns' });
|
|
78
|
-
}
|
|
79
|
-
return items;
|
|
80
|
-
}
|