@rozie-ui/data-table-lit 0.2.7 → 0.3.2
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 +3 -2
- package/dist/index.cjs +758 -327
- package/dist/index.d.cts +63 -15
- package/dist/index.d.mts +63 -15
- package/dist/index.mjs +758 -327
- package/package.json +2 -2
- package/src/DataTable.ts +1425 -468
- package/src/helpers/columnDefUtils.ts +111 -0
- package/src/helpers/indexMath.ts +25 -0
- package/src/helpers/rowValueUtils.ts +62 -0
- package/src/helpers/tsvGrid.ts +115 -0
package/src/DataTable.ts
CHANGED
|
@@ -5,6 +5,10 @@ import { RozieSlotDistributor, createLitControllableProperty, rozieAttr, rozieDi
|
|
|
5
5
|
import { ContextProvider, createContext } from '@lit/context';
|
|
6
6
|
import { repeat } from 'lit/directives/repeat.js';
|
|
7
7
|
import '@rozie-ui/popover-lit';
|
|
8
|
+
import { isSafeKey, wrapAggregationFn, indexDefsById, collectGroupableLeafDefs } from './helpers/columnDefUtils';
|
|
9
|
+
import { applyUpdater, clamp, focusables } from './helpers/indexMath';
|
|
10
|
+
import { escapeTsvField, parseTsv, tileGridToBox, tileIndex } from './helpers/tsvGrid';
|
|
11
|
+
import { replaceRowValue, indexOfRowIn, replaceRowValues } from './helpers/rowValueUtils';
|
|
8
12
|
import { createTable, getCoreRowModel, getSortedRowModel, getFilteredRowModel, getPaginationRowModel, getExpandedRowModel, getGroupedRowModel,
|
|
9
13
|
// Faceted filtering (phase 50 reqs 8-9, D-03). All three are supplied UNCONDITIONALLY
|
|
10
14
|
// (mirrors the expand/group models) — inert until a consumer READS a column facet via the
|
|
@@ -101,6 +105,13 @@ export default class DataTable extends SignalWatcher(LitElement) {
|
|
|
101
105
|
font: var(--rdt-font, 14px system-ui, sans-serif);
|
|
102
106
|
color: var(--rdt-color, inherit);
|
|
103
107
|
}
|
|
108
|
+
.rozie-data-table.rdt-col-windowed[data-rozie-s-d5dcab4c] {
|
|
109
|
+
table-layout: fixed;
|
|
110
|
+
}
|
|
111
|
+
.rozie-data-table.rdt-col-windowed[data-rozie-s-d5dcab4c] .rdt-th[data-rozie-s-d5dcab4c],
|
|
112
|
+
.rozie-data-table.rdt-col-windowed[data-rozie-s-d5dcab4c] .rdt-td[data-rozie-s-d5dcab4c] {
|
|
113
|
+
box-sizing: border-box;
|
|
114
|
+
}
|
|
104
115
|
.rdt-sr-live[data-rozie-s-d5dcab4c] {
|
|
105
116
|
position: absolute;
|
|
106
117
|
width: 1px;
|
|
@@ -551,13 +562,17 @@ export default class DataTable extends SignalWatcher(LitElement) {
|
|
|
551
562
|
*/
|
|
552
563
|
@property({ type: Number, reflect: true }) undoLimit: number = 100;
|
|
553
564
|
/**
|
|
554
|
-
* Opt-in vertical
|
|
565
|
+
* Opt-in windowing grammar: `false` (default, off — byte-identical to a non-virtual table) | `true` or `'rows'` (vertical row windowing; `true` is byte-behavior-identical to every existing consumer, zero churn) | `'columns'` (horizontal column windowing) | `'both'` (both axes windowed). Row windowing renders only the visible slice of rows inside a bounded `rdt-scroll` container (with leading/trailing spacer rows preserving total scroll height), windowing over the full filtered + sorted (pre-pagination) model and suppressing the client pagination chrome. Column windowing renders only the visible slice of leaf columns inside the same `rdt-scroll` container. An unrecognised string behaves as `false`.
|
|
555
566
|
*/
|
|
556
|
-
@property({
|
|
567
|
+
@property({ converter: { fromAttribute: (v: string | null) => (v === null ? false : v === 'true' ? true : v === 'false' ? false : v === '' ? true : v) } }) virtual: boolean | string = false;
|
|
557
568
|
/**
|
|
558
|
-
* Estimated row height (px)
|
|
569
|
+
* Estimated row height (px) — the first-paint seed for the windowing engine before any row has been measured. Only consulted when rows are windowed. When `autoMeasure` is `true`, later renders progressively refine the estimate from measured content; when `autoMeasure` is `false` this remains the explicit override for every render.
|
|
559
570
|
*/
|
|
560
571
|
@property({ type: Number, reflect: true }) estimateRowHeight: number = 40;
|
|
572
|
+
/**
|
|
573
|
+
* Opt-in content-driven row-size estimation. When `true`, the windowing engine feeds `estimateSize()` a running mean of measured row heights instead of the fixed `estimateRowHeight` seed, so `getTotalSize()` converges to the true content total on a large table with variable-height rows. Falls back to `estimateRowHeight` before any row has been measured. Default `false` keeps the estimate fixed at `estimateRowHeight` for every render (today's behavior).
|
|
574
|
+
*/
|
|
575
|
+
@property({ type: Boolean, reflect: true }) autoMeasure: boolean = false;
|
|
561
576
|
/**
|
|
562
577
|
* A CSS length string bounding the `rdt-scroll` container when `virtual` is on (e.g. `'400px'`). Mirrored to the `--rozie-data-table-max-height` custom property; the prop wins, the token is the fallback.
|
|
563
578
|
*/
|
|
@@ -957,10 +972,15 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
957
972
|
// into the virtualizer + reconcile IMPERATIVELY here (the table.setOptions re-feed path),
|
|
958
973
|
// NEVER in a render helper (Pitfall 1). Pass the COMPLETE options set (virtual-core's
|
|
959
974
|
// setOptions replaces, not merges). Guarded so the off path executes no virtual-core code.
|
|
960
|
-
if (this.
|
|
975
|
+
if (this.rowsWindowed() && this.virtualizer) {
|
|
961
976
|
this.virtualizer.setOptions(this.virtualizerOptions());
|
|
962
977
|
this.virtualizer._willUpdate();
|
|
963
978
|
}
|
|
979
|
+
// Phase 87 87-04: re-feed the column virtualizer's count/size on the SAME re-derive cycle
|
|
980
|
+
// (a column-visibility/order/pinning change is already in the $watch list below and re-pulls
|
|
981
|
+
// through here) — guarded internally on colsWindowed() && colVirtualizer, so this is a no-op
|
|
982
|
+
// on every other path.
|
|
983
|
+
this.remeasureColumnWindow();
|
|
964
984
|
// D-05: on every data change (re-sort/filter/paginate/page-size — all re-pull here),
|
|
965
985
|
// clamp the active cell to the new bounds (same indices, clamped if the grid shrank;
|
|
966
986
|
// no row-id following, no top-bounce). isGrid()-gated so 'table' mode is untouched.
|
|
@@ -1005,7 +1025,7 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
1005
1025
|
if (this.pendingEditFollow && this.isGrid()) {
|
|
1006
1026
|
const follow = this.pendingEditFollow;
|
|
1007
1027
|
this.pendingEditFollow = null;
|
|
1008
|
-
const followIdx =
|
|
1028
|
+
const followIdx = indexOfRowIn(nextRows, follow.rowOriginal, follow.rowId);
|
|
1009
1029
|
if (followIdx >= 0) this.focusCellWhenReady(followIdx, follow.col);
|
|
1010
1030
|
}
|
|
1011
1031
|
// keep the select-all checkbox's `indeterminate` DOM property in lockstep with the
|
|
@@ -1034,60 +1054,132 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
1034
1054
|
// cell IS the first Tab-in target (matching the Wave-0 probe's "no auto-focus on
|
|
1035
1055
|
// mount"); the consumer drives focus by Tabbing/clicking in, never the component.
|
|
1036
1056
|
|
|
1037
|
-
// ──
|
|
1038
|
-
//
|
|
1039
|
-
//
|
|
1040
|
-
//
|
|
1041
|
-
// the
|
|
1057
|
+
// ── Windowing: capture the scroll element + construct the row virtualizer (req-1/2 — the
|
|
1058
|
+
// row virtual-core instance ONLY when rows are windowed; the scroll-element capture whenever
|
|
1059
|
+
// EITHER axis is windowed, since both axes observe the SAME .rdt-scroll element — D-03). Built
|
|
1060
|
+
// HERE (post-mount) so getScrollElement resolves the rendered .rdt-scroll div and
|
|
1061
|
+
// getPrePaginationRowModel reads the live table. ENTIRELY inside the isWindowed()/rowsWindowed()
|
|
1062
|
+
// guards: when both axes are off, NO virtual-core runtime code executes (byte-identical-off).
|
|
1063
|
+
// _didMount() registers the scroll-element ResizeObserver and returns the teardown stored for
|
|
1064
|
+
// $onUnmount.
|
|
1042
1065
|
// WR-04: NO on-mount auto-focus of the entry cell. Auto-focusing here stole focus on
|
|
1043
1066
|
// page load AND was non-deterministic on React/Solid (the entry cell may not be
|
|
1044
1067
|
// committed to the DOM yet at the $onMount microtask). The roving tabindex="0" entry
|
|
1045
1068
|
// cell IS the first Tab-in target (matching the Wave-0 probe's "no auto-focus on
|
|
1046
1069
|
// mount"); the consumer drives focus by Tabbing/clicking in, never the component.
|
|
1047
1070
|
|
|
1048
|
-
// ──
|
|
1049
|
-
//
|
|
1050
|
-
//
|
|
1051
|
-
//
|
|
1052
|
-
// the
|
|
1053
|
-
|
|
1071
|
+
// ── Windowing: capture the scroll element + construct the row virtualizer (req-1/2 — the
|
|
1072
|
+
// row virtual-core instance ONLY when rows are windowed; the scroll-element capture whenever
|
|
1073
|
+
// EITHER axis is windowed, since both axes observe the SAME .rdt-scroll element — D-03). Built
|
|
1074
|
+
// HERE (post-mount) so getScrollElement resolves the rendered .rdt-scroll div and
|
|
1075
|
+
// getPrePaginationRowModel reads the live table. ENTIRELY inside the isWindowed()/rowsWindowed()
|
|
1076
|
+
// guards: when both axes are off, NO virtual-core runtime code executes (byte-identical-off).
|
|
1077
|
+
// _didMount() registers the scroll-element ResizeObserver and returns the teardown stored for
|
|
1078
|
+
// $onUnmount.
|
|
1079
|
+
if (this.isWindowed()) {
|
|
1054
1080
|
this.gridScrollEl = this._ref__rozieRoot ? this._ref__rozieRoot.querySelector('.rdt-scroll') : null;
|
|
1081
|
+
}
|
|
1082
|
+
if (this.rowsWindowed()) {
|
|
1055
1083
|
this.virtualizer = new Virtualizer(this.virtualizerOptions());
|
|
1056
1084
|
this.virtualizerCleanup = this.virtualizer._didMount();
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1085
|
+
}
|
|
1086
|
+
// Phase 87 87-04: construct the SECOND, horizontal Virtualizer instance when the COLUMN
|
|
1087
|
+
// axis is windowed — entirely inside its own colsWindowed() guard, byte-identical-off for
|
|
1088
|
+
// 'rows'/true/false.
|
|
1089
|
+
// Phase 87 87-04: construct the SECOND, horizontal Virtualizer instance when the COLUMN
|
|
1090
|
+
// axis is windowed — entirely inside its own colsWindowed() guard, byte-identical-off for
|
|
1091
|
+
// 'rows'/true/false.
|
|
1092
|
+
if (this.colsWindowed()) {
|
|
1093
|
+
this.colVirtualizer = new Virtualizer(this.columnVirtualizerOptions());
|
|
1094
|
+
this.colVirtualizerCleanup = this.colVirtualizer._didMount();
|
|
1095
|
+
}
|
|
1096
|
+
// FINE-GRAINED FIRST-WINDOW KICK (Solid/Svelte), either axis: the windowed <For>/{#each}
|
|
1097
|
+
// accessor was first evaluated at initial render — while `virtualizer`/`colVirtualizer` were
|
|
1098
|
+
// still null — and (because windowedRows()/windowedColIndices() read $data.windowVer up top)
|
|
1099
|
+
// subscribed to windowVer then returned []/the full set. Both `let`s are non-reactive, so
|
|
1100
|
+
// their assignment above does NOT notify the accessor; we must bump the SIGNAL it subscribed
|
|
1101
|
+
// to. _didMount() computes the first window synchronously but its onChange only fires on
|
|
1102
|
+
// SUBSEQUENT scroll/resize, so without this explicit bump the first window would never paint
|
|
1103
|
+
// on the fine-grained targets. Idempotent + harmless on the coarse targets (they re-render
|
|
1104
|
+
// wholesale anyway). ONE bump here (not one per axis, Rule 1 — see the ROZ138 note below)
|
|
1105
|
+
// covers both: windowedRows()/windowedColIndices() each simply re-derive against their own
|
|
1106
|
+
// now-non-null virtualizer instance off the SAME counter (87-02 checkpoint decision (b): no
|
|
1107
|
+
// separate $data.colWindowVer). Gated on isWindowed() (either axis) rather than two separate
|
|
1108
|
+
// per-axis bumps: a second `$data.windowVer = $data.windowVer + 1` write later in this SAME
|
|
1109
|
+
// $onMount body would dominate-flag ROZ138 (React stale-read) on its own right-hand-side read
|
|
1110
|
+
// of the counter it had just written a few lines above — a false positive (this increment
|
|
1111
|
+
// idiom lowers to a functional React setState update and is safe regardless of ordering, the
|
|
1112
|
+
// same reasoning virtualizerOptions()'s onChange documents), but collapsing to one bump avoids
|
|
1113
|
+
// manufacturing the false-positive warning at all rather than accepting it.
|
|
1114
|
+
// FINE-GRAINED FIRST-WINDOW KICK (Solid/Svelte), either axis: the windowed <For>/{#each}
|
|
1115
|
+
// accessor was first evaluated at initial render — while `virtualizer`/`colVirtualizer` were
|
|
1116
|
+
// still null — and (because windowedRows()/windowedColIndices() read $data.windowVer up top)
|
|
1117
|
+
// subscribed to windowVer then returned []/the full set. Both `let`s are non-reactive, so
|
|
1118
|
+
// their assignment above does NOT notify the accessor; we must bump the SIGNAL it subscribed
|
|
1119
|
+
// to. _didMount() computes the first window synchronously but its onChange only fires on
|
|
1120
|
+
// SUBSEQUENT scroll/resize, so without this explicit bump the first window would never paint
|
|
1121
|
+
// on the fine-grained targets. Idempotent + harmless on the coarse targets (they re-render
|
|
1122
|
+
// wholesale anyway). ONE bump here (not one per axis, Rule 1 — see the ROZ138 note below)
|
|
1123
|
+
// covers both: windowedRows()/windowedColIndices() each simply re-derive against their own
|
|
1124
|
+
// now-non-null virtualizer instance off the SAME counter (87-02 checkpoint decision (b): no
|
|
1125
|
+
// separate $data.colWindowVer). Gated on isWindowed() (either axis) rather than two separate
|
|
1126
|
+
// per-axis bumps: a second `$data.windowVer = $data.windowVer + 1` write later in this SAME
|
|
1127
|
+
// $onMount body would dominate-flag ROZ138 (React stale-read) on its own right-hand-side read
|
|
1128
|
+
// of the counter it had just written a few lines above — a false positive (this increment
|
|
1129
|
+
// idiom lowers to a functional React setState update and is safe regardless of ordering, the
|
|
1130
|
+
// same reasoning virtualizerOptions()'s onChange documents), but collapsing to one bump avoids
|
|
1131
|
+
// manufacturing the false-positive warning at all rather than accepting it.
|
|
1132
|
+
if (this.isWindowed()) {
|
|
1066
1133
|
this._windowVer.value = this._windowVer.value + 1;
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1134
|
+
}
|
|
1135
|
+
// After the first window commits (next frame), refine heights + fire the dev-mode warns ONCE.
|
|
1136
|
+
// Entirely inside the isWindowed() guard so the off (neither axis windowed) emitted path adds
|
|
1137
|
+
// NO code and these warns can never fire there (req-1 byte-identical-off preserved). The
|
|
1138
|
+
// row-specific measurement + warns stay further guarded by rowsWindowed() (byte-identical to
|
|
1139
|
+
// before this restructure); the column-axis warn (Phase 87 D-07(a)) is guarded by
|
|
1140
|
+
// colsWindowed() so it can fire ONLY on the 'columns'/'both' path, never on 'rows'/true/false.
|
|
1141
|
+
// After the first window commits (next frame), refine heights + fire the dev-mode warns ONCE.
|
|
1142
|
+
// Entirely inside the isWindowed() guard so the off (neither axis windowed) emitted path adds
|
|
1143
|
+
// NO code and these warns can never fire there (req-1 byte-identical-off preserved). The
|
|
1144
|
+
// row-specific measurement + warns stay further guarded by rowsWindowed() (byte-identical to
|
|
1145
|
+
// before this restructure); the column-axis warn (Phase 87 D-07(a)) is guarded by
|
|
1146
|
+
// colsWindowed() so it can fire ONLY on the 'columns'/'both' path, never on 'rows'/true/false.
|
|
1147
|
+
if (this.isWindowed()) {
|
|
1070
1148
|
const afterFirstFrame = () => {
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1149
|
+
if (this.rowsWindowed()) {
|
|
1150
|
+
// D-10: measure the rendered rows.
|
|
1151
|
+
this.remeasureWindow();
|
|
1152
|
+
// D-08/A1: a dev-mode runtime warn when the scroll container has no bounded height (the
|
|
1153
|
+
// bound may come from consumer CSS the compiler can't see — no compile diagnostic). No
|
|
1154
|
+
// process.env guard (not bundler-portable); always-warn-on-misconfig is acceptable.
|
|
1155
|
+
const h = this.gridScrollEl ? this.gridScrollEl.clientHeight : 0;
|
|
1156
|
+
if (!h) {
|
|
1157
|
+
console.warn('[rozie-data-table] virtual is on but the scroll container has no bounded height; set maxHeight or --rozie-data-table-max-height');
|
|
1158
|
+
}
|
|
1159
|
+
// D-07 (RESOLVED — runtime warn, not a compile diagnostic): warn ONCE when the consumer
|
|
1160
|
+
// CONFIGURED client pagination alongside virtual, in the non-manual case (the valid
|
|
1161
|
+
// virtual+manual combo per D-09 is silent). The pagination prop carries a non-null default
|
|
1162
|
+
// ({ pageIndex: 0, pageSize: 10 }) so it is never strictly null — "configured" is therefore
|
|
1163
|
+
// detected as a pagination that DIFFERS from that default (a consumer who set a real page
|
|
1164
|
+
// size / index). The uncontrolled default ({0,10}) does NOT trip the warn. Behavior + the
|
|
1165
|
+
// virtual=false path are untouched (rowsWindowed()-guarded, as before this restructure).
|
|
1166
|
+
const pg = this.pagination;
|
|
1167
|
+
const pgConfigured = pg != null && !(pg.pageIndex === 0 && pg.pageSize === 10);
|
|
1168
|
+
if (this.manual !== true && pgConfigured) {
|
|
1169
|
+
console.warn('[rozie-data-table] virtual+pagination: client pagination is configured but virtual windowing replaces it — the pagination chrome is auto-suppressed. Remove the pagination prop or set manual to silence this.');
|
|
1170
|
+
}
|
|
1079
1171
|
}
|
|
1080
|
-
// D-07
|
|
1081
|
-
//
|
|
1082
|
-
//
|
|
1083
|
-
//
|
|
1084
|
-
//
|
|
1085
|
-
//
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1172
|
+
// Phase 87 Task 1(a) (D-07/D-08 precedent, APPROVED): a dev-mode runtime warn when the
|
|
1173
|
+
// column axis is windowed but the scroll container has no bounded width — the horizontal
|
|
1174
|
+
// failure mode is worse than the vertical one (an unbounded width renders every column
|
|
1175
|
+
// with no scrollbar, so the feature silently does nothing and there is no compile
|
|
1176
|
+
// diagnostic possible, since the bound may come from consumer CSS). Guarded by
|
|
1177
|
+
// colsWindowed() so it can NEVER fire on the 'rows'/true/false path.
|
|
1178
|
+
if (this.colsWindowed()) {
|
|
1179
|
+
const w = this.gridScrollEl ? this.gridScrollEl.clientWidth : 0;
|
|
1180
|
+
if (!w) {
|
|
1181
|
+
console.warn('[rozie-data-table] virtual is on for columns but the scroll container has no bounded width; set a CSS width on an ancestor so the column window can be measured');
|
|
1182
|
+
}
|
|
1091
1183
|
}
|
|
1092
1184
|
};
|
|
1093
1185
|
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(() => requestAnimationFrame(afterFirstFrame));else setTimeout(afterFirstFrame, 0);
|
|
@@ -1129,6 +1221,17 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
1129
1221
|
this._rozieTornDown = true;
|
|
1130
1222
|
() => {
|
|
1131
1223
|
if (this.virtualizerCleanup) this.virtualizerCleanup();
|
|
1224
|
+
// Phase 87 87-04: tear down the column virtualizer's scroll-element ResizeObserver too.
|
|
1225
|
+
// No-op when column windowing was off (cleanup stays null).
|
|
1226
|
+
if (this.colVirtualizerCleanup) this.colVirtualizerCleanup();
|
|
1227
|
+
// Gap-closure 87-09: disconnect the RTL dir-attribute MutationObserver (ensureColRtlWatch(),
|
|
1228
|
+
// windowing.rzts). No-op if column windowing was never on (the observer was never attached).
|
|
1229
|
+
this.teardownColRtlWatch();
|
|
1230
|
+
// Gap-closure 87-13 (CR-01): cancel a deferred remeasure sweep still pending at unmount.
|
|
1231
|
+
// Must run here — remeasureWindow()'s `!virtualizer || !gridRoot` guard never fires, since
|
|
1232
|
+
// neither ref is nulled — or the pass reaches 87-07's refineRowEstimate() and writes
|
|
1233
|
+
// $data.windowVer against a torn-down instance.
|
|
1234
|
+
this.teardownRemeasure();
|
|
1132
1235
|
// CR-04: remove any live fill-drag document listeners if we unmount mid-drag.
|
|
1133
1236
|
this.teardownFillDrag();
|
|
1134
1237
|
// §6 (260709-3qt): remove any live drag-select document listeners on a mid-drag unmount.
|
|
@@ -1180,82 +1283,86 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1180
1283
|
${this.groupBar !== undefined ? this.groupBar({grouping: this.groupingKeys(), groupableColumns: this.groupableColumns(), applyGrouping: this.applyGrouping, clearGrouping: this.clearGrouping}) : html`<slot name="groupBar" data-rozie-params=${(() => { try { return JSON.stringify({grouping: this.groupingKeys(), groupableColumns: this.groupableColumns()}); } catch { return '{}'; } })()} @rozie-group-bar-apply-grouping=${($event: CustomEvent) => ((this.applyGrouping) as (...args: any[]) => any)($event.detail)} @rozie-group-bar-clear-grouping=${($event: CustomEvent) => ((this.clearGrouping) as (...args: any[]) => any)($event.detail)}>
|
|
1181
1284
|
${repeat<any>(this.groupingKeys(), (gk, _idx) => gk, (gk, _idx) => html`<span class="rdt-group-token" data-group-token="" data-rozie-s-d5dcab4c>${rozieDisplay(gk)}</span>`)}
|
|
1182
1285
|
</slot>`}
|
|
1183
|
-
</div>` : nothing}${this.
|
|
1184
|
-
<table class="${Object.entries({ "rozie-data-table": true, 'rdt-sticky': this.stickyHeader }).filter(([, v]) => v).map(([k]) => k).join(' ')}" role=${rozieAttr(this.tableRole())} aria-rowcount=${rozieAttr(this.gridAriaRowCount())} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridKeyDown($event); }} @focusin=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.syncActiveFromEvent($event); }} @focusout=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridFocusOut($event); }} @mousedown=${($event: MouseEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridMouseDown($event); }} @dblclick=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridDblClick($event); }} @click=${($event: MouseEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridClick($event); }} data-rozie-s-d5dcab4c>
|
|
1286
|
+
</div>` : nothing}${this.isWindowed() ? html`<div class="rdt-scroll" style=${rozieStyle(this.rowsWindowed() && this.maxHeight ? 'max-height:' + this.maxHeight + ';overflow:auto;--rozie-data-table-max-height:' + this.maxHeight : 'overflow:auto')} data-rozie-s-d5dcab4c>
|
|
1287
|
+
<table class="${Object.entries({ "rozie-data-table": true, 'rdt-sticky': this.stickyHeader, 'rdt-col-windowed': this.colsWindowed() }).filter(([, v]) => v).map(([k]) => k).join(' ')}" role=${rozieAttr(this.tableRole())} aria-rowcount=${rozieAttr(this.gridAriaRowCount())} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridKeyDown($event); }} @focusin=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.syncActiveFromEvent($event); }} @focusout=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridFocusOut($event); }} @mousedown=${($event: MouseEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridMouseDown($event); }} @dblclick=${($event: Event & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridDblClick($event); }} @click=${($event: MouseEvent & { currentTarget: HTMLTableElement; target: HTMLTableElement }) => { this.onGridClick($event); }} data-rozie-s-d5dcab4c>
|
|
1185
1288
|
<thead class="rdt-thead" role="rowgroup" data-rozie-s-d5dcab4c>
|
|
1186
1289
|
${repeat<any>(this._headerGroups.value, (hg, hgLevel) => hg.id, (hg, hgLevel) => html`<tr class="rdt-tr" role="row" aria-rowindex=${rozieAttr(hgLevel + 1)} data-rozie-s-d5dcab4c>
|
|
1187
|
-
|
|
1188
|
-
|
|
1290
|
+
|
|
1291
|
+
${this.colsWindowed() ? html`<th class="rdt-col-spacer" aria-hidden="true" style=${rozieStyle('width:' + this.colPadLeft() + 'px;padding:0;border:0')} data-rozie-s-d5dcab4c></th>` : nothing}${repeat<any>(this.windowedHeadersFor(hg, hgLevel), (wh, _idx) => wh.header.id, (wh, _idx) => html`<th class="${Object.entries({ "rdt-th": true, 'rdt-select-th': this.isSelectColumn(wh.header.column.id), 'rdt-expander-th': this.isExpanderColumn(wh.header.column.id), 'rdt-th-resizing': this.columnIsResizing(wh.header.column.id), 'rdt-cell-active': this.isActiveCell('__header', this.headerColIndexOf(hg, wh.header), hgLevel) }).filter(([, v]) => v).map(([k]) => k).join(' ')}" role="columnheader" data-col=${rozieAttr(wh.header.column.id)} data-grid-cell="" data-row="__header" data-header-level=${rozieAttr(hgLevel)} colspan=${rozieAttr(wh.span > 1 ? wh.span : null)} data-col-index=${rozieAttr(this.headerColIndexOf(hg, wh.header))} tabindex=${rozieAttr(this.cellTabindex('__header', this.headerColIndexOf(hg, wh.header), hgLevel))} aria-sort=${rozieAttr(this.ariaSortFor(wh.header.column.id))} style=${rozieStyle(this.thStyle(wh.header.column.id))} data-rozie-s-d5dcab4c>
|
|
1292
|
+
${this.isSelectColumn(wh.header.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
1189
1293
|
${this.selectAll !== undefined ? this.selectAll({checked: this.isAllRowsSelected(), indeterminate: this.isSomeRowsSelected(), toggle: this.onToggleAllRows}) : html`<slot name="selectAll" data-rozie-params=${(() => { try { return JSON.stringify({checked: this.isAllRowsSelected(), indeterminate: this.isSomeRowsSelected()}); } catch { return '{}'; } })()} @rozie-select-all-toggle=${($event: CustomEvent) => ((this.onToggleAllRows) as (...args: any[]) => any)($event.detail)}>
|
|
1190
1294
|
${this.selectionMode === 'multiple' ? html`<input class="rdt-select-all" type="checkbox" aria-label="Select all rows" ?checked=${this.isAllRowsSelected()} @change=${($event: Event & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onToggleAllRows($event); }} data-rozie-s-d5dcab4c />` : nothing}</slot>`}
|
|
1191
|
-
</span>` : this.isExpanderColumn(header.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c></span>` : html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
1192
|
-
${header.column.getCanSort && header.column.getCanSort() ? html`<button class="rdt-sort-btn" type="button" @click=${($event: MouseEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.onHeaderSort(header.column.id, $event); }} data-rozie-s-d5dcab4c>
|
|
1295
|
+
</span>` : this.isExpanderColumn(wh.header.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c></span>` : html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
1296
|
+
${wh.header.column.getCanSort && wh.header.column.getCanSort() ? html`<button class="rdt-sort-btn" type="button" @click=${($event: MouseEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.onHeaderSort(wh.header.column.id, $event); }} data-rozie-s-d5dcab4c>
|
|
1193
1297
|
<span class="rdt-header-label" data-rozie-s-d5dcab4c>
|
|
1194
|
-
${this.colHeader !== undefined ? this.colHeader({columnId: header.column.id, column: header.column, label: this.headerLabel(header.column.id)}) : html`<slot name="colHeader" data-rozie-params=${(() => { try { return JSON.stringify({columnId: header.column.id, column: header.column, label: this.headerLabel(header.column.id)}); } catch { return '{}'; } })()}>${rozieDisplay(this.headerLabel(header.column.id))}</slot>`}
|
|
1298
|
+
${this.colHeader !== undefined ? this.colHeader({columnId: wh.header.column.id, column: wh.header.column, label: this.headerLabel(wh.header.column.id)}) : html`<slot name="colHeader" data-rozie-params=${(() => { try { return JSON.stringify({columnId: wh.header.column.id, column: wh.header.column, label: this.headerLabel(wh.header.column.id)}); } catch { return '{}'; } })()}>${rozieDisplay(this.headerLabel(wh.header.column.id))}</slot>`}
|
|
1195
1299
|
</span>
|
|
1196
|
-
<span class="rdt-sort-ind" aria-hidden="true" data-rozie-s-d5dcab4c>${rozieDisplay(this.sortIndicator(header.column.id))}</span>
|
|
1300
|
+
<span class="rdt-sort-ind" aria-hidden="true" data-rozie-s-d5dcab4c>${rozieDisplay(this.sortIndicator(wh.header.column.id))}</span>
|
|
1197
1301
|
</button>` : html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
1198
1302
|
<span class="rdt-header-label" data-rozie-s-d5dcab4c>
|
|
1199
|
-
${this.colHeader !== undefined ? this.colHeader({columnId: header.column.id, column: header.column, label: this.headerLabel(header.column.id)}) : html`<slot name="colHeader" data-rozie-params=${(() => { try { return JSON.stringify({columnId: header.column.id, column: header.column, label: this.headerLabel(header.column.id)}); } catch { return '{}'; } })()}>${rozieDisplay(this.headerLabel(header.column.id))}</slot>`}
|
|
1303
|
+
${this.colHeader !== undefined ? this.colHeader({columnId: wh.header.column.id, column: wh.header.column, label: this.headerLabel(wh.header.column.id)}) : html`<slot name="colHeader" data-rozie-params=${(() => { try { return JSON.stringify({columnId: wh.header.column.id, column: wh.header.column, label: this.headerLabel(wh.header.column.id)}); } catch { return '{}'; } })()}>${rozieDisplay(this.headerLabel(wh.header.column.id))}</slot>`}
|
|
1200
1304
|
</span>
|
|
1201
|
-
</span>`}<rozie-popover trigger="click" placement="bottom-end" strategy="fixed" .offset=${4} data-rozie-s-d5dcab4c><button class="rdt-col-menu-trigger" type="button" aria-label=${rozieAttr('Column options for ' + this.headerLabel(header.column.id))} data-rozie-s-d5dcab4c slot="anchor">⋯</button><div class="rdt-col-menu" role="menu" data-rozie-s-d5dcab4c>
|
|
1202
|
-
<button class="rdt-col-menu-item" type="button" role="menuitem" aria-pressed=${this.columnPinSide(header.column.id) === 'left'} @click=${($event: MouseEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.onPinColumn(header.column.id, 'left', $event); }} data-rozie-s-d5dcab4c>Pin left</button>
|
|
1203
|
-
<button class="rdt-col-menu-item" type="button" role="menuitem" aria-pressed=${this.columnPinSide(header.column.id) === 'right'} @click=${($event: MouseEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.onPinColumn(header.column.id, 'right', $event); }} data-rozie-s-d5dcab4c>Pin right</button>
|
|
1204
|
-
<button class="rdt-col-menu-item" type="button" role="menuitem" aria-pressed=${!this.columnPinSide(header.column.id)} @click=${($event: MouseEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.onPinColumn(header.column.id, false, $event); }} data-rozie-s-d5dcab4c>Unpin</button>
|
|
1305
|
+
</span>`}<rozie-popover trigger="click" placement="bottom-end" strategy="fixed" .offset=${4} data-rozie-s-d5dcab4c><button class="rdt-col-menu-trigger" type="button" aria-label=${rozieAttr('Column options for ' + this.headerLabel(wh.header.column.id))} data-rozie-s-d5dcab4c slot="anchor">⋯</button><div class="rdt-col-menu" role="menu" data-rozie-s-d5dcab4c>
|
|
1306
|
+
<button class="rdt-col-menu-item" type="button" role="menuitem" aria-pressed=${this.columnPinSide(wh.header.column.id) === 'left'} @click=${($event: MouseEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.onPinColumn(wh.header.column.id, 'left', $event); }} data-rozie-s-d5dcab4c>Pin left</button>
|
|
1307
|
+
<button class="rdt-col-menu-item" type="button" role="menuitem" aria-pressed=${this.columnPinSide(wh.header.column.id) === 'right'} @click=${($event: MouseEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.onPinColumn(wh.header.column.id, 'right', $event); }} data-rozie-s-d5dcab4c>Pin right</button>
|
|
1308
|
+
<button class="rdt-col-menu-item" type="button" role="menuitem" aria-pressed=${!this.columnPinSide(wh.header.column.id)} @click=${($event: MouseEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.onPinColumn(wh.header.column.id, false, $event); }} data-rozie-s-d5dcab4c>Unpin</button>
|
|
1205
1309
|
<hr class="rdt-col-menu-sep" data-rozie-s-d5dcab4c />
|
|
1206
|
-
<button class="rdt-col-menu-item" type="button" role="menuitem" @click=${($event: MouseEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.onHideColumn(header.column.id, $event); }} data-rozie-s-d5dcab4c>Hide column</button>
|
|
1310
|
+
<button class="rdt-col-menu-item" type="button" role="menuitem" @click=${($event: MouseEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.onHideColumn(wh.header.column.id, $event); }} data-rozie-s-d5dcab4c>Hide column</button>
|
|
1207
1311
|
</div></rozie-popover>
|
|
1208
|
-
<button class="rdt-resize-handle" type="button" aria-label=${rozieAttr('Resize ' + this.headerLabel(header.column.id))} @pointerdown=${($event: PointerEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.onResizeStart(header.column.id, $event); }} @touchstart=${($event: TouchEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.onResizeStart(header.column.id, $event); }} data-rozie-s-d5dcab4c><span class="rdt-resize-grip" aria-hidden="true" data-rozie-s-d5dcab4c></span></button>
|
|
1312
|
+
<button class="rdt-resize-handle" type="button" aria-label=${rozieAttr('Resize ' + this.headerLabel(wh.header.column.id))} @pointerdown=${($event: PointerEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.onResizeStart(wh.header.column.id, $event); }} @touchstart=${($event: TouchEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.onResizeStart(wh.header.column.id, $event); }} data-rozie-s-d5dcab4c><span class="rdt-resize-grip" aria-hidden="true" data-rozie-s-d5dcab4c></span></button>
|
|
1209
1313
|
</span>`}</th>`)}
|
|
1210
|
-
|
|
1314
|
+
|
|
1315
|
+
${this.colsWindowed() ? html`<th class="rdt-col-spacer" aria-hidden="true" style=${rozieStyle('width:' + this.colPadRight() + 'px;padding:0;border:0')} data-rozie-s-d5dcab4c></th>` : nothing}</tr>`)}
|
|
1211
1316
|
|
|
1212
1317
|
${this.hasAnyFilterableColumn() ? html`<tr class="rdt-filter-row" data-rozie-s-d5dcab4c>
|
|
1213
|
-
${repeat<any>(this._headerGroups.value[this._headerGroups.value.length - 1].
|
|
1214
|
-
${this.isSelectColumn(header.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c></span>` : this.isExpanderColumn(header.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c></span>` : html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
1215
|
-
${this.columnIsFilterable(header.column.id) && !this.hasFilterSlot() ? html`<input class="rdt-col-filter" type="text" aria-label=${rozieAttr('Filter ' + this.headerLabel(header.column.id))} .value=${this.columnFilterValue(header.column.id)} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onColumnFilterInput(header.column.id, $event); }} @click=${($event: MouseEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.stopEvent($event); }} data-rozie-s-d5dcab4c />` : nothing}${this.columnIsFilterable(header.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
1216
|
-
${this.filter !== undefined ? this.filter({columnId: header.column.id, value: this.columnFilterValue(header.column.id), uniqueValues: this.getFacetedUniqueValues(header.column.id), minMax: this.getFacetedMinMaxValues(header.column.id), setFilter: this.setColumnFilter}) : html`<slot name="filter" data-rozie-params=${(() => { try { return JSON.stringify({columnId: header.column.id, value: this.columnFilterValue(header.column.id), uniqueValues: this.getFacetedUniqueValues(header.column.id), minMax: this.getFacetedMinMaxValues(header.column.id)}); } catch { return '{}'; } })()} @rozie-filter-set-filter=${($event: CustomEvent) => ((this.setColumnFilter) as (...args: any[]) => any)($event.detail)}></slot>`}
|
|
1318
|
+
${this.colsWindowed() ? html`<th class="rdt-col-spacer" aria-hidden="true" style=${rozieStyle('width:' + this.colPadLeft() + 'px;padding:0;border:0')} data-rozie-s-d5dcab4c></th>` : nothing}${repeat<any>(this.windowedHeadersFor(this._headerGroups.value[this._headerGroups.value.length - 1], this._headerGroups.value.length - 1), (wh, _idx) => wh.header.id, (wh, _idx) => html`<th class="rdt-filter-cell" role="presentation" data-col=${rozieAttr(wh.header.column.id)} style=${rozieStyle(this.pinStyle(wh.header.column.id))} data-rozie-s-d5dcab4c>
|
|
1319
|
+
${this.isSelectColumn(wh.header.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c></span>` : this.isExpanderColumn(wh.header.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c></span>` : html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
1320
|
+
${this.columnIsFilterable(wh.header.column.id) && !this.hasFilterSlot() ? html`<input class="rdt-col-filter" type="text" aria-label=${rozieAttr('Filter ' + this.headerLabel(wh.header.column.id))} .value=${this.columnFilterValue(wh.header.column.id)} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onColumnFilterInput(wh.header.column.id, $event); }} @click=${($event: MouseEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.stopEvent($event); }} data-rozie-s-d5dcab4c />` : nothing}${this.columnIsFilterable(wh.header.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
1321
|
+
${this.filter !== undefined ? this.filter({columnId: wh.header.column.id, value: this.columnFilterValue(wh.header.column.id), uniqueValues: this.getFacetedUniqueValues(wh.header.column.id), minMax: this.getFacetedMinMaxValues(wh.header.column.id), setFilter: this.setColumnFilter}) : html`<slot name="filter" data-rozie-params=${(() => { try { return JSON.stringify({columnId: wh.header.column.id, value: this.columnFilterValue(wh.header.column.id), uniqueValues: this.getFacetedUniqueValues(wh.header.column.id), minMax: this.getFacetedMinMaxValues(wh.header.column.id)}); } catch { return '{}'; } })()} @rozie-filter-set-filter=${($event: CustomEvent) => ((this.setColumnFilter) as (...args: any[]) => any)($event.detail)}></slot>`}
|
|
1217
1322
|
</span>` : nothing}</span>`}</th>`)}
|
|
1218
|
-
|
|
1323
|
+
${this.colsWindowed() ? html`<th class="rdt-col-spacer" aria-hidden="true" style=${rozieStyle('width:' + this.colPadRight() + 'px;padding:0;border:0')} data-rozie-s-d5dcab4c></th>` : nothing}</tr>` : nothing}</thead>
|
|
1219
1324
|
|
|
1220
1325
|
<tbody class="rdt-tbody" role="rowgroup" data-rozie-s-d5dcab4c>
|
|
1221
1326
|
|
|
1222
1327
|
<tr class="rdt-spacer" aria-hidden="true" data-rozie-s-d5dcab4c>
|
|
1223
|
-
<td colspan=${rozieAttr(this.
|
|
1328
|
+
<td colspan=${rozieAttr(this.windowedColSpan())} style=${rozieStyle('height:' + this.padTop() + 'px;padding:0;border:0')} data-rozie-s-d5dcab4c></td>
|
|
1224
1329
|
</tr>
|
|
1225
1330
|
|
|
1226
1331
|
${repeat<any>(this.windowedRows(), (wr, _idx) => wr.row.id, (wr, _idx) => html`
|
|
1227
1332
|
<tr class="${Object.entries({ "rdt-tr": true, 'rdt-group-header': this.rowIsGrouped(wr.row), 'rdt-row-pinned': wr.pinned }).filter(([, v]) => v).map(([k]) => k).join(' ')}" role="row" data-row=${rozieAttr(wr.vi.index)} aria-rowindex=${rozieAttr(this.headerRowCount() + wr.vi.index + 1)} data-index=${rozieAttr(wr.vi.index)} data-pinned=${rozieAttr(wr.pinned ? 'true' : null)} data-depth=${rozieAttr(wr.row.depth)} data-group-header=${rozieAttr(this.rowIsGrouped(wr.row) ? wr.row.id : null)} data-group-leaf=${rozieAttr(this.groupingActive() && !this.rowIsGrouped(wr.row) ? wr.row.id : null)} aria-expanded=${rozieAttr(this.rowIsGrouped(wr.row) ? !!this.rowIsExpanded(wr.row) : null)} aria-selected=${rozieAttr(this.selectionMode !== 'none' ? !!this.rowIsSelected(wr.row) : null)} aria-level=${rozieAttr(this.groupingActive() ? wr.row.depth + 1 : null)} data-rozie-s-d5dcab4c>
|
|
1228
|
-
|
|
1333
|
+
|
|
1334
|
+
${this.colsWindowed() ? html`<td class="rdt-col-spacer" aria-hidden="true" style=${rozieStyle('width:' + this.colPadLeft() + 'px;padding:0;border:0')} data-rozie-s-d5dcab4c></td>` : nothing}${repeat<any>(this.windowedCells(wr.row), (cell, _idx) => cell.id, (cell, _idx) => html`<td class="${Object.entries({ "rdt-td": true, 'rdt-select-td': this.isSelectColumn(cell.column.id), 'rdt-expander-td': this.isExpanderColumn(cell.column.id), 'rdt-in-range': this.inRange(wr.vi.index, this.colIndexOf(wr.row, cell)), 'rdt-cell-active': this.isActiveCell(String(wr.vi.index), this.colIndexOf(wr.row, cell)) }).filter(([, v]) => v).map(([k]) => k).join(' ')}" role=${rozieAttr(this.cellRole())} data-col=${rozieAttr(cell.column.id)} data-grid-cell="" data-row=${rozieAttr(wr.vi.index)} data-col-index=${rozieAttr(this.colIndexOf(wr.row, cell))} tabindex=${rozieAttr(this.cellTabindex(String(wr.vi.index), this.colIndexOf(wr.row, cell)))} style=${rozieStyle(this.bodyCellStyle(wr.row, cell.column.id))} aria-invalid=${rozieAttr(this.cellAriaInvalid(wr.vi.index, this.colIndexOf(wr.row, cell)))} data-in-range=${rozieAttr(this.inRange(wr.vi.index, this.colIndexOf(wr.row, cell)) ? 'true' : null)} data-agg-cell=${rozieAttr(this.cellIsAggregated(cell) ? cell.column.id : null)} data-rozie-s-d5dcab4c>
|
|
1229
1335
|
|
|
1230
1336
|
${this.isExpanderColumn(cell.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
1231
1337
|
${this.rowCanExpand(wr.row) ? html`<button class="rdt-expander" type="button" data-expander="" aria-expanded=${!!this.rowIsExpanded(wr.row)} aria-label=${rozieAttr(this.rowIsExpanded(wr.row) ? 'Collapse row' : 'Expand row')} @click=${($event: MouseEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.onToggleExpand(wr.row, $event); }} data-rozie-s-d5dcab4c>${rozieDisplay(this.rowIsExpanded(wr.row) ? '▾' : '▸')}</button>` : nothing}</span>` : this.isSelectColumn(cell.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
1232
|
-
${this.selectCell !== undefined ? this.selectCell({row: wr.row
|
|
1338
|
+
${this.selectCell !== undefined ? this.selectCell({row: this.cellSlotRow(wr.row), checked: this.rowIsSelected(wr.row), toggle: e => this.onToggleRow(wr.row, e)}) : html`<slot name="selectCell" data-rozie-params=${(() => { try { return JSON.stringify({row: this.cellSlotRow(wr.row), checked: this.rowIsSelected(wr.row)}); } catch { return '{}'; } })()} @rozie-select-cell-toggle=${($event: CustomEvent) => ((e => this.onToggleRow(wr.row, e)) as (...args: any[]) => any)($event.detail)}>
|
|
1233
1339
|
<input class="rdt-select-row" type="checkbox" aria-label="Select row" ?checked=${this.rowIsSelected(wr.row)} @change=${($event: Event & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onToggleRow(wr.row, $event); }} data-rozie-s-d5dcab4c />
|
|
1234
1340
|
</slot>`}
|
|
1235
1341
|
</span>` : this.cellIsGrouped(cell) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
1236
1342
|
<button class="rdt-expander rdt-group-toggle" type="button" data-expander="" aria-expanded=${!!this.rowIsExpanded(wr.row)} aria-label=${rozieAttr(this.rowIsExpanded(wr.row) ? 'Collapse group' : 'Expand group')} @click=${($event: MouseEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.onToggleExpand(wr.row, $event); }} data-rozie-s-d5dcab4c>${rozieDisplay(this.rowIsExpanded(wr.row) ? '▾' : '▸')}</button>
|
|
1237
1343
|
<span class="rdt-group-value" data-rozie-s-d5dcab4c>
|
|
1238
|
-
${this.cell !== undefined ? this.cell({columnId: cell.column.id, column: cell.column, row: wr.row
|
|
1344
|
+
${this.cell !== undefined ? this.cell({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: cell.getValue()}) : html`<slot name="cell" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: cell.getValue()}); } catch { return '{}'; } })()}>${rozieDisplay(cell.getValue())}</slot>`}
|
|
1239
1345
|
</span>
|
|
1240
1346
|
<span class="rdt-group-count" data-rozie-s-d5dcab4c>${rozieDisplay('(' + this.groupSubRowCount(wr.row) + ')')}</span>
|
|
1241
1347
|
</span>` : this.isEditing(wr.vi.index, this.colIndexOf(wr.row, cell)) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
1242
1348
|
${this.hasEditorSlot(cell.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
1243
|
-
${this.editor !== undefined ? this.editor({columnId: cell.column.id, column: cell.column, row: wr.row
|
|
1349
|
+
${this.editor !== undefined ? this.editor({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index)}) : html`<slot name="editor" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index)}); } catch { return '{}'; } })()}></slot>`}
|
|
1244
1350
|
</span>` : this.editorTypeOf(cell.column.id) === 'number' ? html`<input class="rdt-cell-editor" type="number" data-editing-cell="" .value=${this.editorValueFor(cell.column.id)} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />` : this.editorTypeOf(cell.column.id) === 'select' ? html`<select class="rdt-cell-editor" data-editing-cell="" .value=${this.editorValueFor(cell.column.id)} @change=${($event: Event & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c>
|
|
1245
1351
|
${repeat<any>(this.editorOptionsOf(cell.column.id), (opt, _idx) => opt.value, (opt, _idx) => html`<option value=${rozieAttr(opt.value)} data-rozie-s-d5dcab4c>${rozieDisplay(opt.label)}</option>`)}
|
|
1246
1352
|
</select>` : this.editorTypeOf(cell.column.id) === 'checkbox' ? html`<input class="rdt-cell-editor" type="checkbox" data-editing-cell="" ?checked=${this.editorCheckedFor(cell.column.id)} @change=${($event: Event & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorCheckbox(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />` : html`<input class="rdt-cell-editor" type="text" data-editing-cell="" .value=${this.editorValueFor(cell.column.id)} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />`}</span>` : this.cellIsPlaceholder(cell) ? html`<span style="display:contents" data-rozie-s-d5dcab4c></span>` : html`<span class="rdt-cell-value" data-rozie-s-d5dcab4c>
|
|
1247
|
-
${this.cell !== undefined ? this.cell({columnId: cell.column.id, column: cell.column, row: wr.row
|
|
1353
|
+
${this.cell !== undefined ? this.cell({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: cell.getValue()}) : html`<slot name="cell" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: cell.getValue()}); } catch { return '{}'; } })()}>${rozieDisplay(cell.getValue())}</slot>`}
|
|
1248
1354
|
</span>`}${this.isFillHandleCell(wr.vi.index, this.colIndexOf(wr.row, cell)) ? html`<span class="rdt-fill-handle" data-fill-handle="" data-testid="fill-handle" aria-hidden="true" @pointerdown=${($event: PointerEvent & { currentTarget: HTMLSpanElement; target: HTMLSpanElement }) => { this.onFillHandlePointerDown($event); }} data-rozie-s-d5dcab4c></span>` : nothing}</td>`)}
|
|
1249
|
-
|
|
1355
|
+
|
|
1356
|
+
${this.colsWindowed() ? html`<td class="rdt-col-spacer" aria-hidden="true" style=${rozieStyle('width:' + this.colPadRight() + 'px;padding:0;border:0')} data-rozie-s-d5dcab4c></td>` : nothing}</tr>
|
|
1250
1357
|
|
|
1251
1358
|
${this.rowShowsDetail(wr.row) ? html`<tr class="rdt-detail-row" role="row" data-detail-row=${rozieAttr(wr.row.id)} data-rozie-s-d5dcab4c>
|
|
1252
|
-
<td class="rdt-detail-cell" colspan=${rozieAttr(this.
|
|
1359
|
+
<td class="rdt-detail-cell" colspan=${rozieAttr(this.windowedColSpan())} data-rozie-s-d5dcab4c>
|
|
1253
1360
|
${this.detail !== undefined ? this.detail({row: wr.row.original}) : html`<slot name="detail" data-rozie-params=${(() => { try { return JSON.stringify({row: wr.row.original}); } catch { return '{}'; } })()}></slot>`}
|
|
1254
1361
|
</td>
|
|
1255
1362
|
</tr>` : nothing}`)}
|
|
1256
1363
|
|
|
1257
1364
|
<tr class="rdt-spacer" aria-hidden="true" data-rozie-s-d5dcab4c>
|
|
1258
|
-
<td colspan=${rozieAttr(this.
|
|
1365
|
+
<td colspan=${rozieAttr(this.windowedColSpan())} style=${rozieStyle('height:' + this.padBottom() + 'px;padding:0;border:0')} data-rozie-s-d5dcab4c></td>
|
|
1259
1366
|
</tr>
|
|
1260
1367
|
</tbody>
|
|
1261
1368
|
</table>
|
|
@@ -1309,22 +1416,22 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1309
1416
|
|
|
1310
1417
|
${this.isExpanderColumn(cell.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
1311
1418
|
${this.rowCanExpand(row) ? html`<button class="rdt-expander" type="button" data-expander="" aria-expanded=${!!this.rowIsExpanded(row)} aria-label=${rozieAttr(this.rowIsExpanded(row) ? 'Collapse row' : 'Expand row')} @click=${($event: MouseEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.onToggleExpand(row, $event); }} data-rozie-s-d5dcab4c>${rozieDisplay(this.rowIsExpanded(row) ? '▾' : '▸')}</button>` : nothing}</span>` : this.isSelectColumn(cell.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
1312
|
-
${this.selectCell !== undefined ? this.selectCell({row: row
|
|
1419
|
+
${this.selectCell !== undefined ? this.selectCell({row: this.cellSlotRow(row), checked: this.rowIsSelected(row), toggle: e => this.onToggleRow(row, e)}) : html`<slot name="selectCell" data-rozie-params=${(() => { try { return JSON.stringify({row: this.cellSlotRow(row), checked: this.rowIsSelected(row)}); } catch { return '{}'; } })()} @rozie-select-cell-toggle=${($event: CustomEvent) => ((e => this.onToggleRow(row, e)) as (...args: any[]) => any)($event.detail)}>
|
|
1313
1420
|
<input class="rdt-select-row" type="checkbox" aria-label="Select row" ?checked=${this.rowIsSelected(row)} @change=${($event: Event & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onToggleRow(row, $event); }} data-rozie-s-d5dcab4c />
|
|
1314
1421
|
</slot>`}
|
|
1315
1422
|
</span>` : this.cellIsGrouped(cell) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
1316
1423
|
<button class="rdt-expander rdt-group-toggle" type="button" data-expander="" aria-expanded=${!!this.rowIsExpanded(row)} aria-label=${rozieAttr(this.rowIsExpanded(row) ? 'Collapse group' : 'Expand group')} @click=${($event: MouseEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.onToggleExpand(row, $event); }} data-rozie-s-d5dcab4c>${rozieDisplay(this.rowIsExpanded(row) ? '▾' : '▸')}</button>
|
|
1317
1424
|
<span class="rdt-group-value" data-rozie-s-d5dcab4c>
|
|
1318
|
-
${this.cell !== undefined ? this.cell({columnId: cell.column.id, column: cell.column, row: row
|
|
1425
|
+
${this.cell !== undefined ? this.cell({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: cell.getValue()}) : html`<slot name="cell" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: cell.getValue()}); } catch { return '{}'; } })()}>${rozieDisplay(cell.getValue())}</slot>`}
|
|
1319
1426
|
</span>
|
|
1320
1427
|
<span class="rdt-group-count" data-rozie-s-d5dcab4c>${rozieDisplay('(' + this.groupSubRowCount(row) + ')')}</span>
|
|
1321
1428
|
</span>` : this.isEditing(this.rowIndexOf(row), this.colIndexOf(row, cell)) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
1322
1429
|
${this.hasEditorSlot(cell.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
1323
|
-
${this.editor !== undefined ? this.editor({columnId: cell.column.id, column: cell.column, row: row
|
|
1430
|
+
${this.editor !== undefined ? this.editor({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row))}) : html`<slot name="editor" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row))}); } catch { return '{}'; } })()}></slot>`}
|
|
1324
1431
|
</span>` : this.editorTypeOf(cell.column.id) === 'number' ? html`<input class="rdt-cell-editor" type="number" data-editing-cell="" .value=${this.editorValueFor(cell.column.id)} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />` : this.editorTypeOf(cell.column.id) === 'select' ? html`<select class="rdt-cell-editor" data-editing-cell="" .value=${this.editorValueFor(cell.column.id)} @change=${($event: Event & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c>
|
|
1325
1432
|
${repeat<any>(this.editorOptionsOf(cell.column.id), (opt, _idx) => opt.value, (opt, _idx) => html`<option value=${rozieAttr(opt.value)} data-rozie-s-d5dcab4c>${rozieDisplay(opt.label)}</option>`)}
|
|
1326
1433
|
</select>` : this.editorTypeOf(cell.column.id) === 'checkbox' ? html`<input class="rdt-cell-editor" type="checkbox" data-editing-cell="" ?checked=${this.editorCheckedFor(cell.column.id)} @change=${($event: Event & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorCheckbox(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />` : html`<input class="rdt-cell-editor" type="text" data-editing-cell="" .value=${this.editorValueFor(cell.column.id)} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />`}</span>` : this.cellIsPlaceholder(cell) ? html`<span style="display:contents" data-rozie-s-d5dcab4c></span>` : html`<span class="rdt-cell-value" data-rozie-s-d5dcab4c>
|
|
1327
|
-
${this.cell !== undefined ? this.cell({columnId: cell.column.id, column: cell.column, row: row
|
|
1434
|
+
${this.cell !== undefined ? this.cell({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: cell.getValue()}) : html`<slot name="cell" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: cell.getValue()}); } catch { return '{}'; } })()}>${rozieDisplay(cell.getValue())}</slot>`}
|
|
1328
1435
|
</span>`}${this.isFillHandleCell(this.rowIndexOf(row), this.colIndexOf(row, cell)) ? html`<span class="rdt-fill-handle" data-fill-handle="" data-testid="fill-handle" aria-hidden="true" @pointerdown=${($event: PointerEvent & { currentTarget: HTMLSpanElement; target: HTMLSpanElement }) => { this.onFillHandlePointerDown($event); }} data-rozie-s-d5dcab4c></span>` : nothing}</td>`)}
|
|
1329
1436
|
</tr>
|
|
1330
1437
|
|
|
@@ -1334,7 +1441,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1334
1441
|
</td>
|
|
1335
1442
|
</tr>` : nothing}`)}
|
|
1336
1443
|
</tbody>
|
|
1337
|
-
</table>`}${!this.
|
|
1444
|
+
</table>`}${!this.rowsWindowed() ? html`<div class="rdt-pagination" role="group" aria-label="Pagination" data-rozie-s-d5dcab4c>
|
|
1338
1445
|
<button class="rdt-page-btn rdt-page-prev" type="button" ?disabled=${!this.canPrevPage()} @click=${($event: MouseEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.onPrevPage(); }} data-rozie-s-d5dcab4c>Prev</button>
|
|
1339
1446
|
<span class="rdt-page-status" aria-live="polite" data-rozie-s-d5dcab4c>
|
|
1340
1447
|
${rozieDisplay('Page ' + (this.pageIndex() + 1) + ' of ' + this.displayPageCount())}
|
|
@@ -1363,6 +1470,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1363
1470
|
|
|
1364
1471
|
gridScrollEl: any = null;
|
|
1365
1472
|
|
|
1473
|
+
// ── Horizontal (column-axis) windowing instance state (Phase 87 87-04) ─────────────────
|
|
1474
|
+
// The SECOND, independent virtual-core instance (AXIS MECHANISM, windowing.rzts): mutable
|
|
1475
|
+
// top-level `let`s (the `virtualizer` precedent above — React hoists to useRef). NULL until
|
|
1476
|
+
// $onMount, and ONLY constructed when colsWindowed(). Shares gridScrollEl (the SAME
|
|
1477
|
+
// .rdt-scroll element) with the row instance — D-03.
|
|
1478
|
+
colVirtualizer: any = null;
|
|
1479
|
+
|
|
1480
|
+
colVirtualizerCleanup: any = null;
|
|
1481
|
+
|
|
1366
1482
|
// CR-01 remeasure scheduling state. remeasurePending dedupes the deferred sweep — at most ONE
|
|
1367
1483
|
// rAF is in flight, so a burst of onChange ticks (a fast scroll) collapses to a single measure
|
|
1368
1484
|
// pass per frame instead of piling up rAF callbacks that fire mid-gesture. The piled-up
|
|
@@ -1374,6 +1490,19 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1374
1490
|
// once motion settles between scroll steps) is unaffected.
|
|
1375
1491
|
remeasurePending = false;
|
|
1376
1492
|
|
|
1493
|
+
// Gap-closure 87-13: the deferred sweep's own rAF handle, plus a disposed latch. Until 87-07 a
|
|
1494
|
+
// post-unmount sweep was inert — it re-measured a detached tree and wrote nothing. 87-07's D-15
|
|
1495
|
+
// re-feed changed that: remeasureWindow() now reaches refineRowEstimate(), which calls
|
|
1496
|
+
// virtualizer.setOptions()/_willUpdate(), mutates gridScrollEl.scrollTop, and WRITES
|
|
1497
|
+
// $data.windowVer. remeasureWindow()'s existing `!virtualizer || !gridRoot` guard cannot catch
|
|
1498
|
+
// this — neither ref is ever nulled after $onMount assigns it — so a scroll/resize burst that
|
|
1499
|
+
// leaves a pass pending across unmount (a route transition mid-scroll) lands a state write on a
|
|
1500
|
+
// torn-down instance. Handle init is `null`, never 0: an Angular leaf types this as the DOM
|
|
1501
|
+
// handle and TS2322s on a numeric seed.
|
|
1502
|
+
remeasureRaf: any = null;
|
|
1503
|
+
|
|
1504
|
+
remeasureDisposed = false;
|
|
1505
|
+
|
|
1377
1506
|
// ── Grid interaction-mode constants + DOM root (phase 49, REQ-2/6) ────────────────────
|
|
1378
1507
|
// Fixed PageUp/PageDown row step (D-06). Phase 53 swaps this for the visible-window size
|
|
1379
1508
|
// via the same focusActiveCell() scroll-into-view seam — kept a top-level const so that
|
|
@@ -1533,34 +1662,6 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1533
1662
|
// re-feed sources here so editing works whether or not the consumer binds r-model:data.
|
|
1534
1663
|
currentData = (): any => this.data != null ? this.data : this._dataDefault.value;
|
|
1535
1664
|
|
|
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.)
|
|
1542
|
-
isSafeKey = (k: any) => k !== '__proto__' && k !== 'constructor' && k !== 'prototype';
|
|
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).
|
|
1552
|
-
wrapAggregationFn = (fn: any) => {
|
|
1553
|
-
if (typeof fn === 'string') return fn;
|
|
1554
|
-
if (typeof fn !== 'function') return undefined;
|
|
1555
|
-
return (columnId: any, leafRows: any, childRows: any) => {
|
|
1556
|
-
try {
|
|
1557
|
-
return fn(columnId, leafRows, childRows);
|
|
1558
|
-
} catch (err: any) {
|
|
1559
|
-
return undefined;
|
|
1560
|
-
}
|
|
1561
|
-
};
|
|
1562
|
-
};
|
|
1563
|
-
|
|
1564
1665
|
// Build the table-core ColumnDef for ONE config-array entry. A LEAF entry
|
|
1565
1666
|
// ({ id?, field, header?, … }) maps to an accessor ColumnDef; a GROUP entry
|
|
1566
1667
|
// ({ id?, header, columns: [...] }) maps to a multi-level header GROUP column
|
|
@@ -1590,7 +1691,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1590
1691
|
if (gid == null) gid = c.header != null ? '__grp_' + kids.map((k: any) => k.id).join('_') : null;
|
|
1591
1692
|
if (gid == null) return null;
|
|
1592
1693
|
const id = String(gid);
|
|
1593
|
-
if (!
|
|
1694
|
+
if (!isSafeKey(id)) return null;
|
|
1594
1695
|
return {
|
|
1595
1696
|
id,
|
|
1596
1697
|
header: c.header != null ? c.header : id,
|
|
@@ -1600,7 +1701,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1600
1701
|
const rawId = c.id != null ? c.id : c.field;
|
|
1601
1702
|
if (rawId == null) return null;
|
|
1602
1703
|
const id = String(rawId);
|
|
1603
|
-
if (!
|
|
1704
|
+
if (!isSafeKey(id)) return null;
|
|
1604
1705
|
return {
|
|
1605
1706
|
id,
|
|
1606
1707
|
accessorKey: c.field != null ? c.field : id,
|
|
@@ -1618,7 +1719,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1618
1719
|
// aggregationFn (built-in name OR custom fn) flows straight onto the ColumnDef (D-05),
|
|
1619
1720
|
// a custom fn defensively wrapped (T-50-04).
|
|
1620
1721
|
groupable: c.groupable !== false,
|
|
1621
|
-
aggregationFn:
|
|
1722
|
+
aggregationFn: wrapAggregationFn(c.aggregationFn),
|
|
1622
1723
|
pinned: c.pinned != null ? c.pinned : '',
|
|
1623
1724
|
width: c.width != null ? c.width : '',
|
|
1624
1725
|
// Editable-cell config (Phase 51) → ColumnDef.meta, the table-core per-column
|
|
@@ -1632,10 +1733,58 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1632
1733
|
};
|
|
1633
1734
|
};
|
|
1634
1735
|
|
|
1736
|
+
// ══ D-23 (gap-closure, Phase 87 follow-up) — columnDefs() memoization ═══════════════════
|
|
1737
|
+
// ROOT CAUSE (confirmed via CPU profiling the D-13 [vue] fill-drag auto-scroll failure):
|
|
1738
|
+
// defFor(colId) (columnChrome.rzts) — the sole resolver behind editMetaOf/columnEditable/
|
|
1739
|
+
// editorTypeOf/isEditing, all called PER CELL, PER RENDER (isEditing's own header comment:
|
|
1740
|
+
// "Called per-cell in both <td> bodies") — called columnDefs() with ZERO caching. columnDefs()
|
|
1741
|
+
// rebuilds the FULL column-def array (buildConfigDef for every entry in $props.columns,
|
|
1742
|
+
// including recursive group traversal) from scratch on every single invocation. With N
|
|
1743
|
+
// columns and M rendered rows that is an O(N x M x N) = O(N^2 x M) cost PER RENDER COMMIT —
|
|
1744
|
+
// on this plan's 60-column fixture, up to ~1200 cells x 60 rebuilt defs = ~72,000
|
|
1745
|
+
// buildConfigDef calls per commit, repeated on every windowVer-driven re-render during a
|
|
1746
|
+
// sustained scroll gesture (the fill-drag edge-auto-scroll rAF loop bumps windowVer on
|
|
1747
|
+
// every frame). A CPU profile of the failing case showed buildConfigDef consuming ~39% of
|
|
1748
|
+
// ALL wall-clock time during the gesture, with a FURTHER ~27% in Vue's own reactive-Proxy
|
|
1749
|
+
// `get` trap — $props.columns/$data.colReg are reactive on Vue, so every one of the
|
|
1750
|
+
// ~15 property reads buildConfigDef performs per column pays Vue's track()/Reflect.get
|
|
1751
|
+
// overhead, an expense React's plain (non-Proxy) props do not pay. The SAME O(N^2 x M)
|
|
1752
|
+
// algorithmic cost exists on all six targets (this is shared-engine code, dissolved
|
|
1753
|
+
// identically everywhere) — Vue's per-access reactivity overhead is what tips an already
|
|
1754
|
+
// wasteful recompute over the fixed real-time budget used by this test's rAF-driven
|
|
1755
|
+
// auto-scroll hold, not a Vue-specific logic defect. Fixing the recompute (not the timing)
|
|
1756
|
+
// removes the accidental n^2 blowup for every target, and Vue-specifically restores enough
|
|
1757
|
+
// per-frame headroom to keep pace with the auto-scroll loop.
|
|
1758
|
+
//
|
|
1759
|
+
// FIX: cache the resolved array, keyed on the REFERENCE identity of $props.columns and
|
|
1760
|
+
// $data.colReg — the exact two dependencies the reFeed() $watch (DataTable.rozie) already
|
|
1761
|
+
// treats as "changed only when the reference changes" (columns is consumer-memoized like
|
|
1762
|
+
// $props.data/$props.sorting; colReg is reassigned via `{ ...colReg, [key]: spec }` on every
|
|
1763
|
+
// real mutation, NEVER written in place — see DataTable.rozie's own colReg-mutation comment).
|
|
1764
|
+
// A cache hit is an O(1) reference-equality check; a cache miss re-runs the exact same
|
|
1765
|
+
// computation as before (byte-identical output either way — this is a pure memoization, not
|
|
1766
|
+
// a behavior change).
|
|
1767
|
+
columnDefsCache: any[] | null = null;
|
|
1768
|
+
|
|
1769
|
+
columnDefsCacheColumnsRef: any = undefined;
|
|
1770
|
+
|
|
1771
|
+
columnDefsCacheColRegRef: any = undefined;
|
|
1772
|
+
|
|
1773
|
+
// B1 (quick 260906-afh) — the def-lookup index, populated INSIDE this SAME memo block on
|
|
1774
|
+
// a cache MISS only (never rebuilt per call — a per-call recursive scan would restore the
|
|
1775
|
+
// O(N^2 x M) blowup D-23 removed, T-AFH-03). A top-level `let` lowers to a per-instance
|
|
1776
|
+
// `useRef` on React (verified in the emitted leaf), so this is instance-scoped like
|
|
1777
|
+
// columnDefsCache above.
|
|
1778
|
+
columnDefsIndexCache: any = null;
|
|
1779
|
+
|
|
1635
1780
|
columnDefs = () => {
|
|
1781
|
+
const cfg = this.columns || [];
|
|
1782
|
+
const reg = this._colReg.value || {};
|
|
1783
|
+
if (this.columnDefsCache && this.columns === this.columnDefsCacheColumnsRef && this._colReg.value === this.columnDefsCacheColRegRef) {
|
|
1784
|
+
return this.columnDefsCache;
|
|
1785
|
+
}
|
|
1636
1786
|
const byId = Object.create(null);
|
|
1637
1787
|
const order = [];
|
|
1638
|
-
const cfg = this.columns || [];
|
|
1639
1788
|
for (const c of cfg as any) {
|
|
1640
1789
|
const def = this.buildConfigDef(c);
|
|
1641
1790
|
if (!def) continue;
|
|
@@ -1643,9 +1792,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1643
1792
|
if (!(id in byId)) order.push(id);
|
|
1644
1793
|
byId[id] = def;
|
|
1645
1794
|
}
|
|
1646
|
-
const reg = this._colReg.value || {};
|
|
1647
1795
|
for (const id in reg) {
|
|
1648
|
-
if (!
|
|
1796
|
+
if (!isSafeKey(id)) continue;
|
|
1649
1797
|
const spec = reg[id];
|
|
1650
1798
|
if (!spec) continue;
|
|
1651
1799
|
if (!(id in byId)) order.push(id);
|
|
@@ -1660,7 +1808,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1660
1808
|
expandable: spec.expandable === true,
|
|
1661
1809
|
// Grouping (phase 50 reqs 4-7) — same shape as the config branch (D-05 / T-50-04).
|
|
1662
1810
|
groupable: spec.groupable !== false,
|
|
1663
|
-
aggregationFn:
|
|
1811
|
+
aggregationFn: wrapAggregationFn(spec.aggregationFn),
|
|
1664
1812
|
pinned: spec.pinned != null ? spec.pinned : '',
|
|
1665
1813
|
width: spec.width != null ? spec.width : '',
|
|
1666
1814
|
// Editable-cell config (Phase 51) → ColumnDef.meta from the <Column> registry spec.
|
|
@@ -1674,9 +1822,23 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1674
1822
|
}
|
|
1675
1823
|
const out = [];
|
|
1676
1824
|
for (const id of order as any) if (byId[id]) out.push(byId[id]);
|
|
1825
|
+
this.columnDefsCache = out;
|
|
1826
|
+
this.columnDefsCacheColumnsRef = this.columns;
|
|
1827
|
+
this.columnDefsCacheColRegRef = this._colReg.value;
|
|
1828
|
+
this.columnDefsIndexCache = indexDefsById(out);
|
|
1677
1829
|
return out;
|
|
1678
1830
|
};
|
|
1679
1831
|
|
|
1832
|
+
// defIndex: the def-lookup map for the CURRENT columnDefs() array. Calling columnDefs()
|
|
1833
|
+
// first is deliberate — either an O(1) reference-equality cache hit (columnDefsIndexCache
|
|
1834
|
+
// is already fresh) or a miss that repopulates BOTH slots together, so the index can never
|
|
1835
|
+
// go stale relative to the array. Falls back to an empty null-prototype map (never null/
|
|
1836
|
+
// undefined) so a caller can read it unconditionally.
|
|
1837
|
+
defIndex = () => {
|
|
1838
|
+
this.columnDefs();
|
|
1839
|
+
return this.columnDefsIndexCache || Object.create(null);
|
|
1840
|
+
};
|
|
1841
|
+
|
|
1680
1842
|
// The constant id of the auto-injected leading checkbox column (D-04). Distinct from
|
|
1681
1843
|
// any consumer column id (the registry/config guard never produces a leading "__").
|
|
1682
1844
|
SELECT_COL_ID = '__rdt_select';
|
|
@@ -1762,8 +1924,6 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1762
1924
|
this.programmatic--;
|
|
1763
1925
|
};
|
|
1764
1926
|
|
|
1765
|
-
applyUpdater = (updater: any, current: any) => typeof updater === 'function' ? updater(current) : updater;
|
|
1766
|
-
|
|
1767
1927
|
// ── expanded slice: STATIC-KEY fresh-value echo-guarded write funnel (A4) ──────────
|
|
1768
1928
|
// table-core hands an Updater<ExpandedState> = value | (old)=>new; onExpandedChange
|
|
1769
1929
|
// applies it against the CURRENT expanded, then this funnel writes a FRESH value to the
|
|
@@ -2145,51 +2305,51 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2145
2305
|
// fresh (recreated each render on React, reading fresh currentState) in the re-feed
|
|
2146
2306
|
// keeps the Updater base value current. No-op cost on the other five.
|
|
2147
2307
|
onSortingChangeCb = (updater: any) => {
|
|
2148
|
-
this.writeSorting(
|
|
2308
|
+
this.writeSorting(applyUpdater(updater, this.currentState().sorting));
|
|
2149
2309
|
};
|
|
2150
2310
|
|
|
2151
2311
|
onExpandedChangeCb = (updater: any) => {
|
|
2152
|
-
this.writeExpanded(
|
|
2312
|
+
this.writeExpanded(applyUpdater(updater, this.currentState().expanded));
|
|
2153
2313
|
};
|
|
2154
2314
|
|
|
2155
2315
|
onGroupingChangeCb = (updater: any) => {
|
|
2156
|
-
this.writeGrouping(
|
|
2316
|
+
this.writeGrouping(applyUpdater(updater, this.currentState().grouping));
|
|
2157
2317
|
};
|
|
2158
2318
|
|
|
2159
2319
|
onGlobalFilterChangeCb = (updater: any) => {
|
|
2160
|
-
this.writeGlobalFilter(
|
|
2320
|
+
this.writeGlobalFilter(applyUpdater(updater, this.currentState().globalFilter));
|
|
2161
2321
|
};
|
|
2162
2322
|
|
|
2163
2323
|
onColumnFiltersChangeCb = (updater: any) => {
|
|
2164
|
-
this.writeColumnFilters(
|
|
2324
|
+
this.writeColumnFilters(applyUpdater(updater, this.currentState().columnFilters));
|
|
2165
2325
|
};
|
|
2166
2326
|
|
|
2167
2327
|
onPaginationChangeCb = (updater: any) => {
|
|
2168
|
-
this.writePagination(
|
|
2328
|
+
this.writePagination(applyUpdater(updater, this.currentState().pagination));
|
|
2169
2329
|
};
|
|
2170
2330
|
|
|
2171
2331
|
onRowSelectionChangeCb = (updater: any) => {
|
|
2172
|
-
this.writeRowSelection(
|
|
2332
|
+
this.writeRowSelection(applyUpdater(updater, this.currentState().rowSelection));
|
|
2173
2333
|
};
|
|
2174
2334
|
|
|
2175
2335
|
onColumnVisibilityChangeCb = (updater: any) => {
|
|
2176
|
-
this.writeColumnVisibility(
|
|
2336
|
+
this.writeColumnVisibility(applyUpdater(updater, this.currentState().columnVisibility));
|
|
2177
2337
|
};
|
|
2178
2338
|
|
|
2179
2339
|
onColumnSizingChangeCb = (updater: any) => {
|
|
2180
|
-
this.writeColumnSizing(
|
|
2340
|
+
this.writeColumnSizing(applyUpdater(updater, this.currentState().columnSizing));
|
|
2181
2341
|
};
|
|
2182
2342
|
|
|
2183
2343
|
onColumnOrderChangeCb = (updater: any) => {
|
|
2184
|
-
this.writeColumnOrder(
|
|
2344
|
+
this.writeColumnOrder(applyUpdater(updater, this.currentState().columnOrder));
|
|
2185
2345
|
};
|
|
2186
2346
|
|
|
2187
2347
|
onColumnPinningChangeCb = (updater: any) => {
|
|
2188
|
-
this.writeColumnPinning(
|
|
2348
|
+
this.writeColumnPinning(applyUpdater(updater, this.currentState().columnPinning));
|
|
2189
2349
|
};
|
|
2190
2350
|
|
|
2191
2351
|
onColumnSizingInfoChangeCb = (updater: any) => {
|
|
2192
|
-
const next =
|
|
2352
|
+
const next = applyUpdater(updater, this._columnSizingInfo.value);
|
|
2193
2353
|
this._columnSizingInfo.value = next != null ? next : this._columnSizingInfo.value;
|
|
2194
2354
|
};
|
|
2195
2355
|
|
|
@@ -2209,13 +2369,157 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2209
2369
|
// dist-parity are the net). The host satisfies the windowing.rzts contract by convention:
|
|
2210
2370
|
// windowSource() (the row source), pinnedEditIndex()/pinnedMeasurement() (the D-05 pin hook),
|
|
2211
2371
|
// scheduleRemeasure(), and the gridScrollEl/virtualizer/virtual-core-fn references.
|
|
2372
|
+
// ══ D-05 predicate layer (Phase 87 87-02) — resolves the windowing prop into an axis state,
|
|
2373
|
+
// replacing every bare truthiness read of $props.virtual (which is unsafe once the prop widens
|
|
2374
|
+
// to a string grammar in 87-03: 'columns' is truthy but does NOT mean "windows rows"). Modeled on
|
|
2375
|
+
// resolveAppendTo() in CommandPalette.rozie (the D-01 value-grammar precedent): branch on
|
|
2376
|
+
// typeof === 'string', switch on the known string values, fall back to the boolean cases. THIS
|
|
2377
|
+
// plan still declares `virtual: Boolean` (87-03 widens the declaration to [Boolean, String]), so
|
|
2378
|
+
// resolveVirtual() can currently only return 'off' or 'rows' and colsWindowed() is constantly
|
|
2379
|
+
// false — inert until 87-03/87-04 land the column axis.
|
|
2380
|
+
//
|
|
2381
|
+
// Declared BEFORE windowSource() (below): windowSource() calls rowsWindowed(), and the React
|
|
2382
|
+
// emitter lowers each of these to a useCallback whose dependency array is evaluated eagerly at
|
|
2383
|
+
// the declaration site — a forward reference here is a genuine TDZ (TS2448), not just a style
|
|
2384
|
+
// preference (found running the strict react typecheck gate this task, Rule 1).
|
|
2385
|
+
resolveVirtual = () => {
|
|
2386
|
+
const v = this.virtual;
|
|
2387
|
+
if (typeof v === 'string') {
|
|
2388
|
+
if (v === 'rows') return 'rows';
|
|
2389
|
+
if (v === 'columns') return 'columns';
|
|
2390
|
+
if (v === 'both') return 'both';
|
|
2391
|
+
return 'off';
|
|
2392
|
+
}
|
|
2393
|
+
return v === true ? 'rows' : 'off';
|
|
2394
|
+
};
|
|
2395
|
+
|
|
2396
|
+
// rowsWindowed(): the windowing.rzts host-contract symbol (D-05). Byte-behaviorally identical to
|
|
2397
|
+
// today's `$props.virtual` truthiness — this plan changes NO user-visible behavior.
|
|
2398
|
+
rowsWindowed = () => {
|
|
2399
|
+
const s = this.resolveVirtual();
|
|
2400
|
+
return s === 'rows' || s === 'both';
|
|
2401
|
+
};
|
|
2402
|
+
|
|
2403
|
+
colsWindowed = () => {
|
|
2404
|
+
const s = this.resolveVirtual();
|
|
2405
|
+
return s === 'columns' || s === 'both';
|
|
2406
|
+
};
|
|
2407
|
+
|
|
2408
|
+
isWindowed = () => this.rowsWindowed() || this.colsWindowed();
|
|
2409
|
+
|
|
2410
|
+
// autoMeasureOn() (Phase 87 87-07, D-18): the windowing.rzts host-contract gate for the
|
|
2411
|
+
// running-mean row-size estimator. Real body here: $props.autoMeasure === true. listbox/
|
|
2412
|
+
// combobox define this as a permanent `() => false` one-liner (D-20) so estimateRowSize()'s
|
|
2413
|
+
// accumulator branch is dead code for them.
|
|
2414
|
+
autoMeasureOn = () => this.autoMeasure === true;
|
|
2415
|
+
|
|
2416
|
+
// D-10 column-axis host-contract symbols. columnCount()/columnSize() got REAL bodies in
|
|
2417
|
+
// 87-04 (the tracer); forcedColumns() stays an empty-array stub until 87-05 gives it the
|
|
2418
|
+
// pinned + active-cell + editing-column union. Explicit return-type annotations (columnSize /
|
|
2419
|
+
// forcedColumns) copy the pinMeasurement() trick (windowing.rzts) so the strict bundled-leaf
|
|
2420
|
+
// tsc does not flow-narrow a no-op host's return to `never`.
|
|
2421
|
+
//
|
|
2422
|
+
// columnCount(): the leaf-column count the column virtualizer windows over — visibleColCount()
|
|
2423
|
+
// (gridFocusNav.rzts), the SAME index space colIndexOf()/data-col-index already address (D-08).
|
|
2424
|
+
columnCount = (): number => this.visibleColCount();
|
|
2425
|
+
|
|
2426
|
+
// columnSize(i): the authoritative width of absolute leaf column i (D-06). Reads table-core's
|
|
2427
|
+
// OWN getVisibleLeafColumns() directly — NOT the host's allLeafColumns() wrapper
|
|
2428
|
+
// (columnChrome.rzts), which returns plain `{ id, label, visible }` POJOs with no getSize()
|
|
2429
|
+
// and also EXCLUDES the auto-injected chrome columns (select/expander), breaking the index
|
|
2430
|
+
// alignment with columnCount()/colIndexOf(). getVisibleLeafColumns() is the array
|
|
2431
|
+
// row.getVisibleCells() itself is built from, so its index space matches exactly. Falls back
|
|
2432
|
+
// to table-core's own default column width (150) when the index is out of range.
|
|
2433
|
+
columnSize = (i: number): number => {
|
|
2434
|
+
if (!this.table || !this.table.getVisibleLeafColumns) return 150;
|
|
2435
|
+
const cols = this.table.getVisibleLeafColumns();
|
|
2436
|
+
const c = cols[i];
|
|
2437
|
+
return c && typeof c.getSize === 'function' ? c.getSize() : 150;
|
|
2438
|
+
};
|
|
2439
|
+
|
|
2440
|
+
// forcedColumns() (Phase 87 87-05, D-10): the column-axis mirror of pinnedEditIndex()/
|
|
2441
|
+
// pinnedMeasurement() (the row axis's D-05 pin-extension hook) — unions pinned + active-cell +
|
|
2442
|
+
// editing column indices into the window slice so none of them ever recycle out from under the
|
|
2443
|
+
// user. Reads table.getVisibleLeafColumns() DIRECTLY (the SAME array columnCount()/columnSize()
|
|
2444
|
+
// index against, and the array row.getVisibleCells() is itself built from) — NOT
|
|
2445
|
+
// allLeafColumns() (columnChrome.rzts), which excludes the auto-injected chrome columns and
|
|
2446
|
+
// returns plain POJOs with no getSize(), breaking the index alignment (the exact columnSize()
|
|
2447
|
+
// Rule-1 fix from 87-04, mirrored here rather than repeated).
|
|
2448
|
+
//
|
|
2449
|
+
// The fill-handle corner is DELIBERATELY not forced (D-10 explicit) — no call to
|
|
2450
|
+
// isFillHandleCell here. The whole selected RANGE is not forced either: a range's moving
|
|
2451
|
+
// corner tracks into $data.activeColIndex (extendRange/setRangeFocus), so even a 200-column
|
|
2452
|
+
// range forces at most the ONE active-cell column via the branch below, never the full span —
|
|
2453
|
+
// forcing the whole range would defeat the feature precisely where it is most wanted (D-10).
|
|
2454
|
+
forcedColumns = (): number[] => {
|
|
2455
|
+
if (!this.colsWindowed()) return [];
|
|
2456
|
+
const out = [];
|
|
2457
|
+
if (this.table && this.table.getVisibleLeafColumns) {
|
|
2458
|
+
const cols = this.table.getVisibleLeafColumns();
|
|
2459
|
+
for (let i = 0; i < cols.length; i++) {
|
|
2460
|
+
const c = cols[i];
|
|
2461
|
+
if (c && c.getIsPinned && c.getIsPinned()) out.push(i);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
// Active cell's column (D-10): required or focusActiveCell()'s [data-col-index="N"]
|
|
2465
|
+
// querySelector cannot resolve for an off-window active cell. Body cells only — a header-
|
|
2466
|
+
// active cell has no column-axis rendering dependency here (headers window separately).
|
|
2467
|
+
if (!this._activeIsHeader.value && this._activeColIndex.value >= 0 && out.indexOf(this._activeColIndex.value) === -1) {
|
|
2468
|
+
out.push(this._activeColIndex.value);
|
|
2469
|
+
}
|
|
2470
|
+
// Editing column (D-10): required or an open editor unmounts mid-keystroke when a
|
|
2471
|
+
// horizontal scroll recycles it — the exact failure D-02 was built to fix on the row axis
|
|
2472
|
+
// (T-87-05-01, the phase's one HIGH-severity threat). Single-cell edit mode only
|
|
2473
|
+
// ($data.editingRow/editingCol, mirroring editingColumnId()'s own index pair) — full-row
|
|
2474
|
+
// edit mode (editingRowIndex) has no single "the editing column" to force.
|
|
2475
|
+
if (this._editingRow.value >= 0 && this._editingCol.value >= 0 && out.indexOf(this._editingCol.value) === -1) {
|
|
2476
|
+
out.push(this._editingCol.value);
|
|
2477
|
+
}
|
|
2478
|
+
return out;
|
|
2479
|
+
};
|
|
2480
|
+
|
|
2481
|
+
// windowedCells(row) (D-09): the TEMPLATE-ONLY windowed slice of a row's visible cells — wraps
|
|
2482
|
+
// visibleCellsFor(row) (columnChrome.rzts) WITHOUT modifying it. Degrades to the full,
|
|
2483
|
+
// unsliced set when columns are not windowed (byte-identical to today). visibleCellsFor()
|
|
2484
|
+
// itself must never be sliced: colIndexOf/visibleColCount/columnIdAt/cellValueAt/beginEdit all
|
|
2485
|
+
// index into its FULL result (the A2 invariant, 87-02), and slicing it would silently break
|
|
2486
|
+
// all five with no compile error.
|
|
2487
|
+
windowedCells = (row: any) => {
|
|
2488
|
+
// subscribe-first (the windowedRows() discipline) so the fine-grained targets' body <td>
|
|
2489
|
+
// r-for re-derives when the column window moves.
|
|
2490
|
+
void this._windowVer.value;
|
|
2491
|
+
const cells = this.visibleCellsFor(row);
|
|
2492
|
+
if (!this.colsWindowed()) return cells;
|
|
2493
|
+
const idx = this.windowedColIndices();
|
|
2494
|
+
const out = [];
|
|
2495
|
+
for (let i = 0; i < idx.length; i++) {
|
|
2496
|
+
// WR-01 shrink-window guard, one axis over: drop any index that outruns the row's
|
|
2497
|
+
// current cell list (a brief stale-count window analogous to windowedRows()'s WR-01).
|
|
2498
|
+
const cell = cells[idx[i]];
|
|
2499
|
+
if (cell) out.push(cell);
|
|
2500
|
+
}
|
|
2501
|
+
return out;
|
|
2502
|
+
};
|
|
2503
|
+
|
|
2504
|
+
// remeasureColumnWindow() (Phase 87 87-04) is declared in DataTable.rozie's OWN script, NOT
|
|
2505
|
+
// here — even though it is conceptually this file's D-10 host-shell twin of remeasureWindow()
|
|
2506
|
+
// above. Reason (Rule 1, found via the react typecheck gate): remeasureColumnWindow() must call
|
|
2507
|
+
// columnVirtualizerOptions() (headless-core windowing.rzts). inlineScriptPartials() groups a
|
|
2508
|
+
// RELATIVE partial import (this file) BEFORE a bare-package-specifier partial import
|
|
2509
|
+
// (@rozie-ui/headless-core/windowing.rzts) in the merged per-target output, regardless of which
|
|
2510
|
+
// import statement appears first in DataTable.rozie's own script (verified: columnVirtualizerOptions
|
|
2511
|
+
// emits as a `useCallback` LATER in the react output than every symbol this file exports). A
|
|
2512
|
+
// function declared HERE calling INTO windowing.rzts is therefore a forward reference — the
|
|
2513
|
+
// exact TS2448 TDZ class the 87-02 predicate-layer reordering already fixed once. DataTable.rozie's
|
|
2514
|
+
// OWN top-level script code lands AFTER both partial groups, so remeasureColumnWindow() is safe
|
|
2515
|
+
// declared there instead (see the comment beside windowedHeaderRow()/windowedColSpan()).
|
|
2212
2516
|
// windowSource(): the rows fed to the virtualizer AND held in $data.rows — the windowing.rzts
|
|
2213
2517
|
// host-contract source. When virtual, the FULL filtered+sorted PRE-PAGINATION model
|
|
2214
2518
|
// (A2-verified table.getPrePaginationRowModel()) so windowing REPLACES client pagination (req-9);
|
|
2215
2519
|
// else the normal (paginated) row model — the non-virtual path is byte-unchanged.
|
|
2216
2520
|
windowSource = () => {
|
|
2217
2521
|
if (!this.table) return [];
|
|
2218
|
-
if (this.
|
|
2522
|
+
if (this.rowsWindowed()) return this.table.getPrePaginationRowModel().rows;
|
|
2219
2523
|
return this.table.getRowModel().rows;
|
|
2220
2524
|
};
|
|
2221
2525
|
|
|
@@ -2238,6 +2542,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2238
2542
|
// Each pass only OBSERVES + measures the live window; measureElement is idempotent on an
|
|
2239
2543
|
// already-observed node, so running both is cheap and loop-free.
|
|
2240
2544
|
scheduleRemeasure = () => {
|
|
2545
|
+
// 87-13: never arm a new sweep after teardown — an onChange can still fire from a
|
|
2546
|
+
// virtualizer whose own ResizeObserver cleanup has run but whose queued tick was already
|
|
2547
|
+
// in flight.
|
|
2548
|
+
if (this.remeasureDisposed) return;
|
|
2241
2549
|
if (this.remeasurePending) return;
|
|
2242
2550
|
this.remeasurePending = true;
|
|
2243
2551
|
let ranMicro = false;
|
|
@@ -2245,6 +2553,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2245
2553
|
this.remeasureWindow();
|
|
2246
2554
|
};
|
|
2247
2555
|
const rafPass = () => {
|
|
2556
|
+
this.remeasureRaf = null;
|
|
2248
2557
|
this.remeasurePending = false;
|
|
2249
2558
|
this.remeasureWindow();
|
|
2250
2559
|
};
|
|
@@ -2252,7 +2561,25 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2252
2561
|
ranMicro = true;
|
|
2253
2562
|
queueMicrotask(microPass);
|
|
2254
2563
|
}
|
|
2255
|
-
|
|
2564
|
+
// 87-13: retain the handle so teardownRemeasure() can cancel a pass that is still pending
|
|
2565
|
+
// at unmount. Without this the callback survives teardown and writes $data.windowVer.
|
|
2566
|
+
if (typeof requestAnimationFrame === 'function') this.remeasureRaf = requestAnimationFrame(rafPass);else if (ranMicro) this.remeasurePending = false;else this.remeasureRaf = setTimeout(rafPass, 0);
|
|
2567
|
+
};
|
|
2568
|
+
|
|
2569
|
+
// 87-13 (CR-01): idempotent teardown for the deferred sweep — the same discipline
|
|
2570
|
+
// teardownFillDrag()/teardownRangeDrag() already follow. Cancels a pending pass and latches
|
|
2571
|
+
// `remeasureDisposed` so the microtask twin (which carries no cancellable handle) and any
|
|
2572
|
+
// in-flight onChange both bail instead of writing to a torn-down instance.
|
|
2573
|
+
teardownRemeasure = () => {
|
|
2574
|
+
this.remeasureDisposed = true;
|
|
2575
|
+
if (this.remeasureRaf != null) {
|
|
2576
|
+
// Discriminate on requestAnimationFrame — the SAME predicate scheduleRemeasure() branched
|
|
2577
|
+
// on when it produced this handle. Testing cancelAnimationFrame instead would hand a
|
|
2578
|
+
// setTimeout id to cancelAnimationFrame in any env where the two disagree.
|
|
2579
|
+
if (typeof requestAnimationFrame === 'function') cancelAnimationFrame(this.remeasureRaf);else clearTimeout(this.remeasureRaf);
|
|
2580
|
+
this.remeasureRaf = null;
|
|
2581
|
+
}
|
|
2582
|
+
this.remeasurePending = false;
|
|
2256
2583
|
};
|
|
2257
2584
|
|
|
2258
2585
|
// pinnedEditIndex(): the FULL-MODEL row index of the row currently in edit (D-02 pin-row),
|
|
@@ -2288,6 +2615,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2288
2615
|
// idempotent on an already-observed node (the `prevNode !== node` guard), so re-sweeping the
|
|
2289
2616
|
// visible window each commit is cheap and loop-free.
|
|
2290
2617
|
remeasureWindow = () => {
|
|
2618
|
+
// 87-13 (CR-01): the disposed latch, checked BEFORE the ref guard below. That guard cannot
|
|
2619
|
+
// cover unmount — virtualizer/gridRoot are assigned in $onMount and never nulled — so this
|
|
2620
|
+
// is the only thing standing between a pass queued pre-unmount and refineRowEstimate()'s
|
|
2621
|
+
// setOptions/scrollTop/$data.windowVer writes against a torn-down instance.
|
|
2622
|
+
if (this.remeasureDisposed) return;
|
|
2291
2623
|
if (!this.virtualizer || !this.gridRoot) return;
|
|
2292
2624
|
// Bail ONLY while a PROGRAMMATIC scroll is in flight: virtualizer.scrollState is non-null
|
|
2293
2625
|
// exclusively during scrollToIndex / scrollToOffset (the D-12 scroll-then-focus seam) and
|
|
@@ -2299,6 +2631,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2299
2631
|
if (this.virtualizer.scrollState) return;
|
|
2300
2632
|
const trs = this.gridRoot.querySelectorAll('tbody.rdt-tbody > tr[data-index]');
|
|
2301
2633
|
for (const tr of trs as any) this.virtualizer.measureElement(tr);
|
|
2634
|
+
// D-15 (Phase 87 87-07): trigger the shared engine's post-measurement fold + hysteresis
|
|
2635
|
+
// re-feed via the afterRowRemeasure mutable-let hook (DataTable.rozie's own script,
|
|
2636
|
+
// assigned to windowing.rzts's refineRowEstimate()) — NEVER a direct call from this file
|
|
2637
|
+
// into windowing.rzts (see windowing.rzts's HOST CONTRACT comment for the forward-
|
|
2638
|
+
// reference TDZ this indirection avoids, the same class remeasureColumnWindow()'s comment
|
|
2639
|
+
// documents for columnVirtualizerOptions()). Inert (no-op) until the host assigns it; the
|
|
2640
|
+
// off/pre-mount path never reaches this line since virtualizer is null there.
|
|
2641
|
+
if (this.afterRowRemeasure) this.afterRowRemeasure();
|
|
2302
2642
|
};
|
|
2303
2643
|
|
|
2304
2644
|
// D-04: this shell exports ONLY the impure, data-table-specific host pieces. The pure windowing
|
|
@@ -2329,6 +2669,61 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2329
2669
|
// defaulting to no-op): the DataTable host passes its edit-pinning hooks;
|
|
2330
2670
|
// listbox passes nothing. Routing pinning through this host hook (NOT
|
|
2331
2671
|
// inlining it) keeps DataTable's B13 edit-pinning behavior byte-identical.
|
|
2672
|
+
// - rowsWindowed(): boolean — is the ROW axis windowed. REQUIRED, no default — replaces every bare
|
|
2673
|
+
// truthiness read of the host's windowing prop (D-05); `windowedRows()` /
|
|
2674
|
+
// `padTop()` / `padBottom()` / `rowIsOutsideWindow()` below call it by
|
|
2675
|
+
// convention exactly as they already call `pinnedEditIndex()`.
|
|
2676
|
+
// - colsWindowed(): boolean — is the COLUMN axis windowed. REQUIRED, no default. `false` for every
|
|
2677
|
+
// host until it defines the real column-axis mechanism (87-04+).
|
|
2678
|
+
// - columnCount(): number — the leaf-column count the column virtualizer windows over. REQUIRED,
|
|
2679
|
+
// no default.
|
|
2680
|
+
// - columnSize(i: number): number — the authoritative width of absolute leaf column `i`, sourced
|
|
2681
|
+
// from table-core's `getSize()` under D-06. REQUIRED, no default.
|
|
2682
|
+
// - forcedColumns(): number[] — the D-10 OPTIONAL column-axis mirror of `pinnedEditIndex()`: the
|
|
2683
|
+
// DataTable host unions pinned + active-cell + editing column indices into
|
|
2684
|
+
// the column-window slice; listbox/combobox pass an empty array (host-
|
|
2685
|
+
// provided, defaulting to `[]`).
|
|
2686
|
+
// - colVirtualizer — the host's SECOND virtual-core instance, windowing the COLUMN axis
|
|
2687
|
+
// (see the AXIS MECHANISM note below). Host-provided, defaulting to `null`.
|
|
2688
|
+
// - autoMeasureOn(): boolean — the D-18 REQUIRED content-driven-estimate gate (Phase 87 87-07):
|
|
2689
|
+
// data-table's real body reads `$props.autoMeasure === true`; listbox/
|
|
2690
|
+
// combobox/command-palette return `false` so the accumulator branch
|
|
2691
|
+
// estimateRowSize() gates on is dead code for them (D-20).
|
|
2692
|
+
// - afterRowRemeasure — OPTIONAL host-owned mutable `let` (defaults to a no-op / undefined),
|
|
2693
|
+
// assigned to refineRowEstimate() (below) by the host. The DataTable
|
|
2694
|
+
// host's remeasureWindow() (virtualization.rzts) calls it AFTER its
|
|
2695
|
+
// measureElement sweep so the fold + hysteresis re-feed run on every
|
|
2696
|
+
// window commit. Routed through a mutable `let` rather than a direct
|
|
2697
|
+
// call FROM virtualization.rzts INTO this file: a relative-partial CONST
|
|
2698
|
+
// calling a bare-specifier-partial CONST is the exact forward-reference
|
|
2699
|
+
// TDZ class remeasureColumnWindow()'s own DataTable.rozie comment
|
|
2700
|
+
// documents for columnVirtualizerOptions() (inlineScriptPartials()
|
|
2701
|
+
// groups the relative partial BEFORE the bare-specifier partial in the
|
|
2702
|
+
// merged per-target output, regardless of source import order). A
|
|
2703
|
+
// mutable `let` hoists to `useRef` on React and is excluded from a
|
|
2704
|
+
// useCallback's dependency array, sidestepping the hazard entirely — the
|
|
2705
|
+
// SAME mechanism `refreshRowModel` already relies on.
|
|
2706
|
+
//
|
|
2707
|
+
// AXIS MECHANISM (OQ1 / Assumption A1 — resolved from the installed source in 87-02;
|
|
2708
|
+
// LANDED in 87-04: `columnVirtualizerOptions()` below IS the second, horizontal instance this
|
|
2709
|
+
// note originally only documented). `horizontal` is a PER-INSTANCE field of `VirtualizerOptions`
|
|
2710
|
+
// (`node_modules/@tanstack/virtual-core/dist/esm/index.d.ts:67`, installed version 3.17.1 per
|
|
2711
|
+
// `package.json`), and every axis-sensitive internal read consults `instance.options.horizontal` —
|
|
2712
|
+
// `measureElement`'s inlineSize/blockSize + offsetWidth/offsetHeight branch
|
|
2713
|
+
// (`dist/esm/index.js:137,150`), `observeElementOffset`'s scrollLeft/scrollTop branch
|
|
2714
|
+
// (`dist/esm/index.js:118-121`), `getMaxScrollOffset`'s scrollWidth/scrollHeight branch
|
|
2715
|
+
// (`dist/esm/index.js:907-915`), and `scrollWithAdjustments`'s left/top branch
|
|
2716
|
+
// (`dist/esm/index.js:152-161`). So ONE `Virtualizer` instance windows exactly ONE axis: the column
|
|
2717
|
+
// axis needs its own SECOND, independent `Virtualizer` instance constructed with `horizontal: true`,
|
|
2718
|
+
// sharing the SAME `getScrollElement()` (the `rdt-scroll` wrapper) the row instance already uses.
|
|
2719
|
+
// Two options the row axis does not set that the column instance will need: `isRtl?: boolean`
|
|
2720
|
+
// (data-table ships an RTL grid path) and `overscan?: number` (D-07 gives the column axis its own
|
|
2721
|
+
// hardcoded constant, separate from the row axis's `overscan: 8` below).
|
|
2722
|
+
//
|
|
2723
|
+
// isRtl WIRING (gap-closure 87-09, LANDED — see `ensureColRtlWatch()`/`isColRtl()` below,
|
|
2724
|
+
// immediately ahead of `columnVirtualizerOptions()`): data-table has no construction-time RTL
|
|
2725
|
+
// signal (no `dir`/`rtl` prop), and `dir` can be set on `gridScrollEl` at ANY point relative to
|
|
2726
|
+
// mount. `isRtl` is therefore computed LIVE via `getComputedStyle`, not baked in once.
|
|
2332
2727
|
// getItemKey reads the LIVE source (never a frozen mount-render $data.rows closure — the F6
|
|
2333
2728
|
// React stale-closure lesson) so virtual-core's measurement cache keys by stable full-model row
|
|
2334
2729
|
// id across recycling, aligned with the windowed <tr> :key="row.id" (Pitfall 3 / req-10).
|
|
@@ -2337,16 +2732,321 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2337
2732
|
return src && src[i] ? src[i].id : undefined;
|
|
2338
2733
|
};
|
|
2339
2734
|
|
|
2735
|
+
// COL_OVERSCAN (D-07): the column axis's own hardcoded overscan constant, separate from the
|
|
2736
|
+
// row axis's `overscan: 8` below. Columns are far wider than rows are tall, so one number
|
|
2737
|
+
// cannot serve both axes; no prop is exposed because no consumer has asked to tune the row
|
|
2738
|
+
// overscan across the four phases it has shipped. Unused until 87-04 constructs the second,
|
|
2739
|
+
// horizontal Virtualizer instance (see the AXIS MECHANISM note above).
|
|
2740
|
+
COL_OVERSCAN = 3;
|
|
2741
|
+
|
|
2742
|
+
// columnVirtualizerOptions() (Phase 87 87-04): the SECOND, horizontal Virtualizer instance's
|
|
2743
|
+
// full options set — the AXIS MECHANISM note's second instance, landed. Mirrors
|
|
2744
|
+
// virtualizerOptions() exactly except: count/estimateSize read the COLUMN host symbols,
|
|
2745
|
+
// horizontal:true selects the axis, overscan is the column axis's OWN constant (D-07), and
|
|
2746
|
+
// there is NO getItemKey — columns have no stable per-column id in the virtual-core sense, so
|
|
2747
|
+
// they key by index (virtual-core's default). onChange bumps the SAME $data.windowVer counter
|
|
2748
|
+
// the row axis uses (87-02 checkpoint decision (b): no separate $data.colWindowVer) but does
|
|
2749
|
+
// NOT call scheduleRemeasure(): under D-06 column widths come from table-core's getSize()
|
|
2750
|
+
// oracle and are NEVER measured from the DOM, so a horizontal scroll has nothing new to
|
|
2751
|
+
// observe (unlike the row axis's CR-01 recycled-row remeasure sweep).
|
|
2752
|
+
// ══ Gap-closure 87-09 (dir="rtl" column windowing) ═══════════════════════════════════════
|
|
2753
|
+
// virtual-core reads `instance.options.isRtl` FRESH on every native 'scroll' event — the
|
|
2754
|
+
// negation lives inside `observeElementOffset`'s internal per-event closure
|
|
2755
|
+
// (`dist/esm/index.js:119-120`: `el.scrollLeft * (isRtl && -1 || 1)`), called anew each time
|
|
2756
|
+
// the browser fires 'scroll'. But `instance.options` itself is only a plain VALUE snapshot
|
|
2757
|
+
// taken by the LAST `setOptions()` call (`dist/esm/index.js:277-280`:
|
|
2758
|
+
// `for (const key in opts2) merged[key] = opts2[key]` copies the boolean, not a live binding
|
|
2759
|
+
// or getter) — so a static `isRtl` computed ONCE at `colVirtualizer` construction would only
|
|
2760
|
+
// ever reflect direction AT THAT INSTANT. `dir` can be set on `gridScrollEl` at any point
|
|
2761
|
+
// relative to mount (data-table has no construction-time RTL prop), so a MutationObserver on
|
|
2762
|
+
// `gridScrollEl`'s own `dir` attribute re-feeds `colVirtualizer`'s options — a cheap
|
|
2763
|
+
// object-merge via `setOptions()`, deliberately NOT a forced `_willUpdate()` recompute, since
|
|
2764
|
+
// nothing needs to repaint until the next real scroll — the moment direction flips. By the
|
|
2765
|
+
// time the NEXT native 'scroll' event's own offset read runs, `instance.options.isRtl`
|
|
2766
|
+
// already reflects the correction. Ordering is spec-guaranteed, not a race: MutationObserver
|
|
2767
|
+
// callbacks run on the microtask queue, which the HTML event loop always drains before the
|
|
2768
|
+
// next task — and a native 'scroll' event dispatched after a programmatic `scrollLeft` write
|
|
2769
|
+
// is itself a subsequent task, not a microtask racing the same tick.
|
|
2770
|
+
//
|
|
2771
|
+
// F6 STALE-CLOSURE HAZARD (found + fixed this task, React only): calling
|
|
2772
|
+
// columnVirtualizerOptions() FRESH from inside the MutationObserver's callback would
|
|
2773
|
+
// re-derive `count`/`estimateSize` via columnCount() (virtualization.rzts's
|
|
2774
|
+
// visibleColCount(), which reads `$data.rows`). The MutationObserver is attached ONCE,
|
|
2775
|
+
// inside ensureColRtlWatch() at colVirtualizer CONSTRUCTION time ($onMount) — a closure
|
|
2776
|
+
// FROZEN to that render's scope on React, where `$data.rows` is a useState value.
|
|
2777
|
+
// $onMount's row-model seed (refreshRowModel()) resolves via an ASYNC setState, so AT THE
|
|
2778
|
+
// EXACT INSTANT colVirtualizer is constructed, `rows` may still be `[]` in THAT render's
|
|
2779
|
+
// closure — and unlike a `let`-hoisted ref (virtualizer, table, gridScrollEl — always read
|
|
2780
|
+
// via `.current`, always live no matter which render's closure holds the reference),
|
|
2781
|
+
// `$data.rows` is genuine React state: a plain local binding recreated fresh EVERY render,
|
|
2782
|
+
// so a function captured from an OLDER render reads that OLDER render's snapshot FOREVER,
|
|
2783
|
+
// no matter how many later renders populate it. Confirmed empirically: calling
|
|
2784
|
+
// columnVirtualizerOptions() from the frozen callback read `count` back as 0 (not 60),
|
|
2785
|
+
// collapsing getVirtualItems() to []. This is the exact class of bug `virtualItemKey`'s own
|
|
2786
|
+
// header comment already documents ("never a frozen mount-render $data.rows closure") — and
|
|
2787
|
+
// a MUTABLE-LET INDIRECTION (the `afterRowRemeasure` shape, 87-07) does NOT fix it here,
|
|
2788
|
+
// because the hazard lives INSIDE columnVirtualizerOptions()'s own dependency chain
|
|
2789
|
+
// ($data.rows), not in "which function identity gets called" — wrapping a stale call in
|
|
2790
|
+
// another layer of ref does not make an inner React-state read fresh.
|
|
2791
|
+
//
|
|
2792
|
+
// FIX: never call columnVirtualizerOptions() again from this callback at all. Read
|
|
2793
|
+
// `colVirtualizer.options` — virtual-core's OWN internal state, mutated in place on every
|
|
2794
|
+
// setOptions() call regardless of which render's closure triggered it (a REAL object
|
|
2795
|
+
// reference the SAME way `virtualizer`/`table` are, not React state) — and merge in ONLY
|
|
2796
|
+
// the corrected `isRtl`. `count`/`estimateSize`/etc. keep whatever value the LAST correct
|
|
2797
|
+
// setOptions() call gave them (from construction, or from remeasureColumnWindow()'s own
|
|
2798
|
+
// FRESH-per-render re-feed via the $watch-driven refreshRowModel cycle) — untouched,
|
|
2799
|
+
// because this dir-flip has nothing to do with row/column count. isColRtl() itself reads
|
|
2800
|
+
// only `gridScrollEl` (also a ref) and does a live DOM getComputedStyle read — no React
|
|
2801
|
+
// state involved, so it is never stale regardless of which render's closure calls it.
|
|
2802
|
+
colRtlObserver: any = null;
|
|
2803
|
+
|
|
2804
|
+
colRtlObserverEl: any = null;
|
|
2805
|
+
|
|
2806
|
+
// isColRtl(): getComputedStyle (not a raw `.getAttribute('dir')` read) so an ANCESTOR's `dir`
|
|
2807
|
+
// or a CSS `direction: rtl` rule is honored the same way the browser's own bidi layout honors
|
|
2808
|
+
// it, not merely a literal attribute on gridScrollEl itself.
|
|
2809
|
+
isColRtl = (): boolean => {
|
|
2810
|
+
if (!this.gridScrollEl || typeof getComputedStyle !== 'function') return false;
|
|
2811
|
+
return getComputedStyle(this.gridScrollEl).direction === 'rtl';
|
|
2812
|
+
};
|
|
2813
|
+
|
|
2814
|
+
// ensureColRtlWatch(): idempotent — a no-op once already attached to the CURRENT gridScrollEl
|
|
2815
|
+
// (a stable ref for the component's lifetime once mounted). Self-attaching from inside
|
|
2816
|
+
// columnVirtualizerOptions() means no new host-contract symbol is needed: listbox/combobox/
|
|
2817
|
+
// command-palette never call columnVirtualizerOptions() at all (colsWindowed() is permanently
|
|
2818
|
+
// false for them, D-20), so this code path — and the observer it creates — never exists there.
|
|
2819
|
+
ensureColRtlWatch = (): void => {
|
|
2820
|
+
if (!this.gridScrollEl || this.colRtlObserverEl === this.gridScrollEl || typeof MutationObserver !== 'function') return;
|
|
2821
|
+
if (this.colRtlObserver) this.colRtlObserver.disconnect();
|
|
2822
|
+
this.colRtlObserver = new MutationObserver(() => {
|
|
2823
|
+
if (this.colVirtualizer) this.colVirtualizer.setOptions({
|
|
2824
|
+
...this.colVirtualizer.options,
|
|
2825
|
+
isRtl: this.isColRtl()
|
|
2826
|
+
});
|
|
2827
|
+
});
|
|
2828
|
+
this.colRtlObserver.observe(this.gridScrollEl, {
|
|
2829
|
+
attributes: true,
|
|
2830
|
+
attributeFilter: ['dir']
|
|
2831
|
+
});
|
|
2832
|
+
this.colRtlObserverEl = this.gridScrollEl;
|
|
2833
|
+
};
|
|
2834
|
+
|
|
2835
|
+
// teardownColRtlWatch(): exported so the DataTable host disconnects the observer alongside
|
|
2836
|
+
// colVirtualizerCleanup() in $onUnmount — mirrors virtual-core's own _didMount()/teardown
|
|
2837
|
+
// discipline for the ResizeObserver it manages internally. No-op if never attached.
|
|
2838
|
+
teardownColRtlWatch = (): void => {
|
|
2839
|
+
if (this.colRtlObserver) this.colRtlObserver.disconnect();
|
|
2840
|
+
this.colRtlObserver = null;
|
|
2841
|
+
this.colRtlObserverEl = null;
|
|
2842
|
+
};
|
|
2843
|
+
|
|
2844
|
+
columnVirtualizerOptions = (): any => {
|
|
2845
|
+
this.ensureColRtlWatch();
|
|
2846
|
+
return {
|
|
2847
|
+
count: this.columnCount(),
|
|
2848
|
+
getScrollElement: () => this.gridScrollEl,
|
|
2849
|
+
estimateSize: (i: any) => this.columnSize(i),
|
|
2850
|
+
horizontal: true,
|
|
2851
|
+
isRtl: this.isColRtl(),
|
|
2852
|
+
observeElementRect,
|
|
2853
|
+
observeElementOffset,
|
|
2854
|
+
scrollToFn: elementScroll,
|
|
2855
|
+
measureElement,
|
|
2856
|
+
overscan: this.COL_OVERSCAN,
|
|
2857
|
+
onChange: () => {
|
|
2858
|
+
this._windowVer.value = this._windowVer.value + 1;
|
|
2859
|
+
}
|
|
2860
|
+
};
|
|
2861
|
+
};
|
|
2862
|
+
|
|
2863
|
+
// ══ Phase 87 87-07 (D-15/D-18) — content-driven auto-measure: the shared engine's FIRST
|
|
2864
|
+
// mutable top-level state. Hoisted to `useRef` PER-INSTANCE by the React emitter's
|
|
2865
|
+
// hoistModuleLet — the SAME mechanism already load-bearing for `table`, `virtualizer`,
|
|
2866
|
+
// `remeasurePending`, and `gridScrollEl` in the DataTable host (Task 1's confirmed
|
|
2867
|
+
// precedent), so two DataTable instances on one page never share an accumulator
|
|
2868
|
+
// (T-87-07-04). measuredRowTotal/measuredRowCount together give the running MEAN of every
|
|
2869
|
+
// row folded in so far; lastFedRowEstimate is the estimate value most recently pushed into
|
|
2870
|
+
// virtual-core (the hysteresis comparison baseline). ══
|
|
2871
|
+
measuredRowTotal = 0;
|
|
2872
|
+
|
|
2873
|
+
measuredRowCount = 0;
|
|
2874
|
+
|
|
2875
|
+
lastFedRowEstimate = 0;
|
|
2876
|
+
|
|
2877
|
+
// foldedRowHeights: dedupe map (full-model row index -> its last-folded height) so a row
|
|
2878
|
+
// measured TWICE (e.g. scrolled back into view) UPDATES the running mean rather than being
|
|
2879
|
+
// added a second time (T-87-07-03) — a skewed mean produces a lying scrollbar, the exact
|
|
2880
|
+
// defect this feature exists to remove.
|
|
2881
|
+
foldedRowHeights: Record<number, number> = {};
|
|
2882
|
+
|
|
2883
|
+
// ══ Gap-closure 87-10 — windowVerBumpPending / bumpWindowVer(): coalesce EVERY $data.windowVer
|
|
2884
|
+
// write behind a SINGLE microtask-deferred increment, regardless of how many callers request
|
|
2885
|
+
// one within the same synchronous JS task. ══
|
|
2886
|
+
//
|
|
2887
|
+
// ROOT CAUSE (framework-agnostic; the Solid-specific symptom this closes only EXPOSES it) —
|
|
2888
|
+
// confirmed by instrumenting the installed @tanstack/virtual-core@3.17.1 source directly
|
|
2889
|
+
// (dist/esm/index.js), not by reasoning abstractly: virtual-core's resizeItem() calls
|
|
2890
|
+
// `this.notify(false)` — synchronously invoking `virtualizerOptions().onChange` below — EVERY
|
|
2891
|
+
// TIME a measured row's real size differs from its cached one (`delta !== 0`), independent of
|
|
2892
|
+
// framework (dist/esm/index.js:836-874). remeasureWindow()'s CR-01 sweep
|
|
2893
|
+
// (packages/ui/data-table/src/virtualization.rzts) measures EVERY currently-rendered `<tr>` in
|
|
2894
|
+
// ONE for-loop BEFORE calling afterRowRemeasure() (refineRowEstimate() below) — so a single
|
|
2895
|
+
// synchronous JS task (e.g. the very first measurement pass, which transitions N never-before-
|
|
2896
|
+
// measured rows from the flat seed to their real heights) can fire onChange, and therefore an
|
|
2897
|
+
// UNCOALESCED `$data.windowVer = $data.windowVer + 1`, MANY times in a row — well BEFORE
|
|
2898
|
+
// refineRowEstimate()'s own fold-then-re-feed (which runs only AFTER that loop finishes) has
|
|
2899
|
+
// folded those same measurements into the running mean or re-fed the converged estimate into
|
|
2900
|
+
// virtual-core via setOptions(). A live trace of this exact sequence (instrumented resizeItem/
|
|
2901
|
+
// getMeasurements calls, DataTableColumnVirtualDemo, autoMeasure on) showed Vue batching 3
|
|
2902
|
+
// resizeItem calls before its ONE downstream re-render reads getMeasurements() — already
|
|
2903
|
+
// reflecting the fully-folded, re-fed state — versus Solid re-running its padTop()/padBottom()
|
|
2904
|
+
// effects SYNCHRONOUSLY and IMMEDIATELY on EVERY individual windowVer write (11 interleaved
|
|
2905
|
+
// resize-then-immediate-recompute pairs, each recompute happening mid-sweep, before
|
|
2906
|
+
// refineRowEstimate() had run even once). React/Vue/Svelte/Angular/Lit all batch their own
|
|
2907
|
+
// reactivity to at least a microtask boundary, so their downstream reads land AFTER the whole
|
|
2908
|
+
// synchronous burst (measurement sweep + fold + re-feed) completes — accidentally correct, not
|
|
2909
|
+
// correct by construction. Solid does not auto-batch a signal write made from outside a
|
|
2910
|
+
// Solid-owned event/effect context, so it is the one target where the mid-burst TORN read is
|
|
2911
|
+
// externally observable. Because `setOptions()` + `_willUpdate()` alone do NOT invalidate
|
|
2912
|
+
// virtual-core's own `getMeasurements()` memo (keyed on itemSizeCacheVersion /
|
|
2913
|
+
// getMeasurementOptions() — never on the estimateSize FUNCTION reference itself; confirmed from
|
|
2914
|
+
// the same installed source, dist/esm/index.js:585-587,624), Solid's LAST such mid-sweep
|
|
2915
|
+
// recompute is also the LAST time getMeasurements() is ever invoked for that sweep once no
|
|
2916
|
+
// further row happens to differ from its cache — so the DOM stays frozen on that stale,
|
|
2917
|
+
// pre-fold/pre-re-feed snapshot indefinitely, even though the accumulator itself has already
|
|
2918
|
+
// converged correctly (T-87-07's own confirmed finding).
|
|
2919
|
+
//
|
|
2920
|
+
// FIX: coalesce every requester of a windowVer bump — virtual-core's own onChange AND
|
|
2921
|
+
// refineRowEstimate()'s explicit re-feed bump — behind ONE microtask-deferred write, the SAME
|
|
2922
|
+
// idiom scheduleRemeasure() already uses in virtualization.rzts. This makes the render happen
|
|
2923
|
+
// EXACTLY ONCE, strictly AFTER the entire synchronous burst (including refineRowEstimate()'s
|
|
2924
|
+
// fold + re-feed) on EVERY target, by construction rather than by incidental host-framework
|
|
2925
|
+
// batching. Scoped to the ROW axis only: colVirtualizer never calls resizeItem() at all (D-06 —
|
|
2926
|
+
// column widths come from table-core's getSize() oracle, never measured from the DOM), so
|
|
2927
|
+
// columnVirtualizerOptions()'s onChange cannot hit this burst class and is left untouched.
|
|
2928
|
+
windowVerBumpPending = false;
|
|
2929
|
+
|
|
2930
|
+
bumpWindowVer = (): void => {
|
|
2931
|
+
if (this.windowVerBumpPending) return;
|
|
2932
|
+
this.windowVerBumpPending = true;
|
|
2933
|
+
const flush = () => {
|
|
2934
|
+
this.windowVerBumpPending = false;
|
|
2935
|
+
this._windowVer.value = this._windowVer.value + 1;
|
|
2936
|
+
};
|
|
2937
|
+
// Mirrors scheduleRemeasure()'s own defensive queueMicrotask-with-setTimeout-fallback
|
|
2938
|
+
// (virtualization.rzts) for environments where queueMicrotask is unavailable.
|
|
2939
|
+
if (typeof queueMicrotask !== 'undefined') queueMicrotask(flush);else setTimeout(flush, 0);
|
|
2940
|
+
};
|
|
2941
|
+
|
|
2942
|
+
// ESTIMATE_REFEED_DELTA_PX (D-15): the hysteresis threshold gating a re-feed into
|
|
2943
|
+
// virtual-core. Without it, a mean nudging by a fraction of a pixel on every fold would
|
|
2944
|
+
// re-feed on every window commit — the T-87-07-01 DoS control, paired with virtual-core's
|
|
2945
|
+
// own measureElement/resizeItem idempotence (see refineRowEstimate() below).
|
|
2946
|
+
ESTIMATE_REFEED_DELTA_PX = 4;
|
|
2947
|
+
|
|
2948
|
+
// estimateRowSize(i) (D-15/D-17): the estimateSize() resolver. MUST check !autoMeasureOn()
|
|
2949
|
+
// FIRST so the off path touches zero accumulator state and returns $props.estimateRowHeight
|
|
2950
|
+
// verbatim (D-17's byte-behavioral no-op). The zero-measurements case (first paint,
|
|
2951
|
+
// regardless of autoMeasure) still returns the seed — the very first render has nothing
|
|
2952
|
+
// measured yet either way (D-15).
|
|
2953
|
+
estimateRowSize = (i: number): number => {
|
|
2954
|
+
if (!this.autoMeasureOn()) return this.estimateRowHeight;
|
|
2955
|
+
if (this.measuredRowCount === 0) return this.estimateRowHeight;
|
|
2956
|
+
return Math.round(this.measuredRowTotal / this.measuredRowCount);
|
|
2957
|
+
};
|
|
2958
|
+
|
|
2959
|
+
// foldMeasuredRow(index, height): fold ONE measured row's height into the running-mean
|
|
2960
|
+
// accumulator, UPDATING (not double-adding) an already-folded index (T-87-07-03).
|
|
2961
|
+
foldMeasuredRow = (index: number, height: number): void => {
|
|
2962
|
+
const prev = this.foldedRowHeights[index];
|
|
2963
|
+
if (prev === height) return;
|
|
2964
|
+
if (prev == null) {
|
|
2965
|
+
this.measuredRowTotal = this.measuredRowTotal + height;
|
|
2966
|
+
this.measuredRowCount = this.measuredRowCount + 1;
|
|
2967
|
+
} else {
|
|
2968
|
+
this.measuredRowTotal = this.measuredRowTotal - prev + height;
|
|
2969
|
+
}
|
|
2970
|
+
this.foldedRowHeights[index] = height;
|
|
2971
|
+
};
|
|
2972
|
+
|
|
2973
|
+
// refineRowEstimate() (D-15/D-16, Phase 87 87-07): the shared engine's post-measurement
|
|
2974
|
+
// orchestration, called via the host's afterRowRemeasure hook (see the HOST CONTRACT note
|
|
2975
|
+
// above for why this is never called directly FROM virtualization.rzts). Folds every
|
|
2976
|
+
// CURRENTLY RENDERED row's REAL measurement (from virtual-core's own getMeasurements() —
|
|
2977
|
+
// never a DOM read), then hysteresis-gates a re-feed into virtual-core, reusing the EXACT
|
|
2978
|
+
// `virtualizer.scrollState` bail remeasureWindow() already applies (T-87-07-02) rather than
|
|
2979
|
+
// inventing a second gating mechanism, and finally (Task 3, D-16) anchor-corrects scrollTop
|
|
2980
|
+
// around the re-feed so the row under the user's eye does not visibly lurch. Entirely a
|
|
2981
|
+
// no-op when !autoMeasureOn() — the off path never folds a measurement, touches
|
|
2982
|
+
// lastFedRowEstimate, or adjusts scrollTop (D-17).
|
|
2983
|
+
refineRowEstimate = (): void => {
|
|
2984
|
+
if (!this.autoMeasureOn() || !this.virtualizer) return;
|
|
2985
|
+
const items = this.virtualizer.getVirtualItems();
|
|
2986
|
+
const measurements = this.virtualizer.getMeasurements();
|
|
2987
|
+
for (let i = 0; i < items.length; i++) {
|
|
2988
|
+
const idx = items[i].index;
|
|
2989
|
+
const m = measurements && measurements[idx];
|
|
2990
|
+
if (m) this.foldMeasuredRow(idx, m.size);
|
|
2991
|
+
}
|
|
2992
|
+
// T-87-07-02: never re-feed while a PROGRAMMATIC scroll is in flight — the exact bail
|
|
2993
|
+
// remeasureWindow() already applies (virtualizer.scrollState is non-null only during
|
|
2994
|
+
// scrollToIndex/scrollToOffset).
|
|
2995
|
+
if (this.virtualizer.scrollState) return;
|
|
2996
|
+
const est = this.estimateRowSize(0);
|
|
2997
|
+
if (Math.abs(est - this.lastFedRowEstimate) < this.ESTIMATE_REFEED_DELTA_PX) return;
|
|
2998
|
+
// D-16 anchor capture: the topmost rendered row's full-model index + its CURRENT offset,
|
|
2999
|
+
// taken from virtual-core's own measurements (never a getBoundingClientRect() layout read
|
|
3000
|
+
// — slower, and inconsistent with the measurements the spacer math already uses) — taken
|
|
3001
|
+
// BEFORE the re-feed so the correction below can compute exactly the delta the re-feed
|
|
3002
|
+
// itself introduces. Not restricted to scroll offset 0 (D-16 explicit): a table restored
|
|
3003
|
+
// mid-scroll, or whose content changes height after scrolling, must refine too.
|
|
3004
|
+
const anchorIndex = items.length ? items[0].index : -1;
|
|
3005
|
+
const anchorStart = items.length ? items[0].start : 0;
|
|
3006
|
+
this.virtualizer.setOptions(this.virtualizerOptions());
|
|
3007
|
+
this.virtualizer._willUpdate();
|
|
3008
|
+
this.lastFedRowEstimate = est;
|
|
3009
|
+
// D-16 anchor correction: read the SAME index's fresh measurement post-re-feed and adjust
|
|
3010
|
+
// scrollTop by the delta so the row under the user's eye stays visually put. Guarded by
|
|
3011
|
+
// the SAME path the re-feed itself is gated on (autoMeasureOn() && scrollState === null,
|
|
3012
|
+
// both already checked above), so it can never fight a programmatic scroll (T-87-07-02).
|
|
3013
|
+
if (anchorIndex >= 0 && this.gridScrollEl) {
|
|
3014
|
+
const freshMeasurements = this.virtualizer.getMeasurements();
|
|
3015
|
+
const fresh = freshMeasurements && freshMeasurements[anchorIndex];
|
|
3016
|
+
if (fresh) {
|
|
3017
|
+
const delta = fresh.start - anchorStart;
|
|
3018
|
+
if (delta !== 0) this.gridScrollEl.scrollTop = this.gridScrollEl.scrollTop + delta;
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
// Explicit windowVer bump (87-07, Rule 1): virtual-core's own setOptions()+_willUpdate()
|
|
3022
|
+
// re-feed above does NOT itself invoke notify()/onChange (confirmed from the installed
|
|
3023
|
+
// source: _willUpdate() is a no-op once `scrollElement` is already set, dist/esm/
|
|
3024
|
+
// index.js:389-393), so a re-feed needs its own explicit trigger to make the spacer
|
|
3025
|
+
// re-derivation happen at all. Routed through bumpWindowVer() (87-10, see its own comment
|
|
3026
|
+
// above) rather than a raw `$data.windowVer = $data.windowVer + 1` — coalescing this bump
|
|
3027
|
+
// with any onChange-triggered ones already pending from the SAME synchronous sweep is what
|
|
3028
|
+
// makes the eventual render land strictly AFTER this fold + re-feed has fully landed, on
|
|
3029
|
+
// every target (the 87-10 fix for the confirmed Solid-specific rendering gap).
|
|
3030
|
+
this.bumpWindowVer();
|
|
3031
|
+
};
|
|
3032
|
+
|
|
2340
3033
|
// The FULL virtualizer options. virtual-core's setOptions REPLACES options with
|
|
2341
3034
|
// `{ ...defaults, ...opts }` (it does NOT merge with prior options — verified in the 3.17.1
|
|
2342
3035
|
// source), so the re-feed MUST pass the complete set, exactly like every TanStack adapter.
|
|
2343
3036
|
// Returned `any` (the currentState() precedent) so the strict bundled-leaf tsc does not choke
|
|
2344
|
-
// on virtual-core's generic option inference. onChange
|
|
2345
|
-
//
|
|
3037
|
+
// on virtual-core's generic option inference. onChange's windowVer write is routed through
|
|
3038
|
+
// bumpWindowVer() (87-10) rather than a raw `$data.x = $data.x + 1` — resizeItem() can call
|
|
3039
|
+
// this onChange MANY times in a single synchronous sweep (once per row whose real measured
|
|
3040
|
+
// size differs from its cache, e.g. every never-before-measured row in the FIRST window),
|
|
3041
|
+
// and coalescing those into one microtask-deferred write is what keeps every target's render
|
|
3042
|
+
// landing strictly AFTER the whole sweep (see bumpWindowVer()'s own comment for the confirmed
|
|
3043
|
+
// Solid-specific rendering gap this closes). The React emitter still lowers the underlying
|
|
3044
|
+
// `$data.windowVer = $data.windowVer + 1` to functional setState — correct even deferred to a
|
|
3045
|
+
// microtask, exactly as it was correct from a mount closure before.
|
|
2346
3046
|
virtualizerOptions = (): any => ({
|
|
2347
3047
|
count: this.windowSource().length,
|
|
2348
3048
|
getScrollElement: () => this.gridScrollEl,
|
|
2349
|
-
estimateSize: () => this.
|
|
3049
|
+
estimateSize: (i: any) => this.estimateRowSize(i),
|
|
2350
3050
|
observeElementRect,
|
|
2351
3051
|
observeElementOffset,
|
|
2352
3052
|
scrollToFn: elementScroll,
|
|
@@ -2354,7 +3054,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2354
3054
|
overscan: 8,
|
|
2355
3055
|
getItemKey: this.virtualItemKey,
|
|
2356
3056
|
onChange: () => {
|
|
2357
|
-
this.
|
|
3057
|
+
this.bumpWindowVer();
|
|
2358
3058
|
// CR-01: re-observe the freshly-committed window so RECYCLED rows get measured.
|
|
2359
3059
|
// virtual-core only observe()s a node you explicitly hand to measureElement (it does
|
|
2360
3060
|
// NOT auto-discover rendered rows — measureElement is the SOLE caller of
|
|
@@ -2406,14 +3106,23 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2406
3106
|
void this._windowVer.value;
|
|
2407
3107
|
void this._editVer.value;
|
|
2408
3108
|
if (!this.virtualizer) {
|
|
2409
|
-
//
|
|
2410
|
-
//
|
|
2411
|
-
//
|
|
2412
|
-
//
|
|
2413
|
-
|
|
3109
|
+
// Rows OFF (Phase 87 D-04: this now includes the colsWindowed()-only path, since the
|
|
3110
|
+
// wrapper template is entered whenever isWindowed(), not just rowsWindowed() — the row
|
|
3111
|
+
// virtualizer is never constructed when only the column axis is windowed, D-04) → the FULL
|
|
3112
|
+
// set, with a SYNTHETIC `vi.index` set to each row's array position (matching rowIndexOf's
|
|
3113
|
+
// own `$data.rows.indexOf(row)` semantics exactly, since $data.rows IS windowSource()'s
|
|
3114
|
+
// output here). Every windowed body binding reads wr.vi.index (data-row, aria-rowindex,
|
|
3115
|
+
// colIndexOf, isEditing, the fill handle) — a bare `null` there is a hard crash the moment
|
|
3116
|
+
// this branch is reached with the wrapper mounted, which colsWindowed()-only now does.
|
|
3117
|
+
// Row-virtual ON but the virtualizer is not yet constructed (pre-$onMount first paint) →
|
|
3118
|
+
// render NOTHING so the template never dereferences a not-yet-real `vi`; the rows appear on
|
|
3119
|
+
// the first onChange after _didMount.
|
|
3120
|
+
if (!this.rowsWindowed()) {
|
|
2414
3121
|
const rowList = this._rows.value || [];
|
|
2415
|
-
return rowList.map((r: any) => ({
|
|
2416
|
-
vi:
|
|
3122
|
+
return rowList.map((r: any, i: any) => ({
|
|
3123
|
+
vi: {
|
|
3124
|
+
index: i
|
|
3125
|
+
},
|
|
2417
3126
|
row: r
|
|
2418
3127
|
}));
|
|
2419
3128
|
}
|
|
@@ -2470,7 +3179,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2470
3179
|
// and re-derives on the pin/unpin transition (the D-02 spacer subtraction below).
|
|
2471
3180
|
void this._windowVer.value;
|
|
2472
3181
|
void this._editVer.value;
|
|
2473
|
-
if (!this.
|
|
3182
|
+
if (!this.rowsWindowed() || !this.virtualizer) return 0;
|
|
2474
3183
|
const items = this.virtualizer.getVirtualItems();
|
|
2475
3184
|
let pad = items.length ? items[0].start : 0;
|
|
2476
3185
|
// D-02 spacer subtraction: when the pinned editing row sits ABOVE the window it is rendered
|
|
@@ -2491,7 +3200,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2491
3200
|
// on pin/unpin.
|
|
2492
3201
|
void this._windowVer.value;
|
|
2493
3202
|
void this._editVer.value;
|
|
2494
|
-
if (!this.
|
|
3203
|
+
if (!this.rowsWindowed() || !this.virtualizer) return 0;
|
|
2495
3204
|
const items = this.virtualizer.getVirtualItems();
|
|
2496
3205
|
if (!items.length) return 0;
|
|
2497
3206
|
let pad = this.virtualizer.getTotalSize() - items[items.length - 1].end;
|
|
@@ -2525,12 +3234,141 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2525
3234
|
// rowIsOutsideWindow(r): is the full-model row index r absent from the currently rendered
|
|
2526
3235
|
// window? Used by the scroll-then-focus seam (req-5 — scroll a far row in before focusing).
|
|
2527
3236
|
rowIsOutsideWindow = (r: any) => {
|
|
2528
|
-
if (!this.
|
|
3237
|
+
if (!this.rowsWindowed() || !this.virtualizer) return false;
|
|
2529
3238
|
const items = this.virtualizer.getVirtualItems();
|
|
2530
3239
|
for (const it of items as any) if (it.index === r) return false;
|
|
2531
3240
|
return true;
|
|
2532
3241
|
};
|
|
2533
3242
|
|
|
3243
|
+
// ══ Phase 87 87-04 — the column-axis analogs of windowedRows()/padTop()/padBottom()/
|
|
3244
|
+
// rowIsOutsideWindow() above. The column axis has no "row-shaped" identity to carry alongside
|
|
3245
|
+
// a VirtualItem (a column is not a full-model object the way a row is), so windowedColIndices()
|
|
3246
|
+
// returns bare ABSOLUTE leaf-column indices; the template resolves each index back to a header/
|
|
3247
|
+
// cell through the host's own header-group / visibleCellsFor lookups (D-08/D-09). ══
|
|
3248
|
+
// windowedColIndices(): the ordered array of ABSOLUTE leaf-column indices to render.
|
|
3249
|
+
windowedColIndices = () => {
|
|
3250
|
+
// SUBSCRIBE FIRST (the windowedRows() discipline): colVirtualizer is a non-reactive `let`,
|
|
3251
|
+
// null at first render, so a $data.windowVer read placed BELOW the guards below would mean
|
|
3252
|
+
// Solid's <For> / Svelte's {#each} accessors never subscribe — the column window would stay
|
|
3253
|
+
// blank forever on exactly those two targets. Also subscribe to editVer (mirrors the row
|
|
3254
|
+
// axis's pin/unpin re-derive — the D-10 forced-column union lands on top of this in 87-05).
|
|
3255
|
+
void this._windowVer.value;
|
|
3256
|
+
void this._editVer.value;
|
|
3257
|
+
if (!this.colsWindowed()) {
|
|
3258
|
+
// Columns OFF (or rows-only windowed) → every absolute index, the degrade-to-full-set
|
|
3259
|
+
// path that keeps the caller (windowedCells / the header loop) axis-agnostic.
|
|
3260
|
+
const n = this.columnCount();
|
|
3261
|
+
const out = [];
|
|
3262
|
+
for (let i = 0; i < n; i++) out.push(i);
|
|
3263
|
+
return out;
|
|
3264
|
+
}
|
|
3265
|
+
// Columns windowed but the column virtualizer is not yet constructed (pre-$onMount first
|
|
3266
|
+
// paint) → render NOTHING, matching windowedRows()'s pre-mount behavior.
|
|
3267
|
+
if (!this.colVirtualizer) return [];
|
|
3268
|
+
const items = this.colVirtualizer.getVirtualItems();
|
|
3269
|
+
const idx = items.map((it: any) => it.index);
|
|
3270
|
+
// D-10 forced-column union (host hook — empty array until 87-05 gives it a real body):
|
|
3271
|
+
// dedupe-then-append any forced index not already in the virtualized window.
|
|
3272
|
+
const forced = this.forcedColumns();
|
|
3273
|
+
for (let i = 0; i < forced.length; i++) {
|
|
3274
|
+
if (idx.indexOf(forced[i]) === -1) idx.push(forced[i]);
|
|
3275
|
+
}
|
|
3276
|
+
// Sorted ascending: the body <td> sequence and the header <th> sequence both follow visual
|
|
3277
|
+
// left-to-right order, and the virtual items + forced union are not guaranteed sorted.
|
|
3278
|
+
idx.sort((a: any, b: any) => a - b);
|
|
3279
|
+
return idx;
|
|
3280
|
+
};
|
|
3281
|
+
|
|
3282
|
+
// colPadLeft() / colPadRight() (D-06): the horizontal spacer <td>/<th> widths — the leading
|
|
3283
|
+
// spacer occupies items[0].start, the trailing spacer the gap between the last rendered
|
|
3284
|
+
// item's end and getTotalSize(). Mirrors padTop()/padBottom() exactly, INCLUDING the D-02-style
|
|
3285
|
+
// pin subtraction (87-05): forcedColumns() can return MULTIPLE forced indices (pinned + active +
|
|
3286
|
+
// editing, not just one pin), so each forced index outside the virtual window is checked
|
|
3287
|
+
// individually. A forced column rendered outside the window contributes its OWN real width to
|
|
3288
|
+
// the row (via its <th>/<td>'s own inline width under table-layout:fixed), so that width must
|
|
3289
|
+
// come OUT of the spacer or the row's total overshoots getTotalSize() and the horizontal
|
|
3290
|
+
// scrollbar lengthens on every such scroll. WR-01: decide before/after the window by comparing
|
|
3291
|
+
// the forced INDEX against the window's first/last item INDEX, never by start-OFFSET —
|
|
3292
|
+
// measurement drift makes the offset comparison wrong for the variable-width case (mirrors
|
|
3293
|
+
// padBottom()'s existing WR-01 discipline). A forced column's width is columnSize(i) (the
|
|
3294
|
+
// table-core size oracle, D-06's authoritative source), never a DOM measurement.
|
|
3295
|
+
colPadLeft = () => {
|
|
3296
|
+
void this._windowVer.value;
|
|
3297
|
+
void this._editVer.value;
|
|
3298
|
+
if (!this.colsWindowed() || !this.colVirtualizer) return 0;
|
|
3299
|
+
const items = this.colVirtualizer.getVirtualItems();
|
|
3300
|
+
let pad = items.length ? items[0].start : 0;
|
|
3301
|
+
if (items.length) {
|
|
3302
|
+
const firstIdx = items[0].index;
|
|
3303
|
+
const forced = this.forcedColumns();
|
|
3304
|
+
for (let i = 0; i < forced.length; i++) {
|
|
3305
|
+
if (forced[i] < firstIdx) pad = pad - this.columnSize(forced[i]);
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
return pad < 0 ? 0 : pad;
|
|
3309
|
+
};
|
|
3310
|
+
|
|
3311
|
+
colPadRight = () => {
|
|
3312
|
+
void this._windowVer.value;
|
|
3313
|
+
void this._editVer.value;
|
|
3314
|
+
if (!this.colsWindowed() || !this.colVirtualizer) return 0;
|
|
3315
|
+
const items = this.colVirtualizer.getVirtualItems();
|
|
3316
|
+
if (!items.length) return 0;
|
|
3317
|
+
let pad = this.colVirtualizer.getTotalSize() - items[items.length - 1].end;
|
|
3318
|
+
const lastIdx = items[items.length - 1].index;
|
|
3319
|
+
const forced = this.forcedColumns();
|
|
3320
|
+
for (let i = 0; i < forced.length; i++) {
|
|
3321
|
+
if (forced[i] > lastIdx) pad = pad - this.columnSize(forced[i]);
|
|
3322
|
+
}
|
|
3323
|
+
return pad < 0 ? 0 : pad;
|
|
3324
|
+
};
|
|
3325
|
+
|
|
3326
|
+
// colIsOutsideWindow(c): is the absolute leaf-column index c absent from the currently
|
|
3327
|
+
// rendered column window? The column-axis analog of rowIsOutsideWindow(r) (D-12's
|
|
3328
|
+
// scroll-then-focus seam, wired starting 87-06). Gated on colsWindowed() (never a bare prop
|
|
3329
|
+
// truthiness read — the 87-02 prohibition gate).
|
|
3330
|
+
colIsOutsideWindow = (c: any) => {
|
|
3331
|
+
if (!this.colsWindowed() || !this.colVirtualizer) return false;
|
|
3332
|
+
const items = this.colVirtualizer.getVirtualItems();
|
|
3333
|
+
for (const it of items as any) if (it.index === c) return false;
|
|
3334
|
+
return true;
|
|
3335
|
+
};
|
|
3336
|
+
|
|
3337
|
+
// afterRowRemeasure (Phase 87 87-07, D-15): a mutable-`let` indirection so
|
|
3338
|
+
// virtualization.rzts's remeasureWindow() (a relative-partial CONST, textually EARLIER
|
|
3339
|
+
// than windowing.rzts in the merged per-target output) can trigger the shared engine's
|
|
3340
|
+
// post-measurement fold + hysteresis re-feed (refineRowEstimate(), windowing.rzts) without
|
|
3341
|
+
// calling a bare-specifier-partial CONST directly — the exact forward-reference TDZ class
|
|
3342
|
+
// remeasureColumnWindow()'s own comment below documents for columnVirtualizerOptions(). A
|
|
3343
|
+
// mutable `let` hoists to `useRef` on React and never enters a useCallback's dependency
|
|
3344
|
+
// array, sidestepping the hazard there.
|
|
3345
|
+
//
|
|
3346
|
+
// DECLARED HERE — a plain top-level statement placed AFTER both import lines above, NOT
|
|
3347
|
+
// grouped with the other windowing `let`s near the top of this file, and NOT deferred into
|
|
3348
|
+
// $onMount. Two Rule-1 bugs were found and fixed in this exact spot this task, both via the
|
|
3349
|
+
// VR suite:
|
|
3350
|
+
// (a) A top-level `let X = refineRowEstimate` placed BEFORE the import statements (where
|
|
3351
|
+
// the other windowing `let`s live) reads a windowing.rzts symbol at its OWN
|
|
3352
|
+
// declaration's evaluation time. React's own emitter re-groups DataTable.rozie's
|
|
3353
|
+
// entire top-level script AFTER both inlined partials regardless of source position,
|
|
3354
|
+
// so it tolerated that placement — but Vue/Svelte/Solid/Angular/Lit splice each
|
|
3355
|
+
// partial's content at its IMPORT STATEMENT's source position, so a `let` positioned
|
|
3356
|
+
// BEFORE the import genuinely executes before `refineRowEstimate` exists:
|
|
3357
|
+
// `ReferenceError: Cannot access 'refineRowEstimate' before initialization`. Moving the
|
|
3358
|
+
// declaration to AFTER the windowing.rzts import fixes every non-React target — the
|
|
3359
|
+
// splice has already run by this point in the source.
|
|
3360
|
+
// (b) Deferring the assignment into $onMount instead (`afterRowRemeasure = refineRowEstimate`
|
|
3361
|
+
// inside the mount body, mirroring `refreshRowModel`) fixed (a) but broke REACT: it
|
|
3362
|
+
// triggered the react emitter's mount-referenced-helper stable-ref synthesis for
|
|
3363
|
+
// `refineRowEstimate` (a NEW code path for a plain script helper, not the `$computed`
|
|
3364
|
+
// case that mechanism was built for), producing a genuine
|
|
3365
|
+
// `ReferenceError: Cannot access '...' before initialization` in the component's
|
|
3366
|
+
// render body. Reverted; the emitter path itself is out of this plan's scope.
|
|
3367
|
+
// This placement (top-level, immediately after the windowing.rzts import) is therefore the
|
|
3368
|
+
// ONE spot that is simultaneously safe for React (unaffected by source position) and every
|
|
3369
|
+
// other target (past the import-position splice).
|
|
3370
|
+
afterRowRemeasure = this.refineRowEstimate;
|
|
3371
|
+
|
|
2534
3372
|
// ── Sort/filter live-announcement (#14) ─────────────────────────────────────────────
|
|
2535
3373
|
// A polite aria-live announcement whenever the consumer changes sorting or filtering, so a
|
|
2536
3374
|
// screen-reader user hears that the rows were reordered / narrowed (which is otherwise silent).
|
|
@@ -2740,10 +3578,18 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2740
3578
|
|
|
2741
3579
|
// Template helpers reading the resolved column-def metadata by id (plain fns — used
|
|
2742
3580
|
// in template predicates + interpolation; uniform on all 6, no $computed alias trap).
|
|
3581
|
+
// B1 (quick 260906-afh): reads the O(1) def-lookup index (defIndex(), columnBuilders.rzts)
|
|
3582
|
+
// instead of linearly scanning columnDefs() — which only ever held TOP-LEVEL entries, so a
|
|
3583
|
+
// NESTED group leaf (a `columns:` group child) was invisible to every caller (headerLabel,
|
|
3584
|
+
// columnIsFilterable, editMetaOf/columnEditable/editorTypeOf/editorOptionsOf) and silently
|
|
3585
|
+
// took the "no def" fallback. defIndex() resolves both top-level AND nested leaf ids. This
|
|
3586
|
+
// also removes the per-cell O(N) scan D-23 left in place — defIndex() is memoized in the
|
|
3587
|
+
// SAME cache-invalidation block as columnDefs() itself, never rebuilt per call.
|
|
2743
3588
|
defFor = (colId: any) => {
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
3589
|
+
if (colId == null) return null;
|
|
3590
|
+
const idx = this.defIndex();
|
|
3591
|
+
const d = idx[String(colId)];
|
|
3592
|
+
return d != null ? d : null;
|
|
2747
3593
|
};
|
|
2748
3594
|
|
|
2749
3595
|
// Per-row visible cells for the body loop. table-core memoizes row objects by id,
|
|
@@ -3105,6 +3951,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3105
3951
|
// rowIsGrouped: this flattened row is a group-header row.
|
|
3106
3952
|
rowIsGrouped = (row: any) => !!(this.tick() >= 0 && row && row.getIsGrouped && row.getIsGrouped());
|
|
3107
3953
|
|
|
3954
|
+
// rowIndexIsGrouped (quick 260906-cvo, C1/C2): THE single row-level editability/writability
|
|
3955
|
+
// predicate — a group-header row is a SYNTHETIC row whose `original` is table-core's
|
|
3956
|
+
// `leafRows[0].original` (the same fact cellSlotRow's header comment documents), so any
|
|
3957
|
+
// edit or bulk write resolved through it silently lands on an UNRELATED record. Every edit/
|
|
3958
|
+
// write guard in this component calls THIS predicate — never a re-implemented inline
|
|
3959
|
+
// `getIsGrouped()` check — so the invariant has one definition. Indexes into the SAME
|
|
3960
|
+
// `$data.rows` flattened row list rowIsGrouped reads.
|
|
3961
|
+
rowIndexIsGrouped = (rowIndex: any) => this.rowIsGrouped((this._rows.value || [])[rowIndex]);
|
|
3962
|
+
|
|
3108
3963
|
// groupingActive: grouping is currently engaged (a non-empty ordered key list). Drives the
|
|
3109
3964
|
// data-group-leaf marker so it is ABSENT when ungrouped (byte-identical-off, req-10).
|
|
3110
3965
|
groupingActive = () => this.tick() >= 0 && (this.currentState().grouping || []).length > 0;
|
|
@@ -3117,34 +3972,119 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3117
3972
|
|
|
3118
3973
|
cellIsAggregated = (cellCtx: any) => !!(this.tick() >= 0 && cellCtx && cellCtx.getIsAggregated && cellCtx.getIsAggregated());
|
|
3119
3974
|
|
|
3120
|
-
// cellIsPlaceholder:
|
|
3121
|
-
//
|
|
3122
|
-
//
|
|
3123
|
-
//
|
|
3124
|
-
//
|
|
3975
|
+
// cellIsPlaceholder: VERIFIED against @tanstack/table-core@8.21.3 (ColumnGrouping.js:120-121,
|
|
3976
|
+
// quick 260906-afh premise correction). A placeholder cell is a cell IN A GROUPING COLUMN
|
|
3977
|
+
// that is NOT the row's OWN grouping column — `!cell.getIsGrouped() && column.getIsGrouped()`.
|
|
3978
|
+
// That is true both on a group-header row (for every OTHER grouping column) and on every leaf
|
|
3979
|
+
// row (for every grouping column) — it is NOT "never true on a group-header row" and it is NOT
|
|
3980
|
+
// "any non-grouped, non-aggregated cell". The operative consequence for this component: a
|
|
3981
|
+
// placeholder can only EVER be a grouping-column cell, so this branch can never reach a
|
|
3982
|
+
// non-grouping column — it does NOT and CANNOT guard the AGGREGATED-cell `row` leak
|
|
3983
|
+
// (cellSlotRow, below) fixes. Where it DOES apply (e.g. under multi-level grouping, the
|
|
3984
|
+
// OTHER grouping column on a group-header row), table-core still fills cell.getValue() with
|
|
3985
|
+
// the FIRST leaf row's value (cell.getValue() leaks e.g. "Oslo" onto a region-level group
|
|
3986
|
+
// line whose grouping is 'region', not 'city'); this branch renders BLANK via a dedicated
|
|
3987
|
+
// empty template so that leaked value never paints. Tick-gated exactly like cellIsGrouped so
|
|
3988
|
+
// the group chrome re-derives on a re-pull on the fine-grained targets (Solid/Lit).
|
|
3125
3989
|
cellIsPlaceholder = (cellCtx: any) => !!(this.tick() >= 0 && cellCtx && cellCtx.getIsPlaceholder && cellCtx.getIsPlaceholder());
|
|
3126
3990
|
|
|
3127
|
-
// groupSubRowCount: the number of underlying LEAF RECORDS under a group-header row (the
|
|
3128
|
-
// shown in the header, e.g. "North (40)"). row.subRows is the IMMEDIATE members — for
|
|
3129
|
-
// grouping those are sub-GROUPS, not records, so "North" with 2 categories / 40
|
|
3130
|
-
// "North (2)"
|
|
3131
|
-
// subRows
|
|
3132
|
-
//
|
|
3133
|
-
|
|
3991
|
+
// groupSubRowCount: the number of underlying TRUE LEAF RECORDS under a group-header row (the
|
|
3992
|
+
// count shown in the header, e.g. "North (40)"). row.subRows is the IMMEDIATE members — for
|
|
3993
|
+
// MULTI-LEVEL grouping those are sub-GROUPS, not records, so "North" with 2 categories / 40
|
|
3994
|
+
// records would show "North (2)" off subRows alone. row.getLeafRows() is table-core's
|
|
3995
|
+
// `flattenBy(row.subRows, d => d.subRows)` (ColumnGrouping.js / utils.js, verified against
|
|
3996
|
+
// @tanstack/table-core@8.21.3) — it pushes EVERY node it walks, INCLUDING intermediate group
|
|
3997
|
+
// nodes, so under multi-level grouping it over-counts by exactly the number of sub-group
|
|
3998
|
+
// nodes between the row and its true leaves (C4, quick 260906-cvo: North with 2 city
|
|
3999
|
+
// sub-groups + 3 records reported 5, not 3). `row.getIsGrouped()` is `!!row.groupingColumnId`
|
|
4000
|
+
// — precisely the predicate that separates a group node from a real record — so filter the
|
|
4001
|
+
// flattened list on it rather than hand-rolling a recursive `subRows` walk (getLeafRows()
|
|
4002
|
+
// already flattens; the defect is purely that it does not filter). Single-level grouping is
|
|
4003
|
+
// numerically UNCHANGED: a leaf row has no `subRows`, so no node was ever miscounted there.
|
|
4004
|
+
groupSubRowCount = (row: any) => {
|
|
4005
|
+
if (row && row.getLeafRows) {
|
|
4006
|
+
const leaves = row.getLeafRows();
|
|
4007
|
+
let n = 0;
|
|
4008
|
+
for (let i = 0; i < leaves.length; i++) {
|
|
4009
|
+
const r = leaves[i];
|
|
4010
|
+
if (!(r && r.getIsGrouped && r.getIsGrouped())) n = n + 1;
|
|
4011
|
+
}
|
|
4012
|
+
return n;
|
|
4013
|
+
}
|
|
4014
|
+
return row && row.subRows ? row.subRows.length : 0;
|
|
4015
|
+
};
|
|
4016
|
+
|
|
4017
|
+
// ── B2 (quick 260906-afh) — group-header row #cell slot descriptor ─────────────────────
|
|
4018
|
+
// PROBLEM: table-core builds a flattened group-header row via
|
|
4019
|
+
// `createRow(table, id, leafRows[0].original, …)` (getGroupedRowModel.js), so `row.original`
|
|
4020
|
+
// on a group row IS the FIRST LEAF's record. The #cell scoped slot's existing 4 call sites
|
|
4021
|
+
// all bound `:row="row.original"` (or `wr.row.original`), so an AGGREGATED cell (a
|
|
4022
|
+
// non-grouping column on a group row — cellIsGrouped()===false, cellIsPlaceholder()===false,
|
|
4023
|
+
// so it takes the #cell r-else branch, same as any ordinary leaf cell) silently handed a
|
|
4024
|
+
// consumer template the WRONG record's fields (T-AFH-05). The grouped cell itself (the
|
|
4025
|
+
// group's own column, .rdt-group-value) has the same #cell binding and the same leak.
|
|
4026
|
+
//
|
|
4027
|
+
// FIX: `cellSlotRow(row)` returns a STABLE GROUP DESCRIPTOR — never `row.original` — for a
|
|
4028
|
+
// group-header row, and `row.original` unchanged for every other row. The descriptor is an
|
|
4029
|
+
// OBJECT, never `null` (T-AFH-06 — a consumer already reading `row.foo` would throw on
|
|
4030
|
+
// Vue/Angular if handed null).
|
|
4031
|
+
//
|
|
4032
|
+
// Descriptor cache (T-AFH-02/T-AFH-04): a null-prototype map keyed on `String(row.id)`,
|
|
4033
|
+
// invalidated wholesale whenever `$data.rowModelVer` (bumped by every refreshRowModel, the
|
|
4034
|
+
// SAME reactive re-derivation discipline visibleCellsFor/rowIsGrouped use) differs from the
|
|
4035
|
+
// generation the map was built for — so the descriptor's REFERENCE IDENTITY is stable
|
|
4036
|
+
// across every cell of one group row within a row-model generation (an unstable reference
|
|
4037
|
+
// would thrash the fine-grained targets, Solid/Lit) and refreshed the moment the grouping
|
|
4038
|
+
// changes. Group row ids are built from consumer grouping VALUES, so the map MUST be
|
|
4039
|
+
// null-prototype — a `__proto__` grouping value must not reach Object.prototype.
|
|
4040
|
+
groupRowDescriptorCache: any = null;
|
|
4041
|
+
|
|
4042
|
+
groupRowDescriptorCacheVer: any = undefined;
|
|
4043
|
+
|
|
4044
|
+
groupRowDescriptor = (row: any) => {
|
|
4045
|
+
const ver = this._rowModelVer.value;
|
|
4046
|
+
if (!this.groupRowDescriptorCache || this.groupRowDescriptorCacheVer !== ver) {
|
|
4047
|
+
this.groupRowDescriptorCache = Object.create(null);
|
|
4048
|
+
this.groupRowDescriptorCacheVer = ver;
|
|
4049
|
+
}
|
|
4050
|
+
const key = String(row.id);
|
|
4051
|
+
let d = this.groupRowDescriptorCache[key];
|
|
4052
|
+
if (!d) {
|
|
4053
|
+
const groupingColumnId = row.groupingColumnId != null ? row.groupingColumnId : '';
|
|
4054
|
+
const groupingValue = row.getGroupingValue ? row.getGroupingValue(groupingColumnId) : row.groupingValue;
|
|
4055
|
+
d = {
|
|
4056
|
+
isGroupRow: true,
|
|
4057
|
+
groupId: row.id,
|
|
4058
|
+
groupingColumnId,
|
|
4059
|
+
groupingValue,
|
|
4060
|
+
leafCount: this.groupSubRowCount(row)
|
|
4061
|
+
};
|
|
4062
|
+
this.groupRowDescriptorCache[key] = d;
|
|
4063
|
+
}
|
|
4064
|
+
return d;
|
|
4065
|
+
};
|
|
4066
|
+
|
|
4067
|
+
// cellSlotRow: the #cell slot's `:row=` value. A group-header row gets the stable
|
|
4068
|
+
// descriptor above (never the leaked first-leaf record); every other row is unchanged.
|
|
4069
|
+
cellSlotRow = (row: any) => this.rowIsGrouped(row) ? this.groupRowDescriptor(row) : row ? row.original : null;
|
|
3134
4070
|
|
|
3135
4071
|
// groupingKeys: the live ordered grouping array — slot prop for the headless #groupBar + the
|
|
3136
4072
|
// default styled-token reflection. Reads currentState() ($props.grouping ?? $data.groupingDefault),
|
|
3137
4073
|
// both reactive sources, so the bar re-renders on a grouping change across all six targets.
|
|
3138
4074
|
groupingKeys = () => this.currentState().grouping || [];
|
|
3139
4075
|
|
|
3140
|
-
// groupableColumns: the data columns OFFERED to the headless #groupBar
|
|
3141
|
-
//
|
|
3142
|
-
//
|
|
4076
|
+
// groupableColumns: the data columns OFFERED to the headless #groupBar — `[{ id, label }]`.
|
|
4077
|
+
// Excludes the chrome columns (select/expander are not in columnDefs()). C5+C6 (quick
|
|
4078
|
+
// 260906-cvo): delegates the actual walk to collectGroupableLeafDefs (columnDefUtils.ts) —
|
|
4079
|
+
// a GROUP column (a `columns:` group's own entry) is NEVER offered (it has no accessor and
|
|
4080
|
+
// cannot produce a grouping value, regardless of any `groupable` flag on it), and every
|
|
4081
|
+
// groupable NESTED leaf (a `columns:` group child, opted out via `groupable: false` like any
|
|
4082
|
+
// top-level leaf) IS offered, in declaration order. The consumer builds any bar/drag UI from
|
|
4083
|
+
// this; the component ships none.
|
|
3143
4084
|
groupableColumns = () => {
|
|
3144
4085
|
const out = [];
|
|
3145
|
-
const defs = this.columnDefs();
|
|
4086
|
+
const defs = collectGroupableLeafDefs(this.columnDefs());
|
|
3146
4087
|
for (const d of defs as any) {
|
|
3147
|
-
if (!d || d.groupable === false) continue;
|
|
3148
4088
|
out.push({
|
|
3149
4089
|
id: d.id,
|
|
3150
4090
|
label: d.header != null ? d.header : d.id
|
|
@@ -3315,7 +4255,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3315
4255
|
// or inherited Lit DOM method named getRowIndexRelativeToPage (ROZ121/124/137 clear).
|
|
3316
4256
|
getRowIndexRelativeToPage = (absRow: any) => {
|
|
3317
4257
|
const abs = absRow == null ? this.toAbsRow(this._activeRow.value) : Math.trunc(Number(absRow)) || 0;
|
|
3318
|
-
if (this.
|
|
4258
|
+
if (this.rowsWindowed()) return abs;
|
|
3319
4259
|
return abs - this.pageRowOffset();
|
|
3320
4260
|
};
|
|
3321
4261
|
|
|
@@ -3334,11 +4274,6 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3334
4274
|
// DataTable.rozie) — so DataTable.rozie's $expose references them BY NAME with zero
|
|
3335
4275
|
// indirection. This file stays the seam for verbs that need a rename/adapter, not a mandatory
|
|
3336
4276
|
// 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
4277
|
// interactionMode gate. 'grid' lights up roving nav; 'table' (default) is byte-behaviorally
|
|
3343
4278
|
// identical to phase 48 (roles fall back to the literals, tabindex drops).
|
|
3344
4279
|
isGrid = () => this.interactionMode === 'grid';
|
|
@@ -3374,7 +4309,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3374
4309
|
// isGrid()-gated (the active-cell API is grid-only); pageIndex()/pageSize() read live table-core
|
|
3375
4310
|
// state through the reactive tick (filterPaginationRowChrome), so this re-derives on a page change.
|
|
3376
4311
|
pageRowOffset = () => {
|
|
3377
|
-
if (!this.isGrid() || this.
|
|
4312
|
+
if (!this.isGrid() || this.rowsWindowed()) return 0;
|
|
3378
4313
|
return this.pageIndex() * this.pageSize();
|
|
3379
4314
|
};
|
|
3380
4315
|
|
|
@@ -3388,7 +4323,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3388
4323
|
// In virtual mode $data.rows IS the full pre-pagination model (bodyRowCount suffices); in the
|
|
3389
4324
|
// non-virtual paginated body $data.rows is only the page slice, so read the live model.
|
|
3390
4325
|
prePaginationRowCount = () => {
|
|
3391
|
-
if (!this.table || this.
|
|
4326
|
+
if (!this.table || this.rowsWindowed()) return this.bodyRowCount();
|
|
3392
4327
|
const pm = this.table.getPrePaginationRowModel();
|
|
3393
4328
|
return pm && pm.rows ? pm.rows.length : this.bodyRowCount();
|
|
3394
4329
|
};
|
|
@@ -3494,24 +4429,37 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3494
4429
|
// here returns the PRE-write value, resolving focus to the BODY cell instead of the
|
|
3495
4430
|
// header. Callers pass the fresh isHeader local; falls back to $data when omitted.
|
|
3496
4431
|
const header = nextIsHeader == null ? this._activeIsHeader.value : nextIsHeader;
|
|
3497
|
-
// ── phase 53 scroll-then-focus (D-12): when windowing AND the target body row
|
|
3498
|
-
// rendered window, scroll it in first, then defer focus to AFTER the new
|
|
3499
|
-
// double-rAF — a single rAF can fire before React's async commit, Pitfall
|
|
3500
|
-
// in-window
|
|
3501
|
-
// The guard reads the resolved `header` (NOT the raw
|
|
3502
|
-
// while a header cell is active falls back to
|
|
3503
|
-
|
|
3504
|
-
|
|
4432
|
+
// ── phase 53 scroll-then-focus (D-12): when windowing AND the target body row and/or column
|
|
4433
|
+
// is OUTSIDE the rendered window, scroll it in first, then defer focus to AFTER the new
|
|
4434
|
+
// window commits (the double-rAF — a single rAF can fire before React's async commit, Pitfall
|
|
4435
|
+
// 4). Header cells and fully in-window cells keep the synchronous path below (table-mode /
|
|
4436
|
+
// non-windowed stay byte-stable). The guard reads the resolved `header` (NOT the raw
|
|
4437
|
+
// `nextIsHeader`) so an omitted-arg call while a header cell is active falls back to
|
|
4438
|
+
// $data.activeIsHeader and skips the scroll path.
|
|
4439
|
+
// Phase 87 87-06 (D-12): widened to the SECOND axis — colIsOutsideWindow(c)/colsWindowed()
|
|
4440
|
+
// mirror rowIsOutsideWindow(r)/rowsWindowed() exactly (both landed in windowing.rzts in
|
|
4441
|
+
// 87-04). Either axis being out triggers the scroll+poll; both scrollToIndex calls fire when
|
|
4442
|
+
// both are out, and the ONE existing poll below is reused unchanged — it already resolves the
|
|
4443
|
+
// target cell by BOTH (row, col) via resolveCellEl(String(r), c), so a cell that becomes
|
|
4444
|
+
// reachable via the column axis needs no new poll logic (per the plan's own key_links note).
|
|
4445
|
+
const rowOut = this.rowsWindowed() && this.virtualizer && this.rowIsOutsideWindow(r);
|
|
4446
|
+
const colOut = this.colsWindowed() && this.colVirtualizer && this.colIsOutsideWindow(c);
|
|
4447
|
+
if (!header && (rowOut || colOut)) {
|
|
4448
|
+
if (rowOut) this.virtualizer.scrollToIndex(r, {
|
|
4449
|
+
align: 'center'
|
|
4450
|
+
});
|
|
4451
|
+
if (colOut) this.colVirtualizer.scrollToIndex(c, {
|
|
3505
4452
|
align: 'center'
|
|
3506
4453
|
});
|
|
3507
4454
|
// Bounded rAF-poll-until-cell-present (D-12): scrollToIndex → virtual-core onChange → windowVer
|
|
3508
|
-
// bump → the framework commits the scrolled-in row. On React that commit is async
|
|
3509
|
-
// reconcile) and for a far scroll (e.g. row 4000
|
|
3510
|
-
// fires BEFORE resolveCellEl can find the cell,
|
|
3511
|
-
// React off-window-focus failure). Poll
|
|
3512
|
-
// fast-committing targets resolve on the first
|
|
3513
|
-
// across the few frames its async commit needs.
|
|
3514
|
-
// cannot re-introduce the remeasure-vs-scroll
|
|
4455
|
+
// bump → the framework commits the scrolled-in row/column. On React that commit is async
|
|
4456
|
+
// (setState → reconcile) and for a far scroll (e.g. row 4000, or col 55 on a 60-col table)
|
|
4457
|
+
// spans several frames — a one-shot double-rAF fires BEFORE resolveCellEl can find the cell,
|
|
4458
|
+
// so focus is silently lost (the deterministic React off-window-focus failure). Poll
|
|
4459
|
+
// resolveCellEl for up to ~30 frames: the five fast-committing targets resolve on the first
|
|
4460
|
+
// attempt (behavior unchanged), React retries across the few frames its async commit needs.
|
|
4461
|
+
// The poll ONLY focuses (never measures), so it cannot re-introduce the remeasure-vs-scroll
|
|
4462
|
+
// fight. Inside the rowOut||colOut guard only.
|
|
3515
4463
|
let focusAttempts = 0;
|
|
3516
4464
|
// #9: capture the epoch AFTER this call's own bump (above) so the poll never aborts itself
|
|
3517
4465
|
// (its captured value equals the current epoch). A LATER focusActiveCell / focusCell /
|
|
@@ -3603,8 +4551,6 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3603
4551
|
|
|
3604
4552
|
bodyRowCount = () => (this._rows.value || []).length;
|
|
3605
4553
|
|
|
3606
|
-
clamp = (v: any, lo: any, hi: any) => v < lo ? lo : v > hi ? hi : v;
|
|
3607
|
-
|
|
3608
4554
|
// ── Multi-level (grouped) header addressing (B12) ──────────────────────────────────────
|
|
3609
4555
|
// $data.headerGroups is ordered top→bottom; the LEAF header row (the one adjacent to the
|
|
3610
4556
|
// body) is the LAST group. The roving active-header state carries activeHeaderLevel (the
|
|
@@ -3689,7 +4635,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3689
4635
|
// phantom cell past that level's headers. Body cells + the leaf header level keep visibleColCount().
|
|
3690
4636
|
const count = this._activeIsHeader.value ? this.headerCountAtLevel(this._activeHeaderLevel.value) : this.visibleColCount();
|
|
3691
4637
|
const max = count - 1;
|
|
3692
|
-
const nextCol =
|
|
4638
|
+
const nextCol = clamp(this._activeColIndex.value + delta, 0, max < 0 ? 0 : max);
|
|
3693
4639
|
this._activeColIndex.value = nextCol;
|
|
3694
4640
|
return nextCol;
|
|
3695
4641
|
};
|
|
@@ -3734,7 +4680,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3734
4680
|
// (delta-1) continues the descent, so PageDown (delta=GRID_PAGE_STEP) lands a real
|
|
3735
4681
|
// page-down body row, NOT row 0 (== ArrowDown). ArrowDown (delta=1) still lands row 0
|
|
3736
4682
|
// (delta-1 = 0); clamped to the page-last body row.
|
|
3737
|
-
const landRow =
|
|
4683
|
+
const landRow = clamp(delta - 1, 0, maxRow);
|
|
3738
4684
|
this._activeIsHeader.value = false;
|
|
3739
4685
|
this._activeRow.value = landRow;
|
|
3740
4686
|
return {
|
|
@@ -3779,7 +4725,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3779
4725
|
level: leafLevel
|
|
3780
4726
|
};
|
|
3781
4727
|
}
|
|
3782
|
-
const nextRow =
|
|
4728
|
+
const nextRow = clamp(this._activeRow.value + delta, 0, maxRow);
|
|
3783
4729
|
this._activeRow.value = nextRow;
|
|
3784
4730
|
this._activeIsHeader.value = false;
|
|
3785
4731
|
return {
|
|
@@ -3847,19 +4793,12 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3847
4793
|
return this.resolveCellEl(rowKey, this._activeColIndex.value, this._activeIsHeader.value ? this._activeHeaderLevel.value : null);
|
|
3848
4794
|
};
|
|
3849
4795
|
|
|
3850
|
-
// The focusable descendants of a cell (non-disabled), in DOM order. Pure DOM — uniform ×6.
|
|
3851
|
-
focusables = (cellEl: any) => {
|
|
3852
|
-
if (!cellEl || !cellEl.querySelectorAll) return [];
|
|
3853
|
-
const list = Array.prototype.slice.call(cellEl.querySelectorAll('button,[href],input,select,textarea,[tabindex]:not([tabindex="-1"])'));
|
|
3854
|
-
return list.filter((n: any) => !n.disabled);
|
|
3855
|
-
};
|
|
3856
|
-
|
|
3857
4796
|
// Enter/F2 → enter interaction mode: focus the active cell's FIRST interactive control
|
|
3858
4797
|
// (D-07 — uniform for header sort buttons and body controls; Enter does NOT sort directly).
|
|
3859
4798
|
// No-op (stay in navigation mode) if the cell has no focusable control.
|
|
3860
4799
|
enterControl = () => {
|
|
3861
4800
|
const cellEl = this.currentCellEl();
|
|
3862
|
-
const list =
|
|
4801
|
+
const list = focusables(cellEl);
|
|
3863
4802
|
if (!list.length) return;
|
|
3864
4803
|
this._activeInControl.value = true;
|
|
3865
4804
|
list[0].focus();
|
|
@@ -3870,7 +4809,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3870
4809
|
// activeElement read: gridRoot.getRootNode().activeElement is the UNIFORM correct read on
|
|
3871
4810
|
// ALL SIX (document in light DOM; the shadow root on Lit). Reuse verbatim — do NOT re-derive.
|
|
3872
4811
|
cycleWithinCell = (cellEl: any, forward: any) => {
|
|
3873
|
-
const list =
|
|
4812
|
+
const list = focusables(cellEl);
|
|
3874
4813
|
if (!list.length) return;
|
|
3875
4814
|
const active = this.gridRoot ? this.gridRoot.getRootNode().activeElement : null;
|
|
3876
4815
|
const cur = list.indexOf(active);
|
|
@@ -4157,11 +5096,18 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4157
5096
|
return;
|
|
4158
5097
|
}
|
|
4159
5098
|
// ── C2 (phase 63 wave-8): Enter on a GROUP-HEADER cell toggles that group's collapse/
|
|
4160
|
-
// expand (APG treegrid).
|
|
4161
|
-
//
|
|
4162
|
-
//
|
|
4163
|
-
//
|
|
4164
|
-
//
|
|
5099
|
+
// expand (APG treegrid). C1 (quick 260906-cvo) CORRECTION: the comment here used to claim
|
|
5100
|
+
// "a group cell is non-editable" as "the verified invariant" — that was FALSE AS WRITTEN.
|
|
5101
|
+
// It held only for the grouping column's OWN cell (cellIsGrouped's branch renders before
|
|
5102
|
+
// the editor branch); a non-grouping EDITABLE column's cell on a group row fell straight
|
|
5103
|
+
// through the branch chain to isEditing(...) and opened an editor (the C1 defect this
|
|
5104
|
+
// quick task closed). What is now TRUE, and WHY: isActiveCellEditable() carries an
|
|
5105
|
+
// explicit rowIndexIsGrouped(...) row guard (filterPaginationRowChrome.rzts), so EVERY
|
|
5106
|
+
// editable column's cell on a group row is non-editable — that is what makes the four
|
|
5107
|
+
// edit-entry branches above fall through to this one, not an incidental property of the
|
|
5108
|
+
// grouping column alone. Route it to the SAME onToggleExpand path the chevron uses (group
|
|
5109
|
+
// rows ride the expand model) so one Enter toggles the group. Body cells only (a
|
|
5110
|
+
// header-active Enter is unchanged);
|
|
4165
5111
|
// ($data.rows || [])[$data.activeRow] is the active flattened row (page-relative non-virtual /
|
|
4166
5112
|
// full-model virtual — both index $data.rows). Placed BEFORE the reserved enterControl branch.
|
|
4167
5113
|
else if (key === 'Enter' && !this._activeIsHeader.value && this.rowIsGrouped((this._rows.value || [])[this._activeRow.value])) {
|
|
@@ -4515,7 +5461,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4515
5461
|
}
|
|
4516
5462
|
}
|
|
4517
5463
|
const maxCol = colN - 1;
|
|
4518
|
-
const col =
|
|
5464
|
+
const col = clamp(this._activeColIndex.value, 0, maxCol < 0 ? 0 : maxCol);
|
|
4519
5465
|
if (col !== this._activeColIndex.value) this._activeColIndex.value = col;
|
|
4520
5466
|
// B6: an empty / all-filtered grid has NO body cell to hold the active cell. Park the active
|
|
4521
5467
|
// cell on the leaf-header fallback (col 0) so the roving tab-stop stays on a REAL cell (never
|
|
@@ -4550,7 +5496,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4550
5496
|
if (!this._activeIsHeader.value) {
|
|
4551
5497
|
const lastRow = rowN - 1;
|
|
4552
5498
|
const maxRow = lastRow < 0 ? 0 : lastRow;
|
|
4553
|
-
const row =
|
|
5499
|
+
const row = clamp(this._activeRow.value, 0, maxRow);
|
|
4554
5500
|
if (row !== this._activeRow.value) this._activeRow.value = row;
|
|
4555
5501
|
}
|
|
4556
5502
|
// B8: clamp the range-selection corners to the same FRESH bounds (a sort/filter/paginate that
|
|
@@ -4561,12 +5507,81 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4561
5507
|
// when the shrink removed the focused cell. The target is the DOOMED cell's own coords clamped
|
|
4562
5508
|
// into the fresh bounds (React-stale-safe — see the doomedRow/doomedCol note above).
|
|
4563
5509
|
if (recoverFocus) {
|
|
4564
|
-
const recRow =
|
|
4565
|
-
const recCol =
|
|
5510
|
+
const recRow = clamp(doomedRow, 0, rowN - 1);
|
|
5511
|
+
const recCol = clamp(doomedCol, 0, maxCol < 0 ? 0 : maxCol);
|
|
4566
5512
|
this.recoverGridFocus(String(recRow), recCol, null);
|
|
4567
5513
|
}
|
|
4568
5514
|
};
|
|
4569
5515
|
|
|
5516
|
+
// ══ Phase 87 87-04/87-05 — column-axis template windowing (D-06/D-11; declared HERE (after
|
|
5517
|
+
// the columnChrome/gridFocusNav imports above) so a forward cross-partial reference to
|
|
5518
|
+
// colsWindowed()/windowedColIndices() in the merged per-target output never TDZs on React —
|
|
5519
|
+
// the 87-02 useCallback-deps-array ordering lesson, the SAME reason remeasureColumnWindow()
|
|
5520
|
+
// below stays out of virtualization.rzts). ══
|
|
5521
|
+
// windowedHeadersFor(hg, hgLevel) (87-05): generalizes 87-04's windowedHeaderRow() (leaf-level
|
|
5522
|
+
// only) to EVERY header level with a CLAMPED colspan (D-11). A header spans the CONTIGUOUS run
|
|
5523
|
+
// of leaf-column indices [cursor, cursor + header.colSpan) — table-core lays out every header
|
|
5524
|
+
// level's `headers` array left-to-right over the SAME full leaf-column order, so walking a
|
|
5525
|
+
// running cursor by each header's OWN colSpan reconstructs its absolute leaf range with no
|
|
5526
|
+
// extra table-core calls. Intersect that range with windowedColIndices() (which already unions
|
|
5527
|
+
// the D-10 forced set): a header whose intersection is EMPTY is dropped entirely (a group
|
|
5528
|
+
// wholly outside the window renders no <th> at all — D-11); otherwise it is emitted as
|
|
5529
|
+
// { header, span } with span = the intersection SIZE (the clamp). Degrades to hg.headers
|
|
5530
|
+
// UNCHANGED (span = the header's own colSpan, or 1) when columns are not windowed — the
|
|
5531
|
+
// off-path the template's `:colspan="wh.span > 1 ? wh.span : null"` binding preserves
|
|
5532
|
+
// byte-identically (a bare colspan="1" would change the emitted markup on the unwindowed path).
|
|
5533
|
+
windowedHeadersFor = (hg: any, hgLevel: any) => {
|
|
5534
|
+
const headers = hg && hg.headers || [];
|
|
5535
|
+
if (!this.colsWindowed()) {
|
|
5536
|
+
return headers.map((h: any) => ({
|
|
5537
|
+
header: h,
|
|
5538
|
+
span: h && h.colSpan > 1 ? h.colSpan : 1
|
|
5539
|
+
}));
|
|
5540
|
+
}
|
|
5541
|
+
const idx = this.windowedColIndices();
|
|
5542
|
+
const idxSet = {};
|
|
5543
|
+
for (let i = 0; i < idx.length; i++) idxSet[idx[i]] = true;
|
|
5544
|
+
let cursor = 0;
|
|
5545
|
+
const out = [];
|
|
5546
|
+
for (let i = 0; i < headers.length; i++) {
|
|
5547
|
+
const h = headers[i];
|
|
5548
|
+
const leafCount = h && h.colSpan ? h.colSpan : 1;
|
|
5549
|
+
const start = cursor;
|
|
5550
|
+
const end = cursor + leafCount;
|
|
5551
|
+
cursor = end;
|
|
5552
|
+
let span = 0;
|
|
5553
|
+
for (let c = start; c < end; c++) if (idxSet[c]) span = span + 1;
|
|
5554
|
+
if (span > 0) out.push({
|
|
5555
|
+
header: h,
|
|
5556
|
+
span
|
|
5557
|
+
});
|
|
5558
|
+
}
|
|
5559
|
+
return out;
|
|
5560
|
+
};
|
|
5561
|
+
|
|
5562
|
+
// windowedColSpan(): the body row's rendered <td> COUNT under column windowing — the windowed
|
|
5563
|
+
// cell slice length plus the two rdt-col-spacer cells, so the leading/trailing full-row spacer
|
|
5564
|
+
// <tr>s and the #detail <tr> span the SAME column count the body row actually renders (D-06/
|
|
5565
|
+
// D-11's alignment obligation under table-layout:fixed). Off → visibleColCount() unchanged
|
|
5566
|
+
// (byte-identical).
|
|
5567
|
+
windowedColSpan = () => this.colsWindowed() ? this.windowedColIndices().length + 2 : this.visibleColCount();
|
|
5568
|
+
|
|
5569
|
+
// remeasureColumnWindow() (Phase 87 87-04): push a fresh column count/size into the SECOND
|
|
5570
|
+
// (horizontal) virtualizer on every row-model refresh — mirrors the row axis's
|
|
5571
|
+
// virtualizer.setOptions(virtualizerOptions()) + _willUpdate() re-feed in refreshRowModel, so a
|
|
5572
|
+
// runtime column-visibility/order/pinning change (all watched in the $watch list below) keeps
|
|
5573
|
+
// the column virtualizer's count in sync. Never a render helper (Pitfall 1); rides the EXISTING
|
|
5574
|
+
// refreshRowModel imperative re-feed cycle rather than adding a third scheduling path. Declared
|
|
5575
|
+
// HERE (DataTable.rozie's own script), not in virtualization.rzts — see that file's comment for
|
|
5576
|
+
// why: it must call columnVirtualizerOptions() (windowing.rzts), and a function inside
|
|
5577
|
+
// virtualization.rzts calling into windowing.rzts is a forward-reference TDZ on React (the
|
|
5578
|
+
// 87-02 predicate-layer lesson, one file-grouping boundary over).
|
|
5579
|
+
remeasureColumnWindow = () => {
|
|
5580
|
+
if (!this.colsWindowed() || !this.colVirtualizer) return;
|
|
5581
|
+
this.colVirtualizer.setOptions(this.columnVirtualizerOptions());
|
|
5582
|
+
this.colVirtualizer._willUpdate();
|
|
5583
|
+
};
|
|
5584
|
+
|
|
4570
5585
|
// B6 (phase 63 wave-11) — "the active cell is parked on the empty-grid header fallback" control
|
|
4571
5586
|
// flag, written + read ONLY inside clampActiveCell (never bound in the template). It MUST be a
|
|
4572
5587
|
// plain component-scope `let` (React hoists to useRef), NOT a $data reactive field: clampActiveCell
|
|
@@ -4645,8 +5660,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4645
5660
|
focus: null
|
|
4646
5661
|
};
|
|
4647
5662
|
const clampCorner = (c: any) => c == null ? null : {
|
|
4648
|
-
rowIndex:
|
|
4649
|
-
colIndex:
|
|
5663
|
+
rowIndex: clamp(c.rowIndex, 0, maxRow),
|
|
5664
|
+
colIndex: clamp(c.colIndex, 0, maxCol)
|
|
4650
5665
|
};
|
|
4651
5666
|
return {
|
|
4652
5667
|
anchor: clampCorner(a),
|
|
@@ -4706,8 +5721,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4706
5721
|
colIndex: this._activeColIndex.value
|
|
4707
5722
|
};
|
|
4708
5723
|
}
|
|
4709
|
-
const nextRow =
|
|
4710
|
-
const nextCol =
|
|
5724
|
+
const nextRow = clamp(focus.rowIndex + dRow, 0, maxRow);
|
|
5725
|
+
const nextCol = clamp(focus.colIndex + dCol, 0, maxCol);
|
|
4711
5726
|
const nextFocus = {
|
|
4712
5727
|
rowIndex: nextRow,
|
|
4713
5728
|
colIndex: nextCol
|
|
@@ -4744,8 +5759,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4744
5759
|
rowIndex: this._activeRow.value,
|
|
4745
5760
|
colIndex: this._activeColIndex.value
|
|
4746
5761
|
};
|
|
4747
|
-
const r =
|
|
4748
|
-
const c =
|
|
5762
|
+
const r = clamp(Math.trunc(Number(rIdx)) || 0, 0, maxRow);
|
|
5763
|
+
const c = clamp(Math.trunc(Number(cIdx)) || 0, 0, maxCol);
|
|
4749
5764
|
const nextFocus = {
|
|
4750
5765
|
rowIndex: r,
|
|
4751
5766
|
colIndex: c
|
|
@@ -4822,16 +5837,16 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4822
5837
|
return;
|
|
4823
5838
|
}
|
|
4824
5839
|
if (a) {
|
|
4825
|
-
const ar =
|
|
4826
|
-
const ac =
|
|
5840
|
+
const ar = clamp(a.rowIndex, 0, maxRow);
|
|
5841
|
+
const ac = clamp(a.colIndex, 0, maxCol);
|
|
4827
5842
|
if (ar !== a.rowIndex || ac !== a.colIndex) this._rangeAnchor.value = {
|
|
4828
5843
|
rowIndex: ar,
|
|
4829
5844
|
colIndex: ac
|
|
4830
5845
|
};
|
|
4831
5846
|
}
|
|
4832
5847
|
if (f) {
|
|
4833
|
-
const fr =
|
|
4834
|
-
const fc =
|
|
5848
|
+
const fr = clamp(f.rowIndex, 0, maxRow);
|
|
5849
|
+
const fc = clamp(f.colIndex, 0, maxCol);
|
|
4835
5850
|
if (fr !== f.rowIndex || fc !== f.colIndex) this._rangeFocus.value = {
|
|
4836
5851
|
rowIndex: fr,
|
|
4837
5852
|
colIndex: fc
|
|
@@ -4839,14 +5854,6 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4839
5854
|
}
|
|
4840
5855
|
};
|
|
4841
5856
|
|
|
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
5857
|
// announce(msg): write the polite aria-live PASTE-announce region (D-03 — "N of M cells
|
|
4851
5858
|
// pasted"). SEPARATE from the validation invalidMsg region (different semantics). '' clears it.
|
|
4852
5859
|
announce = (msg: any) => {
|
|
@@ -4882,10 +5889,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4882
5889
|
const maxRow = this.bodyRowCount() - 1;
|
|
4883
5890
|
const maxCol = this.visibleColCount() - 1;
|
|
4884
5891
|
if (maxRow < 0 || maxCol < 0) return null;
|
|
4885
|
-
const ar =
|
|
4886
|
-
const ac =
|
|
4887
|
-
const fr =
|
|
4888
|
-
const fc =
|
|
5892
|
+
const ar = clamp(a.rowIndex, 0, maxRow);
|
|
5893
|
+
const ac = clamp(a.colIndex, 0, maxCol);
|
|
5894
|
+
const fr = clamp(f.rowIndex, 0, maxRow);
|
|
5895
|
+
const fc = clamp(f.colIndex, 0, maxCol);
|
|
4889
5896
|
return {
|
|
4890
5897
|
r0: ar < fr ? ar : fr,
|
|
4891
5898
|
r1: ar > fr ? ar : fr,
|
|
@@ -4894,20 +5901,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4894
5901
|
};
|
|
4895
5902
|
};
|
|
4896
5903
|
|
|
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).
|
|
4901
|
-
escapeTsvField = (s: any) => {
|
|
4902
|
-
if (s.indexOf('\t') >= 0 || s.indexOf('\n') >= 0 || s.indexOf('\r') >= 0 || s.indexOf('"') >= 0) {
|
|
4903
|
-
return '"' + s.replace(/"/g, '""') + '"';
|
|
4904
|
-
}
|
|
4905
|
-
return s;
|
|
4906
|
-
};
|
|
4907
|
-
|
|
4908
5904
|
// rangeToTsv(): serialize the current range to TSV — rows joined by '\n', cells by '\t',
|
|
4909
5905
|
// reading each cell's value off the visible model by index (cellValueAt). A single active
|
|
4910
5906
|
// cell (no range) serializes that one cell. Each field is B10-escaped. Pure read — never writes.
|
|
5907
|
+
// C2 (quick 260906-cvo) — DELIBERATE NON-CHANGE: this is a READ, unlike applyGridToRange's
|
|
5908
|
+
// write funnel, and stays UNGUARDED for a group-header row on purpose. cellValueAt reads the
|
|
5909
|
+
// column's DISPLAYED aggregate value on a group row (table-core's cell.getValue()), which is
|
|
5910
|
+
// exactly what the user sees — serializing it is correct behavior, not a leak. Do not "fix"
|
|
5911
|
+
// this asymmetry; it is intentional.
|
|
4911
5912
|
rangeToTsv = () => {
|
|
4912
5913
|
const box = this.normalizedRange();
|
|
4913
5914
|
const r0 = box ? box.r0 : this._activeRow.value;
|
|
@@ -4919,91 +5920,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4919
5920
|
const cells = [];
|
|
4920
5921
|
for (let c = c0; c <= c1; c++) {
|
|
4921
5922
|
const v = this.cellValueAt(r, c);
|
|
4922
|
-
cells.push(
|
|
5923
|
+
cells.push(escapeTsvField(v == null ? '' : String(v)));
|
|
4923
5924
|
}
|
|
4924
5925
|
lines.push(cells.join('\t'));
|
|
4925
5926
|
}
|
|
4926
5927
|
return lines.join('\n');
|
|
4927
5928
|
};
|
|
4928
5929
|
|
|
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).
|
|
4932
|
-
parseTsv = (text: any) => {
|
|
4933
|
-
const str = text != null ? String(text) : '';
|
|
4934
|
-
// CR-03: length guard BEFORE the parse — an empty string is a no-op, and a pathologically
|
|
4935
|
-
// large clipboard payload (>2M chars) is rejected outright (DoS-shaped input) before the
|
|
4936
|
-
// single-pass scan allocates a cell-per-character grid.
|
|
4937
|
-
if (str === '' || str.length > 2000000) return [];
|
|
4938
|
-
// B10: a quote-aware single-pass state machine (replaces the naive split, which corrupted a
|
|
4939
|
-
// cell containing a tab/newline). A field that OPENS with a double-quote is "quoted": tabs,
|
|
4940
|
-
// newlines, and doubled quotes ("") inside it are literal content until the closing quote;
|
|
4941
|
-
// an unquoted field ends at the next tab/newline. CR/LF and CRLF all delimit a row.
|
|
4942
|
-
const rows = [];
|
|
4943
|
-
let row = [];
|
|
4944
|
-
let field = '';
|
|
4945
|
-
let inQuotes = false;
|
|
4946
|
-
let i = 0;
|
|
4947
|
-
const n = str.length;
|
|
4948
|
-
while (i < n) {
|
|
4949
|
-
const ch = str[i];
|
|
4950
|
-
if (inQuotes) {
|
|
4951
|
-
if (ch === '"') {
|
|
4952
|
-
if (i + 1 < n && str[i + 1] === '"') {
|
|
4953
|
-
field = field + '"';
|
|
4954
|
-
i = i + 2;
|
|
4955
|
-
continue;
|
|
4956
|
-
}
|
|
4957
|
-
inQuotes = false;
|
|
4958
|
-
i = i + 1;
|
|
4959
|
-
continue;
|
|
4960
|
-
}
|
|
4961
|
-
field = field + ch;
|
|
4962
|
-
i = i + 1;
|
|
4963
|
-
continue;
|
|
4964
|
-
}
|
|
4965
|
-
if (ch === '"' && field === '') {
|
|
4966
|
-
inQuotes = true;
|
|
4967
|
-
i = i + 1;
|
|
4968
|
-
continue;
|
|
4969
|
-
}
|
|
4970
|
-
if (ch === '\t') {
|
|
4971
|
-
row.push(field);
|
|
4972
|
-
field = '';
|
|
4973
|
-
i = i + 1;
|
|
4974
|
-
continue;
|
|
4975
|
-
}
|
|
4976
|
-
if (ch === '\r') {
|
|
4977
|
-
if (i + 1 < n && str[i + 1] === '\n') i = i + 1;
|
|
4978
|
-
row.push(field);
|
|
4979
|
-
field = '';
|
|
4980
|
-
rows.push(row);
|
|
4981
|
-
row = [];
|
|
4982
|
-
i = i + 1;
|
|
4983
|
-
continue;
|
|
4984
|
-
}
|
|
4985
|
-
if (ch === '\n') {
|
|
4986
|
-
row.push(field);
|
|
4987
|
-
field = '';
|
|
4988
|
-
rows.push(row);
|
|
4989
|
-
row = [];
|
|
4990
|
-
i = i + 1;
|
|
4991
|
-
continue;
|
|
4992
|
-
}
|
|
4993
|
-
field = field + ch;
|
|
4994
|
-
i = i + 1;
|
|
4995
|
-
}
|
|
4996
|
-
// Flush the trailing field + row.
|
|
4997
|
-
row.push(field);
|
|
4998
|
-
rows.push(row);
|
|
4999
|
-
// Drop a single trailing empty row (a TSV that ends with a newline → a phantom [''] row).
|
|
5000
|
-
if (rows.length > 1) {
|
|
5001
|
-
const last = rows[rows.length - 1];
|
|
5002
|
-
if (last.length === 1 && last[0] === '') rows.pop();
|
|
5003
|
-
}
|
|
5004
|
-
return rows;
|
|
5005
|
-
};
|
|
5006
|
-
|
|
5007
5930
|
// copyRange(): write the current range as TSV to the clipboard (async). No-op when the
|
|
5008
5931
|
// async Clipboard API is unavailable (older/insecure contexts) — a copy is best-effort.
|
|
5009
5932
|
copyRange = () => {
|
|
@@ -5039,11 +5962,22 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5039
5962
|
for (let gr = 0; gr < grid.length; gr++) {
|
|
5040
5963
|
const r = originRow + gr;
|
|
5041
5964
|
if (r > maxRow) break;
|
|
5965
|
+
// C2 (quick 260906-cvo) — hoisted ONCE per ROW (never per cell): a group-header row's
|
|
5966
|
+
// `original` is table-core's `leafRows[0].original` (a synthetic row), so a write
|
|
5967
|
+
// resolved through it silently lands on an unrelated record. This ONE placement closes
|
|
5968
|
+
// pasteRange, cutRange, clearActiveRange AND fillRange — all four build a grid and hand
|
|
5969
|
+
// it to this single funnel, so guarding the four callers individually would be four
|
|
5970
|
+
// chances to miss one.
|
|
5971
|
+
const rowGrouped = this.rowIndexIsGrouped(r);
|
|
5042
5972
|
const cols = grid[gr] || [];
|
|
5043
5973
|
for (let gc = 0; gc < cols.length; gc++) {
|
|
5044
5974
|
const c = originCol + gc;
|
|
5045
5975
|
if (c > maxCol) break;
|
|
5976
|
+
// A group-row cell still counts toward `total` — it is a skipped TARGET, exactly like
|
|
5977
|
+
// a non-editable or validator-rejected cell — so the "N of M cells pasted" / "no cells
|
|
5978
|
+
// pasted" announce keeps its existing semantics (never silently under-reports M).
|
|
5046
5979
|
total = total + 1;
|
|
5980
|
+
if (rowGrouped) continue;
|
|
5047
5981
|
const colId = this.columnIdAt(r, c);
|
|
5048
5982
|
if (colId == null || !this.columnEditable(colId)) continue;
|
|
5049
5983
|
const rowObj = this.rowOriginalAt(r);
|
|
@@ -5057,7 +5991,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5057
5991
|
const field = this.fieldOfColId(colId);
|
|
5058
5992
|
const srcIndex = this.sourceIndexOfRow(r);
|
|
5059
5993
|
const oldValue = rowObj ? rowObj[field] : null;
|
|
5060
|
-
next =
|
|
5994
|
+
next = replaceRowValue(next, srcIndex, field, value);
|
|
5061
5995
|
committed.push({
|
|
5062
5996
|
rowId: this.rowIdAt(r),
|
|
5063
5997
|
columnId: colId,
|
|
@@ -5101,43 +6035,6 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5101
6035
|
return row ? row.id : null;
|
|
5102
6036
|
};
|
|
5103
6037
|
|
|
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).
|
|
5112
|
-
tileGridToBox = (grid: any, box: any) => {
|
|
5113
|
-
const srcRows = grid.length;
|
|
5114
|
-
// srcCols is the MAX row width across ALL rows (not grid[0].length): a RAGGED clipboard
|
|
5115
|
-
// (a later row WIDER than the first, e.g. TSV "a\tb\nc\td\te") would otherwise never read
|
|
5116
|
-
// the extra column and silently drop those cells. A row SHORTER than srcCols tiles its
|
|
5117
|
-
// missing cells as '' (the `v != null ? v : ''` coercion below), never undefined.
|
|
5118
|
-
let srcCols = 0;
|
|
5119
|
-
for (let i = 0; i < srcRows; i++) {
|
|
5120
|
-
const w = grid[i] && grid[i].length ? grid[i].length : 0;
|
|
5121
|
-
if (w > srcCols) srcCols = w;
|
|
5122
|
-
}
|
|
5123
|
-
if (srcRows <= 0 || srcCols <= 0) return grid;
|
|
5124
|
-
const boxRows = box.r1 - box.r0 + 1;
|
|
5125
|
-
const boxCols = box.c1 - box.c0 + 1;
|
|
5126
|
-
const rows = boxRows > srcRows ? boxRows : srcRows;
|
|
5127
|
-
const cols = boxCols > srcCols ? boxCols : srcCols;
|
|
5128
|
-
const out = [];
|
|
5129
|
-
for (let r = 0; r < rows; r++) {
|
|
5130
|
-
const srcLine = grid[r % srcRows] || [];
|
|
5131
|
-
const line = [];
|
|
5132
|
-
for (let c = 0; c < cols; c++) {
|
|
5133
|
-
const v = srcLine[c % srcCols];
|
|
5134
|
-
line.push(v != null ? v : '');
|
|
5135
|
-
}
|
|
5136
|
-
out.push(line);
|
|
5137
|
-
}
|
|
5138
|
-
return out;
|
|
5139
|
-
};
|
|
5140
|
-
|
|
5141
6038
|
// pasteRange(): read TSV from the clipboard (async), parse it, TILE it over the destination
|
|
5142
6039
|
// (C3), and apply it anchored at the destination top-left under the D-03 skip rule. The grid is
|
|
5143
6040
|
// clamped to the grid bounds (T-51-02). A failed/empty read is a silent no-op.
|
|
@@ -5169,11 +6066,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5169
6066
|
}
|
|
5170
6067
|
if (!p || !p.then) return;
|
|
5171
6068
|
p.then((text: any) => {
|
|
5172
|
-
const grid =
|
|
6069
|
+
const grid = parseTsv(text);
|
|
5173
6070
|
if (!grid.length) return;
|
|
5174
6071
|
// C3: tile the clipboard block to fill the destination range (single→range fill,
|
|
5175
6072
|
// smaller-tiles-into-larger); a clipboard larger than the box pastes its full block.
|
|
5176
|
-
const tiled =
|
|
6073
|
+
const tiled = tileGridToBox(grid, destBox);
|
|
5177
6074
|
this.applyGridToRange(tiled, anchorRow, anchorCol);
|
|
5178
6075
|
}).catch(() => {});
|
|
5179
6076
|
};
|
|
@@ -5238,18 +6135,6 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5238
6135
|
this.applyGridToRange(grid, r0, c0);
|
|
5239
6136
|
};
|
|
5240
6137
|
|
|
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.
|
|
5245
|
-
tileIndex = (i: any, lo: any, hi: any) => {
|
|
5246
|
-
const span = hi - lo + 1;
|
|
5247
|
-
if (span <= 1) return lo;
|
|
5248
|
-
let k = (i - lo) % span;
|
|
5249
|
-
if (k < 0) k = k + span;
|
|
5250
|
-
return lo + k;
|
|
5251
|
-
};
|
|
5252
|
-
|
|
5253
6138
|
// fillRange(sourceBox): drag-fill (D-04 — VALUE-COPY ONLY, no series detection). B7: the fill
|
|
5254
6139
|
// SOURCE is the PRE-DRAG rectangle (`sourceBox`, captured at pointerdown before the drag grew
|
|
5255
6140
|
// the range); each target cell copies the source cell in its OWN column (and row, when the
|
|
@@ -5295,8 +6180,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5295
6180
|
for (let r = box.r0; r <= box.r1; r++) {
|
|
5296
6181
|
const cols = [];
|
|
5297
6182
|
for (let c = box.c0; c <= box.c1; c++) {
|
|
5298
|
-
const sr =
|
|
5299
|
-
const sc =
|
|
6183
|
+
const sr = tileIndex(r, src.r0, src.r1);
|
|
6184
|
+
const sc = tileIndex(c, src.c0, src.c1);
|
|
5300
6185
|
const v = this.cellValueAt(sr, sc);
|
|
5301
6186
|
cols.push(v == null ? '' : String(v));
|
|
5302
6187
|
}
|
|
@@ -5318,6 +6203,52 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5318
6203
|
|
|
5319
6204
|
fillDragUp: any = null;
|
|
5320
6205
|
|
|
6206
|
+
// Phase 87 87-06 (D-13, T-87-06-02): the single rAF handle driving the fill-drag per-axis
|
|
6207
|
+
// edge auto-scroll — a module-`let` beside fillDragMove/fillDragUp (the `let table` precedent;
|
|
6208
|
+
// React hoists to useRef), cleared IDEMPOTENTLY in teardownFillDrag() so an interrupted gesture
|
|
6209
|
+
// never leaves an orphaned scroll loop running forever.
|
|
6210
|
+
fillEdgeScrollRaf: any = null;
|
|
6211
|
+
|
|
6212
|
+
// Phase 87 87-06 (D-13): per-axis edge-triggered auto-scroll for the fill drag.
|
|
6213
|
+
// FILL_EDGE_SCROLL_PX — the pixel distance from the scroll container's edge, on either axis,
|
|
6214
|
+
// that engages auto-scroll. 24px: comfortably wider than the fill handle's own hit target but
|
|
6215
|
+
// well inside a typical row/column size, so the gesture recognizably nears an edge before
|
|
6216
|
+
// scrolling engages (not so wide that an ordinary drag near the middle of the viewport
|
|
6217
|
+
// spuriously triggers it).
|
|
6218
|
+
FILL_EDGE_SCROLL_PX = 24;
|
|
6219
|
+
|
|
6220
|
+
// FILL_EDGE_SCROLL_STEP — pixels scrolled per animation frame while the pointer sits inside an
|
|
6221
|
+
// edge zone. A fixed step (not distance-proportional) keeps the scroll speed predictable and
|
|
6222
|
+
// the WR-01-style spacer/window math simple to reason about frame-to-frame.
|
|
6223
|
+
FILL_EDGE_SCROLL_STEP = 16;
|
|
6224
|
+
|
|
6225
|
+
// edgeDelta(clientX, clientY): the per-frame (dx, dy) scroll delta for a pointer at the given
|
|
6226
|
+
// screen position, relative to the scroll container `gridScrollEl` already observed by the row
|
|
6227
|
+
// AND column virtualizers (D-03 — one shared `.rdt-scroll` wrapper for whichever axis/axes are
|
|
6228
|
+
// windowed). Each axis is independently gated on colsWindowed()/rowsWindowed() (never a bare
|
|
6229
|
+
// prop truthiness read — the 87-02 prohibition gate) so a drag near the left/right edge of a
|
|
6230
|
+
// row-only-windowed table (or vice versa) never scrolls an axis that has nothing to reveal.
|
|
6231
|
+
// Pure — reads no gesture state, only the live container rect + the windowing predicates.
|
|
6232
|
+
edgeDelta = (clientX: any, clientY: any) => {
|
|
6233
|
+
if (!this.gridScrollEl) return {
|
|
6234
|
+
dx: 0,
|
|
6235
|
+
dy: 0
|
|
6236
|
+
};
|
|
6237
|
+
const rect = this.gridScrollEl.getBoundingClientRect();
|
|
6238
|
+
let dx = 0;
|
|
6239
|
+
let dy = 0;
|
|
6240
|
+
if (this.colsWindowed()) {
|
|
6241
|
+
if (clientX - rect.left < this.FILL_EDGE_SCROLL_PX) dx = -this.FILL_EDGE_SCROLL_STEP;else if (rect.right - clientX < this.FILL_EDGE_SCROLL_PX) dx = this.FILL_EDGE_SCROLL_STEP;
|
|
6242
|
+
}
|
|
6243
|
+
if (this.rowsWindowed()) {
|
|
6244
|
+
if (clientY - rect.top < this.FILL_EDGE_SCROLL_PX) dy = -this.FILL_EDGE_SCROLL_STEP;else if (rect.bottom - clientY < this.FILL_EDGE_SCROLL_PX) dy = this.FILL_EDGE_SCROLL_STEP;
|
|
6245
|
+
}
|
|
6246
|
+
return {
|
|
6247
|
+
dx,
|
|
6248
|
+
dy
|
|
6249
|
+
};
|
|
6250
|
+
};
|
|
6251
|
+
|
|
5321
6252
|
teardownFillDrag = () => {
|
|
5322
6253
|
if (typeof document !== 'undefined') {
|
|
5323
6254
|
if (this.fillDragMove) document.removeEventListener('pointermove', this.fillDragMove);
|
|
@@ -5326,6 +6257,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5326
6257
|
this.fillDragMove = null;
|
|
5327
6258
|
this.fillDragUp = null;
|
|
5328
6259
|
this.fillDragging = false;
|
|
6260
|
+
// D-13 (T-87-06-02): idempotent cleanup of the edge-auto-scroll rAF loop — the SAME discipline
|
|
6261
|
+
// fillDragMove/fillDragUp already follow. Called both on a normal pointerup release AND at the
|
|
6262
|
+
// TOP of the next onFillHandlePointerDown (the #leak guard below), so an interrupted gesture
|
|
6263
|
+
// (pointer released off-window, context menu, alt-tab) can never leave a stray rAF scrolling
|
|
6264
|
+
// the table forever.
|
|
6265
|
+
if (this.fillEdgeScrollRaf != null) {
|
|
6266
|
+
if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(this.fillEdgeScrollRaf);
|
|
6267
|
+
this.fillEdgeScrollRaf = null;
|
|
6268
|
+
}
|
|
5329
6269
|
};
|
|
5330
6270
|
|
|
5331
6271
|
cellIndexFromPoint = (clientX: any, clientY: any) => {
|
|
@@ -5363,7 +6303,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5363
6303
|
// pointerup was missed (pointer released off-window, context menu, alt-tab), the prior fillDrag's
|
|
5364
6304
|
// document pointermove/pointerup stay attached; overwriting fillDragMove/fillDragUp below would
|
|
5365
6305
|
// strand them (removeEventListener could never reach the old refs) → a permanent global
|
|
5366
|
-
// pointermove leak. teardownFillDrag is idempotent (no-op when nothing is attached)
|
|
6306
|
+
// pointermove leak. teardownFillDrag is idempotent (no-op when nothing is attached) — it ALSO
|
|
6307
|
+
// cancels any orphaned edge-auto-scroll rAF from the same prior gesture.
|
|
5367
6308
|
this.teardownFillDrag();
|
|
5368
6309
|
this.fillDragging = true;
|
|
5369
6310
|
// B7: snapshot the PRE-DRAG rectangle (the fill SOURCE) NOW, before pointermove grows the
|
|
@@ -5377,21 +6318,74 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5377
6318
|
r: sourceBox.r1,
|
|
5378
6319
|
c: sourceBox.c1
|
|
5379
6320
|
} : null;
|
|
6321
|
+
// D-13: the pointer's LAST known screen position, updated on every real pointermove AND read
|
|
6322
|
+
// by the rAF loop below on every frame. The scroll loop must re-resolve the cell under this
|
|
6323
|
+
// FIXED screen point each frame — the pointer itself may not move at all while the user simply
|
|
6324
|
+
// holds it at the edge, but the content sliding beneath it means a DIFFERENT cell sits there
|
|
6325
|
+
// each frame, which is what must extend the range (this is the whole point of "edge-triggered
|
|
6326
|
+
// auto-scroll": a real pointermove would never fire on a stationary pointer).
|
|
6327
|
+
let lastClientX = 0;
|
|
6328
|
+
let lastClientY = 0;
|
|
6329
|
+
// applyPointAt: resolve the cell under (clientX, clientY) and extend the range to it if it is
|
|
6330
|
+
// a NEW cell (B20 dedup — unchanged from the pre-87-06 shape). Shared by both a real
|
|
6331
|
+
// pointermove and every frame of the edge-auto-scroll loop.
|
|
6332
|
+
const applyPointAt = (clientX: any, clientY: any) => {
|
|
6333
|
+
const cell = this.cellIndexFromPoint(clientX, clientY);
|
|
6334
|
+
if (cell && (!lastCell || cell.r !== lastCell.r || cell.c !== lastCell.c)) {
|
|
6335
|
+
lastCell = cell;
|
|
6336
|
+
this.setRangeFocus(cell.r, cell.c);
|
|
6337
|
+
}
|
|
6338
|
+
};
|
|
6339
|
+
// scrollStep: the single rAF loop (T-87-06-02) — reads the LAST known pointer position each
|
|
6340
|
+
// frame, scrolls the windowed axis/axes still within the edge threshold, then re-resolves the
|
|
6341
|
+
// cell now under that same screen point. Self-terminates (never reschedules) the moment the
|
|
6342
|
+
// gesture ends OR the pointer no longer sits in an edge zone — so leaving the edge, or a
|
|
6343
|
+
// pointerup, stops the scroll immediately with no lingering frame.
|
|
6344
|
+
const scrollStep = () => {
|
|
6345
|
+
if (!this.fillDragging) {
|
|
6346
|
+
this.fillEdgeScrollRaf = null;
|
|
6347
|
+
return;
|
|
6348
|
+
}
|
|
6349
|
+
const d = this.edgeDelta(lastClientX, lastClientY);
|
|
6350
|
+
if (d.dx === 0 && d.dy === 0) {
|
|
6351
|
+
this.fillEdgeScrollRaf = null;
|
|
6352
|
+
return;
|
|
6353
|
+
}
|
|
6354
|
+
if (this.gridScrollEl) {
|
|
6355
|
+
// T-87-06-03: direct scrollLeft/scrollTop writes — NEVER virtualizer.scrollToOffset /
|
|
6356
|
+
// colVirtualizer.scrollToOffset. A programmatic virtual-core scroll sets
|
|
6357
|
+
// virtualizer.scrollState, which remeasureWindow() bails on for as long as it is non-null;
|
|
6358
|
+
// a SUSTAINED auto-scroll driven through that API would suppress row remeasurement for the
|
|
6359
|
+
// whole gesture. scrollLeft/scrollTop are ordinary user-scroll-shaped writes (scrollState
|
|
6360
|
+
// stays null), so remeasureWindow() keeps observing recycled rows normally throughout.
|
|
6361
|
+
if (d.dx !== 0) this.gridScrollEl.scrollLeft = this.gridScrollEl.scrollLeft + d.dx;
|
|
6362
|
+
if (d.dy !== 0) this.gridScrollEl.scrollTop = this.gridScrollEl.scrollTop + d.dy;
|
|
6363
|
+
}
|
|
6364
|
+
applyPointAt(lastClientX, lastClientY);
|
|
6365
|
+
this.fillEdgeScrollRaf = requestAnimationFrame(scrollStep);
|
|
6366
|
+
};
|
|
5380
6367
|
const move = (ev: any) => {
|
|
5381
6368
|
if (!this.fillDragging) return;
|
|
5382
|
-
|
|
6369
|
+
lastClientX = ev.clientX;
|
|
6370
|
+
lastClientY = ev.clientY;
|
|
5383
6371
|
// B20: dedup by target cell. setRangeFocus emits range-change, so calling it on EVERY
|
|
5384
6372
|
// pointermove (the pointer fires many per cell) spams the event with identical payloads.
|
|
5385
6373
|
// Only extend (and emit) when the pointer enters a DIFFERENT cell than the last — lastCell
|
|
5386
6374
|
// seeds from the pre-drag bottom-right corner, so a move that stays on the source corner
|
|
5387
6375
|
// or re-enters the same cell is suppressed (the range is unchanged).
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
6376
|
+
applyPointAt(ev.clientX, ev.clientY);
|
|
6377
|
+
// D-13: start the edge-auto-scroll loop the FIRST time the pointer enters an edge zone
|
|
6378
|
+
// (idempotent — fillEdgeScrollRaf is only null when no loop is currently scheduled). The
|
|
6379
|
+
// loop itself (scrollStep) re-evaluates edgeDelta every frame and self-terminates once the
|
|
6380
|
+
// pointer leaves the zone, so no separate "still in zone" check is needed here.
|
|
6381
|
+
if (this.fillEdgeScrollRaf == null && typeof requestAnimationFrame === 'function') {
|
|
6382
|
+
const d = this.edgeDelta(ev.clientX, ev.clientY);
|
|
6383
|
+
if (d.dx !== 0 || d.dy !== 0) this.fillEdgeScrollRaf = requestAnimationFrame(scrollStep);
|
|
5391
6384
|
}
|
|
5392
6385
|
};
|
|
5393
6386
|
const up = () => {
|
|
5394
|
-
// teardownFillDrag clears fillDragging + removes both listeners
|
|
6387
|
+
// teardownFillDrag clears fillDragging + removes both listeners + cancels any in-flight
|
|
6388
|
+
// edge-auto-scroll rAF (CR-04 / T-87-06-02 shared path).
|
|
5395
6389
|
this.teardownFillDrag();
|
|
5396
6390
|
// A plain click on the fill handle (pointerdown+up with NO intervening drag) leaves lastCell
|
|
5397
6391
|
// at the source box's own origin corner (r1,c1), so fillRange(sourceBox, corner) would
|
|
@@ -5490,12 +6484,6 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5490
6484
|
}
|
|
5491
6485
|
};
|
|
5492
6486
|
|
|
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
6487
|
// The column id at the active cell (the active row's visible cell list @ activeColIndex).
|
|
5500
6488
|
// Null when out of range (no body rows, or active cell is a header / select column).
|
|
5501
6489
|
activeCellColumnId = () => {
|
|
@@ -5511,7 +6499,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5511
6499
|
// isActiveCellEditable: the active cell sits in an editable column AND is a body cell
|
|
5512
6500
|
// (req-1). Gates the F2/Enter/printable edit-entry branches in onGridKeyDown; a
|
|
5513
6501
|
// non-editable active cell falls through to the reserved enterControl path.
|
|
6502
|
+
// C1 (quick 260906-cvo) — Layer 1: a group-header row is NEVER editable, regardless of
|
|
6503
|
+
// column. This is the single highest-value guard in the cluster: it closes Shift+F2, the
|
|
6504
|
+
// checkbox toggle, and the F2/Enter/printable edit-entry branches in ONE edit (they are all
|
|
6505
|
+
// `else if` branches gated on this predicate), AND restores fallthrough so Enter on a group
|
|
6506
|
+
// row reaches the group-toggle branch below them instead of opening an editor.
|
|
5514
6507
|
isActiveCellEditable = () => {
|
|
6508
|
+
if (this.rowIndexIsGrouped(this._activeRow.value)) return false;
|
|
5515
6509
|
const colId = this.activeCellColumnId();
|
|
5516
6510
|
return colId != null && this.columnEditable(colId);
|
|
5517
6511
|
};
|
|
@@ -5529,6 +6523,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5529
6523
|
// non-virtual, wr.vi.index virtual).
|
|
5530
6524
|
isEditing = (rowIndex: any, colIndex: any) => {
|
|
5531
6525
|
if (this._editVer.value < 0) return false;
|
|
6526
|
+
// C1 (quick 260906-cvo) — Layer 6: the ONLY layer whose reachability does not depend on a
|
|
6527
|
+
// keystroke. Editing state is INDEX-based over the visible model, and nothing clears it
|
|
6528
|
+
// when the row model re-derives — a consumer that changes the grouping model WHILE a cell
|
|
6529
|
+
// editor is open can move a group-header row under the index $data.editingRow/
|
|
6530
|
+
// editingRowIndex still holds, rendering the editor on a group row with no beginEdit
|
|
6531
|
+
// involved. Placed AFTER the editVer read (preserves the Svelte/Solid re-derivation
|
|
6532
|
+
// discipline the header comment above documents).
|
|
6533
|
+
if (this.rowIndexIsGrouped(rowIndex)) return false;
|
|
5532
6534
|
if (this._editingRowIndex.value != null && this._editingRowIndex.value === rowIndex) {
|
|
5533
6535
|
const colId = this.columnIdAt(rowIndex, colIndex);
|
|
5534
6536
|
return colId != null && this.columnEditable(colId);
|
|
@@ -5568,29 +6570,6 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5568
6570
|
this._invalidMsg.value = msg != null ? msg : '';
|
|
5569
6571
|
};
|
|
5570
6572
|
|
|
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).
|
|
5576
|
-
replaceRowValue = (rows: any, rowIndex: any, field: any, value: any) => {
|
|
5577
|
-
const src = rows || [];
|
|
5578
|
-
const out = [];
|
|
5579
|
-
for (let i = 0; i < src.length; i++) {
|
|
5580
|
-
if (i === rowIndex) {
|
|
5581
|
-
// WR-03: own-property spread, NOT `for (const k in orig)` which walks the prototype chain
|
|
5582
|
-
// and would copy inherited enumerable props of typed/class-instance row objects.
|
|
5583
|
-
out.push({
|
|
5584
|
-
...(src[i] || {}),
|
|
5585
|
-
[field]: value
|
|
5586
|
-
});
|
|
5587
|
-
} else {
|
|
5588
|
-
out.push(src[i]);
|
|
5589
|
-
}
|
|
5590
|
-
}
|
|
5591
|
-
return out;
|
|
5592
|
-
};
|
|
5593
|
-
|
|
5594
6573
|
// Map a visible-model body-row index ($data.rows index) to its underlying currentData()
|
|
5595
6574
|
// index via the row's original object identity (sorting/filtering/pagination may reorder
|
|
5596
6575
|
// the visible model away from the source array order). Falls back to the same index.
|
|
@@ -5729,6 +6708,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5729
6708
|
// $data) so a Tab-advance that just setState'd activeRow/Col works on React. Clears any
|
|
5730
6709
|
// prior invalid state. Focus moves into the editor.
|
|
5731
6710
|
beginEdit = (rowIndex: any, colIndex: any, seed: any) => {
|
|
6711
|
+
// C1 (quick 260906-cvo) — Layer 2: closes the `onGridClick` (singleClickEdit) path, which
|
|
6712
|
+
// calls beginEdit directly gated ONLY on columnEditable — isActiveCellEditable (Layer 1)
|
|
6713
|
+
// does not run on that path at all.
|
|
6714
|
+
if (this.rowIndexIsGrouped(rowIndex)) return;
|
|
5732
6715
|
const colId = this.columnIdAt(rowIndex, colIndex);
|
|
5733
6716
|
if (colId == null || !this.columnEditable(colId)) return;
|
|
5734
6717
|
// A new edit session starts — reset the sync idempotency latch so THIS session's eventual
|
|
@@ -5775,23 +6758,6 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5775
6758
|
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(tryFocus);else setTimeout(tryFocus, 0);
|
|
5776
6759
|
};
|
|
5777
6760
|
|
|
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).
|
|
5784
|
-
indexOfRowIn = (rows: any, rowOriginal: any, rowId: any) => {
|
|
5785
|
-
const list = rows || [];
|
|
5786
|
-
for (let i = 0; i < list.length; i++) {
|
|
5787
|
-
const r = list[i];
|
|
5788
|
-
if (!r) continue;
|
|
5789
|
-
if (rowId != null && r.id === rowId) return i;
|
|
5790
|
-
if (rowOriginal != null && r.original === rowOriginal) return i;
|
|
5791
|
-
}
|
|
5792
|
-
return -1;
|
|
5793
|
-
};
|
|
5794
|
-
|
|
5795
6761
|
// endEdit: tear down the editor (shared by commit/cancel). Clears the editing pair +
|
|
5796
6762
|
// draft + invalid state and returns to navigation mode. Does NOT move focus (callers
|
|
5797
6763
|
// decide where focus lands — commit/cancel return it to the owning cell).
|
|
@@ -5905,7 +6871,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5905
6871
|
this.committedThisSession = true;
|
|
5906
6872
|
if (changed) {
|
|
5907
6873
|
const srcIndex = this.sourceIndexOfRow(this._editingRow.value);
|
|
5908
|
-
const next =
|
|
6874
|
+
const next = replaceRowValue(this.currentData(), srcIndex, field, newValue);
|
|
5909
6875
|
this.writeData(next);
|
|
5910
6876
|
// Exactly one emit per commit, from this single call site (writeData does NOT emit).
|
|
5911
6877
|
this.dispatchEvent(new CustomEvent("cell-edit-commit", {
|
|
@@ -5956,6 +6922,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5956
6922
|
// editor:'checkbox' columns only (Space/Enter/F2), full-row edit mode is unaffected (the
|
|
5957
6923
|
// editingRowIndex early return in onGridKeyDown already excludes it).
|
|
5958
6924
|
toggleActiveBooleanCell = () => {
|
|
6925
|
+
// C1 (quick 260906-cvo) — Layer 4: the only write funnel bypassing both beginEdit/
|
|
6926
|
+
// commitEdit and applyGridToRange. Reachable today only through the Layer-1-gated
|
|
6927
|
+
// checkbox branch, so this line is redundant AT HEAD — it earns its place by making "no
|
|
6928
|
+
// write ever targets a group row" a property of the write funnels themselves.
|
|
6929
|
+
if (this.rowIndexIsGrouped(this._activeRow.value)) return;
|
|
5959
6930
|
const colId = this.columnIdAt(this._activeRow.value, this._activeColIndex.value);
|
|
5960
6931
|
if (colId == null || !this.columnEditable(colId)) return;
|
|
5961
6932
|
const rowList = this._rows.value || [];
|
|
@@ -5979,7 +6950,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5979
6950
|
// Sync idempotency latch: this toggle is a commit-equivalent (mirrors commitEdit's D-07
|
|
5980
6951
|
// single-emit discipline) — flip it too so a stray re-entry after this toggle no-ops.
|
|
5981
6952
|
this.committedThisSession = true;
|
|
5982
|
-
this.writeData(
|
|
6953
|
+
this.writeData(replaceRowValue(this.currentData(), srcIndex, field, newValue));
|
|
5983
6954
|
// Exactly one emit per toggle, from this single call site (writeData does NOT emit) —
|
|
5984
6955
|
// mirrors commitEdit's D-07 single-emit discipline.
|
|
5985
6956
|
this.dispatchEvent(new CustomEvent("cell-edit-commit", {
|
|
@@ -6018,12 +6989,6 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6018
6989
|
this.focusCellWhenReady(focusRow, focusCol);
|
|
6019
6990
|
};
|
|
6020
6991
|
|
|
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
6992
|
// The editable [columnId, field] pairs for a body row at the given visible-model index,
|
|
6028
6993
|
// in visible-cell order. field is the column's accessorKey (the row-object key to write).
|
|
6029
6994
|
editableColumnsForRow = (rowIndex: any) => {
|
|
@@ -6093,6 +7058,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6093
7058
|
beginRowEdit = (row: any) => {
|
|
6094
7059
|
const rowIndex = this.rowIndexOf(row);
|
|
6095
7060
|
if (rowIndex < 0) return;
|
|
7061
|
+
// C1 (quick 260906-cvo) — Layer 5: a second, independent editing-state funnel that takes a
|
|
7062
|
+
// row OBJECT from its caller. Reachable today only via Shift+F2 (Layer 1), so also
|
|
7063
|
+
// redundant at HEAD; kept for the same reason as Layer 4 — it is a mutation entry point,
|
|
7064
|
+
// and the natural call site for a future #rowActions "Edit" drop-in button.
|
|
7065
|
+
if (this.rowIndexIsGrouped(rowIndex)) return;
|
|
6096
7066
|
const editable = this.editableColumnsForRow(rowIndex);
|
|
6097
7067
|
if (editable.length === 0) return;
|
|
6098
7068
|
// A new edit session starts — reset the sync idempotency latch (see editCellLifecycle.rzts).
|
|
@@ -6200,7 +7170,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6200
7170
|
if (changed) {
|
|
6201
7171
|
// ONE fresh-array replace of the SINGLE row object with all field values applied at once.
|
|
6202
7172
|
const srcIndex = this.sourceIndexOfRow(rowIndex);
|
|
6203
|
-
const next =
|
|
7173
|
+
const next = replaceRowValues(this.currentData(), srcIndex, fieldValues);
|
|
6204
7174
|
this.writeData(next);
|
|
6205
7175
|
// EXACTLY ONE emit per row commit, from THIS single call site (React multi-emit dedup, D-07).
|
|
6206
7176
|
this.dispatchEvent(new CustomEvent("row-edit-commit", {
|
|
@@ -6250,27 +7220,6 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6250
7220
|
this.focusCellWhenReady(focusRow, focusCol);
|
|
6251
7221
|
};
|
|
6252
7222
|
|
|
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).
|
|
6255
|
-
replaceRowValues = (rows: any, rowIndex: any, fieldValues: any) => {
|
|
6256
|
-
const src = rows || [];
|
|
6257
|
-
const fv = fieldValues || {};
|
|
6258
|
-
const out = [];
|
|
6259
|
-
for (let i = 0; i < src.length; i++) {
|
|
6260
|
-
if (i === rowIndex) {
|
|
6261
|
-
// WR-03: own-property spread (orig then the field→value map), NOT a `for..in`
|
|
6262
|
-
// prototype-walking copy. Spread copies own enumerable props only.
|
|
6263
|
-
out.push({
|
|
6264
|
-
...(src[i] || {}),
|
|
6265
|
-
...fv
|
|
6266
|
-
});
|
|
6267
|
-
} else {
|
|
6268
|
-
out.push(src[i]);
|
|
6269
|
-
}
|
|
6270
|
-
}
|
|
6271
|
-
return out;
|
|
6272
|
-
};
|
|
6273
|
-
|
|
6274
7223
|
// Compute the next editable cell for Tab-advance (req-3, RESEARCH Open-Q3 deterministic
|
|
6275
7224
|
// rule): skip non-editable columns within the row; wrap to the NEXT row's first editable
|
|
6276
7225
|
// cell at the row's end; stop (return null) at grid end. Pure index math over the visible
|
|
@@ -6602,8 +7551,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6602
7551
|
const lastRow = this.bodyRowCount() - 1;
|
|
6603
7552
|
const maxRow = lastRow < 0 ? 0 : lastRow;
|
|
6604
7553
|
const maxCol = this.visibleColCount() - 1;
|
|
6605
|
-
const r =
|
|
6606
|
-
const c =
|
|
7554
|
+
const r = clamp(Math.trunc(Number(rowIndex)) || 0, 0, maxRow);
|
|
7555
|
+
const c = clamp(Math.trunc(Number(colIndex)) || 0, 0, maxCol < 0 ? 0 : maxCol);
|
|
6607
7556
|
// A new edit session starts — reset the sync idempotency latch (see editCellLifecycle.rzts).
|
|
6608
7557
|
this.committedThisSession = false;
|
|
6609
7558
|
this._activeIsHeader.value = false;
|
|
@@ -6634,7 +7583,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6634
7583
|
editRow = (rowIndex: any) => {
|
|
6635
7584
|
const lastRow = this.bodyRowCount() - 1;
|
|
6636
7585
|
const maxRow = lastRow < 0 ? 0 : lastRow;
|
|
6637
|
-
const r =
|
|
7586
|
+
const r = clamp(Math.trunc(Number(rowIndex)) || 0, 0, maxRow);
|
|
6638
7587
|
const rowList = this._rows.value || [];
|
|
6639
7588
|
const row = rowList[r];
|
|
6640
7589
|
if (!row) return;
|
|
@@ -6706,16 +7655,24 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6706
7655
|
// or user nav bumps again → a pending focusAbsCellWhenReady from THIS call aborts.
|
|
6707
7656
|
this.focusIntentEpoch = this.focusIntentEpoch + 1;
|
|
6708
7657
|
const maxCol = this.visibleColCount() - 1;
|
|
6709
|
-
const c =
|
|
7658
|
+
const c = clamp(Math.trunc(Number(colIndex)) || 0, 0, maxCol < 0 ? 0 : maxCol);
|
|
6710
7659
|
// C1: clamp the ABSOLUTE row index to the full filtered+sorted (pre-pagination) bounds.
|
|
6711
7660
|
const absLast = this.prePaginationRowCount() - 1;
|
|
6712
|
-
const absRow =
|
|
7661
|
+
const absRow = clamp(Math.trunc(Number(rowIndex)) || 0, 0, absLast < 0 ? 0 : absLast);
|
|
6713
7662
|
// B14: snapshot the PRE-write ABSOLUTE position so the activecell-change emit fires ONLY on a
|
|
6714
7663
|
// real move (mirrors the keyboard path's WR-06 suppression). A no-op focusCell to the already-
|
|
6715
7664
|
// active cell must NOT emit; a header→body landing (prevIsHeader) is a real move.
|
|
7665
|
+
// Phase 87 87-06 (D-08, deferred-items.md "87-03: focusCell's activecell-change emit guard
|
|
7666
|
+
// ignores column-only moves"): also snapshot the PRE-write column so a column-ONLY move (e.g.
|
|
7667
|
+
// focusCell(sameRow, differentCol) — the exact D-08 case) is recognized as a real move too. The
|
|
7668
|
+
// pre-fix guard compared only the row, so DOM focus moved correctly but the event never fired —
|
|
7669
|
+
// logged as a genuine, pre-existing bug in 87-03 and explicitly revisited here because this
|
|
7670
|
+
// plan's absolute-column-index work (and its own acceptance criteria) makes fixing it correct
|
|
7671
|
+
// and cheap now, rather than leaving it deferred.
|
|
6716
7672
|
const prevAbs = this.toAbsRow(this._activeRow.value);
|
|
7673
|
+
const prevCol = this._activeColIndex.value;
|
|
6717
7674
|
const prevIsHeader = this._activeIsHeader.value;
|
|
6718
|
-
if (this.
|
|
7675
|
+
if (this.rowsWindowed()) {
|
|
6719
7676
|
// Virtual mode: $data.activeRow IS the full pre-pagination index (the wr.vi.index space), so
|
|
6720
7677
|
// the absolute index maps 1:1. focusActiveCell already runs the D-12 off-window scroll-then-
|
|
6721
7678
|
// focus path (scrollToIndex(absRow) → deferred-rAF focus) when the row is outside the window.
|
|
@@ -6749,7 +7706,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6749
7706
|
this.focusActiveCell(localRow, c, false);
|
|
6750
7707
|
}
|
|
6751
7708
|
}
|
|
6752
|
-
if (absRow !== prevAbs || prevIsHeader) {
|
|
7709
|
+
if (absRow !== prevAbs || c !== prevCol || prevIsHeader) {
|
|
6753
7710
|
this.dispatchEvent(new CustomEvent("activecell-change", {
|
|
6754
7711
|
detail: {
|
|
6755
7712
|
rowIndex: absRow,
|