@opencorestack/opengridx 2.0.4 → 3.0.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.
Files changed (221) hide show
  1. package/CHANGELOG.md +408 -3
  2. package/README.md +93 -72
  3. package/dist/index.d.ts +611 -121
  4. package/dist/opengridx.css +1 -1
  5. package/dist/opengridx.es.js +7992 -5650
  6. package/dist/opengridx.es.js.map +1 -1
  7. package/dist/opengridx.umd.js +17 -23
  8. package/dist/opengridx.umd.js.map +1 -1
  9. package/docs/API_REFERENCE.md +348 -161
  10. package/docs/README.md +6 -1
  11. package/docs/architecture/datagrid-orchestration.md +81 -0
  12. package/docs/architecture/grid-row-meta.md +102 -0
  13. package/docs/architecture/use-grid-columns.md +128 -0
  14. package/docs/architecture/use-grid-controlled-state.md +157 -0
  15. package/docs/architecture/use-grid-row-pipeline.md +120 -0
  16. package/docs/architecture/use-grid-scroll-sync.md +111 -0
  17. package/docs/architecture/use-grid-state-snapshot.md +90 -0
  18. package/docs/architecture/use-grid-virtualization.md +115 -0
  19. package/docs/architecture/use-grid-visible-rows.md +85 -0
  20. package/docs/assets/banner-alt.png +0 -0
  21. package/docs/assets/banner.png +0 -0
  22. package/docs/assets/hero.png +0 -0
  23. package/docs/assets/logo.png +0 -0
  24. package/docs/components/aggregation-footer.md +15 -10
  25. package/docs/components/cell.md +16 -6
  26. package/docs/components/column-group-header.md +55 -28
  27. package/docs/components/column-resize.md +17 -13
  28. package/docs/components/column-visibility.md +45 -22
  29. package/docs/components/datagrid.md +14 -12
  30. package/docs/components/empty-state.md +2 -1
  31. package/docs/components/error-overlay.md +12 -7
  32. package/docs/components/filter-panel.md +28 -8
  33. package/docs/components/header.md +28 -9
  34. package/docs/components/pagination.md +14 -8
  35. package/docs/components/row.md +21 -5
  36. package/docs/components/toolbar.md +27 -9
  37. package/docs/components/tooltip.md +16 -7
  38. package/docs/customization/slots-api.md +100 -36
  39. package/docs/customization/theming.md +66 -18
  40. package/docs/features/aggregation-pivot.md +75 -9
  41. package/docs/features/cell-spanning.md +80 -125
  42. package/docs/features/clipboard.md +41 -16
  43. package/docs/features/custom-pagination.md +38 -18
  44. package/docs/features/data-source.md +36 -13
  45. package/docs/features/editing-reordering.md +125 -16
  46. package/docs/features/export-guide.md +147 -41
  47. package/docs/features/filtering.md +44 -6
  48. package/docs/features/infinite-scroll.md +54 -46
  49. package/docs/features/keyboard-navigation.md +83 -0
  50. package/docs/features/list-view.md +58 -5
  51. package/docs/features/loading-states.md +18 -20
  52. package/docs/features/master-detail.md +18 -5
  53. package/docs/features/pdf-export.md +56 -19
  54. package/docs/features/pinning.md +23 -5
  55. package/docs/features/selection.md +50 -10
  56. package/docs/features/sorting-pagination.md +66 -14
  57. package/docs/features/state-persistence.md +78 -10
  58. package/docs/features/toolbar-customization.md +26 -17
  59. package/docs/features/tree-data-grouping.md +44 -9
  60. package/docs/features/virtualization.md +64 -15
  61. package/docs/migration/v1-to-v2.md +115 -0
  62. package/docs/migration/v2-to-v3.md +1222 -0
  63. package/docs/performance.md +124 -0
  64. package/docs/roadmap.md +16 -10
  65. package/lib/components/Cell/Cell.css +13 -1
  66. package/lib/components/Cell/Cell.tsx +130 -57
  67. package/lib/components/Cell/CellErrorBoundary.tsx +12 -6
  68. package/lib/components/Cell/GridEditInputCell.tsx +114 -41
  69. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +7 -1
  70. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +11 -0
  71. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +104 -38
  72. package/lib/components/ColumnResizeHandle/clampResizeWidth.ts +13 -0
  73. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +6 -0
  74. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +21 -13
  75. package/lib/components/DataGrid/DataGrid.css +84 -12
  76. package/lib/components/DataGrid/DataGrid.tsx +606 -872
  77. package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
  78. package/lib/components/DataGrid/GridEmptyState.tsx +7 -1
  79. package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
  80. package/lib/components/DataGrid/GridListView.tsx +166 -23
  81. package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
  82. package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
  83. package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
  84. package/lib/components/DataGrid/GridPinnedRows.tsx +65 -11
  85. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
  86. package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
  87. package/lib/components/DataGrid/GridVirtualRows.tsx +48 -25
  88. package/lib/components/DetailPanel/DetailPanel.css +0 -6
  89. package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
  90. package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
  91. package/lib/components/FilterPanel/FilterPanel.css +13 -2
  92. package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
  93. package/lib/components/Header/ColumnMenu.tsx +91 -39
  94. package/lib/components/Header/Header.css +3 -3
  95. package/lib/components/Header/Header.tsx +179 -160
  96. package/lib/components/ListView/ListView.css +25 -0
  97. package/lib/components/ListView/ListViewRow.tsx +79 -10
  98. package/lib/components/Pagination/Pagination.tsx +15 -7
  99. package/lib/components/Row/Row.tsx +185 -75
  100. package/lib/components/Toolbar/GlobalSearch.css +6 -3
  101. package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
  102. package/lib/components/Toolbar/GridToolbar.css +12 -11
  103. package/lib/components/Toolbar/GridToolbar.tsx +133 -89
  104. package/lib/components/Toolbar/PivotPanel.tsx +89 -49
  105. package/lib/components/Tooltip/Tooltip.css +20 -0
  106. package/lib/components/Tooltip/Tooltip.tsx +68 -35
  107. package/lib/components/ui/Button.tsx +3 -0
  108. package/lib/components/ui/Checkbox.tsx +2 -1
  109. package/lib/components/ui/ExpandIcon.css +0 -9
  110. package/lib/components/ui/ExpandIcon.tsx +4 -1
  111. package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
  112. package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
  113. package/lib/hooks/core/useDataGrid.ts +115 -173
  114. package/lib/hooks/core/useGridAggregationApi.ts +72 -0
  115. package/lib/hooks/core/useGridApiMethods.ts +115 -0
  116. package/lib/hooks/core/useGridApiRef.ts +15 -3
  117. package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
  118. package/lib/hooks/core/useGridAriaRows.ts +38 -0
  119. package/lib/hooks/core/useGridClipboardApi.ts +38 -0
  120. package/lib/hooks/core/useGridColumnLookup.ts +30 -0
  121. package/lib/hooks/core/useGridColumns.tsx +284 -76
  122. package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
  123. package/lib/hooks/core/useGridControlledState.ts +99 -6
  124. package/lib/hooks/core/useGridDetailPanel.ts +51 -0
  125. package/lib/hooks/core/useGridDevWarnings.ts +123 -0
  126. package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
  127. package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
  128. package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
  129. package/lib/hooks/core/useGridHierarchy.ts +81 -0
  130. package/lib/hooks/core/useGridKeyboardNavigation.ts +452 -250
  131. package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
  132. package/lib/hooks/core/useGridPageCorrection.ts +55 -0
  133. package/lib/hooks/core/useGridRowIdOf.ts +32 -0
  134. package/lib/hooks/core/useGridRowInteractions.ts +81 -0
  135. package/lib/hooks/core/useGridRowPipeline.ts +69 -39
  136. package/lib/hooks/core/useGridRowSelection.ts +91 -0
  137. package/lib/hooks/core/useGridScrollSync.ts +80 -32
  138. package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
  139. package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
  140. package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
  141. package/lib/hooks/core/useGridToolbarProps.ts +97 -0
  142. package/lib/hooks/core/useGridViewport.ts +38 -0
  143. package/lib/hooks/core/useGridViewportSize.ts +26 -0
  144. package/lib/hooks/core/useGridVisibleRows.ts +20 -7
  145. package/lib/hooks/core/useLayout.ts +215 -195
  146. package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
  147. package/lib/hooks/core/useStableColumns.ts +34 -0
  148. package/lib/hooks/features/useAggregation.ts +86 -90
  149. package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
  150. package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
  151. package/lib/hooks/features/useGridClipboard.ts +133 -73
  152. package/lib/hooks/features/useGridDataSource.ts +354 -128
  153. package/lib/hooks/features/useGridEditing.ts +211 -51
  154. package/lib/hooks/features/useGridPivot.ts +92 -0
  155. package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
  156. package/lib/hooks/features/useGridSpanning.ts +410 -306
  157. package/lib/hooks/features/usePivot.ts +0 -0
  158. package/lib/hooks/features/useServerTreeChildren.ts +50 -0
  159. package/lib/hooks/useColumnReorder.ts +38 -44
  160. package/lib/hooks/useRowGrouping.ts +335 -214
  161. package/lib/hooks/useRowReorder.ts +62 -31
  162. package/lib/hooks/useTreeData.ts +330 -262
  163. package/lib/index.ts +30 -2
  164. package/lib/state/index.ts +1 -0
  165. package/lib/state/types.ts +7 -1
  166. package/lib/state/useGridStateStorage.ts +88 -29
  167. package/lib/theme/DataGridThemeProvider.tsx +143 -38
  168. package/lib/theme/gridThemeContext.ts +5 -0
  169. package/lib/theme/index.ts +5 -0
  170. package/lib/theme/presets.ts +5 -4
  171. package/lib/theme/types.ts +40 -2
  172. package/lib/types/index.ts +311 -108
  173. package/lib/utils/aggregation/index.ts +157 -0
  174. package/lib/utils/aria/index.ts +40 -0
  175. package/lib/utils/columnGroups.ts +110 -0
  176. package/lib/utils/columnLookup.ts +33 -0
  177. package/lib/utils/columnWidth.ts +22 -0
  178. package/lib/utils/dataSource/index.ts +46 -0
  179. package/lib/utils/detailPanel.ts +66 -0
  180. package/lib/utils/dragData.ts +44 -0
  181. package/lib/utils/editing/index.ts +30 -0
  182. package/lib/utils/export/exportShared.ts +197 -0
  183. package/lib/utils/export/exportToExcelAdvanced.ts +344 -121
  184. package/lib/utils/export/exportToPdf.ts +143 -92
  185. package/lib/utils/export/groupLabel.ts +14 -0
  186. package/lib/utils/export/index.ts +278 -272
  187. package/lib/utils/export/pdfText.ts +50 -0
  188. package/lib/utils/filtering/index.ts +277 -148
  189. package/lib/utils/focus/index.ts +115 -0
  190. package/lib/utils/gridApiRows.ts +35 -0
  191. package/lib/utils/gridModes.ts +77 -0
  192. package/lib/utils/grouping/groupedExportRows.ts +91 -0
  193. package/lib/utils/pagination/index.ts +56 -0
  194. package/lib/utils/pinning/index.ts +77 -33
  195. package/lib/utils/pivot/index.ts +368 -0
  196. package/lib/utils/scroll/index.ts +78 -0
  197. package/lib/utils/scroll/scrollEnd.ts +18 -0
  198. package/lib/utils/sorting/index.ts +154 -30
  199. package/lib/utils/syntheticRows.ts +12 -0
  200. package/lib/utils/values.ts +206 -0
  201. package/lib/utils/viewport.ts +14 -0
  202. package/llms.txt +272 -166
  203. package/package.json +18 -6
  204. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
  205. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
  206. package/lib/components/DataGrid/DataGrid.regression.test.tsx +0 -132
  207. package/lib/components/DataGrid/DataGrid.smoke.test.tsx +0 -58
  208. package/lib/components/DataGrid/DataGrid.v2.test.tsx +0 -127
  209. package/lib/components/Header/Header.test.tsx +0 -135
  210. package/lib/components/Pagination/Pagination.test.tsx +0 -64
  211. package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
  212. package/lib/hooks/core/useGridControlledState.test.ts +0 -86
  213. package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
  214. package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
  215. package/lib/hooks/features/useAggregation.test.ts +0 -175
  216. package/lib/hooks/features/usePivot.test.ts +0 -202
  217. package/lib/hooks/useRowGrouping.test.ts +0 -137
  218. package/lib/utils/export/exportToPdf.test.ts +0 -120
  219. package/lib/utils/export/groupedExport.test.ts +0 -144
  220. package/lib/utils/filtering/filtering.test.ts +0 -178
  221. package/lib/utils/sorting/sorting.test.ts +0 -88
