@jielga/tmdatagrid 2.1.4 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +78 -5
- package/dist/index.js +2194 -1992
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/docs/cell-selection.md +1 -1
- package/docs/column-layout.md +13 -8
- package/docs/columns.md +59 -3
- package/docs/components.md +1 -0
- package/docs/export.md +2 -2
- package/docs/filtering.md +108 -15
- package/docs/getting-started.md +40 -0
- package/docs/grouping.md +9 -6
- package/docs/loading-and-empty.md +29 -9
- package/docs/localization.md +130 -3
- package/docs/menu.md +1 -1
- package/docs/pagination.md +26 -7
- package/docs/persistence.md +101 -2
- package/docs/row-details.md +41 -4
- package/docs/row-interaction.md +2 -2
- package/docs/row-selection.md +6 -7
- package/docs/row-styling.md +17 -0
- package/docs/scrolling.md +1 -1
- package/docs/server-side.md +3 -1
- package/docs/testing.md +18 -0
- package/docs/use-tm-data-grid.md +75 -36
- package/package.json +1 -1
- package/skills/appearance/SKILL.md +6 -4
- package/skills/appearance/references/labels.md +126 -0
- package/skills/cell-selection/SKILL.md +2 -2
- package/skills/columns/SKILL.md +44 -43
- package/skills/columns/references/columns-api.md +16 -1
- package/skills/data/SKILL.md +77 -50
- package/skills/editing/SKILL.md +1 -1
- package/skills/filtering/SKILL.md +94 -60
- package/skills/filtering/references/custom-filters.md +118 -0
- package/skills/getting-started/SKILL.md +37 -2
- package/skills/grouping/SKILL.md +16 -9
- package/skills/migrating-to-2/SKILL.md +1 -1
- package/skills/options/SKILL.md +206 -55
- package/skills/rows/SKILL.md +70 -39
- package/skills/server-side/SKILL.md +1 -1
- package/skills/testing/SKILL.md +12 -1
- package/skills/testing-components/SKILL.md +1 -1
- package/skills/testing-editing/SKILL.md +1 -1
- package/src/components/TMDataGridFilterPills.module.css +9 -7
- package/src/components/TMDataGridTable.module.css +15 -6
- package/src/components/TMDataGridTable.tsx +253 -41
- package/src/components/sticky.module.css +27 -7
- package/src/core/autosize.ts +19 -7
- package/src/core/cellRange.ts +11 -2
- package/src/core/colSpan.ts +78 -0
- package/src/core/columnUtils.ts +7 -0
- package/src/core/filterControls.ts +8 -0
- package/src/core/filterOperators.ts +29 -1
- package/src/core/persistence.ts +322 -26
- package/src/index.ts +5 -0
- package/src/useTMDataGrid.tsx +133 -14
package/src/core/autosize.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { RowData } from "@tanstack/react-table";
|
|
2
2
|
import type { TMDataGridRowData } from "../TMDataGridContext";
|
|
3
3
|
import type { TMDataGridTable } from "../useTMDataGrid";
|
|
4
|
+
import { DEFAULT_COLUMN_MIN_SIZE } from "./columnUtils";
|
|
4
5
|
import { isHTMLElement } from "./dom";
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -40,30 +41,41 @@ function contentSpanWidth(content: HTMLElement): number {
|
|
|
40
41
|
return width > 0 ? width : content.scrollWidth;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* One column's body cells that are currently in the DOM.
|
|
46
|
+
*
|
|
47
|
+
* `measurable` leaves out the cells spanning several columns (`meta.colSpan`):
|
|
48
|
+
* such a cell says nothing about this one column's width.
|
|
49
|
+
*/
|
|
44
50
|
function mountedCells(
|
|
45
51
|
container: HTMLElement,
|
|
46
52
|
columnId: string,
|
|
53
|
+
{ measurable }: { measurable: boolean },
|
|
47
54
|
): NodeListOf<HTMLElement> {
|
|
48
55
|
const id = CSS.escape(columnId);
|
|
56
|
+
const unspanned = measurable ? ":not([data-col-span])" : "";
|
|
49
57
|
return container.querySelectorAll<HTMLElement>(
|
|
50
|
-
`[role="cell"][data-column-id="${id}"],` +
|
|
51
|
-
`[role="gridcell"][data-column-id="${id}"]`,
|
|
58
|
+
`[role="cell"][data-column-id="${id}"]${unspanned},` +
|
|
59
|
+
`[role="gridcell"][data-column-id="${id}"]${unspanned}`,
|
|
52
60
|
);
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
/**
|
|
56
|
-
* Whether this column has any body cell in the DOM
|
|
64
|
+
* Whether this column has any body cell in the DOM.
|
|
57
65
|
*
|
|
58
66
|
* A caller sizing a column on its own schedule rather than from a gesture has
|
|
59
67
|
* to ask: the virtualizer mounts its first rows a render *after* the grid
|
|
60
68
|
* itself mounts, and a measurement taken before that fits the header alone.
|
|
69
|
+
*
|
|
70
|
+
* A spanning cell counts. A column whose every mounted cell spans has its
|
|
71
|
+
* rows, and measuring it fits the header, which is the width it gets; left
|
|
72
|
+
* out, the column would wait for cells that never come.
|
|
61
73
|
*/
|
|
62
74
|
export function hasMountedCells(
|
|
63
75
|
container: HTMLElement,
|
|
64
76
|
columnId: string,
|
|
65
77
|
): boolean {
|
|
66
|
-
return mountedCells(container, columnId).length > 0;
|
|
78
|
+
return mountedCells(container, columnId, { measurable: false }).length > 0;
|
|
67
79
|
}
|
|
68
80
|
|
|
69
81
|
/**
|
|
@@ -85,7 +97,7 @@ export function measureColumnContentWidth({
|
|
|
85
97
|
}): number {
|
|
86
98
|
let widest = 0;
|
|
87
99
|
|
|
88
|
-
for (const cell of mountedCells(container, columnId)) {
|
|
100
|
+
for (const cell of mountedCells(container, columnId, { measurable: true })) {
|
|
89
101
|
const content = cell.firstElementChild;
|
|
90
102
|
if (!isHTMLElement(content)) continue;
|
|
91
103
|
const styles = getComputedStyle(cell);
|
|
@@ -143,7 +155,7 @@ export function autosizeColumn<TData extends RowData>({
|
|
|
143
155
|
const measured = measureColumnContentWidth({ container, columnId });
|
|
144
156
|
if (measured <= 0) return false;
|
|
145
157
|
|
|
146
|
-
const minSize = column.columnDef.minSize ??
|
|
158
|
+
const minSize = column.columnDef.minSize ?? DEFAULT_COLUMN_MIN_SIZE;
|
|
147
159
|
const maxSize = column.columnDef.maxSize ?? Number.POSITIVE_INFINITY;
|
|
148
160
|
const width = Math.round(Math.min(Math.max(measured, minSize), maxSize));
|
|
149
161
|
|
package/src/core/cellRange.ts
CHANGED
|
@@ -70,7 +70,13 @@ export function resolveRangeBounds({
|
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Whether a cell is inside the rectangle.
|
|
75
|
+
*
|
|
76
|
+
* A cell spanning several columns (`meta.colSpan`) is addressed by its first
|
|
77
|
+
* column: it is inside when that column is, and a rectangle over only the
|
|
78
|
+
* columns it covers leaves it out.
|
|
79
|
+
*/
|
|
74
80
|
export function isWithinBounds(
|
|
75
81
|
bounds: TMDataGridRangeBounds | null,
|
|
76
82
|
rowIndex: number,
|
|
@@ -105,14 +111,17 @@ export function boundsEdges(
|
|
|
105
111
|
bounds: TMDataGridRangeBounds | null,
|
|
106
112
|
rowIndex: number,
|
|
107
113
|
columnIndex: number,
|
|
114
|
+
lastColumnIndex: number = columnIndex,
|
|
108
115
|
): { top: boolean; bottom: boolean; left: boolean; right: boolean } | null {
|
|
109
116
|
if (!isWithinBounds(bounds, rowIndex, columnIndex) || bounds === null) {
|
|
110
117
|
return null;
|
|
111
118
|
}
|
|
119
|
+
// A cell spanning columns up to `lastColumnIndex` is one box: the
|
|
120
|
+
// rectangle's right edge falling anywhere inside it is drawn on its side.
|
|
112
121
|
return {
|
|
113
122
|
top: rowIndex === bounds.top,
|
|
114
123
|
bottom: rowIndex === bounds.bottom,
|
|
115
124
|
left: columnIndex === bounds.left,
|
|
116
|
-
right:
|
|
125
|
+
right: bounds.right <= lastColumnIndex,
|
|
117
126
|
};
|
|
118
127
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { isGeneratedColumn } from "./columnUtils";
|
|
2
|
+
|
|
3
|
+
/** Which pinned block a displayed column sits in. */
|
|
4
|
+
export type TMDataGridColumnSection = "left" | "center" | "right";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* How wide each displayed cell of one row is, in columns.
|
|
8
|
+
*
|
|
9
|
+
* `spans[i]` is `n >= 1` for a cell that renders and covers `n` tracks from
|
|
10
|
+
* column `i` rightwards, and `0` for a cell covered by one to its left, which
|
|
11
|
+
* does not render. The array runs in display order, the order of
|
|
12
|
+
* `orderedColumns`, and its entries always sum to the column count.
|
|
13
|
+
*
|
|
14
|
+
* A span stops short at the first column it may not cover:
|
|
15
|
+
*
|
|
16
|
+
* - a generated lane - the checkbox, tree, details and edit lanes stay
|
|
17
|
+
* where they are whatever a data cell asks for, which is also why
|
|
18
|
+
* `Infinity` reads as "to the last of my columns";
|
|
19
|
+
* - the edge of the cell's pinned block - a pinned cell is sticky at the
|
|
20
|
+
* offset of its own column, and one stretching into the scrolling centre
|
|
21
|
+
* would slide over the cells it is meant to leave.
|
|
22
|
+
*
|
|
23
|
+
* A generated lane never spans, and a request below 1, or one that is not a
|
|
24
|
+
* number, is 1.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveColSpans(
|
|
27
|
+
columns: ReadonlyArray<{ id: string; section: TMDataGridColumnSection }>,
|
|
28
|
+
requestedSpan: (columnIndex: number) => number,
|
|
29
|
+
): Array<number> {
|
|
30
|
+
const spans: Array<number> = [];
|
|
31
|
+
let index = 0;
|
|
32
|
+
while (index < columns.length) {
|
|
33
|
+
const column = columns[index]!;
|
|
34
|
+
const requested = isGeneratedColumn(column.id) ? 1 : requestedSpan(index);
|
|
35
|
+
const wanted =
|
|
36
|
+
typeof requested === "number" && requested >= 1
|
|
37
|
+
? Math.floor(requested)
|
|
38
|
+
: 1;
|
|
39
|
+
let span = 1;
|
|
40
|
+
while (span < wanted) {
|
|
41
|
+
const next = columns[index + span];
|
|
42
|
+
if (
|
|
43
|
+
next === undefined ||
|
|
44
|
+
next.section !== column.section ||
|
|
45
|
+
isGeneratedColumn(next.id)
|
|
46
|
+
) {
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
span += 1;
|
|
50
|
+
}
|
|
51
|
+
spans.push(span);
|
|
52
|
+
for (let covered = 1; covered < span; covered += 1) spans.push(0);
|
|
53
|
+
index += span;
|
|
54
|
+
}
|
|
55
|
+
return spans;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The column a position on a row lands on once spans are applied: itself
|
|
60
|
+
* when its cell renders, otherwise the spanning cell covering it.
|
|
61
|
+
*/
|
|
62
|
+
export function spanAnchorIndex(
|
|
63
|
+
spans: ReadonlyArray<number>,
|
|
64
|
+
columnIndex: number,
|
|
65
|
+
): number {
|
|
66
|
+
let index = columnIndex;
|
|
67
|
+
while (index > 0 && spans[index] === 0) index -= 1;
|
|
68
|
+
return index;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The last column the cell at `columnIndex` covers - itself when unspanned. */
|
|
72
|
+
export function spanEndIndex(
|
|
73
|
+
spans: ReadonlyArray<number>,
|
|
74
|
+
columnIndex: number,
|
|
75
|
+
): number {
|
|
76
|
+
const anchor = spanAnchorIndex(spans, columnIndex);
|
|
77
|
+
return anchor + Math.max(1, spans[anchor] ?? 1) - 1;
|
|
78
|
+
}
|
package/src/core/columnUtils.ts
CHANGED
|
@@ -160,6 +160,13 @@ export function isColumnReorderable(column: ColumnLike): boolean {
|
|
|
160
160
|
return column.parent === undefined;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/**
|
|
164
|
+
* The `minSize` of a column that sets none: the floor of its fluid track, of a
|
|
165
|
+
* resize drag and of its pinned width. Passed to TanStack as
|
|
166
|
+
* `defaultColumn.minSize`, which otherwise fills in 20.
|
|
167
|
+
*/
|
|
168
|
+
export const DEFAULT_COLUMN_MIN_SIZE = 80;
|
|
169
|
+
|
|
163
170
|
/** The list length from which a column chooser shows its search box. */
|
|
164
171
|
export const COLUMN_SEARCH_MIN = 6;
|
|
165
172
|
|
|
@@ -106,4 +106,12 @@ export type TMDataGridColumnFilterOptions = {
|
|
|
106
106
|
* See {@link TMDataGridFilterControlComponent}.
|
|
107
107
|
*/
|
|
108
108
|
control?: TMDataGridFilterControlComponent;
|
|
109
|
+
/**
|
|
110
|
+
* Whether this column's filter goes into the `persist.urlKey` search param.
|
|
111
|
+
* Defaults to `true`. `false` keeps it out of the param, on read and on
|
|
112
|
+
* write. Only has an effect when `urlKey` carries `columnFilters`, and since such
|
|
113
|
+
* a slice is not stored under `dataKey` either, the filter is then not
|
|
114
|
+
* persisted anywhere.
|
|
115
|
+
*/
|
|
116
|
+
url?: boolean;
|
|
109
117
|
};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ColumnFiltersState,
|
|
3
|
+
FilterMeta,
|
|
3
4
|
Row,
|
|
4
5
|
RowData,
|
|
5
6
|
TableFeatures,
|
|
6
7
|
} from "@tanstack/react-table";
|
|
7
|
-
import type { TMDataGridTable } from "../useTMDataGrid";
|
|
8
|
+
import type { TMDataGridFeatures, TMDataGridTable } from "../useTMDataGrid";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* The value shape stored in `columnFilters` for every TMDataGrid column.
|
|
@@ -250,6 +251,19 @@ export function isFilterActive(value: unknown): boolean {
|
|
|
250
251
|
: typeof value.value === "string" && value.value.trim() !== "";
|
|
251
252
|
}
|
|
252
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Whether one `columnFilters` entry narrows the rows, by the one rule the grid
|
|
256
|
+
* uses for "active": an envelope counts once {@link isFilterActive} says so,
|
|
257
|
+
* and a value that is not an envelope always counts, since a raw value written
|
|
258
|
+
* by consumer code narrows the rows as far as the grid can tell.
|
|
259
|
+
*
|
|
260
|
+
* @internal Shared by the empty state and the `urlKey` param; not exported
|
|
261
|
+
* from the package.
|
|
262
|
+
*/
|
|
263
|
+
export function filterNarrowsRows(entry: { value: unknown }): boolean {
|
|
264
|
+
return !isTMDataGridFilterValue(entry.value) || isFilterActive(entry.value);
|
|
265
|
+
}
|
|
266
|
+
|
|
253
267
|
/** One column's filter, typed - what `columnFilters` holds per entry. */
|
|
254
268
|
export type TMDataGridColumnFilter = {
|
|
255
269
|
id: string;
|
|
@@ -474,6 +488,20 @@ export function matchesFilter(
|
|
|
474
488
|
}
|
|
475
489
|
}
|
|
476
490
|
|
|
491
|
+
/**
|
|
492
|
+
* The signature of a column's own `filterFn`. The third argument is the
|
|
493
|
+
* `{ operator, value }` envelope the grid writes into `columnFilters`; a
|
|
494
|
+
* column `filterFn` replaces `tmDataGridFilterFn` outright, so it is handed
|
|
495
|
+
* the envelope, not the unwrapped value. Delegate to `tmDataGridFilterFn`
|
|
496
|
+
* for the operators the function does not handle itself.
|
|
497
|
+
*/
|
|
498
|
+
export type TMDataGridFilterFn<TData extends RowData> = (
|
|
499
|
+
row: Row<TMDataGridFeatures, TData>,
|
|
500
|
+
columnId: string,
|
|
501
|
+
filterValue: TMDataGridFilterValue,
|
|
502
|
+
addMeta?: (meta: FilterMeta) => void,
|
|
503
|
+
) => boolean;
|
|
504
|
+
|
|
477
505
|
/**
|
|
478
506
|
* The single filter function every TMDataGrid column uses. It dispatches on the
|
|
479
507
|
* operator carried by the filter value, which is what lets one column offer
|