@opencorestack/opengridx 3.0.1 → 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 +14 -0
- package/README.md +3 -1
- package/dist/index.d.ts +33 -0
- package/dist/opengridx.es.js +3944 -3851
- 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 +4 -1
- package/docs/architecture/datagrid-orchestration.md +1 -0
- package/docs/components/column-resize.md +13 -1
- package/docs/features/sorting-pagination.md +26 -0
- package/docs/migration/v2-to-v3.md +1 -1
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +12 -3
- package/lib/components/DataGrid/DataGrid.tsx +3 -0
- package/lib/components/Header/Header.tsx +4 -0
- package/lib/hooks/core/gridApiPlaceholder.ts +2 -0
- package/lib/hooks/core/useGridColumnAutosize.ts +52 -0
- 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 +32 -0
- 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 +7 -2
- package/package.json +1 -1
package/docs/API_REFERENCE.md
CHANGED
|
@@ -294,6 +294,8 @@ Access these methods via the `apiRef` prop.
|
|
|
294
294
|
| `setPage(page)` | `void` | Change current page (0-indexed). Fires `onPaginationModelChange`. |
|
|
295
295
|
| `setPageSize(pageSize)` | `void` | Change the page size and go to page 0. Fires `onPaginationModelChange`. |
|
|
296
296
|
| `scrollToIndexes(params)` | `void` | Scroll to specific row/column index. Uses the current layout, also when called from a layout effect right after an update (v3.0+). |
|
|
297
|
+
| `autosizeColumn(field)` | `void` | Fit a column to its header and the cells in the current render window, clamped to `minWidth` (default 50px) / `maxWidth`; same as double-clicking its resize handle. No-op for `resizable: false` or a column that is not rendered. A flex column gets a fixed width (v3.1.0+). |
|
|
298
|
+
| `autosizeColumns(fields?)` | `void` | `autosizeColumn` for several columns; every column when `fields` is omitted (v3.1.0+). |
|
|
297
299
|
| `getAllColumns()` | `GridColDef[]` | Get all defined columns. |
|
|
298
300
|
| `getAggregationResult()` | `Record<string, unknown> \| null` | Get current aggregation results (`null` without an `aggregationModel`, and in pivot mode). Live in a parent's `useLayoutEffect` on mount and current after every update (v3.0+; before, it was installed in a passive effect). The same holds for `getAggregationModel()` and `getGroupedExportRows()`. |
|
|
299
301
|
| `getAggregationModel()` | `GridAggregationModel \| null` | Get the active aggregation configuration. |
|
|
@@ -391,8 +393,9 @@ Span values are floored; `Infinity` means "to the end"; `NaN`, `0` and negative
|
|
|
391
393
|
| Property | Type | Default | Description |
|
|
392
394
|
| :--- | :--- | :--- | :--- |
|
|
393
395
|
| `sortable` | `boolean` | `true` | Enable/disable sorting for this column. |
|
|
396
|
+
| `sortComparator` | `(v1, v2, params1, params2) => number` | — | Custom ascending comparator for client-side sorting (v3.1.0+). `v1`/`v2` are `valueGetter` values **including `null`/`undefined`**; `params` are `GridSortCellParams` `{ id, field, row, value }`. Negated for `desc`, chained in multi-sort, used for group ordering under row grouping, tree-data siblings and pivot row labels. Throw / `NaN` reads as 0 (dev warning once). Ignored with `sortingMode="server"`. |
|
|
394
397
|
| `filterable` | `boolean` | `true` | Enable/disable filtering for this column. `false` also removes it from the quick-filter search. |
|
|
395
|
-
| `resizable` | `boolean` | `true` | Allow the user to drag-resize this column. |
|
|
398
|
+
| `resizable` | `boolean` | `true` | Allow the user to drag-resize this column; double-click the handle (or Enter on it) to auto-size to content (v3.1.0+). |
|
|
396
399
|
| `hideable` | `boolean` | `true` | Allow the user to hide this column from the UI. `false` removes it from the Columns panel (unless `showNonHideableColumns`) and removes **Hide Column** from its column menu. |
|
|
397
400
|
| `pinnable` | `boolean` | `true` | Allow this column to be pinned via the UI. `false` removes the pin actions from its column menu. Does not affect drag-reordering (v3.0+). |
|
|
398
401
|
| `disableColumnMenu` | `boolean` | `false` | Hide the column header kebab/context menu. |
|
|
@@ -56,6 +56,7 @@ in an effect, so **keep the call order when you move or add a hook**. In particu
|
|
|
56
56
|
| 29 | `useGridSpanning` | `hooks/features/useGridSpanning.ts` | Row/column spans |
|
|
57
57
|
| 30 | `useGridClipboardApi` | `hooks/core/useGridClipboardApi.ts` | Ctrl/Cmd+C and `copySelectedRows` |
|
|
58
58
|
| 31 | `useGridScrollToIndexesApi` | `hooks/core/useGridScrollToIndexesApi.ts` | `scrollToIndexes` |
|
|
59
|
+
| 31a | `useGridColumnAutosize` | `hooks/core/useGridColumnAutosize.ts` | `autosizeColumn` / `autosizeColumns` on the API, and the resize handle's auto-size handler (v3.1) |
|
|
59
60
|
| 32 | `useGridViewport` | `hooks/core/useGridViewport.ts` | Scroll sync, viewport measurement, viewport callback ref |
|
|
60
61
|
| 33 | `useGridVirtualization` | `hooks/core/useGridVirtualization.ts` | Render window |
|
|
61
62
|
| 34 | `useGridSortHandlers` | `hooks/core/useGridHeaderHandlers.ts` | Header sort / shift-sort |
|
|
@@ -8,7 +8,8 @@ OpenGridX supports dynamic column resizing, allowing users to adjust the width o
|
|
|
8
8
|
- **Keyboard**: with a column header focused, **Alt+ArrowRight** / **Alt+ArrowLeft** widen / narrow the column by 10px (hold **Shift** for 50px). The handle is a `role="separator"` that reports the width in `aria-valuenow` (and `aria-valuemin` / `aria-valuemax`).
|
|
9
9
|
- **Constraints**: `minWidth` and `maxWidth` from your `GridColDef` apply. Without `minWidth` a column can be narrowed to 50px; a column that is already narrower (a 30px icon column) is never snapped up to 50px. Without `maxWidth` there is no upper limit.
|
|
10
10
|
- **Clicks are not resizes**: pressing and releasing the handle without moving it changes nothing (a flex column stays flex), and ending a resize with the pointer over the header never sorts the column.
|
|
11
|
-
- **Double-click**
|
|
11
|
+
- **Double-click to auto-size** (v3.1.0+): double-clicking the handle fits the column to its content — the header (title, sort icon, menu button) and the cells rendered right now, i.e. the rows and pinned rows in the current virtualization window, not the whole data set. The width is clamped to `minWidth` (default 50px) and `maxWidth`; a flex column gets a fixed width, like after a manual drag. The double-click never sorts. With the handle focused, **Enter** does the same. Columns with `resizable: false` have no handle and are never auto-sized.
|
|
12
|
+
- **Programmatic auto-size**: `apiRef.current.autosizeColumn(field)` and `apiRef.current.autosizeColumns(fields?)` (every column when `fields` is omitted) use the same measurement. The new width lands in `columnWidths` state like a drag, so `onStateChange` reports it; there is no dedicated resize callback.
|
|
12
13
|
|
|
13
14
|
---
|
|
14
15
|
|
|
@@ -34,6 +35,17 @@ const columns: GridColDef[] = [
|
|
|
34
35
|
];
|
|
35
36
|
```
|
|
36
37
|
|
|
38
|
+
### Auto-size to content
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
const apiRef = useGridApiRef();
|
|
42
|
+
|
|
43
|
+
<DataGrid apiRef={apiRef} rows={rows} columns={columns} />
|
|
44
|
+
<button onClick={() => apiRef.current.autosizeColumns()}>Fit all columns</button>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Measurement reads the DOM (each cell's `.ogx__cell-content` laid out at `max-content`, plus the cell's padding and borders), so only rendered rows count: scroll first, or turn off virtualization for a small grid, if off-screen rows are wider. Cells that span several columns (`colSpan`) are skipped.
|
|
48
|
+
|
|
37
49
|
---
|
|
38
50
|
|
|
39
51
|
## ⚙️ How it Works
|
|
@@ -48,6 +48,32 @@ Each active sort column shows a numbered priority badge (`1`, `2`, `3`…) next
|
|
|
48
48
|
- A column that mixes kinds of value (numbers and strings, say) is ordered by kind first — numbers, dates, booleans, strings — so the order does not depend on the input order.
|
|
49
49
|
- Ties keep the rows' original order.
|
|
50
50
|
|
|
51
|
+
### Custom comparator: `sortComparator` (v3.1.0+)
|
|
52
|
+
|
|
53
|
+
Give a column `sortComparator(v1, v2, params1, params2)` to replace the built-in comparison for that column. Return a negative number when `v1` sorts before `v2` **in ascending order**, a positive number when after, `0` when equal.
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
const SIZE_ORDER: Record<string, number> = { S: 0, M: 1, L: 2, XL: 3 };
|
|
57
|
+
|
|
58
|
+
const columns: GridColDef<Product>[] = [
|
|
59
|
+
{
|
|
60
|
+
field: 'size',
|
|
61
|
+
sortComparator: (a, b) => (SIZE_ORDER[String(a)] ?? 99) - (SIZE_ORDER[String(b)] ?? 99),
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- `v1` / `v2` are the cell values **after `valueGetter`** (the same values the built-in sort reads). `params1` / `params2` are `{ id, field, row, value }` (`GridSortCellParams`), for tie-breaking on other fields of the row.
|
|
67
|
+
- **The comparator sees every value, including `null` and `undefined`**, so you decide where empty values go. The built-in "empty values last in asc" rule does not apply to a column with a comparator. (Why: a custom order often has its own idea of "empty" — `'N/A'`, `0`, a sentinel — and hiding nulls from the comparator would make those impossible to place consistently with real nulls.)
|
|
68
|
+
- **Direction**: write the ascending order only. For `desc` the grid negates the result, so values the comparator puts last in asc (nulls, typically) come first in desc — the same as the built-in behaviour.
|
|
69
|
+
- **Multi-sort**: the comparator is one key in the chain; when it returns `0` the next sort key decides, then the original row order.
|
|
70
|
+
- **Row grouping**: sorting by the grouping column (or by the grouping/label column) orders the groups by their grouping value using the grouping column's comparator; for a group row, `params.row` is the synthetic group row. Leaf rows inside a group use the sorted column's comparator.
|
|
71
|
+
- **Tree data**: siblings are sorted with the comparator at every level. Auto-created parents (no row of their own) are compared by label with the built-in comparison.
|
|
72
|
+
- **Pivot**: sorting a pivot row-label column uses that source column's comparator on the label values.
|
|
73
|
+
- Everything that reuses the pipeline order uses it too: `apiRef.getAllFilteredRows()`, exports, clipboard.
|
|
74
|
+
- A comparator that throws, or returns `NaN`, reads as `0` (equal) with a one-time dev warning per column; the grid does not crash.
|
|
75
|
+
- **Ignored with `sortingMode="server"`**: the server sorts, the grid shows `rows` as given.
|
|
76
|
+
|
|
51
77
|
```tsx
|
|
52
78
|
// Controlled multi-sort — set programmatically or drive from UI with multiSort:
|
|
53
79
|
const [sortModel, setSortModel] = useState<GridSortItem[]>([
|
|
@@ -284,7 +284,7 @@ Multi-sort:
|
|
|
284
284
|
|
|
285
285
|
**Who is affected:** apps with snapshot tests of sorted output, or server code that expects the old client order.
|
|
286
286
|
|
|
287
|
-
**Fix it:** update the snapshots.
|
|
287
|
+
**Fix it:** update the snapshots. Since **v3.1.0** a column can take a custom `sortComparator(v1, v2, params1, params2)` (see `docs/features/sorting-pagination.md`), which is the simplest way to get a custom order. On 3.0.x there is no per-column comparator; to sort by a different key, return that key from a `valueGetter` and format it for display with `valueFormatter` (or sort on the server with `sortingMode="server"`):
|
|
288
288
|
|
|
289
289
|
```tsx
|
|
290
290
|
import type { GridColDef, GridRowModel } from '@opencorestack/opengridx';
|
|
@@ -6,6 +6,8 @@ export interface ColumnResizeHandleProps {
|
|
|
6
6
|
field: string;
|
|
7
7
|
currentWidth: number;
|
|
8
8
|
onResize: (field: string, newWidth: number) => void;
|
|
9
|
+
/** Fits the column to its content: called on double-click and on Enter. */
|
|
10
|
+
onAutosize?: (field: string) => void;
|
|
9
11
|
/** Defaults to 50 (the layout's default minimum), or the current width if that is smaller. */
|
|
10
12
|
minWidth?: number;
|
|
11
13
|
/** Defaults to no limit. */
|
|
@@ -43,7 +45,7 @@ function suppressNextClick(): void {
|
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
export function ColumnResizeHandle(props: ColumnResizeHandleProps) {
|
|
46
|
-
const { field, currentWidth, onResize, minWidth, maxWidth, edge = 'end' } = props;
|
|
48
|
+
const { field, currentWidth, onResize, onAutosize, minWidth, maxWidth, edge = 'end' } = props;
|
|
47
49
|
const [isDragging, setIsDragging] = useState(false);
|
|
48
50
|
const activeRef = useRef<ActiveResize | null>(null);
|
|
49
51
|
|
|
@@ -103,17 +105,24 @@ export function ColumnResizeHandle(props: ColumnResizeHandleProps) {
|
|
|
103
105
|
}, [field, onResize, widthAt]);
|
|
104
106
|
|
|
105
107
|
const handleKeyDown = useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
108
|
+
if (event.key === 'Enter' && onAutosize) {
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
event.stopPropagation();
|
|
111
|
+
onAutosize(field);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
106
114
|
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
|
|
107
115
|
event.preventDefault();
|
|
108
116
|
event.stopPropagation();
|
|
109
117
|
const step = (event.shiftKey ? 50 : 10) * (event.key === 'ArrowRight' ? 1 : -1);
|
|
110
118
|
onResize(field, clampResizeWidth(currentWidth + step, currentWidth, minWidth, maxWidth));
|
|
111
|
-
}, [field, currentWidth, onResize, minWidth, maxWidth]);
|
|
119
|
+
}, [field, currentWidth, onResize, onAutosize, minWidth, maxWidth]);
|
|
112
120
|
|
|
113
121
|
const handleDoubleClick = useCallback((event: React.MouseEvent) => {
|
|
114
122
|
event.preventDefault();
|
|
115
123
|
event.stopPropagation();
|
|
116
|
-
|
|
124
|
+
onAutosize?.(field);
|
|
125
|
+
}, [field, onAutosize]);
|
|
117
126
|
|
|
118
127
|
const classNames = [
|
|
119
128
|
'ogx-column-resize-handle',
|
|
@@ -51,6 +51,7 @@ import { useStableColumns } from '../../hooks/core/useStableColumns';
|
|
|
51
51
|
import { useGridAggregationApi } from '../../hooks/core/useGridAggregationApi';
|
|
52
52
|
import { useGridClipboardApi } from '../../hooks/core/useGridClipboardApi';
|
|
53
53
|
import { useGridScrollToIndexesApi } from '../../hooks/core/useGridScrollToIndexesApi';
|
|
54
|
+
import { useGridColumnAutosize } from '../../hooks/core/useGridColumnAutosize';
|
|
54
55
|
import { useGridSortHandlers, useGridColumnMenuHandlers } from '../../hooks/core/useGridHeaderHandlers';
|
|
55
56
|
import { useGridKeyboardMode, useGridPointerFocusHandlers } from '../../hooks/core/useGridFocusHandlers';
|
|
56
57
|
import { getPaginationRowCount, pickPaginationLocaleText } from '../../utils/pagination';
|
|
@@ -500,6 +501,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
500
501
|
disableKeyboardShortcut: disableClipboardCopy,
|
|
501
502
|
});
|
|
502
503
|
useGridScrollToIndexesApi<R>({ apiRef, viewportRef, layout, rowHeight: effectiveRowHeight });
|
|
504
|
+
const handleColumnAutosize = useGridColumnAutosize<R>({ apiRef, containerRef, columns: orderedColumns, onColumnResize: handleColumnResize });
|
|
503
505
|
|
|
504
506
|
const { scrollTop, scrollLeft, overscanRows, handleScroll, setViewportElement } = useGridViewport({
|
|
505
507
|
onRowsScrollEnd, overscanRowCount, rowCount: layout.unpinnedRowsLength, autoHeight, setDimensions, viewportRef,
|
|
@@ -779,6 +781,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
|
|
|
779
781
|
multiSort={multiSort}
|
|
780
782
|
|
|
781
783
|
onColumnResize={handleColumnResize}
|
|
784
|
+
onColumnAutosize={handleColumnAutosize}
|
|
782
785
|
columnWidths={columnWidths}
|
|
783
786
|
pinnedColumns={effectivePinnedColumns}
|
|
784
787
|
|
|
@@ -43,6 +43,8 @@ export interface HeaderProps<R extends GridRowModel = GridRowModel> {
|
|
|
43
43
|
onSort?: (field: string, direction: GridSortDirection) => void;
|
|
44
44
|
sortModel?: Array<{ field: string; sort: 'asc' | 'desc' }>;
|
|
45
45
|
onColumnResize?: (field: string, newWidth: number) => void;
|
|
46
|
+
/** Fits a column to its content (double-click or Enter on its resize handle). */
|
|
47
|
+
onColumnAutosize?: (field: string) => void;
|
|
46
48
|
columnWidths?: Record<string, number>;
|
|
47
49
|
pinnedColumns?: GridColumnPinning;
|
|
48
50
|
hasDetailPanel?: boolean;
|
|
@@ -81,6 +83,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
81
83
|
onSort,
|
|
82
84
|
sortModel = [],
|
|
83
85
|
onColumnResize,
|
|
86
|
+
onColumnAutosize,
|
|
84
87
|
columnWidths = {},
|
|
85
88
|
pinnedColumns,
|
|
86
89
|
hasDetailPanel = false,
|
|
@@ -525,6 +528,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
|
|
|
525
528
|
field={colDef.field}
|
|
526
529
|
currentWidth={getRenderedColumnWidth(colDef, columnWidths)}
|
|
527
530
|
onResize={onColumnResize}
|
|
531
|
+
onAutosize={onColumnAutosize}
|
|
528
532
|
minWidth={colDef.minWidth}
|
|
529
533
|
maxWidth={colDef.maxWidth}
|
|
530
534
|
edge={pinnedPosition === 'right' ? 'start' : 'end'}
|
|
@@ -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
|
+
}
|
|
@@ -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. */
|
|
@@ -1127,6 +1149,16 @@ export interface GridApi<R extends GridValidRowModel = GridRowModel> {
|
|
|
1127
1149
|
/** Scrolls the grid viewport to a specific row or column index. */
|
|
1128
1150
|
scrollToIndexes: (params: { rowIndex?: number; colIndex?: number }) => void;
|
|
1129
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
|
+
|
|
1130
1162
|
/**
|
|
1131
1163
|
* Programmatically copies all currently selected rows to the clipboard as TSV: every selected
|
|
1132
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;
|