@opencorestack/opengridx 3.7.1 → 3.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/dist/ai.cjs.map +1 -1
- package/dist/ai.es.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +6043 -5979
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +16 -16
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +28 -0
- package/docs/features/filtering.md +2 -0
- package/lib/components/AiAssistant/GridAiAssistantPanel.css +2 -2
- package/lib/components/Cell/Cell.css +1 -3
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +1 -2
- package/lib/components/DataGrid/DataGrid.css +1 -28
- package/lib/components/DataGrid/DataGrid.tsx +2 -4
- package/lib/components/DataGrid/GridListView.tsx +1 -1
- package/lib/components/DataGrid/GridPinnedRows.tsx +1 -1
- package/lib/components/DataGrid/GridVirtualRows.tsx +3 -2
- package/lib/components/FilterPanel/FilterPanel.tsx +7 -39
- package/lib/components/Header/Header.css +2 -11
- package/lib/components/HeaderFilter/HeaderFilterCell.tsx +6 -37
- package/lib/components/ListView/ListView.css +0 -16
- package/lib/components/Row/Row.tsx +4 -1
- package/lib/hooks/core/useDataGrid.ts +40 -35
- package/lib/hooks/core/useGridAggregationApi.ts +1 -1
- package/lib/hooks/core/useGridCellSelection.ts +3 -4
- package/lib/hooks/core/useGridClipboardPaste.ts +1 -1
- package/lib/hooks/core/useGridDetailPanel.ts +1 -1
- package/lib/hooks/core/useGridFillHandle.ts +1 -1
- package/lib/hooks/core/useGridHierarchy.ts +2 -2
- package/lib/hooks/core/useGridKeyboardNavigation.ts +1 -1
- package/lib/hooks/core/useGridLiveRowSelection.ts +9 -2
- package/lib/hooks/core/useGridRangeEditing.ts +1 -1
- package/lib/hooks/core/useGridRowInteractions.ts +3 -2
- package/lib/hooks/features/useGridSpanning.ts +4 -4
- package/lib/hooks/useRowGrouping.ts +19 -10
- package/lib/hooks/useRowReorder.ts +9 -3
- package/lib/styles/base/reset.css +6 -7
- package/lib/theme/__baseline__/default-theme.darwin.chromium.json +1 -1503
- package/lib/theme/__baseline__/default-theme.darwin.firefox.json +1 -1503
- package/lib/theme/__baseline__/default-theme.darwin.webkit.json +1 -1503
- package/lib/types/rows.ts +0 -2
- package/lib/types/state.ts +9 -0
- package/lib/utils/cellSelection.ts +0 -2
- package/lib/utils/editing/index.ts +1 -1
- package/lib/utils/editing/targets.ts +2 -3
- package/lib/utils/filtering/filterInput.ts +45 -0
- package/lib/utils/filtering/index.ts +0 -28
- package/lib/utils/gridApiRows.ts +1 -1
- package/lib/utils/grouping/groupedExportRows.ts +1 -1
- package/lib/utils/grouping/layeredMap.ts +52 -0
- package/lib/utils/pinning/index.ts +0 -107
- package/lib/utils/rowIndex.ts +67 -0
- package/lib/utils/sorting/index.ts +42 -33
- package/lib/utils/values.ts +5 -1
- package/llms.txt +7 -3
- package/package.json +1 -1
- package/lib/theme/defaultStyles.ts +0 -0
package/lib/types/rows.ts
CHANGED
|
@@ -5,8 +5,6 @@ export type GridRowId = string | number;
|
|
|
5
5
|
export interface GridRowModel {
|
|
6
6
|
/** Uniquely identifies the row. */
|
|
7
7
|
id: GridRowId;
|
|
8
|
-
/** Internal: row is a skeleton placeholder during infinite-scroll loading. */
|
|
9
|
-
_isSkeleton?: boolean;
|
|
10
8
|
/** Internal: server-reported child count for lazy-loaded tree nodes. */
|
|
11
9
|
serverChildrenCount?: number;
|
|
12
10
|
/** Any other dynamic row properties. */
|
package/lib/types/state.ts
CHANGED
|
@@ -2,11 +2,20 @@ import type { GridRowId, GridRowModel, GridPinnedRows } from './rows';
|
|
|
2
2
|
import type { GridColDef, GridColumnVisibilityModel, GridPinnedColumns } from './columns';
|
|
3
3
|
|
|
4
4
|
export interface GridInternalState {
|
|
5
|
+
/**
|
|
6
|
+
* The row store. Cell edits (`REPLACE_ROW` / `REPLACE_ROWS`) update `idRowsLookup`, `idByRow`
|
|
7
|
+
* and `indexById` IN PLACE and only replace `rows`: never rely on those maps changing identity
|
|
8
|
+
* to detect an edit; depend on `rows` (or `state.rows`) instead. `SET_ROWS` builds new maps.
|
|
9
|
+
*/
|
|
5
10
|
rows: {
|
|
6
11
|
idRowsLookup: Map<GridRowId, GridRowModel>;
|
|
7
12
|
allRows: GridRowId[];
|
|
8
13
|
/** Row object → its getRowId() key. Rows are stored untouched, so this is how ids are resolved. */
|
|
9
14
|
idByRow: Map<GridRowModel, GridRowId>;
|
|
15
|
+
/** The stored rows in `allRows` order. */
|
|
16
|
+
rows: GridRowModel[];
|
|
17
|
+
/** Row id → its index in `allRows` / `rows`. */
|
|
18
|
+
indexById: Map<GridRowId, number>;
|
|
10
19
|
};
|
|
11
20
|
pagination: {
|
|
12
21
|
/** Server-reported total (dataSource responses); undefined until one arrives. */
|
|
@@ -17,8 +17,6 @@ export function isRangeColumnField(field: string): boolean {
|
|
|
17
17
|
export const isSameCoordinates = (a: GridCellCoordinates | null | undefined, b: GridCellCoordinates | null | undefined): boolean =>
|
|
18
18
|
a === b || (!!a && !!b && a.id === b.id && a.field === b.field);
|
|
19
19
|
|
|
20
|
-
export const isSingleCellRange = (range: GridCellRange): boolean => isSameCoordinates(range.anchor, range.head);
|
|
21
|
-
|
|
22
20
|
/** A model holding the same range(s) as `b`. */
|
|
23
21
|
export function isSameCellSelectionModel(a: GridCellSelectionModel, b: GridCellSelectionModel): boolean {
|
|
24
22
|
if (a === b) return true;
|
|
@@ -20,7 +20,7 @@ export function resolveCellEditable<R extends GridRowModel>(
|
|
|
20
20
|
): boolean {
|
|
21
21
|
const { row, field, value, colDef, rowIndex, colIndex, rowMeta } = params;
|
|
22
22
|
if (!colDef.editable) return false;
|
|
23
|
-
if (rowMeta?.isGroupRow
|
|
23
|
+
if (rowMeta?.isGroupRow) return false;
|
|
24
24
|
if (!isCellEditable) return true;
|
|
25
25
|
try {
|
|
26
26
|
return Boolean(isCellEditable({ row, field, value, colDef, rowIndex, colIndex }));
|
|
@@ -5,6 +5,7 @@ import { isSystemField } from '../focus';
|
|
|
5
5
|
import { isSyntheticRowId } from '../syntheticRows';
|
|
6
6
|
import { resolveCellEditable } from './index';
|
|
7
7
|
import { getCellValue } from '../values';
|
|
8
|
+
import { getRowIndex } from '../rowIndex';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Where batch edits (paste, range clear, undo/redo) land: the renderable rows and the range columns
|
|
@@ -26,9 +27,7 @@ export function buildEditTargetContext<R extends GridRowModel>(
|
|
|
26
27
|
getRowId: (row: R) => GridRowId,
|
|
27
28
|
navigationColumns: ReadonlyArray<GridColDef<R>>,
|
|
28
29
|
): GridEditTargetContext<R> {
|
|
29
|
-
const rowIds = rows
|
|
30
|
-
const rowIndexById = new Map<GridRowId, number>();
|
|
31
|
-
rowIds.forEach((id, index) => { if (!rowIndexById.has(id)) rowIndexById.set(id, index); });
|
|
30
|
+
const { rowIds, rowIndexById } = getRowIndex(rows, getRowId, 'renderable');
|
|
32
31
|
const columns = navigationColumns.filter(c => isRangeColumnField(c.field));
|
|
33
32
|
const colIndexByField = new Map<string, number>();
|
|
34
33
|
columns.forEach((c, index) => { colIndexByField.set(c.field, index); });
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { GridColDef, GridFilterOperator } from '../../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helpers shared by the filter panel and the header filter row: both edit a filter item's value
|
|
5
|
+
* through a text box or a select of the column's `valueOptions`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** One entry of a `valueOptions` select. `key` is the option's index, used as the `<option>` value. */
|
|
9
|
+
export interface FilterValueOption {
|
|
10
|
+
key: string;
|
|
11
|
+
value: unknown;
|
|
12
|
+
label: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getFilterValueOptions(col: Pick<GridColDef, 'valueOptions'>): FilterValueOption[] {
|
|
16
|
+
return (col.valueOptions ?? []).map((option, index) => (
|
|
17
|
+
option !== null && typeof option === 'object'
|
|
18
|
+
? { key: String(index), value: option.value, label: option.label }
|
|
19
|
+
: { key: String(index), value: option, label: String(option) }
|
|
20
|
+
));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Whether a filter value selects this option: the same value, or the same text ignoring case (a
|
|
25
|
+
* value typed or restored from a URL is a string). A list matches by its first entry.
|
|
26
|
+
*/
|
|
27
|
+
export function matchesFilterValueOption(option: FilterValueOption, value: unknown): boolean {
|
|
28
|
+
const single = Array.isArray(value) ? value[0] : value;
|
|
29
|
+
if (Object.is(option.value, single)) return true;
|
|
30
|
+
return single != null && String(option.value).toLowerCase() === String(single).toLowerCase();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The item value for typed text. `isAnyOf` takes a comma-separated list. */
|
|
34
|
+
export function filterValueFromInputText(text: string, operator: GridFilterOperator): unknown {
|
|
35
|
+
if (operator === 'isAnyOf') return text.split(',').map(part => part.trim()).filter(part => part !== '');
|
|
36
|
+
return text;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Equal filter values: the same value, dates at the same time, or lists of equal values. */
|
|
40
|
+
export function isSameFilterValue(a: unknown, b: unknown): boolean {
|
|
41
|
+
if (Object.is(a, b)) return true;
|
|
42
|
+
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
|
|
43
|
+
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => isSameFilterValue(v, b[i]));
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
@@ -362,19 +362,6 @@ export function applyQuickFilter<R extends GridRowModel>(
|
|
|
362
362
|
return predicate ? predicate(row) : true;
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
-
/**
|
|
366
|
-
* Check if a row matches the filter model. Compiles the model on every call: to test many rows,
|
|
367
|
-
* build a predicate once with createRowFilter.
|
|
368
|
-
*/
|
|
369
|
-
export function isRowMatchingFilter<R extends GridRowModel>(
|
|
370
|
-
row: R,
|
|
371
|
-
filterModel: GridFilterModel | GridFilterGroup,
|
|
372
|
-
columns?: GridColumnLookup
|
|
373
|
-
): boolean {
|
|
374
|
-
const predicate = createRowFilter(filterModel, columns);
|
|
375
|
-
return predicate ? predicate(row) : true;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
365
|
export function filterRows<R extends GridRowModel>(
|
|
379
366
|
rows: R[],
|
|
380
367
|
filterModel: GridFilterModel,
|
|
@@ -396,21 +383,6 @@ export function filterRows<R extends GridRowModel>(
|
|
|
396
383
|
return rows.filter(row => predicate(row));
|
|
397
384
|
}
|
|
398
385
|
|
|
399
|
-
export function getDefaultOperator(type?: string): GridFilterOperator {
|
|
400
|
-
switch (type) {
|
|
401
|
-
case 'number':
|
|
402
|
-
return '=';
|
|
403
|
-
case 'date':
|
|
404
|
-
return 'is';
|
|
405
|
-
case 'boolean':
|
|
406
|
-
return 'is';
|
|
407
|
-
case 'singleSelect':
|
|
408
|
-
return 'isAnyOf';
|
|
409
|
-
default:
|
|
410
|
-
return 'contains';
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
386
|
export function getOperatorsForType(type?: string): GridFilterOperator[] {
|
|
415
387
|
switch (type) {
|
|
416
388
|
case 'number':
|
package/lib/utils/gridApiRows.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface CollectAllFilteredRowsParams<R extends GridRowModel> {
|
|
|
9
9
|
/** Every unpinned row that passes the filter, sorted, across all pages. */
|
|
10
10
|
sortedUnpinnedRows: R[];
|
|
11
11
|
pinnedBottomRows: R[];
|
|
12
|
-
rowMetaMap:
|
|
12
|
+
rowMetaMap: ReadonlyMap<GridRowId, GridRowMeta>;
|
|
13
13
|
getRowId: (row: GridRowModel) => GridRowId;
|
|
14
14
|
/**
|
|
15
15
|
* With a hierarchy: the rows that pass the filter (the set select-all and the footer use).
|
|
@@ -16,7 +16,7 @@ export interface BuildGroupedExportRowsParams<R extends GridRowModel> {
|
|
|
16
16
|
*/
|
|
17
17
|
rows: R[];
|
|
18
18
|
getRowId: (row: R) => GridRowId;
|
|
19
|
-
rowMetaMap:
|
|
19
|
+
rowMetaMap: ReadonlyMap<GridRowId, GridRowMeta>;
|
|
20
20
|
columns: GridColDef<R>[];
|
|
21
21
|
aggregationModel: GridAggregationModel;
|
|
22
22
|
/** The footer's grand total (already over the filtered data rows); omitted when null. */
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A read-only view of two maps with disjoint keys, read as one: `top` first, then `base`. Row
|
|
3
|
+
* grouping keeps one entry per leaf row in `base` (built once per tree) and the group rows in
|
|
4
|
+
* `top` (rebuilt on every expand / collapse), so toggling a group at 1M rows does not copy a
|
|
5
|
+
* 1M-entry map. Iteration yields `base` entries, then `top` entries.
|
|
6
|
+
*/
|
|
7
|
+
export class LayeredMap<K, V> implements ReadonlyMap<K, V> {
|
|
8
|
+
private readonly base: ReadonlyMap<K, V>;
|
|
9
|
+
private readonly top: ReadonlyMap<K, V>;
|
|
10
|
+
|
|
11
|
+
constructor(base: ReadonlyMap<K, V>, top: ReadonlyMap<K, V>) {
|
|
12
|
+
this.base = base;
|
|
13
|
+
this.top = top;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get size(): number {
|
|
17
|
+
return this.base.size + this.top.size;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get(key: K): V | undefined {
|
|
21
|
+
const value = this.top.get(key);
|
|
22
|
+
return value !== undefined || this.top.has(key) ? value : this.base.get(key);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
has(key: K): boolean {
|
|
26
|
+
return this.top.has(key) || this.base.has(key);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
forEach(callback: (value: V, key: K, map: ReadonlyMap<K, V>) => void, thisArg?: unknown): void {
|
|
30
|
+
this.base.forEach((value, key) => callback.call(thisArg, value, key, this));
|
|
31
|
+
this.top.forEach((value, key) => callback.call(thisArg, value, key, this));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
*entries(): MapIterator<[K, V]> {
|
|
35
|
+
yield* this.base.entries();
|
|
36
|
+
yield* this.top.entries();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
*keys(): MapIterator<K> {
|
|
40
|
+
yield* this.base.keys();
|
|
41
|
+
yield* this.top.keys();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
*values(): MapIterator<V> {
|
|
45
|
+
yield* this.base.values();
|
|
46
|
+
yield* this.top.values();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
[Symbol.iterator](): MapIterator<[K, V]> {
|
|
50
|
+
return this.entries();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -137,67 +137,6 @@ export function getPinnedEdgeFields<R extends GridRowModel = GridRowModel>(
|
|
|
137
137
|
return { lastLeft, firstRight };
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
export function isColumnPinnable<R extends GridRowModel = GridRowModel>(
|
|
141
|
-
colDef: GridColDef<R>
|
|
142
|
-
): boolean {
|
|
143
|
-
return colDef.pinnable !== false;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export function pinColumn(
|
|
147
|
-
field: string,
|
|
148
|
-
position: GridPinnedPosition,
|
|
149
|
-
currentPinning?: GridColumnPinning
|
|
150
|
-
): GridColumnPinning {
|
|
151
|
-
const newPinning: GridColumnPinning = {
|
|
152
|
-
left: [...(currentPinning?.left || [])],
|
|
153
|
-
right: [...(currentPinning?.right || [])]
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
newPinning.left = (newPinning.left || []).filter(f => f !== field);
|
|
157
|
-
newPinning.right = (newPinning.right || []).filter(f => f !== field);
|
|
158
|
-
|
|
159
|
-
if (position === 'left') {
|
|
160
|
-
if (!newPinning.left) newPinning.left = [];
|
|
161
|
-
newPinning.left.push(field);
|
|
162
|
-
} else {
|
|
163
|
-
if (!newPinning.right) newPinning.right = [];
|
|
164
|
-
newPinning.right.push(field);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return newPinning;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export function unpinColumn(
|
|
171
|
-
field: string,
|
|
172
|
-
currentPinning?: GridColumnPinning
|
|
173
|
-
): GridColumnPinning {
|
|
174
|
-
if (!currentPinning) return {};
|
|
175
|
-
|
|
176
|
-
return {
|
|
177
|
-
left: currentPinning.left?.filter(f => f !== field) || [],
|
|
178
|
-
right: currentPinning.right?.filter(f => f !== field) || []
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export function getPinnedColumnsWidth(
|
|
183
|
-
columns: GridColDef[],
|
|
184
|
-
columnWidths: Record<string, number>,
|
|
185
|
-
position: GridPinnedPosition,
|
|
186
|
-
pinnedColumns?: GridColumnPinning
|
|
187
|
-
): number {
|
|
188
|
-
if (!pinnedColumns) return 0;
|
|
189
|
-
|
|
190
|
-
const pinnedFields = position === 'left' ? pinnedColumns.left : pinnedColumns.right;
|
|
191
|
-
if (!pinnedFields) return 0;
|
|
192
|
-
|
|
193
|
-
return pinnedFields.reduce((total, field) => {
|
|
194
|
-
const col = columns.find(c => c.field === field);
|
|
195
|
-
if (!col) return total;
|
|
196
|
-
|
|
197
|
-
return total + getRenderedColumnWidth(col, columnWidths);
|
|
198
|
-
}, 0);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
140
|
export function isRowPinned(
|
|
202
141
|
rowId: GridRowId,
|
|
203
142
|
pinnedRows?: GridRowPinning
|
|
@@ -263,52 +202,6 @@ export function getPinnedRowGroups<R extends GridRowModel = GridRowModel>(
|
|
|
263
202
|
return { top, center, bottom };
|
|
264
203
|
}
|
|
265
204
|
|
|
266
|
-
export function pinRow(
|
|
267
|
-
rowId: GridRowId,
|
|
268
|
-
position: 'top' | 'bottom',
|
|
269
|
-
currentPinning?: GridRowPinning
|
|
270
|
-
): GridRowPinning {
|
|
271
|
-
const newPinning: GridRowPinning = {
|
|
272
|
-
top: [...(currentPinning?.top || [])],
|
|
273
|
-
bottom: [...(currentPinning?.bottom || [])]
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
newPinning.top = (newPinning.top || []).filter(id => id !== rowId);
|
|
277
|
-
newPinning.bottom = (newPinning.bottom || []).filter(id => id !== rowId);
|
|
278
|
-
|
|
279
|
-
if (position === 'top') {
|
|
280
|
-
if (!newPinning.top) newPinning.top = [];
|
|
281
|
-
newPinning.top.push(rowId);
|
|
282
|
-
} else {
|
|
283
|
-
if (!newPinning.bottom) newPinning.bottom = [];
|
|
284
|
-
newPinning.bottom.push(rowId);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return newPinning;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
export function unpinRow(
|
|
291
|
-
rowId: GridRowId,
|
|
292
|
-
currentPinning?: GridRowPinning
|
|
293
|
-
): GridRowPinning {
|
|
294
|
-
if (!currentPinning) return {};
|
|
295
|
-
|
|
296
|
-
return {
|
|
297
|
-
top: currentPinning.top?.filter(id => id !== rowId) || [],
|
|
298
|
-
bottom: currentPinning.bottom?.filter(id => id !== rowId) || []
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
export function getPinnedRowsCount(
|
|
303
|
-
position: 'top' | 'bottom',
|
|
304
|
-
pinnedRows?: GridRowPinning
|
|
305
|
-
): number {
|
|
306
|
-
if (!pinnedRows) return 0;
|
|
307
|
-
|
|
308
|
-
const pinnedIds = position === 'top' ? pinnedRows.top : pinnedRows.bottom;
|
|
309
|
-
return pinnedIds?.length || 0;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
205
|
/**
|
|
313
206
|
* The column pinning model after the column menu pins `field` to `side` (appended after the
|
|
314
207
|
* columns already pinned there) or unpins it (`null`). The field is removed from the other side.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { GridRowId } from '../types';
|
|
2
|
+
|
|
3
|
+
/** The ids of a row array in order, and each id's first index. */
|
|
4
|
+
export interface GridRowIndex {
|
|
5
|
+
rowIds: GridRowId[];
|
|
6
|
+
rowIndexById: Map<GridRowId, number>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface CacheEntry {
|
|
10
|
+
rows: readonly object[];
|
|
11
|
+
value: GridRowIndex;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Last result per id resolver and view. The grid's resolver keeps its identity while the row store
|
|
15
|
+
// keeps its id maps (cell edits do), so the entry lives exactly as long as it can be reused.
|
|
16
|
+
const cache = new WeakMap<(row: never) => GridRowId, Map<string, CacheEntry>>();
|
|
17
|
+
|
|
18
|
+
function build<R extends object>(rows: readonly R[], getRowId: (row: R) => GridRowId): GridRowIndex {
|
|
19
|
+
const rowIds = new Array<GridRowId>(rows.length);
|
|
20
|
+
const rowIndexById = new Map<GridRowId, number>();
|
|
21
|
+
for (let index = 0; index < rows.length; index++) {
|
|
22
|
+
const id = getRowId(rows[index]);
|
|
23
|
+
rowIds[index] = id;
|
|
24
|
+
if (!rowIndexById.has(id)) rowIndexById.set(id, index);
|
|
25
|
+
}
|
|
26
|
+
return { rowIds, rowIndexById };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* `rows.map(getRowId)` plus an id → first index map, reusing the previous result for the same
|
|
31
|
+
* resolver and `view` when the ids are unchanged position by position. A cell edit replaces row
|
|
32
|
+
* objects but never moves or re-keys rows, so after an edit only the replaced positions are
|
|
33
|
+
* resolved (a reference comparison per row instead of a map lookup per row, ~20× cheaper at 1M
|
|
34
|
+
* rows), and callers memoized on the returned object do not recompute. Treat the result as
|
|
35
|
+
* read-only: it may be shared.
|
|
36
|
+
*/
|
|
37
|
+
export function getRowIndex<R extends object>(
|
|
38
|
+
rows: readonly R[],
|
|
39
|
+
getRowId: (row: R) => GridRowId,
|
|
40
|
+
view: string,
|
|
41
|
+
): GridRowIndex {
|
|
42
|
+
let byView = cache.get(getRowId);
|
|
43
|
+
const previous = byView?.get(view);
|
|
44
|
+
if (previous && previous.rows.length === rows.length) {
|
|
45
|
+
const prevRows = previous.rows as readonly R[];
|
|
46
|
+
const prevIds = previous.value.rowIds;
|
|
47
|
+
let same = true;
|
|
48
|
+
for (let index = 0; index < rows.length; index++) {
|
|
49
|
+
const row = rows[index];
|
|
50
|
+
if (row !== prevRows[index] && getRowId(row) !== prevIds[index]) {
|
|
51
|
+
same = false;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (same) {
|
|
56
|
+
previous.rows = rows;
|
|
57
|
+
return previous.value;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const value = build(rows, getRowId);
|
|
61
|
+
if (!byView) {
|
|
62
|
+
byView = new Map();
|
|
63
|
+
cache.set(getRowId, byView);
|
|
64
|
+
}
|
|
65
|
+
byView.set(view, { rows, value });
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
@@ -18,7 +18,8 @@ const RANK_STRING = 3;
|
|
|
18
18
|
const RANK_OTHER = 4;
|
|
19
19
|
|
|
20
20
|
/** A normalized sort value. null is the empty bucket (null, undefined, NaN, Invalid Date, unparsable). */
|
|
21
|
-
|
|
21
|
+
/** `big`: the exact value of a bigint key, whose `num` is only its nearest double. */
|
|
22
|
+
type SortKey = { rank: number; num: number; str: string; big?: bigint } | null;
|
|
22
23
|
|
|
23
24
|
function numberKey(rank: number, num: number): SortKey {
|
|
24
25
|
return { rank, num, str: '' };
|
|
@@ -32,6 +33,7 @@ function numberKey(rank: number, num: number): SortKey {
|
|
|
32
33
|
function toSortKey(value: unknown, type?: GridColDef['type']): SortKey {
|
|
33
34
|
if (value == null) return null;
|
|
34
35
|
if (type === 'number') {
|
|
36
|
+
if (typeof value === 'bigint') return { rank: RANK_NUMBER, num: Number(value), str: '', big: value };
|
|
35
37
|
const n = toNumber(value);
|
|
36
38
|
return n === null ? null : numberKey(RANK_NUMBER, n);
|
|
37
39
|
}
|
|
@@ -43,7 +45,7 @@ function toSortKey(value: unknown, type?: GridColDef['type']): SortKey {
|
|
|
43
45
|
case 'number':
|
|
44
46
|
return Number.isNaN(value) ? null : numberKey(RANK_NUMBER, value);
|
|
45
47
|
case 'bigint':
|
|
46
|
-
return
|
|
48
|
+
return { rank: RANK_NUMBER, num: Number(value), str: '', big: value };
|
|
47
49
|
case 'boolean':
|
|
48
50
|
return numberKey(RANK_BOOLEAN, value ? 1 : 0);
|
|
49
51
|
case 'string':
|
|
@@ -65,9 +67,29 @@ function compareSortKeys(a: SortKey, b: SortKey): number {
|
|
|
65
67
|
if (b === null) return -1;
|
|
66
68
|
if (a.rank !== b.rank) return a.rank - b.rank;
|
|
67
69
|
if (a.rank === RANK_STRING || a.rank === RANK_OTHER) return collator.compare(a.str, b.str);
|
|
68
|
-
|
|
70
|
+
if (a.num !== b.num) return a.num < b.num ? -1 : 1;
|
|
71
|
+
// Number(bigint) rounds monotonically, so only equal doubles can hide an order between bigints.
|
|
72
|
+
return a.big !== undefined || b.big !== undefined ? compareExact(a.big ?? a.num, b.big ?? b.num) : 0;
|
|
69
73
|
}
|
|
70
74
|
|
|
75
|
+
/** Exact ascending comparison of numbers and bigints (no rounding of either). */
|
|
76
|
+
function compareExact(a: number | bigint, b: number | bigint): number {
|
|
77
|
+
if (typeof a === 'number' && typeof b === 'number') return a < b ? -1 : a > b ? 1 : 0;
|
|
78
|
+
if (typeof a === 'bigint' && typeof b === 'bigint') return a < b ? -1 : a > b ? 1 : 0;
|
|
79
|
+
if (typeof a === 'number') return -compareExact(b, a);
|
|
80
|
+
// a is a bigint, b a number.
|
|
81
|
+
const n = b as number;
|
|
82
|
+
if (!Number.isFinite(n)) return n > 0 ? -1 : 1;
|
|
83
|
+
const floor = Math.floor(n);
|
|
84
|
+
const whole = BigInt(floor);
|
|
85
|
+
if (a < whole) return -1;
|
|
86
|
+
if (a > whole) return 1;
|
|
87
|
+
return n > floor ? -1 : 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const isUnsafeBigInt = (value: unknown): value is bigint =>
|
|
91
|
+
typeof value === 'bigint' && (value > BigInt(Number.MAX_SAFE_INTEGER) || value < BigInt(Number.MIN_SAFE_INTEGER));
|
|
92
|
+
|
|
71
93
|
/** Reverse an ascending comparison for 'desc'. */
|
|
72
94
|
export function applySortDirection(comparison: number, direction: 'asc' | 'desc'): number {
|
|
73
95
|
if (comparison === 0) return 0;
|
|
@@ -115,36 +137,6 @@ export function callSortComparator(
|
|
|
115
137
|
|
|
116
138
|
const defaultRowId = (row: GridRowModel): GridRowId => row.id;
|
|
117
139
|
|
|
118
|
-
/**
|
|
119
|
-
* Compare two rows by a sort model, reading cells through `valueGetter` and using each column's
|
|
120
|
-
* `type` (or its `sortComparator`). Returns 0 when every key is equal.
|
|
121
|
-
*/
|
|
122
|
-
export function compareRowsBySortModel(
|
|
123
|
-
a: GridRowModel,
|
|
124
|
-
b: GridRowModel,
|
|
125
|
-
sortModel: readonly GridSortItem[],
|
|
126
|
-
columns?: GridColumnLookup,
|
|
127
|
-
getRowId: (row: GridRowModel) => GridRowId = defaultRowId
|
|
128
|
-
): number {
|
|
129
|
-
for (const sortItem of sortModel) {
|
|
130
|
-
const colDef = columns?.byField.get(sortItem.field);
|
|
131
|
-
const va = getCellValue(a, sortItem.field, colDef);
|
|
132
|
-
const vb = getCellValue(b, sortItem.field, colDef);
|
|
133
|
-
const comparison = colDef?.sortComparator
|
|
134
|
-
? applySortDirection(
|
|
135
|
-
callSortComparator(
|
|
136
|
-
colDef.sortComparator,
|
|
137
|
-
{ id: getRowId(a), field: sortItem.field, row: a, value: va },
|
|
138
|
-
{ id: getRowId(b), field: sortItem.field, row: b, value: vb }
|
|
139
|
-
),
|
|
140
|
-
sortItem.sort
|
|
141
|
-
)
|
|
142
|
-
: compareValues(va, vb, sortItem.sort, colDef?.type);
|
|
143
|
-
if (comparison !== 0) return comparison;
|
|
144
|
-
}
|
|
145
|
-
return 0;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
140
|
export function sortRows<R extends GridRowModel>(
|
|
149
141
|
rows: R[],
|
|
150
142
|
sortModel: GridSortItem[],
|
|
@@ -230,6 +222,19 @@ function rankTexts(buckets: Map<string, number[]>, num: Float64Array): void {
|
|
|
230
222
|
}
|
|
231
223
|
}
|
|
232
224
|
|
|
225
|
+
/** Replaces the numbers of a column holding unsafe bigints by their exact ascending position. */
|
|
226
|
+
function rankNumbersExactly(group: Uint8Array, num: Float64Array, bigints: ReadonlyMap<number, bigint>): void {
|
|
227
|
+
const indices: number[] = [];
|
|
228
|
+
for (let i = 0; i < group.length; i++) if (group[i] === RANK_NUMBER) indices.push(i);
|
|
229
|
+
const exact = (i: number): number | bigint => bigints.get(i) ?? num[i];
|
|
230
|
+
indices.sort((ia, ib) => compareExact(exact(ia), exact(ib)));
|
|
231
|
+
let position = 0;
|
|
232
|
+
for (let k = 0; k < indices.length; k++) {
|
|
233
|
+
if (k > 0 && compareExact(exact(indices[k - 1]), exact(indices[k])) !== 0) position++;
|
|
234
|
+
num[indices[k]] = position;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
233
238
|
function addToBucket(buckets: Map<string, number[]>, text: string, index: number): void {
|
|
234
239
|
const bucket = buckets.get(text);
|
|
235
240
|
if (bucket) bucket.push(index);
|
|
@@ -248,9 +253,12 @@ function encodeSortColumn(count: number, valueAt: (i: number) => unknown, typeAt
|
|
|
248
253
|
const num = new Float64Array(count);
|
|
249
254
|
const strings = new Map<string, number[]>();
|
|
250
255
|
const others = new Map<string, number[]>();
|
|
256
|
+
// Bigints beyond 2^53 lose precision as doubles: if any appears, numbers are re-ranked exactly.
|
|
257
|
+
let bigints: Map<number, bigint> | null = null;
|
|
251
258
|
for (let i = 0; i < count; i++) {
|
|
252
259
|
const value = valueAt(i);
|
|
253
260
|
const type = typeAt(i);
|
|
261
|
+
if (isUnsafeBigInt(value) && type !== 'date') (bigints ??= new Map()).set(i, value);
|
|
254
262
|
if (typeof value === 'number' && type !== 'date') {
|
|
255
263
|
// toSortKey for a number, without the allocation.
|
|
256
264
|
if (Number.isNaN(value)) {
|
|
@@ -273,6 +281,7 @@ function encodeSortColumn(count: number, valueAt: (i: number) => unknown, typeAt
|
|
|
273
281
|
}
|
|
274
282
|
rankTexts(strings, num);
|
|
275
283
|
rankTexts(others, num);
|
|
284
|
+
if (bigints) rankNumbersExactly(group, num, bigints);
|
|
276
285
|
return { kind: 'encoded', group, num };
|
|
277
286
|
}
|
|
278
287
|
|
package/lib/utils/values.ts
CHANGED
|
@@ -84,6 +84,8 @@ export function getFormattedValue<R extends GridRowModel>(
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
const RADIX_PREFIX = /^[+-]?0[box]/i;
|
|
88
|
+
|
|
87
89
|
/**
|
|
88
90
|
* Coerce a cell or filter value to a number. Returns null for null/undefined, blank strings,
|
|
89
91
|
* NaN and anything that does not parse, so an empty cell is never read as 0.
|
|
@@ -94,8 +96,10 @@ export function toNumber(value: unknown): number | null {
|
|
|
94
96
|
if (typeof value === 'string') {
|
|
95
97
|
const trimmed = value.trim();
|
|
96
98
|
if (trimmed === '') return null;
|
|
99
|
+
// Number() also reads "0x10", "0b1", "0o7" and "Infinity", which nobody types as a number.
|
|
100
|
+
if (RADIX_PREFIX.test(trimmed)) return null;
|
|
97
101
|
const n = Number(trimmed);
|
|
98
|
-
return Number.
|
|
102
|
+
return Number.isFinite(n) ? n : null;
|
|
99
103
|
}
|
|
100
104
|
if (typeof value === 'boolean') return null;
|
|
101
105
|
if (value instanceof Date) {
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# OpenGridX — AI Agent Context
|
|
2
2
|
# Package: @opencorestack/opengridx
|
|
3
|
-
# Version: 3.
|
|
3
|
+
# Version: 3.8.1
|
|
4
4
|
# Docs: https://opencorestack.github.io/OpenGridX/
|
|
5
5
|
# This file is machine-readable. AI developers: start here.
|
|
6
6
|
|
|
@@ -721,9 +721,13 @@ Arrows move between cells (skipping hidden columns); Home/End, Ctrl+Home/End, Pa
|
|
|
721
721
|
13. **`valueFormatter` + `renderCell`**: `renderCell` receives `params.formattedValue` (v2.1+), so a custom renderer can reuse the formatter instead of duplicating it.
|
|
722
722
|
14. **`slots.footer`** replaces the pagination area and receives `apiRef`, `aggregationModel`, `aggregationResult`, `rowCount` (filtered data rows excluding pinned rows in flat grids — data rows under tree data / grouping; the server total with `paginationMode="server"`), `pagination`, `paginationModel`, `pageSizeOptions` and `onPaginationModelChange`.
|
|
723
723
|
15. **v3: no underscore row fields** — `params.row._hasChildren` / `_treeDepth` / … are `undefined` since 3.0.0; use `params.rowMeta?.hasChildren` etc. Under grouping and tree data `params.row` is the consumer's own object; never mutate it in render callbacks. Migration guide: `docs/migration/v2-to-v3.md`.
|
|
724
|
-
16. **Row limit**: the scroll height is `rows × rowHeight` px and browsers cap element height
|
|
724
|
+
16. **Row limit**: the scroll height is `rows × rowHeight` px and browsers cap element height: Chromium and WebKit about 33.5M px (~645,000 rows at 52 px), Firefox about 17.9M px (~344,000 rows). Since 3.5.1 the grid caps its content height to the measured limit and warns in development, naming how many rows are reachable. Beyond that use a `dataSource` (server pages or infinite scroll) or pagination; `density="compact"` stretches each limit about 1.6×.
|
|
725
725
|
17. **Server modes without dataSource**: with `paginationMode` / `sortingMode` / `filterMode` = `'server'` the grid shows `rows` as given (no slicing, sorting or filtering). Fetch from the change callbacks and control the models.
|
|
726
726
|
18. **`valueFormatter` / `valueGetter` params are `unknown`-typed**: cast (`(value as number).toFixed(2)`) before calling methods, or TypeScript strict mode rejects it.
|
|
727
727
|
19. **Inline `columns` are reused while shallow-equal**: a new array with the same column objects does not re-run filtering/sorting, but mutating a column object in place is not detected — pass new objects when a definition changes.
|
|
728
728
|
20. **Next.js App Router**: render `<DataGrid>` from a `'use client'` file (column functions such as `valueFormatter` cannot cross from a Server Component) and import the stylesheet in `app/layout.tsx`. Rows fetched in a Server Component pass as props (serialisable data only). The ES bundle starts with `'use client'` (3.6+), so a Server Component may also render `<DataGrid>` with serialisable props. `next build` fails with `Can't resolve 'exceljs'` unless the optional peers are installed or aliased in `next.config.ts` (`turbopack.resolveAlias` to an empty module; webpack `resolve.alias[name] = false`). `@opencorestack/opengridx/ai` runs on the server too, but the validation brand is a Symbol that does not survive JSON: validate on the client (`createGridAiPromptHandler`). Guide: `docs/features/nextjs.md`.
|
|
729
|
-
21. **Right-to-left**: wrap the grid in `dir="rtl"` (or set `document.dir`); no prop. Works without pinned columns (3.6+): column order, text alignment (`align: 'left'`/'right' = start/end), horizontal scrolling + column virtualization, ArrowLeft = next column, resize handle on the left edge, fill handle bottom-left, toolbar panels open rightwards.
|
|
729
|
+
21. **Right-to-left**: wrap the grid in `dir="rtl"` (or set `document.dir`); no prop. Works without pinned columns (3.6+): column order, text alignment (`align: 'left'`/'right' = start/end), horizontal scrolling + column virtualization, ArrowLeft = next column, resize handle on the left edge, fill handle bottom-left, toolbar panels open rightwards. Pinned columns work in RTL (3.6+): `pinnedColumns.left` / `right` are logical (inline start / end), so `left` sticks to the right edge in RTL. Guide: `docs/features/rtl.md`.
|
|
730
|
+
22. **Rows are replaced, not mutated** (3.8+ relies on it): change a row by passing a new object (`rows.map(r => r.id === id ? { ...r, ...changes } : r)`). The quick filter caches each row object's searched text, and an in-place mutation keeps the old text. A `valueFormatter` that reads outside state needs new column definitions when that state changes.
|
|
731
|
+
23. **Large grids (3.8+)**: committing an edit, paste, undo, redo or fill at 1,000,000 rows takes ~35–50 ms; expanding a group ~135 ms, collapsing ~1 ms; select-all ~270 ms (it builds the selection model array). Sorting is ~0.4–0.5 s at 1M rows.
|
|
732
|
+
24. **Styling buttons inside the grid (3.8.1+)**: the grid's button reset sets `background` and `border` at zero specificity, so any class you put on a button inside the grid (custom toolbar slot, `renderCell`) wins without `!important`.
|
|
733
|
+
25. **Quick filter text** (3.7.1+): besides the raw value and any `valueFormatter` text, `singleSelect` columns match their option label, `boolean` columns Yes / No, and `date` columns the locale date shown. Exports read values through the same `valueGetter` handling as the grid (a throw exports an empty cell), and CSV / HTML-Excel escape formula text from a `valueFormatter` unless it reads as a plain number.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencorestack/opengridx",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.8.1",
|
|
4
4
|
"description": "OpenGridX: High-performance React data infrastructure. Unlock advanced Row Grouping, Excel Export, and Column Pinning without the usual \"Pro\" gatekeeping. Built for speed, scale, and complete architectural freedom. Fully open, virtualization-ready, and feature-complete.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/opengridx.umd.js",
|
|
File without changes
|