@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,50 @@
1
+ /**
2
+ * jsPDF's built-in fonts (Helvetica & co.) use WinAnsiEncoding. When a string contains a single
3
+ * character outside it, jsPDF switches the whole string to 16-bit output drawn with the WinAnsi
4
+ * font, so the entire cell comes out garbled, not just that character. `toWinAnsi` keeps every
5
+ * character the font can draw, maps look-alikes (special spaces, dashes, minus) to ASCII and
6
+ * replaces the rest with '?', so only the undrawable characters are lost.
7
+ */
8
+
9
+ /** Characters above U+00FF that WinAnsiEncoding (Windows-1252) still contains. */
10
+ const WIN_ANSI_EXTRA = new Set<number>([
11
+ 0x20ac, 0x201a, 0x0192, 0x201e, 0x2026, 0x2020, 0x2021, 0x02c6, 0x2030, 0x0160, 0x2039, 0x0152,
12
+ 0x017d, 0x2018, 0x2019, 0x201c, 0x201d, 0x2022, 0x2013, 0x2014, 0x02dc, 0x2122, 0x0161, 0x203a,
13
+ 0x0153, 0x017e, 0x0178,
14
+ ]);
15
+
16
+ function substitute(codePoint: number): string | null {
17
+ // En quad … hair space, narrow no-break space (fr-FR thousands separator), math space, ideographic space
18
+ if ((codePoint >= 0x2000 && codePoint <= 0x200a) || codePoint === 0x202f || codePoint === 0x205f || codePoint === 0x3000) return ' ';
19
+ // Zero-width space / joiners, word joiner, BOM
20
+ if ((codePoint >= 0x200b && codePoint <= 0x200d) || codePoint === 0x2060 || codePoint === 0xfeff) return '';
21
+ // Hyphen, non-breaking hyphen, figure dash, minus sign (used for negatives by some locales)
22
+ if (codePoint === 0x2010 || codePoint === 0x2011 || codePoint === 0x2012 || codePoint === 0x2212) return '-';
23
+ return null;
24
+ }
25
+
26
+ export interface WinAnsiText {
27
+ text: string;
28
+ /** True when a character had to be replaced with '?'. */
29
+ lossy: boolean;
30
+ }
31
+
32
+ export function toWinAnsi(input: string): WinAnsiText {
33
+ let text = '';
34
+ let lossy = false;
35
+ for (const char of input) {
36
+ const codePoint = char.codePointAt(0) ?? 0;
37
+ if (codePoint <= 0xff || WIN_ANSI_EXTRA.has(codePoint)) {
38
+ text += char;
39
+ continue;
40
+ }
41
+ const replacement = substitute(codePoint);
42
+ if (replacement !== null) {
43
+ text += replacement;
44
+ } else {
45
+ text += '?';
46
+ lossy = true;
47
+ }
48
+ }
49
+ return { text, lossy };
50
+ }
@@ -1,215 +1,344 @@
1
1
 
2
- import type { GridRowModel, GridFilterItem, GridFilterModel, GridFilterGroup } from '../../types';
2
+ import type { GridRowModel, GridFilterItem, GridFilterModel, GridFilterGroup, GridFilterOperator, GridColDef } from '../../types';
3
+ import type { GridColumnLookup } from '../columnLookup';
4
+ import { getCellValue, getFormattedValue, toLocalDateString, toLocalDayKey, toNumber } from '../values';
3
5
 
