@cdx-ui/data-grid 0.3.10 → 0.3.12

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 (82) hide show
  1. package/README.md +3 -1
  2. package/coverage.svg +1 -1
  3. package/lib/commonjs/DataGrid.js +232 -62
  4. package/lib/commonjs/DataGrid.js.map +1 -1
  5. package/lib/commonjs/DataGridFilterPanel.js +420 -0
  6. package/lib/commonjs/DataGridFilterPanel.js.map +1 -0
  7. package/lib/commonjs/adapter/columnTypes.js +256 -0
  8. package/lib/commonjs/adapter/columnTypes.js.map +1 -0
  9. package/lib/commonjs/adapter/columns.js +132 -4
  10. package/lib/commonjs/adapter/columns.js.map +1 -1
  11. package/lib/commonjs/adapter/filterOperators.js +422 -0
  12. package/lib/commonjs/adapter/filterOperators.js.map +1 -0
  13. package/lib/commonjs/adapter/pinning.js +101 -0
  14. package/lib/commonjs/adapter/pinning.js.map +1 -0
  15. package/lib/commonjs/index.js +26 -0
  16. package/lib/commonjs/index.js.map +1 -1
  17. package/lib/commonjs/numberCells.js +280 -0
  18. package/lib/commonjs/numberCells.js.map +1 -0
  19. package/lib/commonjs/styles.js +61 -1
  20. package/lib/commonjs/styles.js.map +1 -1
  21. package/lib/commonjs/types.js +29 -1
  22. package/lib/commonjs/types.js.map +1 -1
  23. package/lib/commonjs/useDataGridTable.js +98 -13
  24. package/lib/commonjs/useDataGridTable.js.map +1 -1
  25. package/lib/module/DataGrid.js +237 -67
  26. package/lib/module/DataGrid.js.map +1 -1
  27. package/lib/module/DataGridFilterPanel.js +414 -0
  28. package/lib/module/DataGridFilterPanel.js.map +1 -0
  29. package/lib/module/adapter/columnTypes.js +240 -0
  30. package/lib/module/adapter/columnTypes.js.map +1 -0
  31. package/lib/module/adapter/columns.js +129 -4
  32. package/lib/module/adapter/columns.js.map +1 -1
  33. package/lib/module/adapter/filterOperators.js +406 -0
  34. package/lib/module/adapter/filterOperators.js.map +1 -0
  35. package/lib/module/adapter/pinning.js +90 -0
  36. package/lib/module/adapter/pinning.js.map +1 -0
  37. package/lib/module/index.js +2 -0
  38. package/lib/module/index.js.map +1 -1
  39. package/lib/module/numberCells.js +275 -0
  40. package/lib/module/numberCells.js.map +1 -0
  41. package/lib/module/styles.js +60 -0
  42. package/lib/module/styles.js.map +1 -1
  43. package/lib/module/types.js +28 -0
  44. package/lib/module/types.js.map +1 -1
  45. package/lib/module/useDataGridTable.js +99 -14
  46. package/lib/module/useDataGridTable.js.map +1 -1
  47. package/lib/typescript/DataGrid.d.ts.map +1 -1
  48. package/lib/typescript/DataGridFilterPanel.d.ts +17 -0
  49. package/lib/typescript/DataGridFilterPanel.d.ts.map +1 -0
  50. package/lib/typescript/adapter/columnTypes.d.ts +32 -0
  51. package/lib/typescript/adapter/columnTypes.d.ts.map +1 -0
  52. package/lib/typescript/adapter/columns.d.ts +14 -5
  53. package/lib/typescript/adapter/columns.d.ts.map +1 -1
  54. package/lib/typescript/adapter/filterOperators.d.ts +43 -0
  55. package/lib/typescript/adapter/filterOperators.d.ts.map +1 -0
  56. package/lib/typescript/adapter/pinning.d.ts +25 -0
  57. package/lib/typescript/adapter/pinning.d.ts.map +1 -0
  58. package/lib/typescript/index.d.ts +4 -1
  59. package/lib/typescript/index.d.ts.map +1 -1
  60. package/lib/typescript/numberCells.d.ts +46 -0
  61. package/lib/typescript/numberCells.d.ts.map +1 -0
  62. package/lib/typescript/styles.d.ts +21 -0
  63. package/lib/typescript/styles.d.ts.map +1 -1
  64. package/lib/typescript/types.d.ts +86 -7
  65. package/lib/typescript/types.d.ts.map +1 -1
  66. package/lib/typescript/useDataGridTable.d.ts +18 -4
  67. package/lib/typescript/useDataGridTable.d.ts.map +1 -1
  68. package/package.json +5 -5
  69. package/src/DataGrid.tsx +377 -67
  70. package/src/DataGridFilterPanel.tsx +477 -0
  71. package/src/adapter/columnTypes.ts +327 -0
  72. package/src/adapter/columns.test.ts +75 -1
  73. package/src/adapter/columns.ts +178 -3
  74. package/src/adapter/filterOperators.test.ts +292 -0
  75. package/src/adapter/filterOperators.ts +524 -0
  76. package/src/adapter/pinning.ts +125 -0
  77. package/src/index.ts +15 -0
  78. package/src/numberCells.tsx +379 -0
  79. package/src/styles.ts +75 -0
  80. package/src/types.ts +114 -7
  81. package/src/useDataGridTable.test.ts +69 -0
  82. package/src/useDataGridTable.ts +127 -8
