@rozie-ui/data-table-lit 0.2.4 → 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/README.md +2 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +3 -2
- package/dist/index.d.mts +3 -2
- package/dist/index.mjs +1 -0
- package/package.json +2 -2
- package/src/Column.ts +13 -4
- package/src/DataTable.ts +1404 -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/themes/base.css +15 -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 });
|
|
@@ -661,6 +661,16 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
661
661
|
@state() private _hasSlotDetail = false;
|
|
662
662
|
@queryAssignedElements({ slot: 'detail', flatten: true }) private _slotDetailElements!: Element[];
|
|
663
663
|
@property({ attribute: false }) detail?: (scope: { row: any }) => unknown;
|
|
664
|
+
// Phase 79 Plan 08 (R4) contract for 79-09: the record intake for
|
|
665
|
+
// record-routed slot fills. 79-09's consumer-side emitSlotFiller
|
|
666
|
+
// accumulates an object literal onto the SAME `.rozieSlots=${{ ... }}`
|
|
667
|
+
// open-tag binding; the KEY is the fill's authored (possibly
|
|
668
|
+
// non-identifier) name and the VALUE is a scope-taking render
|
|
669
|
+
// function. `rozieSlots?.[name]` must be checked BEFORE the legacy
|
|
670
|
+
// named function-prop / <slot> fallback (AC-9). Attribute
|
|
671
|
+
// deserialization is disabled — this is a function-valued record,
|
|
672
|
+
// never reflected to/from an HTML attribute.
|
|
673
|
+
@property({ attribute: false }) rozieSlots?: Record<string, (scope: any) => unknown>;
|
|
664
674
|
|
|
665
675
|
private _disconnectCleanups: Array<() => void> = [];
|
|
666
676
|
// Re-parenting guard: set true once the deferred teardown has actually
|
|
@@ -1342,22 +1352,86 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1342
1352
|
|
|
1343
1353
|
table: any = null;
|
|
1344
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.
|
|
1345
1360
|
virtualizer: any = null;
|
|
1346
1361
|
|
|
1347
1362
|
virtualizerCleanup: any = null;
|
|
1348
1363
|
|
|
1349
1364
|
gridScrollEl: any = null;
|
|
1350
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.
|
|
1351
1375
|
remeasurePending = false;
|
|
1352
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.
|
|
1353
1381
|
GRID_PAGE_STEP = 10;
|
|
1354
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).
|
|
1355
1388
|
gridRoot: any = null;
|
|
1356
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.
|
|
1357
1392
|
programmatic = 0;
|
|
1358
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.
|
|
1359
1403
|
focusIntentEpoch = 0;
|
|
1360
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.
|
|
1361
1435
|
DATA_WRITE_TOKEN_KEY = '__rozieDataWriteToken';
|
|
1362
1436
|
|
|
1363
1437
|
undoStack: unknown[] = [];
|
|
@@ -1366,10 +1440,37 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1366
1440
|
|
|
1367
1441
|
restoringHistory: boolean = false;
|
|
1368
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).
|
|
1369
1450
|
expandedTouched = false;
|
|
1370
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.
|
|
1371
1455
|
groupingActiveDefault = () => ((this.grouping != null ? this.grouping : this._groupingDefault.value) || []).length > 0;
|
|
1372
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).
|
|
1373
1474
|
effectiveColumnPinning = (): any => {
|
|
1374
1475
|
const base = this.columnPinning != null ? this.columnPinning : this._columnPinningDefault.value;
|
|
1375
1476
|
const rail: string[] = [];
|
|
@@ -1384,6 +1485,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1384
1485
|
};
|
|
1385
1486
|
};
|
|
1386
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).
|
|
1387
1495
|
currentState = (): any => ({
|
|
1388
1496
|
sorting: this.sorting != null ? this.sorting : this._sortingDefault.value,
|
|
1389
1497
|
globalFilter: this.globalFilter != null ? this.globalFilter : this._globalFilterDefault.value,
|
|
@@ -1419,10 +1527,28 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1419
1527
|
columnSizingInfo: this._columnSizingInfo.value
|
|
1420
1528
|
});
|
|
1421
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.
|
|
1422
1534
|
currentData = (): any => this.data != null ? this.data : this._dataDefault.value;
|
|
1423
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.)
|
|
1424
1542
|
isSafeKey = (k: any) => k !== '__proto__' && k !== 'constructor' && k !== 'prototype';
|
|
1425
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).
|
|
1426
1552
|
wrapAggregationFn = (fn: any) => {
|
|
1427
1553
|
if (typeof fn === 'string') return fn;
|
|
1428
1554
|
if (typeof fn !== 'function') return undefined;
|
|
@@ -1435,6 +1561,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1435
1561
|
};
|
|
1436
1562
|
};
|
|
1437
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).
|
|
1438
1569
|
buildConfigDef = (c: any) => {
|
|
1439
1570
|
if (!c) return null;
|
|
1440
1571
|
// Grouped (multi-level) header column: an entry carrying a `columns` array. table-core's
|
|
@@ -1546,10 +1677,24 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1546
1677
|
return out;
|
|
1547
1678
|
};
|
|
1548
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 "__").
|
|
1549
1682
|
SELECT_COL_ID = '__rdt_select';
|
|
1550
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]).
|
|
1551
1687
|
EXPANDER_COL_ID = '__rdt_expander';
|
|
1552
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.
|
|
1553
1698
|
selectionEnabled = () => this.selectionMode === 'single' || this.selectionMode === 'multiple';
|
|
1554
1699
|
|
|
1555
1700
|
tableColumns = () => {
|
|
@@ -1597,6 +1742,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1597
1742
|
return withExpander;
|
|
1598
1743
|
};
|
|
1599
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.
|
|
1600
1752
|
writeSorting = (next: any) => {
|
|
1601
1753
|
if (this.programmatic) return;
|
|
1602
1754
|
this.programmatic++;
|
|
@@ -1612,6 +1764,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1612
1764
|
|
|
1613
1765
|
applyUpdater = (updater: any, current: any) => typeof updater === 'function' ? updater(current) : updater;
|
|
1614
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).
|
|
1615
1774
|
writeExpanded = (next: any) => {
|
|
1616
1775
|
if (this.programmatic) return;
|
|
1617
1776
|
this.programmatic++;
|
|
@@ -1635,6 +1794,17 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1635
1794
|
this.programmatic--;
|
|
1636
1795
|
};
|
|
1637
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.
|
|
1638
1808
|
writeGrouping = (next: any) => {
|
|
1639
1809
|
if (this.programmatic) return;
|
|
1640
1810
|
this.programmatic++;
|
|
@@ -1648,6 +1818,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1648
1818
|
this.programmatic--;
|
|
1649
1819
|
};
|
|
1650
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.
|
|
1651
1824
|
writeGlobalFilter = (next: any) => {
|
|
1652
1825
|
if (this.programmatic) return;
|
|
1653
1826
|
this.programmatic++;
|
|
@@ -1663,6 +1836,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1663
1836
|
this.programmatic--;
|
|
1664
1837
|
};
|
|
1665
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).
|
|
1666
1843
|
writeColumnFilters = (next: any) => {
|
|
1667
1844
|
if (this.programmatic) return;
|
|
1668
1845
|
this.programmatic++;
|
|
@@ -1678,6 +1855,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1678
1855
|
this.programmatic--;
|
|
1679
1856
|
};
|
|
1680
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`.
|
|
1681
1860
|
writePagination = (next: any) => {
|
|
1682
1861
|
if (this.programmatic) return;
|
|
1683
1862
|
this.programmatic++;
|
|
@@ -1691,6 +1870,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1691
1870
|
this.programmatic--;
|
|
1692
1871
|
};
|
|
1693
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.
|
|
1694
1876
|
writeRowSelection = (next: any) => {
|
|
1695
1877
|
if (this.programmatic) return;
|
|
1696
1878
|
this.programmatic++;
|
|
@@ -1704,6 +1886,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1704
1886
|
this.programmatic--;
|
|
1705
1887
|
};
|
|
1706
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.
|
|
1707
1892
|
writeColumnVisibility = (next: any) => {
|
|
1708
1893
|
if (this.programmatic) return;
|
|
1709
1894
|
this.programmatic++;
|
|
@@ -1717,6 +1902,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1717
1902
|
this.programmatic--;
|
|
1718
1903
|
};
|
|
1719
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.
|
|
1720
1908
|
writeColumnSizing = (next: any) => {
|
|
1721
1909
|
if (this.programmatic) return;
|
|
1722
1910
|
this.programmatic++;
|
|
@@ -1730,6 +1918,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1730
1918
|
this.programmatic--;
|
|
1731
1919
|
};
|
|
1732
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.
|
|
1733
1924
|
writeColumnOrder = (next: any) => {
|
|
1734
1925
|
if (this.programmatic) return;
|
|
1735
1926
|
this.programmatic++;
|
|
@@ -1743,6 +1934,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1743
1934
|
this.programmatic--;
|
|
1744
1935
|
};
|
|
1745
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.
|
|
1746
1941
|
writeColumnPinning = (next: any) => {
|
|
1747
1942
|
if (this.programmatic) return;
|
|
1748
1943
|
// effectiveColumnPinning() forces the auto-injected chrome ids (select/expander) into the
|
|
@@ -1766,6 +1961,38 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1766
1961
|
this.programmatic--;
|
|
1767
1962
|
};
|
|
1768
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.
|
|
1769
1996
|
writeData = (next: any) => {
|
|
1770
1997
|
if (this.programmatic) return;
|
|
1771
1998
|
if (this.undoable && !this.restoringHistory) {
|
|
@@ -1789,12 +2016,18 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1789
2016
|
this.programmatic--;
|
|
1790
2017
|
};
|
|
1791
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).
|
|
1792
2022
|
columnFilterValue = (colId: any) => {
|
|
1793
2023
|
const cf = this.currentState().columnFilters || [];
|
|
1794
2024
|
for (const f of cf as any) if (f && f.id === colId) return f.value != null ? f.value : '';
|
|
1795
2025
|
return '';
|
|
1796
2026
|
};
|
|
1797
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).
|
|
1798
2031
|
setColumnFilter = (colId: any, value: any) => {
|
|
1799
2032
|
const prev = this.currentState().columnFilters || [];
|
|
1800
2033
|
const next = [];
|
|
@@ -1806,6 +2039,25 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1806
2039
|
this.writeColumnFilters(next);
|
|
1807
2040
|
};
|
|
1808
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).
|
|
1809
2061
|
recordSnapshot = (current: any) => {
|
|
1810
2062
|
this.undoStack.push(current);
|
|
1811
2063
|
const limit = this.undoLimit != null ? this.undoLimit : 100;
|
|
@@ -1817,11 +2069,17 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1817
2069
|
|
|
1818
2070
|
canRedo = () => this.redoStack.length > 0;
|
|
1819
2071
|
|
|
2072
|
+
// Both stacks empty — the external-swap latch (DataTable.rozie reFeed) and the
|
|
2073
|
+
// clearHistory() $expose verb share this single implementation.
|
|
1820
2074
|
clearHistory = () => {
|
|
1821
2075
|
this.undoStack = [];
|
|
1822
2076
|
this.redoStack = [];
|
|
1823
2077
|
};
|
|
1824
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).
|
|
1825
2083
|
emitHistoryChange = () => {
|
|
1826
2084
|
this.dispatchEvent(new CustomEvent("history-change", {
|
|
1827
2085
|
detail: {
|
|
@@ -1833,12 +2091,22 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1833
2091
|
}));
|
|
1834
2092
|
};
|
|
1835
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.
|
|
1836
2098
|
emitHistoryChangeIfEdged = (prevU: any, prevR: any) => {
|
|
1837
2099
|
const nextU = this.canUndo();
|
|
1838
2100
|
const nextR = this.canRedo();
|
|
1839
2101
|
if (nextU !== prevU || nextR !== prevR) this.emitHistoryChange();
|
|
1840
2102
|
};
|
|
1841
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.
|
|
1842
2110
|
undo = () => {
|
|
1843
2111
|
if (!this.canUndo()) return;
|
|
1844
2112
|
const prev = this.undoStack.pop();
|
|
@@ -1849,6 +2117,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1849
2117
|
this.emitHistoryChange();
|
|
1850
2118
|
};
|
|
1851
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.
|
|
1852
2122
|
redo = () => {
|
|
1853
2123
|
if (!this.canRedo()) return;
|
|
1854
2124
|
const next = this.redoStack.pop();
|
|
@@ -1859,8 +2129,21 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1859
2129
|
this.emitHistoryChange();
|
|
1860
2130
|
};
|
|
1861
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`.
|
|
1862
2136
|
refreshRowModel: any = null;
|
|
1863
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.
|
|
1864
2147
|
onSortingChangeCb = (updater: any) => {
|
|
1865
2148
|
this.writeSorting(this.applyUpdater(updater, this.currentState().sorting));
|
|
1866
2149
|
};
|
|
@@ -1910,12 +2193,50 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1910
2193
|
this._columnSizingInfo.value = next != null ? next : this._columnSizingInfo.value;
|
|
1911
2194
|
};
|
|
1912
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.
|
|
1913
2216
|
windowSource = () => {
|
|
1914
2217
|
if (!this.table) return [];
|
|
1915
2218
|
if (this.virtual) return this.table.getPrePaginationRowModel().rows;
|
|
1916
2219
|
return this.table.getRowModel().rows;
|
|
1917
2220
|
};
|
|
1918
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.
|
|
1919
2240
|
scheduleRemeasure = () => {
|
|
1920
2241
|
if (this.remeasurePending) return;
|
|
1921
2242
|
this.remeasurePending = true;
|
|
@@ -1934,18 +2255,38 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1934
2255
|
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(rafPass);else if (ranMicro) this.remeasurePending = false;else setTimeout(rafPass, 0);
|
|
1935
2256
|
};
|
|
1936
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).
|
|
1937
2265
|
pinnedEditIndex = () => {
|
|
1938
2266
|
if (this._editingRow.value >= 0) return this._editingRow.value;
|
|
1939
2267
|
if (this._editingRowIndex.value != null) return this._editingRowIndex.value;
|
|
1940
2268
|
return -1;
|
|
1941
2269
|
};
|
|
1942
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.
|
|
1943
2275
|
pinnedMeasurement = (pin: any) => {
|
|
1944
2276
|
if (!this.virtualizer || pin < 0) return null;
|
|
1945
2277
|
const ms = this.virtualizer.getMeasurements();
|
|
1946
2278
|
return ms && ms[pin] ? ms[pin] : null;
|
|
1947
2279
|
};
|
|
1948
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.
|
|
1949
2290
|
remeasureWindow = () => {
|
|
1950
2291
|
if (!this.virtualizer || !this.gridRoot) return;
|
|
1951
2292
|
// Bail ONLY while a PROGRAMMATIC scroll is in flight: virtualizer.scrollState is non-null
|
|
@@ -1960,11 +2301,48 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1960
2301
|
for (const tr of trs as any) this.virtualizer.measureElement(tr);
|
|
1961
2302
|
};
|
|
1962
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).
|
|
1963
2335
|
virtualItemKey = (i: any) => {
|
|
1964
2336
|
const src = this.windowSource();
|
|
1965
2337
|
return src && src[i] ? src[i].id : undefined;
|
|
1966
2338
|
};
|
|
1967
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.
|
|
1968
2346
|
virtualizerOptions = (): any => ({
|
|
1969
2347
|
count: this.windowSource().length,
|
|
1970
2348
|
getScrollElement: () => this.gridScrollEl,
|
|
@@ -1992,6 +2370,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1992
2370
|
}
|
|
1993
2371
|
});
|
|
1994
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.
|
|
1995
2382
|
pinMeasurement = (pin: number): {
|
|
1996
2383
|
start: number;
|
|
1997
2384
|
size: number;
|
|
@@ -1999,6 +2386,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1999
2386
|
end: number;
|
|
2000
2387
|
} | null => this.pinnedMeasurement(pin);
|
|
2001
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).
|
|
2002
2394
|
windowedRows = () => {
|
|
2003
2395
|
// SUBSCRIBE FIRST (fine-grained targets): touch the reactive windowVer at the TOP — BEFORE any
|
|
2004
2396
|
// early return — so Solid's <For>/Svelte's {#each} accessor subscribes to it on its FIRST eval,
|
|
@@ -2069,6 +2461,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2069
2461
|
return out;
|
|
2070
2462
|
};
|
|
2071
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.
|
|
2072
2467
|
padTop = () => {
|
|
2073
2468
|
// SUBSCRIBE FIRST (the windowedRows() discipline): touch windowVer + editVer at the TOP so the
|
|
2074
2469
|
// spacer-<td> :style binding subscribes on the fine-grained targets before the early return,
|
|
@@ -2121,11 +2516,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2121
2516
|
return pad < 0 ? 0 : pad;
|
|
2122
2517
|
};
|
|
2123
2518
|
|
|
2519
|
+
// pmIndexInWindow: is full-model index `idx` present in the rendered virtual window?
|
|
2124
2520
|
pmIndexInWindow = (items: any, idx: any) => {
|
|
2125
2521
|
for (let i = 0; i < items.length; i++) if (items[i].index === idx) return true;
|
|
2126
2522
|
return false;
|
|
2127
2523
|
};
|
|
2128
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).
|
|
2129
2527
|
rowIsOutsideWindow = (r: any) => {
|
|
2130
2528
|
if (!this.virtual || !this.virtualizer) return false;
|
|
2131
2529
|
const items = this.virtualizer.getVirtualItems();
|
|
@@ -2133,6 +2531,19 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2133
2531
|
return true;
|
|
2134
2532
|
};
|
|
2135
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).
|
|
2136
2547
|
announceState: {
|
|
2137
2548
|
sorting: unknown;
|
|
2138
2549
|
columnFilters: unknown;
|
|
@@ -2143,12 +2554,18 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2143
2554
|
globalFilter: null
|
|
2144
2555
|
};
|
|
2145
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()).
|
|
2146
2559
|
effectiveSorting = () => this.sorting != null ? this.sorting : this._sortingDefault.value;
|
|
2147
2560
|
|
|
2148
2561
|
effectiveColumnFilters = () => this.columnFilters != null ? this.columnFilters : this._columnFiltersDefault.value;
|
|
2149
2562
|
|
|
2150
2563
|
effectiveGlobalFilter = () => this.globalFilter != null ? this.globalFilter : this._globalFilterDefault.value;
|
|
2151
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).
|
|
2152
2569
|
buildSortFilterAnnounce = () => {
|
|
2153
2570
|
const nextSorting = this.effectiveSorting();
|
|
2154
2571
|
const nextColumnFilters = this.effectiveColumnFilters();
|
|
@@ -2171,6 +2588,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2171
2588
|
return '';
|
|
2172
2589
|
};
|
|
2173
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.
|
|
2174
2593
|
reFeed = () => {
|
|
2175
2594
|
if (!this.table) return;
|
|
2176
2595
|
// NOTE: the external-swap history reset does NOT live here. reFeed() fires on EVERY watched
|
|
@@ -2236,6 +2655,28 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2236
2655
|
if (this.refreshRowModel) this.refreshRowModel();
|
|
2237
2656
|
};
|
|
2238
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.
|
|
2239
2680
|
lastPropsData: unknown = null;
|
|
2240
2681
|
|
|
2241
2682
|
maybeClearHistoryOnExternalSwap = () => {
|
|
@@ -2251,6 +2692,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2251
2692
|
|
|
2252
2693
|
lastDataLen = -1;
|
|
2253
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.
|
|
2254
2697
|
onHeaderSort = (colId: any, evt: any) => {
|
|
2255
2698
|
if (!this.table) return;
|
|
2256
2699
|
const col = this.table.getColumn(colId);
|
|
@@ -2260,8 +2703,20 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2260
2703
|
col.toggleSorting(undefined, multi);
|
|
2261
2704
|
};
|
|
2262
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.
|
|
2263
2716
|
tick = () => this._rowModelVer.value;
|
|
2264
2717
|
|
|
2718
|
+
// the live sort direction off the table-core column (string-safe — never a bound
|
|
2719
|
+
// boolean, the listbox aria lesson).
|
|
2265
2720
|
ariaSortFor = (colId: any) => {
|
|
2266
2721
|
if (this.tick() < 0 || !this.table) return 'none';
|
|
2267
2722
|
const col = this.table.getColumn(colId);
|
|
@@ -2272,6 +2727,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2272
2727
|
return 'none';
|
|
2273
2728
|
};
|
|
2274
2729
|
|
|
2730
|
+
// A small sort-direction glyph for the header (▲/▼/empty). Decorative — aria-hidden.
|
|
2275
2731
|
sortIndicator = (colId: any) => {
|
|
2276
2732
|
if (this.tick() < 0 || !this.table) return '';
|
|
2277
2733
|
const col = this.table.getColumn(colId);
|
|
@@ -2282,36 +2738,63 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2282
2738
|
return '';
|
|
2283
2739
|
};
|
|
2284
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).
|
|
2285
2743
|
defFor = (colId: any) => {
|
|
2286
2744
|
const defs = this.columnDefs();
|
|
2287
2745
|
for (const d of defs as any) if (d.id === colId) return d;
|
|
2288
2746
|
return null;
|
|
2289
2747
|
};
|
|
2290
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.
|
|
2291
2758
|
visibleCellsFor = (row: any) => this._rowModelVer.value >= 0 ? row.getVisibleCells() : [];
|
|
2292
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.
|
|
2293
2764
|
editMetaOf = (colId: any) => {
|
|
2294
2765
|
const d = this.defFor(colId);
|
|
2295
2766
|
return d && d.meta ? d.meta : null;
|
|
2296
2767
|
};
|
|
2297
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).
|
|
2298
2771
|
columnEditable = (colId: any) => {
|
|
2299
2772
|
const m = this.editMetaOf(colId);
|
|
2300
2773
|
return !!(m && m.editable === true);
|
|
2301
2774
|
};
|
|
2302
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'.
|
|
2303
2778
|
editorTypeOf = (colId: any) => {
|
|
2304
2779
|
const m = this.editMetaOf(colId);
|
|
2305
2780
|
return m && m.editor != null ? m.editor : 'text';
|
|
2306
2781
|
};
|
|
2307
2782
|
|
|
2783
|
+
// editorOptionsOf: the select-editor options ([{ value, label }]) for editor='select'.
|
|
2308
2784
|
editorOptionsOf = (colId: any) => {
|
|
2309
2785
|
const m = this.editMetaOf(colId);
|
|
2310
2786
|
return m && m.editorOptions != null ? m.editorOptions : [];
|
|
2311
2787
|
};
|
|
2312
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).
|
|
2313
2793
|
hasEditorSlot = (colId: any) => this.editorTypeOf(colId) === 'custom' && !!(this._hasSlotEditor || this.editor !== undefined);
|
|
2314
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).
|
|
2315
2798
|
hasFilterSlot = () => !!(this._hasSlotFilter || this.filter !== undefined);
|
|
2316
2799
|
|
|
2317
2800
|
columnIsFilterable = (colId: any) => {
|
|
@@ -2324,6 +2807,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2324
2807
|
return d ? d.header : colId;
|
|
2325
2808
|
};
|
|
2326
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).
|
|
2327
2813
|
headerWidth = (colId: any) => {
|
|
2328
2814
|
if (this.tick() < 0 || !this.table) return null;
|
|
2329
2815
|
const col = this.table.getColumn(colId);
|
|
@@ -2332,6 +2818,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2332
2818
|
return w != null && w > 0 ? w + 'px' : null;
|
|
2333
2819
|
};
|
|
2334
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).
|
|
2335
2826
|
onResizeStart = (colId: any, evt: any) => {
|
|
2336
2827
|
// stop here (NOT a `.stop` modifier) — the Angular `.stop`-in-@for hoist is broken (F5).
|
|
2337
2828
|
if (evt && evt.stopPropagation) evt.stopPropagation();
|
|
@@ -2342,6 +2833,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2342
2833
|
if (handler) handler(evt);
|
|
2343
2834
|
};
|
|
2344
2835
|
|
|
2836
|
+
// Find the live header object for a column id across the rendered header groups.
|
|
2345
2837
|
findHeader = (colId: any) => {
|
|
2346
2838
|
const groups = this._headerGroups.value || [];
|
|
2347
2839
|
for (const hg of groups as any) {
|
|
@@ -2357,6 +2849,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2357
2849
|
return !!(header && header.column && header.column.getIsResizing && header.column.getIsResizing());
|
|
2358
2850
|
};
|
|
2359
2851
|
|
|
2852
|
+
// Visibility toggle (req-8) — drive table-core's column.toggleVisibility so the
|
|
2853
|
+
// onColumnVisibilityChange funnel emits the fresh state.
|
|
2360
2854
|
columnIsVisible = (colId: any) => {
|
|
2361
2855
|
if (this.tick() < 0 || !this.table) return true;
|
|
2362
2856
|
const col = this.table.getColumn(colId);
|
|
@@ -2369,6 +2863,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2369
2863
|
if (col && col.toggleVisibility) col.toggleVisibility();
|
|
2370
2864
|
};
|
|
2371
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.
|
|
2372
2871
|
allLeafColumns = () => {
|
|
2373
2872
|
if (this.tick() < 0 || !this.table) return [];
|
|
2374
2873
|
const cols = this.table.getAllLeafColumns ? this.table.getAllLeafColumns() : [];
|
|
@@ -2384,6 +2883,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2384
2883
|
return out;
|
|
2385
2884
|
};
|
|
2386
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).
|
|
2387
2889
|
columnPinSide = (colId: any) => {
|
|
2388
2890
|
if (this.tick() < 0 || !this.table) return false;
|
|
2389
2891
|
const col = this.table.getColumn(colId);
|
|
@@ -2391,6 +2893,12 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2391
2893
|
return col.getIsPinned();
|
|
2392
2894
|
};
|
|
2393
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.
|
|
2394
2902
|
onPinColumn = (colId: any, side: any, evt: any) => {
|
|
2395
2903
|
if (evt && evt.stopPropagation) evt.stopPropagation();
|
|
2396
2904
|
if (!this.table) return;
|
|
@@ -2398,6 +2906,22 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2398
2906
|
if (col && col.pin) col.pin(side);
|
|
2399
2907
|
};
|
|
2400
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.
|
|
2401
2925
|
pinStyle = (colId: any, zIndex = 1) => {
|
|
2402
2926
|
if (this.tick() < 0 || !this.table) return '';
|
|
2403
2927
|
const col = this.table.getColumn(colId);
|
|
@@ -2414,6 +2938,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2414
2938
|
return '';
|
|
2415
2939
|
};
|
|
2416
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).
|
|
2417
2945
|
thStyle = (colId: any) => {
|
|
2418
2946
|
let s = '';
|
|
2419
2947
|
const w = this.headerWidth(colId);
|
|
@@ -2422,6 +2950,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2422
2950
|
return s;
|
|
2423
2951
|
};
|
|
2424
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).
|
|
2425
2957
|
onGlobalFilterInput = (evt: any) => {
|
|
2426
2958
|
const value = evt && evt.target ? evt.target.value : '';
|
|
2427
2959
|
if (this.table) {
|
|
@@ -2431,16 +2963,21 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2431
2963
|
this.writeGlobalFilter(value);
|
|
2432
2964
|
};
|
|
2433
2965
|
|
|
2966
|
+
// Per-column filter input → setColumnFilter (fresh-array funnel).
|
|
2434
2967
|
onColumnFilterInput = (colId: any, evt: any) => {
|
|
2435
2968
|
const value = evt && evt.target ? evt.target.value : '';
|
|
2436
2969
|
this.setColumnFilter(colId, value);
|
|
2437
2970
|
};
|
|
2438
2971
|
|
|
2972
|
+
// The live global filter value (bound to the search <input>, value-driven NOT eval'd).
|
|
2439
2973
|
globalFilterValue = () => {
|
|
2440
2974
|
const v = this.currentState().globalFilter;
|
|
2441
2975
|
return v != null ? v : '';
|
|
2442
2976
|
};
|
|
2443
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).
|
|
2444
2981
|
pageIndex = () => {
|
|
2445
2982
|
if (this.tick() >= 0 && this.table) return this.table.getState().pagination.pageIndex;
|
|
2446
2983
|
const p = this.currentState().pagination;
|
|
@@ -2453,6 +2990,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2453
2990
|
return p && p.pageSize != null ? p.pageSize : 10;
|
|
2454
2991
|
};
|
|
2455
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.
|
|
2456
2998
|
displayPageCount = () => {
|
|
2457
2999
|
if (this.tick() < 0 || !this.table) return 1;
|
|
2458
3000
|
const c = this.table.getPageCount();
|
|
@@ -2478,16 +3020,44 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2478
3020
|
this.table.setPageSize(Number.isFinite(n) && n > 0 ? n : 10);
|
|
2479
3021
|
};
|
|
2480
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).
|
|
2481
3026
|
isSelectColumn = (colId: any) => colId === this.SELECT_COL_ID;
|
|
2482
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).
|
|
2483
3035
|
isExpanderColumn = (colId: any) => colId === this.EXPANDER_COL_ID;
|
|
2484
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.
|
|
2485
3043
|
rowCanExpand = (row: any) => !!(this.tick() >= 0 && row && row.getCanExpand && row.getCanExpand() && !(row.getIsGrouped && row.getIsGrouped()));
|
|
2486
3044
|
|
|
2487
3045
|
rowIsExpanded = (row: any) => !!(this.tick() >= 0 && row && row.getIsExpanded && row.getIsExpanded());
|
|
2488
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).
|
|
2489
3055
|
rowShowsDetail = (row: any) => this.getSubRows == null && !this.rowIsGrouped(row) && this.rowIsExpanded(row);
|
|
2490
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).
|
|
2491
3061
|
onToggleExpand = (row: any, evt: any) => {
|
|
2492
3062
|
if (!row || !row.toggleExpanded) return;
|
|
2493
3063
|
// Capture the owning row element BEFORE the toggle so DOM focus can be restored after the
|
|
@@ -2511,6 +3081,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2511
3081
|
}
|
|
2512
3082
|
};
|
|
2513
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).
|
|
2514
3088
|
bodyCellStyle = (row: any, colId: any) => {
|
|
2515
3089
|
const base = this.pinStyle(colId);
|
|
2516
3090
|
if (this.isExpanderColumn(colId) && row && row.depth) {
|
|
@@ -2521,20 +3095,51 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2521
3095
|
return base;
|
|
2522
3096
|
};
|
|
2523
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.
|
|
2524
3106
|
rowIsGrouped = (row: any) => !!(this.tick() >= 0 && row && row.getIsGrouped && row.getIsGrouped());
|
|
2525
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).
|
|
2526
3110
|
groupingActive = () => this.tick() >= 0 && (this.currentState().grouping || []).length > 0;
|
|
2527
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.
|
|
2528
3116
|
cellIsGrouped = (cellCtx: any) => !!(this.tick() >= 0 && cellCtx && cellCtx.getIsGrouped && cellCtx.getIsGrouped());
|
|
2529
3117
|
|
|
2530
3118
|
cellIsAggregated = (cellCtx: any) => !!(this.tick() >= 0 && cellCtx && cellCtx.getIsAggregated && cellCtx.getIsAggregated());
|
|
2531
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).
|
|
2532
3125
|
cellIsPlaceholder = (cellCtx: any) => !!(this.tick() >= 0 && cellCtx && cellCtx.getIsPlaceholder && cellCtx.getIsPlaceholder());
|
|
2533
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).
|
|
2534
3133
|
groupSubRowCount = (row: any) => row && row.getLeafRows ? row.getLeafRows().length : row && row.subRows ? row.subRows.length : 0;
|
|
2535
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.
|
|
2536
3138
|
groupingKeys = () => this.currentState().grouping || [];
|
|
2537
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.
|
|
2538
3143
|
groupableColumns = () => {
|
|
2539
3144
|
const out = [];
|
|
2540
3145
|
const defs = this.columnDefs();
|
|
@@ -2548,10 +3153,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2548
3153
|
return out;
|
|
2549
3154
|
};
|
|
2550
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.
|
|
2551
3159
|
stopEvent = (evt: any) => {
|
|
2552
3160
|
if (evt && evt.stopPropagation) evt.stopPropagation();
|
|
2553
3161
|
};
|
|
2554
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).
|
|
2555
3165
|
isAllRowsSelected = () => !!(this.tick() >= 0 && this.table && this.table.getIsAllRowsSelected());
|
|
2556
3166
|
|
|
2557
3167
|
isSomeRowsSelected = () => !!(this.tick() >= 0 && this.table && this.table.getIsSomeRowsSelected());
|
|
@@ -2561,6 +3171,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2561
3171
|
this.table.toggleAllRowsSelected(!!(evt && evt.target && evt.target.checked));
|
|
2562
3172
|
};
|
|
2563
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.
|
|
2564
3182
|
rowIsSelected = (row: any) => {
|
|
2565
3183
|
if (!row) return false;
|
|
2566
3184
|
const id = row.id;
|
|
@@ -2574,6 +3192,12 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2574
3192
|
row.toggleSelected(!!(evt && evt.target && evt.target.checked));
|
|
2575
3193
|
};
|
|
2576
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).
|
|
2577
3201
|
onHideColumn = (colId: any, evt: any) => {
|
|
2578
3202
|
if (evt && evt.stopPropagation) evt.stopPropagation();
|
|
2579
3203
|
if (!this.table) return;
|
|
@@ -2581,6 +3205,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2581
3205
|
if (col && col.toggleVisibility) col.toggleVisibility(false);
|
|
2582
3206
|
};
|
|
2583
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.
|
|
2584
3211
|
hasAnyFilterableColumn = () => {
|
|
2585
3212
|
const cols = this.allLeafColumns();
|
|
2586
3213
|
for (const c of cols as any) {
|
|
@@ -2589,6 +3216,18 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2589
3216
|
return false;
|
|
2590
3217
|
};
|
|
2591
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(...)`.
|
|
2592
3231
|
selectAllBox: any = null;
|
|
2593
3232
|
|
|
2594
3233
|
syncIndeterminate = () => {
|
|
@@ -2597,6 +3236,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2597
3236
|
if (this.selectAllBox) this.selectAllBox.indeterminate = this.isSomeRowsSelected() && !this.isAllRowsSelected();
|
|
2598
3237
|
};
|
|
2599
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.
|
|
2600
3246
|
sortColumn = (colId: any, desc: any) => {
|
|
2601
3247
|
if (this.table) this.table.getColumn(colId) && this.table.getColumn(colId).toggleSorting(desc, false);
|
|
2602
3248
|
};
|
|
@@ -2607,6 +3253,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2607
3253
|
|
|
2608
3254
|
getColumnDefs = () => this.columnDefs();
|
|
2609
3255
|
|
|
3256
|
+
// selection verbs (req-7) — drive table-core so the onRowSelectionChange funnel
|
|
3257
|
+
// emits the fresh state + selection-change.
|
|
2610
3258
|
toggleAllRows = (value: any) => {
|
|
2611
3259
|
if (this.table) this.table.toggleAllRowsSelected(value);
|
|
2612
3260
|
};
|
|
@@ -2617,6 +3265,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2617
3265
|
|
|
2618
3266
|
getSelectedRows = () => this.table ? this.table.getSelectedRowModel().rows.map((r: any) => r.original) : [];
|
|
2619
3267
|
|
|
3268
|
+
// pagination verbs.
|
|
2620
3269
|
setPage = (idx: any) => {
|
|
2621
3270
|
if (this.table) this.table.setPageIndex(idx);
|
|
2622
3271
|
};
|
|
@@ -2625,6 +3274,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2625
3274
|
if (this.table) this.table.setPageSize(size);
|
|
2626
3275
|
};
|
|
2627
3276
|
|
|
3277
|
+
// column-management verbs (req-8/9/10/11) — drive table-core so the funnels fire.
|
|
2628
3278
|
toggleColumnVisibility = (colId: any) => {
|
|
2629
3279
|
if (this.table) {
|
|
2630
3280
|
const c = this.table.getColumn(colId);
|
|
@@ -2632,6 +3282,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2632
3282
|
}
|
|
2633
3283
|
};
|
|
2634
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.
|
|
2635
3292
|
applyColumnOrder = (order: any) => {
|
|
2636
3293
|
if (this.table) this.table.setColumnOrder(order);
|
|
2637
3294
|
};
|
|
@@ -2640,6 +3297,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2640
3297
|
if (this.table) this.table.resetColumnSizing(true);
|
|
2641
3298
|
};
|
|
2642
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).
|
|
2643
3302
|
pinColumn = (colId: any, side: any) => {
|
|
2644
3303
|
if (this.table) {
|
|
2645
3304
|
const c = this.table.getColumn(colId);
|
|
@@ -2647,39 +3306,101 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2647
3306
|
}
|
|
2648
3307
|
};
|
|
2649
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).
|
|
2650
3316
|
getRowIndexRelativeToPage = (absRow: any) => {
|
|
2651
3317
|
const abs = absRow == null ? this.toAbsRow(this._activeRow.value) : Math.trunc(Number(absRow)) || 0;
|
|
2652
3318
|
if (this.virtual) return abs;
|
|
2653
3319
|
return abs - this.pageRowOffset();
|
|
2654
3320
|
};
|
|
2655
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.
|
|
2656
3328
|
cut = () => this.cutRange();
|
|
2657
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).
|
|
2658
3344
|
isGrid = () => this.interactionMode === 'grid';
|
|
2659
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.
|
|
2660
3349
|
tableRole = () => this.isGrid() ? 'grid' : 'table';
|
|
2661
3350
|
|
|
2662
3351
|
cellRole = () => this.isGrid() ? 'gridcell' : 'cell';
|
|
2663
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.
|
|
2664
3357
|
rowIndexOf = (row: any) => this.tick() >= 0 ? (this._rows.value || []).indexOf(row) : -1;
|
|
2665
3358
|
|
|
3359
|
+
// colIndexOf: a body cell's position in its row's visible cell list.
|
|
2666
3360
|
colIndexOf = (row: any, cellCtx: any) => this.tick() >= 0 ? this.visibleCellsFor(row).indexOf(cellCtx) : -1;
|
|
2667
3361
|
|
|
3362
|
+
// headerColIndexOf: a header cell's position in its header group's leaf headers.
|
|
2668
3363
|
headerColIndexOf = (hg: any, header: any) => (hg && hg.headers ? hg.headers : []).indexOf(header);
|
|
2669
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.
|
|
2670
3376
|
pageRowOffset = () => {
|
|
2671
3377
|
if (!this.isGrid() || this.virtual) return 0;
|
|
2672
3378
|
return this.pageIndex() * this.pageSize();
|
|
2673
3379
|
};
|
|
2674
3380
|
|
|
3381
|
+
// page-relative active row → absolute (display-order) index.
|
|
2675
3382
|
toAbsRow = (localRow: any) => localRow + this.pageRowOffset();
|
|
2676
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.
|
|
2677
3390
|
prePaginationRowCount = () => {
|
|
2678
3391
|
if (!this.table || this.virtual) return this.bodyRowCount();
|
|
2679
3392
|
const pm = this.table.getPrePaginationRowModel();
|
|
2680
3393
|
return pm && pm.rows ? pm.rows.length : this.bodyRowCount();
|
|
2681
3394
|
};
|
|
2682
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.
|
|
2683
3404
|
cellTabindex = (rowKey: any, colIndex: any, level = null) => {
|
|
2684
3405
|
if (!this.isGrid()) return null;
|
|
2685
3406
|
// B6: an empty / all-filtered grid (no body rows) must STILL be keyboard-reachable. Fall
|
|
@@ -2700,6 +3421,17 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2700
3421
|
return isActive ? 0 : -1;
|
|
2701
3422
|
};
|
|
2702
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.
|
|
2703
3435
|
isActiveCell = (rowKey: any, colIndex: any, level = null) => {
|
|
2704
3436
|
if (!this.isGrid()) return false;
|
|
2705
3437
|
if (this._activeIsHeader.value) {
|
|
@@ -2710,6 +3442,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2710
3442
|
return rowKey === String(this._activeRow.value) && colIndex === this._activeColIndex.value;
|
|
2711
3443
|
};
|
|
2712
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).
|
|
2713
3450
|
resolveCellEl = (rowKey: any, colIndex: any, level = null) => {
|
|
2714
3451
|
if (!this.gridRoot) return null;
|
|
2715
3452
|
// B12: a grouped multi-level header has MULTIPLE cells sharing data-row="__header" at the
|
|
@@ -2723,6 +3460,19 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2723
3460
|
return this.gridRoot.querySelector(sel);
|
|
2724
3461
|
};
|
|
2725
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.
|
|
2726
3476
|
focusActiveCell = (nextRow = null, nextCol = null, nextIsHeader = null, nextLevel = null) => {
|
|
2727
3477
|
if (!this.isGrid() || !this.gridRoot) return;
|
|
2728
3478
|
// #9 focus-intent epoch: focusActiveCell is THE single seam every keyboard nav re-asserts
|
|
@@ -2788,20 +3538,59 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2788
3538
|
if (el) el.focus();
|
|
2789
3539
|
};
|
|
2790
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).
|
|
2791
3556
|
totalRowCount = () => {
|
|
2792
3557
|
if (!this.table) return (this._rows.value || []).length;
|
|
2793
3558
|
const fm = this.table.getFilteredRowModel();
|
|
2794
3559
|
return fm && fm.rows ? fm.rows.length : (this._rows.value || []).length;
|
|
2795
3560
|
};
|
|
2796
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).
|
|
2797
3575
|
headerRowCount = () => (this._headerGroups.value || []).length;
|
|
2798
3576
|
|
|
2799
3577
|
gridAriaRowCount = () => this.headerRowCount() + this.totalRowCount();
|
|
2800
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).
|
|
2801
3583
|
ariaPageOffset = () => this.table ? this.pageIndex() * this.pageSize() : 0;
|
|
2802
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).
|
|
2803
3589
|
bodyAriaRowIndex = (row: any) => this.headerRowCount() + this.rowIndexOf(row) + this.ariaPageOffset() + 1;
|
|
2804
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.
|
|
2805
3594
|
visibleColCount = () => {
|
|
2806
3595
|
// NB: local is `rowList` (NOT `rows`) — the React emitter lowers `$data.rows` to the bare
|
|
2807
3596
|
// state binding `rows`, so a `const rows = $data.rows` self-shadows it (TS2448 TDZ). Same
|
|
@@ -2816,11 +3605,24 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2816
3605
|
|
|
2817
3606
|
clamp = (v: any, lo: any, hi: any) => v < lo ? lo : v > hi ? hi : v;
|
|
2818
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.
|
|
2819
3614
|
headerLeafLevel = () => {
|
|
2820
3615
|
const hg = this._headerGroups.value || [];
|
|
2821
3616
|
return hg.length ? hg.length - 1 : 0;
|
|
2822
3617
|
};
|
|
2823
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.
|
|
2824
3626
|
headerCountAtLevel = (level: any) => {
|
|
2825
3627
|
const hg = this._headerGroups.value || [];
|
|
2826
3628
|
if (!hg.length) return this.visibleColCount();
|
|
@@ -2836,6 +3638,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2836
3638
|
return grp.headers[colIndex] || null;
|
|
2837
3639
|
};
|
|
2838
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.
|
|
2839
3645
|
parentHeaderColIndex = (level: any, colIndex: any) => {
|
|
2840
3646
|
if (level <= 0) return -1;
|
|
2841
3647
|
const h = this.headerAt(level, colIndex);
|
|
@@ -2851,6 +3657,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2851
3657
|
return -1;
|
|
2852
3658
|
};
|
|
2853
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.
|
|
2854
3663
|
firstChildHeaderColIndex = (level: any, colIndex: any) => {
|
|
2855
3664
|
const h = this.headerAt(level, colIndex);
|
|
2856
3665
|
if (!h || !h.column) return -1;
|
|
@@ -2867,6 +3676,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2867
3676
|
return -1;
|
|
2868
3677
|
};
|
|
2869
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).
|
|
2870
3686
|
moveCol = (delta: any) => {
|
|
2871
3687
|
// #10: when a grouped PARENT header is active, clamp against the header count AT THE ACTIVE
|
|
2872
3688
|
// LEVEL (which may be fewer than the leaf-column count) so ArrowRight never overruns onto a
|
|
@@ -2878,6 +3694,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2878
3694
|
return nextCol;
|
|
2879
3695
|
};
|
|
2880
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).
|
|
2881
3702
|
moveRow = (delta: any) => {
|
|
2882
3703
|
const lastRow = this.bodyRowCount() - 1;
|
|
2883
3704
|
const maxRow = lastRow < 0 ? 0 : lastRow;
|
|
@@ -2969,6 +3790,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2969
3790
|
};
|
|
2970
3791
|
};
|
|
2971
3792
|
|
|
3793
|
+
// Home/End within the current row → col 0 / max. Returns the fresh colIndex.
|
|
2972
3794
|
gotoColEdge = (toEnd: any) => {
|
|
2973
3795
|
// #10: End on a grouped PARENT header lands on that level's LAST header (headerCountAtLevel-1),
|
|
2974
3796
|
// not the leaf-column max — otherwise the ring strands on a phantom cell past the level's
|
|
@@ -2980,6 +3802,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2980
3802
|
return nextCol;
|
|
2981
3803
|
};
|
|
2982
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.
|
|
2983
3809
|
gotoRowEdge = (toEnd: any) => {
|
|
2984
3810
|
const lastRow = this.bodyRowCount() - 1;
|
|
2985
3811
|
const nextRow = toEnd ? lastRow < 0 ? 0 : lastRow : 0;
|
|
@@ -2988,6 +3814,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2988
3814
|
return nextRow;
|
|
2989
3815
|
};
|
|
2990
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).
|
|
2991
3819
|
gotoStart = () => {
|
|
2992
3820
|
this._activeIsHeader.value = false;
|
|
2993
3821
|
this._activeRow.value = 0;
|
|
@@ -3012,17 +3840,23 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3012
3840
|
};
|
|
3013
3841
|
};
|
|
3014
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.
|
|
3015
3845
|
currentCellEl = () => {
|
|
3016
3846
|
const rowKey = this._activeIsHeader.value ? '__header' : String(this._activeRow.value);
|
|
3017
3847
|
return this.resolveCellEl(rowKey, this._activeColIndex.value, this._activeIsHeader.value ? this._activeHeaderLevel.value : null);
|
|
3018
3848
|
};
|
|
3019
3849
|
|
|
3850
|
+
// The focusable descendants of a cell (non-disabled), in DOM order. Pure DOM — uniform ×6.
|
|
3020
3851
|
focusables = (cellEl: any) => {
|
|
3021
3852
|
if (!cellEl || !cellEl.querySelectorAll) return [];
|
|
3022
3853
|
const list = Array.prototype.slice.call(cellEl.querySelectorAll('button,[href],input,select,textarea,[tabindex]:not([tabindex="-1"])'));
|
|
3023
3854
|
return list.filter((n: any) => !n.disabled);
|
|
3024
3855
|
};
|
|
3025
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.
|
|
3026
3860
|
enterControl = () => {
|
|
3027
3861
|
const cellEl = this.currentCellEl();
|
|
3028
3862
|
const list = this.focusables(cellEl);
|
|
@@ -3031,6 +3865,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3031
3865
|
list[0].focus();
|
|
3032
3866
|
};
|
|
3033
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.
|
|
3034
3872
|
cycleWithinCell = (cellEl: any, forward: any) => {
|
|
3035
3873
|
const list = this.focusables(cellEl);
|
|
3036
3874
|
if (!list.length) return;
|
|
@@ -3042,6 +3880,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3042
3880
|
list[i].focus();
|
|
3043
3881
|
};
|
|
3044
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).
|
|
3045
3888
|
onGridKeyDown = (e: any) => {
|
|
3046
3889
|
if (!this.isGrid() || !e) return;
|
|
3047
3890
|
const key = e.key;
|
|
@@ -3379,6 +4222,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3379
4222
|
}
|
|
3380
4223
|
};
|
|
3381
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).
|
|
3382
4234
|
syncActiveFromEvent = (e: any) => {
|
|
3383
4235
|
if (!this.isGrid() || !e) return;
|
|
3384
4236
|
const tgt = e.target;
|
|
@@ -3442,6 +4294,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3442
4294
|
if (tgt === cellEl) this._activeInControl.value = false;
|
|
3443
4295
|
};
|
|
3444
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).
|
|
3445
4306
|
onGridMouseDown = (e: any) => {
|
|
3446
4307
|
if (!this.isGrid() || !e) return;
|
|
3447
4308
|
const tgt = e.target;
|
|
@@ -3474,6 +4335,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3474
4335
|
this.beginRangeDrag(row, col);
|
|
3475
4336
|
};
|
|
3476
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.
|
|
3477
4346
|
onGridDblClick = (e: any) => {
|
|
3478
4347
|
if (!this.isGrid() || !e) return;
|
|
3479
4348
|
const tgt = e.target;
|
|
@@ -3512,6 +4381,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3512
4381
|
}
|
|
3513
4382
|
};
|
|
3514
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.
|
|
3515
4391
|
onGridClick = (e: any) => {
|
|
3516
4392
|
if (!this.isGrid() || !e) return;
|
|
3517
4393
|
if (!this.singleClickEdit) return;
|
|
@@ -3539,6 +4415,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3539
4415
|
if (colId != null && this.columnEditable(colId)) this.beginEdit(row, col, null);
|
|
3540
4416
|
};
|
|
3541
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.
|
|
3542
4426
|
onGridFocusOut = (e: any) => {
|
|
3543
4427
|
if (!this.isGrid() || !this._activeInControl.value) return;
|
|
3544
4428
|
const next = e ? e.relatedTarget : null;
|
|
@@ -3546,6 +4430,20 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3546
4430
|
if (!cellEl || !next || !cellEl.contains(next)) this._activeInControl.value = false;
|
|
3547
4431
|
};
|
|
3548
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.
|
|
3549
4447
|
recoverGridFocus = (rowKey: any, col: any, level: any, guardMoved = false) => {
|
|
3550
4448
|
if (!this.gridRoot) return;
|
|
3551
4449
|
let attempts = 0;
|
|
@@ -3570,6 +4468,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3570
4468
|
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(tryFocus);else setTimeout(tryFocus, 0);
|
|
3571
4469
|
};
|
|
3572
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).
|
|
3573
4475
|
clampActiveCell = (rowCount: any, colCount: any) => {
|
|
3574
4476
|
if (!this.isGrid()) return;
|
|
3575
4477
|
// B8/B23 React-stale guard: the bounds come from the FRESH model the caller (refreshRowModel)
|
|
@@ -3665,12 +4567,50 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3665
4567
|
}
|
|
3666
4568
|
};
|
|
3667
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.
|
|
3668
4582
|
gridEmptyFallback = false;
|
|
3669
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.
|
|
3670
4599
|
rangeTransition = false;
|
|
3671
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.
|
|
3672
4604
|
rangeClickPending = false;
|
|
3673
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.
|
|
3674
4614
|
rangeActive = false;
|
|
3675
4615
|
|
|
3676
4616
|
inRange = (rIdx: any, cIdx: any) => {
|
|
@@ -3684,6 +4624,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3684
4624
|
return rIdx >= r0 && rIdx <= r1 && cIdx >= c0 && cIdx <= c1;
|
|
3685
4625
|
};
|
|
3686
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).
|
|
3687
4631
|
getSelectedRange = () => {
|
|
3688
4632
|
// B8: clamp the corners to the CURRENT bounds ON READ so the verb (and the range-change emit
|
|
3689
4633
|
// payload) never reports a corner past a shrunken model — React-stale-safe (the eager
|
|
@@ -3710,6 +4654,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3710
4654
|
};
|
|
3711
4655
|
};
|
|
3712
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).
|
|
3713
4660
|
isFillHandleCell = (rIdx: any, cIdx: any) => {
|
|
3714
4661
|
const a = this._rangeAnchor.value;
|
|
3715
4662
|
const f = this._rangeFocus.value;
|
|
@@ -3719,6 +4666,12 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3719
4666
|
return rIdx === r1 && cIdx === c1;
|
|
3720
4667
|
};
|
|
3721
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.
|
|
3722
4675
|
emitRangeChange = (anchor: any, focus: any) => {
|
|
3723
4676
|
this.dispatchEvent(new CustomEvent("range-change", {
|
|
3724
4677
|
detail: {
|
|
@@ -3730,6 +4683,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3730
4683
|
}));
|
|
3731
4684
|
};
|
|
3732
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.
|
|
3733
4690
|
extendRange = (dRow: any, dCol: any) => {
|
|
3734
4691
|
if (this._activeIsHeader.value) return;
|
|
3735
4692
|
const maxRow = this.bodyRowCount() - 1;
|
|
@@ -3775,6 +4732,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3775
4732
|
}
|
|
3776
4733
|
};
|
|
3777
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.
|
|
3778
4738
|
setRangeFocus = (rIdx: any, cIdx: any) => {
|
|
3779
4739
|
const maxRow = this.bodyRowCount() - 1;
|
|
3780
4740
|
const maxCol = this.visibleColCount() - 1;
|
|
@@ -3796,6 +4756,12 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3796
4756
|
this.emitRangeChange(anchor, nextFocus);
|
|
3797
4757
|
};
|
|
3798
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.
|
|
3799
4765
|
selectAllBody = () => {
|
|
3800
4766
|
const maxRow = this.bodyRowCount() - 1;
|
|
3801
4767
|
const maxCol = this.visibleColCount() - 1;
|
|
@@ -3814,6 +4780,12 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3814
4780
|
this.emitRangeChange(anchor, focus);
|
|
3815
4781
|
};
|
|
3816
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).
|
|
3817
4789
|
clearRange = () => {
|
|
3818
4790
|
// B19: gate on the SYNCHRONOUS rangeActive mirror, NOT a $data re-read. clearRange runs twice
|
|
3819
4791
|
// in one plain-arrow keydown (explicit collapse + the focusin after the programmatic focus
|
|
@@ -3827,6 +4799,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3827
4799
|
this.emitRangeChange(null, null);
|
|
3828
4800
|
};
|
|
3829
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.
|
|
3830
4810
|
clampRange = (maxRowArg: any, maxColArg: any) => {
|
|
3831
4811
|
const a = this._rangeAnchor.value;
|
|
3832
4812
|
const f = this._rangeFocus.value;
|
|
@@ -3859,17 +4839,42 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3859
4839
|
}
|
|
3860
4840
|
};
|
|
3861
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.
|
|
3862
4852
|
announce = (msg: any) => {
|
|
3863
4853
|
this._pasteAnnounce.value = msg != null ? msg : '';
|
|
3864
4854
|
};
|
|
3865
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.
|
|
3866
4862
|
clipboardActiveAllowed = () => !this._activeIsHeader.value;
|
|
3867
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.
|
|
3868
4866
|
fieldOfColId = (colId: any) => {
|
|
3869
4867
|
const d = this.defFor(colId);
|
|
3870
4868
|
return d ? d.accessorKey != null ? d.accessorKey : colId : colId;
|
|
3871
4869
|
};
|
|
3872
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.
|
|
3873
4878
|
normalizedRange = () => {
|
|
3874
4879
|
const a = this._rangeAnchor.value;
|
|
3875
4880
|
const f = this._rangeFocus.value;
|
|
@@ -3889,6 +4894,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3889
4894
|
};
|
|
3890
4895
|
};
|
|
3891
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).
|
|
3892
4901
|
escapeTsvField = (s: any) => {
|
|
3893
4902
|
if (s.indexOf('\t') >= 0 || s.indexOf('\n') >= 0 || s.indexOf('\r') >= 0 || s.indexOf('"') >= 0) {
|
|
3894
4903
|
return '"' + s.replace(/"/g, '""') + '"';
|
|
@@ -3896,6 +4905,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3896
4905
|
return s;
|
|
3897
4906
|
};
|
|
3898
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.
|
|
3899
4911
|
rangeToTsv = () => {
|
|
3900
4912
|
const box = this.normalizedRange();
|
|
3901
4913
|
const r0 = box ? box.r0 : this._activeRow.value;
|
|
@@ -3914,6 +4926,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3914
4926
|
return lines.join('\n');
|
|
3915
4927
|
};
|
|
3916
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).
|
|
3917
4932
|
parseTsv = (text: any) => {
|
|
3918
4933
|
const str = text != null ? String(text) : '';
|
|
3919
4934
|
// CR-03: length guard BEFORE the parse — an empty string is a no-op, and a pathologically
|
|
@@ -3989,6 +5004,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3989
5004
|
return rows;
|
|
3990
5005
|
};
|
|
3991
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.
|
|
3992
5009
|
copyRange = () => {
|
|
3993
5010
|
// B11: never copy from a header-active state (the reusable clipboard guard).
|
|
3994
5011
|
if (!this.clipboardActiveAllowed()) return;
|
|
@@ -3999,6 +5016,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3999
5016
|
} catch (err: any) {/* best-effort copy */}
|
|
4000
5017
|
};
|
|
4001
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 }.
|
|
4002
5027
|
applyGridToRange = (grid: any, originRow: any, originCol: any) => {
|
|
4003
5028
|
const maxRow = this.bodyRowCount() - 1;
|
|
4004
5029
|
const maxCol = this.visibleColCount() - 1;
|
|
@@ -4063,6 +5088,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4063
5088
|
};
|
|
4064
5089
|
};
|
|
4065
5090
|
|
|
5091
|
+
// rowOriginalAt / rowIdAt: the underlying row object / id at a visible-model body index.
|
|
4066
5092
|
rowOriginalAt = (rowIndex: any) => {
|
|
4067
5093
|
const rowList = this._rows.value || [];
|
|
4068
5094
|
const row = rowList[rowIndex];
|
|
@@ -4075,6 +5101,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4075
5101
|
return row ? row.id : null;
|
|
4076
5102
|
};
|
|
4077
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).
|
|
4078
5112
|
tileGridToBox = (grid: any, box: any) => {
|
|
4079
5113
|
const srcRows = grid.length;
|
|
4080
5114
|
// srcCols is the MAX row width across ALL rows (not grid[0].length): a RAGGED clipboard
|
|
@@ -4104,6 +5138,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4104
5138
|
return out;
|
|
4105
5139
|
};
|
|
4106
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.
|
|
4107
5144
|
pasteRange = () => {
|
|
4108
5145
|
// B11: never paste into a header-active state (the reusable clipboard guard) — a header
|
|
4109
5146
|
// anchor would silently write body row 0 at the header's column.
|
|
@@ -4141,6 +5178,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4141
5178
|
}).catch(() => {});
|
|
4142
5179
|
};
|
|
4143
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.
|
|
4144
5190
|
cutRange = () => {
|
|
4145
5191
|
if (!this.clipboardActiveAllowed()) return;
|
|
4146
5192
|
// Snapshot the source rectangle synchronously (same ROZ138 concern as pasteRange).
|
|
@@ -4166,6 +5212,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4166
5212
|
this.applyGridToRange(grid, r0, c0);
|
|
4167
5213
|
};
|
|
4168
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).
|
|
4169
5224
|
clearActiveRange = () => {
|
|
4170
5225
|
if (!this.clipboardActiveAllowed()) return;
|
|
4171
5226
|
// Snapshot the source rectangle synchronously (the ROZ138 concern cutRange/pasteRange share).
|
|
@@ -4183,6 +5238,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4183
5238
|
this.applyGridToRange(grid, r0, c0);
|
|
4184
5239
|
};
|
|
4185
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.
|
|
4186
5245
|
tileIndex = (i: any, lo: any, hi: any) => {
|
|
4187
5246
|
const span = hi - lo + 1;
|
|
4188
5247
|
if (span <= 1) return lo;
|
|
@@ -4191,6 +5250,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4191
5250
|
return lo + k;
|
|
4192
5251
|
};
|
|
4193
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.
|
|
4194
5262
|
fillRange = (sourceBox: any, endCell: any) => {
|
|
4195
5263
|
// B7 (React-stale-safe): compute the EXTENDED rectangle from the gesture's FRESH endpoints —
|
|
4196
5264
|
// the pre-drag sourceBox (∪) the drag's final end cell — NOT a $data.rangeFocus re-read. On
|
|
@@ -4237,8 +5305,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4237
5305
|
this.applyGridToRange(grid, box.r0, box.c0);
|
|
4238
5306
|
};
|
|
4239
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.
|
|
4240
5312
|
fillDragging = false;
|
|
4241
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).
|
|
4242
5317
|
fillDragMove: any = null;
|
|
4243
5318
|
|
|
4244
5319
|
fillDragUp: any = null;
|
|
@@ -4336,6 +5411,12 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4336
5411
|
}
|
|
4337
5412
|
};
|
|
4338
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.
|
|
4339
5420
|
rangeDragging = false;
|
|
4340
5421
|
|
|
4341
5422
|
rangeDragMove: any = null;
|
|
@@ -4344,6 +5425,17 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4344
5425
|
|
|
4345
5426
|
rangeDragMoved = false;
|
|
4346
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.
|
|
4347
5439
|
teardownRangeDrag = () => {
|
|
4348
5440
|
if (typeof document !== 'undefined') {
|
|
4349
5441
|
if (this.rangeDragMove) document.removeEventListener('pointermove', this.rangeDragMove);
|
|
@@ -4354,6 +5446,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4354
5446
|
this.rangeDragging = false;
|
|
4355
5447
|
};
|
|
4356
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).
|
|
4357
5457
|
beginRangeDrag = (anchorR: any, anchorC: any) => {
|
|
4358
5458
|
// #leak: tear down any orphaned PRIOR range gesture BEFORE reassigning the module-let handlers.
|
|
4359
5459
|
// A missed pointerup (off-window release, context menu, alt-tab) leaves the prior drag's document
|
|
@@ -4390,6 +5490,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4390
5490
|
}
|
|
4391
5491
|
};
|
|
4392
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).
|
|
4393
5501
|
activeCellColumnId = () => {
|
|
4394
5502
|
if (this._activeIsHeader.value) return null;
|
|
4395
5503
|
const rowList = this._rows.value || [];
|
|
@@ -4400,11 +5508,25 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4400
5508
|
return cell && cell.column ? cell.column.id : null;
|
|
4401
5509
|
};
|
|
4402
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.
|
|
4403
5514
|
isActiveCellEditable = () => {
|
|
4404
5515
|
const colId = this.activeCellColumnId();
|
|
4405
5516
|
return colId != null && this.columnEditable(colId);
|
|
4406
5517
|
};
|
|
4407
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).
|
|
4408
5530
|
isEditing = (rowIndex: any, colIndex: any) => {
|
|
4409
5531
|
if (this._editVer.value < 0) return false;
|
|
4410
5532
|
if (this._editingRowIndex.value != null && this._editingRowIndex.value === rowIndex) {
|
|
@@ -4414,8 +5536,17 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4414
5536
|
return this._editingRow.value === rowIndex && this._editingCol.value === colIndex;
|
|
4415
5537
|
};
|
|
4416
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.
|
|
4417
5544
|
cellAriaInvalid = (rowIndex: any, colIndex: any): 'true' | null => this.isEditing(rowIndex, colIndex) && !!this._invalidMsg.value ? 'true' : null;
|
|
4418
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).
|
|
4419
5550
|
runValidator = (colId: any, value: any, row: any) => {
|
|
4420
5551
|
const m = this.editMetaOf(colId);
|
|
4421
5552
|
const v = m ? m.validate : null;
|
|
@@ -4431,10 +5562,17 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4431
5562
|
return 'Invalid value';
|
|
4432
5563
|
};
|
|
4433
5564
|
|
|
5565
|
+
// setInvalid: record the current validation error (drives the aria-live region +
|
|
5566
|
+
// :aria-invalid wired in Task 3). Empty string clears it.
|
|
4434
5567
|
setInvalid = (msg: any) => {
|
|
4435
5568
|
this._invalidMsg.value = msg != null ? msg : '';
|
|
4436
5569
|
};
|
|
4437
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).
|
|
4438
5576
|
replaceRowValue = (rows: any, rowIndex: any, field: any, value: any) => {
|
|
4439
5577
|
const src = rows || [];
|
|
4440
5578
|
const out = [];
|
|
@@ -4453,6 +5591,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4453
5591
|
return out;
|
|
4454
5592
|
};
|
|
4455
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.
|
|
4456
5597
|
sourceIndexOfRow = (visibleRowIndex: any) => {
|
|
4457
5598
|
const rowList = this._rows.value || [];
|
|
4458
5599
|
const row = rowList[visibleRowIndex];
|
|
@@ -4463,6 +5604,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4463
5604
|
return idx >= 0 ? idx : visibleRowIndex;
|
|
4464
5605
|
};
|
|
4465
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.
|
|
4466
5611
|
editingColumnId = () => {
|
|
4467
5612
|
const rowList = this._rows.value || [];
|
|
4468
5613
|
const row = rowList[this._editingRow.value];
|
|
@@ -4500,6 +5645,24 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4500
5645
|
return row ? row.id : null;
|
|
4501
5646
|
};
|
|
4502
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.
|
|
4503
5666
|
focusEditorWhenReady = (selectAll = true) => {
|
|
4504
5667
|
if (!this.gridRoot) return;
|
|
4505
5668
|
// Editor-owns-focus contract: when the CURRENT focus target is a #editor drop-in, the host
|
|
@@ -4539,6 +5702,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4539
5702
|
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(tryFocus);else setTimeout(tryFocus, 0);
|
|
4540
5703
|
};
|
|
4541
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).
|
|
4542
5708
|
columnIdAt = (rowIndex: any, colIndex: any) => {
|
|
4543
5709
|
const rowList = this._rows.value || [];
|
|
4544
5710
|
const row = rowList[rowIndex];
|
|
@@ -4557,6 +5723,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4557
5723
|
return cell ? cell.getValue() : null;
|
|
4558
5724
|
};
|
|
4559
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.
|
|
4560
5731
|
beginEdit = (rowIndex: any, colIndex: any, seed: any) => {
|
|
4561
5732
|
const colId = this.columnIdAt(rowIndex, colIndex);
|
|
4562
5733
|
if (colId == null || !this.columnEditable(colId)) return;
|
|
@@ -4582,6 +5753,12 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4582
5753
|
this.focusEditorWhenReady(seed == null);
|
|
4583
5754
|
};
|
|
4584
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.
|
|
4585
5762
|
focusCellWhenReady = (row: any, col: any) => {
|
|
4586
5763
|
if (!this.gridRoot) return;
|
|
4587
5764
|
let attempts = 0;
|
|
@@ -4598,6 +5775,12 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4598
5775
|
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(tryFocus);else setTimeout(tryFocus, 0);
|
|
4599
5776
|
};
|
|
4600
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).
|
|
4601
5784
|
indexOfRowIn = (rows: any, rowOriginal: any, rowId: any) => {
|
|
4602
5785
|
const list = rows || [];
|
|
4603
5786
|
for (let i = 0; i < list.length; i++) {
|
|
@@ -4609,6 +5792,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4609
5792
|
return -1;
|
|
4610
5793
|
};
|
|
4611
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).
|
|
4612
5798
|
endEdit = () => {
|
|
4613
5799
|
this._editingRow.value = -1;
|
|
4614
5800
|
this._editingCol.value = -1;
|
|
@@ -4619,6 +5805,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4619
5805
|
this._editFocusColId.value = null;
|
|
4620
5806
|
};
|
|
4621
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.
|
|
4622
5811
|
endRowEdit = () => {
|
|
4623
5812
|
this._editingRowIndex.value = null;
|
|
4624
5813
|
this._rowDraft.value = {};
|
|
@@ -4628,6 +5817,12 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4628
5817
|
this._editFocusColId.value = null;
|
|
4629
5818
|
};
|
|
4630
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.
|
|
4631
5826
|
editorAutofocusFor = (colId: any, rowIndex: any) => {
|
|
4632
5827
|
if (this._editVer.value < 0) return false;
|
|
4633
5828
|
if (this._editingRowIndex.value != null) {
|
|
@@ -4638,6 +5833,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4638
5833
|
return this._editFocusColId.value != null && this._editFocusColId.value === colId;
|
|
4639
5834
|
};
|
|
4640
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).
|
|
4641
5841
|
coerceCellValue = (colId: any, raw: any) => {
|
|
4642
5842
|
if (this.editorTypeOf(colId) !== 'number') return raw;
|
|
4643
5843
|
if (raw == null) return null;
|
|
@@ -4648,6 +5848,16 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4648
5848
|
return Number.isNaN(n) ? null : n;
|
|
4649
5849
|
};
|
|
4650
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).
|
|
4651
5861
|
commitEdit = (overrideValue = undefined, skipFocusReturn = false) => {
|
|
4652
5862
|
if (this._editingRow.value < 0) return false;
|
|
4653
5863
|
// Sync idempotency latch (drop-in double cell-edit-commit fix): a second commitEdit call
|
|
@@ -4738,6 +5948,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4738
5948
|
return true;
|
|
4739
5949
|
};
|
|
4740
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).
|
|
4741
5958
|
toggleActiveBooleanCell = () => {
|
|
4742
5959
|
const colId = this.columnIdAt(this._activeRow.value, this._activeColIndex.value);
|
|
4743
5960
|
if (colId == null || !this.columnEditable(colId)) return;
|
|
@@ -4785,6 +6002,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4785
6002
|
};
|
|
4786
6003
|
};
|
|
4787
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.
|
|
4788
6007
|
cancelEdit = () => {
|
|
4789
6008
|
if (this._editingRow.value < 0) return;
|
|
4790
6009
|
// CR-01: capture from the EDITING pair (authoritative), NOT the active-cell indices — a
|
|
@@ -4799,6 +6018,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4799
6018
|
this.focusCellWhenReady(focusRow, focusCol);
|
|
4800
6019
|
};
|
|
4801
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).
|
|
4802
6029
|
editableColumnsForRow = (rowIndex: any) => {
|
|
4803
6030
|
const rowList = this._rows.value || [];
|
|
4804
6031
|
const row = rowList[rowIndex];
|
|
@@ -4823,6 +6050,16 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4823
6050
|
return out;
|
|
4824
6051
|
};
|
|
4825
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.
|
|
4826
6063
|
focusRowEditorAt = (rowIndex: any, colIndex: any) => {
|
|
4827
6064
|
if (!this.gridRoot) return;
|
|
4828
6065
|
const colId = this.columnIdAt(rowIndex, colIndex);
|
|
@@ -4847,6 +6084,12 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4847
6084
|
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(tryFocus);else setTimeout(tryFocus, 0);
|
|
4848
6085
|
};
|
|
4849
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.
|
|
4850
6093
|
beginRowEdit = (row: any) => {
|
|
4851
6094
|
const rowIndex = this.rowIndexOf(row);
|
|
4852
6095
|
if (rowIndex < 0) return;
|
|
@@ -4880,6 +6123,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4880
6123
|
this.focusEditorWhenReady();
|
|
4881
6124
|
};
|
|
4882
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.
|
|
4883
6131
|
commitRow = () => {
|
|
4884
6132
|
if (this._editingRowIndex.value == null) return false;
|
|
4885
6133
|
const rowIndex = this._editingRowIndex.value;
|
|
@@ -4990,6 +6238,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4990
6238
|
return true;
|
|
4991
6239
|
};
|
|
4992
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.
|
|
4993
6243
|
cancelRow = () => {
|
|
4994
6244
|
if (this._editingRowIndex.value == null) return;
|
|
4995
6245
|
const focusRow = this._activeRow.value;
|
|
@@ -5000,6 +6250,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5000
6250
|
this.focusCellWhenReady(focusRow, focusCol);
|
|
5001
6251
|
};
|
|
5002
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).
|
|
5003
6255
|
replaceRowValues = (rows: any, rowIndex: any, fieldValues: any) => {
|
|
5004
6256
|
const src = rows || [];
|
|
5005
6257
|
const fv = fieldValues || {};
|
|
@@ -5019,6 +6271,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5019
6271
|
return out;
|
|
5020
6272
|
};
|
|
5021
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.
|
|
5022
6278
|
nextEditableCell = (fromRow: any, fromCol: any) => {
|
|
5023
6279
|
const rowList = this._rows.value || [];
|
|
5024
6280
|
const rowCount = rowList.length;
|
|
@@ -5043,6 +6299,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5043
6299
|
return null;
|
|
5044
6300
|
};
|
|
5045
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.
|
|
5046
6306
|
prevEditableCell = (fromRow: any, fromCol: any) => {
|
|
5047
6307
|
const rowList = this._rows.value || [];
|
|
5048
6308
|
const rowCount = rowList.length;
|
|
@@ -5071,18 +6331,50 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5071
6331
|
return null;
|
|
5072
6332
|
};
|
|
5073
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).
|
|
5074
6338
|
editTransition = false;
|
|
5075
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).
|
|
5076
6345
|
pendingEditFollow: any = null;
|
|
5077
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).
|
|
5078
6358
|
committedThisSession = false;
|
|
5079
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).
|
|
5080
6367
|
inRowEdit = () => this._editingRowIndex.value != null;
|
|
5081
6368
|
|
|
5082
6369
|
editorValueFor = (colId: any) => this.inRowEdit() ? this._rowDraft.value ? this._rowDraft.value[colId] : null : this._draftValue.value;
|
|
5083
6370
|
|
|
5084
6371
|
editorCheckedFor = (colId: any) => !!(this.inRowEdit() ? this._rowDraft.value ? this._rowDraft.value[colId] : null : this._draftValue.value);
|
|
5085
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.
|
|
5086
6378
|
editorCommitFor = (colId: any) => (value: any) => {
|
|
5087
6379
|
if (this.inRowEdit()) {
|
|
5088
6380
|
this.setRowDraft(colId, value);
|
|
@@ -5099,6 +6391,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5099
6391
|
this.cancelEdit();
|
|
5100
6392
|
};
|
|
5101
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.
|
|
5102
6399
|
onCellEditorInput = (colId: any, evt: any) => {
|
|
5103
6400
|
const v = evt && evt.target ? evt.target.value : '';
|
|
5104
6401
|
if (this.inRowEdit()) {
|
|
@@ -5117,6 +6414,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5117
6414
|
this._draftValue.value = v;
|
|
5118
6415
|
};
|
|
5119
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).
|
|
5120
6419
|
setRowDraft = (colId: any, value: any) => {
|
|
5121
6420
|
const src = this._rowDraft.value || {};
|
|
5122
6421
|
const next = {};
|
|
@@ -5125,6 +6424,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5125
6424
|
this._rowDraft.value = next;
|
|
5126
6425
|
};
|
|
5127
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.
|
|
5128
6431
|
rowEditTab = (target: any, backward: any) => {
|
|
5129
6432
|
const rowIndex = this._editingRowIndex.value;
|
|
5130
6433
|
if (rowIndex == null) return;
|
|
@@ -5146,6 +6449,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5146
6449
|
this.focusRowEditorAt(rowIndex, cols[nextPos]);
|
|
5147
6450
|
};
|
|
5148
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.
|
|
5149
6455
|
onEditorKeyDown = (e: any) => {
|
|
5150
6456
|
if (!e) return;
|
|
5151
6457
|
const key = e.key;
|
|
@@ -5204,6 +6510,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5204
6510
|
}
|
|
5205
6511
|
};
|
|
5206
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.
|
|
5207
6520
|
onEditorBlur = (e: any) => {
|
|
5208
6521
|
// Full-row mode (req-6): a blur that stays WITHIN the row editor — Tab/click between the
|
|
5209
6522
|
// row's OWN fields — is a normal focus move and must NOT commit (a per-cell blur-commit
|
|
@@ -5282,6 +6595,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5282
6595
|
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(reseatDestFocus);else setTimeout(reseatDestFocus, 0);
|
|
5283
6596
|
};
|
|
5284
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.
|
|
5285
6601
|
editCell = (rowIndex: any, colIndex: any) => {
|
|
5286
6602
|
const lastRow = this.bodyRowCount() - 1;
|
|
5287
6603
|
const maxRow = lastRow < 0 ? 0 : lastRow;
|
|
@@ -5296,6 +6612,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5296
6612
|
this.beginEdit(r, c, null);
|
|
5297
6613
|
};
|
|
5298
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.
|
|
5299
6620
|
commitEditing = () => {
|
|
5300
6621
|
if (this.inRowEdit()) {
|
|
5301
6622
|
this.commitRow();
|
|
@@ -5304,6 +6625,12 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5304
6625
|
if (this._editingRow.value >= 0) this.commitEdit(undefined);
|
|
5305
6626
|
};
|
|
5306
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.
|
|
5307
6634
|
editRow = (rowIndex: any) => {
|
|
5308
6635
|
const lastRow = this.bodyRowCount() - 1;
|
|
5309
6636
|
const maxRow = lastRow < 0 ? 0 : lastRow;
|
|
@@ -5316,6 +6643,23 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5316
6643
|
this.beginRowEdit(row);
|
|
5317
6644
|
};
|
|
5318
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.
|
|
5319
6663
|
focusAbsCellWhenReady = (absRow: any, localRow: any, col: any) => {
|
|
5320
6664
|
if (!this.gridRoot) return;
|
|
5321
6665
|
let attempts = 0;
|
|
@@ -5343,6 +6687,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5343
6687
|
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(tryFocus);else setTimeout(tryFocus, 0);
|
|
5344
6688
|
};
|
|
5345
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).
|
|
5346
6697
|
focusCell = (rowIndex: any, colIndex: any) => {
|
|
5347
6698
|
// B16: isGrid()-gate the verb. In 'table' mode there is no roving active cell, so focusCell
|
|
5348
6699
|
// is a NO-OP (never an activecell-change emit) — the keyboard path (onGridKeyDown) is already
|
|
@@ -5410,6 +6761,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5410
6761
|
}
|
|
5411
6762
|
};
|
|
5412
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.
|
|
5413
6773
|
getActiveCell = () => this._activeIsHeader.value ? {
|
|
5414
6774
|
rowIndex: null,
|
|
5415
6775
|
colIndex: this._activeColIndex.value,
|
|
@@ -5420,6 +6780,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5420
6780
|
isHeader: false
|
|
5421
6781
|
};
|
|
5422
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.
|
|
5423
6787
|
clearActiveCell = () => {
|
|
5424
6788
|
if (!this.isGrid()) return;
|
|
5425
6789
|
this._activeIsHeader.value = false;
|
|
@@ -5428,6 +6792,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5428
6792
|
this._activeColIndex.value = 0;
|
|
5429
6793
|
};
|
|
5430
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.
|
|
5431
6804
|
toggleRowExpanded = (rowId: any) => {
|
|
5432
6805
|
if (!this.table) return;
|
|
5433
6806
|
const target = String(rowId);
|
|
@@ -5440,16 +6813,23 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5440
6813
|
}
|
|
5441
6814
|
};
|
|
5442
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).
|
|
5443
6818
|
expandAll = () => {
|
|
5444
6819
|
if (!this.table) return;
|
|
5445
6820
|
this.table.toggleAllRowsExpanded(true);
|
|
5446
6821
|
};
|
|
5447
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.
|
|
5448
6825
|
collapseAll = () => {
|
|
5449
6826
|
if (!this.table) return;
|
|
5450
6827
|
this.table.resetExpanded(true);
|
|
5451
6828
|
};
|
|
5452
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.
|
|
5453
6833
|
getExpandedRows = () => {
|
|
5454
6834
|
if (!this.table) return [];
|
|
5455
6835
|
const out = [];
|
|
@@ -5458,6 +6838,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5458
6838
|
return out;
|
|
5459
6839
|
};
|
|
5460
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.
|
|
5461
6848
|
applyGrouping = (cols: any) => {
|
|
5462
6849
|
if (this.table) this.table.setGrouping(cols);
|
|
5463
6850
|
};
|
|
@@ -5466,6 +6853,20 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5466
6853
|
if (this.table) this.table.setGrouping([]);
|
|
5467
6854
|
};
|
|
5468
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).
|
|
5469
6870
|
getFacetedUniqueValues = (colId: any) => {
|
|
5470
6871
|
if (this.tick() < 0 || !this.table) return [];
|
|
5471
6872
|
const col = this.table.getColumn(colId);
|
|
@@ -5474,6 +6875,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5474
6875
|
return map ? Array.from(map.keys()) : []; // KEYS only — counts deferred (D-03)
|
|
5475
6876
|
};
|
|
5476
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).
|
|
5477
6880
|
getFacetedMinMaxValues = (colId: any) => {
|
|
5478
6881
|
if (this.tick() < 0 || !this.table) return null;
|
|
5479
6882
|
const col = this.table.getColumn(colId);
|