@tanstack/table-core 9.0.0-beta.45 → 9.0.0-beta.47

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 (116) hide show
  1. package/dist/features/column-filtering/columnFilteringFeature.types.d.cts +43 -3
  2. package/dist/features/column-filtering/columnFilteringFeature.types.d.ts +43 -3
  3. package/dist/features/column-filtering/columnFilteringFeature.utils.cjs +18 -9
  4. package/dist/features/column-filtering/columnFilteringFeature.utils.cjs.map +1 -1
  5. package/dist/features/column-filtering/columnFilteringFeature.utils.d.cts +6 -2
  6. package/dist/features/column-filtering/columnFilteringFeature.utils.d.ts +6 -2
  7. package/dist/features/column-filtering/columnFilteringFeature.utils.js +18 -9
  8. package/dist/features/column-filtering/columnFilteringFeature.utils.js.map +1 -1
  9. package/dist/features/column-filtering/createFilteredRowModel.cjs +7 -2
  10. package/dist/features/column-filtering/createFilteredRowModel.cjs.map +1 -1
  11. package/dist/features/column-filtering/createFilteredRowModel.d.cts +6 -2
  12. package/dist/features/column-filtering/createFilteredRowModel.d.ts +6 -2
  13. package/dist/features/column-filtering/createFilteredRowModel.js +7 -2
  14. package/dist/features/column-filtering/createFilteredRowModel.js.map +1 -1
  15. package/dist/features/column-grouping/columnGroupingFeature.types.d.cts +41 -3
  16. package/dist/features/column-grouping/columnGroupingFeature.types.d.ts +41 -3
  17. package/dist/features/column-grouping/columnGroupingFeature.utils.cjs +17 -3
  18. package/dist/features/column-grouping/columnGroupingFeature.utils.cjs.map +1 -1
  19. package/dist/features/column-grouping/columnGroupingFeature.utils.d.cts +5 -0
  20. package/dist/features/column-grouping/columnGroupingFeature.utils.d.ts +5 -0
  21. package/dist/features/column-grouping/columnGroupingFeature.utils.js +17 -3
  22. package/dist/features/column-grouping/columnGroupingFeature.utils.js.map +1 -1
  23. package/dist/features/column-grouping/createGroupedRowModel.cjs +6 -3
  24. package/dist/features/column-grouping/createGroupedRowModel.cjs.map +1 -1
  25. package/dist/features/column-grouping/createGroupedRowModel.d.cts +6 -3
  26. package/dist/features/column-grouping/createGroupedRowModel.d.ts +6 -3
  27. package/dist/features/column-grouping/createGroupedRowModel.js +6 -3
  28. package/dist/features/column-grouping/createGroupedRowModel.js.map +1 -1
  29. package/dist/features/global-filtering/globalFilteringFeature.utils.cjs +3 -1
  30. package/dist/features/global-filtering/globalFilteringFeature.utils.cjs.map +1 -1
  31. package/dist/features/global-filtering/globalFilteringFeature.utils.d.cts +2 -6
  32. package/dist/features/global-filtering/globalFilteringFeature.utils.d.ts +2 -6
  33. package/dist/features/global-filtering/globalFilteringFeature.utils.js +3 -1
  34. package/dist/features/global-filtering/globalFilteringFeature.utils.js.map +1 -1
  35. package/dist/features/row-sorting/createSortedRowModel.cjs +6 -2
  36. package/dist/features/row-sorting/createSortedRowModel.cjs.map +1 -1
  37. package/dist/features/row-sorting/createSortedRowModel.d.cts +6 -2
  38. package/dist/features/row-sorting/createSortedRowModel.d.ts +6 -2
  39. package/dist/features/row-sorting/createSortedRowModel.js +6 -2
  40. package/dist/features/row-sorting/createSortedRowModel.js.map +1 -1
  41. package/dist/features/row-sorting/rowSortingFeature.types.d.cts +31 -2
  42. package/dist/features/row-sorting/rowSortingFeature.types.d.ts +31 -2
  43. package/dist/features/row-sorting/rowSortingFeature.utils.cjs +19 -4
  44. package/dist/features/row-sorting/rowSortingFeature.utils.cjs.map +1 -1
  45. package/dist/features/row-sorting/rowSortingFeature.utils.js +19 -4
  46. package/dist/features/row-sorting/rowSortingFeature.utils.js.map +1 -1
  47. package/dist/fns/aggregationFns.cjs +128 -57
  48. package/dist/fns/aggregationFns.cjs.map +1 -1
  49. package/dist/fns/aggregationFns.d.cts +72 -18
  50. package/dist/fns/aggregationFns.d.ts +72 -18
  51. package/dist/fns/aggregationFns.js +126 -58
  52. package/dist/fns/aggregationFns.js.map +1 -1
  53. package/dist/fns/filterFns.cjs +254 -97
  54. package/dist/fns/filterFns.cjs.map +1 -1
  55. package/dist/fns/filterFns.d.cts +126 -94
  56. package/dist/fns/filterFns.d.ts +126 -94
  57. package/dist/fns/filterFns.js +247 -98
  58. package/dist/fns/filterFns.js.map +1 -1
  59. package/dist/fns/sortFns.cjs +68 -21
  60. package/dist/fns/sortFns.cjs.map +1 -1
  61. package/dist/fns/sortFns.d.cts +48 -15
  62. package/dist/fns/sortFns.d.ts +48 -15
  63. package/dist/fns/sortFns.js +68 -22
  64. package/dist/fns/sortFns.js.map +1 -1
  65. package/dist/helpers/tableFeatures.cjs +5 -4
  66. package/dist/helpers/tableFeatures.cjs.map +1 -1
  67. package/dist/helpers/tableFeatures.d.cts +5 -4
  68. package/dist/helpers/tableFeatures.d.ts +5 -4
  69. package/dist/helpers/tableFeatures.js +5 -4
  70. package/dist/helpers/tableFeatures.js.map +1 -1
  71. package/dist/index.cjs +12 -0
  72. package/dist/index.d.cts +8 -8
  73. package/dist/index.d.ts +8 -8
  74. package/dist/index.js +4 -4
  75. package/dist/types/TableFeatures.d.cts +15 -7
  76. package/dist/types/TableFeatures.d.ts +15 -7
  77. package/dist/types/type-utils.d.cts +8 -1
  78. package/dist/types/type-utils.d.ts +8 -1
  79. package/package.json +1 -1
  80. package/skills/api-not-found/SKILL.md +1 -1
  81. package/skills/client-vs-server/SKILL.md +1 -1
  82. package/skills/column-faceting/SKILL.md +11 -5
  83. package/skills/column-filtering/SKILL.md +8 -3
  84. package/skills/column-ordering/SKILL.md +1 -1
  85. package/skills/column-pinning/SKILL.md +1 -1
  86. package/skills/column-resizing/SKILL.md +1 -1
  87. package/skills/column-sizing/SKILL.md +1 -1
  88. package/skills/column-visibility/SKILL.md +1 -1
  89. package/skills/core/SKILL.md +1 -1
  90. package/skills/custom-features/SKILL.md +1 -1
  91. package/skills/expanding/SKILL.md +1 -1
  92. package/skills/global-filtering/SKILL.md +3 -3
  93. package/skills/grouping/SKILL.md +8 -3
  94. package/skills/migrate-v8-to-v9/SKILL.md +7 -7
  95. package/skills/pagination/SKILL.md +1 -1
  96. package/skills/row-pinning/SKILL.md +1 -1
  97. package/skills/row-selection/SKILL.md +1 -1
  98. package/skills/sorting/SKILL.md +9 -3
  99. package/skills/table-features/SKILL.md +16 -13
  100. package/skills/typescript/SKILL.md +1 -1
  101. package/src/features/column-filtering/columnFilteringFeature.types.ts +60 -0
  102. package/src/features/column-filtering/columnFilteringFeature.utils.ts +33 -19
  103. package/src/features/column-filtering/createFilteredRowModel.ts +13 -3
  104. package/src/features/column-grouping/columnGroupingFeature.types.ts +63 -8
  105. package/src/features/column-grouping/columnGroupingFeature.utils.ts +43 -9
  106. package/src/features/column-grouping/createGroupedRowModel.ts +6 -3
  107. package/src/features/global-filtering/globalFilteringFeature.utils.ts +8 -0
  108. package/src/features/row-sorting/createSortedRowModel.ts +6 -2
  109. package/src/features/row-sorting/rowSortingFeature.types.ts +53 -1
  110. package/src/features/row-sorting/rowSortingFeature.utils.ts +45 -13
  111. package/src/fns/aggregationFns.ts +204 -146
  112. package/src/fns/filterFns.ts +361 -280
  113. package/src/fns/sortFns.ts +95 -78
  114. package/src/helpers/tableFeatures.ts +5 -4
  115. package/src/types/TableFeatures.ts +15 -7
  116. package/src/types/type-utils.ts +8 -0