@@ -0,0 +1,524 @@
1
+ import type {
2
+ GridColDef,
3
+ GridColType,
4
+ GridFilterItem,
5
+ GridFilterOperator,
6
+ GridSingleSelectColDef,
7
+ GridValidRowModel,
8
+ GridValueOption,
9
+ } from '../types';
10
+ import {
11
+ getColumnFilterText,
12
+ normalizeFilterQuery,
13
+ untypedCellIncludesQuery,
14
+ usesDisplayFilter,
15
+ } from './columnTypes';
16
+
17
+ const STRING_OPERATORS: readonly GridFilterOperator[] = [
18
+ 'contains',
19
+ 'doesNotContain',
20
+ 'equals',
21
+ 'doesNotEqual',
22
+ 'startsWith',
23
+ 'endsWith',
24
+ 'isEmpty',
25
+ 'isNotEmpty',
26
+ 'isAnyOf',
27
+ ];
28
+
29
+ const NUMBER_OPERATORS: readonly GridFilterOperator[] = [
30
+ '=',
31
+ '!=',
32
+ '>',
33
+ '>=',
34
+ '<',
35
+ '<=',
36
+ 'isEmpty',
37
+ 'isNotEmpty',
38
+ 'isAnyOf',
39
+ ];
40
+
41
+ const DATE_OPERATORS: readonly GridFilterOperator[] = [
42
+ 'is',
43
+ 'not',
44
+ 'after',
45
+ 'onOrAfter',
46
+ 'before',
47
+ 'onOrBefore',
48
+ 'isEmpty',
49
+ 'isNotEmpty',
50
+ ];
51
+
52
+ const BOOLEAN_OPERATORS: readonly GridFilterOperator[] = ['is'];
53
+
54
+ const SINGLE_SELECT_OPERATORS: readonly GridFilterOperator[] = ['is', 'not', 'isAnyOf'];
55
+
56
+ /** Operators a column of `type` accepts. An omitted `type` accepts the `string` operators. */
57
+ export function getGridFilterOperators(type?: GridColType): readonly GridFilterOperator[] {
58
+ switch (type) {
59
+ case 'number':
60
+ return NUMBER_OPERATORS;
61
+ case 'date':
62
+ case 'dateTime':
63
+ return DATE_OPERATORS;
64
+ case 'boolean':
65
+ return BOOLEAN_OPERATORS;
66
+ case 'singleSelect':
67
+ return SINGLE_SELECT_OPERATORS;
68
+ default:
69
+ return STRING_OPERATORS;
70
+ }
71
+ }
72
+
73
+ /** The operator a filter UI preselects for a column of `type`. Matches MUI. */
74
+ export function getDefaultFilterOperator(type?: GridColType): GridFilterOperator {
75
+ return getGridFilterOperators(type)[0];
76
+ }
77
+
78
+ const OPERATOR_LABELS: Record<GridFilterOperator, string> = {
79
+ contains: 'contains',
80
+ doesNotContain: 'does not contain',
81
+ equals: 'equals',
82
+ doesNotEqual: 'does not equal',
83
+ startsWith: 'starts with',
84
+ endsWith: 'ends with',
85
+ isEmpty: 'is empty',
86
+ isNotEmpty: 'is not empty',
87
+ isAnyOf: 'is any of',
88
+ '=': '=',
89
+ '!=': '!=',
90
+ '>': '>',
91
+ '>=': '>=',
92
+ '<': '<',
93
+ '<=': '<=',
94
+ is: 'is',
95
+ not: 'is not',
96
+ after: 'is after',
97
+ onOrAfter: 'is on or after',
98
+ before: 'is before',
99
+ onOrBefore: 'is on or before',
100
+ };
101
+
102
+ /** Short label for an operator, in the style of the MUI operator menu. */
103
+ export function getFilterOperatorLabel(operator: GridFilterOperator): string {
104
+ return OPERATOR_LABELS[operator];
105
+ }
106
+
107
+ /** `isEmpty` and `isNotEmpty` ignore the item's `value`. */
108
+ export function filterOperatorNeedsValue(operator: GridFilterOperator): boolean {
109
+ return operator !== 'isEmpty' && operator !== 'isNotEmpty';
110
+ }
111
+
112
+ /** True when a value-taking operator has nothing to compare against, so it filters nothing. */
113
+ export function isBlankFilterValue(value: unknown): boolean {
114
+ if (value == null) {
115
+ return true;
116
+ }
117
+ if (typeof value === 'string') {
118
+ return value.trim().length === 0;
119
+ }
120
+ return Array.isArray(value) && value.length === 0;
121
+ }
122
+
123
+ export interface OperatorFilterValue {
124
+ operator: GridFilterOperator;
125
+ value: unknown;
126
+ }
127
+
128
+ const KNOWN_OPERATORS: ReadonlySet<string> = new Set([
129
+ ...STRING_OPERATORS,
130
+ ...NUMBER_OPERATORS,
131
+ ...DATE_OPERATORS,
132
+ ...BOOLEAN_OPERATORS,
133
+ ...SINGLE_SELECT_OPERATORS,
134
+ ]);
135
+
136
+ /** Several filter items on one column. Every item must match. */
137
+ export interface FilterValueGroup {
138
+ items: readonly unknown[];
139
+ }
140
+
141
+ export function isFilterValueGroup(filterValue: unknown): filterValue is FilterValueGroup {
142
+ return (
143
+ typeof filterValue === 'object' &&
144
+ filterValue !== null &&
145
+ 'items' in filterValue &&
146
+ Array.isArray(filterValue.items)
147
+ );
148
+ }
149
+
150
+ export function isOperatorFilterValue(filterValue: unknown): filterValue is OperatorFilterValue {
151
+ return (
152
+ typeof filterValue === 'object' &&
153
+ filterValue !== null &&
154
+ 'operator' in filterValue &&
155
+ typeof filterValue.operator === 'string' &&
156
+ KNOWN_OPERATORS.has(filterValue.operator)
157
+ );
158
+ }
159
+
160
+ const DATE_ONLY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
161
+
162
+ function parseDate(value: unknown): Date | null {
163
+ if (value instanceof Date) {
164
+ return Number.isFinite(value.getTime()) ? value : null;
165
+ }
166
+ if (typeof value === 'number') {
167
+ const date = new Date(value);
168
+ return Number.isFinite(date.getTime()) ? date : null;
169
+ }
170
+ if (typeof value !== 'string') {
171
+ return null;
172
+ }
173
+ const text = value.trim();
174
+ const dateOnly = DATE_ONLY_PATTERN.exec(text);
175
+ if (dateOnly) {
176
+ return new Date(Number(dateOnly[1]), Number(dateOnly[2]) - 1, Number(dateOnly[3]));
177
+ }
178
+ const date = new Date(text);
179
+ return Number.isFinite(date.getTime()) ? date : null;
180
+ }
181
+
182
+ function startOfDay(date: Date): number {
183
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
184
+ }
185
+
186
+ function parseNumber(value: unknown): number | null {
187
+ if (typeof value === 'number') {
188
+ return Number.isFinite(value) ? value : null;
189
+ }
190
+ if (typeof value === 'string' && value.trim().length > 0) {
191
+ const parsed = Number(value);
192
+ return Number.isFinite(parsed) ? parsed : null;
193
+ }
194
+ return null;
195
+ }
196
+
197
+ function parseBoolean(value: unknown): boolean | null {
198
+ if (typeof value === 'boolean') {
199
+ return value;
200
+ }
201
+ if (value === 'true') {
202
+ return true;
203
+ }
204
+ if (value === 'false') {
205
+ return false;
206
+ }
207
+ return null;
208
+ }
209
+
210
+ function isEmptyCell(value: unknown): boolean {
211
+ if (value == null || value === '') {
212
+ return true;
213
+ }
214
+ return typeof value === 'number' && Number.isNaN(value);
215
+ }
216
+
217
+ function toList(value: unknown): unknown[] {
218
+ return Array.isArray(value) ? value : [value];
219
+ }
220
+
221
+ function selectKey(value: unknown): string | null {
222
+ return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'
223
+ ? String(value)
224
+ : null;
225
+ }
226
+
227
+ function sameSelectValue(a: unknown, b: unknown): boolean {
228
+ const keyA = selectKey(a);
229
+ return keyA !== null && keyA === selectKey(b);
230
+ }
231
+
232
+ function compareNumbers(operator: GridFilterOperator, cell: number, operand: number): boolean {
233
+ switch (operator) {
234
+ case '=':
235
+ return cell === operand;
236
+ case '!=':
237
+ return cell !== operand;
238
+ case '>':
239
+ return cell > operand;
240
+ case '>=':
241
+ return cell >= operand;
242
+ case '<':
243
+ return cell < operand;
244
+ case '<=':
245
+ return cell <= operand;
246
+ default:
247
+ return true;
248
+ }
249
+ }
250
+
251
+ function matchNumber(operator: GridFilterOperator, cell: unknown, operand: unknown): boolean {
252
+ const cellNumber = typeof cell === 'number' && Number.isFinite(cell) ? cell : null;
253
+ if (operator === 'isAnyOf') {
254
+ const operands = toList(operand)
255
+ .map(parseNumber)
256
+ .filter((n): n is number => n !== null);
257
+ return cellNumber !== null && operands.includes(cellNumber);
258
+ }
259
+ const operandNumber = parseNumber(operand);
260
+ if (operandNumber === null) {
261
+ return true;
262
+ }
263
+ if (cellNumber === null) {
264
+ return operator === '!=';
265
+ }
266
+ return compareNumbers(operator, cellNumber, operandNumber);
267
+ }
268
+
269
+ function matchDate(
270
+ type: 'date' | 'dateTime',
271
+ operator: GridFilterOperator,
272
+ cell: unknown,
273
+ operand: unknown,
274
+ ): boolean {
275
+ const operandDate = parseDate(operand);
276
+ if (operandDate === null) {
277
+ return true;
278
+ }
279
+ const cellDate = cell instanceof Date && Number.isFinite(cell.getTime()) ? cell : null;
280
+ if (cellDate === null) {
281
+ return operator === 'not';
282
+ }
283
+ const normalize = (date: Date) => (type === 'date' ? startOfDay(date) : date.getTime());
284
+ const cellTime = normalize(cellDate);
285
+ const operandTime = normalize(operandDate);
286
+ switch (operator) {
287
+ case 'is':
288
+ return cellTime === operandTime;
289
+ case 'not':
290
+ return cellTime !== operandTime;
291
+ case 'after':
292
+ return cellTime > operandTime;
293
+ case 'onOrAfter':
294
+ return cellTime >= operandTime;
295
+ case 'before':
296
+ return cellTime < operandTime;
297
+ case 'onOrBefore':
298
+ return cellTime <= operandTime;
299
+ default:
300
+ return true;
301
+ }
302
+ }
303
+
304
+ function matchText(operator: GridFilterOperator, text: string, operand: unknown): boolean {
305
+ if (operator === 'isAnyOf') {
306
+ const operands = toList(operand).filter((item) => !isBlankFilterValue(item));
307
+ return operands.some((item) => String(item) === text);
308
+ }
309
+ const needle = String(operand).trim().toLowerCase();
310
+ const haystack = text.toLowerCase();
311
+ switch (operator) {
312
+ case 'contains':
313
+ return haystack.includes(needle);
314
+ case 'doesNotContain':
315
+ return !haystack.includes(needle);
316
+ case 'equals':
317
+ return haystack === needle;
318
+ case 'doesNotEqual':
319
+ return haystack !== needle;
320
+ case 'startsWith':
321
+ return haystack.startsWith(needle);
322
+ case 'endsWith':
323
+ return haystack.endsWith(needle);
324
+ default:
325
+ return true;
326
+ }
327
+ }
328
+
329
+ function matchSingleSelect(operator: GridFilterOperator, cell: unknown, operand: unknown): boolean {
330
+ if (operator === 'isAnyOf') {
331
+ return toList(operand).some((item) => sameSelectValue(item, cell));
332
+ }
333
+ const operandValue = Array.isArray(operand) ? operand[0] : operand;
334
+ switch (operator) {
335
+ case 'is':
336
+ return sameSelectValue(operandValue, cell);
337
+ case 'not':
338
+ return !sameSelectValue(operandValue, cell);
339
+ default:
340
+ return true;
341
+ }
342
+ }
343
+
344
+ function matchBoolean(cell: unknown, operand: unknown): boolean {
345
+ const operandBoolean = parseBoolean(operand);
346
+ if (operandBoolean === null) {
347
+ return true;
348
+ }
349
+ return cell === operandBoolean;
350
+ }
351
+
352
+ /**
353
+ * Evaluates one operator filter against a cell. Operators the column's type does not accept,
354
+ * and operators whose operand is blank, filter nothing, as in MUI.
355
+ */
356
+ export function matchesOperatorFilter<R extends GridValidRowModel>(
357
+ col: GridColDef<R>,
358
+ cell: unknown,
359
+ row: R,
360
+ { operator, value }: OperatorFilterValue,
361
+ ): boolean {
362
+ if (!getGridFilterOperators(col.type).includes(operator)) {
363
+ return true;
364
+ }
365
+ if (operator === 'isEmpty') {
366
+ return isEmptyCell(cell);
367
+ }
368
+ if (operator === 'isNotEmpty') {
369
+ return !isEmptyCell(cell);
370
+ }
371
+ if (isBlankFilterValue(value)) {
372
+ return true;
373
+ }
374
+ switch (col.type) {
375
+ case 'number':
376
+ return matchNumber(operator, cell, value);
377
+ case 'date':
378
+ case 'dateTime':
379
+ return matchDate(col.type, operator, cell, value);
380
+ case 'boolean':
381
+ return matchBoolean(cell, value);
382
+ case 'singleSelect':
383
+ return matchSingleSelect(operator, cell, value);
384
+ default:
385
+ return matchText(operator, cell == null ? '' : getColumnFilterText(col, cell, row), value);
386
+ }
387
+ }
388
+
389
+ /** False for items that filter nothing: a value-taking operator without a value, or blank text. */
390
+ export function isActiveFilterItem(item: GridFilterItem): boolean {
391
+ if (item.operator !== undefined) {
392
+ return !filterOperatorNeedsValue(item.operator) || !isBlankFilterValue(item.value);
393
+ }
394
+ return normalizeFilterQuery(item.value).length > 0;
395
+ }
396
+
397
+ export function rowMatchesFilterItem<R extends GridValidRowModel>(
398
+ col: GridColDef<R>,
399
+ row: R,
400
+ item: GridFilterItem,
401
+ ): boolean {
402
+ const raw = row[col.field];
403
+ const cell: unknown = col.valueGetter ? col.valueGetter(raw, row, col) : raw;
404
+ if (item.operator !== undefined) {
405
+ return matchesOperatorFilter(col, cell, row, { operator: item.operator, value: item.value });
406
+ }
407
+ const query = normalizeFilterQuery(item.value);
408
+ if (usesDisplayFilter(col)) {
409
+ return getColumnFilterText(col, cell, row).toLowerCase().includes(query);
410
+ }
411
+ return untypedCellIncludesQuery(cell, query);
412
+ }
413
+
414
+ function selectOptions<R extends GridValidRowModel>(
415
+ col: GridSingleSelectColDef<R>,
416
+ ): readonly GridValueOption[] {
417
+ return typeof col.valueOptions === 'function' ? col.valueOptions({}) : col.valueOptions;
418
+ }
419
+
420
+ function coerceSelectToken<R extends GridValidRowModel>(col: GridColDef<R>, text: string): unknown {
421
+ if (col.type !== 'singleSelect') {
422
+ return text;
423
+ }
424
+ const trimmed = text.trim();
425
+ const match = selectOptions(col).find((option) => {
426
+ if (typeof option === 'object') {
427
+ return (
428
+ String(option.value) === trimmed || option.label.toLowerCase() === trimmed.toLowerCase()
429
+ );
430
+ }
431
+ return String(option).toLowerCase() === trimmed.toLowerCase();
432
+ });
433
+ if (!match) {
434
+ return text;
435
+ }
436
+ return typeof match === 'object' ? match.value : match;
437
+ }
438
+
439
+ /**
440
+ * Reads a header-filter field. `isAnyOf` splits on commas. Number pieces that round-trip
441
+ * become numbers. A `singleSelect` label (`Open`) becomes its stored value (`open`).
442
+ */
443
+ export function headerFilterValueFromText<R extends GridValidRowModel>(
444
+ col: GridColDef<R> | undefined,
445
+ operator: GridFilterOperator,
446
+ text: string,
447
+ ): unknown {
448
+ if (!filterOperatorNeedsValue(operator)) {
449
+ return undefined;
450
+ }
451
+ if (operator === 'isAnyOf') {
452
+ const parts = text
453
+ .split(',')
454
+ .map((part) => part.trim())
455
+ .filter((part) => part.length > 0);
456
+ if (col?.type === 'number') {
457
+ return parts.map((part) => coerceFilterInput(col, '=', part));
458
+ }
459
+ if (col?.type === 'singleSelect') {
460
+ return parts.map((part) => coerceSelectToken(col, part));
461
+ }
462
+ return parts;
463
+ }
464
+ if (col?.type === 'singleSelect') {
465
+ return coerceSelectToken(col, text);
466
+ }
467
+ return coerceFilterInput(col, operator, text);
468
+ }
469
+
470
+ /**
471
+ * Converts header-input text to the typed operand its column and operator expect. Text that
472
+ * would not survive a round trip back into the input (`'1.'`, `'1.50'`) stays text, so typing
473
+ * is never rewritten under the cursor.
474
+ */
475
+ export function coerceFilterInput<R extends GridValidRowModel>(
476
+ col: GridColDef<R> | undefined,
477
+ operator: GridFilterOperator,
478
+ text: string,
479
+ ): unknown {
480
+ if (operator === 'isAnyOf' || !filterOperatorNeedsValue(operator)) {
481
+ return text;
482
+ }
483
+ const trimmed = text.trim();
484
+ switch (col?.type) {
485
+ case 'number': {
486
+ const parsed = parseNumber(trimmed);
487
+ return parsed !== null && String(parsed) === trimmed ? parsed : text;
488
+ }
489
+ case 'date': {
490
+ return DATE_ONLY_PATTERN.test(trimmed) ? (parseDate(trimmed) ?? text) : text;
491
+ }
492
+ case 'boolean': {
493
+ return parseBoolean(trimmed) ?? text;
494
+ }
495
+ default:
496
+ return text;
497
+ }
498
+ }
499
+
500
+ function pad(value: number, length: number): string {
501
+ return String(value).padStart(length, '0');
502
+ }
503
+
504
+ /** The text a header filter input shows for an operand. */
505
+ export function filterValueToInputText(value: unknown): string {
506
+ if (value == null) {
507
+ return '';
508
+ }
509
+ if (typeof value === 'string') {
510
+ return value;
511
+ }
512
+ if (typeof value === 'number' || typeof value === 'boolean') {
513
+ return String(value);
514
+ }
515
+ if (value instanceof Date) {
516
+ return Number.isFinite(value.getTime())
517
+ ? `${pad(value.getFullYear(), 4)}-${pad(value.getMonth() + 1, 2)}-${pad(value.getDate(), 2)}`
518
+ : '';
519
+ }
520
+ if (Array.isArray(value)) {
521
+ return value.map(filterValueToInputText).join(', ');
522
+ }
523
+ return '';
524
+ }
@@ -0,0 +1,125 @@
1
+ import type { ViewStyle } from 'react-native';
2
+ import type { ColumnPinningState } from '@tanstack/react-table';
3
+ import { SELECT_COLUMN_ID, type GridColDef, type GridValidRowModel } from '../types';
4
+
5
+ export const DEFAULT_PINNED_COLUMN_WIDTH = 100;
6
+ export const SELECT_COLUMN_WIDTH = 56;
7
+
8
+ export type ColumnPinEdge = 'start' | 'end';
9
+
10
+ export function isColumnPinningActive(state: ColumnPinningState): boolean {
11
+ return state.start.length > 0 || state.end.length > 0;
12
+ }
13
+
14
+ export function pinnedFieldSet(state: ColumnPinningState): Set<string> {
15
+ return new Set([...state.start, ...state.end]);
16
+ }
17
+
18
+ export function pinnedColumnPixelWidth<R extends GridValidRowModel>(
19
+ columnId: string,
20
+ col: GridColDef<R> | undefined,
21
+ ): number {
22
+ if (columnId === SELECT_COLUMN_ID) {
23
+ return SELECT_COLUMN_WIDTH;
24
+ }
25
+ return Math.max(col?.width ?? DEFAULT_PINNED_COLUMN_WIDTH, col?.minWidth ?? 0);
26
+ }
27
+
28
+ export interface PinnedOffsetMaps {
29
+ start: ReadonlyMap<string, number>;
30
+ end: ReadonlyMap<string, number>;
31
+ startTotal: number;
32
+ endTotal: number;
33
+ }
34
+
35
+ export function buildPinnedOffsets<R extends GridValidRowModel>(
36
+ state: ColumnPinningState,
37
+ colByField: ReadonlyMap<string, GridColDef<R>>,
38
+ ): PinnedOffsetMaps {
39
+ const start = new Map<string, number>();
40
+ let startOffset = 0;
41
+ for (const id of state.start) {
42
+ start.set(id, startOffset);
43
+ startOffset += pinnedColumnPixelWidth(id, colByField.get(id));
44
+ }
45
+
46
+ const end = new Map<string, number>();
47
+ let endOffset = 0;
48
+ for (let index = state.end.length - 1; index >= 0; index -= 1) {
49
+ const id = state.end[index];
50
+ end.set(id, endOffset);
51
+ endOffset += pinnedColumnPixelWidth(id, colByField.get(id));
52
+ }
53
+
54
+ return { start, end, startTotal: startOffset, endTotal: endOffset };
55
+ }
56
+
57
+ export function pinnedCellStyle(edge: ColumnPinEdge, offset: number, isHeader: boolean): ViewStyle {
58
+ // Logical insets / sticky are valid on web; RN ViewStyle typing does not list them yet.
59
+ return {
60
+ position: 'sticky',
61
+ ...(edge === 'start' ? { insetInlineStart: offset } : { insetInlineEnd: offset }),
62
+ zIndex: isHeader ? 30 : 10,
63
+ } as unknown as ViewStyle;
64
+ }
65
+
66
+ /**
67
+ * Draws the pinned edge dividers on the scroll container itself so they reach its bottom when the
68
+ * rows are shorter than the viewport. A non-local background does not scroll with the content,
69
+ * matching sticky pinned cells, and its positioning area excludes scrollbars.
70
+ */
71
+ export function pinnedEdgeGuidesImage(
72
+ offsets: Pick<PinnedOffsetMaps, 'startTotal' | 'endTotal'>,
73
+ color: string,
74
+ isRTL: boolean,
75
+ ): string | undefined {
76
+ const { startTotal, endTotal } = offsets;
77
+ const stops: string[] = [];
78
+ if (startTotal > 0) {
79
+ const edge = `${startTotal.toString()}px`;
80
+ const inner = `${(startTotal - 1).toString()}px`;
81
+ stops.push(
82
+ `transparent ${inner}`,
83
+ `${color} ${inner}`,
84
+ `${color} ${edge}`,
85
+ `transparent ${edge}`,
86
+ );
87
+ }
88
+ if (endTotal > 0) {
89
+ const edge = `calc(100% - ${endTotal.toString()}px)`;
90
+ const inner = `calc(100% - ${(endTotal - 1).toString()}px)`;
91
+ stops.push(
92
+ `transparent ${edge}`,
93
+ `${color} ${edge}`,
94
+ `${color} ${inner}`,
95
+ `transparent ${inner}`,
96
+ );
97
+ }
98
+ if (stops.length === 0) {
99
+ return undefined;
100
+ }
101
+ return `linear-gradient(${isRTL ? 'to left' : 'to right'}, ${stops.join(', ')})`;
102
+ }
103
+
104
+ const warnedPinnedFlexFields = new Set<string>();
105
+
106
+ export function warnPinnedFlexColumns<R extends GridValidRowModel>(
107
+ columns: readonly GridColDef<R>[],
108
+ pinned: ReadonlySet<string>,
109
+ ): void {
110
+ if (!__DEV__) {
111
+ return;
112
+ }
113
+ for (const column of columns) {
114
+ if (
115
+ pinned.has(column.field) &&
116
+ (column.flex ?? 0) > 0 &&
117
+ !warnedPinnedFlexFields.has(column.field)
118
+ ) {
119
+ warnedPinnedFlexFields.add(column.field);
120
+ console.warn(
121
+ `[DataGrid] Pinned column "${column.field}" ignores flex and uses a fixed width.`,
122
+ );
123
+ }
124
+ }
125
+ }
package/src/index.ts CHANGED
@@ -1,18 +1,33 @@
1
1
  export { DataGrid } from './DataGrid';
2
+ export { getDefaultFilterOperator, getGridFilterOperators } from './adapter/filterOperators';
3
+ export { gridDeltaNumberColDef, gridPolarityNumberColDef } from './numberCells';
4
+ export type {
5
+ GridDeltaNumberColDefOptions,
6
+ GridDeltaNumberSymbol,
7
+ GridNumberCellDensity,
8
+ GridPolarityNumberColDefOptions,
9
+ } from './numberCells';
2
10
  export type {
3
11
  DataGridProps,
4
12
  GridAlignment,
13
+ GridBaseColDef,
5
14
  GridColDef,
15
+ GridColType,
6
16
  GridFeatureMode,
7
17
  GridFilterItem,
8
18
  GridFilterModel,
19
+ GridFilterOperator,
20
+ GridLogicOperator,
9
21
  GridPaginationModel,
22
+ GridPinnedColumnFields,
10
23
  GridRenderCellParams,
11
24
  GridRowId,
12
25
  GridRowSelectionModel,
13
26
  GridRowsProp,
27
+ GridSingleSelectColDef,
14
28
  GridSortDirection,
15
29
  GridSortItem,
16
30
  GridSortModel,
17
31
  GridValidRowModel,
32
+ GridValueOption,
18
33
  } from './types';