4
- export const FILTER_OPERATORS = {
5
- contains: (value: unknown, filterValue: unknown): boolean => {
6
+ /** Compares a cell value with the filter item's value. */
7
+ export type GridFilterOperatorFn = (value: unknown, filterValue?: unknown) => boolean;
8
+
9
+ type RowPredicate = (row: GridRowModel) => boolean;
10
+
11
+ /** Operators that take no value. Every other operator ignores an item whose value is empty. */
12
+ export const NO_VALUE_OPERATORS: ReadonlySet<string> = new Set<GridFilterOperator>(['isEmpty', 'isNotEmpty']);
13
+
14
+ /**
15
+ * True for a filter value that means "nothing entered yet": undefined, null, a blank string or an
16
+ * empty array. Items with such a value (other than isEmpty / isNotEmpty) do not filter.
17
+ */
18
+ export function isEmptyFilterValue(value: unknown): boolean {
19
+ if (value == null) return true;
20
+ if (typeof value === 'string') return value.trim() === '';
21
+ if (Array.isArray(value)) return value.length === 0;
22
+ return false;
23
+ }
24
+
25
+ function isBlankCell(value: unknown): boolean {
26
+ if (value == null) return true;
27
+ if (typeof value === 'number') return Number.isNaN(value);
28
+ return typeof value === 'string' && value.trim() === '';
29
+ }
30
+
31
+ const numeric = (compare: (a: number, b: number) => boolean): GridFilterOperatorFn => (value, filterValue) => {
32
+ const a = toNumber(value);
33
+ const b = toNumber(filterValue);
34
+ if (a === null || b === null) return false;
35
+ return compare(a, b);
36
+ };
37
+
38
+ const byDay = (compare: (a: number, b: number) => boolean): GridFilterOperatorFn => (value, filterValue) => {
39
+ const a = toLocalDayKey(value);
40
+ const b = toLocalDayKey(filterValue);
41
+ if (a === null || b === null) return false;
42
+ return compare(a, b);
43
+ };
44
+
45
+ /** Same local calendar day. Accepts Date objects, epoch milliseconds and date strings on both sides. */
46
+ const dateIs: GridFilterOperatorFn = byDay((a, b) => a === b);
47
+
48
+ /** A different local calendar day. Empty or unparsable cells count as "not that day". */
49
+ const dateNot: GridFilterOperatorFn = (value, filterValue) => {
50
+ const a = toLocalDayKey(value);
51
+ const b = toLocalDayKey(filterValue);
52
+ if (a === null || b === null) return true;
53
+ return a !== b;
54
+ };
55
+
56
+ export const FILTER_OPERATORS: Record<GridFilterOperator, GridFilterOperatorFn> = {
57
+ contains: (value, filterValue) => {
6
58
  if (value == null || filterValue == null) return false;
7
59
  return String(value).toLowerCase().includes(String(filterValue).toLowerCase());
8
60
  },
9
61
 
10
- equals: (value: unknown, filterValue: unknown): boolean => {
62
+ equals: (value, filterValue) => {
11
63
  if (value == null && filterValue == null) return true;
12
64
  if (value == null || filterValue == null) return false;
13
65
  return String(value).toLowerCase() === String(filterValue).toLowerCase();
14
66
  },
15
67
 
16
- startsWith: (value: unknown, filterValue: unknown): boolean => {
68
+ startsWith: (value, filterValue) => {
17
69
  if (value == null || filterValue == null) return false;
18
70
  return String(value).toLowerCase().startsWith(String(filterValue).toLowerCase());
19
71
  },
20
72
 
21
- endsWith: (value: unknown, filterValue: unknown): boolean => {
73
+ endsWith: (value, filterValue) => {
22
74
  if (value == null || filterValue == null) return false;
23
75
  return String(value).toLowerCase().endsWith(String(filterValue).toLowerCase());
24
76
  },
25
77
 
26
- isEmpty: (value: unknown): boolean => {
78
+ isEmpty: (value) => {
27
79
  return value == null || String(value).trim() === '';
28
80
  },
29
81
 
30
- isNotEmpty: (value: unknown): boolean => {
82
+ isNotEmpty: (value) => {
31
83
  return value != null && String(value).trim() !== '';
32
84
  },
33
85
 
34
- '>': (value: unknown, filterValue: unknown): boolean => {
35
- if (value == null || filterValue == null) return false;
36
- const numValue = Number(value);
37
- const numFilter = Number(filterValue);
38
- if (isNaN(numValue) || isNaN(numFilter)) return false;
39
- return numValue > numFilter;
40
- },
41
-
42
- '>=': (value: unknown, filterValue: unknown): boolean => {
43
- if (value == null || filterValue == null) return false;
44
- const numValue = Number(value);
45
- const numFilter = Number(filterValue);
46
- if (isNaN(numValue) || isNaN(numFilter)) return false;
47
- return numValue >= numFilter;
86
+ // Numeric operators: blank cells and non-numeric values never match (a blank cell is not 0).
87
+ '=': numeric((a, b) => a === b),
88
+ '>': numeric((a, b) => a > b),
89
+ '>=': numeric((a, b) => a >= b),
90
+ '<': numeric((a, b) => a < b),
91
+ '<=': numeric((a, b) => a <= b),
92
+
93
+ '!=': (value, filterValue) => {
94
+ if (isEmptyFilterValue(filterValue)) return true;
95
+ const a = toNumber(value);
96
+ const b = toNumber(filterValue);
97
+ if (a !== null && b !== null) return a !== b;
98
+ // An empty cell is "not equal" to every value, whatever that value is.
99
+ if (isBlankCell(value)) return true;
100
+ return String(value).toLowerCase() !== String(filterValue).toLowerCase();
48
101
  },
49
102
 
50
- '<': (value: unknown, filterValue: unknown): boolean => {
51
- if (value == null || filterValue == null) return false;
52
- const numValue = Number(value);
53
- const numFilter = Number(filterValue);
54
- if (isNaN(numValue) || isNaN(numFilter)) return false;
55
- return numValue < numFilter;
103
+ // A scalar value is treated as a one-element list, so a typed "Active" works like ["Active"].
104
+ isAnyOf: (value, filterValue) => {
105
+ if (value == null) return false;
106
+ const options = Array.isArray(filterValue) ? filterValue : [filterValue];
107
+ const cell = String(value).toLowerCase();
108
+ return options.some(option => option != null && String(option).toLowerCase() === cell);
56
109
  },
57
110
 
58
- '<=': (value: unknown, filterValue: unknown): boolean => {
59
- if (value == null || filterValue == null) return false;
60
- const numValue = Number(value);
61
- const numFilter = Number(filterValue);
62
- if (isNaN(numValue) || isNaN(numFilter)) return false;
63
- return numValue <= numFilter;
111
+ // Used by boolean and singleSelect (case-insensitive string comparison), and by dates.
112
+ not: (value, filterValue) => {
113
+ if (value instanceof Date) return dateNot(value, filterValue);
114
+ if (value == null && filterValue == null) return false;
115
+ if (value == null || filterValue == null) return true;
116
+ return String(value).toLowerCase() !== String(filterValue).toLowerCase();
64
117
  },
65
118
 
66
- '!=': (value: unknown, filterValue: unknown): boolean => {
119
+ is: (value, filterValue) => {
67
120
  if (filterValue == null || filterValue === '') return true;
68
- // Numeric comparison if both are numbers, otherwise string
69
- const numV = Number(value);
70
- const numF = Number(filterValue);
71
- if (!isNaN(numV) && !isNaN(numF)) return numV !== numF;
72
- return String(value).toLowerCase() !== String(filterValue).toLowerCase();
121
+ if (value instanceof Date) return dateIs(value, filterValue);
122
+ return String(value).toLowerCase() === String(filterValue).toLowerCase();
73
123
  },
74
124
 
75
- isAnyOf: (value: unknown, filterValue: unknown[]): boolean => {
76
- if (value == null || !Array.isArray(filterValue)) return false;
77
- return filterValue.some(v =>
78
- String(value).toLowerCase() === String(v).toLowerCase()
79
- );
80
- },
125
+ // Date operators compare local calendar days.
126
+ after: byDay((a, b) => a > b),
127
+ onOrAfter: byDay((a, b) => a >= b),
128
+ before: byDay((a, b) => a < b),
129
+ onOrBefore: byDay((a, b) => a <= b),
130
+ };
81
131
 
82
- not: (value: unknown, filterValue: unknown): boolean => {
83
- if (value == null && filterValue == null) return false;
84
- if (value == null || filterValue == null) return true;
85
- return String(value).toLowerCase() !== String(filterValue).toLowerCase();
86
- },
132
+ /** Date columns compare `is` / `not` by calendar day, so ISO strings and Date objects match the typed day. */
133
+ const DATE_COLUMN_OPERATORS: Partial<Record<GridFilterOperator, GridFilterOperatorFn>> = {
134
+ is: dateIs,
135
+ not: dateNot,
136
+ };
87
137
 
88
- // Used by boolean and singleSelect — case-insensitive string comparison
89
- is: (value: unknown, filterValue: unknown): boolean => {
90
- if (filterValue == null || filterValue === '') return true;
91
- const rowStr = String(value).toLowerCase();
92
- const filterStr = String(filterValue).toLowerCase();
93
- return rowStr === filterStr;
94
- },
138
+ function resolveOperator(operator: string, colDef: GridColDef | undefined): GridFilterOperatorFn | undefined {
139
+ if (!Object.prototype.hasOwnProperty.call(FILTER_OPERATORS, operator)) return undefined;
140
+ const op = operator as GridFilterOperator;
141
+ if (colDef?.type === 'date') {
142
+ const dateFn = DATE_COLUMN_OPERATORS[op];
143
+ if (dateFn) return dateFn;
144
+ }
145
+ return FILTER_OPERATORS[op];
146
+ }
95
147
 
96
- '=': (value: unknown, filterValue: unknown): boolean => {
97
- if (value == null || filterValue == null || filterValue === '') return false;
98
- return Number(value) === Number(filterValue);
99
- },
100
- };
148
+ function isFilterGroup(item: GridFilterItem | GridFilterGroup): item is GridFilterGroup {
149
+ return 'items' in item && Array.isArray(item.items);
150
+ }
101
151
 
102
- /**
103
- * Apply a single filter item to a row
104
- */
105
- export function applyFilterItem<R extends GridRowModel>(
106
- row: R,
107
- filterItem: GridFilterItem
108
- ): boolean {
109
- const { field, operator, value } = filterItem;
152
+ interface CompileContext {
153
+ columns?: GridColumnLookup;
154
+ /** Unknown operators already reported in this pass, so the warning fires once per pass, not per row. */
155
+ warned: Set<string>;
156
+ }
110
157
 
111
- // Get the value from the row
112
- const rowValue = row[field];
158
+ function compileItem(item: GridFilterItem, ctx: CompileContext): RowPredicate | null {
159
+ const { field, operator, value } = item;
160
+ // An item whose value has not been entered yet (or was cleared) does not filter.
161
+ if (!NO_VALUE_OPERATORS.has(operator) && isEmptyFilterValue(value)) return null;
113
162
 
114
- // Get the operator function
115
- const operatorFn = FILTER_OPERATORS[operator];
163
+ const colDef = ctx.columns?.byField.get(field);
164
+ const operatorFn = resolveOperator(operator, colDef);
116
165
  if (!operatorFn) {
117
- console.warn(`Unknown filter operator: ${operator}`);
118
- return true;
166
+ if (!ctx.warned.has(operator)) {
167
+ ctx.warned.add(operator);
168
+ console.warn(`Unknown filter operator: ${operator}`);
169
+ }
170
+ return null;
171
+ }
172
+ return (row) => operatorFn(getCellValue(row, field, colDef), value);
173
+ }
174
+
175
+ function compileGroup(
176
+ items: (GridFilterItem | GridFilterGroup)[],
177
+ logicOperator: 'and' | 'or',
178
+ ctx: CompileContext
179
+ ): RowPredicate | null {
180
+ const predicates: RowPredicate[] = [];
181
+ for (const item of items) {
182
+ const predicate = isFilterGroup(item)
183
+ ? compileGroup(item.items, item.logicOperator, ctx)
184
+ : compileItem(item, ctx);
185
+ if (predicate) predicates.push(predicate);
186
+ }
187
+ if (predicates.length === 0) return null;
188
+ if (predicates.length === 1) return predicates[0];
189
+ return logicOperator === 'or'
190
+ ? (row) => predicates.some(p => p(row))
191
+ : (row) => predicates.every(p => p(row));
192
+ }
193
+
194
+ function pushSearchText(out: string[], value: unknown): void {
195
+ if (value == null) return;
196
+ switch (typeof value) {
197
+ case 'string':
198
+ out.push(value.toLowerCase());
199
+ return;
200
+ case 'number':
201
+ if (!Number.isNaN(value)) out.push(String(value));
202
+ return;
203
+ case 'boolean':
204
+ case 'bigint':
205
+ out.push(String(value));
206
+ return;
207
+ default:
208
+ break;
209
+ }
210
+ if (value instanceof Date) {
211
+ // Search the calendar day, not Date.toString()'s "Wed Jan 10 2024 ... GMT+0000".
212
+ const day = toLocalDateString(value);
213
+ if (day) out.push(day);
214
+ return;
119
215
  }
216
+ if (Array.isArray(value)) {
217
+ for (const entry of value) {
218
+ if (entry == null || typeof entry !== 'object') pushSearchText(out, entry);
219
+ }
220
+ }
221
+ // Other objects have no meaningful text ("[object Object]") and are skipped.
222
+ }
120
223
 
121
- // Apply the operator
122
- return (operatorFn as (a: unknown, b: unknown) => boolean)(rowValue, value);
224
+ function getQuickFilterTexts(row: GridRowModel, columns: GridColumnLookup | undefined): string[] {
225
+ const texts: string[] = [];
226
+ if (!columns) {
227
+ for (const value of Object.values(row)) pushSearchText(texts, value);
228
+ return texts;
229
+ }
230
+ for (const col of columns.quickFilterColumns) {
231
+ const value = getCellValue(row, col.field, col);
232
+ const formatted = getFormattedValue(row, col.field, value, col);
233
+ if (formatted != null && formatted !== '') texts.push(String(formatted).toLowerCase());
234
+ pushSearchText(texts, value);
235
+ }
236
+ return texts;
237
+ }
238
+
239
+ // Search texts per column lookup and row object. The grid keeps one lookup while the columns and
240
+ // hidden columns stay the same, so typing in the quick filter reads every cell once, not per keystroke.
241
+ const quickFilterTextCache = new WeakMap<GridColumnLookup, WeakMap<GridRowModel, string[]>>();
242
+
243
+ function getCachedQuickFilterTexts(row: GridRowModel, columns: GridColumnLookup | undefined): string[] {
244
+ if (!columns) return getQuickFilterTexts(row, columns);
245
+ let byRow = quickFilterTextCache.get(columns);
246
+ if (!byRow) {
247
+ byRow = new WeakMap();
248
+ quickFilterTextCache.set(columns, byRow);
249
+ }
250
+ let texts = byRow.get(row);
251
+ if (!texts) {
252
+ texts = getQuickFilterTexts(row, columns);
253
+ byRow.set(row, texts);
254
+ }
255
+ return texts;
256
+ }
257
+
258
+ function compileQuickFilter(values: readonly string[] | undefined, columns: GridColumnLookup | undefined): RowPredicate | null {
259
+ const terms = (values ?? [])
260
+ .map(value => String(value ?? '').trim().toLowerCase())
261
+ .filter(term => term !== '');
262
+ if (terms.length === 0) return null;
263
+ return (row) => {
264
+ const texts = getCachedQuickFilterTexts(row, columns);
265
+ return terms.every(term => texts.some(text => text.includes(term)));
266
+ };
123
267
  }
124
268
 
125
269
  /**
126
- * Apply quick filter (global search across all fields)
270
+ * Compile a filter model into a row predicate. Returns null when the model filters nothing
271
+ * (no items with a value and no quick-filter terms), so callers can skip the pass entirely.
272
+ *
273
+ * Pass `columns` so that cells are read through `valueGetter`, date columns use calendar-day
274
+ * comparison and the quick filter searches only visible, filterable columns (including their
275
+ * `valueFormatter` text). Without it, `row[field]` is read and the quick filter searches every
276
+ * primitive value in the row.
127
277
  */
128
- export function applyQuickFilter<R extends GridRowModel>(
278
+ export function createRowFilter(
279
+ filterModel: GridFilterModel | GridFilterGroup | null | undefined,
280
+ columns?: GridColumnLookup
281
+ ): RowPredicate | null {
282
+ if (!filterModel) return null;
283
+ const ctx: CompileContext = { columns, warned: new Set() };
284
+ const quickFilterValues = 'quickFilterValues' in filterModel ? filterModel.quickFilterValues : undefined;
285
+ const quick = compileQuickFilter(quickFilterValues, columns);
286
+ const itemsPredicate = compileGroup(filterModel.items ?? [], filterModel.logicOperator ?? 'and', ctx);
287
+ if (quick && itemsPredicate) return (row) => quick(row) && itemsPredicate(row);
288
+ return quick ?? itemsPredicate;
289
+ }
290
+
291
+ /**
292
+ * Apply a single filter item to a row
293
+ */
294
+ export function applyFilterItem<R extends GridRowModel>(
129
295
  row: R,
130
- quickFilterValues: string[]
296
+ filterItem: GridFilterItem,
297
+ colDef?: GridColDef
131
298
  ): boolean {
132
- if (!quickFilterValues || quickFilterValues.length === 0) {
133
- return true;
134
- }
135
-
136
- // Check if any quick filter value matches any field in the row
137
- return quickFilterValues.every(filterValue => {
138
- const searchTerm = filterValue.toLowerCase();
139
- return Object.values(row).some(value => {
140
- if (value == null) return false;
141
- return String(value).toLowerCase().includes(searchTerm);
142
- });
143
- });
299
+ const columns: GridColumnLookup | undefined = colDef
300
+ ? { byField: new Map([[filterItem.field, colDef]]), quickFilterColumns: [] }
301
+ : undefined;
302
+ const predicate = compileItem(filterItem, { columns, warned: new Set() });
303
+ return predicate ? predicate(row) : true;
144
304
  }
145
305
 
146
306
  /**
147
- * Filter rows based on filter model
307
+ * Apply the quick filter (global search). Every term must be found in at least one searched value.
148
308
  */
309
+ export function applyQuickFilter<R extends GridRowModel>(
310
+ row: R,
311
+ quickFilterValues: string[],
312
+ columns?: GridColumnLookup
313
+ ): boolean {
314
+ const predicate = compileQuickFilter(quickFilterValues, columns);
315
+ return predicate ? predicate(row) : true;
316
+ }
317
+
149
318
  /**
150
- * Check if a row matches the filter model
319
+ * Check if a row matches the filter model. Compiles the model on every call: to test many rows,
320
+ * build a predicate once with createRowFilter.
151
321
  */
152
322
  export function isRowMatchingFilter<R extends GridRowModel>(
153
323
  row: R,
154
- filterModel: GridFilterModel | GridFilterGroup // Accept Model or Group (similar structure)
324
+ filterModel: GridFilterModel | GridFilterGroup,
325
+ columns?: GridColumnLookup
155
326
  ): boolean {
156
- // Normalize input: GridFilterModel matches GridFilterGroup shape largely, but let's be safe
157
-
158
- const items = filterModel.items ?? [];
159
- const logicOperator = filterModel.logicOperator ?? 'and';
160
- const quickFilterValues = ('quickFilterValues' in filterModel ? filterModel.quickFilterValues : undefined) ?? [];
161
-
162
- if (quickFilterValues.length > 0 && !applyQuickFilter(row, quickFilterValues)) {
163
- return false;
164
- }
165
-
166
- if (items.length === 0) {
167
- return true;
168
- }
169
-
170
- const evaluateItem = (item: GridFilterItem | GridFilterGroup): boolean => {
171
- if ('logicOperator' in item && 'items' in item) {
172
-
173
- const group = item as GridFilterGroup;
174
- const groupItems = group.items;
175
- if (groupItems.length === 0) return true;
176
-
177
- if (group.logicOperator === 'and') {
178
- return groupItems.every(evaluateItem);
179
- } else {
180
- return groupItems.some(evaluateItem);
181
- }
182
- } else {
183
-
184
- return applyFilterItem(row, item as GridFilterItem);
185
- }
186
- };
187
-
188
- if (logicOperator === 'and') {
189
-
190
- return items.every(evaluateItem);
191
- } else {
192
-
193
- return items.some(evaluateItem);
194
- }
327
+ const predicate = createRowFilter(filterModel, columns);
328
+ return predicate ? predicate(row) : true;
195
329
  }
196
330
 
197
331
  export function filterRows<R extends GridRowModel>(
198
332
  rows: R[],
199
- filterModel: GridFilterModel
333
+ filterModel: GridFilterModel,
334
+ columns?: GridColumnLookup
200
335
  ): R[] {
201
- if (!filterModel) return rows;
202
-
203
- const { items = [], quickFilterValues = [] } = filterModel;
204
-
205
- if (items.length === 0 && (!quickFilterValues || quickFilterValues.length === 0)) {
206
- return rows;
207
- }
208
-
209
- return rows.filter(row => isRowMatchingFilter(row, filterModel));
336
+ const predicate = createRowFilter(filterModel, columns);
337
+ if (!predicate) return rows;
338
+ return rows.filter(row => predicate(row));
210
339
  }
211
340
 
212
- export function getDefaultOperator(type?: string): string {
341
+ export function getDefaultOperator(type?: string): GridFilterOperator {
213
342
  switch (type) {
214
343
  case 'number':
215
344
  return '=';
@@ -224,7 +353,7 @@ export function getDefaultOperator(type?: string): string {
224
353
  }
225
354
  }
226
355
 
227
- export function getOperatorsForType(type?: string): string[] {
356
+ export function getOperatorsForType(type?: string): GridFilterOperator[] {
228
357
  switch (type) {
229
358
  case 'number':
230
359
  return ['=', '!=', '>', '>=', '<', '<=', 'isEmpty', 'isNotEmpty'];