@@ -1,7 +1,72 @@
1
+ import type {
2
+ CreatedFilterFn,
3
+ ExtractFilterMeta,
4
+ FilterFnDef,
5
+ } from '../features/column-filtering/columnFilteringFeature.types'
1
6
  import type { RowData } from '../types/type-utils'
2
7
  import type { TableFeatures } from '../types/TableFeatures'
3
8
  import type { Row } from '../types/Row'
4
9
 
10
+ /**
11
+ * Builds a `FilterFn` from a value-level comparator plus optional resolvers.
12
+ *
13
+ * The `filter` comparator receives the row's data value (already passed
14
+ * through `resolveDataValue` when one is defined) and the filter value
15
+ * (already passed through `resolveFilterValue` by the table). Keeping
16
+ * normalization in the resolvers means a variant of an existing filter
17
+ * function only has to swap the resolvers, not re-implement the comparison.
18
+ *
19
+ * The definition is attached to the returned function, so a variant can be
20
+ * created by spreading a built-in filter function and overriding what differs:
21
+ *
22
+ * ```ts
23
+ * const normalize = (value: unknown) =>
24
+ * String(value ?? '')
25
+ * .toLowerCase()
26
+ * .normalize('NFD')
27
+ * .replace(/\p{Diacritic}/gu, '')
28
+ *
29
+ * const includesStringIgnoreDiacritics = constructFilterFn({
30
+ * ...filterFn_includesString,
31
+ * resolveFilterValue: normalize,
32
+ * resolveDataValue: normalize,
33
+ * })
34
+ * ```
35
+ *
36
+ * Note: the table applies `resolveFilterValue` once per filter before any rows
37
+ * are tested. When calling a filter function directly (outside of a table),
38
+ * apply it yourself: `fn(row, columnId, fn.resolveFilterValue?.(value) ?? value)`.
39
+ */
40
+ export function constructFilterFn<
41
+ TFeatures extends TableFeatures = any,
42
+ TData extends RowData = any,
43
+ >(def: FilterFnDef<TFeatures, TData>): CreatedFilterFn<TFeatures, TData> {
44
+ const filterFn: CreatedFilterFn<TFeatures, TData> = Object.assign(
45
+ <TRowFeatures extends TableFeatures, TRowData extends RowData>(
46
+ row: Row<TRowFeatures, TRowData>,
47
+ columnId: string,
48
+ filterValue: any,
49
+ addMeta?: (meta: ExtractFilterMeta<TRowFeatures>) => void,
50
+ ): boolean => {
51
+ const rawValue = row.getValue(columnId)
52
+ const dataValue = filterFn.resolveDataValue
53
+ ? filterFn.resolveDataValue(rawValue)
54
+ : rawValue
55
+ // The def's comparator is typed against the factory's TFeatures/TData;
56
+ // the caller's row generics are erased at this boundary.
57
+ return filterFn.filter(
58
+ dataValue,
59
+ filterValue,
60
+ row as any,
61
+ columnId,
62
+ addMeta as any,
63
+ )
64
+ },
65
+ def,
66
+ )
67
+ return filterFn
68
+ }
69
+
5
70
  // Basic filters
