@jielga/tmdatagrid 2.0.0-beta.2 → 2.0.0-beta.21
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/README.md +5 -212
- package/dist/index.d.ts +1664 -632
- package/dist/index.js +5226 -3223
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/docs/anatomy.md +102 -0
- package/docs/cell-selection.md +154 -0
- package/docs/column-layout.md +204 -0
- package/docs/columns.md +262 -0
- package/docs/components.md +304 -0
- package/docs/editing.md +603 -0
- package/docs/editors.md +250 -0
- package/docs/export.md +326 -0
- package/docs/filtering.md +358 -0
- package/docs/getting-started.md +123 -0
- package/docs/grouping.md +165 -0
- package/docs/loading-and-empty.md +92 -0
- package/docs/localization.md +79 -0
- package/docs/menu.md +143 -0
- package/docs/pagination.md +144 -0
- package/docs/persistence.md +111 -0
- package/docs/portfolio-rebalancer.md +94 -0
- package/docs/query-builder.md +175 -0
- package/docs/quick-search.md +83 -0
- package/docs/row-details.md +113 -0
- package/docs/row-interaction.md +148 -0
- package/docs/row-pinning.md +132 -0
- package/docs/row-selection.md +134 -0
- package/docs/row-styling.md +133 -0
- package/docs/scrolling.md +111 -0
- package/docs/server-query.md +246 -0
- package/docs/server-side.md +206 -0
- package/docs/sorting.md +101 -0
- package/docs/styling.md +126 -0
- package/docs/summary-row.md +76 -0
- package/docs/testing.md +309 -0
- package/docs/toolbar.md +161 -0
- package/docs/use-tm-data-grid.md +361 -0
- package/package.json +21 -45
- package/skills/appearance/SKILL.md +70 -17
- package/skills/cell-selection/SKILL.md +70 -76
- package/skills/columns/SKILL.md +131 -32
- package/skills/data/SKILL.md +100 -23
- package/skills/editing/SKILL.md +217 -96
- package/skills/editing/references/common-mistakes.md +111 -24
- package/skills/editing/references/editing-api.md +63 -39
- package/skills/editing/references/editors-and-validation.md +77 -19
- package/skills/filtering/SKILL.md +148 -40
- package/skills/getting-started/SKILL.md +18 -16
- package/skills/grouping/SKILL.md +32 -15
- package/skills/options/SKILL.md +39 -9
- package/skills/rows/SKILL.md +22 -18
- package/skills/server-side/SKILL.md +170 -17
- package/skills/testing/SKILL.md +10 -7
- package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
- package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
- package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +39 -23
- package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +106 -38
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
- package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +4 -4
- package/src/components/TMDataGridDraftActions.tsx +307 -0
- package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +58 -50
- package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +150 -115
- package/src/components/TMDataGridExportPicker.module.css +77 -0
- package/src/components/TMDataGridExportPicker.tsx +234 -0
- package/src/components/TMDataGridFilterPanel.module.css +54 -0
- package/src/components/TMDataGridFilterPanel.tsx +348 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +7 -5
- package/src/components/TMDataGridFilterSurface.module.css +54 -0
- package/src/components/TMDataGridFilterSurface.tsx +167 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -13
- package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +4 -3
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +10 -0
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +100 -28
- package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
- package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
- package/src/components/TMDataGridMenu.tsx +354 -0
- package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +12 -7
- package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +90 -67
- package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +678 -156
- package/src/components/TMDataGridToolbar.module.css +21 -0
- package/src/components/TMDataGridToolbar.tsx +181 -0
- package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
- package/src/components/editors/TMDataGridNumberEditor.tsx +70 -0
- package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/editorShared.ts +17 -31
- package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
- package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
- package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
- package/src/components/filters/controlLayout.ts +32 -0
- package/src/components/filters/filterControlFor.ts +65 -0
- package/src/{tmdatagrid/components → components}/icons.ts +1 -0
- package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
- package/src/components/useHideableColumns.ts +52 -0
- package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
- package/src/{tmdatagrid/core → core}/capabilities.ts +14 -6
- package/src/{tmdatagrid/core → core}/columnOptions.ts +46 -0
- package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
- package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
- package/src/core/controlledState.ts +179 -0
- package/src/core/controlledStateSync.ts +108 -0
- package/src/core/deletedRows.ts +34 -0
- package/src/core/dom.ts +74 -0
- package/src/core/editEngine.ts +2476 -0
- package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
- package/src/core/export.ts +843 -0
- package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
- package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
- package/src/core/filterSurface.ts +99 -0
- package/src/{tmdatagrid/core → core}/labels.ts +66 -8
- package/src/{tmdatagrid/core → core}/labelsSv.ts +26 -3
- package/src/core/pageReset.ts +120 -0
- package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
- package/src/core/resizePreview.ts +141 -0
- package/src/core/summary.ts +59 -0
- package/src/core/useSettledTableState.ts +36 -0
- package/src/{tmdatagrid/index.ts → index.ts} +75 -12
- package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +734 -135
- package/src/useTMDataGridExport.ts +78 -0
- package/src/tmdatagrid/components/TMDataGridEditActions.tsx +0 -162
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
- package/src/tmdatagrid/components/TMDataGridToolbar.module.css +0 -12
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -162
- package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +0 -40
- package/src/tmdatagrid/core/cellExport.ts +0 -320
- package/src/tmdatagrid/core/editEngine.ts +0 -1006
- package/src/tmdatagrid/core/summary.ts +0 -35
- /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
- /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
- /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
- /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
- /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
- /package/src/{tmdatagrid/core → core}/grouping.ts +0 -0
- /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
- /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
- /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { TableState } from "@tanstack/react-table";
|
|
2
|
+
import type { TMDataGridFeatures } from "../useTMDataGrid";
|
|
3
|
+
|
|
4
|
+
type GridState = TableState<TMDataGridFeatures>;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Support for TanStack's `state` option (controlled state).
|
|
8
|
+
*
|
|
9
|
+
* table-core re-reads `options.state` on every render (`useTable` calls
|
|
10
|
+
* `setOptions` while rendering) and writes any slice whose value is not
|
|
11
|
+
* identical to the table's current value into the table's atom. The write
|
|
12
|
+
* publishes the store and re-renders the consumer, so a slice object built in
|
|
13
|
+
* the render body causes an infinite render loop.
|
|
14
|
+
*
|
|
15
|
+
* {@link stabilizeControlledState} prevents the loop by forwarding the
|
|
16
|
+
* previous render's value for a slice whose contents are unchanged.
|
|
17
|
+
* {@link findFrozenStateSlices} detects a controlled slice without its
|
|
18
|
+
* `onXChange` callback, which the grid cannot write to.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The `onXChange` option that owns each controlled slice - every slice v9
|
|
23
|
+
* defines. `globalFilterFn` is not among them: in this beta it is a table
|
|
24
|
+
* option, not state.
|
|
25
|
+
*/
|
|
26
|
+
export const CONTROLLED_SLICE_HANDLERS = {
|
|
27
|
+
columnFilters: "onColumnFiltersChange",
|
|
28
|
+
columnOrder: "onColumnOrderChange",
|
|
29
|
+
columnPinning: "onColumnPinningChange",
|
|
30
|
+
columnResizing: "onColumnResizingChange",
|
|
31
|
+
columnSizing: "onColumnSizingChange",
|
|
32
|
+
columnVisibility: "onColumnVisibilityChange",
|
|
33
|
+
expanded: "onExpandedChange",
|
|
34
|
+
globalFilter: "onGlobalFilterChange",
|
|
35
|
+
grouping: "onGroupingChange",
|
|
36
|
+
pagination: "onPaginationChange",
|
|
37
|
+
rowPinning: "onRowPinningChange",
|
|
38
|
+
rowSelection: "onRowSelectionChange",
|
|
39
|
+
sorting: "onSortingChange",
|
|
40
|
+
} as const satisfies Partial<Record<keyof GridState, string>>;
|
|
41
|
+
|
|
42
|
+
export type TMDataGridControlledSlice = keyof typeof CONTROLLED_SLICE_HANDLERS;
|
|
43
|
+
|
|
44
|
+
/** The options {@link findFrozenStateSlices} reads - a subset of `TableOptions`. */
|
|
45
|
+
type ControlledOptions = {
|
|
46
|
+
state?: Partial<GridState>;
|
|
47
|
+
/**
|
|
48
|
+
* The v9 way of owning a slice: an atom the table writes through directly.
|
|
49
|
+
* It outranks `state`, so a slice backed by one is never frozen.
|
|
50
|
+
*/
|
|
51
|
+
atoms?: Record<string, unknown>;
|
|
52
|
+
} & Partial<
|
|
53
|
+
Record<(typeof CONTROLLED_SLICE_HANDLERS)[TMDataGridControlledSlice], unknown>
|
|
54
|
+
>;
|
|
55
|
+
|
|
56
|
+
const hasOwn = (value: object, key: string): boolean =>
|
|
57
|
+
Object.prototype.hasOwnProperty.call(value, key);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A plain data object: `Object.prototype` or null prototype. table-core builds
|
|
61
|
+
* its state maps with `Object.create(null)`, so null prototypes must compare
|
|
62
|
+
* as plain objects.
|
|
63
|
+
*/
|
|
64
|
+
function isDataObject(value: unknown): value is Record<string, unknown> {
|
|
65
|
+
if (typeof value !== "object" || value === null) return false;
|
|
66
|
+
const proto = Object.getPrototypeOf(value);
|
|
67
|
+
return proto === Object.prototype || proto === null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Structural equality for state values.
|
|
72
|
+
*
|
|
73
|
+
* `Date`s compare by time; the built-in date filters hold them in filter
|
|
74
|
+
* values. `Map`s and class instances compare by identity, so a controlled
|
|
75
|
+
* slice containing one rebuilt each render reads as changed on every render
|
|
76
|
+
* and re-renders the table. Use primitives, plain objects, arrays or `Date`s
|
|
77
|
+
* in controlled state.
|
|
78
|
+
*/
|
|
79
|
+
export function sameStateValue(a: unknown, b: unknown): boolean {
|
|
80
|
+
if (Object.is(a, b)) return true;
|
|
81
|
+
if (a instanceof Date || b instanceof Date) {
|
|
82
|
+
return (
|
|
83
|
+
a instanceof Date && b instanceof Date && a.getTime() === b.getTime()
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
87
|
+
if (!Array.isArray(a) || !Array.isArray(b)) return false;
|
|
88
|
+
return (
|
|
89
|
+
a.length === b.length &&
|
|
90
|
+
a.every((item, index) => sameStateValue(item, b[index]))
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (!isDataObject(a) || !isDataObject(b)) return false;
|
|
94
|
+
const keys = Object.keys(a);
|
|
95
|
+
if (keys.length !== Object.keys(b).length) return false;
|
|
96
|
+
return keys.every((key) => hasOwn(b, key) && sameStateValue(a[key], b[key]));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Removes keys whose value is `undefined`, as in
|
|
101
|
+
* `state: { sorting: cond ? sorting : undefined }`.
|
|
102
|
+
*
|
|
103
|
+
* TanStack's sync writes every present key into the slice's atom, `undefined`
|
|
104
|
+
* included, which breaks everything that reads the slice. A key set to
|
|
105
|
+
* `undefined` is treated as not controlled: the key is removed and the slice
|
|
106
|
+
* falls back to the table's own state. Identity is preserved when there is
|
|
107
|
+
* nothing to remove; the stabilizer compares by it.
|
|
108
|
+
*/
|
|
109
|
+
export function withoutUndefinedSlices(
|
|
110
|
+
state: Partial<GridState> | undefined,
|
|
111
|
+
): Partial<GridState> | undefined {
|
|
112
|
+
if (state === undefined) return state;
|
|
113
|
+
if (!Object.values(state).includes(undefined)) return state;
|
|
114
|
+
const compacted: Record<string, unknown> = {};
|
|
115
|
+
for (const [key, value] of Object.entries(state)) {
|
|
116
|
+
if (value !== undefined) compacted[key] = value;
|
|
117
|
+
}
|
|
118
|
+
return compacted as Partial<GridState>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Returns `next` with every slice that structurally equals its counterpart in
|
|
123
|
+
* `previous` replaced by the previous object, so table-core's identity-based
|
|
124
|
+
* sync finds nothing to write. Returns `previous` when nothing changed.
|
|
125
|
+
*/
|
|
126
|
+
export function stabilizeControlledState(
|
|
127
|
+
next: Partial<GridState> | undefined,
|
|
128
|
+
previous: Partial<GridState> | undefined,
|
|
129
|
+
): Partial<GridState> | undefined {
|
|
130
|
+
if (next === undefined || previous === undefined || next === previous) {
|
|
131
|
+
return next;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const keys = Object.keys(next);
|
|
135
|
+
const stabilized: Record<string, unknown> = {};
|
|
136
|
+
let changed = keys.length !== Object.keys(previous).length;
|
|
137
|
+
|
|
138
|
+
for (const key of keys) {
|
|
139
|
+
const nextValue = (next as Record<string, unknown>)[key];
|
|
140
|
+
const previousValue = (previous as Record<string, unknown>)[key];
|
|
141
|
+
if (hasOwn(previous, key) && sameStateValue(nextValue, previousValue)) {
|
|
142
|
+
// Unchanged contents: keep the object the table already holds.
|
|
143
|
+
stabilized[key] = previousValue;
|
|
144
|
+
} else {
|
|
145
|
+
stabilized[key] = nextValue;
|
|
146
|
+
changed = true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return changed ? (stabilized as Partial<GridState>) : previous;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Controlled slices without a write path: `state.x` is set but no `onXChange`
|
|
155
|
+
* was passed and no external atom owns the slice. TanStack routes every write
|
|
156
|
+
* through the callback, so such a slice cannot change. Reported so the hook
|
|
157
|
+
* can warn in development; `initialState` is the option for a starting value.
|
|
158
|
+
*/
|
|
159
|
+
export function findFrozenStateSlices(
|
|
160
|
+
options: ControlledOptions,
|
|
161
|
+
): Array<{ slice: TMDataGridControlledSlice; handler: string }> {
|
|
162
|
+
const state = options.state;
|
|
163
|
+
if (state === undefined) return [];
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
Object.entries(CONTROLLED_SLICE_HANDLERS) as Array<
|
|
167
|
+
[TMDataGridControlledSlice, string]
|
|
168
|
+
>
|
|
169
|
+
)
|
|
170
|
+
.filter(
|
|
171
|
+
([slice, handler]) =>
|
|
172
|
+
// Undefined-valued keys are removed before the table sees them; the
|
|
173
|
+
// slice is not controlled.
|
|
174
|
+
(state as Record<string, unknown>)[slice] !== undefined &&
|
|
175
|
+
(options as Record<string, unknown>)[handler] === undefined &&
|
|
176
|
+
options.atoms?.[slice] === undefined,
|
|
177
|
+
)
|
|
178
|
+
.map(([slice, handler]) => ({ slice, handler }));
|
|
179
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { ReadonlyStore, Store } from "@tanstack/store";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Timing repair for TanStack's controlled-state sync.
|
|
5
|
+
*
|
|
6
|
+
* `useTable` calls `table.setOptions` from its render body, and that syncs
|
|
7
|
+
* `options.state` into the table's atoms. An atom write publishes the store
|
|
8
|
+
* synchronously, so every component subscribed to `table.store` schedules a
|
|
9
|
+
* setState while the consumer's component is still rendering - which React
|
|
10
|
+
* reports as "Cannot update a component (X) while rendering a different
|
|
11
|
+
* component (Y)", pointing at the consumer's component rather than at the
|
|
12
|
+
* grid.
|
|
13
|
+
*
|
|
14
|
+
* The write has to stay where it is: the atoms must hold the controlled value
|
|
15
|
+
* before the table builds its row models for that render. Only the
|
|
16
|
+
* notification is early, so it moves to a microtask. `getSnapshot` already
|
|
17
|
+
* returns the new value, so a component reads the same state either way; the
|
|
18
|
+
* subscription is only what schedules a re-render for the components the
|
|
19
|
+
* consumer's own render does not reach.
|
|
20
|
+
*
|
|
21
|
+
* Scope and assumptions. Only `table.store` is patched: a subscription made
|
|
22
|
+
* on `table.atoms.*` or `table.baseAtoms.*` keeps the synchronous timing.
|
|
23
|
+
* The patch leans on three table-core 9.0.0-beta.21 internals - the store is
|
|
24
|
+
* created once in `constructTable`, `useTable` returns a shallow copy that
|
|
25
|
+
* shares it, and the sync runs inside `setOptions` during render. The
|
|
26
|
+
* publish test in controlledState.test.tsx fails loudly if any of them
|
|
27
|
+
* moves; it is the guard for this module, not a redundant check. The
|
|
28
|
+
* intended end state - owning the controlled slices through `options.atoms`,
|
|
29
|
+
* which removes the render-time sync entirely - is in the backlog.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
type Observer = ((value: unknown) => void) | { next?: (value: unknown) => void };
|
|
33
|
+
|
|
34
|
+
type Subscribe = (observer: Observer) => { unsubscribe: () => void };
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* True while `useTable` is syncing controlled state inside a render pass.
|
|
38
|
+
* Process-global, so while one grid is inside its `useTable` call a publish
|
|
39
|
+
* on any patched store is deferred too - harmless, since nothing else runs
|
|
40
|
+
* during a synchronous render pass.
|
|
41
|
+
*/
|
|
42
|
+
let syncing = false;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Called immediately before the `useTable` call that performs the sync.
|
|
46
|
+
*
|
|
47
|
+
* The microtask is the backstop that bounds the flag to the current task: if
|
|
48
|
+
* `useTable` throws and an error boundary unmounts the grid, nothing after
|
|
49
|
+
* the call runs, and without it every patched store would keep deferring its
|
|
50
|
+
* notifications for the life of the page.
|
|
51
|
+
*/
|
|
52
|
+
export function beginControlledStateSync(): void {
|
|
53
|
+
syncing = true;
|
|
54
|
+
queueMicrotask(() => {
|
|
55
|
+
syncing = false;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Called immediately after it - the prompt clear; correctness rests on the
|
|
61
|
+
* backstop above. Not a `finally` around the call, because hooks inside
|
|
62
|
+
* `try` opt the whole hook out of the React Compiler.
|
|
63
|
+
*/
|
|
64
|
+
export function endControlledStateSync(): void {
|
|
65
|
+
syncing = false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const patched = new WeakSet<object>();
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Wraps a table store's `subscribe` so notifications raised during the sync
|
|
72
|
+
* are delivered in a microtask. Idempotent, and installed on the store object
|
|
73
|
+
* itself rather than on a copy of the table - rows hold the original, so
|
|
74
|
+
* `row.table.store` has to be the patched one.
|
|
75
|
+
*/
|
|
76
|
+
export function deferControlledStateSyncPublishes<TState>(
|
|
77
|
+
store: Store<TState> | ReadonlyStore<TState>,
|
|
78
|
+
): void {
|
|
79
|
+
if (patched.has(store)) return;
|
|
80
|
+
patched.add(store);
|
|
81
|
+
|
|
82
|
+
const subscribe = store.subscribe.bind(store) as Subscribe;
|
|
83
|
+
|
|
84
|
+
(store as unknown as { subscribe: Subscribe }).subscribe = (observer) => {
|
|
85
|
+
const next =
|
|
86
|
+
typeof observer === "function" ? observer : observer.next?.bind(observer);
|
|
87
|
+
if (next === undefined) return subscribe(observer);
|
|
88
|
+
|
|
89
|
+
let live = true;
|
|
90
|
+
const subscription = subscribe((value) => {
|
|
91
|
+
if (!syncing) {
|
|
92
|
+
next(value);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
queueMicrotask(() => {
|
|
96
|
+
// The subscriber may have unmounted between the write and the flush.
|
|
97
|
+
if (live) next(value);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
unsubscribe: () => {
|
|
103
|
+
live = false;
|
|
104
|
+
subscription.unsubscribe();
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { RowData } from "@tanstack/react-table";
|
|
2
|
+
import type { TMDataGridTable } from "../useTMDataGrid";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Which rows of a table carry a deletion mark, for readers that hold the
|
|
6
|
+
* table and nothing else - `exportGrid`, `buildGridCellMatrix` and the
|
|
7
|
+
* export data they build on. The marks live in the edit engine, which those
|
|
8
|
+
* readers cannot reach; the hook registers the engine's answer per table
|
|
9
|
+
* instead. A table built without the grid has no marks.
|
|
10
|
+
*/
|
|
11
|
+
// Keyed on the store, not the table: `useTable` hands out a fresh shallow
|
|
12
|
+
// copy of the table on every render, and every copy shares the one store.
|
|
13
|
+
const sources = new WeakMap<object, (rowId: string) => boolean>();
|
|
14
|
+
|
|
15
|
+
/** Registers the answer for one table; returns the unregister. */
|
|
16
|
+
export function registerDeletedRows<TData extends RowData>(
|
|
17
|
+
table: TMDataGridTable<TData>,
|
|
18
|
+
isRowDeleted: (rowId: string) => boolean,
|
|
19
|
+
): () => void {
|
|
20
|
+
sources.set(table.store, isRowDeleted);
|
|
21
|
+
return () => {
|
|
22
|
+
if (sources.get(table.store) === isRowDeleted) {
|
|
23
|
+
sources.delete(table.store);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Whether the row is marked for deletion under `editing.draft`. */
|
|
29
|
+
export function isRowMarkedDeleted<TData extends RowData>(
|
|
30
|
+
table: TMDataGridTable<TData>,
|
|
31
|
+
rowId: string,
|
|
32
|
+
): boolean {
|
|
33
|
+
return sources.get(table.store)?.(rowId) ?? false;
|
|
34
|
+
}
|
package/src/core/dom.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM checks that survive a second realm.
|
|
3
|
+
*
|
|
4
|
+
* The grid can be rendered through a portal into a window opened with
|
|
5
|
+
* `window.open`. That window is a separate JavaScript realm: its nodes and
|
|
6
|
+
* events are instances of *its* `Node`, `Element`, `HTMLInputElement` and
|
|
7
|
+
* `MouseEvent`, not the opener's. A plain `x instanceof HTMLInputElement`
|
|
8
|
+
* is therefore false for an input sitting in the grid's own window, and the
|
|
9
|
+
* check silently takes the wrong branch.
|
|
10
|
+
*
|
|
11
|
+
* Every helper here answers the same question either by duck-typing, or by
|
|
12
|
+
* taking the constructor from the realm the value itself came from.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The window a node lives in - the realm to take its constructors from.
|
|
17
|
+
*
|
|
18
|
+
* A document made by `createHTMLDocument` has no window of its own, so the
|
|
19
|
+
* global one is the fallback.
|
|
20
|
+
*/
|
|
21
|
+
export function windowOf(node: Node): Window & typeof globalThis {
|
|
22
|
+
return node.ownerDocument?.defaultView ?? window;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Whether a value is a DOM node.
|
|
27
|
+
*
|
|
28
|
+
* Duck-typed on `nodeType`, because `instanceof Node` is false for a node
|
|
29
|
+
* from another realm.
|
|
30
|
+
*/
|
|
31
|
+
export function isNode(value: unknown): value is Node {
|
|
32
|
+
return (
|
|
33
|
+
typeof value === "object" &&
|
|
34
|
+
value !== null &&
|
|
35
|
+
typeof (value as Node).nodeType === "number"
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Whether a value is an element - a node whose `nodeType` is 1
|
|
41
|
+
* (`Node.ELEMENT_NODE`), read as a number so no realm's constant is needed.
|
|
42
|
+
*/
|
|
43
|
+
export function isElement(value: unknown): value is Element {
|
|
44
|
+
return isNode(value) && value.nodeType === 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Whether a value is an HTML element, asked of its own realm's constructor. */
|
|
48
|
+
export function isHTMLElement(value: unknown): value is HTMLElement {
|
|
49
|
+
return isElement(value) && value instanceof windowOf(value).HTMLElement;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Whether a value is an `<input>`, asked of its own realm's constructor. */
|
|
53
|
+
export function isInputElement(value: unknown): value is HTMLInputElement {
|
|
54
|
+
return isElement(value) && value instanceof windowOf(value).HTMLInputElement;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Whether a value is a `<button>`, asked of its own realm's constructor. */
|
|
58
|
+
export function isButtonElement(value: unknown): value is HTMLButtonElement {
|
|
59
|
+
return isElement(value) && value instanceof windowOf(value).HTMLButtonElement;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Whether an event is a mouse event, asked of the constructor in the event's
|
|
64
|
+
* own window.
|
|
65
|
+
*
|
|
66
|
+
* A browser-dispatched event carries that window in `view`; one built by hand
|
|
67
|
+
* may not, and its target still knows the window.
|
|
68
|
+
*/
|
|
69
|
+
export function isMouseEvent(event: Event): event is MouseEvent {
|
|
70
|
+
const view =
|
|
71
|
+
(event as UIEvent).view ??
|
|
72
|
+
(isNode(event.target) ? windowOf(event.target) : window);
|
|
73
|
+
return event instanceof (view as Window & typeof globalThis).MouseEvent;
|
|
74
|
+
}
|