@rozie-ui/data-table-lit 0.2.5 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/package.json +2 -2
- package/src/Column.ts +13 -4
- package/src/DataTable.ts +1394 -1
- package/src/DetailPanel.ts +3 -0
- package/src/EditorCheckbox.ts +2 -0
- package/src/EditorNumber.ts +3 -0
- package/src/EditorSelect.ts +2 -2
- package/src/EditorText.ts +3 -3
- package/src/FilterNumberRange.ts +5 -2
- package/src/FilterSelect.ts +6 -0
- package/src/FilterText.ts +3 -3
- package/src/GroupBar.ts +18 -0
package/src/DataTable.ts
CHANGED
|
@@ -427,7 +427,7 @@ export default class DataTable extends SignalWatcher(LitElement) {
|
|
|
427
427
|
/**
|
|
428
428
|
* The row data — `model: true`, so a committed cell/row edit writes a **fresh** array back through `r-model:data` (uncontrolled fallback `dataDefault`). A stable reference per Rozie's setup-once model — fed directly into table-core (never map/cloned in the watcher).
|
|
429
429
|
* @example
|
|
430
|
-
* <
|
|
430
|
+
* <rozie-data-table .data=${rows} @data-change=${…} .columns=${cols}></rozie-data-table>
|
|
431
431
|
*/
|
|
432
432
|
@property({ type: Array, attribute: 'data' }) _data_attr!: any[];
|
|
433
433
|
private _dataControllable = createLitControllableProperty<any[]>({ host: this, eventName: 'data-change', defaultValue: [], initialControlledValue: undefined });
|
|
@@ -1352,22 +1352,86 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1352
1352
|
|
|
1353
1353
|
table: any = null;
|
|
1354
1354
|
|
|
1355
|
+
// ── Vertical row windowing instance state (phase 53) ──────────────────────────────────
|
|
1356
|
+
// Mutable top-level instances (the `let table` precedent — React hoists to useRef; do NOT
|
|
1357
|
+
// const). NULL until $onMount, and ONLY constructed when $props.virtual. virtualizerCleanup
|
|
1358
|
+
// holds the _didMount() teardown for $onUnmount; gridScrollEl is the captured .rdt-scroll div
|
|
1359
|
+
// the virtualizer observes.
|
|
1355
1360
|
virtualizer: any = null;
|
|
1356
1361
|
|
|
1357
1362
|
virtualizerCleanup: any = null;
|
|
1358
1363
|
|
|
1359
1364
|
gridScrollEl: any = null;
|
|
1360
1365
|
|
|
1366
|
+
// CR-01 remeasure scheduling state. remeasurePending dedupes the deferred sweep — at most ONE
|
|
1367
|
+
// rAF is in flight, so a burst of onChange ticks (a fast scroll) collapses to a single measure
|
|
1368
|
+
// pass per frame instead of piling up rAF callbacks that fire mid-gesture. The piled-up
|
|
1369
|
+
// callbacks were what broke the Solid scroll-then-focus seam (D-12 focusActiveCell →
|
|
1370
|
+
// scrollToIndex → double-rAF focus): a stray remeasure firing inside that focus deferral
|
|
1371
|
+
// disrupted the focus landing. The sweep ALSO bails while virtual-core is mid-scroll
|
|
1372
|
+
// (virtualizer.isScrolling), so a measure can't run during scrollToIndex; the next settled
|
|
1373
|
+
// onChange re-measures the now-stable window. Scroll-driven recycling (the CR-01 case, measured
|
|
1374
|
+
// once motion settles between scroll steps) is unaffected.
|
|
1361
1375
|
remeasurePending = false;
|
|
1362
1376
|
|
|
1377
|
+
// ── Grid interaction-mode constants + DOM root (phase 49, REQ-2/6) ────────────────────
|
|
1378
|
+
// Fixed PageUp/PageDown row step (D-06). Phase 53 swaps this for the visible-window size
|
|
1379
|
+
// via the same focusActiveCell() scroll-into-view seam — kept a top-level const so that
|
|
1380
|
+
// later change is a one-line edit.
|
|
1363
1381
|
GRID_PAGE_STEP = 10;
|
|
1364
1382
|
|
|
1383
|
+
// The stable table-root element, captured in $onMount (the ONLY ROZ123-safe place to read
|
|
1384
|
+
// $el / query DOM across all six). focusActiveCell() resolves cells off this root; it is
|
|
1385
|
+
// shadow-safe because the query runs from INSIDE the component's own scope (the listbox
|
|
1386
|
+
// querySelector-off-root precedent, proven ×6 by plan 01's probe). NEVER read in a
|
|
1387
|
+
// computed/template binding (ROZ123).
|
|
1365
1388
|
gridRoot: any = null;
|
|
1366
1389
|
|
|
1390
|
+
// Echo-guard: while WE are writing a slice back, the re-feed watcher must not re-enter
|
|
1391
|
+
// the funnel. A counter (not a boolean) so nested writes are safe.
|
|
1367
1392
|
programmatic = 0;
|
|
1368
1393
|
|
|
1394
|
+
// Focus-intent epoch (#9) — a monotonic counter bumped at every focus-INTENT entry point
|
|
1395
|
+
// (focusActiveCell / focusCell+focusAbsCellWhenReady arm / a genuine active-cell-moving
|
|
1396
|
+
// focusin in syncActiveFromEvent). The two async focus-recovery polls (focusWhenReady for the
|
|
1397
|
+
// virtual off-window scroll, focusAbsCellWhenReady for the paginated page-switch) CAPTURE this
|
|
1398
|
+
// value at arm time (AFTER their own bump) and abort at the top of each iteration if it has since
|
|
1399
|
+
// changed — so a LATER user nav (ArrowKey / click) supersedes a stale poll instead of the poll
|
|
1400
|
+
// yanking focus back frames later. A naive guardMoved "abort if focus moved" check is WRONG here:
|
|
1401
|
+
// both polls arm while focus still sits on the OLD/being-left cell BY DESIGN (scroll-to /
|
|
1402
|
+
// page-switch), so an epoch — bumped only by a NEWER intent — is the correct abort signal.
|
|
1369
1403
|
focusIntentEpoch = 0;
|
|
1370
1404
|
|
|
1405
|
+
// ── Grid-wide undo/redo (260709-8ct) — history STATE lives in top-level `let` (mirroring
|
|
1406
|
+
// `programmatic` above), NOT $data: recording a snapshot on every keystroke must not trigger
|
|
1407
|
+
// a reactive re-render. React hoists each to useRef. undoStack/redoStack hold `data` array
|
|
1408
|
+
// REFERENCES (never deep copies — see undoHistory.rzts's header comment on the shared-row
|
|
1409
|
+
// invariant). restoringHistory suppresses re-recording while an undo()/redo() replay is
|
|
1410
|
+
// in flight.
|
|
1411
|
+
//
|
|
1412
|
+
// The external-swap history reset keys on data ORIGIN, not a timing window. Every internal
|
|
1413
|
+
// writeback stamps its fresh `data` array with a durable, non-enumerable marker under
|
|
1414
|
+
// DATA_WRITE_TOKEN_KEY (see writeData in writeFunnels.rzts); the reset (maybeClearHistoryOnExternal
|
|
1415
|
+
// Swap, below) clears history ONLY when a newly-supplied `$props.data` carries no marker — it did
|
|
1416
|
+
// not come from us, so it is a genuine external dataset swap. Presence of the marker ⟺ "descends
|
|
1417
|
+
// from one of our writes", and it survives EVERYTHING that defeated the four flag/timer variants:
|
|
1418
|
+
// 1. A raw-reference latch (`lastWrittenData === currentData()`) — Vue `reactive()` / Svelte 5
|
|
1419
|
+
// `$state` / Solid store re-wrap a written array in a NEW Proxy on its way back through props,
|
|
1420
|
+
// so `===` never holds. (A non-enumerable own PROPERTY, by contrast, is forwarded through
|
|
1421
|
+
// every target's reactive Proxy via `Reflect.get` — readable through the wrap.)
|
|
1422
|
+
// 2. A single-consume boolean — the re-feed watch fires MULTIPLE times per write; the first pass
|
|
1423
|
+
// consumed the flag, a later pass wrongly cleared.
|
|
1424
|
+
// 3. A content signature (`JSON.stringify`) — the watch can fire with a TRANSIENTLY STALE
|
|
1425
|
+
// `currentData()` mid-settle (Solid/Lit), a real-but-older value → false mismatch.
|
|
1426
|
+
// 4. A deferred settle-window flag (rAF, then a 96ms macrotask) — a slow re-feed on a LARGE
|
|
1427
|
+
// controlled table OUTRAN the window (#8); no fixed timeout can be correct (re-feed latency
|
|
1428
|
+
// scales with dataset size).
|
|
1429
|
+
// A STRING key (not a JS Symbol) is deliberate: it is stable BY VALUE on all six targets with ZERO
|
|
1430
|
+
// caching, whereas a `Symbol()` needs a per-instance memo to hold one identity — and Lit lowers
|
|
1431
|
+
// `$computed(() => Symbol())` to a plain getter that RE-MINTS the Symbol on every read, so writeData
|
|
1432
|
+
// and the reset would stamp/read DIFFERENT symbols and the marker would never match. Non-enumerable
|
|
1433
|
+
// → invisible to JSON.stringify / spread / Object.keys (the consumer's data stays clean); namespaced
|
|
1434
|
+
// so a consumer array never collides.
|
|
1371
1435
|
DATA_WRITE_TOKEN_KEY = '__rozieDataWriteToken';
|
|
1372
1436
|
|
|
1373
1437
|
undoStack: unknown[] = [];
|
|
@@ -1376,10 +1440,37 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1376
1440
|
|
|
1377
1441
|
restoringHistory: boolean = false;
|
|
1378
1442
|
|
|
1443
|
+
// Grouping auto-expand latch (phase 50 req-4): when grouping is ACTIVE and the consumer
|
|
1444
|
+
// has not bound `expanded` and has not yet toggled any group, group-header rows default to
|
|
1445
|
+
// EXPANDED (so the grouped subtree is visible — the standard grouped-grid affordance + the
|
|
1446
|
+
// roundout-VR leaf-visible baseline). The FIRST group/row toggle sets this true (in
|
|
1447
|
+
// writeExpanded), after which the user's expanded state wins. Stays false (untouched) on the
|
|
1448
|
+
// non-grouping path → byte-identical-off (the `expanded` slice resolves to $data.expandedDefault
|
|
1449
|
+
// exactly as before, both for the plain table AND the expandable-rows feature).
|
|
1379
1450
|
expandedTouched = false;
|
|
1380
1451
|
|
|
1452
|
+
// groupingActiveDefault(): is grouping currently engaged (a non-empty ordered key list)? Reads
|
|
1453
|
+
// the same source order as currentState().grouping ($props.grouping ?? $data.groupingDefault) so
|
|
1454
|
+
// the expanded auto-default below tracks the live grouping state on every target.
|
|
1381
1455
|
groupingActiveDefault = () => ((this.grouping != null ? this.grouping : this._groupingDefault.value) || []).length > 0;
|
|
1382
1456
|
|
|
1457
|
+
// effectiveColumnPinning(): the auto-injected select/expander chrome columns are a STRUCTURAL
|
|
1458
|
+
// left-pinned rail — they ALWAYS lead the pinned-left group so the checkbox/chevron stay the
|
|
1459
|
+
// leftmost body cells in EVERY case (fresh, pinned, AND grouped). Two forces would otherwise
|
|
1460
|
+
// push a data column ahead of the checkbox:
|
|
1461
|
+
// 1. Pinning — a consumer pinning `name` left makes it left-pinned; getVisibleCells() returns
|
|
1462
|
+
// [left-pinned, center, right-pinned], so an unpinned (center) checkbox renders AFTER it.
|
|
1463
|
+
// 2. Grouping — table-core's groupedColumnMode defaults to 'reorder', which moves a grouped
|
|
1464
|
+
// column to the FRONT of the order, ahead of an unpinned center checkbox.
|
|
1465
|
+
// Pinning the rail left beats BOTH: the left group always precedes the (grouped-reordered)
|
|
1466
|
+
// center group. We prepend SELECT_COL_ID then EXPANDER_COL_ID (matching the tableColumns
|
|
1467
|
+
// injection order [select, expander, ...userCols]) ahead of any consumer left-pins.
|
|
1468
|
+
// REQUIRES: the chrome column defs carry an explicit `size` (columnBuilders.rzts) — pinStyle's
|
|
1469
|
+
// sticky offset is col.getStart('left') = Σ preceding pinned SIZES, so a size-less chrome column
|
|
1470
|
+
// (table-core's 150px default) would inflate every real pinned column's `left` and overlap.
|
|
1471
|
+
// The consumer's columnPinning model never sees these ids: writeColumnPinning() strips them
|
|
1472
|
+
// on the way back out (writeFunnels.rzts). Note: this ALWAYS-pin makes the default (no-pin)
|
|
1473
|
+
// checkbox a sticky-left rail — an intentional baseline change (VR/snapshot baselines drift).
|
|
1383
1474
|
effectiveColumnPinning = (): any => {
|
|
1384
1475
|
const base = this.columnPinning != null ? this.columnPinning : this._columnPinningDefault.value;
|
|
1385
1476
|
const rail: string[] = [];
|
|
@@ -1394,6 +1485,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1394
1485
|
};
|
|
1395
1486
|
};
|
|
1396
1487
|
|
|
1488
|
+
// Assemble the live state object from bound r-model slices (?? uncontrolled fallback).
|
|
1489
|
+
// All NINE slices are wired (each ?? its own $data.<slice>Default). table-core reads
|
|
1490
|
+
// this whole object as `state`. Return type annotated `any`: the inferred object-literal
|
|
1491
|
+
// type does not structurally match table-core's `Partial<TableState>` under the strict
|
|
1492
|
+
// bundled-leaf tsc (the columnSizingInfo/pagination shapes widen to Record) — the
|
|
1493
|
+
// runtime shape is correct; `any` sidesteps the over-strict structural check (the
|
|
1494
|
+
// deferred-items strict-tsc #2 / leaf-output-strict-typecheck close).
|
|
1397
1495
|
currentState = (): any => ({
|
|
1398
1496
|
sorting: this.sorting != null ? this.sorting : this._sortingDefault.value,
|
|
1399
1497
|
globalFilter: this.globalFilter != null ? this.globalFilter : this._globalFilterDefault.value,
|
|
@@ -1429,10 +1527,28 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1429
1527
|
columnSizingInfo: this._columnSizingInfo.value
|
|
1430
1528
|
});
|
|
1431
1529
|
|
|
1530
|
+
// The live row data (Phase 51 req-4): the bound `data` prop when controlled, else the
|
|
1531
|
+
// uncontrolled $data.dataDefault fallback (mirrors currentState's per-slice ?? pattern).
|
|
1532
|
+
// A committed edit funnels a FRESH array through writeData, which writes BOTH sinks; the
|
|
1533
|
+
// re-feed sources here so editing works whether or not the consumer binds r-model:data.
|
|
1432
1534
|
currentData = (): any => this.data != null ? this.data : this._dataDefault.value;
|
|
1433
1535
|
|
|
1536
|
+
// Prototype-safe id-keyed column resolution (T-48-PP): the `:columns` config array is
|
|
1537
|
+
// applied FIRST (lower precedence), then the <Column> registry OVERRIDES by id (LWW).
|
|
1538
|
+
// byId is a null-prototype object so a consumer column id of "__proto__"/"constructor"
|
|
1539
|
+
// cannot pollute Object.prototype. Returns the table-core ColumnDef[]. (No per-column
|
|
1540
|
+
// render callbacks — cells render via the single #cell/#header scoped slot on this
|
|
1541
|
+
// component, dispatched by columnId; <Column> carries metadata only.)
|
|
1434
1542
|
isSafeKey = (k: any) => k !== '__proto__' && k !== 'constructor' && k !== 'prototype';
|
|
1435
1543
|
|
|
1544
|
+
// wrapAggregationFn (phase 50 req-5, D-05, threat T-50-04): resolve a per-column
|
|
1545
|
+
// aggregationFn straight onto the ColumnDef (no component-side switch — RESEARCH
|
|
1546
|
+
// anti-pattern). A built-in NAME string ('sum'/'min'/'max'/'extent'/'mean'/'median'/
|
|
1547
|
+
// 'unique'/'uniqueCount'/'count') passes through verbatim — table-core resolves it from its
|
|
1548
|
+
// built-in `aggregationFns` map. A CUSTOM function `(columnId, leafRows, childRows) => any`
|
|
1549
|
+
// is DEFENSIVELY WRAPPED (the runValidator precedent): a consumer fn runs per group, so a
|
|
1550
|
+
// throw is coerced to `undefined` and can never crash getGroupedRowModel (DoS guard).
|
|
1551
|
+
// Anything else → undefined (no aggregation; the cell renders as a placeholder).
|
|
1436
1552
|
wrapAggregationFn = (fn: any) => {
|
|
1437
1553
|
if (typeof fn === 'string') return fn;
|
|
1438
1554
|
if (typeof fn !== 'function') return undefined;
|
|
@@ -1445,6 +1561,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1445
1561
|
};
|
|
1446
1562
|
};
|
|
1447
1563
|
|
|
1564
|
+
// Build the table-core ColumnDef for ONE config-array entry. A LEAF entry
|
|
1565
|
+
// ({ id?, field, header?, … }) maps to an accessor ColumnDef; a GROUP entry
|
|
1566
|
+
// ({ id?, header, columns: [...] }) maps to a multi-level header GROUP column
|
|
1567
|
+
// whose children are built recursively (B12 — grouped/multi-level column headers).
|
|
1568
|
+
// Returns null for an unusable entry (no id/field, unsafe key, empty group).
|
|
1448
1569
|
buildConfigDef = (c: any) => {
|
|
1449
1570
|
if (!c) return null;
|
|
1450
1571
|
// Grouped (multi-level) header column: an entry carrying a `columns` array. table-core's
|
|
@@ -1556,10 +1677,24 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1556
1677
|
return out;
|
|
1557
1678
|
};
|
|
1558
1679
|
|
|
1680
|
+
// The constant id of the auto-injected leading checkbox column (D-04). Distinct from
|
|
1681
|
+
// any consumer column id (the registry/config guard never produces a leading "__").
|
|
1559
1682
|
SELECT_COL_ID = '__rdt_select';
|
|
1560
1683
|
|
|
1684
|
+
// The constant id of the auto-injected leading chevron expander column (phase 50, D-04).
|
|
1685
|
+
// Distinct from any consumer column id (the registry/config guard never produces a leading
|
|
1686
|
+
// "__"). Injected AFTER the select column (so order is [select, expander, ...userCols]).
|
|
1561
1687
|
EXPANDER_COL_ID = '__rdt_expander';
|
|
1562
1688
|
|
|
1689
|
+
// The table-core ColumnDef set actually fed to createTable / setOptions: the resolved
|
|
1690
|
+
// user columns, PLUS a LEADING checkbox column when selectionMode is 'single' OR
|
|
1691
|
+
// 'multiple' (D-04). The select column carries enableSorting/enableColumnFilter:false
|
|
1692
|
+
// and an isSelectColumn marker the template uses to render checkbox chrome (NOT an
|
|
1693
|
+
// accessor value). 'none' injects nothing. In 'single' mode the per-row checkbox
|
|
1694
|
+
// renders but the select-all HEADER checkbox is suppressed (selecting a row caps at
|
|
1695
|
+
// ≤1 via enableMultiRowSelection:false) — a single-select needs a per-row control,
|
|
1696
|
+
// not a select-all, so without injecting the column single mode would expose NO
|
|
1697
|
+
// selection UI at all.
|
|
1563
1698
|
selectionEnabled = () => this.selectionMode === 'single' || this.selectionMode === 'multiple';
|
|
1564
1699
|
|
|
1565
1700
|
tableColumns = () => {
|
|
@@ -1607,6 +1742,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1607
1742
|
return withExpander;
|
|
1608
1743
|
};
|
|
1609
1744
|
|
|
1745
|
+
// ── sorting slice: STATIC-KEY fresh-object echo-guarded write funnel (A4) ──────────
|
|
1746
|
+
// table-core hands an Updater<SortingState> = value | (old)=>new; the onSortingChange
|
|
1747
|
+
// callback applies it against the CURRENT sorting, then this funnel writes a FRESH
|
|
1748
|
+
// array to the uncontrolled default + the two-way model + fires the change event
|
|
1749
|
+
// REGARDLESS of binding. STATIC key (`$data.sortingDefault` / `$model.sorting`) — a
|
|
1750
|
+
// dynamic-key funnel is ROZ106 on all six. The remaining 8 slices each get their own
|
|
1751
|
+
// such funnel in Plans 04/05.
|
|
1610
1752
|
writeSorting = (next: any) => {
|
|
1611
1753
|
if (this.programmatic) return;
|
|
1612
1754
|
this.programmatic++;
|
|
@@ -1622,6 +1764,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1622
1764
|
|
|
1623
1765
|
applyUpdater = (updater: any, current: any) => typeof updater === 'function' ? updater(current) : updater;
|
|
1624
1766
|
|
|
1767
|
+
// ── expanded slice: STATIC-KEY fresh-value echo-guarded write funnel (A4) ──────────
|
|
1768
|
+
// table-core hands an Updater<ExpandedState> = value | (old)=>new; onExpandedChange
|
|
1769
|
+
// applies it against the CURRENT expanded, then this funnel writes a FRESH value to the
|
|
1770
|
+
// uncontrolled default + the two-way model + fires `expanded-change` REGARDLESS of binding.
|
|
1771
|
+
// `next` may be the `true` expand-all literal OR a { [rowId]: true } object — written
|
|
1772
|
+
// verbatim (Pitfall 2). One emit per change (the shared `programmatic` guard dedups the
|
|
1773
|
+
// React multi-render re-entry, D-07). STATIC key ($data.expandedDefault / $model.expanded).
|
|
1625
1774
|
writeExpanded = (next: any) => {
|
|
1626
1775
|
if (this.programmatic) return;
|
|
1627
1776
|
this.programmatic++;
|
|
@@ -1645,6 +1794,17 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1645
1794
|
this.programmatic--;
|
|
1646
1795
|
};
|
|
1647
1796
|
|
|
1797
|
+
// ── grouping slice: STATIC-KEY fresh-array echo-guarded write funnel (phase 50 reqs 4-7) ──
|
|
1798
|
+
// table-core hands an Updater<GroupingState> = value | (old)=>new; onGroupingChange applies it
|
|
1799
|
+
// against the CURRENT grouping, then this funnel writes a FRESH ordered array to the
|
|
1800
|
+
// uncontrolled default + the two-way model + fires `group-change` REGARDLESS of binding. One
|
|
1801
|
+
// emit per change (the shared `programmatic` guard dedups the React multi-render re-entry, D-07).
|
|
1802
|
+
// STATIC key ($data.groupingDefault / $model.grouping). Event stem is `group-change`, NOT
|
|
1803
|
+
// `grouping-change`: the model:true `grouping` prop auto-generates an `onGroupingChange` callback
|
|
1804
|
+
// on the React/Solid flat Props interface, and a `grouping-change` event would camelCase to the
|
|
1805
|
+
// SAME identifier → duplicate-identifier TS2300 (the model-prop==emit-name collision class 50-02
|
|
1806
|
+
// hit with expanded/expanded-change → expand-change). Every sibling slice stems off a DISTINCT
|
|
1807
|
+
// name (sorting→sort-change, rowSelection→selection-change); grouping→group-change follows suit.
|
|
1648
1808
|
writeGrouping = (next: any) => {
|
|
1649
1809
|
if (this.programmatic) return;
|
|
1650
1810
|
this.programmatic++;
|
|
@@ -1658,6 +1818,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1658
1818
|
this.programmatic--;
|
|
1659
1819
|
};
|
|
1660
1820
|
|
|
1821
|
+
// ── globalFilter slice: STATIC-KEY fresh-value echo-guarded write funnel (A4) ──────
|
|
1822
|
+
// A fresh string (primitive) to the uncontrolled default + the two-way model + fires
|
|
1823
|
+
// `filter-change` REGARDLESS of binding.
|
|
1661
1824
|
writeGlobalFilter = (next: any) => {
|
|
1662
1825
|
if (this.programmatic) return;
|
|
1663
1826
|
this.programmatic++;
|
|
@@ -1673,6 +1836,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1673
1836
|
this.programmatic--;
|
|
1674
1837
|
};
|
|
1675
1838
|
|
|
1839
|
+
// ── columnFilters slice: STATIC-KEY fresh-array echo-guarded write funnel (A4) ─────
|
|
1840
|
+
// table-core hands ColumnFiltersState = [{ id, value }]; write a FRESH array (never
|
|
1841
|
+
// in-place push) + fire `filter-change`. globalFilter + columnFilters both surface
|
|
1842
|
+
// through `filter-change` (per the plan: filter-change fires regardless of binding).
|
|
1676
1843
|
writeColumnFilters = (next: any) => {
|
|
1677
1844
|
if (this.programmatic) return;
|
|
1678
1845
|
this.programmatic++;
|
|
@@ -1688,6 +1855,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1688
1855
|
this.programmatic--;
|
|
1689
1856
|
};
|
|
1690
1857
|
|
|
1858
|
+
// ── pagination slice: STATIC-KEY fresh-object echo-guarded write funnel (A4) ───────
|
|
1859
|
+
// table-core hands { pageIndex, pageSize }; write a FRESH object + fire `page-change`.
|
|
1691
1860
|
writePagination = (next: any) => {
|
|
1692
1861
|
if (this.programmatic) return;
|
|
1693
1862
|
this.programmatic++;
|
|
@@ -1701,6 +1870,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1701
1870
|
this.programmatic--;
|
|
1702
1871
|
};
|
|
1703
1872
|
|
|
1873
|
+
// ── rowSelection slice: STATIC-KEY fresh-object echo-guarded write funnel (A4) ─────
|
|
1874
|
+
// table-core hands RowSelectionState = { [rowId]: true }; write a FRESH object (never
|
|
1875
|
+
// in-place key-set) + fire `selection-change` REGARDLESS of binding.
|
|
1704
1876
|
writeRowSelection = (next: any) => {
|
|
1705
1877
|
if (this.programmatic) return;
|
|
1706
1878
|
this.programmatic++;
|
|
@@ -1714,6 +1886,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1714
1886
|
this.programmatic--;
|
|
1715
1887
|
};
|
|
1716
1888
|
|
|
1889
|
+
// ── columnVisibility slice: STATIC-KEY fresh-object echo-guarded write funnel (A4) ──
|
|
1890
|
+
// table-core hands VisibilityState = { [colId]: boolean }; write a FRESH object (never
|
|
1891
|
+
// in-place key-set) + fire `visibility-change` REGARDLESS of binding.
|
|
1717
1892
|
writeColumnVisibility = (next: any) => {
|
|
1718
1893
|
if (this.programmatic) return;
|
|
1719
1894
|
this.programmatic++;
|
|
@@ -1727,6 +1902,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1727
1902
|
this.programmatic--;
|
|
1728
1903
|
};
|
|
1729
1904
|
|
|
1905
|
+
// ── columnSizing slice: STATIC-KEY fresh-object echo-guarded write funnel (A4) ──────
|
|
1906
|
+
// table-core hands ColumnSizingState = { [colId]: number }; the pointer-drag resize
|
|
1907
|
+
// handle funnels a FRESH sizing object + fires `resize-change` REGARDLESS of binding.
|
|
1730
1908
|
writeColumnSizing = (next: any) => {
|
|
1731
1909
|
if (this.programmatic) return;
|
|
1732
1910
|
this.programmatic++;
|
|
@@ -1740,6 +1918,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1740
1918
|
this.programmatic--;
|
|
1741
1919
|
};
|
|
1742
1920
|
|
|
1921
|
+
// ── columnOrder slice: STATIC-KEY fresh-array echo-guarded write funnel (A4) ────────
|
|
1922
|
+
// table-core hands ColumnOrderState = string[]; write a FRESH order array (never an
|
|
1923
|
+
// in-place splice) + fire `reorder-change` REGARDLESS of binding.
|
|
1743
1924
|
writeColumnOrder = (next: any) => {
|
|
1744
1925
|
if (this.programmatic) return;
|
|
1745
1926
|
this.programmatic++;
|
|
@@ -1753,6 +1934,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1753
1934
|
this.programmatic--;
|
|
1754
1935
|
};
|
|
1755
1936
|
|
|
1937
|
+
// ── columnPinning slice: STATIC-KEY fresh-object echo-guarded write funnel (A4) ─────
|
|
1938
|
+
// table-core hands ColumnPinningState = { left: string[], right: string[] }; write a
|
|
1939
|
+
// FRESH object (never in-place push into left/right) + fire `pin-change` REGARDLESS of
|
|
1940
|
+
// binding.
|
|
1756
1941
|
writeColumnPinning = (next: any) => {
|
|
1757
1942
|
if (this.programmatic) return;
|
|
1758
1943
|
// effectiveColumnPinning() forces the auto-injected chrome ids (select/expander) into the
|
|
@@ -1776,6 +1961,38 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1776
1961
|
this.programmatic--;
|
|
1777
1962
|
};
|
|
1778
1963
|
|
|
1964
|
+
// ── data slice: STATIC-KEY fresh-array echo-guarded write funnel (Phase 51 req-4) ──
|
|
1965
|
+
// A committed cell/row edit (or paste/fill in a later wave) replaces ONE row object in
|
|
1966
|
+
// a FRESH array and funnels it here. Writes the uncontrolled default + the two-way
|
|
1967
|
+
// model so editing works controlled OR uncontrolled. CRITICAL: writeData does NOT emit —
|
|
1968
|
+
// unlike the 9 state slices (each has one change event fired inside its funnel), the
|
|
1969
|
+
// `data` slice's commit event (`cell-edit-commit`) carries a PER-CELL payload and fires
|
|
1970
|
+
// from the SINGLE commitEdit call site so the count stays exactly one per commit (React
|
|
1971
|
+
// multi-emit dedup, D-07). Echo-guarded by the shared `programmatic` counter so the
|
|
1972
|
+
// re-feed watch never re-enters mid-write.
|
|
1973
|
+
//
|
|
1974
|
+
// 260709-8ct (grid-wide undo/redo): record the PRE-mutation snapshot BEFORE writing, but
|
|
1975
|
+
// ONLY when `$props.undoable` is on AND we are not mid-replay (`!restoringHistory` — an
|
|
1976
|
+
// undo()/redo() call routes back through THIS SAME writeData to reuse the two-way model +
|
|
1977
|
+
// re-feed watch; without the guard the replay would re-record itself and corrupt the
|
|
1978
|
+
// stack). `emitHistoryChangeIfEdged` fires `history-change` only when canUndo/canRedo
|
|
1979
|
+
// availability actually flipped (a long streak of edits that doesn't change availability
|
|
1980
|
+
// must not spam consumers).
|
|
1981
|
+
//
|
|
1982
|
+
// External-swap origin marker: stamp EVERY array we write (undoable or not, incl. an undo/
|
|
1983
|
+
// redo replay) with the durable, non-enumerable marker under DATA_WRITE_TOKEN_KEY. The reset
|
|
1984
|
+
// (maybeClearHistoryOnExternalSwap in DataTable.rozie) clears history only when a new $props.data
|
|
1985
|
+
// lacks the marker → it did not come from us → a genuine external swap. This replaces the
|
|
1986
|
+
// `dataWriteSettling` settle-window flag that a slow re-feed on a large controlled table outran
|
|
1987
|
+
// (#8) — see DATA_WRITE_TOKEN_KEY's declaration in DataTable.rozie for the four flag/timer
|
|
1988
|
+
// variants it supersedes and why the marker is timing-independent. Stamped on the fresh `next`
|
|
1989
|
+
// array (never the consumer's original), non-enumerable so JSON.stringify / spread / Object.keys
|
|
1990
|
+
// never see it. We write a FRESH RAW shallow copy (`fresh`) so the marker lands on an UNWRAPPED
|
|
1991
|
+
// array: an undo/redo replay reuses a snapshot that, in controlled mode, is a framework reactive
|
|
1992
|
+
// PROXY (svelte `$state`, vue `reactive`) — and `Object.defineProperty` does NOT reliably stick
|
|
1993
|
+
// through a proxy's trap, so stamping the raw copy (never the possibly-proxied `next`) keeps the
|
|
1994
|
+
// marker readable. Normal edits already pass a fresh array; the copy shares row references (cheap).
|
|
1995
|
+
// `try` guards the (never-expected) frozen/sealed-array case.
|
|
1779
1996
|
writeData = (next: any) => {
|
|
1780
1997
|
if (this.programmatic) return;
|
|
1781
1998
|
if (this.undoable && !this.restoringHistory) {
|
|
@@ -1799,12 +2016,18 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1799
2016
|
this.programmatic--;
|
|
1800
2017
|
};
|
|
1801
2018
|
|
|
2019
|
+
// Read the live columnFilters value for a given column id (string-safe; drives the
|
|
2020
|
+
// per-column filter input's bound value). Reads currentState() (NOT a $data re-read
|
|
2021
|
+
// of a just-written key → React stale-read safe).
|
|
1802
2022
|
columnFilterValue = (colId: any) => {
|
|
1803
2023
|
const cf = this.currentState().columnFilters || [];
|
|
1804
2024
|
for (const f of cf as any) if (f && f.id === colId) return f.value != null ? f.value : '';
|
|
1805
2025
|
return '';
|
|
1806
2026
|
};
|
|
1807
2027
|
|
|
2028
|
+
// Apply a per-column filter value: build a FRESH ColumnFiltersState array (drop the
|
|
2029
|
+
// column's prior entry, append the new one unless empty) and funnel it. Never mutate
|
|
2030
|
+
// the existing array in place (silent on React/Solid/Angular/Lit).
|
|
1808
2031
|
setColumnFilter = (colId: any, value: any) => {
|
|
1809
2032
|
const prev = this.currentState().columnFilters || [];
|
|
1810
2033
|
const next = [];
|
|
@@ -1816,6 +2039,25 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1816
2039
|
this.writeColumnFilters(next);
|
|
1817
2040
|
};
|
|
1818
2041
|
|
|
2042
|
+
// ── grid-wide undo/redo (260709-8ct) — snapshot-stack history engine ──────────────────────
|
|
2043
|
+
// Per the approved design (docs/superpowers/specs/2026-07-09-data-table-undo-history-design.md,
|
|
2044
|
+
// decisions 1-6, LOCKED). A pure, unit-testable buffer over the FOUR history lets declared
|
|
2045
|
+
// top-level in DataTable.rozie beside `let programmatic = 0` (undoStack/redoStack/
|
|
2046
|
+
// restoringHistory/lastPropsData — NOT $data, so recording an edit causes no reactive
|
|
2047
|
+
// re-render churn on every keystroke). This module holds the FUNCTIONS only; it references
|
|
2048
|
+
// those component-scope lets + `$props`, `$emit`, `writeData`, `currentData` BARE (by name,
|
|
2049
|
+
// zero ES imports) — the SAME inlined-partial pattern writeFunnels.rzts uses for
|
|
2050
|
+
// `programmatic`/`$data`/`$model` (DataTable.rozie is the ONLY place that ES-imports across
|
|
2051
|
+
// .rzts partials; a cross-import between writeFunnels and undoHistory would create an ES
|
|
2052
|
+
// cycle and/or a TDZ on the inlined lets).
|
|
2053
|
+
//
|
|
2054
|
+
// Collision-safe (ROZ121/124/137): none of undo/redo/canUndo/canRedo/clearHistory are
|
|
2055
|
+
// HTMLElement methods, model props, or React auto-generated setters.
|
|
2056
|
+
// Push the PRE-mutation snapshot (a `data` array reference — never a deep copy; unchanged
|
|
2057
|
+
// rows are shared across every retained snapshot because every write funnel already builds a
|
|
2058
|
+
// fresh array reusing unchanged row references, per the design's Memory analysis). Evict the
|
|
2059
|
+
// oldest snapshot once the stack exceeds `undoLimit` (default 100 — DataTable.rozie prop).
|
|
2060
|
+
// Any NEW recording invalidates the redo stack (standard undo semantics).
|
|
1819
2061
|
recordSnapshot = (current: any) => {
|
|
1820
2062
|
this.undoStack.push(current);
|
|
1821
2063
|
const limit = this.undoLimit != null ? this.undoLimit : 100;
|
|
@@ -1827,11 +2069,17 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1827
2069
|
|
|
1828
2070
|
canRedo = () => this.redoStack.length > 0;
|
|
1829
2071
|
|
|
2072
|
+
// Both stacks empty — the external-swap latch (DataTable.rozie reFeed) and the
|
|
2073
|
+
// clearHistory() $expose verb share this single implementation.
|
|
1830
2074
|
clearHistory = () => {
|
|
1831
2075
|
this.undoStack = [];
|
|
1832
2076
|
this.redoStack = [];
|
|
1833
2077
|
};
|
|
1834
2078
|
|
|
2079
|
+
// `$emit('history-change', { canUndo, canRedo })` — the imperative/keyboard $expose verb
|
|
2080
|
+
// contract. Unconditional (used by undo()/redo() themselves, which always fire exactly once
|
|
2081
|
+
// per call per the design — NOT edge-gated there; only the writeData-triggered recording path
|
|
2082
|
+
// below is edge-gated, since a routine sequence of edits would otherwise spam the event).
|
|
1835
2083
|
emitHistoryChange = () => {
|
|
1836
2084
|
this.dispatchEvent(new CustomEvent("history-change", {
|
|
1837
2085
|
detail: {
|
|
@@ -1843,12 +2091,22 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1843
2091
|
}));
|
|
1844
2092
|
};
|
|
1845
2093
|
|
|
2094
|
+
// Fire `history-change` ONLY when canUndo/canRedo availability flipped since `prevU`/`prevR`
|
|
2095
|
+
// were captured (BEFORE recordSnapshot ran). Called from writeData's recording hook so a
|
|
2096
|
+
// long streak of edits that doesn't change availability (canUndo already true, redo already
|
|
2097
|
+
// empty) does not spam consumers with a no-op event per keystroke.
|
|
1846
2098
|
emitHistoryChangeIfEdged = (prevU: any, prevR: any) => {
|
|
1847
2099
|
const nextU = this.canUndo();
|
|
1848
2100
|
const nextR = this.canRedo();
|
|
1849
2101
|
if (nextU !== prevU || nextR !== prevR) this.emitHistoryChange();
|
|
1850
2102
|
};
|
|
1851
2103
|
|
|
2104
|
+
// undo(): pop the most recent pre-mutation snapshot, push the CURRENT data onto the redo
|
|
2105
|
+
// stack (so redo can restore it), then replay the popped snapshot through the SAME writeData
|
|
2106
|
+
// seam — under `restoringHistory = true` so writeData's own recording hook does not
|
|
2107
|
+
// re-capture this replay (which would corrupt the stack). Replaying through writeData
|
|
2108
|
+
// (rather than writing $data/$model directly) is deliberate: the two-way $model.data
|
|
2109
|
+
// writeback, the re-feed $watch, and the echo guard all keep working with zero new code.
|
|
1852
2110
|
undo = () => {
|
|
1853
2111
|
if (!this.canUndo()) return;
|
|
1854
2112
|
const prev = this.undoStack.pop();
|
|
@@ -1859,6 +2117,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1859
2117
|
this.emitHistoryChange();
|
|
1860
2118
|
};
|
|
1861
2119
|
|
|
2120
|
+
// redo(): symmetric — pop the redo stack, push the CURRENT data back onto the undo stack,
|
|
2121
|
+
// replay through the same guarded writeData seam.
|
|
1862
2122
|
redo = () => {
|
|
1863
2123
|
if (!this.canRedo()) return;
|
|
1864
2124
|
const next = this.redoStack.pop();
|
|
@@ -1869,8 +2129,21 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1869
2129
|
this.emitHistoryChange();
|
|
1870
2130
|
};
|
|
1871
2131
|
|
|
2132
|
+
// Re-read the row model + header groups into $data (fresh arrays → the template
|
|
2133
|
+
// re-renders). A plain fn (NOT a $computed — getRowModel() must be pulled AFTER a
|
|
2134
|
+
// setOptions re-feed, imperatively). Defined inside $onMount so it captures the live
|
|
2135
|
+
// `table`.
|
|
1872
2136
|
refreshRowModel: any = null;
|
|
1873
2137
|
|
|
2138
|
+
// PER-SLICE callbacks hoisted to top-level consts (NOT inlined in createTable) so the
|
|
2139
|
+
// re-feed $watch can re-pass them on every setOptions. On React the createTable
|
|
2140
|
+
// callbacks would otherwise capture the MOUNT-render's currentState() closure (table
|
|
2141
|
+
// instance is built once in $onMount); table-core's setOptions keeps the prior
|
|
2142
|
+
// callbacks unless new ones are supplied, so a stale callback applied each updater
|
|
2143
|
+
// against the mount-time empty slice → the sort cycle never advances + multi-row
|
|
2144
|
+
// selection collapses to the last row (React stale-closure, F6). Re-passing these
|
|
2145
|
+
// fresh (recreated each render on React, reading fresh currentState) in the re-feed
|
|
2146
|
+
// keeps the Updater base value current. No-op cost on the other five.
|
|
1874
2147
|
onSortingChangeCb = (updater: any) => {
|
|
1875
2148
|
this.writeSorting(this.applyUpdater(updater, this.currentState().sorting));
|
|
1876
2149
|
};
|
|
@@ -1920,12 +2193,50 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1920
2193
|
this._columnSizingInfo.value = next != null ? next : this._columnSizingInfo.value;
|
|
1921
2194
|
};
|
|
1922
2195
|
|
|
2196
|
+
// ══ Vertical row windowing (phase 53, req-1/2/3/6/9/10) — the virtual-core bridge ════════
|
|
2197
|
+
// virtual-core is a pure state machine EXACTLY like table-core: constructed once in $onMount
|
|
2198
|
+
// (ONLY when $props.virtual), its imperative onChange push converted to per-target reactivity
|
|
2199
|
+
// via the SEPARATE $data.windowVer tick, re-fed via setOptions()+_willUpdate() in the
|
|
2200
|
+
// refreshRowModel path (NEVER a render helper — Pitfall 1). Every runtime reference is guarded
|
|
2201
|
+
// so the virtual=false emitted path is dead (req-1).
|
|
2202
|
+
//
|
|
2203
|
+
// Phase 64 (D-04): the PURE windowing math (windowedRows / padTop / padBottom / pmIndexInWindow /
|
|
2204
|
+
// rowIsOutsideWindow / virtualizerOptions / virtualItemKey) now lives in the shared, target-agnostic
|
|
2205
|
+
// `@rozie-ui/headless-core/windowing.rzts` partial and is re-exported below — this file is now the
|
|
2206
|
+
// thin DATA-TABLE HOST SHELL holding only the impure, per-consumer pieces (the table-bound row
|
|
2207
|
+
// source + the DOM/refs/virtualizer-instance machinery + the D-05 edit-pinning hook). The math
|
|
2208
|
+
// dissolves in via inlineScriptPartials() byte-identically; behavior is unchanged (the B13 specs +
|
|
2209
|
+
// dist-parity are the net). The host satisfies the windowing.rzts contract by convention:
|
|
2210
|
+
// windowSource() (the row source), pinnedEditIndex()/pinnedMeasurement() (the D-05 pin hook),
|
|
2211
|
+
// scheduleRemeasure(), and the gridScrollEl/virtualizer/virtual-core-fn references.
|
|
2212
|
+
// windowSource(): the rows fed to the virtualizer AND held in $data.rows — the windowing.rzts
|
|
2213
|
+
// host-contract source. When virtual, the FULL filtered+sorted PRE-PAGINATION model
|
|
2214
|
+
// (A2-verified table.getPrePaginationRowModel()) so windowing REPLACES client pagination (req-9);
|
|
2215
|
+
// else the normal (paginated) row model — the non-virtual path is byte-unchanged.
|
|
1923
2216
|
windowSource = () => {
|
|
1924
2217
|
if (!this.table) return [];
|
|
1925
2218
|
if (this.virtual) return this.table.getPrePaginationRowModel().rows;
|
|
1926
2219
|
return this.table.getRowModel().rows;
|
|
1927
2220
|
};
|
|
1928
2221
|
|
|
2222
|
+
// Defer remeasureWindow() until AFTER the framework commits the recycled window (onChange fires
|
|
2223
|
+
// BEFORE React/Solid commit), falling back to a microtask/timeout where rAF is unavailable (SSR /
|
|
2224
|
+
// test envs). DEDUPED via remeasurePending so a scroll burst queues at most one in-flight sweep
|
|
2225
|
+
// (piled-up rAF sweeps broke the Solid scroll-then-focus seam — and the focus seam itself now
|
|
2226
|
+
// polls for its target cell, so it no longer depends on remeasure timing).
|
|
2227
|
+
//
|
|
2228
|
+
// TWO deferred passes (microtask THEN rAF), both behind the single in-flight flag:
|
|
2229
|
+
// - Solid's <For> / Svelte's {#each} commit the recycled <tr> set SYNCHRONOUSLY in the reactive
|
|
2230
|
+
// tick that the windowVer bump triggers, so the recycled nodes already exist by the next
|
|
2231
|
+
// microtask — measuring there observes them while they are still connected, BEFORE the next
|
|
2232
|
+
// fast-scroll step recycles them away. A single rAF (a full frame later) was too late on the
|
|
2233
|
+
// fine-grained targets under a 40ms-per-step scroll: many rows mounted-and-recycled within one
|
|
2234
|
+
// frame, so the once-per-frame rAF sweep observed only a fraction of them and the measured
|
|
2235
|
+
// total under-converged (the Solid ~23.5k-vs-≥24k residual). The microtask catches them.
|
|
2236
|
+
// - React's setState→reconcile→commit is async (a microtask is too early — the new window is not
|
|
2237
|
+
// committed yet), so the rAF pass is what observes React's recycled rows.
|
|
2238
|
+
// Each pass only OBSERVES + measures the live window; measureElement is idempotent on an
|
|
2239
|
+
// already-observed node, so running both is cheap and loop-free.
|
|
1929
2240
|
scheduleRemeasure = () => {
|
|
1930
2241
|
if (this.remeasurePending) return;
|
|
1931
2242
|
this.remeasurePending = true;
|
|
@@ -1944,18 +2255,38 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1944
2255
|
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(rafPass);else if (ranMicro) this.remeasurePending = false;else setTimeout(rafPass, 0);
|
|
1945
2256
|
};
|
|
1946
2257
|
|
|
2258
|
+
// pinnedEditIndex(): the FULL-MODEL row index of the row currently in edit (D-02 pin-row),
|
|
2259
|
+
// or -1 when no editor is open. Under virtualization `$data.rows` is the FULL pre-pagination
|
|
2260
|
+
// model, so editingRow (single-cell) / editingRowIndex (full-row) — both in that index space —
|
|
2261
|
+
// ARE the full-model index. The pinned row must never recycle while editing (req-9): it is
|
|
2262
|
+
// unioned into the windowed slice when it scrolls off-window and its height is subtracted from
|
|
2263
|
+
// the appropriate spacer so the total stays exactly getTotalSize() (the 51-01-proven mechanism).
|
|
2264
|
+
// This is the data-table half of the D-05 windowing.rzts pin-extension hook (listbox provides none).
|
|
1947
2265
|
pinnedEditIndex = () => {
|
|
1948
2266
|
if (this._editingRow.value >= 0) return this._editingRow.value;
|
|
1949
2267
|
if (this._editingRowIndex.value != null) return this._editingRowIndex.value;
|
|
1950
2268
|
return -1;
|
|
1951
2269
|
};
|
|
1952
2270
|
|
|
2271
|
+
// pinnedMeasurement(pin): the virtual-core measurement { index, start, size, end, key } for the
|
|
2272
|
+
// pinned full-model index — its measured (or estimated) height + offset, used to (a) decide
|
|
2273
|
+
// whether it sits above/below the rendered window and (b) subtract its height from the right
|
|
2274
|
+
// spacer. Null when out of range / not virtual.
|
|
1953
2275
|
pinnedMeasurement = (pin: any) => {
|
|
1954
2276
|
if (!this.virtualizer || pin < 0) return null;
|
|
1955
2277
|
const ms = this.virtualizer.getMeasurements();
|
|
1956
2278
|
return ms && ms[pin] ? ms[pin] : null;
|
|
1957
2279
|
};
|
|
1958
2280
|
|
|
2281
|
+
// measureElement sweep (D-10 / CR-01): refine estimated heights to MEASURED ones. The off-root
|
|
2282
|
+
// querySelector idiom (chartjs/cropper/embla precedent — no per-row callback ref). Each rendered
|
|
2283
|
+
// <tr> MUST be handed to virtualizer.measureElement on every window commit for it to be observed:
|
|
2284
|
+
// virtual-core does NOT auto-register rendered rows — measureElement is the SOLE caller of its
|
|
2285
|
+
// internal ResizeObserver's observe() (virtual-core@3.17.1 dist/esm/index.js:794-817), keyed by
|
|
2286
|
+
// getItemKey. So this sweep must run not just once at mount but on every onChange tick (via
|
|
2287
|
+
// scheduleRemeasure), or recycled rows keep the estimateRowHeight seed forever. measureElement is
|
|
2288
|
+
// idempotent on an already-observed node (the `prevNode !== node` guard), so re-sweeping the
|
|
2289
|
+
// visible window each commit is cheap and loop-free.
|
|
1959
2290
|
remeasureWindow = () => {
|
|
1960
2291
|
if (!this.virtualizer || !this.gridRoot) return;
|
|
1961
2292
|
// Bail ONLY while a PROGRAMMATIC scroll is in flight: virtualizer.scrollState is non-null
|
|
@@ -1970,11 +2301,48 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1970
2301
|
for (const tr of trs as any) this.virtualizer.measureElement(tr);
|
|
1971
2302
|
};
|
|
1972
2303
|
|
|
2304
|
+
// D-04: this shell exports ONLY the impure, data-table-specific host pieces. The pure windowing
|
|
2305
|
+
// math (windowedRows / padTop / padBottom / pmIndexInWindow / rowIsOutsideWindow / virtualizerOptions
|
|
2306
|
+
// / virtualItemKey) is imported DIRECTLY by the host (DataTable.rozie) from
|
|
2307
|
+
// `@rozie-ui/headless-core/windowing.rzts` via bare specifier — the P0-proven cross-package inline
|
|
2308
|
+
// path that DISSOLVES the partial into the leaf (a re-export-from THROUGH this shell would survive as
|
|
2309
|
+
// a runtime import, not inline — verified). The math closes over these host symbols by convention.
|
|
2310
|
+
// ══ Generic vertical windowing math (Phase 64, D-04) — the target-agnostic virtual-core bridge ══
|
|
2311
|
+
// Lifted verbatim from the DataTable virtualization.rzts (the Phase 53/63 B13 baseline). This partial
|
|
2312
|
+
// holds ONLY the PURE windowing math; every DOM/refs/virtualizer-instance impurity stays per-consumer
|
|
2313
|
+
// in the host (ROZ123). It is a compile-time `.rzts` script-partial: it dissolves into each consumer's
|
|
2314
|
+
// compiled leaf via inlineScriptPartials() before IR lowering — leaving zero runtime dependency.
|
|
2315
|
+
//
|
|
2316
|
+
// HOST CONTRACT (symbols the consuming host MUST define before importing — the same implicit
|
|
2317
|
+
// by-convention mixin contract the DataTable host's other partials already use for `$data.windowVer`):
|
|
2318
|
+
// - windowSource(): T[] — the full list to window (the KEY generalization; the DataTable host
|
|
2319
|
+
// returns its pre-pagination row model, listbox/combobox return the
|
|
2320
|
+
// filtered options). This partial MUST NOT reach into the host data engine
|
|
2321
|
+
// directly — rows arrive ONLY through windowSource().
|
|
2322
|
+
// - $props.estimateRowHeight — per-item size estimate (kept aliased for DataTable back-compat).
|
|
2323
|
+
// - $data.windowVer / $data.editVer — window/edit-version reactivity bumps.
|
|
2324
|
+
// - gridScrollEl — the scroll-container element handle.
|
|
2325
|
+
// - virtualizer — the host virtual-core instance (built in $onMount from the ref).
|
|
2326
|
+
// - observeElementRect / observeElementOffset / elementScroll / measureElement — virtual-core fns.
|
|
2327
|
+
// - scheduleRemeasure() — the host's rAF/microtask remeasure defer.
|
|
2328
|
+
// - pinnedEditIndex() / pinnedMeasurement(pin) — the D-05 OPTIONAL pin-extension hook (host-provided,
|
|
2329
|
+
// defaulting to no-op): the DataTable host passes its edit-pinning hooks;
|
|
2330
|
+
// listbox passes nothing. Routing pinning through this host hook (NOT
|
|
2331
|
+
// inlining it) keeps DataTable's B13 edit-pinning behavior byte-identical.
|
|
2332
|
+
// getItemKey reads the LIVE source (never a frozen mount-render $data.rows closure — the F6
|
|
2333
|
+
// React stale-closure lesson) so virtual-core's measurement cache keys by stable full-model row
|
|
2334
|
+
// id across recycling, aligned with the windowed <tr> :key="row.id" (Pitfall 3 / req-10).
|
|
1973
2335
|
virtualItemKey = (i: any) => {
|
|
1974
2336
|
const src = this.windowSource();
|
|
1975
2337
|
return src && src[i] ? src[i].id : undefined;
|
|
1976
2338
|
};
|
|
1977
2339
|
|
|
2340
|
+
// The FULL virtualizer options. virtual-core's setOptions REPLACES options with
|
|
2341
|
+
// `{ ...defaults, ...opts }` (it does NOT merge with prior options — verified in the 3.17.1
|
|
2342
|
+
// source), so the re-feed MUST pass the complete set, exactly like every TanStack adapter.
|
|
2343
|
+
// Returned `any` (the currentState() precedent) so the strict bundled-leaf tsc does not choke
|
|
2344
|
+
// on virtual-core's generic option inference. onChange uses the `$data.x = $data.x + 1`
|
|
2345
|
+
// increment the React emitter lowers to functional setState — correct even from a mount closure.
|
|
1978
2346
|
virtualizerOptions = (): any => ({
|
|
1979
2347
|
count: this.windowSource().length,
|
|
1980
2348
|
getScrollElement: () => this.gridScrollEl,
|
|
@@ -2002,6 +2370,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2002
2370
|
}
|
|
2003
2371
|
});
|
|
2004
2372
|
|
|
2373
|
+
// pinMeasurement(pin): the D-05 pin-hook read, RE-TYPED at the windowing layer so the
|
|
2374
|
+
// shared math is strict-clean across every host. The host-provided pinnedMeasurement() has
|
|
2375
|
+
// two shapes: the DataTable host returns a real virtual-core measurement; the listbox/combobox
|
|
2376
|
+
// no-op host returns bare `null` (inferred `(pin) => null`). Calling it directly makes
|
|
2377
|
+
// `const pm = pinnedMeasurement(pin)` flow-narrow to `null`, so the downstream `pm && pm.start`
|
|
2378
|
+
// guard collapses the object branch to `never` (TS2339, Class 3). Reading the hook through this
|
|
2379
|
+
// thin wrapper with an EXPLICIT return type (a return-type annotation is NOT flow-narrowed)
|
|
2380
|
+
// gives the measurement a real object-or-null shape, so `pm && pm.start` keeps the object branch.
|
|
2381
|
+
// Typing-only: the runtime value (a measurement or null) is unchanged.
|
|
2005
2382
|
pinMeasurement = (pin: number): {
|
|
2006
2383
|
start: number;
|
|
2007
2384
|
size: number;
|
|
@@ -2009,6 +2386,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2009
2386
|
end: number;
|
|
2010
2387
|
} | null => this.pinnedMeasurement(pin);
|
|
2011
2388
|
|
|
2389
|
+
// windowedRows(): the rendered slice. Off / pre-mount → the full $data.rows mapped to
|
|
2390
|
+
// { vi:null, row } (the r-else path never calls this, but the guard keeps it total). On → read
|
|
2391
|
+
// $data.windowVer to SUBSCRIBE (the rowIndexOf tick discipline) then map each VirtualItem to its
|
|
2392
|
+
// full-model row. NB the local is `rowList` (NOT `rows` — React lowers $data.rows to a bare
|
|
2393
|
+
// `rows` binding → TS2448 self-shadow, line ~1149 lesson).
|
|
2012
2394
|
windowedRows = () => {
|
|
2013
2395
|
// SUBSCRIBE FIRST (fine-grained targets): touch the reactive windowVer at the TOP — BEFORE any
|
|
2014
2396
|
// early return — so Solid's <For>/Svelte's {#each} accessor subscribes to it on its FIRST eval,
|
|
@@ -2079,6 +2461,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2079
2461
|
return out;
|
|
2080
2462
|
};
|
|
2081
2463
|
|
|
2464
|
+
// Spacer-<tr> heights (D-03): the leading spacer occupies items[0].start; the trailing spacer
|
|
2465
|
+
// the gap between the last rendered item's end and getTotalSize(). Both windowVer-gated reads
|
|
2466
|
+
// (the `$data.windowVer` touch re-derives them as the window/measurements change). 0 when off.
|
|
2082
2467
|
padTop = () => {
|
|
2083
2468
|
// SUBSCRIBE FIRST (the windowedRows() discipline): touch windowVer + editVer at the TOP so the
|
|
2084
2469
|
// spacer-<td> :style binding subscribes on the fine-grained targets before the early return,
|
|
@@ -2131,11 +2516,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2131
2516
|
return pad < 0 ? 0 : pad;
|
|
2132
2517
|
};
|
|
2133
2518
|
|
|
2519
|
+
// pmIndexInWindow: is full-model index `idx` present in the rendered virtual window?
|
|
2134
2520
|
pmIndexInWindow = (items: any, idx: any) => {
|
|
2135
2521
|
for (let i = 0; i < items.length; i++) if (items[i].index === idx) return true;
|
|
2136
2522
|
return false;
|
|
2137
2523
|
};
|
|
2138
2524
|
|
|
2525
|
+
// rowIsOutsideWindow(r): is the full-model row index r absent from the currently rendered
|
|
2526
|
+
// window? Used by the scroll-then-focus seam (req-5 — scroll a far row in before focusing).
|
|
2139
2527
|
rowIsOutsideWindow = (r: any) => {
|
|
2140
2528
|
if (!this.virtual || !this.virtualizer) return false;
|
|
2141
2529
|
const items = this.virtualizer.getVirtualItems();
|
|
@@ -2143,6 +2531,19 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2143
2531
|
return true;
|
|
2144
2532
|
};
|
|
2145
2533
|
|
|
2534
|
+
// ── Sort/filter live-announcement (#14) ─────────────────────────────────────────────
|
|
2535
|
+
// A polite aria-live announcement whenever the consumer changes sorting or filtering, so a
|
|
2536
|
+
// screen-reader user hears that the rows were reordered / narrowed (which is otherwise silent).
|
|
2537
|
+
// announceState holds the last-seen references so the lazy watch below can tell WHICH slice
|
|
2538
|
+
// changed (sort vs filter) and pick the message. It is a top-level mutable const → stabilized
|
|
2539
|
+
// once per instance on all six targets (React useMemo-wraps a mutable instance; the others run
|
|
2540
|
+
// setup once), so it PERSISTS across renders — unlike a top-level `let`, which React resets per
|
|
2541
|
+
// render. Seeded from the initial state in $onMount so the first (post-mount) change compares
|
|
2542
|
+
// against the true starting values, not a null sentinel.
|
|
2543
|
+
// Typed as `unknown` members: these hold opaque last-seen references compared only by
|
|
2544
|
+
// identity (!==) below, never read in a typed context — the annotation keeps the null seed
|
|
2545
|
+
// from narrowing the members to `null` (which would reject the real reassignments under
|
|
2546
|
+
// strictNullChecks in the emitted leaves).
|
|
2146
2547
|
announceState: {
|
|
2147
2548
|
sorting: unknown;
|
|
2148
2549
|
columnFilters: unknown;
|
|
@@ -2153,12 +2554,18 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2153
2554
|
globalFilter: null
|
|
2154
2555
|
};
|
|
2155
2556
|
|
|
2557
|
+
// Effective (controlled-or-uncontrolled) reads of the sort/filter slices: the bound prop when
|
|
2558
|
+
// the consumer bound the matching r-model, else the uncontrolled $data default (mirrors currentState()).
|
|
2156
2559
|
effectiveSorting = () => this.sorting != null ? this.sorting : this._sortingDefault.value;
|
|
2157
2560
|
|
|
2158
2561
|
effectiveColumnFilters = () => this.columnFilters != null ? this.columnFilters : this._columnFiltersDefault.value;
|
|
2159
2562
|
|
|
2160
2563
|
effectiveGlobalFilter = () => this.globalFilter != null ? this.globalFilter : this._globalFilterDefault.value;
|
|
2161
2564
|
|
|
2565
|
+
// Build the polite message for a sort/filter change and advance announceState. Sort takes
|
|
2566
|
+
// precedence when the sorting reference changed; otherwise a filter changed → the post-filter
|
|
2567
|
+
// result count (the FILTERED total via totalRowCount(), NOT the page slice). Returns '' when
|
|
2568
|
+
// neither actually changed (a no-op watch tick — do not re-announce).
|
|
2162
2569
|
buildSortFilterAnnounce = () => {
|
|
2163
2570
|
const nextSorting = this.effectiveSorting();
|
|
2164
2571
|
const nextColumnFilters = this.effectiveColumnFilters();
|
|
@@ -2181,6 +2588,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2181
2588
|
return '';
|
|
2182
2589
|
};
|
|
2183
2590
|
|
|
2591
|
+
// Push fresh options into table-core + re-pull the row model. Extracted so BOTH the
|
|
2592
|
+
// re-feed $watch (above) and the Lit data-change $onUpdate (below) call it.
|
|
2184
2593
|
reFeed = () => {
|
|
2185
2594
|
if (!this.table) return;
|
|
2186
2595
|
// NOTE: the external-swap history reset does NOT live here. reFeed() fires on EVERY watched
|
|
@@ -2246,6 +2655,28 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2246
2655
|
if (this.refreshRowModel) this.refreshRowModel();
|
|
2247
2656
|
};
|
|
2248
2657
|
|
|
2658
|
+
// LIT (+ any fine-grained target whose effect-tracked watch does NOT observe the plain
|
|
2659
|
+
// `data` PROPERTY): the re-feed $watch reads `(this.data||[]).length` inside a
|
|
2660
|
+
// preact-signals effect, but `data` is a Lit @property (not a signal) so the effect
|
|
2661
|
+
// never re-runs when the consumer pushes new rows post-mount (the sticky demo seeds 20
|
|
2662
|
+
// rows in its own $onMount AFTER the child mounted empty → the body stayed at 0). The
|
|
2663
|
+
// slice models DO re-pull (their $data.<slice>Default signals are effect-tracked), so
|
|
2664
|
+
// only a raw `data` reference/length change slips through. $onUpdate (Lit updated())
|
|
2665
|
+
// fires on ANY property change incl `data`; guard with a stored last-seen data ref +
|
|
2666
|
+
// length so it re-feeds ONLY on a real data change (no churn). On the coarse-render
|
|
2667
|
+
// targets the watch already covers it; this is a cheap idempotent backstop.
|
|
2668
|
+
// External-swap history reset (grid-wide undo/redo, 260709-8ct; #8 fix). Keyed on the CONTROLLED
|
|
2669
|
+
// `$props.data` REFERENCE changing — deliberately NOT on `currentData()` inside reFeed. An internal
|
|
2670
|
+
// writeback changes `$data.dataDefault` SYNCHRONOUSLY and only LATER round-trips into `$props.data`;
|
|
2671
|
+
// keying on `$props.data`'s OWN change means we never observe the transient window where a fine-
|
|
2672
|
+
// grained target's reFeed reads a stale, unstamped `$props.data` mid-write and wrongly wipes a
|
|
2673
|
+
// just-recorded edit's history (the stale-read false-clear — the SAME failure that broke the
|
|
2674
|
+
// content-signature variant — that regressed Solid/Lit when this clear lived in reFeed). When
|
|
2675
|
+
// `$props.data` genuinely changes: a new array carrying DATA_WRITE_TOKEN round-tripped from one of
|
|
2676
|
+
// OUR writes → keep; one without it is a dataset the consumer handed us → external swap → clear. A
|
|
2677
|
+
// non-data tick (sort/filter/pagination) never touches `$props.data` → never clears. Called from
|
|
2678
|
+
// BOTH the coarse re-feed watch AND the $onUpdate backstop (Lit's @property `data` the effect-
|
|
2679
|
+
// tracked watch can't observe); both are ref-gated so the redundant call is an idempotent no-op.
|
|
2249
2680
|
lastPropsData: unknown = null;
|
|
2250
2681
|
|
|
2251
2682
|
maybeClearHistoryOnExternalSwap = () => {
|
|
@@ -2261,6 +2692,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2261
2692
|
|
|
2262
2693
|
lastDataLen = -1;
|
|
2263
2694
|
|
|
2695
|
+
// Header click → toggle sort. Shift-click → ADD a secondary sort (multi-sort). Driven
|
|
2696
|
+
// through table-core's column API so the onSortingChange funnel emits the fresh state.
|
|
2264
2697
|
onHeaderSort = (colId: any, evt: any) => {
|
|
2265
2698
|
if (!this.table) return;
|
|
2266
2699
|
const col = this.table.getColumn(colId);
|
|
@@ -2270,8 +2703,20 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2270
2703
|
col.toggleSorting(undefined, multi);
|
|
2271
2704
|
};
|
|
2272
2705
|
|
|
2706
|
+
// aria-sort string for a column header: 'ascending' | 'descending' | 'none'. Reads
|
|
2707
|
+
// Reactive tick: read $data.rowModelVer (bumped by every refreshRowModel) so a
|
|
2708
|
+
// template binding that calls a table-READING chrome helper (pagination/sort/pin/
|
|
2709
|
+
// visibility predicates below) re-evaluates when the row model changes. On the
|
|
2710
|
+
// coarse-render targets (Vue/React/Angular) the whole template re-runs anyway so this
|
|
2711
|
+
// is a no-op; on the FINE-GRAINED targets (Solid/Lit) a helper that only reads the
|
|
2712
|
+
// non-reactive `table` let would be computed ONCE (when table is still null → the
|
|
2713
|
+
// default branch) and never update — pagination would read "Page 1 of 1" forever,
|
|
2714
|
+
// aria-sort never flips, the pin position never sticks. Touching rowModelVer puts each
|
|
2715
|
+
// helper in the reactive scope. The chrome helpers prefix `tick()` in their guard.
|
|
2273
2716
|
tick = () => this._rowModelVer.value;
|
|
2274
2717
|
|
|
2718
|
+
// the live sort direction off the table-core column (string-safe — never a bound
|
|
2719
|
+
// boolean, the listbox aria lesson).
|
|
2275
2720
|
ariaSortFor = (colId: any) => {
|
|
2276
2721
|
if (this.tick() < 0 || !this.table) return 'none';
|
|
2277
2722
|
const col = this.table.getColumn(colId);
|
|
@@ -2282,6 +2727,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2282
2727
|
return 'none';
|
|
2283
2728
|
};
|
|
2284
2729
|
|
|
2730
|
+
// A small sort-direction glyph for the header (▲/▼/empty). Decorative — aria-hidden.
|
|
2285
2731
|
sortIndicator = (colId: any) => {
|
|
2286
2732
|
if (this.tick() < 0 || !this.table) return '';
|
|
2287
2733
|
const col = this.table.getColumn(colId);
|
|
@@ -2292,36 +2738,63 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2292
2738
|
return '';
|
|
2293
2739
|
};
|
|
2294
2740
|
|
|
2741
|
+
// Template helpers reading the resolved column-def metadata by id (plain fns — used
|
|
2742
|
+
// in template predicates + interpolation; uniform on all 6, no $computed alias trap).
|
|
2295
2743
|
defFor = (colId: any) => {
|
|
2296
2744
|
const defs = this.columnDefs();
|
|
2297
2745
|
for (const d of defs as any) if (d.id === colId) return d;
|
|
2298
2746
|
return null;
|
|
2299
2747
|
};
|
|
2300
2748
|
|
|
2749
|
+
// Per-row visible cells for the body loop. table-core memoizes row objects by id,
|
|
2750
|
+
// so a re-pull after a column change (visibility/reorder/pin, or the late <Column>
|
|
2751
|
+
// registry on first mount) returns the SAME row references with a different cell
|
|
2752
|
+
// set. On Solid the row loop keeps the existing <tr> across that pull (`:key="row.id"`
|
|
2753
|
+
// is stable, so the emitter's `<Key>` reconciler holds the node), and Solid will NOT
|
|
2754
|
+
// re-run a child loop whose `each` reads no signal — so a bare `row.getVisibleCells()`
|
|
2755
|
+
// goes stale (header reorders, cells don't). Reading `$data.rowModelVer` (bumped by every
|
|
2756
|
+
// refreshRowModel) inside the `each` puts the inner loop in the reactive scope, so it
|
|
2757
|
+
// re-derives the cells on every row-model change. No-op on the coarse-render targets.
|
|
2301
2758
|
visibleCellsFor = (row: any) => this._rowModelVer.value >= 0 ? row.getVisibleCells() : [];
|
|
2302
2759
|
|
|
2760
|
+
// ── Editable-cell column-meta accessors (phase 51 req-1/2/5) ───────────────────────
|
|
2761
|
+
// editMetaOf: the resolved ColumnDef.meta for a column id (the editable config carried
|
|
2762
|
+
// from <Column>/`:columns` via columnDefs). Null-safe — an unknown/non-editable column
|
|
2763
|
+
// returns null and every predicate below short-circuits to the read-only path.
|
|
2303
2764
|
editMetaOf = (colId: any) => {
|
|
2304
2765
|
const d = this.defFor(colId);
|
|
2305
2766
|
return d && d.meta ? d.meta : null;
|
|
2306
2767
|
};
|
|
2307
2768
|
|
|
2769
|
+
// columnEditable: whether this column opted into editing (req-1). Drives every editor
|
|
2770
|
+
// gate; false → the cell stays the read-only #cell display (byte-identical-off).
|
|
2308
2771
|
columnEditable = (colId: any) => {
|
|
2309
2772
|
const m = this.editMetaOf(colId);
|
|
2310
2773
|
return !!(m && m.editable === true);
|
|
2311
2774
|
};
|
|
2312
2775
|
|
|
2776
|
+
// editorTypeOf: the built-in editor kind ('text'|'number'|'select'|'checkbox') OR
|
|
2777
|
+
// 'custom' (the #editor scoped-slot escape hatch, req-2). Defaults to 'text'.
|
|
2313
2778
|
editorTypeOf = (colId: any) => {
|
|
2314
2779
|
const m = this.editMetaOf(colId);
|
|
2315
2780
|
return m && m.editor != null ? m.editor : 'text';
|
|
2316
2781
|
};
|
|
2317
2782
|
|
|
2783
|
+
// editorOptionsOf: the select-editor options ([{ value, label }]) for editor='select'.
|
|
2318
2784
|
editorOptionsOf = (colId: any) => {
|
|
2319
2785
|
const m = this.editMetaOf(colId);
|
|
2320
2786
|
return m && m.editorOptions != null ? m.editorOptions : [];
|
|
2321
2787
|
};
|
|
2322
2788
|
|
|
2789
|
+
// hasEditorSlot: this column routes through the consumer's #editor scoped slot (req-2)
|
|
2790
|
+
// — true only when the column declared editor='custom' AND the consumer actually
|
|
2791
|
+
// provided an #editor slot. Falls through to the built-in editor otherwise (e.g. a
|
|
2792
|
+
// column marked 'custom' with no slot supplied degrades to the text editor, never blank).
|
|
2323
2793
|
hasEditorSlot = (colId: any) => this.editorTypeOf(colId) === 'custom' && !!(this._hasSlotEditor || this.editor !== undefined);
|
|
2324
2794
|
|
|
2795
|
+
// hasFilterSlot: the consumer supplied a #filter scoped slot, so it OWNS the per-column
|
|
2796
|
+
// filter UI (re-added in 72-05 alongside the dedicated filter row's `<slot name="filter">`
|
|
2797
|
+
// host — see the 72-03 removal note in that plan's SUMMARY for why this was briefly gone).
|
|
2325
2798
|
hasFilterSlot = () => !!(this._hasSlotFilter || this.filter !== undefined);
|
|
2326
2799
|
|
|
2327
2800
|
columnIsFilterable = (colId: any) => {
|
|
@@ -2334,6 +2807,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2334
2807
|
return d ? d.header : colId;
|
|
2335
2808
|
};
|
|
2336
2809
|
|
|
2810
|
+
// ── Column-management chrome (req-8/9/10/11) ────────────────────────────────────────
|
|
2811
|
+
// Live header width (px) for a column — drives the <th> :style width binding. Reads the
|
|
2812
|
+
// table-core column size (post-mount) with a fallback to undefined (auto width).
|
|
2337
2813
|
headerWidth = (colId: any) => {
|
|
2338
2814
|
if (this.tick() < 0 || !this.table) return null;
|
|
2339
2815
|
const col = this.table.getColumn(colId);
|
|
@@ -2342,6 +2818,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2342
2818
|
return w != null && w > 0 ? w + 'px' : null;
|
|
2343
2819
|
};
|
|
2344
2820
|
|
|
2821
|
+
// Pointer-drag resize handler for a resizable header — table-core's getResizeHandler()
|
|
2822
|
+
// returns a function bound to a pointerdown/touchstart event that drives the column
|
|
2823
|
+
// size through onColumnSizingChange (our writeColumnSizing funnel) under
|
|
2824
|
+
// columnResizeMode:'onChange'. Pure delegation; no scratch gesture state held in a
|
|
2825
|
+
// top-level const (the React fragile-binding rule — table-core owns the gesture state).
|
|
2345
2826
|
onResizeStart = (colId: any, evt: any) => {
|
|
2346
2827
|
// stop here (NOT a `.stop` modifier) — the Angular `.stop`-in-@for hoist is broken (F5).
|
|
2347
2828
|
if (evt && evt.stopPropagation) evt.stopPropagation();
|
|
@@ -2352,6 +2833,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2352
2833
|
if (handler) handler(evt);
|
|
2353
2834
|
};
|
|
2354
2835
|
|
|
2836
|
+
// Find the live header object for a column id across the rendered header groups.
|
|
2355
2837
|
findHeader = (colId: any) => {
|
|
2356
2838
|
const groups = this._headerGroups.value || [];
|
|
2357
2839
|
for (const hg of groups as any) {
|
|
@@ -2367,6 +2849,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2367
2849
|
return !!(header && header.column && header.column.getIsResizing && header.column.getIsResizing());
|
|
2368
2850
|
};
|
|
2369
2851
|
|
|
2852
|
+
// Visibility toggle (req-8) — drive table-core's column.toggleVisibility so the
|
|
2853
|
+
// onColumnVisibilityChange funnel emits the fresh state.
|
|
2370
2854
|
columnIsVisible = (colId: any) => {
|
|
2371
2855
|
if (this.tick() < 0 || !this.table) return true;
|
|
2372
2856
|
const col = this.table.getColumn(colId);
|
|
@@ -2379,6 +2863,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2379
2863
|
if (col && col.toggleVisibility) col.toggleVisibility();
|
|
2380
2864
|
};
|
|
2381
2865
|
|
|
2866
|
+
// The full set of leaf columns (for the visibility-toggle menu) — id + header label +
|
|
2867
|
+
// current visibility. Excludes the auto-injected CHROME columns (select + expander) —
|
|
2868
|
+
// neither is a data column: they carry no header label (so they'd surface their raw
|
|
2869
|
+
// internal id, e.g. '__rdt_expander') and their presence is governed by the
|
|
2870
|
+
// selectionMode/expandable props, not user-toggleable visibility.
|
|
2382
2871
|
allLeafColumns = () => {
|
|
2383
2872
|
if (this.tick() < 0 || !this.table) return [];
|
|
2384
2873
|
const cols = this.table.getAllLeafColumns ? this.table.getAllLeafColumns() : [];
|
|
@@ -2394,6 +2883,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2394
2883
|
return out;
|
|
2395
2884
|
};
|
|
2396
2885
|
|
|
2886
|
+
// Pinning (req-11) — drive table-core's column.pin('left'|'right'|false) so the
|
|
2887
|
+
// onColumnPinningChange funnel emits a fresh state. Sticky offsets read the live column
|
|
2888
|
+
// start/after positions (table-core computes them from the pinned column sizes).
|
|
2397
2889
|
columnPinSide = (colId: any) => {
|
|
2398
2890
|
if (this.tick() < 0 || !this.table) return false;
|
|
2399
2891
|
const col = this.table.getColumn(colId);
|
|
@@ -2401,6 +2893,12 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2401
2893
|
return col.getIsPinned();
|
|
2402
2894
|
};
|
|
2403
2895
|
|
|
2896
|
+
// NOTE: the event is stopped HERE (evt.stopPropagation()) rather than via a `.stop`
|
|
2897
|
+
// template modifier. The Angular emitter, hoisting a `.stop`-modified handler that
|
|
2898
|
+
// lives INSIDE an `@for` loop into a class-field wrapper, drops the component `this.`
|
|
2899
|
+
// qualifier (→ `onPinColumn(...)` bare ReferenceError) and fails to capture the loop
|
|
2900
|
+
// var — so a `@click.stop="onPinColumn(...)"` inside the header `@for` breaks on
|
|
2901
|
+
// Angular (F5). Stopping inside the handler sidesteps the broken hoist on all six.
|
|
2404
2902
|
onPinColumn = (colId: any, side: any, evt: any) => {
|
|
2405
2903
|
if (evt && evt.stopPropagation) evt.stopPropagation();
|
|
2406
2904
|
if (!this.table) return;
|
|
@@ -2408,6 +2906,22 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2408
2906
|
if (col && col.pin) col.pin(side);
|
|
2409
2907
|
};
|
|
2410
2908
|
|
|
2909
|
+
// Sticky inline style for a pinned header/cell — position:sticky + the computed left or
|
|
2910
|
+
// right offset. Returns '' (no sticky) for unpinned columns. Returned as a STRING (the
|
|
2911
|
+
// :style binding is value-driven — never an eval'd attr).
|
|
2912
|
+
//
|
|
2913
|
+
// `zIndex` (phase 72 fix, default 1 — body <td> / filter-row <th> layer): an INLINE style
|
|
2914
|
+
// ALWAYS wins over the stylesheet's `.rozie-data-table.rdt-sticky .rdt-thead .rdt-th
|
|
2915
|
+
// { z-index: var(--rdt-sticky-z, 2) }` rule, so a pinned header cell that unconditionally
|
|
2916
|
+
// got `z-index:1` here (same as the pinned body/filter-row cells) silently DOWNGRADED the
|
|
2917
|
+
// intended sticky-header stacking level from 2 to 1 — tying it with the dedicated filter
|
|
2918
|
+
// row's own pinned <th> (72-05), which sits LATER in DOM order (a sibling <tr> beneath the
|
|
2919
|
+
// header row) and therefore visually/interactively covers the header's ⋯ menu (phase 72,
|
|
2920
|
+
// z-index:1000 relative to Popover's OWN local stacking context — capped by the pinned
|
|
2921
|
+
// header <th>'s z-index, since a `position:fixed` descendant does not escape an ancestor's
|
|
2922
|
+
// stacking context, only its layout containing block) whenever that SAME column is both
|
|
2923
|
+
// pinned and filterable. thStyle() (the header caller) passes zIndex=2 so the header layer
|
|
2924
|
+
// always wins ties against the filter-row/body layers, which keep the default of 1.
|
|
2411
2925
|
pinStyle = (colId: any, zIndex = 1) => {
|
|
2412
2926
|
if (this.tick() < 0 || !this.table) return '';
|
|
2413
2927
|
const col = this.table.getColumn(colId);
|
|
@@ -2424,6 +2938,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2424
2938
|
return '';
|
|
2425
2939
|
};
|
|
2426
2940
|
|
|
2941
|
+
// Combined inline style for a <th> (width + pin) and a <td> (pin). Plain string concat —
|
|
2942
|
+
// uniform on all 6, no bound-object trap. zIndex=2 (see pinStyle) so a pinned header cell
|
|
2943
|
+
// — which hosts the ⋯ menu's floating content — always stacks above the pinned filter-row
|
|
2944
|
+
// cell for the same column (zIndex=1, its own default).
|
|
2427
2945
|
thStyle = (colId: any) => {
|
|
2428
2946
|
let s = '';
|
|
2429
2947
|
const w = this.headerWidth(colId);
|
|
@@ -2432,6 +2950,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2432
2950
|
return s;
|
|
2433
2951
|
};
|
|
2434
2952
|
|
|
2953
|
+
// ── Filter chrome handlers ─────────────────────────────────────────────────────────
|
|
2954
|
+
// Global search input → funnel through table-core's setGlobalFilter so the
|
|
2955
|
+
// onGlobalFilterChange callback fires the echo-guarded writer. Capture the fresh local
|
|
2956
|
+
// value (never re-read a just-written $data key — React stale-read).
|
|
2435
2957
|
onGlobalFilterInput = (evt: any) => {
|
|
2436
2958
|
const value = evt && evt.target ? evt.target.value : '';
|
|
2437
2959
|
if (this.table) {
|
|
@@ -2441,16 +2963,21 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2441
2963
|
this.writeGlobalFilter(value);
|
|
2442
2964
|
};
|
|
2443
2965
|
|
|
2966
|
+
// Per-column filter input → setColumnFilter (fresh-array funnel).
|
|
2444
2967
|
onColumnFilterInput = (colId: any, evt: any) => {
|
|
2445
2968
|
const value = evt && evt.target ? evt.target.value : '';
|
|
2446
2969
|
this.setColumnFilter(colId, value);
|
|
2447
2970
|
};
|
|
2448
2971
|
|
|
2972
|
+
// The live global filter value (bound to the search <input>, value-driven NOT eval'd).
|
|
2449
2973
|
globalFilterValue = () => {
|
|
2450
2974
|
const v = this.currentState().globalFilter;
|
|
2451
2975
|
return v != null ? v : '';
|
|
2452
2976
|
};
|
|
2453
2977
|
|
|
2978
|
+
// ── Pagination chrome ────────────────────────────────────────────────────────────
|
|
2979
|
+
// Read the live pagination state off table-core (post-mount) with a currentState()
|
|
2980
|
+
// fallback (pre-mount / SSR). All string-safe (no bound booleans).
|
|
2454
2981
|
pageIndex = () => {
|
|
2455
2982
|
if (this.tick() >= 0 && this.table) return this.table.getState().pagination.pageIndex;
|
|
2456
2983
|
const p = this.currentState().pagination;
|
|
@@ -2463,6 +2990,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2463
2990
|
return p && p.pageSize != null ? p.pageSize : 10;
|
|
2464
2991
|
};
|
|
2465
2992
|
|
|
2993
|
+
// Renamed from `pageCount` → `displayPageCount`: `pageCount` is now a public prop
|
|
2994
|
+
// (server-side manual pagination), and a same-named top-level helper collides with the
|
|
2995
|
+
// destructured prop on Svelte and the @Input/@property class field on Angular/Lit. This
|
|
2996
|
+
// reader is internal (drives the "Page X of Y" chrome) and reads table-core's live
|
|
2997
|
+
// getPageCount(), which now reflects rowCount/pageCount when manual.
|
|
2466
2998
|
displayPageCount = () => {
|
|
2467
2999
|
if (this.tick() < 0 || !this.table) return 1;
|
|
2468
3000
|
const c = this.table.getPageCount();
|
|
@@ -2488,16 +3020,44 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2488
3020
|
this.table.setPageSize(Number.isFinite(n) && n > 0 ? n : 10);
|
|
2489
3021
|
};
|
|
2490
3022
|
|
|
3023
|
+
// ── Row-selection chrome (req-7) ───────────────────────────────────────────────────
|
|
3024
|
+
// Detect the auto-injected leading checkbox column by its constant id (template uses
|
|
3025
|
+
// this to render checkbox chrome instead of an accessor value).
|
|
2491
3026
|
isSelectColumn = (colId: any) => colId === this.SELECT_COL_ID;
|
|
2492
3027
|
|
|
3028
|
+
// ── Expandable-rows template helpers (phase 50, D-04) ──────────────────────────────
|
|
3029
|
+
// isExpanderColumn: the auto-injected leading chevron column predicate (mirrors
|
|
3030
|
+
// isSelectColumn). rowIsExpanded / rowCanExpand read table-core row handles THROUGH the
|
|
3031
|
+
// reactive tick (rowModelVer) so the chevron glyph + aria-expanded + the #detail r-if
|
|
3032
|
+
// re-derive on a re-pull on the fine-grained targets (Solid/Lit) — same discipline as
|
|
3033
|
+
// visibleCellsFor. `!!`-coerced so a bound aria-expanded emits an UNWRAPPED boolean (the
|
|
3034
|
+
// listbox aria lesson — never a rozieAttr string → TS2322 on React/Solid).
|
|
2493
3035
|
isExpanderColumn = (colId: any) => colId === this.EXPANDER_COL_ID;
|
|
2494
3036
|
|
|
3037
|
+
// rowCanExpand gates ONLY the leading expander-column detail chevron. Group-header rows
|
|
3038
|
+
// are excluded (`!getIsGrouped`): with `expandable` + grouping, getRowCanExpand returns
|
|
3039
|
+
// `() => true` for EVERY flattened row, so without this a group header rendered TWO
|
|
3040
|
+
// chevrons — the group-toggle in its grouped cell AND a redundant detail chevron in the
|
|
3041
|
+
// leading column (both fire onToggleExpand on the shared expanded state). A group row's
|
|
3042
|
+
// expand affordance is the group-toggle; the leading-column chevron is detail-only.
|
|
2495
3043
|
rowCanExpand = (row: any) => !!(this.tick() >= 0 && row && row.getCanExpand && row.getCanExpand() && !(row.getIsGrouped && row.getIsGrouped()));
|
|
2496
3044
|
|
|
2497
3045
|
rowIsExpanded = (row: any) => !!(this.tick() >= 0 && row && row.getIsExpanded && row.getIsExpanded());
|
|
2498
3046
|
|
|
3047
|
+
// rowShowsDetail: the #detail <tr> renders ONLY in #detail mode (no getSubRows) when the
|
|
3048
|
+
// row is expanded AND is NOT a group-header row. With getSubRows the children arrive as
|
|
3049
|
+
// ordinary depth-indented rows in $data.rows (table-core flattens) — NO additive detail
|
|
3050
|
+
// row, NO nested r-for (Pitfall 1). The `!rowIsGrouped` guard is load-bearing: grouping
|
|
3051
|
+
// and detail-expand share table-core's SINGLE `expanded` state, so a group-header row is
|
|
3052
|
+
// `getIsExpanded()===true` the moment its group opens; without this guard that expanded
|
|
3053
|
+
// group row also satisfied `getSubRows==null && rowIsExpanded`, painting a spurious
|
|
3054
|
+
// #detail panel under every opened group (the group-toggle looked "linked" to detail).
|
|
2499
3055
|
rowShowsDetail = (row: any) => this.getSubRows == null && !this.rowIsGrouped(row) && this.rowIsExpanded(row);
|
|
2500
3056
|
|
|
3057
|
+
// Toggle a row's expanded state through table-core so onExpandedChange → writeExpanded
|
|
3058
|
+
// fires exactly one expanded-change. Used by the chevron @click (native <button> handles
|
|
3059
|
+
// Enter/Space → click, so NO explicit @keydown.enter/.space — that would DOUBLE-toggle on
|
|
3060
|
+
// a real button; the grid @keydown is inert in 'table' mode, isGrid()-gated).
|
|
2501
3061
|
onToggleExpand = (row: any, evt: any) => {
|
|
2502
3062
|
if (!row || !row.toggleExpanded) return;
|
|
2503
3063
|
// Capture the owning row element BEFORE the toggle so DOM focus can be restored after the
|
|
@@ -2521,6 +3081,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2521
3081
|
}
|
|
2522
3082
|
};
|
|
2523
3083
|
|
|
3084
|
+
// bodyCellStyle: the non-virtual <td> inline style — pinStyle PLUS a depth-proportional
|
|
3085
|
+
// left pad on the EXPANDER cell so nested getSubRows children visibly indent (row.depth).
|
|
3086
|
+
// Only the expander column indents (the tree affordance lives in its dedicated column);
|
|
3087
|
+
// data columns stay grid-aligned. depth 0 → unchanged (byte-identical-off).
|
|
2524
3088
|
bodyCellStyle = (row: any, colId: any) => {
|
|
2525
3089
|
const base = this.pinStyle(colId);
|
|
2526
3090
|
if (this.isExpanderColumn(colId) && row && row.depth) {
|
|
@@ -2531,20 +3095,51 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2531
3095
|
return base;
|
|
2532
3096
|
};
|
|
2533
3097
|
|
|
3098
|
+
// ── Grouping template helpers (phase 50 reqs 4-7, D-04/D-05) ───────────────────────────
|
|
3099
|
+
// Group-header rows ARE expandable rows: table-core's getGroupedRowModel FLATTENS them into
|
|
3100
|
+
// $data.rows carrying getIsGrouped()/subRows, so they ride the SAME D-04 <template r-for> seam
|
|
3101
|
+
// (no parallel render path, no nested r-for). These predicates read through the reactive tick
|
|
3102
|
+
// (rowModelVer) so the group chrome + collapse state re-derive on a re-pull on the fine-grained
|
|
3103
|
+
// targets (Solid/Lit) — same discipline as rowIsExpanded/visibleCellsFor. `!!`-coerced (the
|
|
3104
|
+
// listbox aria lesson — a bound boolean must be UNWRAPPED, never a rozieAttr string → TS2322).
|
|
3105
|
+
// rowIsGrouped: this flattened row is a group-header row.
|
|
2534
3106
|
rowIsGrouped = (row: any) => !!(this.tick() >= 0 && row && row.getIsGrouped && row.getIsGrouped());
|
|
2535
3107
|
|
|
3108
|
+
// groupingActive: grouping is currently engaged (a non-empty ordered key list). Drives the
|
|
3109
|
+
// data-group-leaf marker so it is ABSENT when ungrouped (byte-identical-off, req-10).
|
|
2536
3110
|
groupingActive = () => this.tick() >= 0 && (this.currentState().grouping || []).length > 0;
|
|
2537
3111
|
|
|
3112
|
+
// cellIsGrouped / cellIsAggregated: per-CELL roles on a group-header row. The grouped cell shows
|
|
3113
|
+
// the group key + toggle + count; an aggregated cell shows the rolled-up value through the
|
|
3114
|
+
// EXISTING #cell slot (cell.getValue()) — NO new aggregatedCell template (RESEARCH State of the
|
|
3115
|
+
// Art). A placeholder cell (neither) falls through to the #cell r-else and renders its empty value.
|
|
2538
3116
|
cellIsGrouped = (cellCtx: any) => !!(this.tick() >= 0 && cellCtx && cellCtx.getIsGrouped && cellCtx.getIsGrouped());
|
|
2539
3117
|
|
|
2540
3118
|
cellIsAggregated = (cellCtx: any) => !!(this.tick() >= 0 && cellCtx && cellCtx.getIsAggregated && cellCtx.getIsAggregated());
|
|
2541
3119
|
|
|
3120
|
+
// cellIsPlaceholder: a PLACEHOLDER cell on a group-header row — a non-grouped, non-aggregated
|
|
3121
|
+
// cell that table-core fills with the FIRST leaf row's value (cell.getValue() leaks e.g.
|
|
3122
|
+
// "Services"/"Edsger Dijkstra" onto the group line). Renders BLANK via a dedicated empty
|
|
3123
|
+
// template branch so the leaked leaf value never paints. Tick-gated exactly like cellIsGrouped
|
|
3124
|
+
// so the group chrome re-derives on a re-pull on the fine-grained targets (Solid/Lit).
|
|
2542
3125
|
cellIsPlaceholder = (cellCtx: any) => !!(this.tick() >= 0 && cellCtx && cellCtx.getIsPlaceholder && cellCtx.getIsPlaceholder());
|
|
2543
3126
|
|
|
3127
|
+
// groupSubRowCount: the number of underlying LEAF RECORDS under a group-header row (the count
|
|
3128
|
+
// shown in the header, e.g. "North (40)"). row.subRows is the IMMEDIATE members — for MULTI-LEVEL
|
|
3129
|
+
// grouping those are sub-GROUPS, not records, so "North" with 2 categories / 40 records would show
|
|
3130
|
+
// "North (2)". getLeafRows() returns all leaf descendants (the actual record count); keep the
|
|
3131
|
+
// subRows fallback for safety. Single-level grouping is unchanged (getLeafRows == subRows when the
|
|
3132
|
+
// children are already leaves).
|
|
2544
3133
|
groupSubRowCount = (row: any) => row && row.getLeafRows ? row.getLeafRows().length : row && row.subRows ? row.subRows.length : 0;
|
|
2545
3134
|
|
|
3135
|
+
// groupingKeys: the live ordered grouping array — slot prop for the headless #groupBar + the
|
|
3136
|
+
// default styled-token reflection. Reads currentState() ($props.grouping ?? $data.groupingDefault),
|
|
3137
|
+
// both reactive sources, so the bar re-renders on a grouping change across all six targets.
|
|
2546
3138
|
groupingKeys = () => this.currentState().grouping || [];
|
|
2547
3139
|
|
|
3140
|
+
// groupableColumns: the data columns OFFERED to the headless #groupBar (those whose Column/config
|
|
3141
|
+
// `groupable` is not false) — `[{ id, label }]`. Excludes the chrome columns (select/expander are
|
|
3142
|
+
// not in columnDefs()). The consumer builds any bar/drag UI from this; the component ships none.
|
|
2548
3143
|
groupableColumns = () => {
|
|
2549
3144
|
const out = [];
|
|
2550
3145
|
const defs = this.columnDefs();
|
|
@@ -2558,10 +3153,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2558
3153
|
return out;
|
|
2559
3154
|
};
|
|
2560
3155
|
|
|
3156
|
+
// Plain stop-propagation handler (used in place of the `@click.stop` bare modifier —
|
|
3157
|
+
// a bare `.stop` with no handler hoists to `_guardedUndefined` → `this.undefined($event)`
|
|
3158
|
+
// on Angular inside an `@for`, F5). Calling an explicit handler is uniform on all six.
|
|
2561
3159
|
stopEvent = (evt: any) => {
|
|
2562
3160
|
if (evt && evt.stopPropagation) evt.stopPropagation();
|
|
2563
3161
|
};
|
|
2564
3162
|
|
|
3163
|
+
// select-all header state (D-06: scopes to all filtered rows = TanStack default).
|
|
3164
|
+
// `!!`-coerced booleans (the listbox aria lesson — never a bound rozieAttr string).
|
|
2565
3165
|
isAllRowsSelected = () => !!(this.tick() >= 0 && this.table && this.table.getIsAllRowsSelected());
|
|
2566
3166
|
|
|
2567
3167
|
isSomeRowsSelected = () => !!(this.tick() >= 0 && this.table && this.table.getIsSomeRowsSelected());
|
|
@@ -2571,6 +3171,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2571
3171
|
this.table.toggleAllRowsSelected(!!(evt && evt.target && evt.target.checked));
|
|
2572
3172
|
};
|
|
2573
3173
|
|
|
3174
|
+
// per-row checkbox state + toggle (checkbox-only, D-05 — row body does NOT select).
|
|
3175
|
+
// Read selection from the LIVE controlled state (currentState().rowSelection keyed by
|
|
3176
|
+
// row.id) — NOT row.getIsSelected(). The latter reads table-core's row model, which
|
|
3177
|
+
// only reflects a selection AFTER the re-feed watch pushes the new `state` + re-pulls
|
|
3178
|
+
// (two reactive cycles on React). The controlled-state read updates in the SAME cycle
|
|
3179
|
+
// as the write funnel, so the controlled <input :checked> reflects the toggle without
|
|
3180
|
+
// the row-model-re-pull latency — the React controlled-checkbox revert that left
|
|
3181
|
+
// `.check()` seeing no state change (F6). row.getIsSelected() is the fallback.
|
|
2574
3182
|
rowIsSelected = (row: any) => {
|
|
2575
3183
|
if (!row) return false;
|
|
2576
3184
|
const id = row.id;
|
|
@@ -2584,6 +3192,12 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2584
3192
|
row.toggleSelected(!!(evt && evt.target && evt.target.checked));
|
|
2585
3193
|
};
|
|
2586
3194
|
|
|
3195
|
+
// ── Header ⋯ menu chrome (phase 72, D-06) ──────────────────────────────────────────
|
|
3196
|
+
// onHideColumn: the ⋯ menu's "Hide column" item. Reuses the SAME columnVisibility write
|
|
3197
|
+
// funnel as the existing colvis toggle (onToggleVisibility in columnChrome.rzts) — just
|
|
3198
|
+
// forced to `false` rather than toggled, since hide is a one-directional action from the
|
|
3199
|
+
// menu (the colvis panel is the re-show path). Event stopped HERE (not a `.stop`
|
|
3200
|
+
// modifier) — same Angular @for-hoist hazard as onPinColumn/onResizeStart (F5).
|
|
2587
3201
|
onHideColumn = (colId: any, evt: any) => {
|
|
2588
3202
|
if (evt && evt.stopPropagation) evt.stopPropagation();
|
|
2589
3203
|
if (!this.table) return;
|
|
@@ -2591,6 +3205,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2591
3205
|
if (col && col.toggleVisibility) col.toggleVisibility(false);
|
|
2592
3206
|
};
|
|
2593
3207
|
|
|
3208
|
+
// hasAnyFilterableColumn: gates the dedicated filter row (72-05) — true when at least one
|
|
3209
|
+
// leaf column (excluding the select/expander chrome columns, already excluded by
|
|
3210
|
+
// allLeafColumns) is filterable. Reactive via allLeafColumns()'s own tick() gate.
|
|
2594
3211
|
hasAnyFilterableColumn = () => {
|
|
2595
3212
|
const cols = this.allLeafColumns();
|
|
2596
3213
|
for (const c of cols as any) {
|
|
@@ -2599,6 +3216,18 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2599
3216
|
return false;
|
|
2600
3217
|
};
|
|
2601
3218
|
|
|
3219
|
+
// `indeterminate` is a DOM PROPERTY, not an HTML attribute — a `:indeterminate="…"`
|
|
3220
|
+
// binding only takes effect on Vue (which binds known DOM props); on
|
|
3221
|
+
// React/Solid/Angular/Lit/Svelte it lands as an inert attribute and `el.indeterminate`
|
|
3222
|
+
// stays false. So set it IMPERATIVELY: query the select-all checkbox off the component
|
|
3223
|
+
// root ($el — post-mount safe) and assign the property. Called from refreshRowModel
|
|
3224
|
+
// (every selection change re-pulls the row model) so it stays in lockstep with the
|
|
3225
|
+
// table-core selection state. The select-all box is NOT re-created by a selection
|
|
3226
|
+
// change (only its checked attr flips), so the live element persists.
|
|
3227
|
+
// `box` is aliased through a module-scope null-let (typeNeutralize → `any`) so the
|
|
3228
|
+
// strict bundled-leaf tsc accepts `.indeterminate` (querySelector returns `Element`,
|
|
3229
|
+
// which has no `indeterminate` — it is an HTMLInputElement DOM property). Same idiom
|
|
3230
|
+
// as Column's `let reg = null; reg = $inject(...)`.
|
|
2602
3231
|
selectAllBox: any = null;
|
|
2603
3232
|
|
|
2604
3233
|
syncIndeterminate = () => {
|
|
@@ -2607,6 +3236,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2607
3236
|
if (this.selectAllBox) this.selectAllBox.indeterminate = this.isSomeRowsSelected() && !this.isAllRowsSelected();
|
|
2608
3237
|
};
|
|
2609
3238
|
|
|
3239
|
+
// The registry API handed to <Column> children (whole-object-replace — T-48-PP guard).
|
|
3240
|
+
// Imperative handle (consumer-callable). Each verb is a PRE-DECLARED top-level
|
|
3241
|
+
// `const` (the canonical $expose contract — `$expose({ name })` references a
|
|
3242
|
+
// binding ALREADY in scope; an INLINE-defined verb `$expose({ name: () => {} })`
|
|
3243
|
+
// is dropped on ALL SIX targets, only the by-reference key survives → a
|
|
3244
|
+
// runtime ReferenceError at `defineExpose`/`useImperativeHandle`). Sorting verbs +
|
|
3245
|
+
// a fresh column-def readout, selection, pagination, and column-management verbs.
|
|
2610
3246
|
sortColumn = (colId: any, desc: any) => {
|
|
2611
3247
|
if (this.table) this.table.getColumn(colId) && this.table.getColumn(colId).toggleSorting(desc, false);
|
|
2612
3248
|
};
|
|
@@ -2617,6 +3253,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2617
3253
|
|
|
2618
3254
|
getColumnDefs = () => this.columnDefs();
|
|
2619
3255
|
|
|
3256
|
+
// selection verbs (req-7) — drive table-core so the onRowSelectionChange funnel
|
|
3257
|
+
// emits the fresh state + selection-change.
|
|
2620
3258
|
toggleAllRows = (value: any) => {
|
|
2621
3259
|
if (this.table) this.table.toggleAllRowsSelected(value);
|
|
2622
3260
|
};
|
|
@@ -2627,6 +3265,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2627
3265
|
|
|
2628
3266
|
getSelectedRows = () => this.table ? this.table.getSelectedRowModel().rows.map((r: any) => r.original) : [];
|
|
2629
3267
|
|
|
3268
|
+
// pagination verbs.
|
|
2630
3269
|
setPage = (idx: any) => {
|
|
2631
3270
|
if (this.table) this.table.setPageIndex(idx);
|
|
2632
3271
|
};
|
|
@@ -2635,6 +3274,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2635
3274
|
if (this.table) this.table.setPageSize(size);
|
|
2636
3275
|
};
|
|
2637
3276
|
|
|
3277
|
+
// column-management verbs (req-8/9/10/11) — drive table-core so the funnels fire.
|
|
2638
3278
|
toggleColumnVisibility = (colId: any) => {
|
|
2639
3279
|
if (this.table) {
|
|
2640
3280
|
const c = this.table.getColumn(colId);
|
|
@@ -2642,6 +3282,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2642
3282
|
}
|
|
2643
3283
|
};
|
|
2644
3284
|
|
|
3285
|
+
// NOT `setColumnOrder`: a verb named `set<ModelProp>` collides with React's
|
|
3286
|
+
// auto-generated `setColumnOrder` useState setter for the `columnOrder` model
|
|
3287
|
+
// prop, and an $expose verb is PUBLIC-CONTRACT-PROTECTED from the React
|
|
3288
|
+
// deconfliction rename (ROZ524 — the rename target is the verb, which is
|
|
3289
|
+
// off-limits). So the public verb is `applyColumnOrder` (semantically: apply a
|
|
3290
|
+
// new column order). The other set* verbs (setPage/setRowsPerPage) do NOT match
|
|
3291
|
+
// any model prop's setter, so they are collision-free.
|
|
2645
3292
|
applyColumnOrder = (order: any) => {
|
|
2646
3293
|
if (this.table) this.table.setColumnOrder(order);
|
|
2647
3294
|
};
|
|
@@ -2650,6 +3297,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2650
3297
|
if (this.table) this.table.resetColumnSizing(true);
|
|
2651
3298
|
};
|
|
2652
3299
|
|
|
3300
|
+
// pinColumn: the verb that drives column.pin; distinct from the template handler
|
|
3301
|
+
// onPinColumn (no shadow — the deferred-items finding #4 collision check).
|
|
2653
3302
|
pinColumn = (colId: any, side: any) => {
|
|
2654
3303
|
if (this.table) {
|
|
2655
3304
|
const c = this.table.getColumn(colId);
|
|
@@ -2657,39 +3306,101 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2657
3306
|
}
|
|
2658
3307
|
};
|
|
2659
3308
|
|
|
3309
|
+
// getRowIndexRelativeToPage(absRow?) — C1 (phase 63 wave-6) converter: an ABSOLUTE display-order
|
|
3310
|
+
// index (the focusCell/getActiveCell/activecell-change space) → the PAGE-RELATIVE index. Mirrors
|
|
3311
|
+
// MUI getRowIndexRelativeToVisibleRows. With NO argument it converts the CURRENT active cell
|
|
3312
|
+
// (toAbsRow($data.activeRow) - pageRowOffset() collapses to $data.activeRow). In virtual mode
|
|
3313
|
+
// there is no page (windowing replaces pagination) → the windowed model IS the full model, so it
|
|
3314
|
+
// returns the absolute index unchanged. Collision-safe: no *-change event, prop, React auto-setter,
|
|
3315
|
+
// or inherited Lit DOM method named getRowIndexRelativeToPage (ROZ121/124/137 clear).
|
|
2660
3316
|
getRowIndexRelativeToPage = (absRow: any) => {
|
|
2661
3317
|
const abs = absRow == null ? this.toAbsRow(this._activeRow.value) : Math.trunc(Number(absRow)) || 0;
|
|
2662
3318
|
if (this.virtual) return abs;
|
|
2663
3319
|
return abs - this.pageRowOffset();
|
|
2664
3320
|
};
|
|
2665
3321
|
|
|
3322
|
+
// C3 (phase 63 wave-9) — the PUBLIC Cut verb: copy the current cell range to the clipboard then
|
|
3323
|
+
// clear the source cells through the write-funnel (one writeData), delegating to cutRange (the
|
|
3324
|
+
// clipboardFill funnel that also backs the Ctrl+X shortcut). Reads the persisted $data range /
|
|
3325
|
+
// active cell, so it cuts the current selection even when the call arrives off a control that
|
|
3326
|
+
// moved DOM focus off the grid. Collision-safe: no `cut` event / model prop / React auto-setter /
|
|
3327
|
+
// inherited Lit DOM method named `cut` (ROZ121/124/137 clear) — `cut` is not on HTMLElement.
|
|
2666
3328
|
cut = () => this.cutRange();
|
|
2667
3329
|
|
|
3330
|
+
// 260709-8ct (grid-wide undo/redo): NO pass-through wrapper lands here for
|
|
3331
|
+
// undo/redo/canUndo/canRedo/clearHistory. Unlike `cut` above (which delegates to a
|
|
3332
|
+
// differently-named clipboardFill export, `cutRange`), the undoHistory.rzts exports already
|
|
3333
|
+
// use the exact public verb names and are already component-scope (imported directly into
|
|
3334
|
+
// DataTable.rozie) — so DataTable.rozie's $expose references them BY NAME with zero
|
|
3335
|
+
// indirection. This file stays the seam for verbs that need a rename/adapter, not a mandatory
|
|
3336
|
+
// stop for every $expose entry.
|
|
3337
|
+
// ══ Grid interaction mode (phase 49) — STATE + STRUCTURE only ═══════════════════════════
|
|
3338
|
+
// This plan (02) establishes the gated ARIA roles, the roving single-tab-stop tabindex,
|
|
3339
|
+
// the active-cell index-pair state, the data-* cell markers, and the SINGLE
|
|
3340
|
+
// focusActiveCell() seam. Plan 03 adds the keydown navigation math, the $expose verbs
|
|
3341
|
+
// (focusCell/getActiveCell/clearActiveCell), and the activecell-change event ON TOP.
|
|
3342
|
+
// interactionMode gate. 'grid' lights up roving nav; 'table' (default) is byte-behaviorally
|
|
3343
|
+
// identical to phase 48 (roles fall back to the literals, tabindex drops).
|
|
2668
3344
|
isGrid = () => this.interactionMode === 'grid';
|
|
2669
3345
|
|
|
3346
|
+
// Role computeds (RESEARCH Pattern 4). The 'table' branch returns the EXACT phase-48
|
|
3347
|
+
// literal so 'table'-mode DOM is unchanged. Header cells keep 'columnheader' and rows keep
|
|
3348
|
+
// 'row'/'rowgroup' in BOTH modes (APG grid) — those stay static literals in the template.
|
|
2670
3349
|
tableRole = () => this.isGrid() ? 'grid' : 'table';
|
|
2671
3350
|
|
|
2672
3351
|
cellRole = () => this.isGrid() ? 'gridcell' : 'cell';
|
|
2673
3352
|
|
|
3353
|
+
// ── Cell addressing helpers (plain fns — no $computed alias trap; safe in template) ────
|
|
3354
|
+
// rowIndexOf: a body row's index over the visible model ($data.rows). tick() puts the read
|
|
3355
|
+
// in the fine-grained reactive scope (Solid/Lit) so the data-row marker re-derives on a
|
|
3356
|
+
// re-pull (reorder/filter) — matching visibleCellsFor's discipline.
|
|
2674
3357
|
rowIndexOf = (row: any) => this.tick() >= 0 ? (this._rows.value || []).indexOf(row) : -1;
|
|
2675
3358
|
|
|
3359
|
+
// colIndexOf: a body cell's position in its row's visible cell list.
|
|
2676
3360
|
colIndexOf = (row: any, cellCtx: any) => this.tick() >= 0 ? this.visibleCellsFor(row).indexOf(cellCtx) : -1;
|
|
2677
3361
|
|
|
3362
|
+
// headerColIndexOf: a header cell's position in its header group's leaf headers.
|
|
2678
3363
|
headerColIndexOf = (hg: any, header: any) => (hg && hg.headers ? hg.headers : []).indexOf(header);
|
|
2679
3364
|
|
|
3365
|
+
// ── C1 (phase 63 wave-6) absolute-index bridge ─────────────────────────────────────────
|
|
3366
|
+
// The PUBLIC active-cell rowIndex (focusCell/getActiveCell/activecell-change) is the ABSOLUTE
|
|
3367
|
+
// display-order position in getPrePaginationRowModel().rows (filter+sort+expand applied, BEFORE
|
|
3368
|
+
// pagination/windowing), in BOTH paginated and virtual modes — reversing the old page-relative
|
|
3369
|
+
// paginated meaning. INTERNALLY $data.activeRow stays PAGE-RELATIVE in the non-virtual paginated
|
|
3370
|
+
// body (the data-row markers + the nav math index the page slice) and FULL-MODEL in virtual mode
|
|
3371
|
+
// (the wr.vi.index space). pageRowOffset() bridges the two so the API speaks one absolute language.
|
|
3372
|
+
// - virtual mode: activeRow is already the full pre-pagination index → offset 0.
|
|
3373
|
+
// - non-virtual: activeRow is page-relative → offset = pageIndex * pageSize.
|
|
3374
|
+
// isGrid()-gated (the active-cell API is grid-only); pageIndex()/pageSize() read live table-core
|
|
3375
|
+
// state through the reactive tick (filterPaginationRowChrome), so this re-derives on a page change.
|
|
2680
3376
|
pageRowOffset = () => {
|
|
2681
3377
|
if (!this.isGrid() || this.virtual) return 0;
|
|
2682
3378
|
return this.pageIndex() * this.pageSize();
|
|
2683
3379
|
};
|
|
2684
3380
|
|
|
3381
|
+
// page-relative active row → absolute (display-order) index.
|
|
2685
3382
|
toAbsRow = (localRow: any) => localRow + this.pageRowOffset();
|
|
2686
3383
|
|
|
3384
|
+
// A body row's ABSOLUTE display-order index = its page-relative index + the page offset. Drives
|
|
3385
|
+
// aria-rowindex on the non-virtual paginated body (B27); the virtual path uses wr.vi.index
|
|
3386
|
+
// directly (already absolute). Reactive via rowIndexOf's tick().
|
|
3387
|
+
// Total filtered+sorted PRE-pagination row count — the clamp bound for an absolute focusCell.
|
|
3388
|
+
// In virtual mode $data.rows IS the full pre-pagination model (bodyRowCount suffices); in the
|
|
3389
|
+
// non-virtual paginated body $data.rows is only the page slice, so read the live model.
|
|
2687
3390
|
prePaginationRowCount = () => {
|
|
2688
3391
|
if (!this.table || this.virtual) return this.bodyRowCount();
|
|
2689
3392
|
const pm = this.table.getPrePaginationRowModel();
|
|
2690
3393
|
return pm && pm.rows ? pm.rows.length : this.bodyRowCount();
|
|
2691
3394
|
};
|
|
2692
3395
|
|
|
3396
|
+
// Roving tabindex (RESEARCH Code Examples). Reads ONLY reactive $data (ROZ123-safe,
|
|
3397
|
+
// fine-grained-reactive). Returns null in 'table' mode → the bound numeric attribute
|
|
3398
|
+
// DROPS entirely (IN-01: on React via the `cellTabindex(...) ?? undefined` numeric-attr
|
|
3399
|
+
// emitter path landed in 4bec3b8e — NOT rozieAttr, which would string-widen tabIndex and
|
|
3400
|
+
// TS2322; the other five targets drop it via their own nullish-attr handling), keeping
|
|
3401
|
+
// 'table'-mode DOM clean. rowKey is the literal
|
|
3402
|
+
// '__header' for header cells or the String(bodyRowIndex) for body cells, so the active
|
|
3403
|
+
// header state (activeIsHeader) is addressable through the same computed.
|
|
2693
3404
|
cellTabindex = (rowKey: any, colIndex: any, level = null) => {
|
|
2694
3405
|
if (!this.isGrid()) return null;
|
|
2695
3406
|
// B6: an empty / all-filtered grid (no body rows) must STILL be keyboard-reachable. Fall
|
|
@@ -2710,6 +3421,17 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2710
3421
|
return isActive ? 0 : -1;
|
|
2711
3422
|
};
|
|
2712
3423
|
|
|
3424
|
+
// ── Active-cell ring predicate (grid pointer §1, 260708-ni6) ───────────────────────────
|
|
3425
|
+
// isActiveCell mirrors cellTabindex's ACTIVE branch (the same (rowKey, colIndex, level)
|
|
3426
|
+
// address tuples the roving tabindex uses) but returns a BOOLEAN for the `.rdt-cell-active`
|
|
3427
|
+
// :class binding, and is STATE-DRIVEN — so the ring shows identically on click AND keyboard
|
|
3428
|
+
// (independent of :focus-visible, which browsers gate off for a mouse-focused non-text <td>).
|
|
3429
|
+
// It DELIBERATELY omits cellTabindex's B6 empty-grid / header-fallback branch: the ring must
|
|
3430
|
+
// NOT light on an empty grid's synthetic tab-stop (there is no real active cell there). Reads
|
|
3431
|
+
// ONLY reactive $data (ROZ123-safe, fine-grained). Returns false in 'table' mode so table-mode
|
|
3432
|
+
// markup is byte-behaviorally unchanged. Header cells are active only while activeIsHeader is
|
|
3433
|
+
// true (addressed by BOTH colIndex and level — a grouped multi-level header carries exactly one
|
|
3434
|
+
// ring); body cells only while activeIsHeader is false.
|
|
2713
3435
|
isActiveCell = (rowKey: any, colIndex: any, level = null) => {
|
|
2714
3436
|
if (!this.isGrid()) return false;
|
|
2715
3437
|
if (this._activeIsHeader.value) {
|
|
@@ -2720,6 +3442,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2720
3442
|
return rowKey === String(this._activeRow.value) && colIndex === this._activeColIndex.value;
|
|
2721
3443
|
};
|
|
2722
3444
|
|
|
3445
|
+
// ── The focus SEAM (RESEARCH Pattern 1 + 3, req-6) ─────────────────────────────────────
|
|
3446
|
+
// resolveCellEl: index pair → DOM element, via a data-* attribute query off the stable
|
|
3447
|
+
// post-mount root. Uniform on all six, shadow-safe (the query runs from inside the
|
|
3448
|
+
// component's own scope). rowKey is the literal '__header' or a String(integer index) and
|
|
3449
|
+
// colIndex is an integer — NO consumer string is interpolated into the selector (T-49-01).
|
|
2723
3450
|
resolveCellEl = (rowKey: any, colIndex: any, level = null) => {
|
|
2724
3451
|
if (!this.gridRoot) return null;
|
|
2725
3452
|
// B12: a grouped multi-level header has MULTIPLE cells sharing data-row="__header" at the
|
|
@@ -2733,6 +3460,19 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2733
3460
|
return this.gridRoot.querySelector(sel);
|
|
2734
3461
|
};
|
|
2735
3462
|
|
|
3463
|
+
// focusActiveCell: THE single DOM-focus-resolution path (req-6). Every focus change —
|
|
3464
|
+
// the D-04 entry cell here, and (plan 03) arrow nav / focusCell() / the data-change clamp —
|
|
3465
|
+
// routes through this one function, so a verifier can point to it and phase 53 windowing
|
|
3466
|
+
// hooks it without a rewrite. Accepts OPTIONAL explicit (nextRow,nextCol) so callers can
|
|
3467
|
+
// pass FRESH post-write locals (React ROZ138 / Angular signal async — pinned by plan 01);
|
|
3468
|
+
// falls back to $data when none passed. NEVER stores a DOM node (index-only state).
|
|
3469
|
+
// 260618-ao9 — params carry explicit `= null` defaults so the cross-target
|
|
3470
|
+
// emitters type them OPTIONAL (untyped params lower to REQUIRED `any`, making the
|
|
3471
|
+
// 2-arg `focusActiveCell(r, c)` call sites a TS2554 on React/Solid/Lit — a
|
|
3472
|
+
// pre-existing regression from the d7166c5e header-crossing `nextIsHeader` add).
|
|
3473
|
+
// The `= null` default reproduces the documented "falls back to $data when
|
|
3474
|
+
// omitted" contract: an omitted arg arrives as `null`, and the body's `== null`
|
|
3475
|
+
// checks already route those to the live `$data` value — behavior-identical.
|
|
2736
3476
|
focusActiveCell = (nextRow = null, nextCol = null, nextIsHeader = null, nextLevel = null) => {
|
|
2737
3477
|
if (!this.isGrid() || !this.gridRoot) return;
|
|
2738
3478
|
// #9 focus-intent epoch: focusActiveCell is THE single seam every keyboard nav re-asserts
|
|
@@ -2798,20 +3538,59 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2798
3538
|
if (el) el.focus();
|
|
2799
3539
|
};
|
|
2800
3540
|
|
|
3541
|
+
// ══ Grid keyboard navigation (phase 49 plan 03 — RESEARCH Pattern 5 + the delegated handler) ═══
|
|
3542
|
+
// The nav model is plain ARRAY-INDEX MATH over the VISIBLE model. table-core has already
|
|
3543
|
+
// done the hard part: $data.rows (body) and $data.headerGroups (header) hold the visible,
|
|
3544
|
+
// reordered, pinned cell set (row.getVisibleCells() / getHeaderGroups()) — hidden columns
|
|
3545
|
+
// are ALREADY ABSENT, reorder/pinning is ALREADY REFLECTED (REQ-7). There is NO separate
|
|
3546
|
+
// "compute visible order" step. Every index is clamped to [0,max] so an out-of-range key
|
|
3547
|
+
// never throws or builds an injection-shaped selector (Security V5 / T-49-03).
|
|
3548
|
+
// IN-01: aria-rowcount for the NON-VIRTUAL table. The virtual table binds $data.rows.length
|
|
3549
|
+
// (the full pre-pagination model). For the non-virtual path $data.rows is the PAGINATED slice,
|
|
3550
|
+
// so report the FILTERED (pre-pagination) total instead — the count AT users need to know "row N
|
|
3551
|
+
// of TOTAL". Falls back to $data.rows.length pre-mount (table is null until $onMount).
|
|
3552
|
+
// NB the helper is named `totalRowCount`, NOT `ariaRowCount`: `ariaRowCount` is an inherited
|
|
3553
|
+
// HTMLElement ARIA-reflected property (`Element.ariaRowCount: string`), so a same-named method
|
|
3554
|
+
// becomes a class field that shadows it on Lit → TS2416 cascades to EVERY @property decorator
|
|
3555
|
+
// (the `valueOf`/`nodeType` inherited-DOM-member collision class, authoring playbook §6).
|
|
2801
3556
|
totalRowCount = () => {
|
|
2802
3557
|
if (!this.table) return (this._rows.value || []).length;
|
|
2803
3558
|
const fm = this.table.getFilteredRowModel();
|
|
2804
3559
|
return fm && fm.rows ? fm.rows.length : (this._rows.value || []).length;
|
|
2805
3560
|
};
|
|
2806
3561
|
|
|
3562
|
+
// ── A11y row bookkeeping (#13): consistent aria-rowindex / aria-rowcount ──────────────
|
|
3563
|
+
// WAI-ARIA: when aria-rowcount is set on the grid/table, EVERY row (header rows + body rows)
|
|
3564
|
+
// must carry an aria-rowindex, and aria-rowcount must equal the total number of rows INCLUDING
|
|
3565
|
+
// the header rows. Before this fix aria-rowcount was set unconditionally to totalRowCount() but
|
|
3566
|
+
// aria-rowindex was grid-only — so a paginated 'table'-mode grid advertised e.g. rowcount=100
|
|
3567
|
+
// while its 10 visible rows carried NO index (SR announced "row 1..10 of 100" on the LAST page).
|
|
3568
|
+
// headerRowCount = the columnheader rows ($data.headerGroups — a grouped/multi-level header is
|
|
3569
|
+
// >1; the role="presentation" filter row is NOT a row and is excluded).
|
|
3570
|
+
// gridAriaRowCount = header rows + the FILTERED pre-pagination data total → equals the largest
|
|
3571
|
+
// aria-rowindex any body row carries, so count and indices are always mutually consistent.
|
|
3572
|
+
// NB the helpers are gridAriaRowCount / bodyAriaRowIndex, NOT ariaRowCount / ariaRowIndex: the
|
|
3573
|
+
// latter collide with the inherited HTMLElement.ariaRowCount / .ariaRowIndex reflected properties
|
|
3574
|
+
// on Lit (TS2416 — the same inherited-DOM-member collision class as totalRowCount's rename note).
|
|
2807
3575
|
headerRowCount = () => (this._headerGroups.value || []).length;
|
|
2808
3576
|
|
|
2809
3577
|
gridAriaRowCount = () => this.headerRowCount() + this.totalRowCount();
|
|
2810
3578
|
|
|
3579
|
+
// Page offset that is MODE-INDEPENDENT (works in BOTH 'table' and 'grid' mode), unlike
|
|
3580
|
+
// pageRowOffset() which is isGrid()-gated for the active-cell API. In the non-virtual body
|
|
3581
|
+
// $data.rows is only the page slice, so a data row's ABSOLUTE index = its page-relative
|
|
3582
|
+
// rowIndexOf + this offset. Virtual mode never reaches here (that body uses wr.vi.index).
|
|
2811
3583
|
ariaPageOffset = () => this.table ? this.pageIndex() * this.pageSize() : 0;
|
|
2812
3584
|
|
|
3585
|
+
// A non-virtual body row's 1-based aria-rowindex: the header rows come first (headerRowCount),
|
|
3586
|
+
// then the absolute (page-aware) 0-based data index, +1 to 1-base it. Present in BOTH modes so
|
|
3587
|
+
// it is always consistent with gridAriaRowCount. The virtual body binds
|
|
3588
|
+
// `headerRowCount() + wr.vi.index + 1` inline (wr.vi.index is already the absolute full-model index).
|
|
2813
3589
|
bodyAriaRowIndex = (row: any) => this.headerRowCount() + this.rowIndexOf(row) + this.ariaPageOffset() + 1;
|
|
2814
3590
|
|
|
3591
|
+
// Column count = the visible cell list length (uniform header+body in a flat grid). Reads
|
|
3592
|
+
// $data.rows (reactive) so it is fine-grained-correct on Solid/Lit; falls back to the
|
|
3593
|
+
// header leaf count when there are no body rows.
|
|
2815
3594
|
visibleColCount = () => {
|
|
2816
3595
|
// NB: local is `rowList` (NOT `rows`) — the React emitter lowers `$data.rows` to the bare
|
|
2817
3596
|
// state binding `rows`, so a `const rows = $data.rows` self-shadows it (TS2448 TDZ). Same
|
|
@@ -2826,11 +3605,24 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2826
3605
|
|
|
2827
3606
|
clamp = (v: any, lo: any, hi: any) => v < lo ? lo : v > hi ? hi : v;
|
|
2828
3607
|
|
|
3608
|
+
// ── Multi-level (grouped) header addressing (B12) ──────────────────────────────────────
|
|
3609
|
+
// $data.headerGroups is ordered top→bottom; the LEAF header row (the one adjacent to the
|
|
3610
|
+
// body) is the LAST group. The roving active-header state carries activeHeaderLevel (the
|
|
3611
|
+
// group index) alongside activeColIndex (the index within THAT level's headers) so the
|
|
3612
|
+
// single-tab-stop invariant + ArrowUp parent-resolution span every header level — a flat
|
|
3613
|
+
// grid has one level (leafLevel 0), so the table-mode/flat path is unchanged.
|
|
2829
3614
|
headerLeafLevel = () => {
|
|
2830
3615
|
const hg = this._headerGroups.value || [];
|
|
2831
3616
|
return hg.length ? hg.length - 1 : 0;
|
|
2832
3617
|
};
|
|
2833
3618
|
|
|
3619
|
+
// #10: the number of header cells AT a given level. A grouped PARENT level may have FEWER
|
|
3620
|
+
// headers than there are leaf columns (one parent spans several leaves), so horizontal nav on a
|
|
3621
|
+
// non-leaf header must clamp against THIS count — not visibleColCount() (the leaf-column count),
|
|
3622
|
+
// which would let ArrowRight/End overrun into a phantom (null) cell → focus dropped to <body>.
|
|
3623
|
+
// Degenerate cases (no headerGroups, level out of range) fall back to visibleColCount() so the
|
|
3624
|
+
// clamp is never negative or NaN. The LEAF level's count equals visibleColCount() (one header per
|
|
3625
|
+
// visible leaf column), so leaf-header + body horizontal nav is byte-behaviorally unchanged.
|
|
2834
3626
|
headerCountAtLevel = (level: any) => {
|
|
2835
3627
|
const hg = this._headerGroups.value || [];
|
|
2836
3628
|
if (!hg.length) return this.visibleColCount();
|
|
@@ -2846,6 +3638,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2846
3638
|
return grp.headers[colIndex] || null;
|
|
2847
3639
|
};
|
|
2848
3640
|
|
|
3641
|
+
// ArrowUp from a (level, colIndex) leaf/child header → the index of its PARENT header in the
|
|
3642
|
+
// level above (the parent column that spans it, via table-core header.column.parent). -1 when
|
|
3643
|
+
// there is no real parent (already at the top, or a placeholder with no group) → the caller
|
|
3644
|
+
// keeps the active header where it is.
|
|
2849
3645
|
parentHeaderColIndex = (level: any, colIndex: any) => {
|
|
2850
3646
|
if (level <= 0) return -1;
|
|
2851
3647
|
const h = this.headerAt(level, colIndex);
|
|
@@ -2861,6 +3657,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2861
3657
|
return -1;
|
|
2862
3658
|
};
|
|
2863
3659
|
|
|
3660
|
+
// ArrowDown from a (level, colIndex) GROUP header → the index of its FIRST child header in the
|
|
3661
|
+
// level below (via table-core column.columns). -1 when the header has no child columns (a leaf)
|
|
3662
|
+
// → the caller drops into the body instead.
|
|
2864
3663
|
firstChildHeaderColIndex = (level: any, colIndex: any) => {
|
|
2865
3664
|
const h = this.headerAt(level, colIndex);
|
|
2866
3665
|
if (!h || !h.column) return -1;
|
|
@@ -2877,6 +3676,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2877
3676
|
return -1;
|
|
2878
3677
|
};
|
|
2879
3678
|
|
|
3679
|
+
// ── Nav helpers: compute the NEXT indices into LOCAL consts, write $data from them, and
|
|
3680
|
+
// RETURN the fresh locals so the caller threads the SAME values into BOTH focusActiveCell
|
|
3681
|
+
// AND the activecell-change emit. NEVER re-read $data.activeRow/activeColIndex after the
|
|
3682
|
+
// write (React setState is async — ROZ138 — the re-read binds the PRE-write value; Angular
|
|
3683
|
+
// signal writes are async too — both proven live by plan 01's probe). ──────────────────────
|
|
3684
|
+
// ArrowRight/Left — clamp colIndex over [0, visibleColCount()-1] (no wrap; hidden cols
|
|
3685
|
+
// already excluded from the visible list per REQ-7).
|
|
2880
3686
|
moveCol = (delta: any) => {
|
|
2881
3687
|
// #10: when a grouped PARENT header is active, clamp against the header count AT THE ACTIVE
|
|
2882
3688
|
// LEVEL (which may be fewer than the leaf-column count) so ArrowRight never overruns onto a
|
|
@@ -2888,6 +3694,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2888
3694
|
return nextCol;
|
|
2889
3695
|
};
|
|
2890
3696
|
|
|
3697
|
+
// ArrowUp/Down + PageUp/Down — cross the header boundary and clamp at body edges (no
|
|
3698
|
+
// page-cross per D-06/REQ-7). Returns { row, isHeader } fresh locals.
|
|
3699
|
+
// - From the header, ArrowDown (delta>0) drops into body row 0 (activeIsHeader=false).
|
|
3700
|
+
// - From body row 0, ArrowUp (delta<0) crosses into the header (activeIsHeader=true).
|
|
3701
|
+
// - PageUp/Down jump by ±GRID_PAGE_STEP, clamped to the current page bounds (no cross).
|
|
2891
3702
|
moveRow = (delta: any) => {
|
|
2892
3703
|
const lastRow = this.bodyRowCount() - 1;
|
|
2893
3704
|
const maxRow = lastRow < 0 ? 0 : lastRow;
|
|
@@ -2979,6 +3790,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2979
3790
|
};
|
|
2980
3791
|
};
|
|
2981
3792
|
|
|
3793
|
+
// Home/End within the current row → col 0 / max. Returns the fresh colIndex.
|
|
2982
3794
|
gotoColEdge = (toEnd: any) => {
|
|
2983
3795
|
// #10: End on a grouped PARENT header lands on that level's LAST header (headerCountAtLevel-1),
|
|
2984
3796
|
// not the leaf-column max — otherwise the ring strands on a phantom cell past the level's
|
|
@@ -2990,6 +3802,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2990
3802
|
return nextCol;
|
|
2991
3803
|
};
|
|
2992
3804
|
|
|
3805
|
+
// gotoRowEdge(toEnd): the §8 (260709-3qt) Ctrl+ArrowUp/Down vertical region-edge jump — move the
|
|
3806
|
+
// active cell to the data-region row edge (row 0 / last body row) in the CURRENT column, mirroring
|
|
3807
|
+
// gotoColEdge's horizontal edge jump. Body cells only (the caller gates on !activeIsHeader); always
|
|
3808
|
+
// lands in the body (activeIsHeader=false). Returns the fresh row index for the shared focus seam.
|
|
2993
3809
|
gotoRowEdge = (toEnd: any) => {
|
|
2994
3810
|
const lastRow = this.bodyRowCount() - 1;
|
|
2995
3811
|
const nextRow = toEnd ? lastRow < 0 ? 0 : lastRow : 0;
|
|
@@ -2998,6 +3814,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2998
3814
|
return nextRow;
|
|
2999
3815
|
};
|
|
3000
3816
|
|
|
3817
|
+
// Ctrl+Home → first body cell (0,0); Ctrl+End → last body cell (lastRow,max). Returns the
|
|
3818
|
+
// fresh { row, col } locals. Both land in the body (activeIsHeader=false).
|
|
3001
3819
|
gotoStart = () => {
|
|
3002
3820
|
this._activeIsHeader.value = false;
|
|
3003
3821
|
this._activeRow.value = 0;
|
|
@@ -3022,17 +3840,23 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3022
3840
|
};
|
|
3023
3841
|
};
|
|
3024
3842
|
|
|
3843
|
+
// Resolve the active cell element (for the in-cell trap) — uses the same data-* query as
|
|
3844
|
+
// the focus seam. rowKey is the literal '__header' or String(integer) — no consumer string.
|
|
3025
3845
|
currentCellEl = () => {
|
|
3026
3846
|
const rowKey = this._activeIsHeader.value ? '__header' : String(this._activeRow.value);
|
|
3027
3847
|
return this.resolveCellEl(rowKey, this._activeColIndex.value, this._activeIsHeader.value ? this._activeHeaderLevel.value : null);
|
|
3028
3848
|
};
|
|
3029
3849
|
|
|
3850
|
+
// The focusable descendants of a cell (non-disabled), in DOM order. Pure DOM — uniform ×6.
|
|
3030
3851
|
focusables = (cellEl: any) => {
|
|
3031
3852
|
if (!cellEl || !cellEl.querySelectorAll) return [];
|
|
3032
3853
|
const list = Array.prototype.slice.call(cellEl.querySelectorAll('button,[href],input,select,textarea,[tabindex]:not([tabindex="-1"])'));
|
|
3033
3854
|
return list.filter((n: any) => !n.disabled);
|
|
3034
3855
|
};
|
|
3035
3856
|
|
|
3857
|
+
// Enter/F2 → enter interaction mode: focus the active cell's FIRST interactive control
|
|
3858
|
+
// (D-07 — uniform for header sort buttons and body controls; Enter does NOT sort directly).
|
|
3859
|
+
// No-op (stay in navigation mode) if the cell has no focusable control.
|
|
3036
3860
|
enterControl = () => {
|
|
3037
3861
|
const cellEl = this.currentCellEl();
|
|
3038
3862
|
const list = this.focusables(cellEl);
|
|
@@ -3041,6 +3865,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3041
3865
|
list[0].focus();
|
|
3042
3866
|
};
|
|
3043
3867
|
|
|
3868
|
+
// Cycle focus among the controls WITHIN the active cell (D-08 focus containment) — Tab
|
|
3869
|
+
// forward / Shift+Tab backward, wrapping at the ends. Uses the plan-01-PROVEN per-target
|
|
3870
|
+
// activeElement read: gridRoot.getRootNode().activeElement is the UNIFORM correct read on
|
|
3871
|
+
// ALL SIX (document in light DOM; the shadow root on Lit). Reuse verbatim — do NOT re-derive.
|
|
3044
3872
|
cycleWithinCell = (cellEl: any, forward: any) => {
|
|
3045
3873
|
const list = this.focusables(cellEl);
|
|
3046
3874
|
if (!list.length) return;
|
|
@@ -3052,6 +3880,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3052
3880
|
list[i].focus();
|
|
3053
3881
|
};
|
|
3054
3882
|
|
|
3883
|
+
// THE single delegated keydown handler (RESEARCH "Single delegated keydown handler"). Wired
|
|
3884
|
+
// as ONE keydown listener on the <table> root — NOT per-cell, NOT with .stop/.prevent modifiers (the
|
|
3885
|
+
// Angular .stop-in-@for hoist bug, F5/ROZ723). e.preventDefault() is called IMPERATIVELY for
|
|
3886
|
+
// handled keys. Each nav helper writes $data and RETURNS the fresh post-write locals; those
|
|
3887
|
+
// SAME locals feed BOTH focusActiveCell AND the activecell-change emit (no $data re-read).
|
|
3055
3888
|
onGridKeyDown = (e: any) => {
|
|
3056
3889
|
if (!this.isGrid() || !e) return;
|
|
3057
3890
|
const key = e.key;
|
|
@@ -3389,6 +4222,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3389
4222
|
}
|
|
3390
4223
|
};
|
|
3391
4224
|
|
|
4225
|
+
// WR-03: integrate mouse-click + programmatic focus with the roving model. A click on a
|
|
4226
|
+
// tabindex="-1" cell (or focus arriving any way other than the keyboard nav path) moves
|
|
4227
|
+
// DOM focus there but does NOT run onGridKeyDown — so activeRow/activeColIndex would stay
|
|
4228
|
+
// on the OLD cell and the NEXT arrow key would jump from the stale active cell. Wired as
|
|
4229
|
+
// ONE @focusin on the <table> root (focusin bubbles): resolve the focused element's owning
|
|
4230
|
+
// [data-grid-cell], parse its data-row/data-col-index, and write them into the active-cell
|
|
4231
|
+
// state (mirroring the keyboard path). Clears activeInControl ONLY when the cell ITSELF
|
|
4232
|
+
// (not an inner control) received focus — focusing a control via Enter keeps the in-control
|
|
4233
|
+
// flag. NEVER emits activecell-change (a focus sync is not a keyboard navigation event).
|
|
3392
4234
|
syncActiveFromEvent = (e: any) => {
|
|
3393
4235
|
if (!this.isGrid() || !e) return;
|
|
3394
4236
|
const tgt = e.target;
|
|
@@ -3452,6 +4294,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3452
4294
|
if (tgt === cellEl) this._activeInControl.value = false;
|
|
3453
4295
|
};
|
|
3454
4296
|
|
|
4297
|
+
// onGridMouseDown: the pointer range seam (phase 51 req-7 / D-07 Shift+Click; §6 260709-3qt
|
|
4298
|
+
// plain drag-to-select). A focusin event carries no reliable `shiftKey`, so the modifier MUST
|
|
4299
|
+
// be read off the pointer event — @mousedown fires BEFORE the cell's focusin and DOES carry
|
|
4300
|
+
// shiftKey. A shift-held mousedown on a BODY cell sets the range's moving corner to that cell
|
|
4301
|
+
// (keeping the anchor), then flags rangeClickPending so the follow-up focusin does not collapse
|
|
4302
|
+
// the range. A PLAIN (non-shift) mousedown BEGINS a drag-select anchored at that cell (§6): the
|
|
4303
|
+
// document pointermove/up listeners paint the range as the pointer moves. The fill handle owns
|
|
4304
|
+
// its own @pointerdown drag (it stops propagation), so a plain mousedown originating inside it is
|
|
4305
|
+
// skipped. Do NOT preventDefault — native focus must still land (focusin sync + roving tabindex).
|
|
3455
4306
|
onGridMouseDown = (e: any) => {
|
|
3456
4307
|
if (!this.isGrid() || !e) return;
|
|
3457
4308
|
const tgt = e.target;
|
|
@@ -3484,6 +4335,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3484
4335
|
this.beginRangeDrag(row, col);
|
|
3485
4336
|
};
|
|
3486
4337
|
|
|
4338
|
+
// onGridDblClick: the double-click-into-edit seam (grid pointer §3+§5, 260708-ni6). Wired as
|
|
4339
|
+
// ONE @dblclick on the <table> root (mirroring the already-delegated @mousedown/@focusin). A
|
|
4340
|
+
// double-click on a BODY cell either toggles a group (group-header cell) or opens the editor
|
|
4341
|
+
// (editable cell); a non-editable body cell is a no-op (the cell stays active — its focusin
|
|
4342
|
+
// already set the active state + the §1 ring). Header cells return early so they keep their
|
|
4343
|
+
// native sort/menu/resize semantics. Reuses the SAME closest/parse/finite guards as
|
|
4344
|
+
// syncActiveFromEvent and the SAME beginEdit / onToggleExpand funnels the keyboard path uses —
|
|
4345
|
+
// no new edit or expand machinery. isGrid()-gated so 'table' mode never runs it.
|
|
3487
4346
|
onGridDblClick = (e: any) => {
|
|
3488
4347
|
if (!this.isGrid() || !e) return;
|
|
3489
4348
|
const tgt = e.target;
|
|
@@ -3522,6 +4381,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3522
4381
|
}
|
|
3523
4382
|
};
|
|
3524
4383
|
|
|
4384
|
+
// onGridClick: the opt-in single-click-to-edit seam (grid pointer §4, 260708-ni6). Only active
|
|
4385
|
+
// when the `singleClickEdit` prop is true (default false, negative-opt-out). Wired as ONE @click
|
|
4386
|
+
// on the <table> root — @click fires on a genuine mouseup-no-drag click (NOT @mousedown), which
|
|
4387
|
+
// honors the deferred §6 drag guard (a mousedown that begins a drag-select must not open an
|
|
4388
|
+
// editor). A plain click on an EDITABLE body cell opens its editor via the SAME beginEdit funnel;
|
|
4389
|
+
// shift+click (range extend) and non-editable cells are unaffected. Same closest/parse/header-skip
|
|
4390
|
+
// /finite guards as onGridDblClick. isGrid()-gated so 'table' mode never runs it.
|
|
3525
4391
|
onGridClick = (e: any) => {
|
|
3526
4392
|
if (!this.isGrid() || !e) return;
|
|
3527
4393
|
if (!this.singleClickEdit) return;
|
|
@@ -3549,6 +4415,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3549
4415
|
if (colId != null && this.columnEditable(colId)) this.beginEdit(row, col, null);
|
|
3550
4416
|
};
|
|
3551
4417
|
|
|
4418
|
+
// WR-02: reset the interaction-mode flag when focus leaves the active cell's subtree.
|
|
4419
|
+
// Without this, activeInControl could stick `true` — a mouse click OUTSIDE the cell, or
|
|
4420
|
+
// the focused inner control being removed from the DOM — leaving onGridKeyDown wedged in
|
|
4421
|
+
// the in-cell-trap branch so arrow nav is dead until Escape. Wired as ONE @focusout on
|
|
4422
|
+
// the <table> root (focusout bubbles, unlike blur). relatedTarget is the element RECEIVING
|
|
4423
|
+
// focus (null when focus leaves the document / is retargeted across a shadow boundary). If
|
|
4424
|
+
// focus is NOT moving to a descendant of the active cell, drop the flag. A Tab-cycle WITHIN
|
|
4425
|
+
// the cell (interaction mode) keeps relatedTarget inside cellEl → no reset.
|
|
3552
4426
|
onGridFocusOut = (e: any) => {
|
|
3553
4427
|
if (!this.isGrid() || !this._activeInControl.value) return;
|
|
3554
4428
|
const next = e ? e.relatedTarget : null;
|
|
@@ -3556,6 +4430,20 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3556
4430
|
if (!cellEl || !next || !cellEl.contains(next)) this._activeInControl.value = false;
|
|
3557
4431
|
};
|
|
3558
4432
|
|
|
4433
|
+
// B25: re-focus a resolved valid cell AFTER a programmatic shrink re-renders. The clamp
|
|
4434
|
+
// runs synchronously BEFORE the framework commits the new tbody, so a deferred rAF-poll
|
|
4435
|
+
// resolves the [data-row][data-col-index] cell off gridRoot once it has rendered (the fast
|
|
4436
|
+
// targets land on attempt 1; React/Solid retry across the async commit). Mirrors
|
|
4437
|
+
// focusCellWhenReady (B23) — DOM-only (reads gridRoot), so it is React-stale-safe.
|
|
4438
|
+
// guardMoved (default false): when true, the poll does NOT stomp focus that a later nav has
|
|
4439
|
+
// already moved to a DIFFERENT, STILL-VALID row — used only by the group-collapse re-seat (the
|
|
4440
|
+
// target group-header row is unchanged, so a stale late rAF must not steal focus back after the
|
|
4441
|
+
// user ArrowDown'd away → the non-deterministic treegrid collapsed-nav focus-theft). It is left
|
|
4442
|
+
// OFF for the B25 shrink-recovery site, whose target is a CLAMPED index of a now-REMOVED cell:
|
|
4443
|
+
// there focus legitimately sits on the doomed old cell (a different row) mid-async-render on
|
|
4444
|
+
// React and MUST be recovered onto the clamped survivor, not preserved. Compare data-row (NOT
|
|
4445
|
+
// node identity) so a stale SAME-row cell on Solid's node-replacing re-render still resolves as
|
|
4446
|
+
// the target — a genuinely dropped focus is always recovered on both sites.
|
|
3559
4447
|
recoverGridFocus = (rowKey: any, col: any, level: any, guardMoved = false) => {
|
|
3560
4448
|
if (!this.gridRoot) return;
|
|
3561
4449
|
let attempts = 0;
|
|
@@ -3580,6 +4468,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3580
4468
|
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(tryFocus);else setTimeout(tryFocus, 0);
|
|
3581
4469
|
};
|
|
3582
4470
|
|
|
4471
|
+
// D-05: clamp the active cell to bounds on every underlying-data change (re-sort, filter,
|
|
4472
|
+
// pagination, page-size). KEEP the same indices; clamp ONLY when the grid shrank — NO
|
|
4473
|
+
// row-id following, NO bounce-to-top on a filter keystroke. Gated by isGrid() so 'table'
|
|
4474
|
+
// mode is entirely untouched. Invoked at the rowModelVer bump path (refreshRowModel).
|
|
3583
4475
|
clampActiveCell = (rowCount: any, colCount: any) => {
|
|
3584
4476
|
if (!this.isGrid()) return;
|
|
3585
4477
|
// B8/B23 React-stale guard: the bounds come from the FRESH model the caller (refreshRowModel)
|
|
@@ -3675,12 +4567,50 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3675
4567
|
}
|
|
3676
4568
|
};
|
|
3677
4569
|
|
|
4570
|
+
// B6 (phase 63 wave-11) — "the active cell is parked on the empty-grid header fallback" control
|
|
4571
|
+
// flag, written + read ONLY inside clampActiveCell (never bound in the template). It MUST be a
|
|
4572
|
+
// plain component-scope `let` (React hoists to useRef), NOT a $data reactive field: clampActiveCell
|
|
4573
|
+
// is reached through the mount-time refreshRowModel closure, so a `$data.gridEmptyFallback` READ
|
|
4574
|
+
// there binds the async-stale mount-time value on React (setState is async — the rangeActive /
|
|
4575
|
+
// pendingEditFollow / B23-nextRows stale-read class). With the body re-populated after a filter
|
|
4576
|
+
// CLEAR, that stale read skipped the recovery branch on React → the roving tab-stop stayed on the
|
|
4577
|
+
// header fallback (columnheader) instead of re-seating a body cell (the B6 recovery gap). A
|
|
4578
|
+
// synchronously-written plain `let` is read fresh on all six → the empty→non-empty recovery
|
|
4579
|
+
// re-seats activeRow 0 on React too. The other 5 targets are byte-behaviorally identical (they
|
|
4580
|
+
// already read reactive $data synchronously). A top-level reassigned `let` referenced from the
|
|
4581
|
+
// refreshRowModel/clampActiveCell chain → React hoists to useRef → persists per-instance.
|
|
3678
4582
|
gridEmptyFallback = false;
|
|
3679
4583
|
|
|
4584
|
+
// ══ Cell-range selection (phase 51 plan 04 / req-7 / D-07) ═══════════════════════════════
|
|
4585
|
+
// A rectangular cell range over the FULL visible model, addressed BY INDEX PAIRS
|
|
4586
|
+
// (rangeAnchor/rangeFocus = { rowIndex, colIndex }) — NEVER a stored DOM node, so the
|
|
4587
|
+
// highlight reattaches to the correct cells across virtualization recycling (the
|
|
4588
|
+
// activeRow/activeColIndex invariant). ONE-WAY (D-07): exposed via getSelectedRange +
|
|
4589
|
+
// range-change, NOT a model:true slice. Coexists with — and is visually distinct from —
|
|
4590
|
+
// the row-selection slice (the two never touch each other's state).
|
|
4591
|
+
// inRange(rIdx, cIdx): is the cell at the visible-model index pair inside the current
|
|
4592
|
+
// rectangle? Pure index math (the min/max box of anchor+focus). False when no range —
|
|
4593
|
+
// the byte-identical-off guard for the range markup (no anchor/focus → no :data-in-range).
|
|
4594
|
+
// rangeTransition: set true while extendRange/setRangeFocus moves DOM focus to the new
|
|
4595
|
+
// range-focus corner. That focus move fires @focusin → syncActiveFromEvent with NO shiftKey
|
|
4596
|
+
// (a programmatic focus carries no modifier), which would otherwise clearRange() and wipe the
|
|
4597
|
+
// range we just set. The flag suppresses that collapse for the in-flight focus settle (the
|
|
4598
|
+
// editTransition blur-guard precedent). A top-level let → React hoists to useRef.
|
|
3680
4599
|
rangeTransition = false;
|
|
3681
4600
|
|
|
4601
|
+
// rangeClickPending: set by onGridMouseDown on a Shift+Click (the range is set off the
|
|
4602
|
+
// pointer event's shiftKey BEFORE the cell's focusin fires); the follow-up focusin reads it
|
|
4603
|
+
// to SKIP the range-collapse (a focusin carries no reliable shiftKey). Reset on consumption.
|
|
3682
4604
|
rangeClickPending = false;
|
|
3683
4605
|
|
|
4606
|
+
// B19: a SYNCHRONOUS mirror of "a range currently exists" — extendRange/setRangeFocus set it
|
|
4607
|
+
// true, clearRange/clampRange-to-empty set it false. clearRange is invoked TWICE in one plain-
|
|
4608
|
+
// arrow keydown (the explicit collapse + the focusin that follows the programmatic focus move);
|
|
4609
|
+
// on React `$data.rangeAnchor = null` is an async setState, so the SECOND clearRange's
|
|
4610
|
+
// `$data.rangeAnchor == null` guard reads the STALE (pre-write) range and fires a duplicate
|
|
4611
|
+
// range-change. This module-let is written synchronously (no setState async), so the second
|
|
4612
|
+
// clearRange sees `rangeActive === false` and returns → exactly ONE range-change per real drop
|
|
4613
|
+
// across all six targets. A top-level let → React hoists to useRef.
|
|
3684
4614
|
rangeActive = false;
|
|
3685
4615
|
|
|
3686
4616
|
inRange = (rIdx: any, cIdx: any) => {
|
|
@@ -3694,6 +4624,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3694
4624
|
return rIdx >= r0 && rIdx <= r1 && cIdx >= c0 && cIdx <= c1;
|
|
3695
4625
|
};
|
|
3696
4626
|
|
|
4627
|
+
// getSelectedRange(): the current range as plain integers — { anchor, focus } each a
|
|
4628
|
+
// { rowIndex, colIndex } pair (or null when no range). T-49-02: positions only, no row
|
|
4629
|
+
// data, no DOM node. Used by the getSelectedRange $expose verb AND every range-change emit
|
|
4630
|
+
// (the single payload source) AND copyRange/fillRange (the rectangle they operate over).
|
|
3697
4631
|
getSelectedRange = () => {
|
|
3698
4632
|
// B8: clamp the corners to the CURRENT bounds ON READ so the verb (and the range-change emit
|
|
3699
4633
|
// payload) never reports a corner past a shrunken model — React-stale-safe (the eager
|
|
@@ -3720,6 +4654,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3720
4654
|
};
|
|
3721
4655
|
};
|
|
3722
4656
|
|
|
4657
|
+
// isFillHandleCell(rIdx, cIdx): is this cell the BOTTOM-RIGHT corner of the current range?
|
|
4658
|
+
// That corner hosts the fill-handle affordance (req-8 / D-04). False without a range — the
|
|
4659
|
+
// byte-identical-off guard for the handle markup (no range → no handle).
|
|
3723
4660
|
isFillHandleCell = (rIdx: any, cIdx: any) => {
|
|
3724
4661
|
const a = this._rangeAnchor.value;
|
|
3725
4662
|
const f = this._rangeFocus.value;
|
|
@@ -3729,6 +4666,12 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3729
4666
|
return rIdx === r1 && cIdx === c1;
|
|
3730
4667
|
};
|
|
3731
4668
|
|
|
4669
|
+
// emitRangeChange(anchor, focus): fire range-change with the FRESH range corners passed by
|
|
4670
|
+
// the caller — NOT a re-read of $data.rangeAnchor/rangeFocus. The range corners are <data>
|
|
4671
|
+
// (useState on React), so re-reading right after the same-tick setState returns the STALE
|
|
4672
|
+
// pre-write value (ROZ138). extendRange/setRangeFocus thread the just-computed locals through
|
|
4673
|
+
// here so the emitted payload matches the write. The single call site keeps the count
|
|
4674
|
+
// predictable (React multi-emit dedup, D-07). One-way notification.
|
|
3732
4675
|
emitRangeChange = (anchor: any, focus: any) => {
|
|
3733
4676
|
this.dispatchEvent(new CustomEvent("range-change", {
|
|
3734
4677
|
detail: {
|
|
@@ -3740,6 +4683,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3740
4683
|
}));
|
|
3741
4684
|
};
|
|
3742
4685
|
|
|
4686
|
+
// extendRange(dRow, dCol): move rangeFocus by the (row,col) delta, clamped to the grid
|
|
4687
|
+
// bounds, seeding rangeAnchor from the active cell when no range exists yet (Shift+Arrow
|
|
4688
|
+
// from a bare active cell starts a 1×N / N×1 rectangle anchored at that cell). Body cells
|
|
4689
|
+
// only (header rows are not range-selectable). Emits range-change from this single site.
|
|
3743
4690
|
extendRange = (dRow: any, dCol: any) => {
|
|
3744
4691
|
if (this._activeIsHeader.value) return;
|
|
3745
4692
|
const maxRow = this.bodyRowCount() - 1;
|
|
@@ -3785,6 +4732,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3785
4732
|
}
|
|
3786
4733
|
};
|
|
3787
4734
|
|
|
4735
|
+
// setRangeFocus(rIdx, cIdx): set the moving corner to an explicit cell (Shift+Click),
|
|
4736
|
+
// seeding the anchor from the active cell when no range exists yet. Clamped to bounds.
|
|
4737
|
+
// Emits range-change from this single site.
|
|
3788
4738
|
setRangeFocus = (rIdx: any, cIdx: any) => {
|
|
3789
4739
|
const maxRow = this.bodyRowCount() - 1;
|
|
3790
4740
|
const maxCol = this.visibleColCount() - 1;
|
|
@@ -3806,6 +4756,12 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3806
4756
|
this.emitRangeChange(anchor, nextFocus);
|
|
3807
4757
|
};
|
|
3808
4758
|
|
|
4759
|
+
// selectAllBody(): the §8 (260709-3qt) Ctrl+A whole-body select — set the range to span EVERY
|
|
4760
|
+
// body cell (anchor at the first body cell (0,0), moving corner at the last (maxRow, maxCol)),
|
|
4761
|
+
// driving the SAME range corners shift+arrow / setRangeFocus use. Emits range-change from a single
|
|
4762
|
+
// site (the emitRangeChange contract — pass the FRESH corners, never a $data re-read). No-op on an
|
|
4763
|
+
// empty grid. Body cells only — a header-active Ctrl+A is gated OUT by the caller (never builds a
|
|
4764
|
+
// range from a header). rangeActive is set synchronously so a follow-up clearRange collapses it.
|
|
3809
4765
|
selectAllBody = () => {
|
|
3810
4766
|
const maxRow = this.bodyRowCount() - 1;
|
|
3811
4767
|
const maxCol = this.visibleColCount() - 1;
|
|
@@ -3824,6 +4780,12 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3824
4780
|
this.emitRangeChange(anchor, focus);
|
|
3825
4781
|
};
|
|
3826
4782
|
|
|
4783
|
+
// clearRange(): drop the rectangle (a non-shift navigation / edit-entry collapses any
|
|
4784
|
+
// range back to a single active cell). Cheap no-op when no range is set (the guard keeps a
|
|
4785
|
+
// plain navigation with no active range from emitting). B19: when a range DID exist, emit
|
|
4786
|
+
// range-change with null corners so a consumer mirroring the selection through the event sees
|
|
4787
|
+
// the drop — without this they hold a STALE rectangle after every non-shift navigation /
|
|
4788
|
+
// edit-entry collapse (getSelectedRange already reports null, but the event never fired).
|
|
3827
4789
|
clearRange = () => {
|
|
3828
4790
|
// B19: gate on the SYNCHRONOUS rangeActive mirror, NOT a $data re-read. clearRange runs twice
|
|
3829
4791
|
// in one plain-arrow keydown (explicit collapse + the focusin after the programmatic focus
|
|
@@ -3837,6 +4799,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3837
4799
|
this.emitRangeChange(null, null);
|
|
3838
4800
|
};
|
|
3839
4801
|
|
|
4802
|
+
// B8: clamp the range corners to the current grid bounds after an underlying-data change
|
|
4803
|
+
// (sort/filter/paginate/page-size all re-derive the row model). A range whose rows now exceed
|
|
4804
|
+
// the shrunken model would otherwise leave STALE/phantom corners → a copy serializes empty
|
|
4805
|
+
// rows past the model's end (and getSelectedRange reports out-of-bounds corners). We CLAMP each
|
|
4806
|
+
// corner into [0,maxRow]×[0,maxCol] (preserving a valid rectangle — a corner that clamps onto
|
|
4807
|
+
// another keeps the range non-empty); when no selectable body cell remains the rectangle is
|
|
4808
|
+
// dropped. Does NOT emit range-change here — the clamp is a reconcile, not a user selection
|
|
4809
|
+
// move (the emit-on-change work, B18/B19, lands in plan 63-05). Called from clampActiveCell.
|
|
3840
4810
|
clampRange = (maxRowArg: any, maxColArg: any) => {
|
|
3841
4811
|
const a = this._rangeAnchor.value;
|
|
3842
4812
|
const f = this._rangeFocus.value;
|
|
@@ -3869,17 +4839,42 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3869
4839
|
}
|
|
3870
4840
|
};
|
|
3871
4841
|
|
|
4842
|
+
// ══ Clipboard (TSV copy/paste) + drag-fill (phase 51 plan 04 / req-8 / D-03 / D-04) ══════
|
|
4843
|
+
// The async Clipboard API (grantPermissions confirmed in 51-01). Copy = range→TSV; paste =
|
|
4844
|
+
// TSV→cells under the D-03 skip rule (editable AND validator-passing cells only) with an
|
|
4845
|
+
// N-of-M aria-live announce + one cell-edit-commit per committed cell; drag-fill = value-copy
|
|
4846
|
+
// ONLY (D-04, NO series detection). T-51-01 (BLOCKING-high): pasted TSV is UNTRUSTED — every
|
|
4847
|
+
// cell is written as plain string DATA through the per-column validator and rendered via the
|
|
4848
|
+
// SAME {{ }}/rozieDisplay text path as #cell (never innerHTML / a template / a selector); the
|
|
4849
|
+
// cell-resolution query interpolates integer indices only (resolveCellEl, T-49-01).
|
|
4850
|
+
// announce(msg): write the polite aria-live PASTE-announce region (D-03 — "N of M cells
|
|
4851
|
+
// pasted"). SEPARATE from the validation invalidMsg region (different semantics). '' clears it.
|
|
3872
4852
|
announce = (msg: any) => {
|
|
3873
4853
|
this._pasteAnnounce.value = msg != null ? msg : '';
|
|
3874
4854
|
};
|
|
3875
4855
|
|
|
4856
|
+
// B11: copy / paste (and the Cut verb plan 63-09 adds) are NO-OPS while a HEADER cell is
|
|
4857
|
+
// active. A header has no body value to copy, and a paste anchored at a header would silently
|
|
4858
|
+
// write body row 0 at the header's column (a silent body mutation, borderline P0). This is the
|
|
4859
|
+
// SINGLE reusable guard every clipboard entry path checks — copyRange/pasteRange self-guard
|
|
4860
|
+
// with it AND the onGridKeyDown Ctrl+C/Ctrl+V branches gate on it (so the native shortcut is
|
|
4861
|
+
// left untouched on a header). Plan 63-09's Cut reuses this exact predicate.
|
|
3876
4862
|
clipboardActiveAllowed = () => !this._activeIsHeader.value;
|
|
3877
4863
|
|
|
4864
|
+
// fieldOfColId: the row-object key (accessorKey) to write for a column id — the same
|
|
4865
|
+
// accessorKey-or-id rule the edit funnels use. Used by paste/fill to apply values by field.
|
|
3878
4866
|
fieldOfColId = (colId: any) => {
|
|
3879
4867
|
const d = this.defFor(colId);
|
|
3880
4868
|
return d ? d.accessorKey != null ? d.accessorKey : colId : colId;
|
|
3881
4869
|
};
|
|
3882
4870
|
|
|
4871
|
+
// normalizedRange(): the current rectangle as { r0, r1, c0, c1 } (min/max of anchor+focus),
|
|
4872
|
+
// or null when no range. The shared rectangle source for copy/paste/fill. B8: the corners are
|
|
4873
|
+
// CLAMPED to the CURRENT grid bounds ON READ (read at call time → React-stale-safe), so a copy
|
|
4874
|
+
// after a filter-to-fewer can never serialize phantom rows past the shrunken model even when
|
|
4875
|
+
// the stored corners were not eagerly re-clamped (refreshRowModel's clamp is async-defeated on
|
|
4876
|
+
// React; this read-time clamp is the cross-target guarantee). Returns null when no body cell
|
|
4877
|
+
// remains.
|
|
3883
4878
|
normalizedRange = () => {
|
|
3884
4879
|
const a = this._rangeAnchor.value;
|
|
3885
4880
|
const f = this._rangeFocus.value;
|
|
@@ -3899,6 +4894,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3899
4894
|
};
|
|
3900
4895
|
};
|
|
3901
4896
|
|
|
4897
|
+
// B10: escape a TSV field per the spreadsheet convention — a field containing a tab, a CR/LF,
|
|
4898
|
+
// or a double-quote is wrapped in double-quotes with internal quotes DOUBLED; an ordinary
|
|
4899
|
+
// field is emitted verbatim. parseTsv() unescapes symmetrically, so a cell carrying a tab /
|
|
4900
|
+
// newline / quote round-trips without smearing into adjacent cells (T-63-03-02).
|
|
3902
4901
|
escapeTsvField = (s: any) => {
|
|
3903
4902
|
if (s.indexOf('\t') >= 0 || s.indexOf('\n') >= 0 || s.indexOf('\r') >= 0 || s.indexOf('"') >= 0) {
|
|
3904
4903
|
return '"' + s.replace(/"/g, '""') + '"';
|
|
@@ -3906,6 +4905,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3906
4905
|
return s;
|
|
3907
4906
|
};
|
|
3908
4907
|
|
|
4908
|
+
// rangeToTsv(): serialize the current range to TSV — rows joined by '\n', cells by '\t',
|
|
4909
|
+
// reading each cell's value off the visible model by index (cellValueAt). A single active
|
|
4910
|
+
// cell (no range) serializes that one cell. Each field is B10-escaped. Pure read — never writes.
|
|
3909
4911
|
rangeToTsv = () => {
|
|
3910
4912
|
const box = this.normalizedRange();
|
|
3911
4913
|
const r0 = box ? box.r0 : this._activeRow.value;
|
|
@@ -3924,6 +4926,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3924
4926
|
return lines.join('\n');
|
|
3925
4927
|
};
|
|
3926
4928
|
|
|
4929
|
+
// parseTsv(text): a TSV string → string[][] (rows of cells). Tolerates \r\n; a trailing
|
|
4930
|
+
// newline does not add a phantom empty row. Pure — produces plain string DATA only (T-51-01:
|
|
4931
|
+
// the cells are NEVER eval'd / interpolated into a selector / rendered as markup).
|
|
3927
4932
|
parseTsv = (text: any) => {
|
|
3928
4933
|
const str = text != null ? String(text) : '';
|
|
3929
4934
|
// CR-03: length guard BEFORE the parse — an empty string is a no-op, and a pathologically
|
|
@@ -3999,6 +5004,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3999
5004
|
return rows;
|
|
4000
5005
|
};
|
|
4001
5006
|
|
|
5007
|
+
// copyRange(): write the current range as TSV to the clipboard (async). No-op when the
|
|
5008
|
+
// async Clipboard API is unavailable (older/insecure contexts) — a copy is best-effort.
|
|
4002
5009
|
copyRange = () => {
|
|
4003
5010
|
// B11: never copy from a header-active state (the reusable clipboard guard).
|
|
4004
5011
|
if (!this.clipboardActiveAllowed()) return;
|
|
@@ -4009,6 +5016,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4009
5016
|
} catch (err: any) {/* best-effort copy */}
|
|
4010
5017
|
};
|
|
4011
5018
|
|
|
5019
|
+
// applyGridToRange(grid, originRow, originCol): the SHARED write path for paste + fill. Walks
|
|
5020
|
+
// the grid (string[][]) anchored at (originRow, originCol), CLAMPED to the grid bounds (no
|
|
5021
|
+
// unbounded loop — T-51-02). For each target cell: count it (total); SKIP if the column is
|
|
5022
|
+
// non-editable (D-03) or the per-column validator rejects the value (D-03, T-51-01 — the
|
|
5023
|
+
// value passes runValidator as plain string DATA before any write); else stage it into ONE
|
|
5024
|
+
// running fresh array (replaceRowValue) and record the committed cell. After the walk: ONE
|
|
5025
|
+
// writeData (the single r-model:data write), ONE cell-edit-commit per COMMITTED cell, and the
|
|
5026
|
+
// N-of-M aria-live announce. Returns { wrote, total }.
|
|
4012
5027
|
applyGridToRange = (grid: any, originRow: any, originCol: any) => {
|
|
4013
5028
|
const maxRow = this.bodyRowCount() - 1;
|
|
4014
5029
|
const maxCol = this.visibleColCount() - 1;
|
|
@@ -4073,6 +5088,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4073
5088
|
};
|
|
4074
5089
|
};
|
|
4075
5090
|
|
|
5091
|
+
// rowOriginalAt / rowIdAt: the underlying row object / id at a visible-model body index.
|
|
4076
5092
|
rowOriginalAt = (rowIndex: any) => {
|
|
4077
5093
|
const rowList = this._rows.value || [];
|
|
4078
5094
|
const row = rowList[rowIndex];
|
|
@@ -4085,6 +5101,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4085
5101
|
return row ? row.id : null;
|
|
4086
5102
|
};
|
|
4087
5103
|
|
|
5104
|
+
// C3: tile a parsed clipboard `grid` (string[][]) to fill a destination `box` — the spreadsheet
|
|
5105
|
+
// paste-into-range semantics. The target rectangle is the MAX of the box dims and the source
|
|
5106
|
+
// dims per axis, so a SMALLER clipboard TILES across a LARGER selection (a single 1×1 cell fills
|
|
5107
|
+
// the whole range; a 2×2 block repeats — tiled[dr][dc] = src[dr % srcRows][dc % srcCols]), while a
|
|
5108
|
+
// clipboard LARGER than the selection pastes its full block from the top-left (preserving the
|
|
5109
|
+
// no-range "clipboard-sized block at the active cell" behavior — a 1×1 destBox + a 1×N clipboard
|
|
5110
|
+
// yields the full 1×N block, byte-for-byte the prior path). Pure — returns a fresh grid; applies
|
|
5111
|
+
// nothing. A ragged/short source row defaults the missing cell to '' (coerced per column on write).
|
|
4088
5112
|
tileGridToBox = (grid: any, box: any) => {
|
|
4089
5113
|
const srcRows = grid.length;
|
|
4090
5114
|
// srcCols is the MAX row width across ALL rows (not grid[0].length): a RAGGED clipboard
|
|
@@ -4114,6 +5138,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4114
5138
|
return out;
|
|
4115
5139
|
};
|
|
4116
5140
|
|
|
5141
|
+
// pasteRange(): read TSV from the clipboard (async), parse it, TILE it over the destination
|
|
5142
|
+
// (C3), and apply it anchored at the destination top-left under the D-03 skip rule. The grid is
|
|
5143
|
+
// clamped to the grid bounds (T-51-02). A failed/empty read is a silent no-op.
|
|
4117
5144
|
pasteRange = () => {
|
|
4118
5145
|
// B11: never paste into a header-active state (the reusable clipboard guard) — a header
|
|
4119
5146
|
// anchor would silently write body row 0 at the header's column.
|
|
@@ -4151,6 +5178,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4151
5178
|
}).catch(() => {});
|
|
4152
5179
|
};
|
|
4153
5180
|
|
|
5181
|
+
// cutRange(): C3 Cut — copy the current range to the clipboard (rangeToTsv — the SAME escaped
|
|
5182
|
+
// serialization copyRange uses) THEN CLEAR the source cells through the SAME write-funnel as
|
|
5183
|
+
// paste/fill: applyGridToRange of an empty-string grid sized to the range → coerceCellValue('')
|
|
5184
|
+
// per column (null on a numeric column, '' on text) + the D-03 editable/validator skip rule +
|
|
5185
|
+
// ONE writeData + one cell-edit-commit per cleared cell + the N-of-M announce. A read-only /
|
|
5186
|
+
// required cell is left intact (the funnel skips it). B11: a no-op while a header cell is active
|
|
5187
|
+
// (reuses clipboardActiveAllowed — Cut can never silently clear a body cell from a header anchor).
|
|
5188
|
+
// The clear is SYNCHRONOUS and runs AFTER rangeToTsv has already serialized, so the copy reads the
|
|
5189
|
+
// pre-clear values; the clipboard write is best-effort/async and never blocks the clear.
|
|
4154
5190
|
cutRange = () => {
|
|
4155
5191
|
if (!this.clipboardActiveAllowed()) return;
|
|
4156
5192
|
// Snapshot the source rectangle synchronously (same ROZ138 concern as pasteRange).
|
|
@@ -4176,6 +5212,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4176
5212
|
this.applyGridToRange(grid, r0, c0);
|
|
4177
5213
|
};
|
|
4178
5214
|
|
|
5215
|
+
// clearActiveRange(): the §7 (260709-3qt) Delete/Backspace clear — cutRange() MINUS the clipboard
|
|
5216
|
+
// copy. Clears the active cell / selected range through the SAME write-funnel as Cut/paste/fill:
|
|
5217
|
+
// applyGridToRange of an empty-string grid sized to the range → coerceCellValue('') per column
|
|
5218
|
+
// (null on a numeric column, '' on text) + the D-03 editable/validator/read-only skip rule + ONE
|
|
5219
|
+
// writeData + one cell-edit-commit per cleared cell + the N-of-M announce. B11: a no-op while a
|
|
5220
|
+
// header cell is active (reuses clipboardActiveAllowed — Delete can never silently clear a body
|
|
5221
|
+
// cell from a header anchor). NO undo — the grid is controlled (writeData → $model.data; every
|
|
5222
|
+
// clear fires cell-edit-commit), so undo is the consumer's responsibility, the SAME contract
|
|
5223
|
+
// Cut/Paste/Fill already carry (design §7, approved 2026-07-09).
|
|
4179
5224
|
clearActiveRange = () => {
|
|
4180
5225
|
if (!this.clipboardActiveAllowed()) return;
|
|
4181
5226
|
// Snapshot the source rectangle synchronously (the ROZ138 concern cutRange/pasteRange share).
|
|
@@ -4193,6 +5238,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4193
5238
|
this.applyGridToRange(grid, r0, c0);
|
|
4194
5239
|
};
|
|
4195
5240
|
|
|
5241
|
+
// tileIndex(i, lo, hi): map an index into the inclusive [lo,hi] source span by TILING (repeat
|
|
5242
|
+
// the source block), handling indices below lo (negative offset) correctly. A 1-wide source
|
|
5243
|
+
// (lo===hi) always returns lo. Used by fillRange to resolve, per target cell, WHICH source
|
|
5244
|
+
// cell it copies — so each column copies its OWN source value down its OWN column.
|
|
4196
5245
|
tileIndex = (i: any, lo: any, hi: any) => {
|
|
4197
5246
|
const span = hi - lo + 1;
|
|
4198
5247
|
if (span <= 1) return lo;
|
|
@@ -4201,6 +5250,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4201
5250
|
return lo + k;
|
|
4202
5251
|
};
|
|
4203
5252
|
|
|
5253
|
+
// fillRange(sourceBox): drag-fill (D-04 — VALUE-COPY ONLY, no series detection). B7: the fill
|
|
5254
|
+
// SOURCE is the PRE-DRAG rectangle (`sourceBox`, captured at pointerdown before the drag grew
|
|
5255
|
+
// the range); each target cell copies the source cell in its OWN column (and row, when the
|
|
5256
|
+
// source spans rows), TILED across the source dimensions. This fixes two data-loss bugs: (1) a
|
|
5257
|
+
// single-scalar broadcast clobbered the other columns' data, and (2) reading box.r0/box.c0
|
|
5258
|
+
// flipped to the WRONG corner on an up/left drag (the box top-left is a TARGET cell there, not
|
|
5259
|
+
// the source). `sourceBox` falls back to the box's top-left 1×1 for a no-source fill. Honors the
|
|
5260
|
+
// SAME editable + validation + type-coercion skip rule as paste (via applyGridToRange): one
|
|
5261
|
+
// writeData + one cell-edit-commit per committed cell + the N-of-M announce. No-op without a range.
|
|
4204
5262
|
fillRange = (sourceBox: any, endCell: any) => {
|
|
4205
5263
|
// B7 (React-stale-safe): compute the EXTENDED rectangle from the gesture's FRESH endpoints —
|
|
4206
5264
|
// the pre-drag sourceBox (∪) the drag's final end cell — NOT a $data.rangeFocus re-read. On
|
|
@@ -4247,8 +5305,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4247
5305
|
this.applyGridToRange(grid, box.r0, box.c0);
|
|
4248
5306
|
};
|
|
4249
5307
|
|
|
5308
|
+
// onFillHandlePointerDown: begin a fill-handle drag (req-8 / D-04). The handle sits on the
|
|
5309
|
+
// range's bottom-right cell; a pointer drag extends the range (reusing setRangeFocus off the
|
|
5310
|
+
// cell under the pointer) and, on release, value-fills the dragged rectangle. Kept minimal:
|
|
5311
|
+
// pointermove extends the range to the cell under the pointer; pointerup commits the fill.
|
|
4250
5312
|
fillDragging = false;
|
|
4251
5313
|
|
|
5314
|
+
// CR-04: track the live fill-drag document listeners in module-lets so $onUnmount can remove
|
|
5315
|
+
// them if the component unmounts MID-DRAG (the `up` handler clears them on a normal release,
|
|
5316
|
+
// but a mid-drag unmount would otherwise leak a pointermove/pointerup listener on document).
|
|
4252
5317
|
fillDragMove: any = null;
|
|
4253
5318
|
|
|
4254
5319
|
fillDragUp: any = null;
|
|
@@ -4346,6 +5411,12 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4346
5411
|
}
|
|
4347
5412
|
};
|
|
4348
5413
|
|
|
5414
|
+
// §6 (260709-3qt) drag-to-select — mirror the fill-drag listener discipline. rangeDragging gates
|
|
5415
|
+
// the live gesture; rangeDragMove/rangeDragUp track the document pointermove/pointerup handlers so
|
|
5416
|
+
// a mid-drag unmount ($onUnmount → teardownRangeDrag) can remove them (CR-04). rangeDragMoved flips
|
|
5417
|
+
// true once the drag enters a DIFFERENT cell than its mousedown anchor; onGridClick reads it to
|
|
5418
|
+
// suppress a singleClickEdit editor-open after a drag (reset per-gesture in beginRangeDrag). Each
|
|
5419
|
+
// top-level let → React hoists to useRef.
|
|
4349
5420
|
rangeDragging = false;
|
|
4350
5421
|
|
|
4351
5422
|
rangeDragMove: any = null;
|
|
@@ -4354,6 +5425,17 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4354
5425
|
|
|
4355
5426
|
rangeDragMoved = false;
|
|
4356
5427
|
|
|
5428
|
+
// ══ Mouse drag-to-select (grid cell-interaction §6, 260709-3qt) ═════════════════════════
|
|
5429
|
+
// A plain (non-shift) mousedown on a body cell begins a document-level drag: the FIRST
|
|
5430
|
+
// pointermove that reaches a DIFFERENT body cell paints the range moving corner via the
|
|
5431
|
+
// SHARED-scope setRangeFocus (the SAME range model shift+click / shift+arrow drive), pointerup
|
|
5432
|
+
// ends it. Mirrors fillDrag.rzts's listener discipline VERBATIM (document pointermove/pointerup
|
|
5433
|
+
// tracked in module-lets so a mid-drag unmount can remove them — CR-04), and REUSES fillDrag's
|
|
5434
|
+
// shadow-piercing cellIndexFromPoint (shared scope) so the Lit shadow target is covered uniformly.
|
|
5435
|
+
// teardownRangeDrag(): remove the live drag listeners, null them, clear the dragging flag. The
|
|
5436
|
+
// `up` handler calls it on a normal release; $onUnmount calls it if we unmount MID-DRAG (mirrors
|
|
5437
|
+
// teardownFillDrag). rangeDragMoved is NOT reset here — it is read by onGridClick AFTER pointerup
|
|
5438
|
+
// (to suppress a singleClickEdit editor-open) and reset per-gesture in beginRangeDrag.
|
|
4357
5439
|
teardownRangeDrag = () => {
|
|
4358
5440
|
if (typeof document !== 'undefined') {
|
|
4359
5441
|
if (this.rangeDragMove) document.removeEventListener('pointermove', this.rangeDragMove);
|
|
@@ -4364,6 +5446,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4364
5446
|
this.rangeDragging = false;
|
|
4365
5447
|
};
|
|
4366
5448
|
|
|
5449
|
+
// beginRangeDrag(anchorR, anchorC): start a drag-select anchored at the mousedown cell. The
|
|
5450
|
+
// mousedown's native focus/focusin already committed the ACTIVE cell to (anchorR, anchorC), so
|
|
5451
|
+
// setRangeFocus (which seeds the anchor from the ACTIVE cell) spans mousedown-cell→pointer-cell —
|
|
5452
|
+
// we NEVER write $data.rangeAnchor directly (it is React-stale, ROZ138). rangeDragMoved starts
|
|
5453
|
+
// false and flips true only once the pointer reaches a DIFFERENT cell, so a mousedown-with-no-move
|
|
5454
|
+
// leaves a single active cell + no range (a normal click). lastCell dedups the many pointermove
|
|
5455
|
+
// events per cell (setRangeFocus emits range-change — only extend on a NEW cell, mirroring fillDrag's
|
|
5456
|
+
// B20 dedup). Captured per-gesture in the closure (no module-let needed for lastCell).
|
|
4367
5457
|
beginRangeDrag = (anchorR: any, anchorC: any) => {
|
|
4368
5458
|
// #leak: tear down any orphaned PRIOR range gesture BEFORE reassigning the module-let handlers.
|
|
4369
5459
|
// A missed pointerup (off-window release, context menu, alt-tab) leaves the prior drag's document
|
|
@@ -4400,6 +5490,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4400
5490
|
}
|
|
4401
5491
|
};
|
|
4402
5492
|
|
|
5493
|
+
// ══ Editable-cell lifecycle (phase 51 plan 02 — RESEARCH Pattern 1/3/4/5) ════════════════
|
|
5494
|
+
// Single-cell, non-virtual. Index-based state (editingRow/editingCol over the visible model),
|
|
5495
|
+
// the display↔editor branch in the keyed <td>, F2/Enter/printable entry off the reserved
|
|
5496
|
+
// onGridKeyDown seam, commit on Enter/Tab/blur, cancel+revert on Escape, sync validation with
|
|
5497
|
+
// D-01 keep-open. All gated by columnEditable() / the editing index pair so a table with no
|
|
5498
|
+
// editable columns lowers byte-identical (the editor branch r-if is always false).
|
|
5499
|
+
// The column id at the active cell (the active row's visible cell list @ activeColIndex).
|
|
5500
|
+
// Null when out of range (no body rows, or active cell is a header / select column).
|
|
4403
5501
|
activeCellColumnId = () => {
|
|
4404
5502
|
if (this._activeIsHeader.value) return null;
|
|
4405
5503
|
const rowList = this._rows.value || [];
|
|
@@ -4410,11 +5508,25 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4410
5508
|
return cell && cell.column ? cell.column.id : null;
|
|
4411
5509
|
};
|
|
4412
5510
|
|
|
5511
|
+
// isActiveCellEditable: the active cell sits in an editable column AND is a body cell
|
|
5512
|
+
// (req-1). Gates the F2/Enter/printable edit-entry branches in onGridKeyDown; a
|
|
5513
|
+
// non-editable active cell falls through to the reserved enterControl path.
|
|
4413
5514
|
isActiveCellEditable = () => {
|
|
4414
5515
|
const colId = this.activeCellColumnId();
|
|
4415
5516
|
return colId != null && this.columnEditable(colId);
|
|
4416
5517
|
};
|
|
4417
5518
|
|
|
5519
|
+
// isEditing: is the cell at (rowIndex, colIndex) over the visible model in edit? ONE
|
|
5520
|
+
// predicate covers BOTH modes (RESEARCH Pattern 6):
|
|
5521
|
+
// - row mode (req-6): editingRowIndex === rowIndex AND the column at colIndex is editable —
|
|
5522
|
+
// so EVERY editable cell in the row enters edit simultaneously (the editor template branch
|
|
5523
|
+
// re-uses this gate verbatim, no template fork);
|
|
5524
|
+
// - single-cell mode (req-1/3): the editingRow/editingCol pair matches exactly.
|
|
5525
|
+
// Pure index compare (editingRowIndex null + editingRow -1 = none) → the byte-identical-off
|
|
5526
|
+
// guard for the editor template branch. $data.editVer is read first so the per-cell branch
|
|
5527
|
+
// re-derives on Svelte/Solid when editing state mutates from a foreign slot-callback scope.
|
|
5528
|
+
// Called per-cell in both <td> bodies with the body-specific row index (rowIndexOf(row)
|
|
5529
|
+
// non-virtual, wr.vi.index virtual).
|
|
4418
5530
|
isEditing = (rowIndex: any, colIndex: any) => {
|
|
4419
5531
|
if (this._editVer.value < 0) return false;
|
|
4420
5532
|
if (this._editingRowIndex.value != null && this._editingRowIndex.value === rowIndex) {
|
|
@@ -4424,8 +5536,17 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4424
5536
|
return this._editingRow.value === rowIndex && this._editingCol.value === colIndex;
|
|
4425
5537
|
};
|
|
4426
5538
|
|
|
5539
|
+
// cellAriaInvalid (req-5/D-01): the STRING 'true' ONLY for the editing cell while it holds
|
|
5540
|
+
// an invalid value — drives :aria-invalid on the <td>. Returns null otherwise so the bound
|
|
5541
|
+
// attribute DROPS (the rozieAttr nullish-attr path), keeping non-editing cells byte-clean.
|
|
5542
|
+
// Returns the literal 'true' (NOT boolean true) so rozieAttr's string-literal-union preserve
|
|
5543
|
+
// keeps React's aria-invalid (Booleanish incl. 'true') happy instead of widening to string.
|
|
4427
5544
|
cellAriaInvalid = (rowIndex: any, colIndex: any): 'true' | null => this.isEditing(rowIndex, colIndex) && !!this._invalidMsg.value ? 'true' : null;
|
|
4428
5545
|
|
|
5546
|
+
// runValidator: the sync per-column validator (req-5). Reads col.meta.validate; not a
|
|
5547
|
+
// function → valid (true). Calls it (defensively wrapped — a thrown/non-true/non-string
|
|
5548
|
+
// return coerces to a generic message so a misbehaving validator can never wedge the
|
|
5549
|
+
// keymap, Security V5 DoS). A string return is the error message (commit rejected, D-01).
|
|
4429
5550
|
runValidator = (colId: any, value: any, row: any) => {
|
|
4430
5551
|
const m = this.editMetaOf(colId);
|
|
4431
5552
|
const v = m ? m.validate : null;
|
|
@@ -4441,10 +5562,17 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4441
5562
|
return 'Invalid value';
|
|
4442
5563
|
};
|
|
4443
5564
|
|
|
5565
|
+
// setInvalid: record the current validation error (drives the aria-live region +
|
|
5566
|
+
// :aria-invalid wired in Task 3). Empty string clears it.
|
|
4444
5567
|
setInvalid = (msg: any) => {
|
|
4445
5568
|
this._invalidMsg.value = msg != null ? msg : '';
|
|
4446
5569
|
};
|
|
4447
5570
|
|
|
5571
|
+
// replaceRowValue: build a FRESH array with ONE row object replaced (the column's field
|
|
5572
|
+
// set to the new value); the rest share by reference (the family immutable whole-array
|
|
5573
|
+
// replace — in-place mutation is silently dropped on React/Solid/Angular/Lit). rowIndex
|
|
5574
|
+
// is over currentData() (== the visible model order for the non-virtual, unsorted/
|
|
5575
|
+
// unfiltered single-cell case; the row id is carried for the commit payload).
|
|
4448
5576
|
replaceRowValue = (rows: any, rowIndex: any, field: any, value: any) => {
|
|
4449
5577
|
const src = rows || [];
|
|
4450
5578
|
const out = [];
|
|
@@ -4463,6 +5591,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4463
5591
|
return out;
|
|
4464
5592
|
};
|
|
4465
5593
|
|
|
5594
|
+
// Map a visible-model body-row index ($data.rows index) to its underlying currentData()
|
|
5595
|
+
// index via the row's original object identity (sorting/filtering/pagination may reorder
|
|
5596
|
+
// the visible model away from the source array order). Falls back to the same index.
|
|
4466
5597
|
sourceIndexOfRow = (visibleRowIndex: any) => {
|
|
4467
5598
|
const rowList = this._rows.value || [];
|
|
4468
5599
|
const row = rowList[visibleRowIndex];
|
|
@@ -4473,6 +5604,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4473
5604
|
return idx >= 0 ? idx : visibleRowIndex;
|
|
4474
5605
|
};
|
|
4475
5606
|
|
|
5607
|
+
// The column id / field (accessorKey) / current value / row object / row id for the cell
|
|
5608
|
+
// in EDIT — keyed off the authoritative editing pair ($data.editingRow/editingCol), NOT
|
|
5609
|
+
// the active-cell indices (which can drift from the editing cell on a Tab-advance, and are
|
|
5610
|
+
// async-stale right after a setState on React — ROZ138). Called only from commitEdit.
|
|
4476
5611
|
editingColumnId = () => {
|
|
4477
5612
|
const rowList = this._rows.value || [];
|
|
4478
5613
|
const row = rowList[this._editingRow.value];
|
|
@@ -4510,6 +5645,24 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4510
5645
|
return row ? row.id : null;
|
|
4511
5646
|
};
|
|
4512
5647
|
|
|
5648
|
+
// Focus the freshly-mounted editor (Pitfall 1, ROZ123): after beginEdit flips the editing
|
|
5649
|
+
// state, the editor <input> does not exist until the framework commits the r-if branch
|
|
5650
|
+
// (React setState async; Solid/Lit/Svelte next reactive tick). Poll for the
|
|
5651
|
+
// [data-editing-cell] element off gridRoot for ~30 frames — the five fast targets resolve
|
|
5652
|
+
// on attempt 1, React retries across its async commit. NEVER read $refs eagerly.
|
|
5653
|
+
// B2: selectAll gates the post-focus el.select(). Select-all is right when entering
|
|
5654
|
+
// edit IN PLACE (F2/Enter/click/row-edit/validation-reject — no seeded char, the user
|
|
5655
|
+
// retypes), but WRONG on a type-to-edit entry where a printable key already seeded the
|
|
5656
|
+
// draft (selecting the seeded char makes the next keystroke replace it: Zeta → eta).
|
|
5657
|
+
// beginEdit threads `seed == null` so a seeded entry skips the select and the caret sits
|
|
5658
|
+
// AFTER the seeded char; every other caller keeps the default select-all.
|
|
5659
|
+
// Editor-owns-focus contract (quick 260711-i5m): REVERTS the g52 shadow-piercing helper
|
|
5660
|
+
// (commit 5fa30045) that recursed into descendant shadow roots. Built-in editors are
|
|
5661
|
+
// host-DOM — the plain direct query resolves them on all 6 targets (no shadow to cross). A
|
|
5662
|
+
// #editor DROP-IN now owns its OWN focus via the reactive `autofocus` prop (EditorText's
|
|
5663
|
+
// $onMount + lazy $watch), so the host never needs to reach across a Lit drop-in's nested
|
|
5664
|
+
// shadow root at all — see the !hasEditorSlot gate below, which skips the host focus call
|
|
5665
|
+
// entirely for a drop-in target.
|
|
4513
5666
|
focusEditorWhenReady = (selectAll = true) => {
|
|
4514
5667
|
if (!this.gridRoot) return;
|
|
4515
5668
|
// Editor-owns-focus contract: when the CURRENT focus target is a #editor drop-in, the host
|
|
@@ -4549,6 +5702,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4549
5702
|
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(tryFocus);else setTimeout(tryFocus, 0);
|
|
4550
5703
|
};
|
|
4551
5704
|
|
|
5705
|
+
// Column id + current value at an EXPLICIT (rowIndex, colIndex) over the visible model —
|
|
5706
|
+
// used by beginEdit so it never re-reads $data.activeRow/activeColIndex (which are async-
|
|
5707
|
+
// stale right after a Tab-advance sets them on React — ROZ138).
|
|
4552
5708
|
columnIdAt = (rowIndex: any, colIndex: any) => {
|
|
4553
5709
|
const rowList = this._rows.value || [];
|
|
4554
5710
|
const row = rowList[rowIndex];
|
|
@@ -4567,6 +5723,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4567
5723
|
return cell ? cell.getValue() : null;
|
|
4568
5724
|
};
|
|
4569
5725
|
|
|
5726
|
+
// beginEdit: open the editor on the (rowIndex, colIndex) cell (req-1/3, D-05). seed===null
|
|
5727
|
+
// → seed the EXISTING value (F2/Enter in-place edit); a printable char → REPLACE (the
|
|
5728
|
+
// editor opens holding just that char). Resolves the column from the PASSED indices (not
|
|
5729
|
+
// $data) so a Tab-advance that just setState'd activeRow/Col works on React. Clears any
|
|
5730
|
+
// prior invalid state. Focus moves into the editor.
|
|
4570
5731
|
beginEdit = (rowIndex: any, colIndex: any, seed: any) => {
|
|
4571
5732
|
const colId = this.columnIdAt(rowIndex, colIndex);
|
|
4572
5733
|
if (colId == null || !this.columnEditable(colId)) return;
|
|
@@ -4592,6 +5753,12 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4592
5753
|
this.focusEditorWhenReady(seed == null);
|
|
4593
5754
|
};
|
|
4594
5755
|
|
|
5756
|
+
// Return focus to a body cell AFTER the editor unmounts (commit/cancel). The display↔
|
|
5757
|
+
// editor re-render must commit before the <td> is focusable with its roving tabindex —
|
|
5758
|
+
// on React/Solid/Lit that commit is async, so a synchronous focusActiveCell can run while
|
|
5759
|
+
// the cell is still the editor (or mid-swap) and focus is lost. Bounded rAF-poll resolves
|
|
5760
|
+
// the [data-row][data-col-index] cell off gridRoot for ~30 frames (the fast targets land
|
|
5761
|
+
// on attempt 1; React/Solid retry across the async commit). Mirrors focusEditorWhenReady.
|
|
4595
5762
|
focusCellWhenReady = (row: any, col: any) => {
|
|
4596
5763
|
if (!this.gridRoot) return;
|
|
4597
5764
|
let attempts = 0;
|
|
@@ -4608,6 +5775,12 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4608
5775
|
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(tryFocus);else setTimeout(tryFocus, 0);
|
|
4609
5776
|
};
|
|
4610
5777
|
|
|
5778
|
+
// B23: the index of a committed row WITHIN a given (fresh) visible-model array, resolved by
|
|
5779
|
+
// row IDENTITY. table-core's default getRowId is source-index-based, so a row's id is stable
|
|
5780
|
+
// across a re-sort (only its VISIBLE position moves); a committed edit replaces the row object
|
|
5781
|
+
// via a fresh spread (the `original` reference changes), so match by `id` FIRST, `original`
|
|
5782
|
+
// only as a fallback. Returns -1 when the row filtered out of the view. PURE (the caller passes
|
|
5783
|
+
// the FRESH row list — refreshRowModel's just-pulled `nextRows`, never the React-stale state).
|
|
4611
5784
|
indexOfRowIn = (rows: any, rowOriginal: any, rowId: any) => {
|
|
4612
5785
|
const list = rows || [];
|
|
4613
5786
|
for (let i = 0; i < list.length; i++) {
|
|
@@ -4619,6 +5792,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4619
5792
|
return -1;
|
|
4620
5793
|
};
|
|
4621
5794
|
|
|
5795
|
+
// endEdit: tear down the editor (shared by commit/cancel). Clears the editing pair +
|
|
5796
|
+
// draft + invalid state and returns to navigation mode. Does NOT move focus (callers
|
|
5797
|
+
// decide where focus lands — commit/cancel return it to the owning cell).
|
|
4622
5798
|
endEdit = () => {
|
|
4623
5799
|
this._editingRow.value = -1;
|
|
4624
5800
|
this._editingCol.value = -1;
|
|
@@ -4629,6 +5805,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4629
5805
|
this._editFocusColId.value = null;
|
|
4630
5806
|
};
|
|
4631
5807
|
|
|
5808
|
+
// endRowEdit: tear down full-row edit (shared by commitRow/cancelRow). Clears the row
|
|
5809
|
+
// index + the per-cell drafts + invalid state and returns to navigation mode. Does NOT
|
|
5810
|
+
// move focus (callers return it to the active cell). Mirrors endEdit for the row mode.
|
|
4632
5811
|
endRowEdit = () => {
|
|
4633
5812
|
this._editingRowIndex.value = null;
|
|
4634
5813
|
this._rowDraft.value = {};
|
|
@@ -4638,6 +5817,12 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4638
5817
|
this._editFocusColId.value = null;
|
|
4639
5818
|
};
|
|
4640
5819
|
|
|
5820
|
+
// editorAutofocusFor (quick 260711-i5m, editor-owns-focus contract): the reactive `autofocus`
|
|
5821
|
+
// #editor scope prop for a given (colId, rowIndex) — true for EXACTLY the current focus-
|
|
5822
|
+
// target cell, re-deriving on every editVer bump (mirrors isEditing's reactive gate so
|
|
5823
|
+
// Svelte/Solid re-run this per-cell on a foreign-slot-callback state mutation). Works for
|
|
5824
|
+
// BOTH single-cell ($data.editingRow) and row mode ($data.editingRowIndex) since
|
|
5825
|
+
// $data.editFocusColId is set by both beginEdit and beginRowEdit/commitRow/rowEditTab.
|
|
4641
5826
|
editorAutofocusFor = (colId: any, rowIndex: any) => {
|
|
4642
5827
|
if (this._editVer.value < 0) return false;
|
|
4643
5828
|
if (this._editingRowIndex.value != null) {
|
|
@@ -4648,6 +5833,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4648
5833
|
return this._editFocusColId.value != null && this._editFocusColId.value === colId;
|
|
4649
5834
|
};
|
|
4650
5835
|
|
|
5836
|
+
// B3: coerce the committed value by the column's built-in editor type at the single
|
|
5837
|
+
// commit funnel. A 'number' editor commits a real Number; an empty/whitespace/non-numeric
|
|
5838
|
+
// draft commits null (never '' / never NaN — Number('') === 0 is a silent footgun). Every
|
|
5839
|
+
// other editor type commits the value verbatim. Idempotent for the #editor drop-in path
|
|
5840
|
+
// (an already-numeric override passes through; an explicit null stays null).
|
|
4651
5841
|
coerceCellValue = (colId: any, raw: any) => {
|
|
4652
5842
|
if (this.editorTypeOf(colId) !== 'number') return raw;
|
|
4653
5843
|
if (raw == null) return null;
|
|
@@ -4658,6 +5848,16 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4658
5848
|
return Number.isNaN(n) ? null : n;
|
|
4659
5849
|
};
|
|
4660
5850
|
|
|
5851
|
+
// commitEdit: validate the draft (req-5); on success replace one row in a fresh array,
|
|
5852
|
+
// funnel it through writeData (the controlled r-model:data write, req-4), emit EXACTLY
|
|
5853
|
+
// ONE cell-edit-commit from THIS single call site (React multi-emit dedup, D-07), then
|
|
5854
|
+
// return focus to the cell. On a validation FAILURE keep the editor OPEN (D-01) — set
|
|
5855
|
+
// invalid, re-trap focus, never write the model. Captures the optional override value
|
|
5856
|
+
// (the #editor slot's commit(v) call) else the live draft.
|
|
5857
|
+
// Returns true when the commit succeeded (model written, editor closed); false when a
|
|
5858
|
+
// validation failure kept the editor OPEN (D-01). Callers MUST use this return value, not
|
|
5859
|
+
// a synchronous re-read of $data.editingRow — React's endEdit setState is async, so an
|
|
5860
|
+
// immediate re-read of editingRow still shows the OLD value (the ROZ138 stale-read class).
|
|
4661
5861
|
commitEdit = (overrideValue = undefined, skipFocusReturn = false) => {
|
|
4662
5862
|
if (this._editingRow.value < 0) return false;
|
|
4663
5863
|
// Sync idempotency latch (drop-in double cell-edit-commit fix): a second commitEdit call
|
|
@@ -4748,6 +5948,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4748
5948
|
return true;
|
|
4749
5949
|
};
|
|
4750
5950
|
|
|
5951
|
+
// toggleActiveBooleanCell (design doc 2026-07-05, Change 1): the spreadsheet-standard
|
|
5952
|
+
// single-keystroke boolean toggle. Flips the ACTIVE cell's value and commits it through the
|
|
5953
|
+
// EXACT SAME write funnel commitEdit uses (replaceRowValue → writeData → single $emit) but
|
|
5954
|
+
// WITHOUT opening an editor — there is no editingRow/editingCol involvement at all, so this
|
|
5955
|
+
// operates entirely off $data.activeRow/activeColIndex. Gated in onGridKeyDown to
|
|
5956
|
+
// editor:'checkbox' columns only (Space/Enter/F2), full-row edit mode is unaffected (the
|
|
5957
|
+
// editingRowIndex early return in onGridKeyDown already excludes it).
|
|
4751
5958
|
toggleActiveBooleanCell = () => {
|
|
4752
5959
|
const colId = this.columnIdAt(this._activeRow.value, this._activeColIndex.value);
|
|
4753
5960
|
if (colId == null || !this.columnEditable(colId)) return;
|
|
@@ -4795,6 +6002,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4795
6002
|
};
|
|
4796
6003
|
};
|
|
4797
6004
|
|
|
6005
|
+
// cancelEdit: discard the draft (D-05 — revert to the pre-edit value, no model write) and
|
|
6006
|
+
// return focus to the owning cell.
|
|
4798
6007
|
cancelEdit = () => {
|
|
4799
6008
|
if (this._editingRow.value < 0) return;
|
|
4800
6009
|
// CR-01: capture from the EDITING pair (authoritative), NOT the active-cell indices — a
|
|
@@ -4809,6 +6018,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4809
6018
|
this.focusCellWhenReady(focusRow, focusCol);
|
|
4810
6019
|
};
|
|
4811
6020
|
|
|
6021
|
+
// ══ Full-row edit lifecycle (phase 51 plan 03 / req-6 / D-06, RESEARCH Pattern 6) ════════
|
|
6022
|
+
// Shift+F2 (and the editRow $expose verb) put EVERY editable cell in the active row into
|
|
6023
|
+
// edit at once; one save commits the whole row in ONE writeData (a single fresh-array row
|
|
6024
|
+
// replace) + ONE row-edit-commit event; Escape reverts the whole row as a unit. Per-column
|
|
6025
|
+
// validation still runs on each edited cell at commit (D-01 keep-open if ANY fails). The
|
|
6026
|
+
// editor template branch (isEditing's row arm) is re-used verbatim — no per-mode fork.
|
|
6027
|
+
// The editable [columnId, field] pairs for a body row at the given visible-model index,
|
|
6028
|
+
// in visible-cell order. field is the column's accessorKey (the row-object key to write).
|
|
4812
6029
|
editableColumnsForRow = (rowIndex: any) => {
|
|
4813
6030
|
const rowList = this._rows.value || [];
|
|
4814
6031
|
const row = rowList[rowIndex];
|
|
@@ -4833,6 +6050,16 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4833
6050
|
return out;
|
|
4834
6051
|
};
|
|
4835
6052
|
|
|
6053
|
+
// B21/B22: focus the row-mode editor at a given VISIBLE col index. In full-row edit every
|
|
6054
|
+
// editable cell is already mounted as an editor, so this resolves the cell off gridRoot and
|
|
6055
|
+
// focuses its [data-editing-cell] control. Bounded rAF-poll (mirrors focusEditorWhenReady)
|
|
6056
|
+
// so a React re-render that recreates the input across the focus call still lands it. select-
|
|
6057
|
+
// all on text/number editors (a no-op try/catch on select/checkbox).
|
|
6058
|
+
// Editor-owns-focus contract (quick 260711-i5m): when the TARGET column is a #editor
|
|
6059
|
+
// drop-in, the host does NOT reach into its DOM (early return, before starting the rAF poll
|
|
6060
|
+
// at all) — the drop-in self-focuses via its own reactive `autofocus` prop, which the caller
|
|
6061
|
+
// (commitRow's B22 reject path / rowEditTab) already flips via $data.editFocusColId. Built-in
|
|
6062
|
+
// columns are unaffected (hasEditorSlot is false for them) — unchanged host direct-focus.
|
|
4836
6063
|
focusRowEditorAt = (rowIndex: any, colIndex: any) => {
|
|
4837
6064
|
if (!this.gridRoot) return;
|
|
4838
6065
|
const colId = this.columnIdAt(rowIndex, colIndex);
|
|
@@ -4857,6 +6084,12 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4857
6084
|
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(tryFocus);else setTimeout(tryFocus, 0);
|
|
4858
6085
|
};
|
|
4859
6086
|
|
|
6087
|
+
// beginRowEdit(row): enter full-row edit on a body row (req-6). Seeds rowDraft from each
|
|
6088
|
+
// editable column's CURRENT value (so an immediate save is a no-op), clears any single-cell
|
|
6089
|
+
// edit (mutual exclusivity), and focuses the first editable cell's editor (the bounded
|
|
6090
|
+
// rAF-poll resolves the first [data-editing-cell] off gridRoot — same mechanism as
|
|
6091
|
+
// focusEditorWhenReady). Accepts the row OBJECT (the template/Shift+F2 path) — index-resolved
|
|
6092
|
+
// internally via rowIndexOf so it stays in the editingRow/activeRow index space.
|
|
4860
6093
|
beginRowEdit = (row: any) => {
|
|
4861
6094
|
const rowIndex = this.rowIndexOf(row);
|
|
4862
6095
|
if (rowIndex < 0) return;
|
|
@@ -4890,6 +6123,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4890
6123
|
this.focusEditorWhenReady();
|
|
4891
6124
|
};
|
|
4892
6125
|
|
|
6126
|
+
// commitRow(): validate EVERY edited column (D-01 — keep the row open if ANY fails: set
|
|
6127
|
+
// invalid + announce, NEVER write the model); on all-valid build ONE fresh array replacing
|
|
6128
|
+
// the single row object with all rowDraft values applied at once, call writeData ONCE, then
|
|
6129
|
+
// emit ONE row-edit-commit from THIS single call site, clear the row state, return focus.
|
|
6130
|
+
// Returns true on a written commit, false when a validation failure kept the row open.
|
|
4893
6131
|
commitRow = () => {
|
|
4894
6132
|
if (this._editingRowIndex.value == null) return false;
|
|
4895
6133
|
const rowIndex = this._editingRowIndex.value;
|
|
@@ -5000,6 +6238,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5000
6238
|
return true;
|
|
5001
6239
|
};
|
|
5002
6240
|
|
|
6241
|
+
// cancelRow(): revert the whole row as a unit (D-06 — drop every draft, NO model write) and
|
|
6242
|
+
// return focus to the active cell.
|
|
5003
6243
|
cancelRow = () => {
|
|
5004
6244
|
if (this._editingRowIndex.value == null) return;
|
|
5005
6245
|
const focusRow = this._activeRow.value;
|
|
@@ -5010,6 +6250,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5010
6250
|
this.focusCellWhenReady(focusRow, focusCol);
|
|
5011
6251
|
};
|
|
5012
6252
|
|
|
6253
|
+
// replaceRowValues: like replaceRowValue but applies a MAP of field→value to ONE row object
|
|
6254
|
+
// in a single fresh-array replace (req-6 — the whole-row commit is ONE write, not per cell).
|
|
5013
6255
|
replaceRowValues = (rows: any, rowIndex: any, fieldValues: any) => {
|
|
5014
6256
|
const src = rows || [];
|
|
5015
6257
|
const fv = fieldValues || {};
|
|
@@ -5029,6 +6271,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5029
6271
|
return out;
|
|
5030
6272
|
};
|
|
5031
6273
|
|
|
6274
|
+
// Compute the next editable cell for Tab-advance (req-3, RESEARCH Open-Q3 deterministic
|
|
6275
|
+
// rule): skip non-editable columns within the row; wrap to the NEXT row's first editable
|
|
6276
|
+
// cell at the row's end; stop (return null) at grid end. Pure index math over the visible
|
|
6277
|
+
// model. Returns { row, col } or null.
|
|
5032
6278
|
nextEditableCell = (fromRow: any, fromCol: any) => {
|
|
5033
6279
|
const rowList = this._rows.value || [];
|
|
5034
6280
|
const rowCount = rowList.length;
|
|
@@ -5053,6 +6299,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5053
6299
|
return null;
|
|
5054
6300
|
};
|
|
5055
6301
|
|
|
6302
|
+
// B4: the mirror of nextEditableCell — the PREVIOUS editable cell for a Shift+Tab
|
|
6303
|
+
// backward move. Skips non-editable columns leftward within the row; wraps to the END
|
|
6304
|
+
// of the prior row; stops (returns null) at grid start. Pure index math over the visible
|
|
6305
|
+
// model. Returns { row, col } or null.
|
|
5056
6306
|
prevEditableCell = (fromRow: any, fromCol: any) => {
|
|
5057
6307
|
const rowList = this._rows.value || [];
|
|
5058
6308
|
const rowCount = rowList.length;
|
|
@@ -5081,18 +6331,50 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5081
6331
|
return null;
|
|
5082
6332
|
};
|
|
5083
6333
|
|
|
6334
|
+
// Transient guard: true while an editor commit/cancel/Tab-advance is tearing the current
|
|
6335
|
+
// editor down. The unmounting editor fires a `blur` as it leaves the DOM — without this
|
|
6336
|
+
// guard onEditorBlur would re-enter commitEdit on the (already-resolved or newly-opened)
|
|
6337
|
+
// cell, double-counting cell-edit-commit. A top-level `let` (React hoists to useRef).
|
|
5084
6338
|
editTransition = false;
|
|
5085
6339
|
|
|
6340
|
+
// B23: a pending "follow the committed row's focus" request, set by commitEdit (a single-cell
|
|
6341
|
+
// commit that may relocate the row under an active sort/filter) and consumed ONCE by the next
|
|
6342
|
+
// refreshRowModel pass — which runs with the FRESH re-derived row model, so it can resolve the
|
|
6343
|
+
// committed row's NEW display index (React-stale-safe) and re-seat focus there. Shape:
|
|
6344
|
+
// { rowOriginal, rowId, col } or null. A top-level `let` (React hoists to useRef → persists).
|
|
5086
6345
|
pendingEditFollow: any = null;
|
|
5087
6346
|
|
|
6347
|
+
// Sync idempotency latch for a cell commit (drop-in double cell-edit-commit fix, 260705):
|
|
6348
|
+
// commitEdit's `$data.editingRow < 0` re-entry guard is ASYNC-STALE on React — a deferred
|
|
6349
|
+
// drop-in editor's unmount-blur (onBlur → $props.commit → commitEdit) fires AFTER commitEdit
|
|
6350
|
+
// has already returned (editTransition is a SYNC latch, cleared before the async blur), while
|
|
6351
|
+
// `$data.editingRow` in that stale closure still reads the OLD (pre-endEdit) value, so the
|
|
6352
|
+
// second commit slips through and re-emits `cell-edit-commit`. A top-level `let` is written/read
|
|
6353
|
+
// synchronously by plain assignment (unaffected by React's setState batching — that's the point)
|
|
6354
|
+
// so it stays correct across the async window editTransition/editingRow cannot cover. Set true on
|
|
6355
|
+
// a SUCCESSFUL commitEdit/toggleActiveBooleanCell; reset to false wherever a NEW edit session
|
|
6356
|
+
// begins (beginEdit/beginRowEdit/editCell) so the next legitimate commit fires exactly once.
|
|
6357
|
+
// A top-level `let` (React hoists to useRef → persists).
|
|
5088
6358
|
committedThisSession = false;
|
|
5089
6359
|
|
|
6360
|
+
// ── Per-cell editor draft source (req-6) ──────────────────────────────────────────────
|
|
6361
|
+
// In single-cell mode every editor binds the shared $data.draftValue. In full-row mode
|
|
6362
|
+
// (editingRowIndex != null) each editable cell owns its OWN draft keyed by columnId in
|
|
6363
|
+
// rowDraft — so the four editors open simultaneously never clobber one shared value. These
|
|
6364
|
+
// helpers let the ONE editor template branch serve BOTH modes (no per-mode template fork):
|
|
6365
|
+
// the template binds editorValueFor(colId)/editorCheckedFor(colId) and writes via
|
|
6366
|
+
// onCellEditorInput(colId, evt)/onCellEditorCheckbox(colId, evt).
|
|
5090
6367
|
inRowEdit = () => this._editingRowIndex.value != null;
|
|
5091
6368
|
|
|
5092
6369
|
editorValueFor = (colId: any) => this.inRowEdit() ? this._rowDraft.value ? this._rowDraft.value[colId] : null : this._draftValue.value;
|
|
5093
6370
|
|
|
5094
6371
|
editorCheckedFor = (colId: any) => !!(this.inRowEdit() ? this._rowDraft.value ? this._rowDraft.value[colId] : null : this._draftValue.value);
|
|
5095
6372
|
|
|
6373
|
+
// #editor custom-slot callbacks (req-2/6): the consumer's slot calls commit(value)/cancel().
|
|
6374
|
+
// In SINGLE-CELL mode commit(v) commits that cell (commitEdit override); in ROW mode commit(v)
|
|
6375
|
+
// only WRITES this column's draft (the row commits as a unit later — never per cell). cancel()
|
|
6376
|
+
// reverts the cell (single) or the whole row (row mode). Factory-bound per columnId so the
|
|
6377
|
+
// row-mode commit targets the right draft key.
|
|
5096
6378
|
editorCommitFor = (colId: any) => (value: any) => {
|
|
5097
6379
|
if (this.inRowEdit()) {
|
|
5098
6380
|
this.setRowDraft(colId, value);
|
|
@@ -5109,6 +6391,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5109
6391
|
this.cancelEdit();
|
|
5110
6392
|
};
|
|
5111
6393
|
|
|
6394
|
+
// Editor input handlers (the global-filter `evt.target.value` idiom — an untyped param
|
|
6395
|
+
// neutralizes to `any`, so reading .value/.checked typechecks ×6; an inline
|
|
6396
|
+
// `$data.x = $event.target.value` binding does NOT neutralize and breaks Lit/React JSX).
|
|
6397
|
+
// Column-aware: in row mode they write rowDraft[colId] (a FRESH object so Solid/Svelte/React
|
|
6398
|
+
// re-derive); single-cell they write the shared draftValue.
|
|
5112
6399
|
onCellEditorInput = (colId: any, evt: any) => {
|
|
5113
6400
|
const v = evt && evt.target ? evt.target.value : '';
|
|
5114
6401
|
if (this.inRowEdit()) {
|
|
@@ -5127,6 +6414,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5127
6414
|
this._draftValue.value = v;
|
|
5128
6415
|
};
|
|
5129
6416
|
|
|
6417
|
+
// setRowDraft: write ONE key into a FRESH rowDraft object (whole-object replace — an
|
|
6418
|
+
// in-place mutation is silently dropped on React/Solid; the family immutable rule).
|
|
5130
6419
|
setRowDraft = (colId: any, value: any) => {
|
|
5131
6420
|
const src = this._rowDraft.value || {};
|
|
5132
6421
|
const next = {};
|
|
@@ -5135,6 +6424,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5135
6424
|
this._rowDraft.value = next;
|
|
5136
6425
|
};
|
|
5137
6426
|
|
|
6427
|
+
// B21: contain a Tab WITHIN the editing row (editMode='row'). Resolve the editable cells'
|
|
6428
|
+
// visible col indices for the editing row, find the current editor's col (off the blurring
|
|
6429
|
+
// editor's owning [data-grid-cell]), then move to the next/prev editable col WITH WRAP so
|
|
6430
|
+
// focus never leaves the row. A no-op when no row is editing / the row has no editable cells.
|
|
5138
6431
|
rowEditTab = (target: any, backward: any) => {
|
|
5139
6432
|
const rowIndex = this._editingRowIndex.value;
|
|
5140
6433
|
if (rowIndex == null) return;
|
|
@@ -5156,6 +6449,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5156
6449
|
this.focusRowEditorAt(rowIndex, cols[nextPos]);
|
|
5157
6450
|
};
|
|
5158
6451
|
|
|
6452
|
+
// onEditorKeyDown: the editor-LOCAL keymap (req-3). Enter → commit + stay (focus returns
|
|
6453
|
+
// to the cell); Tab → commit + advance to the next editable cell; Escape → cancel +
|
|
6454
|
+
// revert. preventDefault on handled keys so the grid keymap / native Tab don't double-act.
|
|
5159
6455
|
onEditorKeyDown = (e: any) => {
|
|
5160
6456
|
if (!e) return;
|
|
5161
6457
|
const key = e.key;
|
|
@@ -5214,6 +6510,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5214
6510
|
}
|
|
5215
6511
|
};
|
|
5216
6512
|
|
|
6513
|
+
// onEditorBlur: commit on a genuine click/focus-away (D-01 — an invalid value keeps the
|
|
6514
|
+
// editor open via commitEdit's reject path). SKIP when:
|
|
6515
|
+
// - editTransition is set (a synchronous commit/cancel teardown is unmounting the editor), or
|
|
6516
|
+
// - the blur is part of a controlled keyboard transition: focus is moving to a grid cell
|
|
6517
|
+
// or another editor inside our gridRoot (Tab-advance, Enter/Escape focus-return). On the
|
|
6518
|
+
// async-render targets the unmount-blur can fire AFTER the synchronous flag cleared, so
|
|
6519
|
+
// the relatedTarget/containment check is the load-bearing guard, not the flag alone.
|
|
5217
6520
|
onEditorBlur = (e: any) => {
|
|
5218
6521
|
// Full-row mode (req-6): a blur that stays WITHIN the row editor — Tab/click between the
|
|
5219
6522
|
// row's OWN fields — is a normal focus move and must NOT commit (a per-cell blur-commit
|
|
@@ -5292,6 +6595,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5292
6595
|
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(reseatDestFocus);else setTimeout(reseatDestFocus, 0);
|
|
5293
6596
|
};
|
|
5294
6597
|
|
|
6598
|
+
// editCell(rowIndex, colIndex) — programmatic edit-entry ($expose, req-3). Coerces +
|
|
6599
|
+
// clamps indices, moves the active cell, and opens the editor (no-op on a non-editable
|
|
6600
|
+
// cell). Collision-clean (RESEARCH name-check): not a verb/event/prop/ROZ137 member.
|
|
5295
6601
|
editCell = (rowIndex: any, colIndex: any) => {
|
|
5296
6602
|
const lastRow = this.bodyRowCount() - 1;
|
|
5297
6603
|
const maxRow = lastRow < 0 ? 0 : lastRow;
|
|
@@ -5306,6 +6612,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5306
6612
|
this.beginEdit(r, c, null);
|
|
5307
6613
|
};
|
|
5308
6614
|
|
|
6615
|
+
// commitEditing() — programmatic commit of the open editor ($expose, req-3). No-op when
|
|
6616
|
+
// nothing is editing. Collision-clean (not `commit`). Handles BOTH edit modes: a full-row
|
|
6617
|
+
// edit (editRow()/Shift+F2) drives editingRowIndex and leaves editingRow at -1, so the
|
|
6618
|
+
// single-cell commitEdit guard (editingRow >= 0) is false during a row edit — route to
|
|
6619
|
+
// commitRow() first so a programmatic commit of a row editor is not a silent no-op.
|
|
5309
6620
|
commitEditing = () => {
|
|
5310
6621
|
if (this.inRowEdit()) {
|
|
5311
6622
|
this.commitRow();
|
|
@@ -5314,6 +6625,12 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5314
6625
|
if (this._editingRow.value >= 0) this.commitEdit(undefined);
|
|
5315
6626
|
};
|
|
5316
6627
|
|
|
6628
|
+
// editRow(rowIndex) — programmatically enter full-row edit on a body row ($expose, req-6 /
|
|
6629
|
+
// D-06), the API twin of the Shift+F2 shortcut. Addressed BY INDEX over the visible model
|
|
6630
|
+
// (coerced + clamped); no-op on a row with no editable columns. Collision-clean (RESEARCH
|
|
6631
|
+
// name-check): `editRow` is not in the 15 existing verbs, not a prop, not a *-change/commit
|
|
6632
|
+
// event, not a Lit ROZ137-reserved host member. Moves the active cell to the row first so the
|
|
6633
|
+
// commit/cancel focus-return lands in the right row.
|
|
5317
6634
|
editRow = (rowIndex: any) => {
|
|
5318
6635
|
const lastRow = this.bodyRowCount() - 1;
|
|
5319
6636
|
const maxRow = lastRow < 0 ? 0 : lastRow;
|
|
@@ -5326,6 +6643,23 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5326
6643
|
this.beginRowEdit(row);
|
|
5327
6644
|
};
|
|
5328
6645
|
|
|
6646
|
+
// ── Grid active-cell $expose verbs (phase 49 plan 03, D-01) — exactly THREE, joining the
|
|
6647
|
+
// existing 12 (→ 15). Collision-safe names (Pitfall 1): focusCell NOT `focus` (would shadow
|
|
6648
|
+
// HTMLElement.focus on Lit — ROZ137); clearActiveCell NOT `clear` (listbox already exposes
|
|
6649
|
+
// `clear`); getActiveCell is a read-style getter. None collide with the 9 *-change events,
|
|
6650
|
+
// any prop, or a React auto-setter (ROZ121/137/524 clear). ──────────────────────────────────
|
|
6651
|
+
// focusAbsCellWhenReady — paginated page-switch focus poll (C1). After a programmatic page
|
|
6652
|
+
// switch the in-page (localRow, col) cell is ambiguous: EVERY page renders a row at the same
|
|
6653
|
+
// page-relative index, so a plain resolveCellEl(localRow, col) poll would grab the OLD page's
|
|
6654
|
+
// cell on frame 1 (before the switch commits) and focus it — only for the page switch to then
|
|
6655
|
+
// REMOVE it, dropping focus to <body>. Disambiguate by the ABSOLUTE aria-rowindex: poll until
|
|
6656
|
+
// the cell at (localRow, col) carries the TARGET page's body aria-rowindex (i.e. the TARGET
|
|
6657
|
+
// page has actually rendered), THEN focus. DOM-only (reads gridRoot), so React-stale-safe; works
|
|
6658
|
+
// for both controlled (round-trips through page-change) and uncontrolled pagination. ~60 frames
|
|
6659
|
+
// (~1s) to cover the controlled-state parent round-trip on React/Solid/Lit.
|
|
6660
|
+
// #13: the body aria-rowindex is now header-offset (bodyAriaRowIndex = headerRowCount + absRow
|
|
6661
|
+
// + 1) so header rows + body rows form one consistent aria-rowindex/aria-rowcount space — so
|
|
6662
|
+
// the poll target must add headerRowCount() too, else it never matches and focus drops.
|
|
5329
6663
|
focusAbsCellWhenReady = (absRow: any, localRow: any, col: any) => {
|
|
5330
6664
|
if (!this.gridRoot) return;
|
|
5331
6665
|
let attempts = 0;
|
|
@@ -5353,6 +6687,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5353
6687
|
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(tryFocus);else setTimeout(tryFocus, 0);
|
|
5354
6688
|
};
|
|
5355
6689
|
|
|
6690
|
+
// focusCell(rowIndex, colIndex) — move + focus the active cell. C1 (phase 63 wave-6): rowIndex
|
|
6691
|
+
// is the ABSOLUTE display-order position in getPrePaginationRowModel().rows (filter+sort+expand
|
|
6692
|
+
// applied, BEFORE pagination/windowing), in BOTH paginated and virtual modes — REVERSING the old
|
|
6693
|
+
// page-relative-when-paginated meaning. Args are COERCED to integers and CLAMPED before the
|
|
6694
|
+
// data-* selector is built (T-49-01/T-63-06-01: never interpolate a raw consumer string; clamp
|
|
6695
|
+
// the abs index into getPrePaginationRowModel bounds). The activecell-change payload + getActiveCell
|
|
6696
|
+
// speak the SAME absolute language (toAbsRow).
|
|
5356
6697
|
focusCell = (rowIndex: any, colIndex: any) => {
|
|
5357
6698
|
// B16: isGrid()-gate the verb. In 'table' mode there is no roving active cell, so focusCell
|
|
5358
6699
|
// is a NO-OP (never an activecell-change emit) — the keyboard path (onGridKeyDown) is already
|
|
@@ -5420,6 +6761,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5420
6761
|
}
|
|
5421
6762
|
};
|
|
5422
6763
|
|
|
6764
|
+
// getActiveCell() — return the current active-cell position. Integers only — no row data,
|
|
6765
|
+
// no DOM node (T-49-02 Information-Disclosure: return the screen position, nothing else).
|
|
6766
|
+
// B15: reflect the HEADER-active state. When a header cell is active the roving position is
|
|
6767
|
+
// NOT a body row — return the header sentinel (rowIndex null + isHeader true, colIndex the
|
|
6768
|
+
// header column) so a consumer never mistakes a header focus for body 'row 0'. A body cell
|
|
6769
|
+
// returns the integer rowIndex + isHeader false (back-compatible: the rowIndex/colIndex pair
|
|
6770
|
+
// is unchanged for the body case).
|
|
6771
|
+
// C1: a body cell returns the ABSOLUTE display-order rowIndex (toAbsRow) — matching focusCell's
|
|
6772
|
+
// addressing + the activecell-change payload — in BOTH paginated and virtual modes.
|
|
5423
6773
|
getActiveCell = () => this._activeIsHeader.value ? {
|
|
5424
6774
|
rowIndex: null,
|
|
5425
6775
|
colIndex: this._activeColIndex.value,
|
|
@@ -5430,6 +6780,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5430
6780
|
isHeader: false
|
|
5431
6781
|
};
|
|
5432
6782
|
|
|
6783
|
+
// clearActiveCell() — reset the roving position to the D-04 entry cell (row 0, col 0) and
|
|
6784
|
+
// exit interaction mode; the next Tab-in re-enters at the entry cell (D-01). Does NOT emit
|
|
6785
|
+
// (no move to a new addressable cell — a reset, not a navigation). B16: isGrid()-gated — a
|
|
6786
|
+
// table-mode instance has no roving active cell, so the verb is a no-op there.
|
|
5433
6787
|
clearActiveCell = () => {
|
|
5434
6788
|
if (!this.isGrid()) return;
|
|
5435
6789
|
this._activeIsHeader.value = false;
|
|
@@ -5438,6 +6792,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5438
6792
|
this._activeColIndex.value = 0;
|
|
5439
6793
|
};
|
|
5440
6794
|
|
|
6795
|
+
// ── Expand $expose verbs (phase 50 req-3, D-06) — joining the existing 19 (→ 23).
|
|
6796
|
+
// Collision-safe names (ROZ121/137/524): toggleRowExpanded / expandAll / collapseAll are
|
|
6797
|
+
// not inherited HTMLElement members, Lit lifecycle names, React auto-setters, prop names,
|
|
6798
|
+
// or *-change events; getExpandedRows is a read-style getter (twin of getSelectedRows).
|
|
6799
|
+
// Each drives @tanstack/table-core so the onExpandedChange → writeExpanded funnel fires
|
|
6800
|
+
// one expanded-change. ──────────────────────────────────────────────────────────────────
|
|
6801
|
+
// toggleRowExpanded(rowId) — toggle ONE row's expanded state, addressed by the consumer's
|
|
6802
|
+
// row id (the data `id` field) OR the table-core row id. Scans the core flat-row set (all
|
|
6803
|
+
// rows regardless of current expansion) so a collapsed parent is still resolvable.
|
|
5441
6804
|
toggleRowExpanded = (rowId: any) => {
|
|
5442
6805
|
if (!this.table) return;
|
|
5443
6806
|
const target = String(rowId);
|
|
@@ -5450,16 +6813,23 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5450
6813
|
}
|
|
5451
6814
|
};
|
|
5452
6815
|
|
|
6816
|
+
// expandAll() — open every expandable row (table-core sets ExpandedState to the `true`
|
|
6817
|
+
// literal under the hood → Pitfall 2: writeExpanded passes it through verbatim).
|
|
5453
6818
|
expandAll = () => {
|
|
5454
6819
|
if (!this.table) return;
|
|
5455
6820
|
this.table.toggleAllRowsExpanded(true);
|
|
5456
6821
|
};
|
|
5457
6822
|
|
|
6823
|
+
// collapseAll() — reset to a blank expanded state ({}). resetExpanded(true) forces the
|
|
6824
|
+
// blank reset (NOT the initialState) and fires onExpandedChange → one expanded-change.
|
|
5458
6825
|
collapseAll = () => {
|
|
5459
6826
|
if (!this.table) return;
|
|
5460
6827
|
this.table.resetExpanded(true);
|
|
5461
6828
|
};
|
|
5462
6829
|
|
|
6830
|
+
// getExpandedRows() — return the original row data for every currently-expanded row
|
|
6831
|
+
// (read-verb twin of expanded-change). Integers/data only — scans the core flat rows and
|
|
6832
|
+
// filters by getIsExpanded(). Empty when nothing is expanded.
|
|
5463
6833
|
getExpandedRows = () => {
|
|
5464
6834
|
if (!this.table) return [];
|
|
5465
6835
|
const out = [];
|
|
@@ -5468,6 +6838,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5468
6838
|
return out;
|
|
5469
6839
|
};
|
|
5470
6840
|
|
|
6841
|
+
// ── Grouping $expose verbs (phase 50 reqs 4-7, D-06 name-check) ────────────────────────────
|
|
6842
|
+
// applyGrouping (RENAMED from setGrouping — ROZ524: a bare `set<ModelProp>` verb shadows
|
|
6843
|
+
// React's auto-generated `setGrouping` useState setter for the `grouping` model slice, and an
|
|
6844
|
+
// $expose verb is PUBLIC-CONTRACT-PROTECTED from the deconfliction rename; same precedent as
|
|
6845
|
+
// setColumnOrder→applyColumnOrder) + clearGrouping. Both drive @tanstack/table-core's
|
|
6846
|
+
// table.setGrouping so the onGroupingChange → writeGrouping funnel fires one group-change with
|
|
6847
|
+
// the fresh ordered key list. Also handed to the headless #groupBar slot as apply/clear helpers.
|
|
5471
6848
|
applyGrouping = (cols: any) => {
|
|
5472
6849
|
if (this.table) this.table.setGrouping(cols);
|
|
5473
6850
|
};
|
|
@@ -5476,6 +6853,20 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5476
6853
|
if (this.table) this.table.setGrouping([]);
|
|
5477
6854
|
};
|
|
5478
6855
|
|
|
6856
|
+
// ── Faceted filtering read helpers (phase 50 reqs 8-9, D-03) ────────────────────────────────
|
|
6857
|
+
// Shared by BOTH the getFaceted* $expose verbs AND the #filter slot props. They resolve a
|
|
6858
|
+
// column via table.getColumn(colId) (a table-core lookup — NEVER a string-built querySelector,
|
|
6859
|
+
// T-50-06 / the T-49-01 index-only discipline) and read table-core's CROSS-FILTERED faceted
|
|
6860
|
+
// values (default impl — reflects rows passing all OTHER active column filters, D-03). They
|
|
6861
|
+
// touch the reactive tick (`tick() < 0` guard) so the #filter slot props re-derive when an
|
|
6862
|
+
// upstream filter changes on the fine-grained targets (Solid/Lit) — the visibleCellsFor idiom.
|
|
6863
|
+
//
|
|
6864
|
+
// getFacetedUniqueValues: the column's distinct values, KEYS ONLY — occurrence counts are
|
|
6865
|
+
// deliberately NOT exposed (D-03; the column's getFacetedUniqueValues() returns Map<any,number>,
|
|
6866
|
+
// we return Array.from(map.keys()) — no .entries()/count surface). Empty array on missing
|
|
6867
|
+
// column/table. NAMED to match the $expose verb exactly (the ExposedMethod.name shorthand
|
|
6868
|
+
// contract: an exposed verb lowers to `{ getFacetedUniqueValues }`, which must resolve to THIS
|
|
6869
|
+
// helper — the table-core factory was aliased to makeFacetedUniqueValues to free this name).
|
|
5479
6870
|
getFacetedUniqueValues = (colId: any) => {
|
|
5480
6871
|
if (this.tick() < 0 || !this.table) return [];
|
|
5481
6872
|
const col = this.table.getColumn(colId);
|
|
@@ -5484,6 +6875,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5484
6875
|
return map ? Array.from(map.keys()) : []; // KEYS only — counts deferred (D-03)
|
|
5485
6876
|
};
|
|
5486
6877
|
|
|
6878
|
+
// getFacetedMinMaxValues: the column's [min, max] numeric range, or null when unavailable.
|
|
6879
|
+
// Named to match the $expose verb (same shorthand contract as getFacetedUniqueValues above).
|
|
5487
6880
|
getFacetedMinMaxValues = (colId: any) => {
|
|
5488
6881
|
if (this.tick() < 0 || !this.table) return null;
|
|
5489
6882
|
const col = this.table.getColumn(colId);
|