6
71
 
7
72
  /**
@@ -9,16 +74,10 @@ import type { Row } from '../types/Row'
9
74
  *
10
75
  * Uses JavaScript `===` comparison and auto-removes empty filter values.
11
76
  */
12
- export const filterFn_equals = Object.assign(
13
- <TFeatures extends TableFeatures, TData extends RowData>(
14
- row: Row<TFeatures, TData>,
15
- columnId: string,
16
- filterValue: unknown,
17
- ) => {
18
- return row.getValue(columnId) === filterValue
19
- },
20
- { autoRemove: (val: any) => testFalsy(val) },
21
- )
77
+ export const filterFn_equals = constructFilterFn({
78
+ filter: (dataValue, filterValue) => dataValue === filterValue,
79
+ autoRemove: (val: any) => testFalsy(val),
80
+ })
22
81
 
23
82
  /**
24
83
  * Keeps rows whose column value is loosely equal to the filter value.
@@ -26,16 +85,10 @@ export const filterFn_equals = Object.assign(
26
85
  * Uses JavaScript `==` comparison and auto-removes empty filter values. This is
27
86
  * useful for matching string input against numeric row values.
28
87
  */
29
- export const filterFn_weakEquals = Object.assign(
30
- <TFeatures extends TableFeatures, TData extends RowData>(
31
- row: Row<TFeatures, TData>,
32
- columnId: string,
33
- filterValue: unknown,
34
- ) => {
35
- return row.getValue(columnId) == filterValue
36
- },
37
- { autoRemove: (val: any) => testFalsy(val) },
38
- )
88
+ export const filterFn_weakEquals = constructFilterFn({
89
+ filter: (dataValue, filterValue) => dataValue == filterValue,
90
+ autoRemove: (val: any) => testFalsy(val),
91
+ })
39
92
 
40
93
  // String filters
41
94
 