@@ -0,0 +1,157 @@
1
+ import type { GridAggregationModel, GridAggregationResult, GridColDef, GridRowModel } from '../../types';
2
+ import { formatLocalDate, getCellValue, toNumber } from '../values';
3
+
4
+ export type BuiltInAggFn = 'sum' | 'avg' | 'count' | 'min' | 'max' | 'unique';
5
+
6
+ /**
7
+ * A value that carries no data for aggregation: null, undefined, or a string that is empty or whitespace
8
+ * only. Blank cells come from edits, CSV imports and form inputs; they must not count as zero or as an item.
9
+ */
10
+ /** Stable empty model: no aggregation (e.g. while pivoting, where the Grand Total row is the aggregate). */
11
+ export const NO_AGGREGATION_MODEL: GridAggregationModel = {};
12
+
13
+ export const isEmptyAggregateValue = (v: unknown): boolean =>
14
+ v == null || (typeof v === 'string' && v.trim() === '');
15
+
16
+ // Numeric functions read values with the grid's shared toNumber (as filtering does): numbers, numeric
17
+ // strings and Dates (as timestamps). Booleans, arrays and blank strings are skipped, since Number()
18
+ // would coerce them to 0 or 1 and silently corrupt avg / min / max.
19
+ const toNumbers = (values: unknown[]): number[] => {
20
+ const nums: number[] = [];
21
+ for (const v of values) {
22
+ const n = toNumber(v);
23
+ if (n !== null) nums.push(n);
24
+ }
25
+ return nums;
26
+ };
27
+
28
+ // The smallest / largest value. A loop, not Math.min(...nums): spreading >~120k arguments throws
29
+ // RangeError. The extreme is returned as it was given when it is a Date, so min / max of a date
30
+ // column is a date (formatted as one) rather than epoch milliseconds.
31
+ const extremeOf = (values: unknown[], isBetter: (a: number, b: number) => boolean): number | Date | null => {
32
+ let best: number | null = null;
33
+ let bestValue: unknown = null;
34
+ for (const v of values) {
35
+ const n = toNumber(v);
36
+ if (n === null) continue;
37
+ if (best === null || isBetter(n, best)) {
38
+ best = n;
39
+ bestValue = v;
40
+ }
41
+ }
42
+ if (best === null) return null;
43
+ return bestValue instanceof Date ? bestValue : best;
44
+ };
45
+
46
+ export const AGGREGATION_FUNCTIONS: Record<BuiltInAggFn, (values: unknown[]) => unknown> = {
47
+ sum: (values) => toNumbers(values).reduce((acc, n) => acc + n, 0),
48
+ avg: (values) => {
49
+ const nums = toNumbers(values);
50
+ return nums.length ? nums.reduce((acc, n) => acc + n, 0) / nums.length : null;
51
+ },
52
+ count: (values) => values.filter((v) => !isEmptyAggregateValue(v)).length,
53
+ min: (values) => extremeOf(values, (a, b) => a < b),
54
+ max: (values) => extremeOf(values, (a, b) => a > b),
55
+ unique: (values) => new Set(values.filter((v) => !isEmptyAggregateValue(v))).size,
56
+ };
57
+
58
+ /**
59
+ * Whether `colDef` may be summarised with `fnName`: not when the column sets `aggregable: false`,
60
+ * and not with a function outside its `availableAggregationFunctions`. The footer, group rows,
61
+ * pivot values and exports all go through this, so a model entry the column forbids is ignored.
62
+ */
63
+ export function isAggregationAllowed<R extends GridRowModel>(colDef: GridColDef<R> | undefined, fnName: string): boolean {
64
+ if (!colDef) return true;
65
+ if (colDef.aggregable === false) return false;
66
+ return !colDef.availableAggregationFunctions || colDef.availableAggregationFunctions.includes(fnName);
67
+ }
68
+
69
+ /**
70
+ * Aggregates each `aggregationModel` field over `rows`. Cells are read through the column's
71
+ * `valueGetter` (getCellValue), so a computed column totals what its cells show.
72
+ */
73
+ export function computeAggregations<R extends GridRowModel>(
74
+ rows: R[],
75
+ aggregationModel: GridAggregationModel,
76
+ columnsLookup: Map<string, GridColDef<R>>,
77
+ warnPrefix: string,
78
+ ): GridAggregationResult {
79
+ const result: GridAggregationResult = {};
80
+ for (const [field, fnName] of Object.entries(aggregationModel)) {
81
+ const colDef = columnsLookup.get(field);
82
+ if (!isAggregationAllowed(colDef, fnName)) continue;
83
+ const fn = AGGREGATION_FUNCTIONS[fnName as BuiltInAggFn];
84
+ if (!fn) {
85
+ console.warn(`[${warnPrefix}] Unknown aggregation function: "${fnName}"`);
86
+ continue;
87
+ }
88
+ result[field] = fn(rows.map((row) => getCellValue(row, field, colDef as GridColDef | undefined)));
89
+ }
90
+ return result;
91
+ }
92
+
93
+ /** Default display string for an aggregate: `—` when missing, locale-grouped numbers, avg to 2 decimals. */
94
+ export function formatAggregationValue(value: unknown, fnName: string): string {
95
+ if (value == null) return '—';
96
+ if (typeof value === 'number') {
97
+ if (fnName === 'avg') {
98
+ return value.toLocaleString(undefined, { maximumFractionDigits: 2 });
99
+ }
100
+ return value.toLocaleString();
101
+ }
102
+ return String(value);
103
+ }
104
+
105
+ const COUNT_FUNCTIONS = new Set<string>(['count', 'unique']);
106
+
107
+ /**
108
+ * Whether an aggregation result is a number of values rather than a value of the column's kind:
109
+ * a count of a currency column is not an amount, so the column's valueFormatter must not format it.
110
+ */
111
+ export function isCountAggregation(fnName: string | undefined): boolean {
112
+ return fnName !== undefined && COUNT_FUNCTIONS.has(fnName);
113
+ }
114
+
115
+ /**
116
+ * The aggregation result in the column's own terms: min / max of a `type: 'date'` column given as
117
+ * epoch milliseconds (as a server may return it) becomes a Date again.
118
+ */
119
+ export function normalizeAggregateValue<R extends GridRowModel>(
120
+ colDef: GridColDef<R> | undefined,
121
+ fnName: string | undefined,
122
+ value: unknown,
123
+ ): unknown {
124
+ if (colDef?.type === 'date' && (fnName === 'min' || fnName === 'max') && typeof value === 'number' && Number.isFinite(value)) {
125
+ return new Date(value);
126
+ }
127
+ return value;
128
+ }
129
+
130
+ /**
131
+ * Display text for an aggregate of `colDef`'s values, used by the footer, group rows, pivot cells and
132
+ * every exporter so they all show the same thing.
133
+ * - count / unique are plain counts in the default format; the column's valueFormatter is not applied.
134
+ * - sum / avg / min / max go through the column's valueFormatter. Its `row` is the record of aggregated
135
+ * values (the group row, the pivot row, or the footer's totals). A formatter that throws, typically
136
+ * because it reads row data an aggregate does not have, falls back to the default format.
137
+ * - Without a formatter, dates show as locale dates and numbers in the default format.
138
+ */
139
+ export function formatAggregateForColumn<R extends GridRowModel>(
140
+ value: unknown,
141
+ fnName: string,
142
+ colDef?: GridColDef<R>,
143
+ aggregates?: Record<string, unknown>,
144
+ ): string {
145
+ if (value == null) return '—';
146
+ if (isCountAggregation(fnName)) return formatAggregationValue(value, fnName);
147
+ const normalized = normalizeAggregateValue(colDef, fnName, value);
148
+ if (colDef?.valueFormatter) {
149
+ try {
150
+ return String(colDef.valueFormatter({ value: normalized, row: (aggregates ?? {}) as R, field: colDef.field }) ?? '');
151
+ } catch {
152
+ // Fall through to the default format rather than failing the whole grid or export.
153
+ }
154
+ }
155
+ if (normalized instanceof Date) return Number.isNaN(normalized.getTime()) ? '' : formatLocalDate(normalized);
156
+ return formatAggregationValue(normalized, fnName);
157
+ }
@@ -0,0 +1,40 @@
1
+ export interface GridAriaRowLayoutParams {
2
+ /** Header rows: the column header row plus any column-group rows. */
3
+ headerRowCount: number;
4
+ pinnedTopCount: number;
5
+ pinnedBottomCount: number;
6
+ /** Centre rows rendered for the current page. */
7
+ pageRowCount: number;
8
+ /** Centre rows across every page (the server's rowCount under server pagination). */
9
+ totalCenterRowCount: number;
10
+ /** Centre rows on the pages before the current one. */
11
+ pageOffset: number;
12
+ }
13
+
14
+ export interface GridAriaRowLayout {
15
+ /** `aria-rowcount` of the grid. */
16
+ rowCount: number;
17
+ /** `aria-rowindex` of the first top-pinned row. */
18
+ topBase: number;
19
+ /** Added to a centre row's `rowIndex` (which counts top-pinned rows) to get its `aria-rowindex`. */
20
+ centerOffset: number;
21
+ /** `aria-rowindex` of the first bottom-pinned row. */
22
+ bottomBase: number;
23
+ /** `rowIndex` of the first bottom-pinned row (top-pinned + centre rows of this page). */
24
+ bottomRowIndexOffset: number;
25
+ }
26
+
27
+ /**
28
+ * Row positions for assistive technology. Header rows come first; data rows are numbered across
29
+ * the whole dataset, so page 2 does not restart at 1 and bottom-pinned rows follow every page.
30
+ */
31
+ export function getAriaRowLayout(params: GridAriaRowLayoutParams): GridAriaRowLayout {
32
+ const { headerRowCount, pinnedTopCount, pinnedBottomCount, pageRowCount, totalCenterRowCount, pageOffset } = params;
33
+ return {
34
+ rowCount: headerRowCount + pinnedTopCount + totalCenterRowCount + pinnedBottomCount,
35
+ topBase: headerRowCount + 1,
36
+ centerOffset: headerRowCount + pageOffset + 1,
37
+ bottomBase: headerRowCount + pinnedTopCount + totalCenterRowCount + 1,
38
+ bottomRowIndexOffset: pinnedTopCount + pageRowCount,
39
+ };
40
+ }
@@ -0,0 +1,110 @@
1
+ import type { GridColumnGroup, GridColumnGroupingModel, GridPinnedPosition } from '../types';
2
+
3
+ /** Number of group header rows a model renders (its maximum nesting depth; 0 when there is none). */
4
+ export function getColumnGroupDepth(model?: GridColumnGroupingModel): number {
5
+ if (!model) return 0;
6
+ let depth = 0;
7
+ const walk = (group: GridColumnGroup, level: number) => {
8
+ depth = Math.max(depth, level);
9
+ for (const child of group.children) {
10
+ if (typeof child !== 'string') walk(child, level + 1);
11
+ }
12
+ };
13
+ model.forEach(group => walk(group, 1));
14
+ return depth;
15
+ }
16
+
17
+ /** Maps every leaf field to its group path, outermost group first. */
18
+ export function getColumnGroupPaths(model?: GridColumnGroupingModel): Map<string, GridColumnGroup[]> {
19
+ const paths = new Map<string, GridColumnGroup[]>();
20
+ if (!model) return paths;
21
+ const walk = (group: GridColumnGroup, path: GridColumnGroup[]) => {
22
+ const next = [...path, group];
23
+ for (const child of group.children) {
24
+ if (typeof child === 'string') {
25
+ if (!paths.has(child)) paths.set(child, next);
26
+ } else {
27
+ walk(child, next);
28
+ }
29
+ }
30
+ };
31
+ model.forEach(group => walk(group, []));
32
+ return paths;
33
+ }
34
+
35
+ /**
36
+ * Whether moving `fromField` to the position of `toField` keeps every column group contiguous:
37
+ * both fields must sit in the same innermost group (or both be ungrouped).
38
+ */
39
+ export function canReorderWithinColumnGroups(model: GridColumnGroupingModel | undefined, fromField: string, toField: string): boolean {
40
+ if (!model || model.length === 0) return true;
41
+ const paths = getColumnGroupPaths(model);
42
+ const from = paths.get(fromField);
43
+ const to = paths.get(toField);
44
+ return (from?.[from.length - 1] ?? null) === (to?.[to.length - 1] ?? null);
45
+ }
46
+
47
+ export interface ColumnGroupRowColumn {
48
+ field: string;
49
+ width: number;
50
+ flex?: number;
51
+ pinned: GridPinnedPosition | null;
52
+ /** Horizontal-virtualization spacer: counts as ungrouped width. */
53
+ isSpacer?: boolean;
54
+ }
55
+
56
+ export interface ColumnGroupRowCell {
57
+ key: string;
58
+ isGroup: boolean;
59
+ label: string;
60
+ headerClassName?: string;
61
+ width: number;
62
+ flexGrow: number;
63
+ /** Data (non-spacer) fields the cell covers, in order. */
64
+ fields: string[];
65
+ /** Index of the first covered data column among all rendered data columns. */
66
+ firstDataIndex: number;
67
+ pinned: GridPinnedPosition | null;
68
+ }
69
+
70
+ /**
71
+ * Builds one group header row (level 0 = outermost). Consecutive columns that share the group at
72
+ * `level` and the same pinned section form one cell; consecutive ungrouped columns form one filler.
73
+ * A group whose columns are not adjacent (hidden columns excepted) renders one cell per run.
74
+ */
75
+ export function buildColumnGroupRow(
76
+ columns: ColumnGroupRowColumn[],
77
+ paths: Map<string, GridColumnGroup[]>,
78
+ level: number,
79
+ ): ColumnGroupRowCell[] {
80
+ const cells: ColumnGroupRowCell[] = [];
81
+ let dataIndex = 0;
82
+ let current: ColumnGroupRowCell | null = null;
83
+ let currentGroup: GridColumnGroup | null = null;
84
+
85
+ for (const column of columns) {
86
+ const group = column.isSpacer ? null : (paths.get(column.field)?.[level] ?? null);
87
+ if (current === null || currentGroup !== group || current.pinned !== column.pinned) {
88
+ current = {
89
+ key: `${group ? group.groupId : 'filler'}-${level}-${column.field}`,
90
+ isGroup: group !== null,
91
+ label: group?.headerName ?? '',
92
+ headerClassName: group?.headerClassName,
93
+ width: 0,
94
+ flexGrow: 0,
95
+ fields: [],
96
+ firstDataIndex: dataIndex,
97
+ pinned: column.pinned,
98
+ };
99
+ currentGroup = group;
100
+ cells.push(current);
101
+ }
102
+ current.width += column.width;
103
+ current.flexGrow += column.flex ?? 0;
104
+ if (!column.isSpacer) {
105
+ current.fields.push(column.field);
106
+ dataIndex++;
107
+ }
108
+ }
109
+ return cells;
110
+ }
@@ -0,0 +1,33 @@
1
+ import type { GridColDef } from '../types';
2
+
3
+ /**
4
+ * Column information the client-side filter and sort need: the column definition for each field
5
+ * (for `valueGetter` and `type`) and the columns the quick filter searches.
6
+ */
7
+ export interface GridColumnLookup {
8
+ /** Column definitions keyed by field. */
9
+ byField: ReadonlyMap<string, GridColDef>;
10
+ /** Visible, filterable columns. The quick filter searches only these. */
11
+ quickFilterColumns: readonly GridColDef[];
12
+ }
13
+
14
+ /**
15
+ * Build the lookup from the grid's columns. Columns hidden through `columnVisibilityModel`,
16
+ * columns with `filterable: false` and internal `__`-prefixed columns are not searched by the
17
+ * quick filter; column filter items still apply to every column.
18
+ */
19
+ export function buildColumnLookup(
20
+ columns: readonly GridColDef[],
21
+ columnVisibilityModel: Record<string, boolean> = {},
22
+ ): GridColumnLookup {
23
+ const byField = new Map<string, GridColDef>();
24
+ const quickFilterColumns: GridColDef[] = [];
25
+ for (const col of columns) {
26
+ byField.set(col.field, col);
27
+ if (col.isSpacer || col.field.startsWith('__')) continue;
28
+ if (col.filterable === false) continue;
29
+ if (columnVisibilityModel[col.field] === false) continue;
30
+ quickFilterColumns.push(col);
31
+ }
32
+ return { byField, quickFilterColumns };
33
+ }
@@ -0,0 +1,22 @@
1
+ import type { GridColDef, GridRowModel } from '../types';
2
+
3
+ /** Clamps a column width the way CSS applies `min-width` / `max-width` (min-width wins a conflict). */
4
+ export function clampColumnWidth(width: number, minWidth?: number, maxWidth?: number): number {
5
+ return Math.max(minWidth ?? 0, Math.min(maxWidth ?? Infinity, width));
6
+ }
7
+
8
+ /**
9
+ * The width a rendered column occupies: the user's resize override, else its width (the
10
+ * layout-resolved pixel width for grid columns), clamped to `minWidth` / `maxWidth` exactly as
11
+ * the cell and header CSS clamp it. Non-numeric widths fall back to `fallback`.
12
+ */
13
+ export function getRenderedColumnWidth<R extends GridRowModel>(
14
+ col: GridColDef<R>,
15
+ columnWidths: Record<string, number>,
16
+ fallback = 100,
17
+ ): number {
18
+ if (col.isSpacer) return typeof col.width === 'number' ? col.width : 0;
19
+ const raw = columnWidths[col.field] ?? col.width;
20
+ const width = typeof raw === 'number' && Number.isFinite(raw) ? raw : fallback;
21
+ return clampColumnWidth(width, col.minWidth, col.maxWidth);
22
+ }
@@ -0,0 +1,46 @@
1
+ export interface ServerDrivenParams {
2
+ dataSource?: unknown;
3
+ paginationMode?: string;
4
+ sortingMode?: string;
5
+ filterMode?: string;
6
+ }
7
+
8
+ /**
9
+ * True when a dataSource is present and the server shapes the rows: any of pagination (including
10
+ * infinite scroll), sorting or filtering is done on the server. The grid then holds only the rows
11
+ * the server returned, so anything computed over "all rows" (such as aggregation totals) must come
12
+ * from the server. With every mode on 'client' the grid fetches all rows once and computes
13
+ * everything itself.
14
+ */
15
+ export function isServerDrivenDataSource({ dataSource, paginationMode, sortingMode, filterMode }: ServerDrivenParams): boolean {
16
+ return Boolean(dataSource) && (
17
+ paginationMode === 'server'
18
+ || paginationMode === 'infinite'
19
+ || sortingMode === 'server'
20
+ || filterMode === 'server'
21
+ );
22
+ }
23
+
24
+ /**
25
+ * `endRow` of a request for every row: sent when the grid pages on the client (`paginationMode`
26
+ * 'client') and when it loads the children of a server-side tree node. `endRow` is exclusive, and
27
+ * `Number.MAX_SAFE_INTEGER` keeps both `rows.slice(startRow, endRow)` and
28
+ * `LIMIT endRow - startRow` returning everything while staying JSON-serializable.
29
+ */
30
+ export const GRID_ALL_ROWS_END_ROW = Number.MAX_SAFE_INTEGER;
31
+
32
+ /** Shown when a failed getRows call gives no usable message. */
33
+ export const DATA_SOURCE_ERROR_FALLBACK_MESSAGE = 'An unexpected error occurred while loading the data.';
34
+
35
+ /**
36
+ * The message of the value a getRows call rejected with: an `Error`, a string, or any object with a
37
+ * string `message` (the usual shape of fetch and HTTP-client errors).
38
+ */
39
+ export function getDataSourceErrorMessage(error: unknown): string {
40
+ if (typeof error === 'string' && error) return error;
41
+ if (typeof error === 'object' && error !== null && 'message' in error
42
+ && typeof error.message === 'string' && error.message) {
43
+ return error.message;
44
+ }
45
+ return DATA_SOURCE_ERROR_FALLBACK_MESSAGE;
46
+ }
@@ -0,0 +1,66 @@
1
+ import type { GridDetailPanelHeight, GridRowId, GridRowMeta } from '../types';
2
+ import { callSafely } from './values';
3
+
4
+ /** Panel height used when `getDetailPanelHeight` is omitted or returns something unusable. */
5
+ export const DEFAULT_DETAIL_PANEL_HEIGHT = 200;
6
+
7
+ /**
8
+ * Normalises a `getDetailPanelHeight` result. The layout and the rendered panel both go through
9
+ * this, so they always agree: `0` is a 0px panel, `'auto'` fits the content, and a missing,
10
+ * negative or non-numeric value is the 200px default.
11
+ */
12
+ export function resolveDetailPanelHeight(height: GridDetailPanelHeight | string | null | undefined): number | 'auto' {
13
+ if (height === 'auto') return 'auto';
14
+ const value = typeof height === 'string' ? Number.parseFloat(height) : height;
15
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0) return value;
16
+ return DEFAULT_DETAIL_PANEL_HEIGHT;
17
+ }
18
+
19
+ /**
20
+ * Calls `getDetailPanelHeight`; a callback that throws gets the default height (with a one-time
21
+ * dev warning) instead of crashing the grid. The layout and both row renderers call this.
22
+ */
23
+ export function callGetDetailPanelHeight<P>(
24
+ getDetailPanelHeight: ((params: P) => GridDetailPanelHeight) | undefined,
25
+ params: P,
26
+ ): GridDetailPanelHeight | undefined {
27
+ if (!getDetailPanelHeight) return undefined;
28
+ return callSafely<GridDetailPanelHeight | undefined>(
29
+ () => getDetailPanelHeight(params),
30
+ undefined,
31
+ 'getDetailPanelHeight',
32
+ 'getDetailPanelHeight threw; the panel uses the default height',
33
+ );
34
+ }
35
+
36
+ /**
37
+ * The height the layout reserves for an expanded panel: its fixed height, or for an `'auto'`
38
+ * panel the height it was measured at (the default until it has rendered once).
39
+ */
40
+ export function getDetailPanelLayoutHeight(
41
+ height: GridDetailPanelHeight | string | null | undefined,
42
+ measuredHeight: number | undefined,
43
+ ): number {
44
+ const resolved = resolveDetailPanelHeight(height);
45
+ if (resolved === 'auto') return measuredHeight ?? DEFAULT_DETAIL_PANEL_HEIGHT;
46
+ return resolved;
47
+ }
48
+
49
+ /**
50
+ * The expanded detail-panel ids that can actually show a panel: synthetic hierarchy rows
51
+ * (row-grouping groups, generated tree parents) have no detail panel, so an id of one in a
52
+ * controlled `detailPanelExpandedRowIds` is ignored. Returns the same set when nothing is dropped.
53
+ */
54
+ export function getDetailPanelRowIds(
55
+ expandedRowIds: Set<GridRowId>,
56
+ rowMetaMap: ReadonlyMap<GridRowId, GridRowMeta>,
57
+ ): Set<GridRowId> {
58
+ if (rowMetaMap.size === 0 || expandedRowIds.size === 0) return expandedRowIds;
59
+ let dropped = false;
60
+ const ids = new Set<GridRowId>();
61
+ for (const id of expandedRowIds) {
62
+ if (rowMetaMap.get(id)?.isGroupRow) dropped = true;
63
+ else ids.add(id);
64
+ }
65
+ return dropped ? ids : expandedRowIds;
66
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * HTML5 drag-and-drop helpers shared by row and column reorder.
3
+ *
4
+ * Each reorder drag puts a MIME type of its own into the DataTransfer. Firefox only starts a drag
5
+ * when dragstart sets some data, and drop targets use the type to accept only the grid's own drag,
6
+ * so a file, a text selection or another grid's drag is never taken for a reorder.
7
+ */
8
+
9
+ export const ROW_DRAG_MIME = 'application/x-ogx-row';
10
+ export const COLUMN_DRAG_MIME = 'application/x-ogx-column';
11
+
12
+ interface DragEventLike {
13
+ dataTransfer?: Partial<Pick<DataTransfer, 'setData' | 'types'>> | null;
14
+ }
15
+
16
+ /** Stores the drag token (and a plain-text copy) on dragstart. */
17
+ export function setDragToken(event: DragEventLike, mime: string, value: string): void {
18
+ const dataTransfer = event.dataTransfer;
19
+ if (!dataTransfer || typeof dataTransfer.setData !== 'function') return;
20
+ dataTransfer.setData(mime, value);
21
+ dataTransfer.setData('text/plain', value);
22
+ }
23
+
24
+ /**
25
+ * False when the drag carries data types and the grid's token is not among them. When the
26
+ * browser (or a test double) exposes no type list, the drag is not rejected on that ground.
27
+ */
28
+ export function hasDragToken(event: DragEventLike, mime: string): boolean {
29
+ const types = event.dataTransfer?.types;
30
+ if (!types) return true;
31
+ return Array.from(types).includes(mime);
32
+ }
33
+
34
+ /**
35
+ * Calls `onEnd` once when the drag that started on `source` ends. The listener sits on the
36
+ * source node itself, so it also fires when that node has been unmounted mid-drag (a virtualized
37
+ * row scrolled out of the render window), which React's delegated `onDragEnd` never sees.
38
+ */
39
+ export function onNativeDragEnd(source: EventTarget | null | undefined, onEnd: () => void): () => void {
40
+ if (!source || typeof source.addEventListener !== 'function') return () => {};
41
+ const handler = () => onEnd();
42
+ source.addEventListener('dragend', handler, { once: true });
43
+ return () => source.removeEventListener('dragend', handler);
44
+ }
@@ -0,0 +1,30 @@
1
+ import type { GridCellParams, GridRowMeta, GridRowModel } from '../../types';
2
+
3
+ // Cells are read through the one `getCellValue` in `lib/utils/values`.
4
+
5
+ export interface CellEditabilityParams<R extends GridRowModel = GridRowModel> extends GridCellParams<R> {
6
+ rowMeta?: GridRowMeta;
7
+ }
8
+
9
+ /**
10
+ * Whether a body cell may enter edit mode. Double-click, Enter, Tab stops and `aria-readonly` all use
11
+ * this one rule:
12
+ * - the column must have `editable: true` (`isCellEditable` can only restrict, never grant);
13
+ * - the row must be a real data row, not a synthetic row-grouping header or an auto-generated tree
14
+ * ancestor (`rowMeta.isGroupRow`). Tree-data parents are real rows and stay editable;
15
+ * - `isCellEditable`, when given, must return true. A predicate that throws counts as read-only.
16
+ */
17
+ export function resolveCellEditable<R extends GridRowModel>(
18
+ params: CellEditabilityParams<R>,
19
+ isCellEditable?: (params: GridCellParams<R>) => boolean
20
+ ): boolean {
21
+ const { row, field, value, colDef, rowIndex, colIndex, rowMeta } = params;
22
+ if (!colDef.editable) return false;
23
+ if (rowMeta?.isGroupRow || row._isSkeleton) return false;
24
+ if (!isCellEditable) return true;
25
+ try {
26
+ return Boolean(isCellEditable({ row, field, value, colDef, rowIndex, colIndex }));
27
+ } catch {
28
+ return false;
29
+ }
30
+ }