@opencorestack/opengridx 3.0.0 → 3.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/CHANGELOG.md +50 -0
- package/README.md +10 -6
- package/dist/index.d.ts +51 -8
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +4056 -3957
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +14 -14
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +15 -8
- package/docs/architecture/datagrid-orchestration.md +1 -0
- package/docs/components/column-resize.md +14 -2
- package/docs/components/datagrid.md +1 -1
- package/docs/customization/theming.md +1 -1
- package/docs/features/list-view.md +1 -1
- package/docs/features/pdf-export.md +16 -0
- package/docs/features/pinning.md +1 -0
- package/docs/features/sorting-pagination.md +26 -0
- package/docs/features/virtualization.md +11 -9
- package/docs/migration/v2-to-v3.md +13 -2
- package/docs/roadmap.md +1 -1
- package/lib/components/Cell/Cell.css +4 -0
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +5 -3
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +12 -3
- package/lib/components/DataGrid/DataGrid.css +8 -0
- package/lib/components/DataGrid/DataGrid.tsx +9 -2
- package/lib/components/DataGrid/GridListView.tsx +2 -1
- package/lib/components/Header/Header.css +3 -0
- package/lib/components/Header/Header.tsx +7 -1
- package/lib/hooks/core/gridApiPlaceholder.ts +4 -2
- package/lib/hooks/core/useGridApiRef.ts +7 -4
- package/lib/hooks/core/useGridColumnAutosize.ts +52 -0
- package/lib/hooks/core/useGridDevWarnings.ts +23 -5
- package/lib/hooks/core/useGridRowPipeline.ts +2 -2
- package/lib/hooks/useRowGrouping.ts +20 -3
- package/lib/hooks/useTreeData.ts +8 -1
- package/lib/index.ts +1 -0
- package/lib/types/index.ts +46 -7
- package/lib/utils/columnAutosize.ts +56 -0
- package/lib/utils/pivot/index.ts +11 -2
- package/lib/utils/sorting/index.ts +77 -24
- package/llms.txt +12 -7
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GridApi } from '../../types';
|
|
1
|
+
import type { GridApi, GridRowModel, GridValidRowModel } from '../../types';
|
|
2
2
|
|
|
3
3
|
const noop = () => {};
|
|
4
4
|
|
|
@@ -7,7 +7,7 @@ const noop = () => {};
|
|
|
7
7
|
* `apiRef.current` is never null (its type says it is not) before the grid has mounted;
|
|
8
8
|
* the grid replaces it with the live API when it mounts.
|
|
9
9
|
*/
|
|
10
|
-
export function createGridApiPlaceholder(): GridApi {
|
|
10
|
+
export function createGridApiPlaceholder<R extends GridValidRowModel = GridRowModel>(): GridApi<R> {
|
|
11
11
|
return {
|
|
12
12
|
getRow: () => null,
|
|
13
13
|
getAllRows: () => [],
|
|
@@ -29,6 +29,8 @@ export function createGridApiPlaceholder(): GridApi {
|
|
|
29
29
|
setPage: noop,
|
|
30
30
|
setPageSize: noop,
|
|
31
31
|
scrollToIndexes: noop,
|
|
32
|
+
autosizeColumn: noop,
|
|
33
|
+
autosizeColumns: noop,
|
|
32
34
|
copySelectedRows: () => Promise.resolve(),
|
|
33
35
|
};
|
|
34
36
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useRef, useState } from 'react';
|
|
2
2
|
import type { MutableRefObject } from 'react';
|
|
3
|
-
import type { GridApi } from '../../types';
|
|
3
|
+
import type { GridApi, GridRowModel, GridValidRowModel } from '../../types';
|
|
4
4
|
import { createGridApiPlaceholder } from './gridApiPlaceholder';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -15,8 +15,11 @@ import { createGridApiPlaceholder } from './gridApiPlaceholder';
|
|
|
15
15
|
* which matches `DataGridProps.apiRef` under both @types/react 18 and 19. Left to inference,
|
|
16
16
|
* React 19's types emit `RefObject<GridApi>`, which React 18's types treat as read-only and
|
|
17
17
|
* nullable, so `apiRef={useGridApiRef()}` failed to type-check on React 18.
|
|
18
|
+
*
|
|
19
|
+
* Pass your row type to have the row getters typed: `useGridApiRef<Invoice>().current.getRow(id)`
|
|
20
|
+
* returns `Invoice | null`.
|
|
18
21
|
*/
|
|
19
|
-
export function useGridApiRef(): MutableRefObject<GridApi
|
|
20
|
-
const [placeholder] = useState(createGridApiPlaceholder);
|
|
21
|
-
return useRef<GridApi
|
|
22
|
+
export function useGridApiRef<R extends GridValidRowModel = GridRowModel>(): MutableRefObject<GridApi<R>> {
|
|
23
|
+
const [placeholder] = useState(createGridApiPlaceholder<R>);
|
|
24
|
+
return useRef<GridApi<R>>(placeholder);
|
|
22
25
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useCallback, useLayoutEffect, useRef } from 'react';
|
|
2
|
+
import type { MutableRefObject, RefObject } from 'react';
|
|
3
|
+
import type { GridApi, GridColDef, GridValidRowModel } from '../../types';
|
|
4
|
+
import { clampAutosizeWidth, measureColumnContentWidth } from '../../utils/columnAutosize';
|
|
5
|
+
|
|
6
|
+
export interface UseGridColumnAutosizeParams<R extends GridValidRowModel> {
|
|
7
|
+
apiRef: MutableRefObject<GridApi>;
|
|
8
|
+
/** The grid root (`.ogx`). */
|
|
9
|
+
containerRef: RefObject<HTMLDivElement | null>;
|
|
10
|
+
/** Every column the grid knows (hidden ones are measured as not rendered and left alone). */
|
|
11
|
+
columns: readonly GridColDef<R>[];
|
|
12
|
+
/** The width setter a manual resize uses. */
|
|
13
|
+
onColumnResize: (field: string, width: number) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Auto-sizing of columns to their content: the header and the cells in the current render window,
|
|
18
|
+
* measured in the DOM, clamped to `minWidth` / `maxWidth`. Columns with `resizable: false` are left
|
|
19
|
+
* alone. A flex column gets a fixed width, like after a manual resize. Installs `autosizeColumn` /
|
|
20
|
+
* `autosizeColumns` on the API and returns `autosizeColumn` for the resize handle.
|
|
21
|
+
*/
|
|
22
|
+
export function useGridColumnAutosize<R extends GridValidRowModel>(params: UseGridColumnAutosizeParams<R>): (field: string) => void {
|
|
23
|
+
const { apiRef } = params;
|
|
24
|
+
const latestRef = useRef(params);
|
|
25
|
+
useLayoutEffect(() => {
|
|
26
|
+
latestRef.current = params;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const autosizeColumns = useCallback((fields?: readonly string[]) => {
|
|
30
|
+
const { containerRef, columns, onColumnResize } = latestRef.current;
|
|
31
|
+
const root = containerRef.current;
|
|
32
|
+
if (!root) return;
|
|
33
|
+
// Measure every column first: the reads all see the same layout, then the widths are set.
|
|
34
|
+
const widths: Array<[string, number]> = [];
|
|
35
|
+
for (const field of fields ?? columns.map(c => c.field)) {
|
|
36
|
+
const colDef = columns.find(c => c.field === field);
|
|
37
|
+
if (!colDef || colDef.resizable === false) continue;
|
|
38
|
+
const measured = measureColumnContentWidth(root, field);
|
|
39
|
+
if (measured !== null) widths.push([field, clampAutosizeWidth(measured, colDef.minWidth, colDef.maxWidth)]);
|
|
40
|
+
}
|
|
41
|
+
for (const [field, width] of widths) onColumnResize(field, width);
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
const autosizeColumn = useCallback((field: string) => autosizeColumns([field]), [autosizeColumns]);
|
|
45
|
+
|
|
46
|
+
useLayoutEffect(() => {
|
|
47
|
+
apiRef.current.autosizeColumn = autosizeColumn;
|
|
48
|
+
apiRef.current.autosizeColumns = autosizeColumns;
|
|
49
|
+
}, [apiRef, autosizeColumn, autosizeColumns]);
|
|
50
|
+
|
|
51
|
+
return autosizeColumn;
|
|
52
|
+
}
|
|
@@ -14,11 +14,16 @@ export interface UseGridDevWarningsParams {
|
|
|
14
14
|
scrollHeight?: number;
|
|
15
15
|
/** `listView` is set but `listViewColumn` is not, so the grid view is shown. */
|
|
16
16
|
listViewWithoutColumn?: boolean;
|
|
17
|
+
/** Number of items the list view renders (0 when the list view is not shown). It does not virtualize. */
|
|
18
|
+
listViewRowCount?: number;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
// Below this many rows, rendering everything is harmless, so an unbounded container is not worth a warning.
|
|
20
22
|
const UNBOUNDED_WARN_THRESHOLD = 200;
|
|
21
23
|
|
|
24
|
+
/** The list view renders every item; above this many the page slows down noticeably. */
|
|
25
|
+
export const LIST_VIEW_WARN_THRESHOLD = 2000;
|
|
26
|
+
|
|
22
27
|
/**
|
|
23
28
|
* Browsers cap element heights (Firefox at about 17.9M px, Chromium and Safari at about 33.5M px):
|
|
24
29
|
* rows laid out below the cap cannot be scrolled to.
|
|
@@ -26,12 +31,22 @@ const UNBOUNDED_WARN_THRESHOLD = 200;
|
|
|
26
31
|
export const MAX_SCROLLABLE_HEIGHT = 17_800_000;
|
|
27
32
|
|
|
28
33
|
export function useGridDevWarnings(params: UseGridDevWarningsParams): void {
|
|
29
|
-
const { paginationRequested, isRowGrouping, autoHeight, viewportHeight, renderedRowCount, totalRowCount, pinnedRowsIgnored = false, scrollHeight = 0, listViewWithoutColumn = false } = params;
|
|
34
|
+
const { paginationRequested, isRowGrouping, autoHeight, viewportHeight, renderedRowCount, totalRowCount, pinnedRowsIgnored = false, scrollHeight = 0, listViewWithoutColumn = false, listViewRowCount = 0 } = params;
|
|
30
35
|
const warnedPaginationRef = useRef(false);
|
|
31
36
|
const warnedUnboundedRef = useRef(false);
|
|
32
37
|
const warnedPinnedRowsRef = useRef(false);
|
|
33
38
|
const warnedHeightRef = useRef(false);
|
|
34
39
|
const warnedListViewRef = useRef(false);
|
|
40
|
+
const warnedListViewSizeRef = useRef(false);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (process.env.NODE_ENV === 'production' || warnedListViewSizeRef.current || listViewRowCount <= LIST_VIEW_WARN_THRESHOLD) return;
|
|
44
|
+
warnedListViewSizeRef.current = true;
|
|
45
|
+
console.warn(
|
|
46
|
+
`[OpenGridX] The list view is rendering all ${listViewRowCount} items: it does not virtualize. ` +
|
|
47
|
+
'Above a few thousand items use `pagination`, server-side loading, or the grid view. See docs/features/list-view.md.'
|
|
48
|
+
);
|
|
49
|
+
}, [listViewRowCount]);
|
|
35
50
|
|
|
36
51
|
useEffect(() => {
|
|
37
52
|
if (process.env.NODE_ENV === 'production' || warnedListViewRef.current || !listViewWithoutColumn) return;
|
|
@@ -74,15 +89,18 @@ export function useGridDevWarnings(params: UseGridDevWarningsParams): void {
|
|
|
74
89
|
|
|
75
90
|
useEffect(() => {
|
|
76
91
|
if (process.env.NODE_ENV === 'production' || warnedUnboundedRef.current || autoHeight) return;
|
|
77
|
-
|
|
92
|
+
// Really unbounded: every row is rendered and the viewport is at least as tall as the rows, so it never scrolls.
|
|
93
|
+
if (viewportHeight > 0 && totalRowCount > UNBOUNDED_WARN_THRESHOLD && renderedRowCount >= totalRowCount && viewportHeight >= scrollHeight) {
|
|
78
94
|
warnedUnboundedRef.current = true;
|
|
79
95
|
console.warn(
|
|
80
96
|
`[OpenGridX] The grid viewport is ${Math.round(viewportHeight)}px tall and is rendering all ${totalRowCount} rows, ` +
|
|
81
|
-
'so row virtualization is effectively off
|
|
82
|
-
'
|
|
97
|
+
'so row virtualization is effectively off: the grid fills its container, and the container grows to fit every row. ' +
|
|
98
|
+
'Give the container a definite height (for example `height: 600px` or `100vh`). If the container, or any ancestor ' +
|
|
99
|
+
'up to the sized one, is a flex or grid item, add `min-height: 0` to it. Alternatively pass a `height` prop, ' +
|
|
100
|
+
'or `autoHeight` if rendering every row is intended. See docs/features/virtualization.md.'
|
|
83
101
|
);
|
|
84
102
|
}
|
|
85
|
-
}, [autoHeight, viewportHeight, renderedRowCount, totalRowCount]);
|
|
103
|
+
}, [autoHeight, viewportHeight, renderedRowCount, totalRowCount, scrollHeight]);
|
|
86
104
|
}
|
|
87
105
|
|
|
88
106
|
/**
|
|
@@ -103,8 +103,8 @@ export function useGridRowPipeline<R extends GridRowModel>(
|
|
|
103
103
|
const sortedUnpinnedRows = useMemo<R[]>(() => {
|
|
104
104
|
if (activeHierarchyHandlers) return unpinnedRows;
|
|
105
105
|
if (sortingMode === 'server') return unpinnedRows;
|
|
106
|
-
return sortRows(unpinnedRows, sortModel, columnLookup) as R[];
|
|
107
|
-
}, [unpinnedRows, sortModel, activeHierarchyHandlers, sortingMode, columnLookup]);
|
|
106
|
+
return sortRows(unpinnedRows, sortModel, columnLookup, getRowId) as R[];
|
|
107
|
+
}, [unpinnedRows, sortModel, activeHierarchyHandlers, sortingMode, columnLookup, getRowId]);
|
|
108
108
|
|
|
109
109
|
const isClientPaged = pagination && paginationMode !== 'server';
|
|
110
110
|
const pageSize = normalizePageSize(effectivePaginationModel.pageSize);
|
|
@@ -362,10 +362,20 @@ export function useRowGrouping<R extends GridRowModel>(params: UseRowGroupingPar
|
|
|
362
362
|
|
|
363
363
|
const readSortValue = (id: GridRowId, sortItem: GridSortItem): GridSortValue => {
|
|
364
364
|
const node = treeNodes.get(id);
|
|
365
|
-
|
|
365
|
+
const groupVariants = groupRowVariants.get(id);
|
|
366
|
+
if (node && groupVariants) {
|
|
366
367
|
const groupingField = node.groupingField ?? '';
|
|
367
368
|
if (sortItem.field === groupingField || sortItem.field === labelField) {
|
|
368
|
-
|
|
369
|
+
const groupingColDef = columnLookup?.byField.get(groupingField);
|
|
370
|
+
const comparator = groupingColDef?.sortComparator;
|
|
371
|
+
return {
|
|
372
|
+
value: node.groupingValue,
|
|
373
|
+
type: groupingColDef?.type,
|
|
374
|
+
comparator,
|
|
375
|
+
params: comparator
|
|
376
|
+
? { id, field: groupingField, row: groupVariants.inline, value: node.groupingValue }
|
|
377
|
+
: undefined,
|
|
378
|
+
};
|
|
369
379
|
}
|
|
370
380
|
if (node.aggregatedValues && Object.prototype.hasOwnProperty.call(node.aggregatedValues, sortItem.field)) {
|
|
371
381
|
return { value: node.aggregatedValues[sortItem.field], type: columnLookup?.byField.get(sortItem.field)?.type };
|
|
@@ -374,7 +384,14 @@ export function useRowGrouping<R extends GridRowModel>(params: UseRowGroupingPar
|
|
|
374
384
|
}
|
|
375
385
|
const row = rowLookup.get(id);
|
|
376
386
|
const colDef = columnLookup?.byField.get(sortItem.field);
|
|
377
|
-
|
|
387
|
+
const value = row ? getCellValue(row, sortItem.field, colDef) : undefined;
|
|
388
|
+
const comparator = colDef?.sortComparator;
|
|
389
|
+
return {
|
|
390
|
+
value,
|
|
391
|
+
type: colDef?.type,
|
|
392
|
+
comparator,
|
|
393
|
+
params: comparator && row ? { id, field: sortItem.field, row, value } : undefined,
|
|
394
|
+
};
|
|
378
395
|
};
|
|
379
396
|
|
|
380
397
|
const result: R[] = [];
|
package/lib/hooks/useTreeData.ts
CHANGED
|
@@ -333,7 +333,14 @@ export function useTreeData<R extends GridRowModel>(props: UseTreeDataProps<R>)
|
|
|
333
333
|
}
|
|
334
334
|
const row = rowLookup.get(id);
|
|
335
335
|
const colDef = columnLookup?.byField.get(sortItem.field);
|
|
336
|
-
|
|
336
|
+
const value = row ? getCellValue(row, sortItem.field, colDef) : undefined;
|
|
337
|
+
const comparator = colDef?.sortComparator;
|
|
338
|
+
return {
|
|
339
|
+
value,
|
|
340
|
+
type: colDef?.type,
|
|
341
|
+
comparator,
|
|
342
|
+
params: comparator && row ? { id, field: sortItem.field, row, value } : undefined,
|
|
343
|
+
};
|
|
337
344
|
};
|
|
338
345
|
|
|
339
346
|
const result: GridRowModel[] = [];
|
package/lib/index.ts
CHANGED
package/lib/types/index.ts
CHANGED
|
@@ -172,6 +172,16 @@ export interface GridColDef<R extends GridValidRowModel = GridRowModel> {
|
|
|
172
172
|
* @since v3.0
|
|
173
173
|
*/
|
|
174
174
|
valueSetter?(params: GridValueSetterParams<R>): R;
|
|
175
|
+
/**
|
|
176
|
+
* Custom ascending comparator for client-side sorting by this column, used instead of the built-in
|
|
177
|
+
* type-aware comparison. `v1` / `v2` are the cell values (`valueGetter` applied) and **include
|
|
178
|
+
* `null` / `undefined`**, so the comparator decides where empty values go. The grid reverses the
|
|
179
|
+
* result for 'desc' and chains it with the other sort keys in a multi-sort. Row grouping uses the
|
|
180
|
+
* grouping column's comparator to order groups by their grouping value. A comparator that throws
|
|
181
|
+
* or returns NaN is read as 0 (with a one-time dev warning). Ignored with `sortingMode="server"`.
|
|
182
|
+
* @since v3.1
|
|
183
|
+
*/
|
|
184
|
+
sortComparator?(v1: unknown, v2: unknown, params1: GridSortCellParams<R>, params2: GridSortCellParams<R>): number;
|
|
175
185
|
/** Function to format a value into a human-readable string. */
|
|
176
186
|
valueFormatter?(params: GridValueFormatterParams<R>): string;
|
|
177
187
|
/** Custom component or element to render in the cell. */
|
|
@@ -262,6 +272,18 @@ export interface GridColumnGroup {
|
|
|
262
272
|
export type GridColumnGroupingModel = GridColumnGroup[];
|
|
263
273
|
|
|
264
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Parameters passed to `GridColDef.sortComparator` for each side of a comparison. For a row-grouping
|
|
277
|
+
* group row, `row` is the synthetic group row and `value` its grouping value.
|
|
278
|
+
* @since v3.1
|
|
279
|
+
*/
|
|
280
|
+
export interface GridSortCellParams<R extends GridValidRowModel = GridRowModel> {
|
|
281
|
+
id: GridRowId;
|
|
282
|
+
field: string;
|
|
283
|
+
row: R;
|
|
284
|
+
value: unknown;
|
|
285
|
+
}
|
|
286
|
+
|
|
265
287
|
/** Parameters passed to the `valueGetter` function. */
|
|
266
288
|
export interface GridValueGetterParams<R extends GridValidRowModel = GridRowModel> {
|
|
267
289
|
/** The row object. */
|
|
@@ -912,8 +934,11 @@ export interface DataGridProps<R extends GridValidRowModel = GridRowModel> {
|
|
|
912
934
|
/** Callback fired when scrolling reaches the bottom of the grid viewport. */
|
|
913
935
|
onRowsScrollEnd?: (params: GridRowScrollEndParams) => void;
|
|
914
936
|
|
|
915
|
-
/**
|
|
916
|
-
|
|
937
|
+
/**
|
|
938
|
+
* Advanced: reactive reference to the internal API for imperative control. Pass
|
|
939
|
+
* `useGridApiRef<MyRow>()` to have the row getters typed as `MyRow`; a plain `useGridApiRef()` also works.
|
|
940
|
+
*/
|
|
941
|
+
apiRef?: React.MutableRefObject<GridApi<R>> | React.MutableRefObject<GridApi>;
|
|
917
942
|
|
|
918
943
|
/** Custom components to replace internal grid parts. */
|
|
919
944
|
slots?: {
|
|
@@ -1052,18 +1077,22 @@ export type GridDetailPanelHeight = number | 'auto';
|
|
|
1052
1077
|
* The Imperative API for interacting with the DataGrid.
|
|
1053
1078
|
* Access this via the `apiRef` prop or a ref passed to the component.
|
|
1054
1079
|
*/
|
|
1055
|
-
|
|
1080
|
+
/**
|
|
1081
|
+
* The imperative grid API. `R` types the row getters (`getRow`, `getAllRows`, `getVisibleRows`,
|
|
1082
|
+
* `getAllFilteredRows`); it defaults to `GridRowModel`, so `GridApi` alone keeps working.
|
|
1083
|
+
*/
|
|
1084
|
+
export interface GridApi<R extends GridValidRowModel = GridRowModel> {
|
|
1056
1085
|
/**
|
|
1057
1086
|
* Returns the row model with the given ID.
|
|
1058
1087
|
* @param id The row unique ID.
|
|
1059
1088
|
*/
|
|
1060
|
-
getRow: (id: GridRowId) =>
|
|
1089
|
+
getRow: (id: GridRowId) => R | null;
|
|
1061
1090
|
/** Returns all rows currently loaded into the grid. */
|
|
1062
|
-
getAllRows: () =>
|
|
1091
|
+
getAllRows: () => R[];
|
|
1063
1092
|
/** Returns all rows currently visible after filtering and sorting. */
|
|
1064
|
-
getVisibleRows: () =>
|
|
1093
|
+
getVisibleRows: () => R[];
|
|
1065
1094
|
/** Returns all filtered and sorted rows, ignoring pagination. Use this for full-dataset exports. */
|
|
1066
|
-
getAllFilteredRows: () =>
|
|
1095
|
+
getAllFilteredRows: () => R[];
|
|
1067
1096
|
/**
|
|
1068
1097
|
* Returns an ordered flat list of `GridGroupedExportRow` entries that mirrors the
|
|
1069
1098
|
* on-screen grouping tree: group-headers, leaf rows, per-group subtotals, and a
|
|
@@ -1120,6 +1149,16 @@ export interface GridApi {
|
|
|
1120
1149
|
/** Scrolls the grid viewport to a specific row or column index. */
|
|
1121
1150
|
scrollToIndexes: (params: { rowIndex?: number; colIndex?: number }) => void;
|
|
1122
1151
|
|
|
1152
|
+
/**
|
|
1153
|
+
* Sizes a column to fit its header and the cells rendered right now (the virtualization window),
|
|
1154
|
+
* clamped to `minWidth` (default 50px) / `maxWidth`, like double-clicking its resize handle.
|
|
1155
|
+
* Does nothing for `resizable: false` columns or a column that is not rendered.
|
|
1156
|
+
* A flex column gets a fixed width. @since v3.1
|
|
1157
|
+
*/
|
|
1158
|
+
autosizeColumn: (field: string) => void;
|
|
1159
|
+
/** `autosizeColumn` for several columns (default: every column). @since v3.1 */
|
|
1160
|
+
autosizeColumns: (fields?: readonly string[]) => void;
|
|
1161
|
+
|
|
1123
1162
|
/**
|
|
1124
1163
|
* Programmatically copies all currently selected rows to the clipboard as TSV: every selected
|
|
1125
1164
|
* row that passes the filter (other pages, collapsed groups and pinned rows included), with the
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { DEFAULT_MIN_COLUMN_WIDTH } from '../components/ColumnResizeHandle/clampResizeWidth';
|
|
2
|
+
|
|
3
|
+
/** The grid root an element belongs to, so a nested grid (in a detail panel) is never measured. */
|
|
4
|
+
const ROOT_SELECTOR = '.ogx';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The width `content` needs to show without clipping, plus the horizontal padding and borders of
|
|
8
|
+
* `cell`. `content` is laid out at `max-content` for the read and restored right after, so the
|
|
9
|
+
* reading works for a column that is too wide (shrinks) as well as one that is too narrow (grows).
|
|
10
|
+
*/
|
|
11
|
+
function measureCell(cell: HTMLElement, content: HTMLElement): number {
|
|
12
|
+
const saved = content.style.cssText;
|
|
13
|
+
content.style.width = 'max-content';
|
|
14
|
+
content.style.minWidth = '0';
|
|
15
|
+
content.style.maxWidth = 'none';
|
|
16
|
+
content.style.flex = 'none';
|
|
17
|
+
const contentWidth = content.getBoundingClientRect().width;
|
|
18
|
+
content.style.cssText = saved;
|
|
19
|
+
|
|
20
|
+
const style = getComputedStyle(cell);
|
|
21
|
+
const extra = [style.paddingLeft, style.paddingRight, style.borderLeftWidth, style.borderRightWidth]
|
|
22
|
+
.reduce((sum, value) => sum + (parseFloat(value) || 0), 0);
|
|
23
|
+
return contentWidth + extra;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The width a column needs to fit its header and the cells rendered right now (the virtualization
|
|
28
|
+
* window, pinned rows included), measured in the DOM. Cells spanning several columns and cells
|
|
29
|
+
* hidden by a row span are skipped. Returns null when nothing of the column is rendered.
|
|
30
|
+
*/
|
|
31
|
+
export function measureColumnContentWidth(root: HTMLElement, field: string): number | null {
|
|
32
|
+
const escaped = typeof CSS !== 'undefined' && CSS.escape ? CSS.escape(field) : field.replace(/"/g, '\\"');
|
|
33
|
+
const ownedByRoot = (el: Element) => el.closest(ROOT_SELECTOR) === root;
|
|
34
|
+
let width: number | null = null;
|
|
35
|
+
const take = (w: number) => { width = width === null ? w : Math.max(width, w); };
|
|
36
|
+
|
|
37
|
+
root.querySelectorAll<HTMLElement>(`[role="columnheader"][data-field="${escaped}"]`).forEach(header => {
|
|
38
|
+
if (!ownedByRoot(header)) return;
|
|
39
|
+
const content = header.querySelector<HTMLElement>('.ogx__header-cell-content');
|
|
40
|
+
if (content) take(measureCell(header, content));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
root.querySelectorAll<HTMLElement>(`[role="gridcell"][data-field="${escaped}"]`).forEach(cell => {
|
|
44
|
+
if (!ownedByRoot(cell) || cell.hasAttribute('aria-colspan')) return;
|
|
45
|
+
const content = cell.querySelector<HTMLElement>(':scope > .ogx__cell-content');
|
|
46
|
+
if (content) take(measureCell(cell, content));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return width === null ? null : Math.ceil(width);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Clamps an autosized width to the column's `minWidth` (default 50px) and `maxWidth`. */
|
|
53
|
+
export function clampAutosizeWidth(width: number, minWidth?: number, maxWidth?: number): number {
|
|
54
|
+
const min = minWidth ?? DEFAULT_MIN_COLUMN_WIDTH;
|
|
55
|
+
return Math.max(min, Math.min(maxWidth ?? Infinity, width));
|
|
56
|
+
}
|
package/lib/utils/pivot/index.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
isEmptyAggregateValue,
|
|
19
19
|
} from '../aggregation';
|
|
20
20
|
import { createRowFilter } from '../filtering';
|
|
21
|
-
import { compareValues } from '../sorting';
|
|
21
|
+
import { applySortDirection, callSortComparator, compareValues } from '../sorting';
|
|
22
22
|
import { buildColumnLookup } from '../columnLookup';
|
|
23
23
|
import { getCellValue, getFormattedValue } from '../values';
|
|
24
24
|
|
|
@@ -256,7 +256,16 @@ export function computePivot(
|
|
|
256
256
|
if (sortModel && sortModel.length > 0) {
|
|
257
257
|
kept = [...kept].sort((a, b) => {
|
|
258
258
|
for (const item of sortModel) {
|
|
259
|
-
const
|
|
259
|
+
const va = sortValue(a.row, item.field);
|
|
260
|
+
const vb = sortValue(b.row, item.field);
|
|
261
|
+
const comparator = isRowField(item.field) ? colDefMap.get(item.field)?.sortComparator : undefined;
|
|
262
|
+
const c = comparator
|
|
263
|
+
? applySortDirection(callSortComparator(
|
|
264
|
+
comparator,
|
|
265
|
+
{ id: a.row.id, field: item.field, row: a.row, value: va },
|
|
266
|
+
{ id: b.row.id, field: item.field, row: b.row, value: vb },
|
|
267
|
+
), item.sort)
|
|
268
|
+
: compareValues(va, vb, item.sort, typeOf(item.field));
|
|
260
269
|
if (c !== 0) return c;
|
|
261
270
|
}
|
|
262
271
|
return 0;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
|
-
import type { GridColDef, GridRowModel, GridSortDirection, GridSortItem } from '../../types';
|
|
2
|
+
import type { GridColDef, GridRowId, GridRowModel, GridSortCellParams, GridSortDirection, GridSortItem } from '../../types';
|
|
3
3
|
import type { GridColumnLookup } from '../columnLookup';
|
|
4
|
-
import { getCellValue, toDate, toNumber } from '../values';
|
|
4
|
+
import { getCellValue, toDate, toNumber, warnCallbackFailure } from '../values';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Language-aware string order: accented letters sort next to their base letter instead of after
|
|
@@ -68,7 +68,8 @@ function compareSortKeys(a: SortKey, b: SortKey): number {
|
|
|
68
68
|
return a.num < b.num ? -1 : a.num > b.num ? 1 : 0;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
/** Reverse an ascending comparison for 'desc'. */
|
|
72
|
+
export function applySortDirection(comparison: number, direction: 'asc' | 'desc'): number {
|
|
72
73
|
if (comparison === 0) return 0;
|
|
73
74
|
return direction === 'asc' ? comparison : -comparison;
|
|
74
75
|
}
|
|
@@ -83,27 +84,62 @@ export function compareValues(
|
|
|
83
84
|
direction: 'asc' | 'desc',
|
|
84
85
|
type?: GridColDef['type']
|
|
85
86
|
): number {
|
|
86
|
-
return
|
|
87
|
+
return applySortDirection(compareSortKeys(toSortKey(a, type), toSortKey(b, type)), direction);
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
/** A column's `sortComparator`, as the sorting utils call it. */
|
|
91
|
+
export type GridSortComparatorFn = NonNullable<GridColDef['sortComparator']>;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Call a consumer `sortComparator`, containing failures: a throw or a non-finite result reads as 0
|
|
95
|
+
* (equal), with a one-time dev warning per column, so one bad comparison cannot break the sort.
|
|
96
|
+
*/
|
|
97
|
+
export function callSortComparator(
|
|
98
|
+
comparator: GridSortComparatorFn,
|
|
99
|
+
a: GridSortCellParams,
|
|
100
|
+
b: GridSortCellParams
|
|
101
|
+
): number {
|
|
102
|
+
let result: number;
|
|
103
|
+
try {
|
|
104
|
+
result = comparator(a.value, b.value, a, b);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
warnCallbackFailure(`sortComparator:${a.field}`, `sortComparator for column "${a.field}" threw; the values are treated as equal`, error);
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
if (typeof result !== 'number' || Number.isNaN(result)) {
|
|
110
|
+
warnCallbackFailure(`sortComparator:${a.field}:nan`, `sortComparator for column "${a.field}" returned ${String(result)}; the values are treated as equal`, undefined);
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const defaultRowId = (row: GridRowModel): GridRowId => row.id;
|
|
117
|
+
|
|
89
118
|
/**
|
|
90
119
|
* Compare two rows by a sort model, reading cells through `valueGetter` and using each column's
|
|
91
|
-
* `type
|
|
120
|
+
* `type` (or its `sortComparator`). Returns 0 when every key is equal.
|
|
92
121
|
*/
|
|
93
122
|
export function compareRowsBySortModel(
|
|
94
123
|
a: GridRowModel,
|
|
95
124
|
b: GridRowModel,
|
|
96
125
|
sortModel: readonly GridSortItem[],
|
|
97
|
-
columns?: GridColumnLookup
|
|
126
|
+
columns?: GridColumnLookup,
|
|
127
|
+
getRowId: (row: GridRowModel) => GridRowId = defaultRowId
|
|
98
128
|
): number {
|
|
99
129
|
for (const sortItem of sortModel) {
|
|
100
130
|
const colDef = columns?.byField.get(sortItem.field);
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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);
|
|
107
143
|
if (comparison !== 0) return comparison;
|
|
108
144
|
}
|
|
109
145
|
return 0;
|
|
@@ -112,28 +148,42 @@ export function compareRowsBySortModel(
|
|
|
112
148
|
export function sortRows<R extends GridRowModel>(
|
|
113
149
|
rows: R[],
|
|
114
150
|
sortModel: GridSortItem[],
|
|
115
|
-
columns?: GridColumnLookup
|
|
151
|
+
columns?: GridColumnLookup,
|
|
152
|
+
getRowId: (row: R) => GridRowId = defaultRowId
|
|
116
153
|
): R[] {
|
|
117
154
|
if (sortModel.length === 0) {
|
|
118
155
|
return rows;
|
|
119
156
|
}
|
|
120
157
|
return sortItemsBySortModel(rows, sortModel, (row, sortItem) => {
|
|
121
158
|
const colDef = columns?.byField.get(sortItem.field);
|
|
122
|
-
|
|
159
|
+
const value = getCellValue(row, sortItem.field, colDef);
|
|
160
|
+
return {
|
|
161
|
+
value,
|
|
162
|
+
type: colDef?.type,
|
|
163
|
+
comparator: colDef?.sortComparator,
|
|
164
|
+
params: colDef?.sortComparator ? { id: getRowId(row), field: sortItem.field, row, value } : undefined,
|
|
165
|
+
};
|
|
123
166
|
});
|
|
124
167
|
}
|
|
125
168
|
|
|
126
|
-
/** The value an item sorts by for one sort key, and
|
|
169
|
+
/** The value an item sorts by for one sort key, and how to compare it. */
|
|
127
170
|
export interface GridSortValue {
|
|
128
171
|
value: unknown;
|
|
129
172
|
type?: GridColDef['type'];
|
|
173
|
+
/**
|
|
174
|
+
* The column's `sortComparator`. Two items are compared with it when both carry the same one
|
|
175
|
+
* (with their `params`); otherwise the built-in comparison of `value` / `type` applies.
|
|
176
|
+
*/
|
|
177
|
+
comparator?: GridSortComparatorFn;
|
|
178
|
+
/** The params passed to `comparator` for this item; `value` here is what the comparator sees. */
|
|
179
|
+
params?: GridSortCellParams;
|
|
130
180
|
}
|
|
131
181
|
|
|
132
182
|
/**
|
|
133
|
-
* Stable sort of any items by a sort model. `readValue` gives the value (and column type
|
|
134
|
-
* sorts by for each sort key; it is called once per item and key, not once per
|
|
135
|
-
* the flat pipeline and by tree data / row grouping, whose synthetic group rows
|
|
136
|
-
* grouping value or label instead of a cell.
|
|
183
|
+
* Stable sort of any items by a sort model. `readValue` gives the value (and column type or
|
|
184
|
+
* comparator) an item sorts by for each sort key; it is called once per item and key, not once per
|
|
185
|
+
* comparison. Used by the flat pipeline and by tree data / row grouping, whose synthetic group rows
|
|
186
|
+
* sort by their grouping value or label instead of a cell.
|
|
137
187
|
*/
|
|
138
188
|
export function sortItemsBySortModel<T>(
|
|
139
189
|
items: readonly T[],
|
|
@@ -146,15 +196,18 @@ export function sortItemsBySortModel<T>(
|
|
|
146
196
|
|
|
147
197
|
// Read and normalize every sort value once, instead of calling valueGetter and parsing dates
|
|
148
198
|
// O(n log n) times inside the comparator.
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
return toSortKey(value, type);
|
|
152
|
-
}));
|
|
199
|
+
const values = sortModel.map(sortItem => items.map(item => readValue(item, sortItem)));
|
|
200
|
+
const keys = values.map(column => column.map(({ value, type }) => toSortKey(value, type)));
|
|
153
201
|
|
|
154
202
|
const order = items.map((_, index) => index);
|
|
155
203
|
order.sort((ia, ib) => {
|
|
156
204
|
for (let k = 0; k < sortModel.length; k++) {
|
|
157
|
-
const
|
|
205
|
+
const a = values[k][ia];
|
|
206
|
+
const b = values[k][ib];
|
|
207
|
+
const ascending = a.comparator && a.comparator === b.comparator && a.params && b.params
|
|
208
|
+
? callSortComparator(a.comparator, a.params, b.params)
|
|
209
|
+
: compareSortKeys(keys[k][ia], keys[k][ib]);
|
|
210
|
+
const comparison = applySortDirection(ascending, sortModel[k].sort);
|
|
158
211
|
if (comparison !== 0) return comparison;
|
|
159
212
|
}
|
|
160
213
|
return ia - ib;
|