@@ -44,21 +97,12 @@ export const filterFn_weakEquals = Object.assign(
44
97
  *
45
98
  * Matching is case-sensitive and empty filter values are auto-removed.
46
99
  */
47
- export const filterFn_includesStringSensitive = Object.assign(
48
- <TFeatures extends TableFeatures, TData extends RowData>(
49
- row: Row<TFeatures, TData>,
50
- columnId: string,
51
- filterValue: unknown,
52
- ) => {
53
- return Boolean(
54
- row.getValue(columnId)?.toString().includes(String(filterValue)),
55
- )
56
- },
57
- {
58
- autoRemove: (val: any) => testFalsy(val),
59
- resolveFilterValue: (val: any) => String(val),
60
- },
61
- )
100
+ export const filterFn_includesStringSensitive = constructFilterFn({
101
+ filter: (dataValue, filterValue) => Boolean(dataValue?.includes(filterValue)),
102
+ autoRemove: (val: any) => testFalsy(val),
103
+ resolveFilterValue: (val: any) => String(val),
104
+ resolveDataValue: (val: any) => (val == null ? undefined : String(val)),
105
+ })
62
106
 
63
107
  /**
64
108
  * Keeps rows whose stringified column value includes the filter text.
@@ -66,25 +110,13 @@ export const filterFn_includesStringSensitive = Object.assign(
66
110
  * Both values are lowercased before comparison, and empty filter values are
67
111
  * auto-removed.
68
112
  */
69
- export const filterFn_includesString = Object.assign(
70
- <TFeatures extends TableFeatures, TData extends RowData>(
71
- row: Row<TFeatures, TData>,
72
- columnId: string,
73
- filterValue: unknown,
74
- ) => {
75
- return Boolean(
76
- row
77
- .getValue(columnId)
78
- ?.toString()
79
- .toLowerCase()
80
- .includes(String(filterValue).toLowerCase()),
81
- )
82
- },
83
- {
84
- autoRemove: (val: any) => testFalsy(val),
85
- resolveFilterValue: (val: any) => String(val).toLowerCase(),
86
- },
87
- )
113
+ export const filterFn_includesString = constructFilterFn({
114
+ filter: (dataValue, filterValue) => Boolean(dataValue?.includes(filterValue)),
115
+ autoRemove: (val: any) => testFalsy(val),
116
+ resolveFilterValue: (val: any) => String(val).toLowerCase(),
117
+ resolveDataValue: (val: any) =>
118
+ val == null ? undefined : String(val).toLowerCase(),
119
+ })
88
120
 
89
121
  /**
90
122
  * Keeps rows whose stringified column value equals the filter text.
@@ -92,41 +124,80 @@ export const filterFn_includesString = Object.assign(
92
124
  * Both values are lowercased before comparison, and empty filter values are
93
125
  * auto-removed.
94
126
  */
95
- export const filterFn_equalsString = Object.assign(
96
- <TFeatures extends TableFeatures, TData extends RowData>(
97
- row: Row<TFeatures, TData>,
98
- columnId: string,
99
- filterValue: unknown,
100
- ) => {
101
- return (
102
- row.getValue(columnId)?.toString().toLowerCase() ===
103
- String(filterValue).toLowerCase()
104
- )
105
- },
106
- {
107
- autoRemove: (val: any) => testFalsy(val),
108
- resolveFilterValue: (val: any) => String(val).toLowerCase(),
109
- },
110
- )
127
+ export const filterFn_equalsString = constructFilterFn({
128
+ filter: (dataValue, filterValue) => dataValue === filterValue,
129
+ autoRemove: (val: any) => testFalsy(val),
130
+ resolveFilterValue: (val: any) => String(val).toLowerCase(),
131
+ resolveDataValue: (val: any) =>
132
+ val == null ? undefined : String(val).toLowerCase(),
133
+ })
111
134
 
112
135
  /**
113
136
  * Keeps rows whose stringified column value exactly equals the filter text.
114
137
  *
115
138
  * Matching is case-sensitive and empty filter values are auto-removed.
116
139
  */
117
- export const filterFn_equalsStringSensitive = Object.assign(
118
- <TFeatures extends TableFeatures, TData extends RowData>(
119
- row: Row<TFeatures, TData>,
120
- columnId: string,
121
- filterValue: unknown,
122
- ) => {
123
- return row.getValue(columnId)?.toString() === String(filterValue)
124
- },
125
- {
126
- autoRemove: (val: any) => testFalsy(val),
127
- resolveFilterValue: (val: any) => String(val),
128
- },
129
- )
140
+ export const filterFn_equalsStringSensitive = constructFilterFn({
141
+ filter: (dataValue, filterValue) => dataValue === filterValue,
142
+ autoRemove: (val: any) => testFalsy(val),
143
+ resolveFilterValue: (val: any) => String(val),
144
+ resolveDataValue: (val: any) => (val == null ? undefined : String(val)),
145
+ })
146
+
147
+ /**
148
+ * Keeps rows whose stringified column value starts with the filter text.
149
+ *
150
+ * Both values are lowercased before comparison, and empty filter values are
151
+ * auto-removed.
152
+ */
153
+ export const filterFn_startsWith = constructFilterFn({
154
+ filter: (dataValue, filterValue) =>
155
+ Boolean(dataValue?.startsWith(filterValue)),
156
+ autoRemove: (val: any) => testFalsy(val),
157
+ resolveFilterValue: (val: any) => String(val).toLowerCase(),
158
+ resolveDataValue: (val: any) =>
159
+ val == null ? undefined : String(val).toLowerCase(),
160
+ })
161
+
162
+ /**
163
+ * Keeps rows whose stringified column value ends with the filter text.
164
+ *
165
+ * Both values are lowercased before comparison, and empty filter values are
166
+ * auto-removed.
167
+ */
168
+ export const filterFn_endsWith = constructFilterFn({
169
+ filter: (dataValue, filterValue) => Boolean(dataValue?.endsWith(filterValue)),
170
+ autoRemove: (val: any) => testFalsy(val),
171
+ resolveFilterValue: (val: any) => String(val).toLowerCase(),
172
+ resolveDataValue: (val: any) =>
173
+ val == null ? undefined : String(val).toLowerCase(),
174
+ })
175
+
176
+ // Blank filters
177
+
178
+ /**
179
+ * Keeps rows whose column value is empty.
180
+ *
181
+ * A value is empty when it is nullish or stringifies to whitespace only. The
182
+ * filter value acts as an on/off flag: `false` and blank values are
183
+ * auto-removed.
184
+ */
185
+ export const filterFn_empty = constructFilterFn({
186
+ filter: (dataValue) => testValueEmpty(dataValue),
187
+ autoRemove: (val: any) => testFalsy(val) || val === false,
188
+ })
189
+
190
+ /**
191
+ * Keeps rows whose column value is not empty.
192
+ *
193
+ * A value is empty when it is nullish or stringifies to whitespace only. The
194
+ * filter value acts as an on/off flag: `false` and blank values are
195
+ * auto-removed.
196
+ */
197
+ export const filterFn_notEmpty = constructFilterFn({
198
+ filter: (dataValue) => !testValueEmpty(dataValue),
199
+ autoRemove: (val: any) => testFalsy(val) || val === false,
200
+ })
130
201
 
131
202
  // Number filters
132
203
 
@@ -136,78 +207,44 @@ export const filterFn_equalsStringSensitive = Object.assign(
136
207
  * Numeric values are compared numerically when both sides can be coerced to
137
208
  * numbers; otherwise normalized strings are compared.
138
209
  */
139
- export const filterFn_greaterThan = Object.assign(
140
- <TFeatures extends TableFeatures, TData extends RowData>(
141
- row: Row<TFeatures, TData>,
142
- columnId: string,
143
- filterValue: unknown,
144
- ) => {
145
- const rowValue = row.getValue(columnId)
146
- const numericRowValue =
147
- rowValue === null || rowValue === undefined ? 0 : +rowValue
148
- const numericFilterValue = Number(filterValue)
149
-
150
- if (!isNaN(numericFilterValue) && !isNaN(numericRowValue)) {
151
- return numericRowValue > numericFilterValue
152
- }
153
-
154
- const stringValue = (rowValue ?? '').toString().toLowerCase().trim()
155
- const stringFilterValue = String(filterValue).toLowerCase().trim()
156
- return stringValue > stringFilterValue
157
- },
158
- { autoRemove: (val: any) => testFalsy(val) },
159
- )
210
+ export const filterFn_greaterThan = constructFilterFn({
211
+ filter: (dataValue, filterValue) =>
212
+ compareGreaterThan(dataValue, filterValue),
213
+ autoRemove: (val: any) => testFalsy(val),
214
+ })
160
215
 
161
216
  /**
162
217
  * Keeps rows whose value is greater than or equal to the filter value.
163
218
  *
164
- * Delegates to the built-in greater-than and equality comparisons.
219
+ * Delegates to the built-in greater-than and strict-equality comparisons.
165
220
  */
166
- export const filterFn_greaterThanOrEqualTo = Object.assign(
167
- <TFeatures extends TableFeatures, TData extends RowData>(
168
- row: Row<TFeatures, TData>,
169
- columnId: string,
170
- filterValue: unknown,
171
- ) => {
172
- return (
173
- filterFn_greaterThan(row, columnId, filterValue) ||
174
- filterFn_equals(row, columnId, filterValue)
175
- )
176
- },
177
- { autoRemove: (val: any) => testFalsy(val) },
178
- )
221
+ export const filterFn_greaterThanOrEqualTo = constructFilterFn({
222
+ filter: (dataValue, filterValue) =>
223
+ compareGreaterThanOrEqualTo(dataValue, filterValue),
224
+ autoRemove: (val: any) => testFalsy(val),
225
+ })
179
226
 
180
227
  /**
181
228
  * Keeps rows whose value is less than the filter value.
182
229
  *
183
230
  * This is implemented as the inverse of greater-than-or-equal comparison.
184
231
  */
185
- export const filterFn_lessThan = Object.assign(
186
- <TFeatures extends TableFeatures, TData extends RowData>(
187
- row: Row<TFeatures, TData>,
188
- columnId: string,
189
- filterValue: unknown,
190
- ) => {
191
- return !filterFn_greaterThanOrEqualTo(row, columnId, filterValue)
192
- },
193
- { autoRemove: (val: any) => testFalsy(val) },
194
- )
232
+ export const filterFn_lessThan = constructFilterFn({
233
+ filter: (dataValue, filterValue) =>
234
+ !compareGreaterThanOrEqualTo(dataValue, filterValue),
235
+ autoRemove: (val: any) => testFalsy(val),
236
+ })
195
237
 
196
238
  /**
197
239
  * Keeps rows whose value is less than or equal to the filter value.
198
240
  *
199
241
  * This is implemented as the inverse of greater-than comparison.
200
242
  */
201
- export const filterFn_lessThanOrEqualTo = Object.assign(
202
- <TFeatures extends TableFeatures, TData extends RowData>(
203
- row: Row<TFeatures, TData>,
204
- columnId: string,
205
- filterValue: unknown,
206
- ) => {
207
- return !filterFn_greaterThan(row, columnId, filterValue)
208
- },
209
- { autoRemove: (val: any) => testFalsy(val) },
210
- )
243
+ export const filterFn_lessThanOrEqualTo = constructFilterFn({
244
+ filter: (dataValue, filterValue) =>
245
+ !compareGreaterThan(dataValue, filterValue),
246
+ autoRemove: (val: any) => testFalsy(val),
247
+ })
211
248
 
212
249
  // Range filters
213
250
 
@@ -216,78 +253,26 @@ export const filterFn_lessThanOrEqualTo = Object.assign(
216
253
  *
217
254
  * Blank range endpoints are treated as open-ended.
218
255
  */
219
- const filterFn_between = Object.assign(
220
- <TFeatures extends TableFeatures, TData extends RowData>(
221
- row: Row<TFeatures, TData>,
222
- columnId: string,
223
- filterValues: [unknown, unknown],
224
- ): boolean => {
225
- const min = filterValues[0]
226
- if (min !== '' && min !== undefined) {
227
- if (!filterFn_greaterThan(row, columnId, min)) {
228
- return false
229
- }
230
- }
231
-
232
- const max = filterValues[1]
233
- if (max === '' || max === undefined) {
234
- return true
235
- }
236
-
237
- if (min !== '' && min !== undefined) {
238
- const numericMin = Number(min)
239
- const numericMax = Number(max)
240
- if (!isNaN(numericMin) && !isNaN(numericMax) && numericMin > numericMax) {
241
- return true
242
- }
243
- }
244
-
245
- return filterFn_lessThan(row, columnId, max)
246
- },
247
- {
248
- autoRemove: (val: any) =>
249
- testFalsy(val) || (testFalsy(val[0]) && testFalsy(val[1])),
250
- },
251
- )
256
+ export const filterFn_between = constructFilterFn({
257
+ filter: (dataValue, filterValues: [unknown, unknown]) =>
258
+ compareBetween(dataValue, filterValues, false),
259
+ autoRemove: (val: any) =>
260
+ testFalsy(val) ||
261
+ (Array.isArray(val) && testFalsy(val[0]) && testFalsy(val[1])),
262
+ })
252
263
 
253
264
  /**
254
265
  * Keeps rows whose value falls between an inclusive min/max pair.
255
266
  *
256
267
  * Blank range endpoints are treated as open-ended.
257
268
  */
258
- const filterFn_betweenInclusive = Object.assign(
259
- <TFeatures extends TableFeatures, TData extends RowData>(
260
- row: Row<TFeatures, TData>,
261
- columnId: string,
262
- filterValues: [unknown, unknown],
263
- ): boolean => {
264
- const min = filterValues[0]
265
- if (min !== '' && min !== undefined) {
266
- if (!filterFn_greaterThanOrEqualTo(row, columnId, min)) {
267
- return false
268
- }
269
- }
270
-
271
- const max = filterValues[1]
272
- if (max === '' || max === undefined) {
273
- return true
274
- }
275
-
276
- if (min !== '' && min !== undefined) {
277
- const numericMin = Number(min)
278
- const numericMax = Number(max)
279
- if (!isNaN(numericMin) && !isNaN(numericMax) && numericMin > numericMax) {
280
- return true
281
- }
282
- }
283
-
284
- return filterFn_lessThanOrEqualTo(row, columnId, max)
285
- },
286
- {
287
- autoRemove: (val: any) =>
288
- testFalsy(val) || (testFalsy(val[0]) && testFalsy(val[1])),
289
- },
290
- )
269
+ export const filterFn_betweenInclusive = constructFilterFn({
270
+ filter: (dataValue, filterValues: [unknown, unknown]) =>
271
+ compareBetween(dataValue, filterValues, true),
272
+ autoRemove: (val: any) =>
273
+ testFalsy(val) ||
274
+ (Array.isArray(val) && testFalsy(val[0]) && testFalsy(val[1])),
275
+ })
291
276
 
292
277
  /**
293
278
  * Keeps rows whose numeric value is inside an inclusive `[min, max]` range.
@@ -295,138 +280,156 @@ const filterFn_betweenInclusive = Object.assign(
295
280
  * Filter values are normalized so blank endpoints become open-ended and
296
281
  * reversed endpoints are swapped.
297
282
  */
298
- export const filterFn_inNumberRange = Object.assign(
299
- <TFeatures extends TableFeatures, TData extends RowData>(
300
- row: Row<TFeatures, TData>,
301
- columnId: string,
302
- filterValue: [number, number],
303
- ) => {
283
+ export const filterFn_inNumberRange = constructFilterFn({
284
+ filter: (dataValue: number, filterValue: [number, number]) => {
304
285
  const [min, max] = filterValue
286
+ return dataValue >= min && dataValue <= max
287
+ },
288
+ resolveFilterValue: (val: [any, any]) => {
289
+ const [unsafeMin, unsafeMax] = val
290
+
291
+ const parsedMin =
292
+ typeof unsafeMin !== 'number' ? parseFloat(unsafeMin) : unsafeMin
293
+ const parsedMax =
294
+ typeof unsafeMax !== 'number' ? parseFloat(unsafeMax) : unsafeMax
295
+
296
+ let min =
297
+ unsafeMin === null || Number.isNaN(parsedMin) ? -Infinity : parsedMin
298
+ let max =
299
+ unsafeMax === null || Number.isNaN(parsedMax) ? Infinity : parsedMax
300
+
301
+ if (min > max) {
302
+ const temp = min
303
+ min = max
304
+ max = temp
305
+ }
305
306
 
306
- const rowValue: number = row.getValue(columnId)
307
- return rowValue >= min && rowValue <= max
307
+ return [min, max] as const
308
308
  },
309
- {
310
- resolveFilterValue: (val: [any, any]) => {
311
- const [unsafeMin, unsafeMax] = val
312
-
313
- const parsedMin =
314
- typeof unsafeMin !== 'number' ? parseFloat(unsafeMin) : unsafeMin
315
- const parsedMax =
316
- typeof unsafeMax !== 'number' ? parseFloat(unsafeMax) : unsafeMax
317
-
318
- let min =
319
- unsafeMin === null || Number.isNaN(parsedMin) ? -Infinity : parsedMin
320
- let max =
321
- unsafeMax === null || Number.isNaN(parsedMax) ? Infinity : parsedMax
322
-
323
- if (min > max) {
324
- const temp = min
325
- min = max
326
- max = temp
327
- }
309
+ autoRemove: (val: any) =>
310
+ testFalsy(val) ||
311
+ (Array.isArray(val) && testFalsy(val[0]) && testFalsy(val[1])),
312
+ })
328
313
 
329
- return [min, max] as const
330
- },
331
- autoRemove: (val: any) =>
332
- testFalsy(val) || (testFalsy(val[0]) && testFalsy(val[1])),
314
+ // Date filters
315
+
316
+ /**
317
+ * Keeps rows whose date value is inside an inclusive `[min, max]` date range.
318
+ *
319
+ * Row values and range endpoints may be `Date` objects, timestamps, or
320
+ * parseable date strings. Blank or invalid endpoints become open-ended and
321
+ * reversed endpoints are swapped. Rows without a valid date never match.
322
+ */
323
+ export const filterFn_inDateRange = constructFilterFn({
324
+ filter: (dataValue: number, filterValue: [number, number]) => {
325
+ const [min, max] = filterValue
326
+ return dataValue >= min && dataValue <= max
333
327
  },
334
- )
328
+ resolveFilterValue: (val: [any, any]) => {
329
+ const [unsafeMin, unsafeMax] = val
330
+
331
+ const parsedMin = toDateTimestamp(unsafeMin)
332
+ const parsedMax = toDateTimestamp(unsafeMax)
333
+
334
+ let min = Number.isNaN(parsedMin) ? -Infinity : parsedMin
335
+ let max = Number.isNaN(parsedMax) ? Infinity : parsedMax
336
+
337
+ if (min > max) {
338
+ const temp = min
339
+ min = max
340
+ max = temp
341
+ }
342
+
343
+ return [min, max] as const
344
+ },
345
+ resolveDataValue: (val: any) => toDateTimestamp(val),
346
+ autoRemove: (val: any) =>
347
+ testFalsy(val) ||
348
+ (Array.isArray(val) && testFalsy(val[0]) && testFalsy(val[1])),
349
+ })
335
350
 
336
351
  // Array filters
337
352
 
338
353
  /**
339
354
  * Keeps rows whose scalar column value equals at least one filter value.
340
355
  */
341
- export const filterFn_arrHas = Object.assign(
342
- <TFeatures extends TableFeatures, TData extends RowData>(
343
- row: Row<TFeatures, TData>,
344
- columnId: string,
345
- filterValue: Array<unknown>,
346
- ) => {
347
- const value = row.getValue<unknown>(columnId)
356
+ export const filterFn_arrHas = constructFilterFn({
357
+ filter: (dataValue, filterValue: Array<unknown>) => {
348
358
  for (let i = 0; i < filterValue.length; i++) {
349
- if (value === filterValue[i]) {
359
+ if (dataValue === filterValue[i]) {
350
360
  return true
351
361
  }
352
362
  }
353
363
  return false
354
364
  },
355
- { autoRemove: (val: any) => testFalsy(val) || !val?.length },
356
- )
365
+ autoRemove: (val: any) => testFalsy(val) || !val?.length,
366
+ })
357
367
 
358
368
  /**
359
369
  * Keeps rows whose array or string column value includes at least one filter value.
360
370
  */
361
- export const filterFn_arrIncludes = Object.assign(
362
- <TFeatures extends TableFeatures, TData extends RowData>(
363
- row: Row<TFeatures, TData>,
364
- columnId: string,
365
- filterValue: Array<unknown>,
366
- ) => {
367
- const value = row.getValue<unknown>(columnId)
368
- if (typeof value !== 'string' && !Array.isArray(value)) {
371
+ export const filterFn_arrIncludes = constructFilterFn({
372
+ filter: (dataValue, filterValue: Array<unknown>) => {
373
+ if (typeof dataValue !== 'string' && !Array.isArray(dataValue)) {
369
374
  return false
370
375
  }
371
376
 
372
377
  for (let i = 0; i < filterValue.length; i++) {
373
- if (value.includes(filterValue[i] as any)) {
378
+ if (dataValue.includes(filterValue[i] as any)) {
374
379
  return true
375
380
  }
376
381
  }
377
382
  return false
378
383
  },
379
- { autoRemove: (val: any) => testFalsy(val) || !val?.length },
380
- )
384
+ autoRemove: (val: any) => testFalsy(val) || !val?.length,
385
+ })
381
386
 
382
387
  /**
383
388
  * Keeps rows whose array column value includes every filter value.
384
389
  */
385
- export const filterFn_arrIncludesAll = Object.assign(
386
- <TFeatures extends TableFeatures, TData extends RowData>(
387
- row: Row<TFeatures, TData>,
388
- columnId: string,
389
- filterValue: Array<unknown>,
390
- ) => {
391
- const value = row.getValue<Array<unknown>>(columnId)
392
- if (!Array.isArray(value)) return false
390
+ export const filterFn_arrIncludesAll = constructFilterFn({
391
+ filter: (dataValue, filterValue: Array<unknown>) => {
392
+ if (!Array.isArray(dataValue)) return false
393
393
  for (let i = 0; i < filterValue.length; i++) {
394
- if (!value.includes(filterValue[i])) {
394
+ if (!dataValue.includes(filterValue[i])) {
395
395
  return false
396
396
  }
397
397
  }
398
398
  return true
399
399
  },
400
- { autoRemove: (val: any) => testFalsy(val) || !val?.length },
401
- )
400
+ autoRemove: (val: any) => testFalsy(val) || !val?.length,
401
+ })
402
402
 
403
403
  /**
404
404
  * Keeps rows whose array column value includes at least one filter value.
405
405
  */
406
- export const filterFn_arrIncludesSome = Object.assign(
407
- <TFeatures extends TableFeatures, TData extends RowData>(
408
- row: Row<TFeatures, TData>,
409
- columnId: string,
410
- filterValue: Array<unknown>,
411
- ) => {
412
- const value = row.getValue<Array<unknown>>(columnId)
413
- if (!Array.isArray(value)) return false
406
+ export const filterFn_arrIncludesSome = constructFilterFn({
407
+ filter: (dataValue, filterValue: Array<unknown>) => {
408
+ if (!Array.isArray(dataValue)) return false
414
409
  for (let i = 0; i < filterValue.length; i++) {
415
- if (value.includes(filterValue[i])) {
410
+ if (dataValue.includes(filterValue[i])) {
416
411
  return true
417
412
  }
418
413
  }
419
414
  return false
420
415
  },
421
- { autoRemove: (val: any) => testFalsy(val) || !val?.length },
422
- )
416
+ autoRemove: (val: any) => testFalsy(val) || !val?.length,
417
+ })
423
418
 
424
419
  // Export
425
420
 
426
421
  /**
427
422
  * The built-in filter function registry.
428
423
  *
429
- * Pass this object to filtered row model creation or extend it with custom filter functions.
424
+ * Registering this full object opts out of tree-shaking: every built-in
425
+ * filter function ends up in your bundle. Prefer importing the `filterFn_*`
426
+ * functions you actually use and registering just those in the `filterFns`
427
+ * slot, or passing them directly to the `filterFn` column option.
428
+ *
429
+ * @deprecated Import individual `filterFn_*` functions instead for a smaller
430
+ * bundle. This export still works and is not going away in v9, but built-in
431
+ * name resolution (including `filterFn: 'auto'`) only finds functions you
432
+ * register yourself.
430
433
  */
431
434
  export const filterFns = {
432
435
  arrIncludes: filterFn_arrIncludes,
@@ -435,11 +438,17 @@ export const filterFns = {
435
438
  arrIncludesSome: filterFn_arrIncludesSome,
436
439
  between: filterFn_between,
437
440
  betweenInclusive: filterFn_betweenInclusive,
441
+ empty: filterFn_empty,
442
+ endsWith: filterFn_endsWith,
438
443
  equals: filterFn_equals,
439
444
  equalsString: filterFn_equalsString,
445
+ equalsStringSensitive: filterFn_equalsStringSensitive,
446
+ inDateRange: filterFn_inDateRange,
440
447
  inNumberRange: filterFn_inNumberRange,
441
448
  includesString: filterFn_includesString,
442
449
  includesStringSensitive: filterFn_includesStringSensitive,
450
+ notEmpty: filterFn_notEmpty,
451
+ startsWith: filterFn_startsWith,
443
452
  weakEquals: filterFn_weakEquals,
444
453
  }
445
454
 
@@ -450,3 +459,75 @@ export type BuiltInFilterFn = keyof typeof filterFns
450
459
  function testFalsy(val: any) {
451
460
  return val === undefined || val === null || val === ''
452
461
  }
462
+
463
+ function testValueEmpty(dataValue: any) {
464
+ return dataValue == null || String(dataValue).trim() === ''
465
+ }
466
+
467
+ function toDateTimestamp(value: any): number {
468
+ if (value instanceof Date) {
469
+ return value.getTime()
470
+ }
471
+ if (typeof value === 'number') {
472
+ return value
473
+ }
474
+ // Guard nullish and blank values explicitly: `new Date(null)` is the epoch,
475
+ // not an invalid date.
476
+ if (value == null || value === '') {
477
+ return NaN
478
+ }
479
+ return new Date(value).getTime()
480
+ }
481
+
482
+ function compareGreaterThan(dataValue: any, filterValue: any): boolean {
483
+ const numericDataValue = dataValue == null ? 0 : +dataValue
484
+ const numericFilterValue = Number(filterValue)
485
+
486
+ if (!isNaN(numericFilterValue) && !isNaN(numericDataValue)) {
487
+ return numericDataValue > numericFilterValue
488
+ }
489
+
490
+ const stringDataValue = String(dataValue ?? '')
491
+ .toLowerCase()
492
+ .trim()
493
+ const stringFilterValue = String(filterValue).toLowerCase().trim()
494
+ return stringDataValue > stringFilterValue
495
+ }
496
+
497
+ function compareGreaterThanOrEqualTo(dataValue: any, filterValue: any) {
498
+ return dataValue === filterValue || compareGreaterThan(dataValue, filterValue)
499
+ }
500
+
501
+ function compareBetween(
502
+ dataValue: any,
503
+ filterValues: [unknown, unknown],
504
+ inclusive: boolean,
505
+ ): boolean {
506
+ const min = filterValues[0]
507
+ const hasMin = min !== '' && min !== undefined
508
+ if (hasMin) {
509
+ const passesMin = inclusive
510
+ ? compareGreaterThanOrEqualTo(dataValue, min)
511
+ : compareGreaterThan(dataValue, min)
512
+ if (!passesMin) {
513
+ return false
514
+ }
515
+ }
516
+
517
+ const max = filterValues[1]
518
+ if (max === '' || max === undefined) {
519
+ return true
520
+ }
521
+
522
+ if (hasMin) {
523
+ const numericMin = Number(min)
524
+ const numericMax = Number(max)
525
+ if (!isNaN(numericMin) && !isNaN(numericMax) && numericMin > numericMax) {
526
+ return true
527
+ }
528
+ }
529
+
530
+ return inclusive
531
+ ? !compareGreaterThan(dataValue, max)
532
+ : !compareGreaterThanOrEqualTo(dataValue, max)
533
+ }