@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,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
|
-
|
|
5
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
78
|
+
isEmpty: (value) => {
|
|
27
79
|
return value == null || String(value).trim() === '';
|
|
28
80
|
},
|
|
29
81
|
|
|
30
|
-
isNotEmpty: (value
|
|
82
|
+
isNotEmpty: (value) => {
|
|
31
83
|
return value != null && String(value).trim() !== '';
|
|
32
84
|
},
|
|
33
85
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
return
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
return
|
|
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
|
-
|
|
119
|
+
is: (value, filterValue) => {
|
|
67
120
|
if (filterValue == null || filterValue === '') return true;
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
104
|
-
*/
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
112
|
-
const
|
|
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
|
-
|
|
115
|
-
const operatorFn =
|
|
163
|
+
const colDef = ctx.columns?.byField.get(field);
|
|
164
|
+
const operatorFn = resolveOperator(operator, colDef);
|
|
116
165
|
if (!operatorFn) {
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
296
|
+
filterItem: GridFilterItem,
|
|
297
|
+
colDef?: GridColDef
|
|
131
298
|
): boolean {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
*
|
|
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
|
|
324
|
+
filterModel: GridFilterModel | GridFilterGroup,
|
|
325
|
+
columns?: GridColumnLookup
|
|
155
326
|
): boolean {
|
|
156
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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):
|
|
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):
|
|
356
|
+
export function getOperatorsForType(type?: string): GridFilterOperator[] {
|
|
228
357
|
switch (type) {
|
|
229
358
|
case 'number':
|
|
230
359
|
return ['=', '!=', '>', '>=', '<', '<=', 'isEmpty', 'isNotEmpty'];
|