@opencorestack/opengridx 2.1.0 → 3.0.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 +399 -0
- package/README.md +95 -72
- package/dist/index.d.ts +590 -131
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +7891 -5631
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +17 -23
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +348 -166
- package/docs/README.md +6 -1
- package/docs/architecture/datagrid-orchestration.md +81 -0
- package/docs/architecture/grid-row-meta.md +102 -0
- package/docs/architecture/use-grid-columns.md +128 -0
- package/docs/architecture/use-grid-controlled-state.md +157 -0
- package/docs/architecture/use-grid-row-pipeline.md +120 -0
- package/docs/architecture/use-grid-scroll-sync.md +111 -0
- package/docs/architecture/use-grid-state-snapshot.md +90 -0
- package/docs/architecture/use-grid-virtualization.md +115 -0
- package/docs/architecture/use-grid-visible-rows.md +85 -0
- package/docs/assets/banner-alt.png +0 -0
- package/docs/assets/banner.png +0 -0
- package/docs/assets/hero.png +0 -0
- package/docs/assets/logo.png +0 -0
- package/docs/components/aggregation-footer.md +15 -10
- package/docs/components/cell.md +16 -6
- package/docs/components/column-group-header.md +55 -28
- package/docs/components/column-resize.md +17 -13
- package/docs/components/column-visibility.md +6 -7
- package/docs/components/datagrid.md +15 -13
- package/docs/components/empty-state.md +2 -1
- package/docs/components/error-overlay.md +12 -7
- package/docs/components/filter-panel.md +28 -8
- package/docs/components/header.md +28 -9
- package/docs/components/pagination.md +14 -8
- package/docs/components/row.md +21 -5
- package/docs/components/toolbar.md +27 -9
- package/docs/components/tooltip.md +16 -7
- package/docs/customization/slots-api.md +90 -38
- package/docs/customization/theming.md +66 -18
- package/docs/features/aggregation-pivot.md +75 -9
- package/docs/features/cell-spanning.md +80 -125
- package/docs/features/clipboard.md +41 -16
- package/docs/features/custom-pagination.md +38 -18
- package/docs/features/data-source.md +36 -13
- package/docs/features/editing-reordering.md +125 -16
- package/docs/features/export-guide.md +126 -39
- package/docs/features/filtering.md +44 -6
- package/docs/features/infinite-scroll.md +54 -46
- package/docs/features/keyboard-navigation.md +83 -0
- package/docs/features/list-view.md +58 -5
- package/docs/features/loading-states.md +18 -20
- package/docs/features/master-detail.md +18 -5
- package/docs/features/pdf-export.md +72 -19
- package/docs/features/pinning.md +24 -5
- package/docs/features/selection.md +50 -10
- package/docs/features/sorting-pagination.md +64 -14
- package/docs/features/state-persistence.md +78 -10
- package/docs/features/toolbar-customization.md +26 -17
- package/docs/features/tree-data-grouping.md +36 -8
- package/docs/features/virtualization.md +37 -14
- package/docs/migration/v1-to-v2.md +115 -0
- package/docs/migration/v2-to-v3.md +1233 -0
- package/docs/performance.md +124 -0
- package/docs/roadmap.md +5 -5
- package/lib/components/Cell/Cell.css +17 -1
- package/lib/components/Cell/Cell.tsx +129 -57
- package/lib/components/Cell/CellErrorBoundary.tsx +12 -6
- package/lib/components/Cell/GridEditInputCell.tsx +114 -41
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +7 -1
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +15 -2
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +104 -38
- package/lib/components/ColumnResizeHandle/clampResizeWidth.ts +13 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +6 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +21 -13
- package/lib/components/DataGrid/DataGrid.css +85 -12
- package/lib/components/DataGrid/DataGrid.tsx +582 -876
- package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
- package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
- package/lib/components/DataGrid/GridListView.tsx +165 -24
- package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
- package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
- package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
- package/lib/components/DataGrid/GridPinnedRows.tsx +62 -11
- package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
- package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
- package/lib/components/DataGrid/GridVirtualRows.tsx +40 -24
- package/lib/components/DetailPanel/DetailPanel.css +0 -6
- package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
- package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
- package/lib/components/FilterPanel/FilterPanel.css +13 -2
- package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
- package/lib/components/Header/ColumnMenu.tsx +91 -39
- package/lib/components/Header/Header.css +6 -3
- package/lib/components/Header/Header.tsx +181 -160
- package/lib/components/ListView/ListView.css +25 -0
- package/lib/components/ListView/ListViewRow.tsx +76 -10
- package/lib/components/Pagination/Pagination.tsx +15 -7
- package/lib/components/Row/Row.tsx +177 -77
- package/lib/components/Toolbar/GlobalSearch.css +6 -3
- package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
- package/lib/components/Toolbar/GridToolbar.css +12 -11
- package/lib/components/Toolbar/GridToolbar.tsx +133 -89
- package/lib/components/Toolbar/PivotPanel.tsx +89 -49
- package/lib/components/Tooltip/Tooltip.css +20 -0
- package/lib/components/Tooltip/Tooltip.tsx +68 -35
- package/lib/components/ui/Button.tsx +3 -0
- package/lib/components/ui/Checkbox.tsx +2 -1
- package/lib/components/ui/ExpandIcon.css +0 -9
- package/lib/components/ui/ExpandIcon.tsx +4 -1
- package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
- package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
- package/lib/hooks/core/useDataGrid.ts +115 -173
- package/lib/hooks/core/useGridAggregationApi.ts +72 -0
- package/lib/hooks/core/useGridApiMethods.ts +115 -0
- package/lib/hooks/core/useGridApiRef.ts +19 -4
- package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
- package/lib/hooks/core/useGridAriaRows.ts +38 -0
- package/lib/hooks/core/useGridClipboardApi.ts +38 -0
- package/lib/hooks/core/useGridColumnLookup.ts +30 -0
- package/lib/hooks/core/useGridColumns.tsx +279 -76
- package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
- package/lib/hooks/core/useGridControlledState.ts +99 -6
- package/lib/hooks/core/useGridDetailPanel.ts +51 -0
- package/lib/hooks/core/useGridDevWarnings.ts +104 -5
- package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
- package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
- package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
- package/lib/hooks/core/useGridHierarchy.ts +81 -0
- package/lib/hooks/core/useGridKeyboardNavigation.ts +447 -243
- package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
- package/lib/hooks/core/useGridPageCorrection.ts +55 -0
- package/lib/hooks/core/useGridRowIdOf.ts +32 -0
- package/lib/hooks/core/useGridRowInteractions.ts +81 -0
- package/lib/hooks/core/useGridRowPipeline.ts +69 -39
- package/lib/hooks/core/useGridRowSelection.ts +91 -0
- package/lib/hooks/core/useGridScrollSync.ts +80 -32
- package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
- package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
- package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
- package/lib/hooks/core/useGridToolbarProps.ts +97 -0
- package/lib/hooks/core/useGridViewport.ts +38 -0
- package/lib/hooks/core/useGridViewportSize.ts +26 -0
- package/lib/hooks/core/useGridVisibleRows.ts +17 -5
- package/lib/hooks/core/useLayout.ts +215 -195
- package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
- package/lib/hooks/core/useStableColumns.ts +34 -0
- package/lib/hooks/features/useAggregation.ts +82 -51
- package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
- package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
- package/lib/hooks/features/useGridClipboard.ts +133 -73
- package/lib/hooks/features/useGridDataSource.ts +354 -128
- package/lib/hooks/features/useGridEditing.ts +211 -51
- package/lib/hooks/features/useGridPivot.ts +92 -0
- package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
- package/lib/hooks/features/useGridSpanning.ts +410 -306
- package/lib/hooks/features/usePivot.ts +0 -0
- package/lib/hooks/features/useServerTreeChildren.ts +50 -0
- package/lib/hooks/useColumnReorder.ts +38 -44
- package/lib/hooks/useRowGrouping.ts +298 -160
- package/lib/hooks/useRowReorder.ts +62 -31
- package/lib/hooks/useTreeData.ts +287 -220
- package/lib/index.ts +28 -2
- package/lib/state/index.ts +1 -0
- package/lib/state/types.ts +7 -1
- package/lib/state/useGridStateStorage.ts +88 -29
- package/lib/theme/DataGridThemeProvider.tsx +143 -38
- package/lib/theme/gridThemeContext.ts +5 -0
- package/lib/theme/index.ts +5 -0
- package/lib/theme/presets.ts +5 -4
- package/lib/theme/types.ts +40 -2
- package/lib/types/index.ts +313 -116
- package/lib/utils/aggregation/index.ts +119 -24
- package/lib/utils/aria/index.ts +40 -0
- package/lib/utils/columnGroups.ts +110 -0
- package/lib/utils/columnLookup.ts +33 -0
- package/lib/utils/columnWidth.ts +22 -0
- package/lib/utils/dataSource/index.ts +46 -0
- package/lib/utils/detailPanel.ts +66 -0
- package/lib/utils/dragData.ts +44 -0
- package/lib/utils/editing/index.ts +30 -0
- package/lib/utils/export/exportShared.ts +197 -0
- package/lib/utils/export/exportToExcelAdvanced.ts +278 -139
- package/lib/utils/export/exportToPdf.ts +143 -92
- package/lib/utils/export/groupLabel.ts +14 -0
- package/lib/utils/export/index.ts +278 -272
- package/lib/utils/export/pdfText.ts +50 -0
- package/lib/utils/filtering/index.ts +277 -148
- package/lib/utils/focus/index.ts +115 -0
- package/lib/utils/gridApiRows.ts +35 -0
- package/lib/utils/gridModes.ts +77 -0
- package/lib/utils/grouping/groupedExportRows.ts +91 -0
- package/lib/utils/pagination/index.ts +56 -0
- package/lib/utils/pinning/index.ts +77 -33
- package/lib/utils/pivot/index.ts +368 -0
- package/lib/utils/scroll/index.ts +55 -5
- package/lib/utils/scroll/scrollEnd.ts +18 -0
- package/lib/utils/sorting/index.ts +154 -30
- package/lib/utils/syntheticRows.ts +12 -0
- package/lib/utils/values.ts +206 -0
- package/lib/utils/viewport.ts +14 -0
- package/llms.txt +261 -164
- package/package.json +13 -5
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
- package/lib/components/DataGrid/DataGrid.defects.test.tsx +0 -103
- package/lib/components/DataGrid/DataGrid.regression.test.tsx +0 -132
- package/lib/components/DataGrid/DataGrid.smoke.test.tsx +0 -58
- package/lib/components/DataGrid/DataGrid.v2.test.tsx +0 -127
- package/lib/components/DataGrid/DataGrid.virtualization.browser.test.tsx +0 -183
- package/lib/components/Header/Header.test.tsx +0 -135
- package/lib/components/Pagination/Pagination.test.tsx +0 -64
- package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
- package/lib/hooks/core/useGridControlledState.test.ts +0 -86
- package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
- package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
- package/lib/hooks/features/useAggregation.test.ts +0 -175
- package/lib/hooks/features/usePivot.test.ts +0 -202
- package/lib/hooks/useRowGrouping.test.ts +0 -298
- package/lib/hooks/useTreeData.test.ts +0 -83
- package/lib/utils/aggregation/aggregation.test.ts +0 -16
- package/lib/utils/export/exportToExcelAdvanced.test.ts +0 -109
- package/lib/utils/export/exportToPdf.test.ts +0 -120
- package/lib/utils/export/groupedExport.test.ts +0 -144
- package/lib/utils/filtering/filtering.test.ts +0 -178
- package/lib/utils/sorting/sorting.test.ts +0 -88
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { GridRowId } from '../../types';
|
|
2
|
+
|
|
3
|
+
/** Synthetic navigation columns rendered before the data columns, in render order. */
|
|
4
|
+
export const REORDER_FIELD = '__reorder_col__';
|
|
5
|
+
export const EXPAND_FIELD = '__expand_col__';
|
|
6
|
+
export const CHECKBOX_FIELD = '__checkbox_col__';
|
|
7
|
+
|
|
8
|
+
export const isSystemField = (field: string): boolean =>
|
|
9
|
+
field === REORDER_FIELD || field === EXPAND_FIELD || field === CHECKBOX_FIELD;
|
|
10
|
+
|
|
11
|
+
/** The grid's logical focus position. */
|
|
12
|
+
export interface FocusedCell {
|
|
13
|
+
/** Row id of the focused body cell, or `null` when a column header cell is focused. */
|
|
14
|
+
id: GridRowId | null;
|
|
15
|
+
field: string;
|
|
16
|
+
/** Index in the renderable rows when focus was set; used to pick a neighbour if the row disappears. */
|
|
17
|
+
rowIndex?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const isSameCell = (a: FocusedCell | null, b: FocusedCell | null): boolean =>
|
|
21
|
+
a === b || (a !== null && b !== null && a.id === b.id && a.field === b.field);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Where a keydown came from, from the grid's point of view:
|
|
25
|
+
* - `grid`: the viewport or a cell / header cell itself — the grid handles the key;
|
|
26
|
+
* - `control`: a button, link or checkbox inside a cell — navigation keys move grid focus, but
|
|
27
|
+
* Enter and Space are left to the control;
|
|
28
|
+
* - `editor`: inside the cell being edited;
|
|
29
|
+
* - `foreign`: anything the grid must leave alone — text inputs rendered in cells, detail-panel
|
|
30
|
+
* content, portals such as the column menu.
|
|
31
|
+
*/
|
|
32
|
+
export type GridKeyTarget = 'grid' | 'control' | 'editor' | 'foreign';
|
|
33
|
+
|
|
34
|
+
const NON_TEXT_INPUT_TYPES = new Set(['checkbox', 'radio', 'button', 'submit', 'reset', 'image', 'file', 'color']);
|
|
35
|
+
const TEXT_ROLES = new Set(['textbox', 'searchbox', 'combobox', 'listbox', 'slider', 'spinbutton']);
|
|
36
|
+
|
|
37
|
+
/** True for elements that use typing or arrow keys themselves. */
|
|
38
|
+
export function isTextEntryElement(el: Element): boolean {
|
|
39
|
+
const tag = el.tagName;
|
|
40
|
+
if (tag === 'INPUT') return !NON_TEXT_INPUT_TYPES.has((el as HTMLInputElement).type);
|
|
41
|
+
if (tag === 'TEXTAREA' || tag === 'SELECT') return true;
|
|
42
|
+
if ((el as HTMLElement).isContentEditable || el.closest('[contenteditable]:not([contenteditable="false"])')) return true;
|
|
43
|
+
const role = el.getAttribute('role');
|
|
44
|
+
return role !== null && TEXT_ROLES.has(role);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const isElement = (target: EventTarget | null): target is Element =>
|
|
48
|
+
target !== null && (target as Node).nodeType === 1;
|
|
49
|
+
|
|
50
|
+
/** The cell or header cell of `viewport` (not of a grid nested inside it) that contains `el`, if any. */
|
|
51
|
+
function owningCell(el: Element, viewport: HTMLElement): HTMLElement | null {
|
|
52
|
+
const panel = el.closest('.ogx__detail-panel');
|
|
53
|
+
if (panel && viewport.contains(panel)) return null;
|
|
54
|
+
const cell = el.closest<HTMLElement>('[role="gridcell"], [role="columnheader"]');
|
|
55
|
+
if (!cell || cell.closest('.ogx__viewport') !== viewport) return null;
|
|
56
|
+
return cell;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function classifyKeyTarget(target: EventTarget | null, viewport: HTMLElement): GridKeyTarget {
|
|
60
|
+
if (target === viewport) return 'grid';
|
|
61
|
+
if (!isElement(target) || !viewport.contains(target)) return 'foreign';
|
|
62
|
+
const cell = owningCell(target, viewport);
|
|
63
|
+
if (!cell) return 'foreign';
|
|
64
|
+
if (cell.classList.contains('ogx__cell--editing')) return 'editor';
|
|
65
|
+
if (cell === target) return 'grid';
|
|
66
|
+
if (isTextEntryElement(target)) return 'foreign';
|
|
67
|
+
// The select-all checkbox input is the header cell's own focus target.
|
|
68
|
+
if (cell.classList.contains('ogx__header-cell--checkbox')) return 'grid';
|
|
69
|
+
return 'control';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Resolves the cell that holds `target` to a focus position, or `null` when it is not in a cell. */
|
|
73
|
+
export function focusedCellFromElement<T>(
|
|
74
|
+
target: EventTarget | null,
|
|
75
|
+
viewport: HTMLElement,
|
|
76
|
+
rows: ReadonlyArray<T>,
|
|
77
|
+
getRowId: (row: T) => GridRowId
|
|
78
|
+
): FocusedCell | null {
|
|
79
|
+
if (!isElement(target)) return null;
|
|
80
|
+
const cell = owningCell(target, viewport);
|
|
81
|
+
const field = cell?.dataset.field;
|
|
82
|
+
if (!cell || !field) return null;
|
|
83
|
+
if (cell.getAttribute('role') === 'columnheader') {
|
|
84
|
+
return cell.parentElement?.classList.contains('ogx__header') ? { id: null, field } : null;
|
|
85
|
+
}
|
|
86
|
+
const rowIndex = Number(cell.parentElement?.getAttribute('data-rowindex'));
|
|
87
|
+
const row = Number.isInteger(rowIndex) ? rows[rowIndex] : undefined;
|
|
88
|
+
return row ? { id: getRowId(row), field, rowIndex } : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function childWithField(parent: Element | null, field: string): HTMLElement | null {
|
|
92
|
+
if (!parent) return null;
|
|
93
|
+
for (const child of Array.from(parent.children)) {
|
|
94
|
+
if ((child as HTMLElement).dataset.field === field) return child as HTMLElement;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The DOM element that should hold focus for `cell`, or `null` while it is not rendered. */
|
|
100
|
+
export function findFocusTarget(viewport: HTMLElement, cell: FocusedCell, rowIndex: number): HTMLElement | null {
|
|
101
|
+
if (cell.id === null) {
|
|
102
|
+
// The header sits in the sticky top block together with the top-pinned rows.
|
|
103
|
+
const header = viewport.querySelector(':scope > .ogx__content > .ogx__sticky-top > .ogx__header-wrap > .ogx__header');
|
|
104
|
+
const el = childWithField(header, cell.field);
|
|
105
|
+
if (el && cell.field === CHECKBOX_FIELD) return el.querySelector('input') ?? el;
|
|
106
|
+
return el;
|
|
107
|
+
}
|
|
108
|
+
if (rowIndex < 0) return null;
|
|
109
|
+
const row = viewport.querySelector(
|
|
110
|
+
`:scope > .ogx__content > .ogx__virtual-container > .ogx__rows > [data-rowindex="${rowIndex}"],` +
|
|
111
|
+
`:scope > .ogx__content > .ogx__sticky-top > .ogx__pinned-rows > [data-rowindex="${rowIndex}"],` +
|
|
112
|
+
`:scope > .ogx__content > .ogx__sticky-bottom > .ogx__pinned-rows > [data-rowindex="${rowIndex}"]`
|
|
113
|
+
);
|
|
114
|
+
return childWithField(row, cell.field);
|
|
115
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { GridRowId, GridRowMeta, GridRowModel } from '../types';
|
|
2
|
+
|
|
3
|
+
export interface CollectAllFilteredRowsParams<R extends GridRowModel> {
|
|
4
|
+
/** The active tree-data / row-grouping handlers, or null when the rows are flat. */
|
|
5
|
+
hierarchyHandlers: { getVisibleRows: (options?: { expandAll?: boolean; labelField?: string }) => R[] | null } | null;
|
|
6
|
+
/** The column that shows the hierarchy, so sorting by it orders the rows as on screen. */
|
|
7
|
+
hierarchyField?: string;
|
|
8
|
+
pinnedTopRows: R[];
|
|
9
|
+
/** Every unpinned row that passes the filter, sorted, across all pages. */
|
|
10
|
+
sortedUnpinnedRows: R[];
|
|
11
|
+
pinnedBottomRows: R[];
|
|
12
|
+
rowMetaMap: Map<GridRowId, GridRowMeta>;
|
|
13
|
+
getRowId: (row: GridRowModel) => GridRowId;
|
|
14
|
+
/**
|
|
15
|
+
* With a hierarchy: the rows that pass the filter (the set select-all and the footer use).
|
|
16
|
+
* Tree-data ancestors shown only to give a match its context are left out. Omitted: every
|
|
17
|
+
* non-synthetic row of the expanded hierarchy is returned.
|
|
18
|
+
*/
|
|
19
|
+
dataRows?: readonly R[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* `GridApi.getAllFilteredRows()`: every data row that passes the filter, in display order,
|
|
24
|
+
* ignoring pagination and hierarchy expansion. Flat rows keep their pinned rows; with a
|
|
25
|
+
* hierarchy every group is expanded, and synthetic group rows and tree-data ancestors that do
|
|
26
|
+
* not match the filter themselves (shown only for context) are left out, so the result is the
|
|
27
|
+
* set select-all and the aggregation footer act on.
|
|
28
|
+
*/
|
|
29
|
+
export function collectAllFilteredRows<R extends GridRowModel>(params: CollectAllFilteredRowsParams<R>): R[] {
|
|
30
|
+
const { hierarchyHandlers, pinnedTopRows, sortedUnpinnedRows, pinnedBottomRows, rowMetaMap, getRowId, hierarchyField, dataRows } = params;
|
|
31
|
+
const hierarchyRows = hierarchyHandlers?.getVisibleRows({ expandAll: true, labelField: hierarchyField });
|
|
32
|
+
if (!hierarchyRows) return [...pinnedTopRows, ...sortedUnpinnedRows, ...pinnedBottomRows];
|
|
33
|
+
const matching = dataRows ? new Set<R>(dataRows) : null;
|
|
34
|
+
return hierarchyRows.filter(row => !rowMetaMap.get(getRowId(row))?.isGroupRow && (!matching || matching.has(row)));
|
|
35
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { GridRowGroupingModel } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stable empty grouping model: used when `rowGroupingModel` is not passed and while pivot mode
|
|
5
|
+
* is active. It must keep its identity across renders, because the hierarchy hooks memoize on it.
|
|
6
|
+
*/
|
|
7
|
+
export const EMPTY_ROW_GROUPING_MODEL: GridRowGroupingModel = [];
|
|
8
|
+
|
|
9
|
+
export interface ResolveGridModesParams {
|
|
10
|
+
/** `rowGroupingModel` prop (or `EMPTY_ROW_GROUPING_MODEL`). */
|
|
11
|
+
rowGroupingModel: GridRowGroupingModel;
|
|
12
|
+
/** `treeData` prop. */
|
|
13
|
+
treeData: boolean;
|
|
14
|
+
/** Whether `getTreeDataPath` was passed. */
|
|
15
|
+
hasTreeDataPath: boolean;
|
|
16
|
+
/** Whether the pivot is producing the rows (`useGridPivot().isActive`). */
|
|
17
|
+
isPivotActive: boolean;
|
|
18
|
+
/** `rowReordering` prop. */
|
|
19
|
+
rowReordering: boolean;
|
|
20
|
+
/** Whether `groupingColDef` was passed. */
|
|
21
|
+
hasGroupingColDef: boolean;
|
|
22
|
+
/** `pagination` prop. */
|
|
23
|
+
pagination: boolean;
|
|
24
|
+
paginationMode: 'client' | 'server' | 'infinite';
|
|
25
|
+
/** `listView` prop. */
|
|
26
|
+
listView: boolean;
|
|
27
|
+
/** Whether `listViewColumn` was passed. */
|
|
28
|
+
hasListViewColumn: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface GridModes {
|
|
32
|
+
/** Row drag-reorder is on (never over generated pivot rows). */
|
|
33
|
+
rowReordering: boolean;
|
|
34
|
+
/** The grouping model the hierarchy hooks see: empty while pivoting. */
|
|
35
|
+
hierarchyRowGroupingModel: GridRowGroupingModel;
|
|
36
|
+
/** `treeData` asked for and not overridden by the pivot. */
|
|
37
|
+
isTreeDataRequested: boolean;
|
|
38
|
+
/** Tree data is built: requested and `getTreeDataPath` is present. */
|
|
39
|
+
isTreeData: boolean;
|
|
40
|
+
isRowGrouping: boolean;
|
|
41
|
+
isHierarchyEnabled: boolean;
|
|
42
|
+
/** The synthetic `__group__` column is shown. */
|
|
43
|
+
hasGroupingColumn: boolean;
|
|
44
|
+
/** Pagination is on: requested, not grouping, and not infinite scroll. */
|
|
45
|
+
pagination: boolean;
|
|
46
|
+
/** List view is shown: requested and `listViewColumn` is present. */
|
|
47
|
+
listView: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Which grid features are in effect, from the props that request them. Features override each
|
|
52
|
+
* other here and nowhere else:
|
|
53
|
+
* - pivot rows are generated, so row-reorder indices over them would mean nothing to the consumer;
|
|
54
|
+
* - pivot rows are already grouped by the pivot row fields, so tree data and row grouping would regroup them;
|
|
55
|
+
* - without `getTreeDataPath` there is no tree to build: the rows are shown flat (useTreeData warns);
|
|
56
|
+
* - infinite scroll loads rows as the user scrolls: it has no pages to show or slice;
|
|
57
|
+
* - list view needs a `listViewColumn` to render its items; without one the grid view is shown
|
|
58
|
+
* (useGridDevWarnings says so) instead of an empty container.
|
|
59
|
+
*/
|
|
60
|
+
export function resolveGridModes(params: ResolveGridModesParams): GridModes {
|
|
61
|
+
const { isPivotActive } = params;
|
|
62
|
+
const hierarchyRowGroupingModel = isPivotActive ? EMPTY_ROW_GROUPING_MODEL : params.rowGroupingModel;
|
|
63
|
+
const isTreeDataRequested = params.treeData && !isPivotActive;
|
|
64
|
+
const isTreeData = isTreeDataRequested && params.hasTreeDataPath;
|
|
65
|
+
const isRowGrouping = hierarchyRowGroupingModel.length > 0;
|
|
66
|
+
return {
|
|
67
|
+
rowReordering: params.rowReordering && !isPivotActive,
|
|
68
|
+
hierarchyRowGroupingModel,
|
|
69
|
+
isTreeDataRequested,
|
|
70
|
+
isTreeData,
|
|
71
|
+
isRowGrouping,
|
|
72
|
+
isHierarchyEnabled: isTreeData || isRowGrouping,
|
|
73
|
+
hasGroupingColumn: params.hasGroupingColDef && (isRowGrouping || isTreeData),
|
|
74
|
+
pagination: params.pagination && !isRowGrouping && params.paginationMode !== 'infinite',
|
|
75
|
+
listView: params.listView && params.hasListViewColumn,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
GridAggregationModel,
|
|
3
|
+
GridAggregationResult,
|
|
4
|
+
GridColDef,
|
|
5
|
+
GridGroupedExportRow,
|
|
6
|
+
GridRowId,
|
|
7
|
+
GridRowMeta,
|
|
8
|
+
GridRowModel,
|
|
9
|
+
} from '../../types';
|
|
10
|
+
import { computeAggregations } from '../aggregation';
|
|
11
|
+
|
|
12
|
+
export interface BuildGroupedExportRowsParams<R extends GridRowModel> {
|
|
13
|
+
/**
|
|
14
|
+
* The grouped rows in display order with every group expanded: group rows followed by
|
|
15
|
+
* their filtered, sorted descendants (useRowGrouping's getVisibleRows({ expandAll: true })).
|
|
16
|
+
*/
|
|
17
|
+
rows: R[];
|
|
18
|
+
getRowId: (row: R) => GridRowId;
|
|
19
|
+
rowMetaMap: Map<GridRowId, GridRowMeta>;
|
|
20
|
+
columns: GridColDef<R>[];
|
|
21
|
+
aggregationModel: GridAggregationModel;
|
|
22
|
+
/** The footer's grand total (already over the filtered data rows); omitted when null. */
|
|
23
|
+
aggregationResult: GridAggregationResult | null;
|
|
24
|
+
/** Groups whose subtotal is hidden (getAggregationPosition returned null for them); no subtotal is written. */
|
|
25
|
+
isSubtotalHidden?: (groupId: GridRowId) => boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface OpenGroup<R> {
|
|
29
|
+
id: GridRowId;
|
|
30
|
+
header: GridGroupedExportRow;
|
|
31
|
+
leaves: R[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Flattens the grouping tree for export: group-header, its leaves and nested groups, then
|
|
36
|
+
* its subtotal, and a grand total at the end. The collapsed/expanded state on screen does
|
|
37
|
+
* not matter, and subtotals are computed over the group's filtered leaves (the rows the
|
|
38
|
+
* export contains), so every subtotal agrees with the rows above it.
|
|
39
|
+
*/
|
|
40
|
+
export function buildGroupedExportRows<R extends GridRowModel>(params: BuildGroupedExportRowsParams<R>): GridGroupedExportRow[] {
|
|
41
|
+
const { rows, getRowId, rowMetaMap, columns, aggregationModel, aggregationResult, isSubtotalHidden } = params;
|
|
42
|
+
const hasAggregation = Object.keys(aggregationModel).length > 0;
|
|
43
|
+
const columnsLookup = new Map(columns.map(col => [col.field, col]));
|
|
44
|
+
|
|
45
|
+
const result: GridGroupedExportRow[] = [];
|
|
46
|
+
const open: OpenGroup<R>[] = [];
|
|
47
|
+
|
|
48
|
+
const closeGroupsFrom = (depth: number) => {
|
|
49
|
+
while (open.length > 0 && (open[open.length - 1].header.depth >= depth)) {
|
|
50
|
+
const group = open.pop()!;
|
|
51
|
+
if (hasAggregation && !isSubtotalHidden?.(group.id)) {
|
|
52
|
+
result.push({
|
|
53
|
+
type: 'group-subtotal',
|
|
54
|
+
depth: group.header.depth,
|
|
55
|
+
groupField: group.header.groupField,
|
|
56
|
+
groupValue: group.header.groupValue,
|
|
57
|
+
groupLabel: group.header.groupLabel,
|
|
58
|
+
aggregatedValues: computeAggregations(group.leaves, aggregationModel, columnsLookup, 'getGroupedExportRows'),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
for (const row of rows) {
|
|
65
|
+
const id = getRowId(row);
|
|
66
|
+
const meta = rowMetaMap.get(id);
|
|
67
|
+
if (meta?.isGroupFooter) continue;
|
|
68
|
+
if (meta?.isGroupRow) {
|
|
69
|
+
const depth = meta.treeDepth ?? 0;
|
|
70
|
+
closeGroupsFrom(depth);
|
|
71
|
+
const header: GridGroupedExportRow = {
|
|
72
|
+
type: 'group-header',
|
|
73
|
+
depth,
|
|
74
|
+
groupField: meta.groupingField,
|
|
75
|
+
groupValue: meta.groupingValue,
|
|
76
|
+
groupLabel: meta.groupLabel,
|
|
77
|
+
};
|
|
78
|
+
result.push(header);
|
|
79
|
+
open.push({ id, header, leaves: [] });
|
|
80
|
+
} else {
|
|
81
|
+
result.push({ type: 'leaf', depth: meta?.treeDepth ?? open.length, row });
|
|
82
|
+
open.forEach(group => group.leaves.push(row));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
closeGroupsFrom(0);
|
|
86
|
+
|
|
87
|
+
if (hasAggregation && aggregationResult) {
|
|
88
|
+
result.push({ type: 'grand-total', depth: 0, aggregatedValues: aggregationResult });
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { GridLocaleText } from '../../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Page-size and page-index arithmetic shared by the row pipeline, the pager and the list view,
|
|
5
|
+
* so all three agree on which page is shown.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** A usable page size: a whole number of at least 1. A size of 0, a negative size or NaN would divide by zero. */
|
|
9
|
+
export function normalizePageSize(pageSize: number): number {
|
|
10
|
+
return pageSize >= 1 ? Math.floor(pageSize) : 1;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The number of pages needed for `rowCount` rows; at least 1, so an empty grid still has page 1. */
|
|
14
|
+
export function getPageCount(rowCount: number, pageSize: number): number {
|
|
15
|
+
return Math.max(1, Math.ceil(Math.max(0, rowCount) / normalizePageSize(pageSize)));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface GetPaginationRowCountParams {
|
|
19
|
+
paginationMode: 'client' | 'server' | 'infinite';
|
|
20
|
+
/** Whether a `dataSource` is set: its response's `rowCount` then wins over the prop. */
|
|
21
|
+
hasDataSource: boolean;
|
|
22
|
+
/** The server total from the last `dataSource` response, if any. */
|
|
23
|
+
dataSourceRowCount: number | null | undefined;
|
|
24
|
+
/** The `rowCount` prop. */
|
|
25
|
+
rowCountProp: number | undefined;
|
|
26
|
+
/** The filtered, unpinned rows the grid holds. */
|
|
27
|
+
clientRowCount: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* What the pager pages through. Client: the filtered, unpinned rows (pinned rows show on every
|
|
32
|
+
* page). Server: the server total, from the dataSource response or the rowCount prop, falling
|
|
33
|
+
* back to the rows the grid holds.
|
|
34
|
+
*/
|
|
35
|
+
export function getPaginationRowCount(params: GetPaginationRowCountParams): number {
|
|
36
|
+
const { paginationMode, hasDataSource, dataSourceRowCount, rowCountProp, clientRowCount } = params;
|
|
37
|
+
if (paginationMode !== 'server') return clientRowCount;
|
|
38
|
+
return (hasDataSource ? dataSourceRowCount ?? rowCountProp : rowCountProp) ?? clientRowCount;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The pager's strings out of the grid's `localeText` (undefined when there is none). */
|
|
42
|
+
export function pickPaginationLocaleText(
|
|
43
|
+
localeText: GridLocaleText | undefined
|
|
44
|
+
): Pick<GridLocaleText, 'paginationRowsPerPage' | 'paginationOf' | 'paginationPage'> | undefined {
|
|
45
|
+
return localeText ? {
|
|
46
|
+
paginationRowsPerPage: localeText.paginationRowsPerPage,
|
|
47
|
+
paginationOf: localeText.paginationOf,
|
|
48
|
+
paginationPage: localeText.paginationPage,
|
|
49
|
+
} : undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** `page` limited to the valid range `0 … pageCount - 1`. */
|
|
53
|
+
export function clampPage(page: number, pageCount: number): number {
|
|
54
|
+
if (!Number.isFinite(page)) return 0;
|
|
55
|
+
return Math.min(Math.max(0, Math.floor(page)), Math.max(0, pageCount - 1));
|
|
56
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
|
|
2
2
|
import type { GridColDef, GridRowModel, GridColumnPinning, GridRowPinning, GridPinnedPosition, GridRowId } from '../../types';
|
|
3
|
+
import { getRenderedColumnWidth } from '../columnWidth';
|
|
4
|
+
|
|
5
|
+
/** Width of each built-in system column (drag handle, detail-panel toggle, checkbox). */
|
|
6
|
+
export const SYSTEM_COLUMN_WIDTH = 48;
|
|
3
7
|
|
|
4
8
|
export function isColumnPinned(field: string, pinnedColumns?: GridColumnPinning): GridPinnedPosition | null {
|
|
5
9
|
if (!pinnedColumns) return null;
|
|
@@ -15,6 +19,11 @@ export function isColumnPinned(field: string, pinnedColumns?: GridColumnPinning)
|
|
|
15
19
|
return null;
|
|
16
20
|
}
|
|
17
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Splits columns into left-pinned, unpinned and right-pinned groups. The unpinned group keeps
|
|
24
|
+
* column order; the pinned groups follow the order of `pinnedColumns.left` / `.right`, which is
|
|
25
|
+
* the order the grid renders them in.
|
|
26
|
+
*/
|
|
18
27
|
export function getPinnedColumnGroups<R extends GridRowModel = GridRowModel>(
|
|
19
28
|
columns: GridColDef<R>[],
|
|
20
29
|
pinnedColumns?: GridColumnPinning
|
|
@@ -62,6 +71,12 @@ export function getPinnedColumnGroups<R extends GridRowModel = GridRowModel>(
|
|
|
62
71
|
return { left, center, right };
|
|
63
72
|
}
|
|
64
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Sticky `left` / `right` offsets of the pinned columns, in pixels. Offsets follow the order of
|
|
76
|
+
* `columns`, which must be the order the cells are rendered in (the grid renders pinned columns
|
|
77
|
+
* in `pinnedColumns.left` / `.right` order), and use each column's rendered width (see
|
|
78
|
+
* `getRenderedColumnWidth`). Left offsets start after the pinned system columns.
|
|
79
|
+
*/
|
|
65
80
|
export function calculatePinnedPositions<R extends GridRowModel = GridRowModel>(
|
|
66
81
|
columns: GridColDef<R>[],
|
|
67
82
|
columnWidths: Record<string, number>,
|
|
@@ -76,39 +91,52 @@ export function calculatePinnedPositions<R extends GridRowModel = GridRowModel>(
|
|
|
76
91
|
|
|
77
92
|
if (!pinnedColumns) return positions;
|
|
78
93
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
leftOffset += 48;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (hasDetailPanel && pinExpandColumn) {
|
|
88
|
-
leftOffset += 48;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (checkboxSelection && pinCheckboxColumn) {
|
|
92
|
-
leftOffset += 48;
|
|
93
|
-
}
|
|
94
|
+
let leftOffset =
|
|
95
|
+
(rowReordering ? SYSTEM_COLUMN_WIDTH : 0) +
|
|
96
|
+
(hasDetailPanel && pinExpandColumn ? SYSTEM_COLUMN_WIDTH : 0) +
|
|
97
|
+
(checkboxSelection && pinCheckboxColumn ? SYSTEM_COLUMN_WIDTH : 0);
|
|
98
|
+
const right: GridColDef<R>[] = [];
|
|
94
99
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
100
|
+
columns.forEach(col => {
|
|
101
|
+
if (col.isSpacer) return;
|
|
102
|
+
const side = isColumnPinned(col.field, pinnedColumns);
|
|
103
|
+
if (side === 'left') {
|
|
104
|
+
positions[col.field] = leftOffset;
|
|
105
|
+
leftOffset += getRenderedColumnWidth(col, columnWidths);
|
|
106
|
+
} else if (side === 'right') {
|
|
107
|
+
right.push(col);
|
|
108
|
+
}
|
|
99
109
|
});
|
|
100
110
|
|
|
101
111
|
let rightOffset = 0;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
rightOffset += width;
|
|
107
|
-
});
|
|
112
|
+
for (let i = right.length - 1; i >= 0; i--) {
|
|
113
|
+
positions[right[i].field] = rightOffset;
|
|
114
|
+
rightOffset += getRenderedColumnWidth(right[i], columnWidths);
|
|
115
|
+
}
|
|
108
116
|
|
|
109
117
|
return positions;
|
|
110
118
|
}
|
|
111
119
|
|
|
120
|
+
/**
|
|
121
|
+
* The last rendered left-pinned column and the first rendered right-pinned column: the cells that
|
|
122
|
+
* carry the pinned-section edge classes. `columns` is in render order; spacers are skipped.
|
|
123
|
+
*/
|
|
124
|
+
export function getPinnedEdgeFields<R extends GridRowModel = GridRowModel>(
|
|
125
|
+
columns: GridColDef<R>[],
|
|
126
|
+
pinnedColumns?: GridColumnPinning
|
|
127
|
+
): { lastLeft: string | null; firstRight: string | null } {
|
|
128
|
+
let lastLeft: string | null = null;
|
|
129
|
+
let firstRight: string | null = null;
|
|
130
|
+
if (!pinnedColumns) return { lastLeft, firstRight };
|
|
131
|
+
for (const col of columns) {
|
|
132
|
+
if (col.isSpacer) continue;
|
|
133
|
+
const side = isColumnPinned(col.field, pinnedColumns);
|
|
134
|
+
if (side === 'left') lastLeft = col.field;
|
|
135
|
+
else if (side === 'right' && firstRight === null) firstRight = col.field;
|
|
136
|
+
}
|
|
137
|
+
return { lastLeft, firstRight };
|
|
138
|
+
}
|
|
139
|
+
|
|
112
140
|
export function isColumnPinnable<R extends GridRowModel = GridRowModel>(
|
|
113
141
|
colDef: GridColDef<R>
|
|
114
142
|
): boolean {
|
|
@@ -166,8 +194,7 @@ export function getPinnedColumnsWidth(
|
|
|
166
194
|
const col = columns.find(c => c.field === field);
|
|
167
195
|
if (!col) return total;
|
|
168
196
|
|
|
169
|
-
|
|
170
|
-
return total + width;
|
|
197
|
+
return total + getRenderedColumnWidth(col, columnWidths);
|
|
171
198
|
}, 0);
|
|
172
199
|
}
|
|
173
200
|
|
|
@@ -190,7 +217,8 @@ export function isRowPinned(
|
|
|
190
217
|
|
|
191
218
|
export function getPinnedRowGroups<R extends GridRowModel = GridRowModel>(
|
|
192
219
|
rows: R[],
|
|
193
|
-
pinnedRows?: GridRowPinning
|
|
220
|
+
pinnedRows?: GridRowPinning,
|
|
221
|
+
getRowId: (row: R) => GridRowId = (row) => row.id
|
|
194
222
|
): {
|
|
195
223
|
top: R[];
|
|
196
224
|
center: R[];
|
|
@@ -205,7 +233,7 @@ export function getPinnedRowGroups<R extends GridRowModel = GridRowModel>(
|
|
|
205
233
|
}
|
|
206
234
|
|
|
207
235
|
rows.forEach(row => {
|
|
208
|
-
const pinnedPosition = isRowPinned(row
|
|
236
|
+
const pinnedPosition = isRowPinned(getRowId(row), pinnedRows);
|
|
209
237
|
|
|
210
238
|
if (pinnedPosition === 'top') {
|
|
211
239
|
top.push(row);
|
|
@@ -218,16 +246,16 @@ export function getPinnedRowGroups<R extends GridRowModel = GridRowModel>(
|
|
|
218
246
|
|
|
219
247
|
if (pinnedRows.top) {
|
|
220
248
|
top.sort((a, b) => {
|
|
221
|
-
const aIndex = pinnedRows.top!.indexOf(a
|
|
222
|
-
const bIndex = pinnedRows.top!.indexOf(b
|
|
249
|
+
const aIndex = pinnedRows.top!.indexOf(getRowId(a));
|
|
250
|
+
const bIndex = pinnedRows.top!.indexOf(getRowId(b));
|
|
223
251
|
return aIndex - bIndex;
|
|
224
252
|
});
|
|
225
253
|
}
|
|
226
254
|
|
|
227
255
|
if (pinnedRows.bottom) {
|
|
228
256
|
bottom.sort((a, b) => {
|
|
229
|
-
const aIndex = pinnedRows.bottom!.indexOf(a
|
|
230
|
-
const bIndex = pinnedRows.bottom!.indexOf(b
|
|
257
|
+
const aIndex = pinnedRows.bottom!.indexOf(getRowId(a));
|
|
258
|
+
const bIndex = pinnedRows.bottom!.indexOf(getRowId(b));
|
|
231
259
|
return aIndex - bIndex;
|
|
232
260
|
});
|
|
233
261
|
}
|
|
@@ -280,3 +308,19 @@ export function getPinnedRowsCount(
|
|
|
280
308
|
const pinnedIds = position === 'top' ? pinnedRows.top : pinnedRows.bottom;
|
|
281
309
|
return pinnedIds?.length || 0;
|
|
282
310
|
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* The column pinning model after the column menu pins `field` to `side` (appended after the
|
|
314
|
+
* columns already pinned there) or unpins it (`null`). The field is removed from the other side.
|
|
315
|
+
*/
|
|
316
|
+
export function pinColumnTo(
|
|
317
|
+
pinnedColumns: GridColumnPinning | undefined,
|
|
318
|
+
field: string,
|
|
319
|
+
side: GridPinnedPosition | null
|
|
320
|
+
): GridColumnPinning {
|
|
321
|
+
const cleanLeft = (pinnedColumns?.left ?? []).filter(f => f !== field);
|
|
322
|
+
const cleanRight = (pinnedColumns?.right ?? []).filter(f => f !== field);
|
|
323
|
+
if (side === 'left') return { left: [...cleanLeft, field], right: cleanRight };
|
|
324
|
+
if (side === 'right') return { left: cleanLeft, right: [...cleanRight, field] };
|
|
325
|
+
return { left: cleanLeft, right: cleanRight };
|
|
326
|
+
}
|