@rozie-ui/data-table-lit 0.7.3 → 0.8.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/CHANGELOG.md +19 -0
- package/README.md +5 -1
- package/dist/index.cjs +534 -33
- package/dist/index.d.cts +79 -9
- package/dist/index.d.mts +79 -9
- package/dist/index.mjs +534 -33
- package/package.json +2 -2
- package/src/Column.ts +59 -23
- package/src/DataTable.ts +516 -37
- package/src/helpers/activeFollow.ts +42 -0
- package/src/helpers/columnRuleUtils.ts +105 -0
- package/src/helpers/heldView.ts +185 -0
package/src/DataTable.ts
CHANGED
|
@@ -6,7 +6,9 @@ import { ContextProvider, createContext } from '@lit/context';
|
|
|
6
6
|
import { repeat } from 'lit/directives/repeat.js';
|
|
7
7
|
import '@rozie-ui/popover-lit';
|
|
8
8
|
import { isSafeKey, wrapAggregationFn, indexDefsById, editorKindWarning, collectGroupableLeafDefs } from './helpers/columnDefUtils';
|
|
9
|
+
import { columnRuleKeys } from './helpers/columnRuleUtils';
|
|
9
10
|
import { applyUpdater, clamp, focusables } from './helpers/indexMath';
|
|
11
|
+
import { resolveFollow, sameIdSequence } from './helpers/activeFollow';
|
|
10
12
|
import { escapeTsvField, parseTsv, tileGridToBox, tileIndex } from './helpers/tsvGrid';
|
|
11
13
|
import { replaceRowValue, indexOfRowIn, replaceRowValues } from './helpers/rowValueUtils';
|
|
12
14
|
import { createTable, getCoreRowModel, getSortedRowModel, getFilteredRowModel, getPaginationRowModel, getExpandedRowModel, getGroupedRowModel,
|
|
@@ -30,6 +32,7 @@ getFacetedUniqueValues as makeFacetedUniqueValues, getFacetedMinMaxValues as mak
|
|
|
30
32
|
// guard so they never execute when off — the import token is the only static virtual-core
|
|
31
33
|
// presence. NO per-framework adapter (the codegen guard forbids @tanstack/<fw>-virtual).
|
|
32
34
|
import { Virtualizer, elementScroll, observeElementRect, observeElementOffset, measureElement } from '@tanstack/virtual-core';
|
|
35
|
+
import { createHoldController, viewStatesEqual, CLEAN_VIEW_STATE } from './helpers/heldView';
|
|
33
36
|
|
|
34
37
|
// table-core instance — top-level `let` referenced from hooks → React hoists to
|
|
35
38
|
// useRef (hoistModuleLet). NULL until $onMount: createTable lives in $onMount so its
|
|
@@ -104,6 +107,12 @@ export interface DataTableHistoryChangePayload {
|
|
|
104
107
|
canUndo: boolean;
|
|
105
108
|
canRedo: boolean;
|
|
106
109
|
}
|
|
110
|
+
/** The held-view state (`holdView`): whether the view is held right now, and how many rows the held view disagrees with. */
|
|
111
|
+
export interface DataTableViewState {
|
|
112
|
+
held: boolean;
|
|
113
|
+
filterMismatches: number;
|
|
114
|
+
sortMismatches: number;
|
|
115
|
+
}
|
|
107
116
|
/** The `range-change` payload: the new range corners (both `null` when the range cleared). */
|
|
108
117
|
export type DataTableRangeChangePayload = DataTableRange;
|
|
109
118
|
/** The `row-activate` payload. `row` is the original data object and `index` its position in the rendered model. */
|
|
@@ -118,7 +127,7 @@ export interface DataTableVisibleRangeChangePayload {
|
|
|
118
127
|
end: number;
|
|
119
128
|
}
|
|
120
129
|
|
|
121
|
-
export interface RozieDataTableEventMap extends Omit<HTMLElementEventMap, 'activecell-change' | 'cell-edit-commit' | 'expand-change' | 'filter-change' | 'group-change' | 'history-change' | 'page-change' | 'pin-change' | 'range-change' | 'reorder-change' | 'resize-change' | 'row-activate' | 'row-edit-commit' | 'selection-change' | 'sort-change' | 'visibility-change' | 'visible-range-change' | 'data-change' | 'sorting-change' | 'global-filter-change' | 'column-filters-change' | 'pagination-change' | 'expanded-change' | 'grouping-change' | 'row-selection-change' | 'column-visibility-change' | 'column-sizing-change' | 'column-order-change' | 'column-pinning-change'> {
|
|
130
|
+
export interface RozieDataTableEventMap extends Omit<HTMLElementEventMap, 'activecell-change' | 'cell-edit-commit' | 'expand-change' | 'filter-change' | 'group-change' | 'history-change' | 'page-change' | 'pin-change' | 'range-change' | 'reorder-change' | 'resize-change' | 'row-activate' | 'row-edit-commit' | 'selection-change' | 'sort-change' | 'visibility-change' | 'view-state-change' | 'visible-range-change' | 'data-change' | 'sorting-change' | 'global-filter-change' | 'column-filters-change' | 'pagination-change' | 'expanded-change' | 'grouping-change' | 'row-selection-change' | 'column-visibility-change' | 'column-sizing-change' | 'column-order-change' | 'column-pinning-change'> {
|
|
122
131
|
'activecell-change': CustomEvent<DataTableActiveCellChangePayload>;
|
|
123
132
|
'cell-edit-commit': CustomEvent<DataTableCellEditCommitPayload>;
|
|
124
133
|
'expand-change': CustomEvent<ExpandedState>;
|
|
@@ -135,6 +144,7 @@ export interface RozieDataTableEventMap extends Omit<HTMLElementEventMap, 'activ
|
|
|
135
144
|
'selection-change': CustomEvent<RowSelectionState>;
|
|
136
145
|
'sort-change': CustomEvent<SortingState>;
|
|
137
146
|
'visibility-change': CustomEvent<VisibilityState>;
|
|
147
|
+
'view-state-change': CustomEvent<DataTableViewState>;
|
|
138
148
|
'visible-range-change': CustomEvent<DataTableVisibleRangeChangePayload>;
|
|
139
149
|
'data-change': CustomEvent<any[]>;
|
|
140
150
|
'sorting-change': CustomEvent<any[]>;
|
|
@@ -683,7 +693,7 @@ export default class DataTable extends SignalWatcher(LitElement) {
|
|
|
683
693
|
@property({ type: Array, attribute: 'data' }) _data_attr!: any[];
|
|
684
694
|
private _dataControllable = createLitControllableProperty<any[]>({ host: this, eventName: 'data-change', defaultValue: [], initialControlledValue: undefined });
|
|
685
695
|
/**
|
|
686
|
-
* Config-array column fallback (lower precedence than `<Column>` children). Each entry: `{ id?, field, header?, sortable?, filterable?, pinned?, width? }
|
|
696
|
+
* Config-array column fallback (lower precedence than `<Column>` children). Each entry: `{ id?, field, header?, sortable?, filterable?, filterFn?, sortingFn?, pinned?, width? }` (`filterFn`/`sortingFn` take a table-core built-in name or a custom function). Columns may come from this array, from `<Column>` children, or both (id-keyed last-write-wins union). A `pinned` value of `'left'` or `'right'` is applied once as the table's initial `columnPinning` state — identical to `<Column pinned>` — which also REORDERS that column, since table-core orders visible cells `[left-pinned, center, right-pinned]`. A consumer who has already expressed a pin (an initial two-way `columnPinning` model value or an interactive pin) owns the slice and this declaration is ignored.
|
|
687
697
|
*/
|
|
688
698
|
@property({ type: Array }) columns: any[] = [];
|
|
689
699
|
/**
|
|
@@ -753,7 +763,7 @@ export default class DataTable extends SignalWatcher(LitElement) {
|
|
|
753
763
|
* `GroupingState` — an ordered `string[]` of column ids (multi-column → nested groups, e.g. `['region','category']`). An empty/unbound list is ungrouped (byte-identical-off). Group-header rows are collapsible (they ride the expand model). Surfaces through `group-change`; uncontrolled fallback (`$data.groupingDefault`, default `[]`) when unbound — the default is `null` (mirroring `expanded`) so the uncontrolled fallback is reachable and the grouping auto-expand default can activate when a consumer applies grouping without binding the two-way `grouping` model (a non-null `[]` default would short-circuit it). All reads are null-guarded, so table-core still receives an array.
|
|
754
764
|
*/
|
|
755
765
|
@property({ type: Array, attribute: 'grouping' }) _grouping_attr: any[] | null = null;
|
|
756
|
-
private _groupingControllable = createLitControllableProperty<any[]>({ host: this, eventName: 'grouping-change', defaultValue: null, initialControlledValue: undefined });
|
|
766
|
+
private _groupingControllable = createLitControllableProperty<any[] | null>({ host: this, eventName: 'grouping-change', defaultValue: null, initialControlledValue: undefined });
|
|
757
767
|
/**
|
|
758
768
|
* `RowSelectionState` — `{ [rowId]: true }`. Checkbox-only toggle (the row body does not select). Driven by the `selectionMode` chrome. Two-way: fires `selection-change` regardless of binding.
|
|
759
769
|
*/
|
|
@@ -805,6 +815,10 @@ export default class DataTable extends SignalWatcher(LitElement) {
|
|
|
805
815
|
* The maximum number of undo steps retained (oldest evicted past this depth). Only consulted when `undoable` is `true`.
|
|
806
816
|
*/
|
|
807
817
|
@property({ type: Number, reflect: true, attribute: 'undo-limit' }) undoLimit: number = 100;
|
|
818
|
+
/**
|
|
819
|
+
* When `true`, the table holds its current view through its own writes (edit, paste, fill, cut, clear, the boolean toggle, undo and redo): rows keep the membership and order they had before the write and new values render in place, so an edited row does not jump or vanish under an active sort or filter. The view is re-derived (and both mismatch counts reset to 0) when `sorting`, `columnFilters` or `globalFilter` change, when `refreshView()` is called, when the dataset is swapped externally, or when `holdView` is toggled. Paging, page size, column visibility/order/pinning/sizing and expanding rows move through the held rows; a table write or `refreshView()` never changes the page (a sort, filter or grouping change returns to the first page). Falls back to live behaviour (with one console warning) while grouping is active, in `manual` mode, or with tree data (`getSubRows`). Passing a fresh `data` array that did not come from the table (for example reloading from the server after a save) is an external swap: it re-derives the view and clears undo history. Track mismatches with `view-state-change` / `getViewState()`.
|
|
820
|
+
*/
|
|
821
|
+
@property({ type: Boolean, reflect: true, attribute: 'hold-view' }) holdView: boolean = false;
|
|
808
822
|
/**
|
|
809
823
|
* Opt-in windowing grammar: `false` (default, off — byte-identical to a non-virtual table) | `true` or `'rows'` (vertical row windowing; `true` is byte-behavior-identical to every existing consumer, zero churn) | `'columns'` (horizontal column windowing) | `'both'` (both axes windowed). Row windowing renders only the visible slice of rows inside a bounded `rdt-scroll` container (with leading/trailing spacer rows preserving total scroll height), windowing over the full filtered + sorted (pre-pagination) model and suppressing the client pagination chrome. Column windowing renders only the visible slice of leaf columns inside the same `rdt-scroll` container. An unrecognised string behaves as `false`.
|
|
810
824
|
*/
|
|
@@ -1144,7 +1158,8 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
1144
1158
|
this._disconnectCleanups.push(effect(() => { const __watchVal = (() => [this.sorting, this.globalFilter, this.columnFilters, this.pagination, // Server-side page-count sources (#2): re-feed when the consumer's rowCount/pageCount
|
|
1145
1159
|
// changes at runtime (e.g. a server response updates the total) so getPageCount() and the
|
|
1146
1160
|
// Next button availability track the new total.
|
|
1147
|
-
this.rowCount, this.pageCount, this.rowSelection, this.expanded, this.expandable, this.grouping, this.groupable, this.columnVisibility, this.columnSizing, this.columnOrder, this.columnPinning, this.selectionMode,
|
|
1161
|
+
this.rowCount, this.pageCount, this.rowSelection, this.expanded, this.expandable, this.grouping, this.groupable, this.columnVisibility, this.columnSizing, this.columnOrder, this.columnPinning, this.selectionMode, // Held view: toggling holdView re-derives (reFeed → setActive).
|
|
1162
|
+
this.holdView, (this.data || []).length, // Phase 51 req-4: key on the data REFERENCE (both sinks) so a committed edit re-feeds
|
|
1148
1163
|
// even when the fresh array is the SAME length (a single-cell edit replaces one row
|
|
1149
1164
|
// object → new array ref, identical length → the .length key alone would miss it). The
|
|
1150
1165
|
// controlled path observes $props.data; the uncontrolled path observes $data.dataDefault.
|
|
@@ -1212,6 +1227,8 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
1212
1227
|
// call. One-shot and self-latching — see seedColumnPinning() in stateAssembly.rzts.
|
|
1213
1228
|
this.seedColumnPinning();
|
|
1214
1229
|
// Build the table instance HERE so the closures below capture the live `table`.
|
|
1230
|
+
const cols0 = this.tableColumns();
|
|
1231
|
+
this.lastColumnSig = this.columnSetSignature(cols0);
|
|
1215
1232
|
this.table = createTable({
|
|
1216
1233
|
// Plain value (NOT a `get data()` getter): an object-literal getter rebinds
|
|
1217
1234
|
// `this` to the options object, and the Angular/Lit emitters resolve $props via
|
|
@@ -1222,11 +1239,12 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
1222
1239
|
// currentData() = the bound prop when controlled, else the uncontrolled $data.dataDefault
|
|
1223
1240
|
// (Phase 51 req-4 — so a committed edit's writeData re-feed is observed either way).
|
|
1224
1241
|
data: this.feedData(),
|
|
1225
|
-
columns:
|
|
1242
|
+
columns: cols0,
|
|
1226
1243
|
state: this.currentState(),
|
|
1227
1244
|
getCoreRowModel: getCoreRowModel(),
|
|
1228
|
-
|
|
1229
|
-
|
|
1245
|
+
// Held view: the wrapped factories, ALWAYS (see holdCtrl's declaration).
|
|
1246
|
+
getSortedRowModel: this.heldSortedRowModel,
|
|
1247
|
+
getFilteredRowModel: this.heldFilteredRowModel,
|
|
1230
1248
|
getPaginationRowModel: getPaginationRowModel(),
|
|
1231
1249
|
// Expandable rows (phase 50, D-04): the expanded row model is supplied UNCONDITIONALLY
|
|
1232
1250
|
// (mirrors the other models) — inert when `expanded` is empty + no getSubRows
|
|
@@ -1311,6 +1329,42 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
1311
1329
|
// satisfies the strict bundled-leaf tsc (deferred-items strict-tsc #2 close).
|
|
1312
1330
|
onStateChange: () => {}
|
|
1313
1331
|
});
|
|
1332
|
+
// Page index (final review #1). table-core's autoResetPageIndex (on by default unless
|
|
1333
|
+
// manualPagination) queues `table.resetPageIndex()` in a microtask whenever the core,
|
|
1334
|
+
// filtered, sorted or grouped memo recomputes — which includes every table write (new data)
|
|
1335
|
+
// and refreshView()'s state copy — so a write on page 2 jumped back to page 1 and left the
|
|
1336
|
+
// active cell on another record. The page reset is explicit instead: reFeed returns to page 1
|
|
1337
|
+
// on a sort / columnFilters / globalFilter / grouping change. An external data swap keeps the
|
|
1338
|
+
// page (the #4 overflow clamp in refreshRowModel still pulls it back when the new data has
|
|
1339
|
+
// fewer pages): the swap is detected on different ticks per target (reFeed vs
|
|
1340
|
+
// maybeClearHistoryOnExternalSwap), so a reset there would race the auto-reset's re-write.
|
|
1341
|
+
// The queued auto-reset is NOT switched off (autoResetPageIndex: false): its pagination write
|
|
1342
|
+
// is a re-render tick that several React/Solid paths have come to rely on (measured: B23/WR-01
|
|
1343
|
+
// relocated-row focus, the column-virtual window, header tab stop, editor focus return all
|
|
1344
|
+
// regress without it — the focus polls run before the framework commits the re-sorted rows).
|
|
1345
|
+
// So it is kept, but it re-writes the CURRENT page (table-core's fed state — fresh on every
|
|
1346
|
+
// target, and already page 1 after an explicit reset) instead of turning to page 1.
|
|
1347
|
+
this.table.resetPageIndex = () => {
|
|
1348
|
+
if (!this.table) return;
|
|
1349
|
+
const pg = this.table.getState().pagination;
|
|
1350
|
+
this.writePagination({
|
|
1351
|
+
pageIndex: pg.pageIndex,
|
|
1352
|
+
pageSize: pg.pageSize
|
|
1353
|
+
});
|
|
1354
|
+
};
|
|
1355
|
+
// Held view: seed the sort/filter references the table was built with, so the first reFeed
|
|
1356
|
+
// (often the first write's own data re-feed) does not misread them as a view change and
|
|
1357
|
+
// release a just-captured hold.
|
|
1358
|
+
const st0 = this.currentState();
|
|
1359
|
+
this.lastHoldSorting = st0.sorting;
|
|
1360
|
+
this.lastHoldColumnFilters = st0.columnFilters;
|
|
1361
|
+
this.lastHoldGlobalFilter = st0.globalFilter;
|
|
1362
|
+
this.lastPageGrouping = st0.grouping;
|
|
1363
|
+
this.syncHoldActive(st0);
|
|
1364
|
+
// Seed the external-swap baseline with the mount-time dataset, so the first re-feed after an
|
|
1365
|
+
// early write (before any maybeClearHistoryOnExternalSwap ran) cannot misread the initial,
|
|
1366
|
+
// unstamped `$props.data` as a swap and release a just-captured hold.
|
|
1367
|
+
this.lastPropsData = this.data;
|
|
1314
1368
|
this.refreshRowModel = () => {
|
|
1315
1369
|
if (!this.table) return;
|
|
1316
1370
|
// Capture fresh locals; never write a $data key then re-read it in the same fn
|
|
@@ -1340,15 +1394,32 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
1340
1394
|
// through here) — guarded internally on colsWindowed() && colVirtualizer, so this is a no-op
|
|
1341
1395
|
// on every other path.
|
|
1342
1396
|
this.remeasureColumnWindow();
|
|
1343
|
-
// D-05: on every data change (re-sort/filter/paginate/page-size — all
|
|
1344
|
-
//
|
|
1345
|
-
//
|
|
1346
|
-
//
|
|
1397
|
+
// Part 3 + D-05: on every data change (re-sort/filter/paginate/page-size/writes — all
|
|
1398
|
+
// re-pull here) the active cell FOLLOWS its record by row id; where it cannot (paging,
|
|
1399
|
+
// expand/collapse, an external swap) it falls back to the D-05 clamp (same indices, clamped
|
|
1400
|
+
// if the grid shrank), and a record that left the view after a view change or a table write
|
|
1401
|
+
// parks on its column header (followActiveCell, gridKeydownHandlers.rzts). isGrid()-gated so
|
|
1402
|
+
// 'table' mode is untouched.
|
|
1403
|
+
// B8/B23: pass the FRESH bounds and ids derived from `nextRows` (NOT $data.rows, which is the
|
|
1347
1404
|
// async-stale useState snapshot on React) so a filter-to-fewer clamps the active cell AND
|
|
1348
|
-
// the range corners on React too — never re-reading the pre-change model.
|
|
1405
|
+
// the range corners on React too — never re-reading the pre-change model. The follow flags
|
|
1406
|
+
// are plain `let`s, read and cleared here (ROZ138-safe).
|
|
1349
1407
|
const nextRowCount = nextRows.length;
|
|
1350
1408
|
const nextColCount = nextRows.length ? nextRows[0].getVisibleCells().length : nextGroups.length ? (nextGroups[nextGroups.length - 1].headers || []).length : 0;
|
|
1351
|
-
|
|
1409
|
+
const nextIds = nextRows.map((r: any) => r.id);
|
|
1410
|
+
const fedData = this.table.options.data;
|
|
1411
|
+
const dataChanged = fedData !== this.lastFedData;
|
|
1412
|
+
this.lastFedData = fedData;
|
|
1413
|
+
const viewChangeOrWrite = this.pendingViewChange || this.pendingWrite && dataChanged;
|
|
1414
|
+
// Final review #8: new data that is NOT one of our writes (an external swap) without
|
|
1415
|
+
// `getRowId` — the row ids are source indices, meaningless across the swap, so following
|
|
1416
|
+
// them could land the active cell (or an open editor) on an unrelated record. Don't follow:
|
|
1417
|
+
// the D-05 clamp keeps today's position. Reads table.options (fresh on every target).
|
|
1418
|
+
const swapWithoutIds = dataChanged && !this.pendingWrite && !this.pendingViewChange && this.table.options.getRowId == null;
|
|
1419
|
+
this.pendingViewChange = false;
|
|
1420
|
+
if (dataChanged) this.pendingWrite = false;
|
|
1421
|
+
this.followActiveCell(swapWithoutIds ? null : this.prevRowIds, nextIds, viewChangeOrWrite, nextRowCount, nextColCount, this.pendingEditFollow != null);
|
|
1422
|
+
this.prevRowIds = nextIds;
|
|
1352
1423
|
// #4: clamp a pageIndex that now points PAST the last page. When the consumer holds
|
|
1353
1424
|
// pagination.pageIndex (controlled) and shrinks the data (filter / replace) so there are
|
|
1354
1425
|
// fewer pages, the body renders blank ("Page 6 of 3" with Next disabled). Read table-core's
|
|
@@ -1362,11 +1433,11 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
1362
1433
|
// the clamp back through the pagination prop, the re-feed re-enters here with
|
|
1363
1434
|
// pageIndex === pc - 1, so `pageIndex > pc - 1` is false → no re-emit; a consumer that
|
|
1364
1435
|
// ignores the event triggers no further re-feed, so it stays a single emit either way.
|
|
1365
|
-
// •
|
|
1366
|
-
//
|
|
1367
|
-
//
|
|
1368
|
-
// overflow — if the index is already valid we stay silent (uncontrolled self-heals too,
|
|
1369
|
-
// writing paginationDefault
|
|
1436
|
+
// • table-core's auto page reset no longer turns the page (table.resetPageIndex is
|
|
1437
|
+
// overridden after createTable): the sort/filter/grouping page reset is explicit (reFeed),
|
|
1438
|
+
// and a table write, refreshView() or an external swap never turns the page. This
|
|
1439
|
+
// clamp therefore only fires on a genuine overflow — if the index is already valid we stay silent (uncontrolled self-heals too,
|
|
1440
|
+
// writing paginationDefault).
|
|
1370
1441
|
const pgState = this.table.getState().pagination;
|
|
1371
1442
|
const pc = this.table.getPageCount();
|
|
1372
1443
|
if (pc > 0 && pgState.pageIndex > pc - 1) {
|
|
@@ -1392,6 +1463,17 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
1392
1463
|
// across selection changes; a microtask defer covers React's post-render DOM patch.
|
|
1393
1464
|
this.syncIndeterminate();
|
|
1394
1465
|
if (typeof queueMicrotask !== 'undefined') queueMicrotask(this.syncIndeterminate);else Promise.resolve().then(this.syncIndeterminate);
|
|
1466
|
+
// Held view: edge-triggered view-state-change (only when held/filterMismatches/
|
|
1467
|
+
// sortMismatches changed). lastViewState is a plain `let` (ROZ138-safe).
|
|
1468
|
+
const vs = this.holdCtrl.viewState(this.table);
|
|
1469
|
+
if (!viewStatesEqual(vs, this.lastViewState)) {
|
|
1470
|
+
this.lastViewState = vs;
|
|
1471
|
+
this.dispatchEvent(new CustomEvent<DataTableViewState>("view-state-change", {
|
|
1472
|
+
detail: vs,
|
|
1473
|
+
bubbles: true,
|
|
1474
|
+
composed: true
|
|
1475
|
+
}));
|
|
1476
|
+
}
|
|
1395
1477
|
};
|
|
1396
1478
|
|
|
1397
1479
|
// initial pull
|
|
@@ -1547,10 +1629,11 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
1547
1629
|
}
|
|
1548
1630
|
|
|
1549
1631
|
updated(changedProperties: Map<string, unknown>): void {
|
|
1550
|
-
if (this.__rozieFirstUpdateDone && (changedProperties.has('rowCount') || changedProperties.has('pageCount') || changedProperties.has('expandable') || changedProperties.has('groupable') || changedProperties.has('selectionMode') || changedProperties.has('columns'))) { const __watchVal = untracked(() => [this.sorting, this.globalFilter, this.columnFilters, this.pagination, // Server-side page-count sources (#2): re-feed when the consumer's rowCount/pageCount
|
|
1632
|
+
if (this.__rozieFirstUpdateDone && (changedProperties.has('rowCount') || changedProperties.has('pageCount') || changedProperties.has('expandable') || changedProperties.has('groupable') || changedProperties.has('selectionMode') || changedProperties.has('holdView') || changedProperties.has('columns'))) { const __watchVal = untracked(() => [this.sorting, this.globalFilter, this.columnFilters, this.pagination, // Server-side page-count sources (#2): re-feed when the consumer's rowCount/pageCount
|
|
1551
1633
|
// changes at runtime (e.g. a server response updates the total) so getPageCount() and the
|
|
1552
1634
|
// Next button availability track the new total.
|
|
1553
|
-
this.rowCount, this.pageCount, this.rowSelection, this.expanded, this.expandable, this.grouping, this.groupable, this.columnVisibility, this.columnSizing, this.columnOrder, this.columnPinning, this.selectionMode,
|
|
1635
|
+
this.rowCount, this.pageCount, this.rowSelection, this.expanded, this.expandable, this.grouping, this.groupable, this.columnVisibility, this.columnSizing, this.columnOrder, this.columnPinning, this.selectionMode, // Held view: toggling holdView re-derives (reFeed → setActive).
|
|
1636
|
+
this.holdView, (this.data || []).length, // Phase 51 req-4: key on the data REFERENCE (both sinks) so a committed edit re-feeds
|
|
1554
1637
|
// even when the fresh array is the SAME length (a single-cell edit replaces one row
|
|
1555
1638
|
// object → new array ref, identical length → the .length key alone would miss it). The
|
|
1556
1639
|
// controlled path observes $props.data; the uncontrolled path observes $data.dataDefault.
|
|
@@ -1640,7 +1723,7 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
1640
1723
|
if (name === 'column-filters') this._columnFiltersControllable.notifyAttributeChange(value as unknown as any[]);
|
|
1641
1724
|
if (name === 'pagination') this._paginationControllable.notifyAttributeChange(value as unknown as any);
|
|
1642
1725
|
if (name === 'expanded') this._expandedControllable.notifyAttributeChange(value as unknown as any | boolean);
|
|
1643
|
-
if (name === 'grouping') this._groupingControllable.notifyAttributeChange(value as unknown as any[]);
|
|
1726
|
+
if (name === 'grouping') this._groupingControllable.notifyAttributeChange(value as unknown as any[] | null);
|
|
1644
1727
|
if (name === 'row-selection') this._rowSelectionControllable.notifyAttributeChange(value as unknown as any);
|
|
1645
1728
|
if (name === 'column-visibility') this._columnVisibilityControllable.notifyAttributeChange(value as unknown as any);
|
|
1646
1729
|
if (name === 'column-sizing') this._columnSizingControllable.notifyAttributeChange(value as unknown as any);
|
|
@@ -2022,6 +2105,108 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2022
2105
|
|
|
2023
2106
|
restoringHistory: boolean = false;
|
|
2024
2107
|
|
|
2108
|
+
// ── Held view (`holdView`) ─────────────────────────────────────────────────────────────
|
|
2109
|
+
// ONE hold controller per instance (helpers/heldView.ts) and the two wrapped row-model
|
|
2110
|
+
// factories built ONCE from it, so createTable and every reFeed setOptions pass the SAME
|
|
2111
|
+
// function references (a fresh wrapper per re-feed would rebuild table-core's memoized
|
|
2112
|
+
// stage). Both wrapped factories are passed ALWAYS — not only while holding — because the
|
|
2113
|
+
// sort-mismatch count is only correct when wrapSorted observes the live sort. While the
|
|
2114
|
+
// controller is not holding, the wrappers return the base model unchanged (same identity),
|
|
2115
|
+
// so a table without `holdView` is behaviour-identical. Top-level `const`s: React lowers each
|
|
2116
|
+
// to a `useMemo(..., [])` (one instance per component). NOT `let`: React hoists a `let` to
|
|
2117
|
+
// `useRef(init)` and does not rewrite a sibling hoisted `let` read inside that initializer to
|
|
2118
|
+
// `.current`, so `let heldX = holdCtrl.wrapX(...)` would call `wrapX` on the ref object.
|
|
2119
|
+
holdCtrl = createHoldController();
|
|
2120
|
+
|
|
2121
|
+
heldFilteredRowModel = this.holdCtrl.wrapFiltered(getFilteredRowModel());
|
|
2122
|
+
|
|
2123
|
+
heldSortedRowModel = this.holdCtrl.wrapSorted(getSortedRowModel());
|
|
2124
|
+
|
|
2125
|
+
// Set when the view was re-derived (a sort/filter change, refreshView()) and by writeData on
|
|
2126
|
+
// every table write; consumed by the active-cell follow (Task 5). Plain `let`s, NOT $data —
|
|
2127
|
+
// written and read inside refreshRowModel's call graph (ROZ138 / React stale reads).
|
|
2128
|
+
pendingViewChange = false;
|
|
2129
|
+
|
|
2130
|
+
pendingWrite = false;
|
|
2131
|
+
|
|
2132
|
+
// Part 3 (active cell follows its record): the visible row ids at the end of the last
|
|
2133
|
+
// refreshRowModel, and the data array that refresh fed. pendingWrite is consumed only by the
|
|
2134
|
+
// refresh whose fed data CHANGED — in controlled `r-model:data` mode writeData's own
|
|
2135
|
+
// $data.dataDefault write re-feeds the OLD `$props.data` first, and the parent's echo arrives
|
|
2136
|
+
// a pass later; consuming the flag on that first, unchanged pass would turn the write's
|
|
2137
|
+
// vanished row into a clamp instead of a park.
|
|
2138
|
+
prevRowIds: string[] | null = null;
|
|
2139
|
+
|
|
2140
|
+
lastFedData: unknown = null;
|
|
2141
|
+
|
|
2142
|
+
// The last emitted view-state-change payload (refreshRowModel edge-triggers the emit).
|
|
2143
|
+
lastViewState = CLEAN_VIEW_STATE;
|
|
2144
|
+
|
|
2145
|
+
// reFeed bookkeeping: the sort/filter references fed last time (a change releases the hold)
|
|
2146
|
+
// and the one-shot fallback warning latch.
|
|
2147
|
+
lastHoldSorting: unknown = null;
|
|
2148
|
+
|
|
2149
|
+
lastHoldColumnFilters: unknown = null;
|
|
2150
|
+
|
|
2151
|
+
lastHoldGlobalFilter: unknown = null;
|
|
2152
|
+
|
|
2153
|
+
holdFallbackWarned = false;
|
|
2154
|
+
|
|
2155
|
+
// The grouping reference fed last time (a change returns to page 1 — see table.resetPageIndex).
|
|
2156
|
+
lastPageGrouping: unknown = null;
|
|
2157
|
+
|
|
2158
|
+
// The column-set signature fed last time (see columnSetSignature / reFeed).
|
|
2159
|
+
lastColumnSig = '';
|
|
2160
|
+
|
|
2161
|
+
// A signature of the column set as table-core's sort / filter stages see it: each column's id
|
|
2162
|
+
// and whether it can sort / filter (groups flattened, iteratively — no self-recursive const,
|
|
2163
|
+
// which React lowers to a useCallback). table-core memoizes those stages on the sorting /
|
|
2164
|
+
// columnFilters references and the upstream model only, NOT on the column defs; a stage that
|
|
2165
|
+
// first ran before a sorted/filtered column existed (a <Column> child registering after the
|
|
2166
|
+
// first pull, the norm on React/Solid/Lit) silently skipped that sort/filter and stays cached.
|
|
2167
|
+
columnSetSignature = (defs: any) => {
|
|
2168
|
+
let sig = '';
|
|
2169
|
+
const stack = (defs || []).slice().reverse();
|
|
2170
|
+
while (stack.length) {
|
|
2171
|
+
const d = stack.pop();
|
|
2172
|
+
if (!d) continue;
|
|
2173
|
+
sig += String(d.id) + (d.enableSorting ? '+' : '') + (d.enableColumnFilter ? '*' : '') + '|';
|
|
2174
|
+
if (Array.isArray(d.columns)) for (let i = d.columns.length - 1; i >= 0; i--) stack.push(d.columns[i]);
|
|
2175
|
+
}
|
|
2176
|
+
return sig;
|
|
2177
|
+
};
|
|
2178
|
+
|
|
2179
|
+
// The last `$props.data` reference maybeClearHistoryOnExternalSwap examined (seeded in
|
|
2180
|
+
// $onMount). Declared up here, before reFeed, so reFeed can call isExternalSwap() without a
|
|
2181
|
+
// forward reference (React useCallback ordering).
|
|
2182
|
+
lastPropsData: unknown = null;
|
|
2183
|
+
|
|
2184
|
+
// Read-only: has `$props.data` changed to a reference we did NOT write since the last
|
|
2185
|
+
// maybeClearHistoryOnExternalSwap? (reFeed uses it to release a held view before its refresh.)
|
|
2186
|
+
isExternalSwap = () => {
|
|
2187
|
+
const pd = this.data;
|
|
2188
|
+
if (pd === this.lastPropsData) return false;
|
|
2189
|
+
return !(pd != null && (pd as any)[this.DATA_WRITE_TOKEN_KEY] != null);
|
|
2190
|
+
};
|
|
2191
|
+
|
|
2192
|
+
// Whether the hold applies for state `st`: holdView on, and not one of the live-behaviour
|
|
2193
|
+
// fallbacks (grouping active, `manual`, tree data). Called from $onMount (before the first
|
|
2194
|
+
// pull) and from every reFeed; warns once when holdView is on but cannot hold.
|
|
2195
|
+
syncHoldActive = (st: any) => {
|
|
2196
|
+
const wanted = this.holdView === true;
|
|
2197
|
+
const active = wanted && this.manual !== true && this.getSubRows == null && !(st.grouping || []).length;
|
|
2198
|
+
// A setActive that RELEASES a hold (holdView on→off, or a fallback flipping the controller
|
|
2199
|
+
// inactive while it was holding) re-derives the live view: for the active-cell follow that
|
|
2200
|
+
// is a view change (a record the live view drops parks on its header).
|
|
2201
|
+
const wasHolding = this.holdCtrl.isHolding();
|
|
2202
|
+
this.holdCtrl.setActive(active);
|
|
2203
|
+
if (wasHolding && !this.holdCtrl.isHolding()) this.pendingViewChange = true;
|
|
2204
|
+
if (wanted && !active && !this.holdFallbackWarned) {
|
|
2205
|
+
this.holdFallbackWarned = true;
|
|
2206
|
+
console.warn('[rozie-data-table] holdView falls back to live behaviour while grouping is active, in manual mode, or with tree data (getSubRows); the view is not held.');
|
|
2207
|
+
}
|
|
2208
|
+
};
|
|
2209
|
+
|
|
2025
2210
|
// Grouping auto-expand latch (phase 50 req-4): when grouping is ACTIVE and the consumer
|
|
2026
2211
|
// has not bound `expanded` and has not yet toggled any group, group-header rows default to
|
|
2027
2212
|
// EXPANDED (so the grouped subtree is visible — the standard grouped-grid affordance + the
|
|
@@ -2313,6 +2498,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2313
2498
|
// a custom fn defensively wrapped (T-50-04).
|
|
2314
2499
|
groupable: c.groupable !== false,
|
|
2315
2500
|
aggregationFn: wrapAggregationFn(c.aggregationFn),
|
|
2501
|
+
// Per-column filterFn/sortingFn (built-in name or custom fn); absent when unset.
|
|
2502
|
+
...columnRuleKeys(c, id),
|
|
2316
2503
|
pinned: c.pinned != null ? c.pinned : '',
|
|
2317
2504
|
width: c.width != null ? c.width : '',
|
|
2318
2505
|
// A-01 — `width` alone is inert; table-core sizes from `size`. Spread so an
|
|
@@ -2408,6 +2595,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2408
2595
|
// Grouping (phase 50 reqs 4-7) — same shape as the config branch (D-05 / T-50-04).
|
|
2409
2596
|
groupable: spec.groupable !== false,
|
|
2410
2597
|
aggregationFn: wrapAggregationFn(spec.aggregationFn),
|
|
2598
|
+
...columnRuleKeys(spec, id),
|
|
2411
2599
|
pinned: spec.pinned != null ? spec.pinned : '',
|
|
2412
2600
|
width: spec.width != null ? spec.width : '',
|
|
2413
2601
|
// A-01 — see buildConfigDef; same mapping for the <Column> registry branch.
|
|
@@ -2764,6 +2952,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2764
2952
|
this.recordSnapshot(this.currentData());
|
|
2765
2953
|
this.emitHistoryChangeIfEdged(prevU, prevR);
|
|
2766
2954
|
}
|
|
2955
|
+
// Held view (`holdView`): freeze the view as it is BEFORE this write (a no-op when the
|
|
2956
|
+
// controller is inactive or already holding), then note which source rows this write
|
|
2957
|
+
// replaced (the sort-mismatch count). Outside the `!restoringHistory` branch on purpose:
|
|
2958
|
+
// undo/redo replays are table writes too. pendingWrite is consumed by the active-cell
|
|
2959
|
+
// follow.
|
|
2960
|
+
if (this.table) this.holdCtrl.capture(this.table);
|
|
2961
|
+
this.holdCtrl.noteWrite(this.currentData() || [], Array.isArray(next) ? next : []);
|
|
2962
|
+
this.pendingWrite = true;
|
|
2767
2963
|
const fresh = Array.isArray(next) ? next.slice() : next;
|
|
2768
2964
|
try {
|
|
2769
2965
|
Object.defineProperty(fresh, this.DATA_WRITE_TOKEN_KEY, {
|
|
@@ -4312,6 +4508,18 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4312
4508
|
return this.lazyRowsActive() ? (row: any) => !this.rowIsLazyPlaceholder(row) : true;
|
|
4313
4509
|
};
|
|
4314
4510
|
|
|
4511
|
+
// Funnel a pagination write the table makes on its own (reFeed's page reset). writePagination is echo-guarded by `programmatic`: a re-feed that runs
|
|
4512
|
+
// synchronously INSIDE another slice's funnel (a header filter input on the fine-grained
|
|
4513
|
+
// targets) would have it silently dropped, so defer it to a microtask then.
|
|
4514
|
+
writePaginationSoon = (next: any) => {
|
|
4515
|
+
if (!this.programmatic) {
|
|
4516
|
+
this.writePagination(next);
|
|
4517
|
+
return;
|
|
4518
|
+
}
|
|
4519
|
+
const run = () => this.writePagination(next);
|
|
4520
|
+
if (typeof queueMicrotask !== 'undefined') queueMicrotask(run);else Promise.resolve().then(run);
|
|
4521
|
+
};
|
|
4522
|
+
|
|
4315
4523
|
// Push fresh options into table-core + re-pull the row model. Extracted so BOTH the
|
|
4316
4524
|
// re-feed $watch (above) and the Lit data-change $onUpdate (below) call it.
|
|
4317
4525
|
reFeed = () => {
|
|
@@ -4322,11 +4530,78 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4322
4530
|
// TRANSIENTLY-STALE `$props.data` and wrongly wipe a just-recorded edit's history. The reset is
|
|
4323
4531
|
// keyed on the `$props.data` REFERENCE actually changing instead — see the $onUpdate backstop
|
|
4324
4532
|
// below (`maybeClearHistoryOnExternalSwap`), which runs on all six targets.
|
|
4533
|
+
//
|
|
4534
|
+
// Held view: decide whether the hold applies (it falls back to live behaviour under
|
|
4535
|
+
// grouping, `manual`, and tree data), and re-derive on a sort/filter change. Fresh locals
|
|
4536
|
+
// only — the state object is read once here and passed to setOptions below.
|
|
4537
|
+
const st0 = this.currentState();
|
|
4538
|
+
this.syncHoldActive(st0);
|
|
4539
|
+
let st = st0;
|
|
4540
|
+
// Page reset (table-core's auto reset no longer turns the page — see table.resetPageIndex): a sort / filter / grouping change
|
|
4541
|
+
// returns to page 1. The first page is fed to table-core in THIS re-feed (a fresh state
|
|
4542
|
+
// object), so the re-derive — and the active-cell follow, which resolves against the current
|
|
4543
|
+
// page — already sees page 1; writePagination (after the refresh, below) carries it to the
|
|
4544
|
+
// consumer's pagination model. Skipped when already on page 1 (no spurious page-change) and
|
|
4545
|
+
// under `manual` (table-core never auto-reset there either).
|
|
4546
|
+
let pageReset: {
|
|
4547
|
+
pageIndex: number;
|
|
4548
|
+
pageSize: number;
|
|
4549
|
+
} | null = null;
|
|
4550
|
+
const groupingChanged = st0.grouping !== this.lastPageGrouping;
|
|
4551
|
+
this.lastPageGrouping = st0.grouping;
|
|
4552
|
+
const viewChanged = st0.sorting !== this.lastHoldSorting || st0.columnFilters !== this.lastHoldColumnFilters || st0.globalFilter !== this.lastHoldGlobalFilter;
|
|
4553
|
+
if (viewChanged) {
|
|
4554
|
+
this.lastHoldSorting = st0.sorting;
|
|
4555
|
+
this.lastHoldColumnFilters = st0.columnFilters;
|
|
4556
|
+
this.lastHoldGlobalFilter = st0.globalFilter;
|
|
4557
|
+
this.holdCtrl.release();
|
|
4558
|
+
this.pendingViewChange = true;
|
|
4559
|
+
}
|
|
4560
|
+
if ((viewChanged || groupingChanged) && this.manual !== true && st0.pagination && st0.pagination.pageIndex !== 0) {
|
|
4561
|
+
pageReset = {
|
|
4562
|
+
pageIndex: 0,
|
|
4563
|
+
pageSize: st0.pagination.pageSize
|
|
4564
|
+
};
|
|
4565
|
+
st = {
|
|
4566
|
+
...st0,
|
|
4567
|
+
pagination: pageReset
|
|
4568
|
+
};
|
|
4569
|
+
}
|
|
4570
|
+
// The column set changed (a <Column> registered, a `columns` entry was added, a column became
|
|
4571
|
+
// sortable/filterable): table-core's sort and filter stages do not key on column defs, so a
|
|
4572
|
+
// sort or filter that referenced a column which did not exist yet stays skipped (an initial
|
|
4573
|
+
// `sorting` was ignored on React/Solid/Lit). Feed shallow copies of both arrays — the same
|
|
4574
|
+
// memo invalidation refreshView() uses — so they re-run against the new columns. Not a view
|
|
4575
|
+
// change: the hold (if any) is kept and the page is not reset.
|
|
4576
|
+
const cols = this.tableColumns();
|
|
4577
|
+
const colSig = this.columnSetSignature(cols);
|
|
4578
|
+
if (colSig !== this.lastColumnSig) {
|
|
4579
|
+
this.lastColumnSig = colSig;
|
|
4580
|
+
st = {
|
|
4581
|
+
...st,
|
|
4582
|
+
sorting: [...(st.sorting || [])],
|
|
4583
|
+
columnFilters: [...(st.columnFilters || [])]
|
|
4584
|
+
};
|
|
4585
|
+
}
|
|
4586
|
+
// An external swap releases the hold BEFORE this re-feed's refresh, so the one
|
|
4587
|
+
// view-state-change it emits is already the re-derived state (never a stale held count
|
|
4588
|
+
// measured against the new dataset). Only the hold is touched here — the history clear and
|
|
4589
|
+
// the lastPropsData bookkeeping stay in maybeClearHistoryOnExternalSwap (keyed on the
|
|
4590
|
+
// `$props.data` reference; see its comment). isExternalSwap() is read-only and compares
|
|
4591
|
+
// against that same lastPropsData, so a transiently-stale `$props.data` (the reference
|
|
4592
|
+
// already recorded) never reads as a swap.
|
|
4593
|
+
if (this.holdCtrl.isHolding() && this.isExternalSwap()) this.holdCtrl.release();
|
|
4594
|
+
// An external swap is a reset, not one of our writes: drop a write flag the echo never
|
|
4595
|
+
// consumed (one-way `data`, no r-model), so the swap keeps the D-05 clamp fallback.
|
|
4596
|
+
if (this.pendingWrite && this.isExternalSwap()) this.pendingWrite = false;
|
|
4325
4597
|
this.table.setOptions((prev: any) => ({
|
|
4326
4598
|
...prev,
|
|
4327
4599
|
data: this.feedData(),
|
|
4328
|
-
columns:
|
|
4329
|
-
state:
|
|
4600
|
+
columns: cols,
|
|
4601
|
+
state: st,
|
|
4602
|
+
// Held view: the SAME wrapped factory references every re-feed (see holdCtrl).
|
|
4603
|
+
getFilteredRowModel: this.heldFilteredRowModel,
|
|
4604
|
+
getSortedRowModel: this.heldSortedRowModel,
|
|
4330
4605
|
enableRowSelection: this.rowSelectionOption(),
|
|
4331
4606
|
enableMultiRowSelection: this.selectionMode === 'multiple',
|
|
4332
4607
|
// Re-pass the server-side page-count sources (#2) so a RUNTIME rowCount/pageCount change
|
|
@@ -4380,6 +4655,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4380
4655
|
onColumnSizingInfoChange: this.onColumnSizingInfoChangeCb
|
|
4381
4656
|
}));
|
|
4382
4657
|
if (this.refreshRowModel) this.refreshRowModel();
|
|
4658
|
+
if (pageReset) this.writePaginationSoon(pageReset);
|
|
4383
4659
|
};
|
|
4384
4660
|
|
|
4385
4661
|
// LIT (+ any fine-grained target whose effect-tracked watch does NOT observe the plain
|
|
@@ -4404,15 +4680,24 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4404
4680
|
// non-data tick (sort/filter/pagination) never touches `$props.data` → never clears. Called from
|
|
4405
4681
|
// BOTH the coarse re-feed watch AND the $onUpdate backstop (Lit's @property `data` the effect-
|
|
4406
4682
|
// tracked watch can't observe); both are ref-gated so the redundant call is an idempotent no-op.
|
|
4407
|
-
lastPropsData: unknown = null;
|
|
4408
|
-
|
|
4409
4683
|
maybeClearHistoryOnExternalSwap = () => {
|
|
4410
4684
|
const pd = this.data;
|
|
4411
4685
|
if (pd === this.lastPropsData) return; // $props.data did not change → not an external swap
|
|
4412
4686
|
this.lastPropsData = pd;
|
|
4413
|
-
if (!this.undoable) return;
|
|
4414
4687
|
if (pd != null && (pd as any)[this.DATA_WRITE_TOKEN_KEY] != null) return; // descends from our write → keep
|
|
4415
|
-
|
|
4688
|
+
// An external swap is a reset, not one of our writes: drop a write flag no echo consumed
|
|
4689
|
+
// (one-way `data`). Here AND in reFeed, so it holds whichever of the two runs first on a
|
|
4690
|
+
// target (Lit's $onUpdate and React's/Solid's effect order run this one before reFeed, after
|
|
4691
|
+
// which reFeed's isExternalSwap() already reads false).
|
|
4692
|
+
this.pendingWrite = false;
|
|
4693
|
+
// A genuine external swap (runs regardless of `undoable`): it re-derives a held view (a
|
|
4694
|
+
// backstop — reFeed normally released it already, before its own refresh) …
|
|
4695
|
+
if (this.holdCtrl.isHolding()) {
|
|
4696
|
+
this.holdCtrl.release();
|
|
4697
|
+
if (this.refreshRowModel) this.refreshRowModel();
|
|
4698
|
+
}
|
|
4699
|
+
// … and clears undo history (only meaningful when `undoable`).
|
|
4700
|
+
if (this.undoable) this.clearHistory();
|
|
4416
4701
|
};
|
|
4417
4702
|
|
|
4418
4703
|
lastData: any = null;
|
|
@@ -5176,6 +5461,31 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5176
5461
|
// DataTable.rozie) — so DataTable.rozie's $expose references them BY NAME with zero
|
|
5177
5462
|
// indirection. This file stays the seam for verbs that need a rename/adapter, not a mandatory
|
|
5178
5463
|
// stop for every $expose entry.
|
|
5464
|
+
// Held view (`holdView`). refreshView() re-derives the view from the current filters and
|
|
5465
|
+
// sort and resets both mismatch counts. It works with holdView off too: it forces custom
|
|
5466
|
+
// filter/sort functions to re-run. table-core's filter and sort stages memoize on the
|
|
5467
|
+
// columnFilters/sorting array REFERENCES (not on the column defs or the functions), and
|
|
5468
|
+
// release() alone touches neither — so feed table-core SHALLOW COPIES of both arrays, which
|
|
5469
|
+
// invalidates those memos. The next reFeed restores the original references (one harmless
|
|
5470
|
+
// extra recompute). pendingViewChange is the component-scope `let` the active-cell follow
|
|
5471
|
+
// consumes.
|
|
5472
|
+
refreshView: () => void = () => {
|
|
5473
|
+
this.holdCtrl.release();
|
|
5474
|
+
this.pendingViewChange = true;
|
|
5475
|
+
if (!this.table) return;
|
|
5476
|
+
this.table.setOptions((p: any) => ({
|
|
5477
|
+
...p,
|
|
5478
|
+
state: {
|
|
5479
|
+
...p.state,
|
|
5480
|
+
columnFilters: [...(p.state.columnFilters || [])],
|
|
5481
|
+
sorting: [...(p.state.sorting || [])]
|
|
5482
|
+
}
|
|
5483
|
+
}));
|
|
5484
|
+
if (this.refreshRowModel) this.refreshRowModel();
|
|
5485
|
+
};
|
|
5486
|
+
|
|
5487
|
+
getViewState: () => DataTableViewState = () => this.holdCtrl.viewState(this.table);
|
|
5488
|
+
|
|
5179
5489
|
// interactionMode gate. 'grid' lights up roving nav; 'table' (default) is byte-behaviorally
|
|
5180
5490
|
// identical to phase 48 (roles fall back to the literals, tabindex drops).
|
|
5181
5491
|
isGrid = () => this.interactionMode === 'grid';
|
|
@@ -6206,6 +6516,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6206
6516
|
if (!tgt || !tgt.closest) return;
|
|
6207
6517
|
const cellEl = tgt.closest('[data-grid-cell]');
|
|
6208
6518
|
if (!cellEl) return;
|
|
6519
|
+
this.activeCellEngaged = true;
|
|
6209
6520
|
const rowAttr = cellEl.getAttribute('data-row');
|
|
6210
6521
|
const colAttr = cellEl.getAttribute('data-col-index');
|
|
6211
6522
|
if (rowAttr == null || colAttr == null) return;
|
|
@@ -6507,11 +6818,101 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6507
6818
|
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(tryFocus);else setTimeout(tryFocus, 0);
|
|
6508
6819
|
};
|
|
6509
6820
|
|
|
6510
|
-
//
|
|
6511
|
-
//
|
|
6512
|
-
//
|
|
6513
|
-
//
|
|
6514
|
-
|
|
6821
|
+
// followOpenEditors: when a re-derive changed the rows under an open single-cell editor or
|
|
6822
|
+
// full-row editor, re-address it to its record's new index (re-seating editor focus there),
|
|
6823
|
+
// or — when the record left the view — close it WITHOUT writing (its draft belonged to a record
|
|
6824
|
+
// that is no longer on screen; the active cell, on the same record, parks). Returns true when an
|
|
6825
|
+
// editor is still open on a moved record (it, not the cell, owns DOM focus).
|
|
6826
|
+
followOpenEditors = (prevIds: any, nextIds: any) => {
|
|
6827
|
+
if (!prevIds || sameIdSequence(prevIds, nextIds)) return false;
|
|
6828
|
+
let owns = false;
|
|
6829
|
+
const er = this._editingRow.value;
|
|
6830
|
+
if (er >= 0) {
|
|
6831
|
+
const ef = resolveFollow(prevIds, er, nextIds, true);
|
|
6832
|
+
if (ef.kind === 'move') {
|
|
6833
|
+
this._editingRow.value = ef.index;
|
|
6834
|
+
this.focusEditorWhenReady(ef.index, this._editingCol.value, false);
|
|
6835
|
+
owns = true;
|
|
6836
|
+
} else if (ef.kind !== 'stay') {
|
|
6837
|
+
// Latch the session (an unmount blur must not commit the stale draft) and tear down.
|
|
6838
|
+
this.committedThisSession = true;
|
|
6839
|
+
this.editTransition = true;
|
|
6840
|
+
this.endEdit();
|
|
6841
|
+
this.editTransition = false;
|
|
6842
|
+
}
|
|
6843
|
+
}
|
|
6844
|
+
const rr = this._editingRowIndex.value;
|
|
6845
|
+
if (rr != null && rr >= 0) {
|
|
6846
|
+
const rf = resolveFollow(prevIds, rr, nextIds, true);
|
|
6847
|
+
if (rf.kind === 'move') {
|
|
6848
|
+
this._editingRowIndex.value = rf.index;
|
|
6849
|
+
owns = true;
|
|
6850
|
+
} else if (rf.kind !== 'stay') {
|
|
6851
|
+
this.editTransition = true;
|
|
6852
|
+
this.endRowEdit();
|
|
6853
|
+
this.editTransition = false;
|
|
6854
|
+
}
|
|
6855
|
+
}
|
|
6856
|
+
return owns;
|
|
6857
|
+
};
|
|
6858
|
+
|
|
6859
|
+
// The row id at a visible index / the visible index of a row id, read off table-core's LIVE
|
|
6860
|
+
// model (windowSource — the same index space as $data.rows), never the React-stale $data.rows.
|
|
6861
|
+
rowIdInModel = (index: any) => {
|
|
6862
|
+
const row = this.windowSource()[index];
|
|
6863
|
+
return row ? row.id : null;
|
|
6864
|
+
};
|
|
6865
|
+
|
|
6866
|
+
modelIndexOfRowId = (id: any) => {
|
|
6867
|
+
const rows = this.windowSource();
|
|
6868
|
+
for (let i = 0; i < rows.length; i++) if (rows[i].id === id) return i;
|
|
6869
|
+
return -1;
|
|
6870
|
+
};
|
|
6871
|
+
|
|
6872
|
+
// Part 3 (design 2026-11-06): the active cell follows its RECORD across every re-derive.
|
|
6873
|
+
// refreshRowModel hands over the previous and next visible row ids (fresh locals — never the
|
|
6874
|
+
// React-stale $data.rows) and whether the rows changed because of a view change (sort /
|
|
6875
|
+
// columnFilters / globalFilter / refreshView / a holdView release) or one of the table's own
|
|
6876
|
+
// writes. resolveFollow (helpers/activeFollow.ts) decides:
|
|
6877
|
+
// • stay / clamp → the D-05 clamp below, unchanged (clamp = the row is gone after navigation
|
|
6878
|
+
// or a reset: paging, page size, expand/collapse, an external data swap);
|
|
6879
|
+
// • move → re-seat the active row at the record's new index;
|
|
6880
|
+
// • park → the record left the view after a view change / write: park on its column header.
|
|
6881
|
+
// Skipped while a header is active (nothing to follow), on an empty model (B6 owns that), and
|
|
6882
|
+
// before the active cell was ever engaged (it is still just the D-04 entry cell, not a record).
|
|
6883
|
+
// The range is cleared when such a change altered row membership or order — a rectangle
|
|
6884
|
+
// cannot follow rows that are no longer adjacent. Cleared AFTER the clamp so clampRange's
|
|
6885
|
+
// (React-stale) corner write can never resurrect it. While the view is held the ids keep
|
|
6886
|
+
// their order, so the range stays.
|
|
6887
|
+
followActiveCell = (prevIds: any, nextIds: any, viewChangeOrWrite: any, rowCount: any, colCount: any, editFollowPending = false) => {
|
|
6888
|
+
// An OPEN editor stays on the record it was opened for (its editing pair is index-addressed
|
|
6889
|
+
// over the visible rows). Done first so the active-cell move below knows the editor owns focus.
|
|
6890
|
+
// NOT grid-gated: the editCell / editRow verbs open editors in 'table' mode too (final review
|
|
6891
|
+
// #2); only the active-cell part below is grid-only.
|
|
6892
|
+
const editorOwnsFocus = this.followOpenEditors(prevIds, nextIds);
|
|
6893
|
+
if (!this.isGrid()) return;
|
|
6894
|
+
let follow: any = null;
|
|
6895
|
+
if (this.activeCellEngaged && rowCount > 0 && !this._activeIsHeader.value && !this.gridEmptyFallback) {
|
|
6896
|
+
follow = resolveFollow(prevIds, this._activeRow.value, nextIds, viewChangeOrWrite);
|
|
6897
|
+
}
|
|
6898
|
+
// Final review #3: a view change or a table write that EMPTIES the grid — B6 parks the
|
|
6899
|
+
// active cell on the header fallback; when DOM focus was on the vanished body cell, move it
|
|
6900
|
+
// there too (never to <body>).
|
|
6901
|
+
const focusEmptyPark = rowCount <= 0 && viewChangeOrWrite;
|
|
6902
|
+
this.clampActiveCell(rowCount, colCount, follow, editFollowPending || editorOwnsFocus, focusEmptyPark);
|
|
6903
|
+
if (viewChangeOrWrite && !sameIdSequence(prevIds, nextIds)) this.clearRange();
|
|
6904
|
+
};
|
|
6905
|
+
|
|
6906
|
+
// D-05 (now the FALLBACK of the Part 3 follow — see followActiveCell above): clamp the active
|
|
6907
|
+
// cell to bounds on an underlying-data change whose record could not be followed for a
|
|
6908
|
+
// navigation / reset reason (paging, page size, expand/collapse, an external data swap), and
|
|
6909
|
+
// on every change that left it in place. KEEP the same indices; clamp ONLY when the grid shrank
|
|
6910
|
+
// — NO bounce-to-top. Where the record CAN be found it is followed first (`follow.kind ===
|
|
6911
|
+
// 'move'`), and a record that left the view after a view change or a table write parks on its
|
|
6912
|
+
// column header (`'park'`) instead of falling through to whichever row now holds its index.
|
|
6913
|
+
// Gated by isGrid() so 'table' mode is entirely untouched. Invoked at the rowModelVer bump
|
|
6914
|
+
// path (refreshRowModel → followActiveCell).
|
|
6915
|
+
clampActiveCell = (rowCount: any, colCount: any, follow: any = null, editFollowPending = false, focusEmptyPark = false) => {
|
|
6515
6916
|
if (!this.isGrid()) return;
|
|
6516
6917
|
// B8/B23 React-stale guard: the bounds come from the FRESH model the caller (refreshRowModel)
|
|
6517
6918
|
// just derived and passes in — NEVER re-read $data.rows here. `$data.rows = nextRows` is an
|
|
@@ -6535,6 +6936,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6535
6936
|
let recoverFocus = false;
|
|
6536
6937
|
let doomedRow = -1;
|
|
6537
6938
|
let doomedCol = 0;
|
|
6939
|
+
// Part 3: is DOM focus on ANY body cell of this grid (the follow re-focuses only then)?
|
|
6940
|
+
let focusOnBody = false;
|
|
6538
6941
|
if (this.gridRoot) {
|
|
6539
6942
|
const rootNode = this.gridRoot.getRootNode ? this.gridRoot.getRootNode() : null;
|
|
6540
6943
|
const focusedEl = rootNode ? rootNode.activeElement : null;
|
|
@@ -6543,6 +6946,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6543
6946
|
const fRowAttr = focusedCell.getAttribute('data-row');
|
|
6544
6947
|
const fColAttr = focusedCell.getAttribute('data-col-index');
|
|
6545
6948
|
if (fRowAttr != null && fRowAttr !== '__header') {
|
|
6949
|
+
focusOnBody = true;
|
|
6546
6950
|
const fr = parseInt(fRowAttr, 10);
|
|
6547
6951
|
const fc = parseInt(fColAttr, 10);
|
|
6548
6952
|
if (Number.isFinite(fr) && fr > rowN - 1) {
|
|
@@ -6576,6 +6980,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6576
6980
|
// Moving DOM focus here would steal focus AND — on React — the fallback's @focusin
|
|
6577
6981
|
// (setActiveIsHeader true) races the next clear-filter re-seat, leaving the tab-stop stuck on
|
|
6578
6982
|
// the header. Focus recovery is for a shrink that leaves a VALID BODY cell to land on (below).
|
|
6983
|
+
// The one exception (final review #3, `focusEmptyPark`): a view change or a table write
|
|
6984
|
+
// emptied the grid while DOM focus was ON a body cell (or a just-committed edit expects it
|
|
6985
|
+
// back) — that focus is about to drop to <body>, so move it to the header fallback cell
|
|
6986
|
+
// (the roving tab stop). Never steals focus that is elsewhere.
|
|
6987
|
+
if (focusEmptyPark && (focusOnBody || editFollowPending)) this.recoverGridFocus('__header', 0, this.headerLeafLevel());
|
|
6579
6988
|
return;
|
|
6580
6989
|
}
|
|
6581
6990
|
// B6 recovery: the body model returned. If we were parked on the empty-grid header fallback,
|
|
@@ -6586,6 +6995,51 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6586
6995
|
this._activeIsHeader.value = false;
|
|
6587
6996
|
this._activeRow.value = 0;
|
|
6588
6997
|
}
|
|
6998
|
+
// Part 3 move: the record is still in the view at a NEW index — follow it. Emit the new
|
|
6999
|
+
// position (consumers tracking the active cell would otherwise hold a stale index) and, when
|
|
7000
|
+
// DOM focus was on a body cell of this grid, re-focus at the new position (which also scrolls
|
|
7001
|
+
// it into view). A pending B23 edit-follow owns focus instead (refreshRowModel resolves the
|
|
7002
|
+
// same row by identity). Fresh locals only — no $data re-read after the writes (ROZ138).
|
|
7003
|
+
if (follow && follow.kind === 'move') {
|
|
7004
|
+
const nextRow = follow.index;
|
|
7005
|
+
this._activeRow.value = nextRow;
|
|
7006
|
+
this.clampRange(rowN - 1, colN - 1);
|
|
7007
|
+
this.dispatchEvent(new CustomEvent<DataTableActiveCellChangePayload>("activecell-change", {
|
|
7008
|
+
detail: {
|
|
7009
|
+
rowIndex: this.toAbsRow(nextRow),
|
|
7010
|
+
colIndex: col,
|
|
7011
|
+
isHeader: false
|
|
7012
|
+
},
|
|
7013
|
+
bubbles: true,
|
|
7014
|
+
composed: true
|
|
7015
|
+
}));
|
|
7016
|
+
if (focusOnBody && !editFollowPending) this.focusCellWhenReady(nextRow, col);
|
|
7017
|
+
return;
|
|
7018
|
+
}
|
|
7019
|
+
// Part 3 park: the record left the view after a view change or one of the table's own
|
|
7020
|
+
// writes. Park on the column's leaf header (keeping the column) rather than falling through
|
|
7021
|
+
// to whichever record now sits at the old index — a typed character then lands on a header
|
|
7022
|
+
// and edits nothing. NOT the B6 empty-grid fallback (no gridEmptyFallback): this park is
|
|
7023
|
+
// user-visible and sticky until the person navigates. When DOM focus was on the vanished
|
|
7024
|
+
// cell (or a just-committed edit expects focus back), move focus to that header so it never
|
|
7025
|
+
// drops to <body> (the B25 problem).
|
|
7026
|
+
if (follow && follow.kind === 'park') {
|
|
7027
|
+
const level = this.headerLeafLevel();
|
|
7028
|
+
this._activeIsHeader.value = true;
|
|
7029
|
+
this._activeHeaderLevel.value = level;
|
|
7030
|
+
this.clampRange(rowN - 1, colN - 1);
|
|
7031
|
+
this.dispatchEvent(new CustomEvent<DataTableActiveCellChangePayload>("activecell-change", {
|
|
7032
|
+
detail: {
|
|
7033
|
+
rowIndex: null,
|
|
7034
|
+
colIndex: col,
|
|
7035
|
+
isHeader: true
|
|
7036
|
+
},
|
|
7037
|
+
bubbles: true,
|
|
7038
|
+
composed: true
|
|
7039
|
+
}));
|
|
7040
|
+
if (focusOnBody || editFollowPending) this.recoverGridFocus('__header', col, level);
|
|
7041
|
+
return;
|
|
7042
|
+
}
|
|
6589
7043
|
if (!this._activeIsHeader.value) {
|
|
6590
7044
|
const lastRow = rowN - 1;
|
|
6591
7045
|
const maxRow = lastRow < 0 ? 0 : lastRow;
|
|
@@ -6740,6 +7194,12 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6740
7194
|
// refreshRowModel/clampActiveCell chain → React hoists to useRef → persists per-instance.
|
|
6741
7195
|
gridEmptyFallback = false;
|
|
6742
7196
|
|
|
7197
|
+
// Part 3: has the active cell ever been placed on a grid cell (a focusin on a cell — click, Tab-in,
|
|
7198
|
+
// keyboard nav, focusCell)? Until then the active cell is only the D-04 ENTRY cell, not a record
|
|
7199
|
+
// cursor, so a mount-time re-derive (columns registering, grouping engaging) must not drag it off
|
|
7200
|
+
// row 0 by following whichever record happened to sit there. Plain `let` (ROZ138-safe).
|
|
7201
|
+
activeCellEngaged = false;
|
|
7202
|
+
|
|
6743
7203
|
// ══ Cell-range selection (phase 51 plan 04 / req-7 / D-07) ═══════════════════════════════
|
|
6744
7204
|
// A rectangular cell range over the FULL visible model, addressed BY INDEX PAIRS
|
|
6745
7205
|
// (rangeAnchor/rangeFocus = { rowIndex, colIndex }) — NEVER a stored DOM node, so the
|
|
@@ -8100,6 +8560,12 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
8100
8560
|
// A new edit session starts — reset the sync idempotency latch so THIS session's eventual
|
|
8101
8561
|
// commit is not silently no-op'd by a PRIOR session's already-set latch.
|
|
8102
8562
|
this.committedThisSession = false;
|
|
8563
|
+
// A pointerdown OUTSIDE the grid that happened BEFORE this editor opened (a toolbar button
|
|
8564
|
+
// that calls the editCell verb) is not a click-away from it. Left set, onEditorBlur would
|
|
8565
|
+
// consume it on the first null-relatedTarget blur of this editor — e.g. the blur Solid/Lit
|
|
8566
|
+
// fire when a keyed re-render MOVES the editor's row (a re-sort under the open editor) —
|
|
8567
|
+
// and commit-and-close the editor (final review #2, measured on solid + lit).
|
|
8568
|
+
this.outsidePointerDown = false;
|
|
8103
8569
|
this.setInvalid('');
|
|
8104
8570
|
// Single-cell and full-row edit are mutually exclusive (D-06): entering a single-cell
|
|
8105
8571
|
// editor clears any row-edit state so isEditing never resolves both modes for one cell.
|
|
@@ -8918,15 +9384,28 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
8918
9384
|
const fromRow = this._editingRow.value;
|
|
8919
9385
|
const fromCol = this._editingCol.value;
|
|
8920
9386
|
const target = e.shiftKey ? this.prevEditableCell(fromRow, fromCol) : this.nextEditableCell(fromRow, fromCol);
|
|
9387
|
+
// Part 3 (2026-11-06 design): the advance targets a RECORD, not an index. The commit below
|
|
9388
|
+
// can re-sort / re-filter the rows (a write under an active sort or filter on the edited
|
|
9389
|
+
// column), so capture the target row's id now — from table-core's live model, which agrees
|
|
9390
|
+
// with the rendered rows before the write — and resolve its index again after the commit.
|
|
9391
|
+
const targetRowId = target ? this.rowIdInModel(target.row) : null;
|
|
8921
9392
|
// skipFocusReturn=true: don't bounce focus back to the committed cell — we advance
|
|
8922
9393
|
// straight into the next editable cell's editor below. Use the RETURN value (not a
|
|
8923
9394
|
// re-read of $data.editingRow — async-stale on React) to gate the advance: a validation
|
|
8924
9395
|
// failure returns false and keeps the editor open (the user must fix the value first).
|
|
8925
9396
|
const committed = this.commitEdit(undefined, true);
|
|
8926
|
-
|
|
8927
|
-
|
|
9397
|
+
// Where the target record is NOW: on targets whose re-derive runs synchronously inside the
|
|
9398
|
+
// write this is already the re-derived index; elsewhere the model has not re-derived yet,
|
|
9399
|
+
// the index is unchanged, and the refresh that follows moves the open editor and the active
|
|
9400
|
+
// cell with the record (followActiveCell). -1: the record already left the view — open
|
|
9401
|
+
// nothing (the refresh parked the active cell on its header).
|
|
9402
|
+
const advanceRow = targetRowId != null ? this.modelIndexOfRowId(targetRowId) : -1;
|
|
9403
|
+
if (committed && target && advanceRow >= 0) {
|
|
9404
|
+
this._activeRow.value = advanceRow;
|
|
8928
9405
|
this._activeColIndex.value = target.col;
|
|
8929
|
-
this.beginEdit(
|
|
9406
|
+
this.beginEdit(advanceRow, target.col, null);
|
|
9407
|
+
} else if (committed && target) {
|
|
9408
|
+
// The target record left the view: nothing to advance into.
|
|
8930
9409
|
} else if (committed) {
|
|
8931
9410
|
// B5: no editable cell in the Tab direction (grid start/end) — keep focus INSIDE the
|
|
8932
9411
|
// grid by returning it to the just-committed cell instead of letting it drop to <body>.
|
|
@@ -9471,8 +9950,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
9471
9950
|
set pagination(v: any) { this._paginationControllable.notifyPropertyWrite(v); }
|
|
9472
9951
|
get expanded(): any | boolean { return this._expandedControllable.read(); }
|
|
9473
9952
|
set expanded(v: any | boolean) { this._expandedControllable.notifyPropertyWrite(v); }
|
|
9474
|
-
get grouping(): any[] { return this._groupingControllable.read(); }
|
|
9475
|
-
set grouping(v: any[]) { this._groupingControllable.notifyPropertyWrite(v); }
|
|
9953
|
+
get grouping(): any[] | null { return this._groupingControllable.read(); }
|
|
9954
|
+
set grouping(v: any[] | null) { this._groupingControllable.notifyPropertyWrite(v); }
|
|
9476
9955
|
get rowSelection(): any { return this._rowSelectionControllable.read(); }
|
|
9477
9956
|
set rowSelection(v: any) { this._rowSelectionControllable.notifyPropertyWrite(v); }
|
|
9478
9957
|
get columnVisibility(): any { return this._columnVisibilityControllable.read(); }
|