@rozie-ui/data-table-lit 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -82
- package/dist/index.cjs +1032 -141
- package/dist/index.d.cts +190 -32
- package/dist/index.d.mts +190 -32
- package/dist/index.mjs +1032 -141
- package/package.json +2 -2
- package/src/Column.ts +3 -3
- package/src/DataTable.ts +1466 -134
- package/src/EditorCheckbox.ts +35 -2
- package/src/EditorDate.ts +59 -6
- package/src/EditorNumber.ts +47 -5
- package/src/EditorSelect.ts +48 -7
- package/src/EditorText.ts +38 -6
- package/src/FilterNumberRange.ts +77 -9
- package/src/FilterSelect.ts +17 -2
- package/src/FilterText.ts +47 -6
- package/src/GroupBar.ts +76 -3
- package/src/helpers/columnDefUtils.ts +73 -1
- package/src/helpers/dateValue.ts +55 -0
- package/src/themes/base.css +122 -88
- package/src/themes/bootstrap.css +60 -24
- package/src/themes/material.css +54 -22
- package/src/themes/shadcn.css +59 -23
package/src/DataTable.ts
CHANGED
|
@@ -5,7 +5,7 @@ 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';
|
|
8
|
+
import { isSafeKey, wrapAggregationFn, indexDefsById, editorKindWarning, collectGroupableLeafDefs } from './helpers/columnDefUtils';
|
|
9
9
|
import { applyUpdater, clamp, focusables } from './helpers/indexMath';
|
|
10
10
|
import { escapeTsvField, parseTsv, tileGridToBox, tileIndex } from './helpers/tsvGrid';
|
|
11
11
|
import { replaceRowValue, indexOfRowIn, replaceRowValues } from './helpers/rowValueUtils';
|
|
@@ -37,6 +37,15 @@ import { Virtualizer, elementScroll, observeElementRect, observeElementOffset, m
|
|
|
37
37
|
// snapshot (the rete stale-closure anti-pattern — a top-level $computed/useCallback
|
|
38
38
|
// freezes the table at the empty-initial state on React).
|
|
39
39
|
|
|
40
|
+
// C-09's value-equality guard. Imported HERE, in DataTable.rozie's OWN script, because that
|
|
41
|
+
// is where it is USED: a `.rzts` partial's import of a symbol the partial itself never
|
|
42
|
+
// references is dropped as unused, and the host script then reaches a free identifier. That
|
|
43
|
+
// failed at RUNTIME, not at compile time — `ReferenceError: columnSpecsEquivalent is not
|
|
44
|
+
// defined` on the first paste — which is the only way this class of mistake shows up.
|
|
45
|
+
import { columnSpecsEquivalent } from './helpers/columnDefUtils';
|
|
46
|
+
|
|
47
|
+
// The registry API handed to <Column> children (whole-object-replace — T-48-PP guard).
|
|
48
|
+
|
|
40
49
|
const __rozieCtx_data_table_columns = createContext(Symbol.for("rozie:data-table:columns"));
|
|
41
50
|
|
|
42
51
|
interface RozieGroupBarSlotCtx {
|
|
@@ -63,9 +72,15 @@ interface RozieDynamicFilterSlotCtx {
|
|
|
63
72
|
value: any;
|
|
64
73
|
uniqueValues: any;
|
|
65
74
|
minMax: any;
|
|
75
|
+
columnLabel: any;
|
|
66
76
|
setFilter: any;
|
|
67
77
|
}
|
|
68
78
|
|
|
79
|
+
interface RoziePlaceholderSlotCtx {
|
|
80
|
+
index: any;
|
|
81
|
+
columnId: any;
|
|
82
|
+
}
|
|
83
|
+
|
|
69
84
|
interface RozieSelectCellSlotCtx {
|
|
70
85
|
row: any;
|
|
71
86
|
checked: any;
|
|
@@ -86,6 +101,7 @@ interface RozieDynamicEditorSlotCtx {
|
|
|
86
101
|
value: any;
|
|
87
102
|
commit: any;
|
|
88
103
|
cancel: any;
|
|
104
|
+
columnLabel: any;
|
|
89
105
|
autofocus: any;
|
|
90
106
|
}
|
|
91
107
|
|
|
@@ -104,6 +120,7 @@ interface RozieFilterSlotCtx {
|
|
|
104
120
|
value: any;
|
|
105
121
|
uniqueValues: any;
|
|
106
122
|
minMax: any;
|
|
123
|
+
columnLabel: any;
|
|
107
124
|
setFilter: any;
|
|
108
125
|
}
|
|
109
126
|
|
|
@@ -121,6 +138,7 @@ interface RozieEditorSlotCtx {
|
|
|
121
138
|
value: any;
|
|
122
139
|
commit: any;
|
|
123
140
|
cancel: any;
|
|
141
|
+
columnLabel: any;
|
|
124
142
|
autofocus: any;
|
|
125
143
|
}
|
|
126
144
|
|
|
@@ -130,6 +148,32 @@ export default class DataTable extends SignalWatcher(LitElement) {
|
|
|
130
148
|
|
|
131
149
|
static styles = css`
|
|
132
150
|
:host{display:contents}
|
|
151
|
+
:host {
|
|
152
|
+
--rdt-color: var(--rozie-data-table-fg, var(--rdt-ds-fg));
|
|
153
|
+
--rdt-select-accent: var(--rozie-data-table-select-accent, var(--rozie-data-table-accent, var(--rdt-ds-select-accent)));
|
|
154
|
+
--rdt-border: 1px solid var(--rozie-data-table-border-color, var(--rdt-ds-border-color));
|
|
155
|
+
--rdt-filter-border: 1px solid var(--rozie-data-table-control-border-color, var(--rdt-ds-control-border-color));
|
|
156
|
+
--rdt-filter-radius: var(--rozie-data-table-radius, var(--rdt-ds-radius));
|
|
157
|
+
--rdt-font: var(--rozie-data-table-font, var(--rdt-ds-font));
|
|
158
|
+
--rdt-cell-padding: var(--rozie-data-table-cell-padding, var(--rdt-ds-cell-padding));
|
|
159
|
+
--rdt-header-bg: var(--rozie-data-table-header-bg, var(--rdt-ds-header-bg));
|
|
160
|
+
--rdt-header-weight: var(--rozie-data-table-header-weight, var(--rdt-ds-header-weight));
|
|
161
|
+
--rdt-sort-ind-opacity: var(--rozie-data-table-sort-indicator-opacity, var(--rdt-ds-sort-indicator-opacity));
|
|
162
|
+
--rdt-resize-grip-color: var(--rozie-data-table-resize-grip-color, var(--rdt-ds-resize-grip-color));
|
|
163
|
+
--rdt-resize-grip-active: var(--rozie-data-table-resize-grip-active, var(--rozie-data-table-accent, var(--rdt-ds-resize-grip-active)));
|
|
164
|
+
--rdt-pin-btn-active-bg: var(--rozie-data-table-pin-active-bg, var(--rdt-ds-pin-active-bg));
|
|
165
|
+
--rdt-colvis-menu-bg: var(--rozie-data-table-menu-bg, var(--rdt-ds-menu-bg));
|
|
166
|
+
--rdt-colvis-menu-shadow: var(--rozie-data-table-menu-shadow, var(--rdt-ds-menu-shadow));
|
|
167
|
+
--rdt-colvis-menu-border: 1px solid var(--rozie-data-table-border-color, var(--rdt-ds-border-color));
|
|
168
|
+
--rdt-colvis-menu-radius: var(--rozie-data-table-radius, var(--rdt-ds-radius));
|
|
169
|
+
--rdt-colvis-summary-border: 1px solid var(--rozie-data-table-control-border-color, var(--rdt-ds-control-border-color));
|
|
170
|
+
--rdt-colvis-summary-radius: var(--rozie-data-table-radius, var(--rdt-ds-radius));
|
|
171
|
+
--rdt-page-btn-disabled-opacity: var(--rozie-data-table-disabled-opacity, var(--rdt-ds-disabled-opacity));
|
|
172
|
+
--rdt-page-btn-border: 1px solid var(--rozie-data-table-control-border-color, var(--rdt-ds-control-border-color));
|
|
173
|
+
--rdt-page-btn-radius: var(--rozie-data-table-radius, var(--rdt-ds-radius));
|
|
174
|
+
--rdt-page-size-border: 1px solid var(--rozie-data-table-control-border-color, var(--rdt-ds-control-border-color));
|
|
175
|
+
--rdt-page-size-radius: var(--rozie-data-table-radius, var(--rdt-ds-radius));
|
|
176
|
+
}
|
|
133
177
|
.rozie-data-table[data-rozie-s-d5dcab4c] {
|
|
134
178
|
border-collapse: collapse;
|
|
135
179
|
width: 100%;
|
|
@@ -264,6 +308,16 @@ export default class DataTable extends SignalWatcher(LitElement) {
|
|
|
264
308
|
flex-direction: column;
|
|
265
309
|
gap: var(--rdt-chrome-gap, 0.5rem);
|
|
266
310
|
}
|
|
311
|
+
.rozie-data-table[data-rozie-s-d5dcab4c] .rdt-skeleton[data-rozie-s-d5dcab4c] {
|
|
312
|
+
/* inline-block on the cell's own line box, exactly like a text value (a block box was 2px
|
|
313
|
+
short of a text row). */
|
|
314
|
+
display: inline-block;
|
|
315
|
+
width: 100%;
|
|
316
|
+
vertical-align: baseline;
|
|
317
|
+
border-radius: 4px;
|
|
318
|
+
background: color-mix(in srgb, currentColor 12%, transparent);
|
|
319
|
+
color: transparent;
|
|
320
|
+
}
|
|
267
321
|
.rozie-data-table-wrap[data-rozie-s-d5dcab4c] .rdt-toolbar[data-rozie-s-d5dcab4c] {
|
|
268
322
|
display: flex;
|
|
269
323
|
gap: var(--rdt-toolbar-gap, 0.5rem);
|
|
@@ -302,6 +356,9 @@ export default class DataTable extends SignalWatcher(LitElement) {
|
|
|
302
356
|
.rozie-data-table[data-rozie-s-d5dcab4c] .rdt-filter-row[data-rozie-s-d5dcab4c] {
|
|
303
357
|
background: var(--rdt-filter-row-bg, rgba(0, 0, 0, 0.015));
|
|
304
358
|
}
|
|
359
|
+
.rozie-data-table[data-rozie-s-d5dcab4c] .rdt-pinned[data-rozie-s-d5dcab4c] {
|
|
360
|
+
box-sizing: border-box;
|
|
361
|
+
}
|
|
305
362
|
.rozie-data-table[data-rozie-s-d5dcab4c] .rdt-filter-cell[data-rozie-s-d5dcab4c] {
|
|
306
363
|
padding: var(--rdt-filter-cell-padding, 0.35rem 0.75rem);
|
|
307
364
|
border-bottom: var(--rdt-border, 1px solid rgba(0, 0, 0, 0.08));
|
|
@@ -421,6 +478,9 @@ export default class DataTable extends SignalWatcher(LitElement) {
|
|
|
421
478
|
.rozie-data-table-wrap[data-rozie-s-d5dcab4c] .rdt-colvis-summary[data-rozie-s-d5dcab4c] {
|
|
422
479
|
cursor: pointer;
|
|
423
480
|
font: inherit;
|
|
481
|
+
/* Own the margin too: content-site typography styles bare \`summary\` (VitePress sets
|
|
482
|
+
\`margin: 16px 0\`), which grew the <details> and stretched the toolbar's search input. */
|
|
483
|
+
margin: 0;
|
|
424
484
|
padding: var(--rdt-colvis-summary-padding, 0.25rem 0.6rem);
|
|
425
485
|
border: var(--rdt-colvis-summary-border, 1px solid rgba(0, 0, 0, 0.2));
|
|
426
486
|
border-radius: var(--rdt-colvis-summary-radius, 4px);
|
|
@@ -459,6 +519,51 @@ export default class DataTable extends SignalWatcher(LitElement) {
|
|
|
459
519
|
text-align: var(--rdt-expander-col-align, center);
|
|
460
520
|
white-space: nowrap;
|
|
461
521
|
}
|
|
522
|
+
.rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="1"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before,
|
|
523
|
+
.rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="2"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before,
|
|
524
|
+
.rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="3"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before,
|
|
525
|
+
.rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="4"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before,
|
|
526
|
+
.rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="5"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before,
|
|
527
|
+
.rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="6"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before,
|
|
528
|
+
.rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="7"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before,
|
|
529
|
+
.rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="8"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before {
|
|
530
|
+
content: '';
|
|
531
|
+
display: inline-block;
|
|
532
|
+
vertical-align: middle;
|
|
533
|
+
}
|
|
534
|
+
.rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="1"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before {
|
|
535
|
+
width: calc(1 * var(--rdt-tree-indent, 1.25rem));
|
|
536
|
+
}
|
|
537
|
+
.rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="2"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before {
|
|
538
|
+
width: calc(2 * var(--rdt-tree-indent, 1.25rem));
|
|
539
|
+
}
|
|
540
|
+
.rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="3"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before {
|
|
541
|
+
width: calc(3 * var(--rdt-tree-indent, 1.25rem));
|
|
542
|
+
}
|
|
543
|
+
.rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="4"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before {
|
|
544
|
+
width: calc(4 * var(--rdt-tree-indent, 1.25rem));
|
|
545
|
+
}
|
|
546
|
+
.rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="5"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before {
|
|
547
|
+
width: calc(5 * var(--rdt-tree-indent, 1.25rem));
|
|
548
|
+
}
|
|
549
|
+
.rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="6"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before {
|
|
550
|
+
width: calc(6 * var(--rdt-tree-indent, 1.25rem));
|
|
551
|
+
}
|
|
552
|
+
.rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="7"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before {
|
|
553
|
+
width: calc(7 * var(--rdt-tree-indent, 1.25rem));
|
|
554
|
+
}
|
|
555
|
+
.rozie-data-table[data-rozie-s-d5dcab4c] tbody[data-rozie-s-d5dcab4c] tr[data-depth="8"][data-rozie-s-d5dcab4c] > .rdt-expander-td[data-rozie-s-d5dcab4c] + .rdt-td[data-rozie-s-d5dcab4c]::before {
|
|
556
|
+
width: calc(8 * var(--rdt-tree-indent, 1.25rem));
|
|
557
|
+
}
|
|
558
|
+
.rozie-data-table[role="grid"][data-rozie-s-d5dcab4c] {
|
|
559
|
+
user-select: none;
|
|
560
|
+
}
|
|
561
|
+
.rozie-data-table[role="grid"][data-rozie-s-d5dcab4c] input[data-rozie-s-d5dcab4c],
|
|
562
|
+
.rozie-data-table[role="grid"][data-rozie-s-d5dcab4c] textarea[data-rozie-s-d5dcab4c],
|
|
563
|
+
.rozie-data-table[role="grid"][data-rozie-s-d5dcab4c] select[data-rozie-s-d5dcab4c],
|
|
564
|
+
.rozie-data-table[role="grid"][data-rozie-s-d5dcab4c] [contenteditable="true"][data-rozie-s-d5dcab4c] {
|
|
565
|
+
user-select: text;
|
|
566
|
+
}
|
|
462
567
|
.rozie-data-table[data-rozie-s-d5dcab4c] .rdt-select-all[data-rozie-s-d5dcab4c],
|
|
463
568
|
.rozie-data-table[data-rozie-s-d5dcab4c] .rdt-select-row[data-rozie-s-d5dcab4c] {
|
|
464
569
|
cursor: pointer;
|
|
@@ -467,14 +572,14 @@ export default class DataTable extends SignalWatcher(LitElement) {
|
|
|
467
572
|
`;
|
|
468
573
|
|
|
469
574
|
/**
|
|
470
|
-
* The row data — `model: true`, so a committed cell/row edit writes a **fresh** array back through `
|
|
575
|
+
* The row data — `model: true`, so a committed cell/row edit writes a **fresh** array back through the two-way `data` binding (uncontrolled fallback `dataDefault`). A stable reference per Rozie's setup-once model — fed directly into table-core (never map/cloned in the watcher).
|
|
471
576
|
* @example
|
|
472
577
|
* <rozie-data-table .data=${rows} @data-change=${…} .columns=${cols}></rozie-data-table>
|
|
473
578
|
*/
|
|
474
579
|
@property({ type: Array, attribute: 'data' }) _data_attr!: any[];
|
|
475
580
|
private _dataControllable = createLitControllableProperty<any[]>({ host: this, eventName: 'data-change', defaultValue: [], initialControlledValue: undefined });
|
|
476
581
|
/**
|
|
477
|
-
* Config-array column fallback (lower precedence than `<Column>` children). Each entry: `{ id?, field, header?, sortable?, filterable?, pinned?, width? }`. Columns may come from this array, from `<Column>` children, or both (id-keyed last-write-wins union).
|
|
582
|
+
* Config-array column fallback (lower precedence than `<Column>` children). Each entry: `{ id?, field, header?, sortable?, filterable?, pinned?, width? }`. Columns may come from this array, from `<Column>` children, or both (id-keyed last-write-wins union). A `pinned` value of `'left'` or `'right'` is applied once as the table's initial `columnPinning` state — identical to `<Column pinned>` — which also REORDERS that column, since table-core orders visible cells `[left-pinned, center, right-pinned]`. A consumer who has already expressed a pin (an initial two-way `columnPinning` model value or an interactive pin) owns the slice and this declaration is ignored.
|
|
478
583
|
*/
|
|
479
584
|
@property({ type: Array }) columns: any[] = [];
|
|
480
585
|
/**
|
|
@@ -520,7 +625,7 @@ export default class DataTable extends SignalWatcher(LitElement) {
|
|
|
520
625
|
*/
|
|
521
626
|
@property({ type: Number, reflect: true }) pageCount: number | null = null;
|
|
522
627
|
/**
|
|
523
|
-
* Opt-in **expandable rows**. When `true`, a leading chevron expander column auto-injects (after the select column) and `getExpandedRowModel` activates; default `false` is byte-identical-off. Every row can expand to reveal a `#detail` panel unless `getSubRows` is supplied (then only rows with children expand).
|
|
628
|
+
* Opt-in **expandable rows**. When `true`, a leading chevron expander column auto-injects (after the select column) and `getExpandedRowModel` activates; default `false` is byte-identical-off. Every row can expand to reveal a `#detail` panel unless `getSubRows` is supplied (then only rows with children expand). Pass `expandable` as a real boolean — a valueless attribute only coerces to `true` on Vue and Lit.
|
|
524
629
|
*/
|
|
525
630
|
@property({ type: Boolean, reflect: true }) expandable: boolean = false;
|
|
526
631
|
/**
|
|
@@ -528,6 +633,10 @@ export default class DataTable extends SignalWatcher(LitElement) {
|
|
|
528
633
|
*/
|
|
529
634
|
@property({ type: Object, attribute: 'expanded' }) _expanded_attr: any | boolean = null;
|
|
530
635
|
private _expandedControllable = createLitControllableProperty<any | boolean>({ host: this, eventName: 'expanded-change', defaultValue: null, initialControlledValue: undefined });
|
|
636
|
+
/**
|
|
637
|
+
* Row identity `(originalRow, index, parentRow?) => string | number` — the key `rowSelection`, `expanded` and the per-row caches use. Default null → table-core keys rows by position (`"0"`, `"1"`, … and `"0.1"` for sub-rows), so inserting or removing rows above a selected or expanded row moves that state onto whatever row now sits at its index. Supply it whenever rows can be added or removed while state is held (server push, live lists); the result is coerced to a string.
|
|
638
|
+
*/
|
|
639
|
+
@property({ type: Function }) getRowId: ((...args: any[]) => any) | null = null;
|
|
531
640
|
/**
|
|
532
641
|
* Table-level child-row accessor `(originalRow, index) => TData[] | undefined` that drives nested sub-rows. When supplied (with `expandable`), table-core flattens the hierarchy and the expand seam reveals depth-indented child rows. Null → the `#detail` scoped slot is the expand mode.
|
|
533
642
|
*/
|
|
@@ -537,7 +646,7 @@ export default class DataTable extends SignalWatcher(LitElement) {
|
|
|
537
646
|
*/
|
|
538
647
|
@property({ type: Boolean, reflect: true }) groupable: boolean = false;
|
|
539
648
|
/**
|
|
540
|
-
* `GroupingState` — an ordered `string[]` of column ids (multi-column → nested groups, e.g. `['region','category']`). An empty/unbound list is ungrouped (byte-identical-off). Group-header rows are collapsible (they ride the expand model). Surfaces through `group-change`; uncontrolled fallback (`$data.groupingDefault`, default `[]`) when unbound — the default is `null` (mirroring `expanded`) so the uncontrolled fallback is reachable and the grouping auto-expand default can activate when a consumer applies grouping without binding `
|
|
649
|
+
* `GroupingState` — an ordered `string[]` of column ids (multi-column → nested groups, e.g. `['region','category']`). An empty/unbound list is ungrouped (byte-identical-off). Group-header rows are collapsible (they ride the expand model). Surfaces through `group-change`; uncontrolled fallback (`$data.groupingDefault`, default `[]`) when unbound — the default is `null` (mirroring `expanded`) so the uncontrolled fallback is reachable and the grouping auto-expand default can activate when a consumer applies grouping without binding the two-way `grouping` model (a non-null `[]` default would short-circuit it). All reads are null-guarded, so table-core still receives an array.
|
|
541
650
|
*/
|
|
542
651
|
@property({ type: Array, attribute: 'grouping' }) _grouping_attr: any[] | null = null;
|
|
543
652
|
private _groupingControllable = createLitControllableProperty<any[]>({ host: this, eventName: 'grouping-change', defaultValue: null, initialControlledValue: undefined });
|
|
@@ -577,7 +686,7 @@ export default class DataTable extends SignalWatcher(LitElement) {
|
|
|
577
686
|
*/
|
|
578
687
|
@property({ type: Boolean, reflect: true }) stickyHeader: boolean = false;
|
|
579
688
|
/**
|
|
580
|
-
* `'table'` (default, row-oriented, byte-behaviorally identical to a plain accessible table) | `'grid'`
|
|
689
|
+
* `'table'` (default, row-oriented, byte-behaviorally identical to a plain accessible table) | `'grid'` — lights up the full WAI-ARIA **[grid interaction mode](/components/data-table-grid-mode)**: `role="grid"`, a roving single tab-stop, 2-D APG arrow-key cell navigation, range selection, and clipboard support.
|
|
581
690
|
*/
|
|
582
691
|
@property({ type: String, reflect: true }) interactionMode: string = 'table';
|
|
583
692
|
/**
|
|
@@ -655,15 +764,27 @@ export default class DataTable extends SignalWatcher(LitElement) {
|
|
|
655
764
|
private _rangeAnchor = signal<any>(null);
|
|
656
765
|
private _rangeFocus = signal<any>(null);
|
|
657
766
|
private _pasteAnnounce = signal('');
|
|
767
|
+
private _rangeAnnounce = signal('');
|
|
658
768
|
private _liveAnnounce = signal('');
|
|
659
769
|
@query('[data-rozie-ref="__rozieRoot"]') private _ref__rozieRoot!: HTMLElement;
|
|
660
770
|
private __rozieWatchInitial_0 = true;
|
|
661
771
|
private __rozieWatchInitial_1 = true;
|
|
772
|
+
private __rozieWatchInitial_2 = true;
|
|
662
773
|
private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { context: __rozieCtx_data_table_columns, initialValue: ((__rozieCtxHost) => ({
|
|
663
774
|
registerColumn: (id: any, spec: any) => {
|
|
664
775
|
if (id == null) return;
|
|
665
776
|
const key = String(id);
|
|
666
777
|
if (key === '__proto__' || key === 'constructor' || key === 'prototype') return;
|
|
778
|
+
// C-09: skip the state write when the incoming spec is EQUIVALENT to the stored one.
|
|
779
|
+
// `<Column>`'s re-register watch keys on reference-typed props whose documented wiring is
|
|
780
|
+
// an inline literal — a fresh identity every consumer render — so without this guard every
|
|
781
|
+
// render whole-object-replaced `$data.colReg`, which the parent's re-feed watch keys on,
|
|
782
|
+
// which re-rendered the parent, which produced a new identity: a feedback loop, not a slow
|
|
783
|
+
// path. Guarded HERE rather than in the watch because this is the single seam every
|
|
784
|
+
// registration passes through, and because value-equality is the correct test for the
|
|
785
|
+
// registry's own purpose (has anything the table renders from actually changed?).
|
|
786
|
+
const prev = __rozieCtxHost._colReg.value ? (__rozieCtxHost._colReg.value as Record<string, any>)[key] : undefined;
|
|
787
|
+
if (prev !== undefined && columnSpecsEquivalent(prev, spec)) return;
|
|
667
788
|
__rozieCtxHost._colReg.value = {
|
|
668
789
|
...__rozieCtxHost._colReg.value,
|
|
669
790
|
[key]: spec
|
|
@@ -693,6 +814,9 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
693
814
|
@queryAssignedElements({ flatten: true }) private _slotDynamicColHeaderElements!: Element[];
|
|
694
815
|
@state() private _hasSlotDynamicFilter = false;
|
|
695
816
|
@queryAssignedElements({ flatten: true }) private _slotDynamicFilterElements!: Element[];
|
|
817
|
+
@state() private _hasSlotPlaceholder = false;
|
|
818
|
+
@queryAssignedElements({ slot: 'placeholder', flatten: true }) private _slotPlaceholderElements!: Element[];
|
|
819
|
+
@property({ attribute: false }) placeholder?: (scope: { index: any; columnId: any }) => unknown;
|
|
696
820
|
@state() private _hasSlotSelectCell = false;
|
|
697
821
|
@queryAssignedElements({ slot: 'selectCell', flatten: true }) private _slotSelectCellElements!: Element[];
|
|
698
822
|
@property({ attribute: false }) selectCell?: (scope: { row: any; checked: any; toggle: any }) => unknown;
|
|
@@ -708,13 +832,13 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
708
832
|
@property({ attribute: false }) colHeader?: (scope: { columnId: any; column: any; label: any }) => unknown;
|
|
709
833
|
@state() private _hasSlotFilter = false;
|
|
710
834
|
@queryAssignedElements({ slot: 'filter', flatten: true }) private _slotFilterElements!: Element[];
|
|
711
|
-
@property({ attribute: false }) filter?: (scope: { columnId: any; value: any; uniqueValues: any; minMax: any; setFilter: any }) => unknown;
|
|
835
|
+
@property({ attribute: false }) filter?: (scope: { columnId: any; value: any; uniqueValues: any; minMax: any; columnLabel: any; setFilter: any }) => unknown;
|
|
712
836
|
@state() private _hasSlotCell = false;
|
|
713
837
|
@queryAssignedElements({ slot: 'cell', flatten: true }) private _slotCellElements!: Element[];
|
|
714
838
|
@property({ attribute: false }) cell?: (scope: { columnId: any; column: any; row: any; value: any }) => unknown;
|
|
715
839
|
@state() private _hasSlotEditor = false;
|
|
716
840
|
@queryAssignedElements({ slot: 'editor', flatten: true }) private _slotEditorElements!: Element[];
|
|
717
|
-
@property({ attribute: false }) editor?: (scope: { columnId: any; column: any; row: any; value: any; commit: any; cancel: any; autofocus: any }) => unknown;
|
|
841
|
+
@property({ attribute: false }) editor?: (scope: { columnId: any; column: any; row: any; value: any; commit: any; cancel: any; columnLabel: any; autofocus: any }) => unknown;
|
|
718
842
|
// Phase 79 Plan 08 (R4) contract for 79-09: the record intake for
|
|
719
843
|
// record-routed slot fills. 79-09's consumer-side emitSlotFiller
|
|
720
844
|
// accumulates an object literal onto the SAME `.rozieSlots=${{ ... }}`
|
|
@@ -724,7 +848,7 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
724
848
|
// named function-prop / <slot> fallback (AC-9). Attribute
|
|
725
849
|
// deserialization is disabled — this is a function-valued record,
|
|
726
850
|
// never reflected to/from an HTML attribute.
|
|
727
|
-
@property({ attribute: false }) rozieSlots?: { [key: `colHeader-${string}`]: (scope: { columnId: any; column: any; label: any }) => unknown; [key: `filter-${string}`]: (scope: { columnId: any; value: any; uniqueValues: any; minMax: any; setFilter: any }) => unknown; [key: `cell-${string}`]: (scope: { columnId: any; column: any; row: any; value: any }) => unknown; [key: `editor-${string}`]: (scope: { columnId: any; column: any; row: any; value: any; commit: any; cancel: any; autofocus: any }) => unknown; } & Record<string, (scope: any) => unknown>;
|
|
851
|
+
@property({ attribute: false }) rozieSlots?: { [key: `colHeader-${string}`]: (scope: { columnId: any; column: any; label: any }) => unknown; [key: `filter-${string}`]: (scope: { columnId: any; value: any; uniqueValues: any; minMax: any; columnLabel: any; setFilter: any }) => unknown; [key: `cell-${string}`]: (scope: { columnId: any; column: any; row: any; value: any }) => unknown; [key: `editor-${string}`]: (scope: { columnId: any; column: any; row: any; value: any; commit: any; cancel: any; columnLabel: any; autofocus: any }) => unknown; } & Record<string, (scope: any) => unknown>;
|
|
728
852
|
|
|
729
853
|
private _disconnectCleanups: Array<() => void> = [];
|
|
730
854
|
// Re-parenting guard: set true once the deferred teardown has actually
|
|
@@ -787,6 +911,17 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
787
911
|
}
|
|
788
912
|
}
|
|
789
913
|
|
|
914
|
+
{
|
|
915
|
+
const slotEl = this.shadowRoot?.querySelector('slot[name="placeholder"]');
|
|
916
|
+
if (slotEl !== null && slotEl !== undefined) {
|
|
917
|
+
const update = () => { this._hasSlotPlaceholder = this._slotPlaceholderElements.length > 0; };
|
|
918
|
+
slotEl.addEventListener('slotchange', update);
|
|
919
|
+
// CR-05 fix: push cleanup so the listener is removed on disconnectedCallback.
|
|
920
|
+
this._disconnectCleanups.push(() => slotEl.removeEventListener('slotchange', update));
|
|
921
|
+
update();
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
790
925
|
{
|
|
791
926
|
const slotEl = this.shadowRoot?.querySelector('slot[name="selectCell"]');
|
|
792
927
|
if (slotEl !== null && slotEl !== undefined) {
|
|
@@ -883,6 +1018,7 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
883
1018
|
this._hasSlotSelectAll = Array.from(this.children).some((el) => el.getAttribute('slot') === 'selectAll');
|
|
884
1019
|
this._hasSlotDynamicColHeader = Array.from(this.children).some((el) => !el.hasAttribute('slot') && (el.nodeType !== 3 || (el.textContent?.trim().length ?? 0) > 0));
|
|
885
1020
|
this._hasSlotDynamicFilter = Array.from(this.children).some((el) => !el.hasAttribute('slot') && (el.nodeType !== 3 || (el.textContent?.trim().length ?? 0) > 0));
|
|
1021
|
+
this._hasSlotPlaceholder = Array.from(this.children).some((el) => el.getAttribute('slot') === 'placeholder');
|
|
886
1022
|
this._hasSlotSelectCell = Array.from(this.children).some((el) => el.getAttribute('slot') === 'selectCell');
|
|
887
1023
|
this._hasSlotDynamicCell = Array.from(this.children).some((el) => !el.hasAttribute('slot') && (el.nodeType !== 3 || (el.textContent?.trim().length ?? 0) > 0));
|
|
888
1024
|
this._hasSlotDynamicEditor = Array.from(this.children).some((el) => !el.hasAttribute('slot') && (el.nodeType !== 3 || (el.textContent?.trim().length ?? 0) > 0));
|
|
@@ -898,6 +1034,7 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
898
1034
|
firstUpdated(): void {
|
|
899
1035
|
this._armListeners();
|
|
900
1036
|
|
|
1037
|
+
this._disconnectCleanups.push(effect(() => { const __watchVal = (() => this._windowVer.value)(); untracked(() => { if (this.__rozieWatchInitial_0) { this.__rozieWatchInitial_0 = false; return; } (() => this.emitVisibleRangeIfChanged())(); }); }));
|
|
901
1038
|
this._disconnectCleanups.push(effect(() => { const __watchVal = (() => [this.sorting, this.globalFilter, this.columnFilters, this.pagination, // Server-side page-count sources (#2): re-feed when the consumer's rowCount/pageCount
|
|
902
1039
|
// changes at runtime (e.g. a server response updates the total) so getPageCount() and the
|
|
903
1040
|
// Next button availability track the new total.
|
|
@@ -911,11 +1048,17 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
911
1048
|
// columnDefs()/tableColumns() build the UNION of both, and reFeed re-passes columns.
|
|
912
1049
|
// (Consumers memoize the array as with $props.data/$props.sorting; the uncontrolled
|
|
913
1050
|
// <Column>-children path leaves $props.columns undefined — a stable no-op getter.)
|
|
914
|
-
this.columns, this._colReg.value])(); untracked(() => { if (this.
|
|
1051
|
+
this.columns, this._colReg.value])(); untracked(() => { if (this.__rozieWatchInitial_1) { this.__rozieWatchInitial_1 = false; return; } (() => {
|
|
1052
|
+
// Seed BEFORE the re-feed so the pinning this call may write is part of the state object
|
|
1053
|
+
// table-core receives on this same tick (rather than landing a frame late and re-ordering
|
|
1054
|
+
// getVisibleCells() a render after the columns appear). One-shot and self-latching — see
|
|
1055
|
+
// seedColumnPinning() in stateAssembly.rzts. This is the `<Column>`-children entry point:
|
|
1056
|
+
// those register during mount, so the $onMount call below can find zero columns.
|
|
1057
|
+
this.seedColumnPinning();
|
|
915
1058
|
this.reFeed();
|
|
916
1059
|
this.maybeClearHistoryOnExternalSwap();
|
|
917
1060
|
})(); }); }));
|
|
918
|
-
this._disconnectCleanups.push(effect(() => { const __watchVal = (() => [this.sorting, this.columnFilters, this.globalFilter, this._sortingDefault.value, this._columnFiltersDefault.value, this._globalFilterDefault.value])(); untracked(() => { if (this.
|
|
1061
|
+
this._disconnectCleanups.push(effect(() => { const __watchVal = (() => [this.sorting, this.columnFilters, this.globalFilter, this._sortingDefault.value, this._columnFiltersDefault.value, this._globalFilterDefault.value])(); untracked(() => { if (this.__rozieWatchInitial_2) { this.__rozieWatchInitial_2 = false; return; } (() => {
|
|
919
1062
|
const msg = this.buildSortFilterAnnounce();
|
|
920
1063
|
if (msg) this._liveAnnounce.value = msg;
|
|
921
1064
|
})(); }); }));
|
|
@@ -925,6 +1068,16 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
925
1068
|
if (id == null) return;
|
|
926
1069
|
const key = String(id);
|
|
927
1070
|
if (key === '__proto__' || key === 'constructor' || key === 'prototype') return;
|
|
1071
|
+
// C-09: skip the state write when the incoming spec is EQUIVALENT to the stored one.
|
|
1072
|
+
// `<Column>`'s re-register watch keys on reference-typed props whose documented wiring is
|
|
1073
|
+
// an inline literal — a fresh identity every consumer render — so without this guard every
|
|
1074
|
+
// render whole-object-replaced `$data.colReg`, which the parent's re-feed watch keys on,
|
|
1075
|
+
// which re-rendered the parent, which produced a new identity: a feedback loop, not a slow
|
|
1076
|
+
// path. Guarded HERE rather than in the watch because this is the single seam every
|
|
1077
|
+
// registration passes through, and because value-equality is the correct test for the
|
|
1078
|
+
// registry's own purpose (has anything the table renders from actually changed?).
|
|
1079
|
+
const prev = __rozieCtxHost._colReg.value ? (__rozieCtxHost._colReg.value as Record<string, any>)[key] : undefined;
|
|
1080
|
+
if (prev !== undefined && columnSpecsEquivalent(prev, spec)) return;
|
|
928
1081
|
__rozieCtxHost._colReg.value = {
|
|
929
1082
|
...__rozieCtxHost._colReg.value,
|
|
930
1083
|
[key]: spec
|
|
@@ -945,6 +1098,19 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
945
1098
|
// a one-way `:data`) has a base array to whole-array-replace. currentData() then sources
|
|
946
1099
|
// the bound prop when controlled, this fallback otherwise.
|
|
947
1100
|
this._dataDefault.value = this.data || [];
|
|
1101
|
+
// Apply any per-column `pinned` declaration as the INITIAL pinning state BEFORE createTable
|
|
1102
|
+
// reads currentState() below, so the first row model table-core builds is already correctly
|
|
1103
|
+
// ordered (getVisibleCells() returns [left-pinned, center, right-pinned]) rather than
|
|
1104
|
+
// re-ordering a render later. Covers the `:columns` config-array form, which is available
|
|
1105
|
+
// now; `<Column>` children register during mount and are covered by the re-feed watch's own
|
|
1106
|
+
// call. One-shot and self-latching — see seedColumnPinning() in stateAssembly.rzts.
|
|
1107
|
+
// Apply any per-column `pinned` declaration as the INITIAL pinning state BEFORE createTable
|
|
1108
|
+
// reads currentState() below, so the first row model table-core builds is already correctly
|
|
1109
|
+
// ordered (getVisibleCells() returns [left-pinned, center, right-pinned]) rather than
|
|
1110
|
+
// re-ordering a render later. Covers the `:columns` config-array form, which is available
|
|
1111
|
+
// now; `<Column>` children register during mount and are covered by the re-feed watch's own
|
|
1112
|
+
// call. One-shot and self-latching — see seedColumnPinning() in stateAssembly.rzts.
|
|
1113
|
+
this.seedColumnPinning();
|
|
948
1114
|
// Build the table instance HERE so the closures below capture the live `table`.
|
|
949
1115
|
// Build the table instance HERE so the closures below capture the live `table`.
|
|
950
1116
|
this.table = createTable({
|
|
@@ -956,7 +1122,7 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
956
1122
|
// the getter bought nothing. Snapshot the initial data here; setOptions owns updates.
|
|
957
1123
|
// currentData() = the bound prop when controlled, else the uncontrolled $data.dataDefault
|
|
958
1124
|
// (Phase 51 req-4 — so a committed edit's writeData re-feed is observed either way).
|
|
959
|
-
data: this.
|
|
1125
|
+
data: this.feedData(),
|
|
960
1126
|
columns: this.tableColumns(),
|
|
961
1127
|
state: this.currentState(),
|
|
962
1128
|
getCoreRowModel: getCoreRowModel(),
|
|
@@ -971,7 +1137,10 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
971
1137
|
// default `!!subRows.length` rule applies (only parents with children expand).
|
|
972
1138
|
getExpandedRowModel: getExpandedRowModel(),
|
|
973
1139
|
getSubRows: (this.getSubRows || undefined) as any,
|
|
974
|
-
|
|
1140
|
+
// Row identity (quick 260925-rew) — see rowIdOption(). Re-passed on re-feed like the
|
|
1141
|
+
// other row-model fns.
|
|
1142
|
+
getRowId: this.rowIdOption(),
|
|
1143
|
+
getRowCanExpand: this.expandable === true && this.getSubRows == null ? (row: any) => !this.rowIsLazyPlaceholder(row) : undefined,
|
|
975
1144
|
onExpandedChange: this.onExpandedChangeCb,
|
|
976
1145
|
// Grouping auto-expand (phase 50 req-4): table-core's autoResetExpanded defaults TRUE, so a
|
|
977
1146
|
// POST-MOUNT setGrouping (the consumer #groupBar / applyGrouping verb) auto-fires
|
|
@@ -1016,7 +1185,7 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
1016
1185
|
// Row selection (req-7): enabled unless 'none'; 'single' caps at ≤1
|
|
1017
1186
|
// (enableMultiRowSelection:false). Select-all scope = filtered rows (TanStack
|
|
1018
1187
|
// default, D-06 — NOT overridden).
|
|
1019
|
-
enableRowSelection: this.
|
|
1188
|
+
enableRowSelection: this.rowSelectionOption(),
|
|
1020
1189
|
enableMultiRowSelection: this.selectionMode === 'multiple',
|
|
1021
1190
|
// PER-SLICE callbacks (Open-Q1: each maps 1:1 to a slice's r-model + change event,
|
|
1022
1191
|
// no global onStateChange diff) — hoisted top-level consts, re-passed by the re-feed
|
|
@@ -1060,6 +1229,10 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
1060
1229
|
// NEVER in a render helper (Pitfall 1). Pass the COMPLETE options set (virtual-core's
|
|
1061
1230
|
// setOptions replaces, not merges). Guarded so the off path executes no virtual-core code.
|
|
1062
1231
|
if (this.rowsWindowed() && this.virtualizer) {
|
|
1232
|
+
// Rekey any placeholder rows this re-feed just filled (quick 260926, VR probe) BEFORE
|
|
1233
|
+
// handing the virtualizer its fresh options — see migratePlaceholderSizeCache()'s own
|
|
1234
|
+
// comment (virtualization.rzts) for the itemSizeCache/getMeasurements memo root cause.
|
|
1235
|
+
this.migratePlaceholderSizeCache();
|
|
1063
1236
|
this.virtualizer.setOptions(this.virtualizerOptions());
|
|
1064
1237
|
this.virtualizer._willUpdate();
|
|
1065
1238
|
}
|
|
@@ -1135,6 +1308,24 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
1135
1308
|
// cell selectors hang off (the exact idiom proven ×6 by plan 01's probe). Captured here
|
|
1136
1309
|
// (post-mount) so it is non-null and ROZ123-clean.
|
|
1137
1310
|
this.gridRoot = this._ref__rozieRoot ? this._ref__rozieRoot.querySelector('.rozie-data-table') : null;
|
|
1311
|
+
// B-02: the click-away detector. `composedPath()` rather than `ev.target` because on Lit the
|
|
1312
|
+
// grid lives in a shadow root and a document-level listener sees the retargeted HOST element,
|
|
1313
|
+
// which is never `gridRoot.contains(...)` — every in-grid click would read as "outside".
|
|
1314
|
+
// B-02: the click-away detector. `composedPath()` rather than `ev.target` because on Lit the
|
|
1315
|
+
// grid lives in a shadow root and a document-level listener sees the retargeted HOST element,
|
|
1316
|
+
// which is never `gridRoot.contains(...)` — every in-grid click would read as "outside".
|
|
1317
|
+
if (typeof document !== 'undefined') {
|
|
1318
|
+
this.docPointerDown = (ev: any) => {
|
|
1319
|
+
if (!this.gridRoot) {
|
|
1320
|
+
this.outsidePointerDown = false;
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
const path = ev && typeof ev.composedPath === 'function' ? ev.composedPath() : null;
|
|
1324
|
+
const inside = path ? path.indexOf(this.gridRoot) !== -1 : !!(ev && ev.target && this.gridRoot.contains && this.gridRoot.contains(ev.target));
|
|
1325
|
+
this.outsidePointerDown = !inside;
|
|
1326
|
+
};
|
|
1327
|
+
document.addEventListener('pointerdown', this.docPointerDown, true);
|
|
1328
|
+
}
|
|
1138
1329
|
// WR-04: NO on-mount auto-focus of the entry cell. Auto-focusing here stole focus on
|
|
1139
1330
|
// page load AND was non-deterministic on React/Solid (the entry cell may not be
|
|
1140
1331
|
// committed to the DOM yet at the $onMount microtask). The roving tabindex="0" entry
|
|
@@ -1267,6 +1458,31 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
1267
1458
|
if (!w) {
|
|
1268
1459
|
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');
|
|
1269
1460
|
}
|
|
1461
|
+
// COLUMN-AXIS FIRST-WINDOW BUMP (quick 260908-vcy). The mount-kick above fires right
|
|
1462
|
+
// after `new Virtualizer(...)` — BEFORE colVirtualizer has measured, and before the
|
|
1463
|
+
// spacer effects even exist. Any fine-grained subscriber created inside that measure
|
|
1464
|
+
// gap takes colPadLeft()/colPadRight()'s `if (!items.length) return 0` early return,
|
|
1465
|
+
// which reads ONLY windowVer + editVer and never reaches the live path's reads
|
|
1466
|
+
// (forcedColumns()/columnSize()/column state). Its dependency set is therefore those
|
|
1467
|
+
// two counters alone — and without this bump nothing ever touches them again, so the
|
|
1468
|
+
// subscriber stays frozen at 0 for the lifetime of the component. Measured on Svelte:
|
|
1469
|
+
// the FIRST header row's trailing spacer froze at 0px while its siblings (created
|
|
1470
|
+
// after measurement, on the live path) tracked correctly; under table-layout:fixed the
|
|
1471
|
+
// first row alone establishes column widths, so that one cell collapsed the whole
|
|
1472
|
+
// table's scrollWidth from 9000 to 1350. The five other targets re-render wholesale
|
|
1473
|
+
// (Solid re-creates these rows), which masked it.
|
|
1474
|
+
//
|
|
1475
|
+
// The ROW axis has always been safe here only by accident: remeasureWindow() above
|
|
1476
|
+
// already routes through bumpWindowVer() on this same post-first-frame callback. This
|
|
1477
|
+
// is the column axis's missing equivalent — not a Svelte workaround (the emitted
|
|
1478
|
+
// Svelte binding is a correct reactive template_effect; the dependency set is what was
|
|
1479
|
+
// wrong), so it is fixed once, in the shared engine's consumer, for all six targets.
|
|
1480
|
+
//
|
|
1481
|
+
// Routed through bumpWindowVer() rather than a raw `$data.windowVer = $data.windowVer + 1`
|
|
1482
|
+
// to keep 87-10's microtask coalescing AND to avoid manufacturing the ROZ138
|
|
1483
|
+
// false positive that a second in-body counter self-read would trip (see the
|
|
1484
|
+
// mount-kick's own comment above).
|
|
1485
|
+
this.bumpWindowVer();
|
|
1270
1486
|
}
|
|
1271
1487
|
};
|
|
1272
1488
|
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(() => requestAnimationFrame(afterFirstFrame));else setTimeout(afterFirstFrame, 0);
|
|
@@ -1285,14 +1501,34 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
1285
1501
|
|
|
1286
1502
|
updated(changedProperties: Map<string, unknown>): void {
|
|
1287
1503
|
this.maybeClearHistoryOnExternalSwap();
|
|
1504
|
+
// visible-range-change backstop (quick 260925-dtl part 2): a re-render from new data can move
|
|
1505
|
+
// the rendered window WITHOUT a window-version bump (measured on React: filling placeholders
|
|
1506
|
+
// shifted the window by two rows and nothing was reported). Deduped, so a render that leaves
|
|
1507
|
+
// the window where it was emits nothing.
|
|
1508
|
+
// visible-range-change backstop (quick 260925-dtl part 2): a re-render from new data can move
|
|
1509
|
+
// the rendered window WITHOUT a window-version bump (measured on React: filling placeholders
|
|
1510
|
+
// shifted the window by two rows and nothing was reported). Deduped, so a render that leaves
|
|
1511
|
+
// the window where it was emits nothing.
|
|
1512
|
+
this.emitVisibleRangeIfChanged();
|
|
1513
|
+
// A-02: the post-render hook is the one place that observes EVERY column-width change on all
|
|
1514
|
+
// six targets — an interactive resize writes `columnSizing` through table-core under
|
|
1515
|
+
// `columnResizeMode: 'onChange'` and never reaches the coarse re-feed watch (which keys on
|
|
1516
|
+
// `$props.columnSizing`, undefined on the uncontrolled path). Self-guarded on the width
|
|
1517
|
+
// signature, so a tick that changed no width costs one loop over the leaf columns.
|
|
1518
|
+
// A-02: the post-render hook is the one place that observes EVERY column-width change on all
|
|
1519
|
+
// six targets — an interactive resize writes `columnSizing` through table-core under
|
|
1520
|
+
// `columnResizeMode: 'onChange'` and never reaches the coarse re-feed watch (which keys on
|
|
1521
|
+
// `$props.columnSizing`, undefined on the uncontrolled path). Self-guarded on the width
|
|
1522
|
+
// signature, so a tick that changed no width costs one loop over the leaf columns.
|
|
1523
|
+
this.remeasureColumnSizes();
|
|
1288
1524
|
if (!this.table) return;
|
|
1289
1525
|
// Phase 51 req-4: track currentData() (the bound prop OR the uncontrolled
|
|
1290
|
-
// $data.dataDefault) so a committed edit re-feeds on Lit whether or not
|
|
1291
|
-
// bound. Compare by reference AND length so a same-length single-cell edit (fresh array,
|
|
1526
|
+
// $data.dataDefault) so a committed edit re-feeds on Lit whether or not the two-way
|
|
1527
|
+
// `data` model is bound. Compare by reference AND length so a same-length single-cell edit (fresh array,
|
|
1292
1528
|
// identical length) still re-feeds.
|
|
1293
1529
|
// Phase 51 req-4: track currentData() (the bound prop OR the uncontrolled
|
|
1294
|
-
// $data.dataDefault) so a committed edit re-feeds on Lit whether or not
|
|
1295
|
-
// bound. Compare by reference AND length so a same-length single-cell edit (fresh array,
|
|
1530
|
+
// $data.dataDefault) so a committed edit re-feeds on Lit whether or not the two-way
|
|
1531
|
+
// `data` model is bound. Compare by reference AND length so a same-length single-cell edit (fresh array,
|
|
1296
1532
|
// identical length) still re-feeds.
|
|
1297
1533
|
const d = this.currentData() || [];
|
|
1298
1534
|
if (d === this.lastData && d.length === this.lastDataLen) return;
|
|
@@ -1307,6 +1543,12 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
1307
1543
|
if (this.isConnected || this._rozieTornDown) return;
|
|
1308
1544
|
this._rozieTornDown = true;
|
|
1309
1545
|
() => {
|
|
1546
|
+
// B-02: drop the document click-away listener (it is attached for the instance's lifetime,
|
|
1547
|
+
// not per edit session — attaching it per session would freeze a stale closure on React).
|
|
1548
|
+
if (this.docPointerDown && typeof document !== 'undefined') {
|
|
1549
|
+
document.removeEventListener('pointerdown', this.docPointerDown, true);
|
|
1550
|
+
this.docPointerDown = null;
|
|
1551
|
+
}
|
|
1310
1552
|
if (this.virtualizerCleanup) this.virtualizerCleanup();
|
|
1311
1553
|
// Phase 87 87-04: tear down the column virtualizer's scroll-element ResizeObserver too.
|
|
1312
1554
|
// No-op when column windowing was off (cleanup stays null).
|
|
@@ -1352,7 +1594,7 @@ private __rozieCtxProvider_data_table_columns = new ContextProvider(this, { cont
|
|
|
1352
1594
|
|
|
1353
1595
|
<div class="rdt-column-defs" style="display:none" aria-hidden="true" data-rozie-s-d5dcab4c><slot></slot></div>
|
|
1354
1596
|
|
|
1355
|
-
${!!this._invalidMsg.value ? html`<div class="rdt-sr-live" role="status" aria-live="polite" aria-atomic="true" data-rozie-s-d5dcab4c>${this._invalidMsg.value}</div>` : nothing}${!!this._pasteAnnounce.value ? html`<div class="rdt-sr-live rdt-sr-paste" data-testid="paste-announce" role="status" aria-live="polite" aria-atomic="true" data-rozie-s-d5dcab4c>${this._pasteAnnounce.value}</div>` : nothing}${!!this._liveAnnounce.value ? html`<div class="rdt-sr-live rdt-sr-sortfilter" data-testid="sortfilter-announce" role="status" aria-live="polite" aria-atomic="true" data-rozie-s-d5dcab4c>${this._liveAnnounce.value}</div>` : nothing}<div class="rdt-toolbar" data-rozie-s-d5dcab4c>
|
|
1597
|
+
${!!this._invalidMsg.value ? html`<div class="rdt-sr-live" role="status" aria-live="polite" aria-atomic="true" data-rozie-s-d5dcab4c>${this._invalidMsg.value}</div>` : nothing}${!!this._pasteAnnounce.value ? html`<div class="rdt-sr-live rdt-sr-paste" data-testid="paste-announce" role="status" aria-live="polite" aria-atomic="true" data-rozie-s-d5dcab4c>${this._pasteAnnounce.value}</div>` : nothing}${!!this._rangeAnnounce.value ? html`<div class="rdt-sr-live rdt-sr-range" data-testid="range-announce" role="status" aria-live="polite" aria-atomic="true" data-rozie-s-d5dcab4c>${this._rangeAnnounce.value}</div>` : nothing}${!!this._liveAnnounce.value ? html`<div class="rdt-sr-live rdt-sr-sortfilter" data-testid="sortfilter-announce" role="status" aria-live="polite" aria-atomic="true" data-rozie-s-d5dcab4c>${this._liveAnnounce.value}</div>` : nothing}<div class="rdt-toolbar" data-rozie-s-d5dcab4c>
|
|
1356
1598
|
<input class="rdt-global-filter" type="text" role="searchbox" aria-label="Search table" .value=${this.globalFilterValue()} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onGlobalFilterInput($event); }} data-rozie-s-d5dcab4c />
|
|
1357
1599
|
|
|
1358
1600
|
${this.allLeafColumns().length ? html`<details class="rdt-colvis" data-rozie-s-d5dcab4c>
|
|
@@ -1371,11 +1613,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1371
1613
|
${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>`)}
|
|
1372
1614
|
</slot>`}
|
|
1373
1615
|
</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>
|
|
1374
|
-
<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>
|
|
1616
|
+
<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())} aria-colcount=${rozieAttr(this.gridAriaColCount())} @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>
|
|
1375
1617
|
<thead class="rdt-thead" role="rowgroup" data-rozie-s-d5dcab4c>
|
|
1376
1618
|
${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>
|
|
1377
1619
|
|
|
1378
|
-
${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))} data-rozie-s-d5dcab4c>
|
|
1620
|
+
${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-pinned': this.columnPinSide(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))} aria-colindex=${rozieAttr(this.headerLeafStart(hg, wh.header) + 1)} 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, wh.width))} data-rozie-s-d5dcab4c>
|
|
1379
1621
|
${this.isSelectColumn(wh.header.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
1380
1622
|
${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)}>
|
|
1381
1623
|
${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>`}
|
|
@@ -1406,11 +1648,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1406
1648
|
${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>`)}
|
|
1407
1649
|
|
|
1408
1650
|
${this.hasAnyFilterableColumn() ? html`<tr class="rdt-filter-row" data-rozie-s-d5dcab4c>
|
|
1409
|
-
${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>
|
|
1651
|
+
${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="${Object.entries({ "rdt-filter-cell": true, 'rdt-pinned': this.columnPinSide(wh.header.column.id) !== '' }).filter(([, v]) => v).map(([k]) => k).join(' ')}" role="presentation" data-col=${rozieAttr(wh.header.column.id)} style=${rozieStyle(this.pinStyle(wh.header.column.id))} data-rozie-s-d5dcab4c>
|
|
1410
1652
|
${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>
|
|
1411
1653
|
${this.columnIsFilterable(wh.header.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
1412
|
-
${this.rozieSlots?.[`filter-${wh.header.column.id}`] !== undefined ? this.rozieSlots?.[`filter-${wh.header.column.id}`]!({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-${wh.header.column.id}`}" 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-default-set-filter=${($event: CustomEvent) => ((this.setColumnFilter) as (...args: any[]) => any)($event.detail)}>
|
|
1413
|
-
${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)}>
|
|
1654
|
+
${this.rozieSlots?.[`filter-${wh.header.column.id}`] !== undefined ? this.rozieSlots?.[`filter-${wh.header.column.id}`]!({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), columnLabel: this.headerLabel(wh.header.column.id), setFilter: this.setColumnFilter}) : html`<slot name="${`filter-${wh.header.column.id}`}" 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), columnLabel: this.headerLabel(wh.header.column.id)}); } catch { return '{}'; } })()} @rozie-default-set-filter=${($event: CustomEvent) => ((this.setColumnFilter) as (...args: any[]) => any)($event.detail)}>
|
|
1655
|
+
${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), columnLabel: this.headerLabel(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), columnLabel: this.headerLabel(wh.header.column.id)}); } catch { return '{}'; } })()} @rozie-filter-set-filter=${($event: CustomEvent) => ((this.setColumnFilter) as (...args: any[]) => any)($event.detail)}>
|
|
1414
1656
|
<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 />
|
|
1415
1657
|
</slot>`}
|
|
1416
1658
|
</slot>`}
|
|
@@ -1424,11 +1666,19 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1424
1666
|
</tr>
|
|
1425
1667
|
|
|
1426
1668
|
${repeat<any>(this.windowedRows(), (wr, _idx) => wr.row.id, (wr, _idx) => html`
|
|
1427
|
-
<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>
|
|
1669
|
+
<tr class="${Object.entries({ "rdt-tr": true, 'rdt-group-header': this.rowIsGrouped(wr.row), 'rdt-row-pinned': wr.pinned, 'rdt-placeholder-row': this.rowIsLazyPlaceholder(wr.row) }).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)} aria-busy=${rozieAttr(this.rowIsLazyPlaceholder(wr.row) ? 'true' : null)} data-rozie-s-d5dcab4c>
|
|
1428
1670
|
|
|
1429
|
-
${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>
|
|
1671
|
+
${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-pinned': this.columnPinSide(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)))} aria-colindex=${rozieAttr(this.colIndexOf(wr.row, cell) + 1)} aria-selected=${rozieAttr(this.inRange(wr.vi.index, this.colIndexOf(wr.row, cell)) ? 'true' : null)} 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>
|
|
1430
1672
|
|
|
1431
|
-
|
|
1673
|
+
|
|
1674
|
+
${this.rowIsLazyPlaceholder(wr.row) && this.isSelectColumn(cell.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
1675
|
+
|
|
1676
|
+
<input class="rdt-select-placeholder" type="checkbox" disabled="" tabindex="-1" aria-hidden="true" style="visibility:hidden" data-rozie-s-d5dcab4c />
|
|
1677
|
+
</span>` : this.rowIsLazyPlaceholder(wr.row) && this.isExpanderColumn(cell.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c></span>` : this.rowIsLazyPlaceholder(wr.row) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
1678
|
+
${this.placeholder !== undefined ? this.placeholder({index: wr.vi.index, columnId: cell.column.id}) : html`<slot name="placeholder" data-rozie-params=${(() => { try { return JSON.stringify({index: wr.vi.index, columnId: cell.column.id}); } catch { return '{}'; } })()}>
|
|
1679
|
+
<span class="rdt-skeleton" aria-hidden="true" data-rozie-s-d5dcab4c>​</span>
|
|
1680
|
+
</slot>`}
|
|
1681
|
+
</span>` : this.isExpanderColumn(cell.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
1432
1682
|
${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>
|
|
1433
1683
|
${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)}>
|
|
1434
1684
|
<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 />
|
|
@@ -1442,15 +1692,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1442
1692
|
</span>
|
|
1443
1693
|
<span class="rdt-group-count" data-rozie-s-d5dcab4c>${rozieDisplay('(' + this.groupSubRowCount(wr.row) + ')')}</span>
|
|
1444
1694
|
</span>` : this.isEditing(wr.vi.index, this.colIndexOf(wr.row, cell)) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
1445
|
-
${this.editorTypeOf(cell.column.id) === 'number' ? html`<input class="rdt-cell-editor" type="number" data-editing-cell="" data-builtin-editor="" .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="" data-builtin-editor="" .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>
|
|
1446
|
-
${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>`)}
|
|
1447
|
-
</select>` : this.editorTypeOf(cell.column.id) === 'checkbox' ? html`<input class="rdt-cell-editor" type="checkbox" data-editing-cell="" data-builtin-editor="" ?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 />` : this.editorTypeOf(cell.column.id) === 'custom' ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
1448
|
-
${this.rozieSlots?.[`editor-${cell.column.id}`] !== undefined ? this.rozieSlots?.[`editor-${cell.column.id}`]!({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-${cell.column.id}`}" 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 '{}'; } })()}>
|
|
1449
|
-
${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 '{}'; } })()}>
|
|
1450
|
-
<input class="rdt-cell-editor" type="text" data-editing-cell="" data-builtin-editor="" .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 />
|
|
1695
|
+
${this.editorTypeOf(cell.column.id) === 'number' ? html`<input class="rdt-cell-editor" type="number" data-editing-cell="" data-builtin-editor="" aria-invalid=${rozieAttr(this._invalidMsg.value ? 'true' : null)} .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="" data-builtin-editor="" aria-invalid=${rozieAttr(this._invalidMsg.value ? 'true' : null)} .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>
|
|
1696
|
+
${repeat<any>(this.editorOptionsOf(cell.column.id), (opt, _idx) => opt.value, (opt, _idx) => html`<option value=${rozieAttr(opt.value)} ?selected=${opt.value === this.editorValueFor(cell.column.id)} data-rozie-s-d5dcab4c>${rozieDisplay(opt.label)}</option>`)}
|
|
1697
|
+
</select>` : this.editorTypeOf(cell.column.id) === 'checkbox' ? html`<input class="rdt-cell-editor" type="checkbox" data-editing-cell="" data-builtin-editor="" aria-invalid=${rozieAttr(this._invalidMsg.value ? 'true' : null)} ?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 />` : this.editorTypeOf(cell.column.id) === 'custom' ? html`<span style="display:contents" @keydown=${($event: KeyboardEvent & { currentTarget: HTMLSpanElement; target: HTMLSpanElement }) => { this.onEditorDropinKeyDown($event); }} data-rozie-s-d5dcab4c>
|
|
1698
|
+
${this.rozieSlots?.[`editor-${cell.column.id}`] !== undefined ? this.rozieSlots?.[`editor-${cell.column.id}`]!({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(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index)}) : html`<slot name="${`editor-${cell.column.id}`}" 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(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index)}); } catch { return '{}'; } })()}>
|
|
1699
|
+
${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(), columnLabel: this.headerLabel(cell.column.id), 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(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index)}); } catch { return '{}'; } })()}>
|
|
1700
|
+
<input class="rdt-cell-editor" type="text" data-editing-cell="" data-builtin-editor="" aria-invalid=${rozieAttr(this._invalidMsg.value ? 'true' : null)} .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 />
|
|
1451
1701
|
</slot>`}
|
|
1452
1702
|
</slot>`}
|
|
1453
|
-
</span>` : html`<input class="rdt-cell-editor" type="text" data-editing-cell="" data-builtin-editor="" .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>
|
|
1703
|
+
</span>` : html`<input class="rdt-cell-editor" type="text" data-editing-cell="" data-builtin-editor="" aria-invalid=${rozieAttr(this._invalidMsg.value ? 'true' : null)} .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>
|
|
1454
1704
|
${this.rozieSlots?.[`cell-${cell.column.id}`] !== undefined ? this.rozieSlots?.[`cell-${cell.column.id}`]!({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: cell.getValue()}) : html`<slot name="${`cell-${cell.column.id}`}" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: cell.getValue()}); } catch { return '{}'; } })()}>
|
|
1455
1705
|
${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>`}
|
|
1456
1706
|
</slot>`}
|
|
@@ -1469,10 +1719,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1469
1719
|
</tr>
|
|
1470
1720
|
</tbody>
|
|
1471
1721
|
</table>
|
|
1472
|
-
</div>` : html`<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>
|
|
1722
|
+
</div>` : html`<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())} aria-colcount=${rozieAttr(this.gridAriaColCount())} @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>
|
|
1473
1723
|
<thead class="rdt-thead" role="rowgroup" data-rozie-s-d5dcab4c>
|
|
1474
1724
|
${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>
|
|
1475
|
-
${repeat<any>(hg.headers, (header, _idx) => header.id, (header, _idx) => html`<th class="${Object.entries({ "rdt-th": true, 'rdt-select-th': this.isSelectColumn(header.column.id), 'rdt-expander-th': this.isExpanderColumn(header.column.id), 'rdt-th-resizing': this.columnIsResizing(header.column.id), 'rdt-cell-active': this.isActiveCell('__header', this.headerColIndexOf(hg, header), hgLevel) }).filter(([, v]) => v).map(([k]) => k).join(' ')}" role="columnheader" data-col=${rozieAttr(header.column.id)} data-grid-cell="" data-row="__header" data-header-level=${rozieAttr(hgLevel)} colspan=${rozieAttr(header.colSpan > 1 ? header.colSpan : null)} data-col-index=${rozieAttr(this.headerColIndexOf(hg, header))} tabindex=${rozieAttr(this.cellTabindex('__header', this.headerColIndexOf(hg, header), hgLevel))} aria-sort=${rozieAttr(this.ariaSortFor(header.column.id))} style=${rozieStyle(this.thStyle(header))} data-rozie-s-d5dcab4c>
|
|
1725
|
+
${repeat<any>(hg.headers, (header, _idx) => header.id, (header, _idx) => html`<th class="${Object.entries({ "rdt-th": true, 'rdt-select-th': this.isSelectColumn(header.column.id), 'rdt-expander-th': this.isExpanderColumn(header.column.id), 'rdt-pinned': this.columnPinSide(header.column.id) !== '', 'rdt-th-resizing': this.columnIsResizing(header.column.id), 'rdt-cell-active': this.isActiveCell('__header', this.headerColIndexOf(hg, header), hgLevel) }).filter(([, v]) => v).map(([k]) => k).join(' ')}" role="columnheader" data-col=${rozieAttr(header.column.id)} data-grid-cell="" data-row="__header" data-header-level=${rozieAttr(hgLevel)} colspan=${rozieAttr(header.colSpan > 1 ? header.colSpan : null)} data-col-index=${rozieAttr(this.headerColIndexOf(hg, header))} aria-colindex=${rozieAttr(this.headerLeafStart(hg, header) + 1)} tabindex=${rozieAttr(this.cellTabindex('__header', this.headerColIndexOf(hg, header), hgLevel))} aria-sort=${rozieAttr(this.ariaSortFor(header.column.id))} style=${rozieStyle(this.thStyle(header))} data-rozie-s-d5dcab4c>
|
|
1476
1726
|
|
|
1477
1727
|
|
|
1478
1728
|
${this.isSelectColumn(header.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
@@ -1508,11 +1758,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1508
1758
|
</tr>`)}
|
|
1509
1759
|
|
|
1510
1760
|
${this.hasAnyFilterableColumn() ? html`<tr class="rdt-filter-row" data-rozie-s-d5dcab4c>
|
|
1511
|
-
${repeat<any>(this._headerGroups.value[this._headerGroups.value.length - 1].headers, (header, _idx) => header.id, (header, _idx) => html`<th class="rdt-filter-cell" role="presentation" style=${rozieStyle(this.pinStyle(header.column.id))} data-rozie-s-d5dcab4c>
|
|
1761
|
+
${repeat<any>(this._headerGroups.value[this._headerGroups.value.length - 1].headers, (header, _idx) => header.id, (header, _idx) => html`<th class="${Object.entries({ "rdt-filter-cell": true, 'rdt-pinned': this.columnPinSide(header.column.id) !== '' }).filter(([, v]) => v).map(([k]) => k).join(' ')}" role="presentation" style=${rozieStyle(this.pinStyle(header.column.id))} data-rozie-s-d5dcab4c>
|
|
1512
1762
|
${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>
|
|
1513
1763
|
${this.columnIsFilterable(header.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
1514
|
-
${this.rozieSlots?.[`filter-${header.column.id}`] !== undefined ? this.rozieSlots?.[`filter-${header.column.id}`]!({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-${header.column.id}`}" 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-default-set-filter=${($event: CustomEvent) => ((this.setColumnFilter) as (...args: any[]) => any)($event.detail)}>
|
|
1515
|
-
${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)}>
|
|
1764
|
+
${this.rozieSlots?.[`filter-${header.column.id}`] !== undefined ? this.rozieSlots?.[`filter-${header.column.id}`]!({columnId: header.column.id, value: this.columnFilterValue(header.column.id), uniqueValues: this.getFacetedUniqueValues(header.column.id), minMax: this.getFacetedMinMaxValues(header.column.id), columnLabel: this.headerLabel(header.column.id), setFilter: this.setColumnFilter}) : html`<slot name="${`filter-${header.column.id}`}" 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), columnLabel: this.headerLabel(header.column.id)}); } catch { return '{}'; } })()} @rozie-default-set-filter=${($event: CustomEvent) => ((this.setColumnFilter) as (...args: any[]) => any)($event.detail)}>
|
|
1765
|
+
${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), columnLabel: this.headerLabel(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), columnLabel: this.headerLabel(header.column.id)}); } catch { return '{}'; } })()} @rozie-filter-set-filter=${($event: CustomEvent) => ((this.setColumnFilter) as (...args: any[]) => any)($event.detail)}>
|
|
1516
1766
|
<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 />
|
|
1517
1767
|
</slot>`}
|
|
1518
1768
|
</slot>`}
|
|
@@ -1523,7 +1773,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1523
1773
|
|
|
1524
1774
|
${repeat<any>(this._rows.value, (row, _idx) => row.id, (row, _idx) => html`
|
|
1525
1775
|
<tr class="${Object.entries({ "rdt-tr": true, 'rdt-group-header': this.rowIsGrouped(row) }).filter(([, v]) => v).map(([k]) => k).join(' ')}" role="row" data-depth=${rozieAttr(row.depth)} aria-rowindex=${rozieAttr(this.bodyAriaRowIndex(row))} data-group-header=${rozieAttr(this.rowIsGrouped(row) ? row.id : null)} data-group-leaf=${rozieAttr(this.groupingActive() && !this.rowIsGrouped(row) ? row.id : null)} aria-expanded=${rozieAttr(this.rowIsGrouped(row) ? !!this.rowIsExpanded(row) : null)} aria-selected=${rozieAttr(this.selectionMode !== 'none' ? !!this.rowIsSelected(row) : null)} aria-level=${rozieAttr(this.groupingActive() ? row.depth + 1 : null)} data-rozie-s-d5dcab4c>
|
|
1526
|
-
${repeat<any>(this.visibleCellsFor(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(this.rowIndexOf(row), this.colIndexOf(row, cell)), 'rdt-cell-active': this.isActiveCell(String(this.rowIndexOf(row)), this.colIndexOf(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(this.rowIndexOf(row))} data-col-index=${rozieAttr(this.colIndexOf(row, cell))} tabindex=${rozieAttr(this.cellTabindex(String(this.rowIndexOf(row)), this.colIndexOf(row, cell)))} style=${rozieStyle(this.bodyCellStyle(row, cell.column.id))} aria-invalid=${rozieAttr(this.cellAriaInvalid(this.rowIndexOf(row), this.colIndexOf(row, cell)))} data-in-range=${rozieAttr(this.inRange(this.rowIndexOf(row), this.colIndexOf(row, cell)) ? 'true' : null)} data-agg-cell=${rozieAttr(this.cellIsAggregated(cell) ? cell.column.id : null)} data-rozie-s-d5dcab4c>
|
|
1776
|
+
${repeat<any>(this.visibleCellsFor(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-pinned': this.columnPinSide(cell.column.id) !== '', 'rdt-in-range': this.inRange(this.rowIndexOf(row), this.colIndexOf(row, cell)), 'rdt-cell-active': this.isActiveCell(String(this.rowIndexOf(row)), this.colIndexOf(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(this.rowIndexOf(row))} data-col-index=${rozieAttr(this.colIndexOf(row, cell))} tabindex=${rozieAttr(this.cellTabindex(String(this.rowIndexOf(row)), this.colIndexOf(row, cell)))} style=${rozieStyle(this.bodyCellStyle(row, cell.column.id))} aria-invalid=${rozieAttr(this.cellAriaInvalid(this.rowIndexOf(row), this.colIndexOf(row, cell)))} aria-colindex=${rozieAttr(this.colIndexOf(row, cell) + 1)} aria-selected=${rozieAttr(this.inRange(this.rowIndexOf(row), this.colIndexOf(row, cell)) ? 'true' : null)} data-in-range=${rozieAttr(this.inRange(this.rowIndexOf(row), this.colIndexOf(row, cell)) ? 'true' : null)} data-agg-cell=${rozieAttr(this.cellIsAggregated(cell) ? cell.column.id : null)} data-rozie-s-d5dcab4c>
|
|
1527
1777
|
|
|
1528
1778
|
${this.isExpanderColumn(cell.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
1529
1779
|
${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>
|
|
@@ -1539,15 +1789,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1539
1789
|
</span>
|
|
1540
1790
|
<span class="rdt-group-count" data-rozie-s-d5dcab4c>${rozieDisplay('(' + this.groupSubRowCount(row) + ')')}</span>
|
|
1541
1791
|
</span>` : this.isEditing(this.rowIndexOf(row), this.colIndexOf(row, cell)) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
1542
|
-
${this.editorTypeOf(cell.column.id) === 'number' ? html`<input class="rdt-cell-editor" type="number" data-editing-cell="" data-builtin-editor="" .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="" data-builtin-editor="" .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>
|
|
1543
|
-
${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>`)}
|
|
1544
|
-
</select>` : this.editorTypeOf(cell.column.id) === 'checkbox' ? html`<input class="rdt-cell-editor" type="checkbox" data-editing-cell="" data-builtin-editor="" ?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 />` : this.editorTypeOf(cell.column.id) === 'custom' ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
|
|
1545
|
-
${this.rozieSlots?.[`editor-${cell.column.id}`] !== undefined ? this.rozieSlots?.[`editor-${cell.column.id}`]!({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-${cell.column.id}`}" 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 '{}'; } })()}>
|
|
1546
|
-
${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 '{}'; } })()}>
|
|
1547
|
-
<input class="rdt-cell-editor" type="text" data-editing-cell="" data-builtin-editor="" .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 />
|
|
1792
|
+
${this.editorTypeOf(cell.column.id) === 'number' ? html`<input class="rdt-cell-editor" type="number" data-editing-cell="" data-builtin-editor="" aria-invalid=${rozieAttr(this._invalidMsg.value ? 'true' : null)} .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="" data-builtin-editor="" aria-invalid=${rozieAttr(this._invalidMsg.value ? 'true' : null)} .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>
|
|
1793
|
+
${repeat<any>(this.editorOptionsOf(cell.column.id), (opt, _idx) => opt.value, (opt, _idx) => html`<option value=${rozieAttr(opt.value)} ?selected=${opt.value === this.editorValueFor(cell.column.id)} data-rozie-s-d5dcab4c>${rozieDisplay(opt.label)}</option>`)}
|
|
1794
|
+
</select>` : this.editorTypeOf(cell.column.id) === 'checkbox' ? html`<input class="rdt-cell-editor" type="checkbox" data-editing-cell="" data-builtin-editor="" aria-invalid=${rozieAttr(this._invalidMsg.value ? 'true' : null)} ?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 />` : this.editorTypeOf(cell.column.id) === 'custom' ? html`<span style="display:contents" @keydown=${($event: KeyboardEvent & { currentTarget: HTMLSpanElement; target: HTMLSpanElement }) => { this.onEditorDropinKeyDown($event); }} data-rozie-s-d5dcab4c>
|
|
1795
|
+
${this.rozieSlots?.[`editor-${cell.column.id}`] !== undefined ? this.rozieSlots?.[`editor-${cell.column.id}`]!({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(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row))}) : html`<slot name="${`editor-${cell.column.id}`}" 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(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row))}); } catch { return '{}'; } })()}>
|
|
1796
|
+
${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(), columnLabel: this.headerLabel(cell.column.id), 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(), columnLabel: this.headerLabel(cell.column.id), autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row))}); } catch { return '{}'; } })()}>
|
|
1797
|
+
<input class="rdt-cell-editor" type="text" data-editing-cell="" data-builtin-editor="" aria-invalid=${rozieAttr(this._invalidMsg.value ? 'true' : null)} .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 />
|
|
1548
1798
|
</slot>`}
|
|
1549
1799
|
</slot>`}
|
|
1550
|
-
</span>` : html`<input class="rdt-cell-editor" type="text" data-editing-cell="" data-builtin-editor="" .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>
|
|
1800
|
+
</span>` : html`<input class="rdt-cell-editor" type="text" data-editing-cell="" data-builtin-editor="" aria-invalid=${rozieAttr(this._invalidMsg.value ? 'true' : null)} .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>
|
|
1551
1801
|
${this.rozieSlots?.[`cell-${cell.column.id}`] !== undefined ? this.rozieSlots?.[`cell-${cell.column.id}`]!({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: cell.getValue()}) : html`<slot name="${`cell-${cell.column.id}`}" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: cell.getValue()}); } catch { return '{}'; } })()}>
|
|
1552
1802
|
${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>`}
|
|
1553
1803
|
</slot>`}
|
|
@@ -1567,10 +1817,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1567
1817
|
</span>
|
|
1568
1818
|
<button class="rdt-page-btn rdt-page-next" type="button" ?disabled=${!this.canNextPage()} @click=${($event: MouseEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.onNextPage(); }} data-rozie-s-d5dcab4c>Next</button>
|
|
1569
1819
|
<select class="rdt-page-size" aria-label="Rows per page" .value=${this.pageSize()} @change=${($event: Event & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onPageSizeChange($event); }} data-rozie-s-d5dcab4c>
|
|
1570
|
-
<option value=${10} data-rozie-s-d5dcab4c>10</option>
|
|
1571
|
-
<option value=${25} data-rozie-s-d5dcab4c>25</option>
|
|
1572
|
-
<option value=${50} data-rozie-s-d5dcab4c>50</option>
|
|
1573
|
-
<option value=${100} data-rozie-s-d5dcab4c>100</option>
|
|
1820
|
+
<option value=${10} ?selected=${10 === this.pageSize()} data-rozie-s-d5dcab4c>10</option>
|
|
1821
|
+
<option value=${25} ?selected=${25 === this.pageSize()} data-rozie-s-d5dcab4c>25</option>
|
|
1822
|
+
<option value=${50} ?selected=${50 === this.pageSize()} data-rozie-s-d5dcab4c>50</option>
|
|
1823
|
+
<option value=${100} ?selected=${100 === this.pageSize()} data-rozie-s-d5dcab4c>100</option>
|
|
1574
1824
|
</select>
|
|
1575
1825
|
</div>` : nothing}</div>
|
|
1576
1826
|
`;
|
|
@@ -1622,6 +1872,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1622
1872
|
|
|
1623
1873
|
remeasureDisposed = false;
|
|
1624
1874
|
|
|
1875
|
+
// D-19 (quick 260923-rrr): "the user scrolled to the end" — recorded only when the scroll
|
|
1876
|
+
// offset actually MOVED (a growth-driven onChange leaves it where it was), with the row count
|
|
1877
|
+
// and offset at that moment. See scheduleRemeasure() in virtualization.rzts.
|
|
1878
|
+
scrollEndPinned: boolean = false;
|
|
1879
|
+
|
|
1880
|
+
scrollEndPinnedCount: number = -1;
|
|
1881
|
+
|
|
1882
|
+
scrollEndPinnedTop: number = -1;
|
|
1883
|
+
|
|
1625
1884
|
// ── Grid interaction-mode constants + DOM root (phase 49, REQ-2/6) ────────────────────
|
|
1626
1885
|
// Fixed PageUp/PageDown row step (D-06). Phase 53 swaps this for the visible-window size
|
|
1627
1886
|
// via the same focusActiveCell() scroll-into-view seam — kept a top-level const so that
|
|
@@ -1635,6 +1894,20 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1635
1894
|
// computed/template binding (ROZ123).
|
|
1636
1895
|
gridRoot: any = null;
|
|
1637
1896
|
|
|
1897
|
+
// ── B-02: "did the user just click OUTSIDE the grid?" ──────────────────────────────────
|
|
1898
|
+
// Set by the document-level pointerdown listener registered in $onMount, consumed by
|
|
1899
|
+
// onEditorBlur's null-relatedTarget branch (editorBindings.rzts), which explains in full why
|
|
1900
|
+
// the decision has to come from here and be routed back there. In short: a null relatedTarget
|
|
1901
|
+
// on its own cannot tell a click onto a non-focusable page region (the edit must commit) from
|
|
1902
|
+
// an editor being recycled out of the virtual window (it must NOT), and a document listener
|
|
1903
|
+
// cannot commit for itself on React because it closes over the render that attached it — the
|
|
1904
|
+
// draft it would write is the one from editor-open. A plain module-let is hoisted to a
|
|
1905
|
+
// per-instance `useRef` there, so it is read FRESH from the template-bound blur handler.
|
|
1906
|
+
// Capture phase, so a stopPropagation anywhere in the page cannot hide the click from us.
|
|
1907
|
+
outsidePointerDown = false;
|
|
1908
|
+
|
|
1909
|
+
docPointerDown: any = null;
|
|
1910
|
+
|
|
1638
1911
|
// Echo-guard: while WE are writing a slice back, the re-feed watcher must not re-enter
|
|
1639
1912
|
// the funnel. A counter (not a boolean) so nested writes are safe.
|
|
1640
1913
|
programmatic = 0;
|
|
@@ -1733,6 +2006,96 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1733
2006
|
};
|
|
1734
2007
|
};
|
|
1735
2008
|
|
|
2009
|
+
// seedColumnPinning(): apply the per-column `pinned` declaration ('left' | 'right') — from the
|
|
2010
|
+
// `:columns` config array or a `<Column pinned="...">` child — as the INITIAL columnPinning
|
|
2011
|
+
// state, exactly once.
|
|
2012
|
+
//
|
|
2013
|
+
// Until now that field was carried onto every ColumnDef by columnBuilders.rzts and then read by
|
|
2014
|
+
// nobody: `column.getIsPinned()` returned false for a column declared `pinned="left"`, so the
|
|
2015
|
+
// documented per-column API was inert while `r-model:columnPinning` was the only mechanism that
|
|
2016
|
+
// actually pinned anything.
|
|
2017
|
+
//
|
|
2018
|
+
// Three invariants, each load-bearing:
|
|
2019
|
+
// 1. ONE-SHOT, LATCHED ON THE OUTCOME — never merely on "we ran". The latch flips only when
|
|
2020
|
+
// a seed is actually applied, or when a consumer pin is observed; a call that finds no
|
|
2021
|
+
// declared pins leaves it DOWN and will look again on the next re-feed. Latching on the
|
|
2022
|
+
// first call that saw any column at all was wrong, and Lit proved it: `<Column>` children
|
|
2023
|
+
// register INCREMENTALLY there, one re-feed per child, so the first call sees only
|
|
2024
|
+
// `name` — no pins declared yet — and a run-once latch would disable the seed
|
|
2025
|
+
// permanently before `amount pinned="right"` ever registered. The other five targets
|
|
2026
|
+
// batch all children into a single flush, which hid the race entirely. Once a seed HAS
|
|
2027
|
+
// been applied the latch holds, so a subsequent interactive UNPIN is never re-seeded —
|
|
2028
|
+
// which is the whole reason this is not folded into effectiveColumnPinning().
|
|
2029
|
+
// 2. ONLY WHEN NOTHING IS PINNED YET. Note this is NOT an `$props.columnPinning == null`
|
|
2030
|
+
// check: the prop carries `default: () => ({ left: [], right: [] })`, so it is never null
|
|
2031
|
+
// and a bound-vs-unbound test is impossible here. The meaningful question is whether the
|
|
2032
|
+
// consumer has actually expressed a pin — if they have, they own the slice and the
|
|
2033
|
+
// per-column declaration must not fight it; if the state is empty, `pinned` IS the
|
|
2034
|
+
// initial state and applying it is the whole point.
|
|
2035
|
+
// 3. NO CHROME IDS. The select/expander rail is injected by effectiveColumnPinning() on read
|
|
2036
|
+
// and stripped by writeColumnPinning() on write; the persisted state must never contain
|
|
2037
|
+
// them. Seeding only real column ids keeps that discipline.
|
|
2038
|
+
//
|
|
2039
|
+
// Writes BOTH the uncontrolled holder and `$model.columnPinning` so a consumer with a two-way
|
|
2040
|
+
// binding sees the pinning that is actually in effect rather than a stale empty object — but
|
|
2041
|
+
// does NOT emit `pin-change`: this is initial state derived from the consumer's own markup, not
|
|
2042
|
+
// a user action, and firing a change event for it on mount would be noise.
|
|
2043
|
+
//
|
|
2044
|
+
// Called from $onMount (covers the `:columns` config-array form, known at mount) AND from the
|
|
2045
|
+
// re-feed watch (covers `<Column>` children, which register during mount, and a runtime
|
|
2046
|
+
// `:columns` swap that arrives before any pin interaction). The `!defs.length` bail is what
|
|
2047
|
+
// makes the mount call harmless when columns are not known yet — the latch stays down and the
|
|
2048
|
+
// next re-feed seeds instead.
|
|
2049
|
+
pinSeedApplied = false;
|
|
2050
|
+
|
|
2051
|
+
seedColumnPinning = (): void => {
|
|
2052
|
+
if (this.pinSeedApplied) return;
|
|
2053
|
+
// Invariant 2: bail if the consumer already pinned something (either an initial bound value
|
|
2054
|
+
// or an interaction that beat us here). Read the same source order effectiveColumnPinning()
|
|
2055
|
+
// does, minus the chrome rail it injects on read — the rail is structural, not a consumer pin.
|
|
2056
|
+
const live = this.columnPinning != null ? this.columnPinning : this._columnPinningDefault.value;
|
|
2057
|
+
const isRealPin = (id: string) => id !== this.SELECT_COL_ID && id !== this.EXPANDER_COL_ID;
|
|
2058
|
+
const pinnedAlready = (live && live.left ? live.left : []).filter(isRealPin).length > 0 || (live && live.right ? live.right : []).filter(isRealPin).length > 0;
|
|
2059
|
+
if (pinnedAlready) {
|
|
2060
|
+
this.pinSeedApplied = true;
|
|
2061
|
+
return;
|
|
2062
|
+
}
|
|
2063
|
+
const defs = this.columnDefs();
|
|
2064
|
+
if (!defs.length) return;
|
|
2065
|
+
const left: string[] = [];
|
|
2066
|
+
const right: string[] = [];
|
|
2067
|
+
for (let i = 0; i < defs.length; i++) {
|
|
2068
|
+
const def = defs[i];
|
|
2069
|
+
if (!def) continue;
|
|
2070
|
+
if (def.pinned === 'left') left.push(def.id);else if (def.pinned === 'right') right.push(def.id);
|
|
2071
|
+
}
|
|
2072
|
+
// Invariant 1: no declared pins in the column set we can currently SEE is not evidence there
|
|
2073
|
+
// are none — on Lit the set is still filling in. Leave the latch down and try again on the
|
|
2074
|
+
// next re-feed; the only exits are "seeded" (below) and "consumer already pinned" (above).
|
|
2075
|
+
if (!left.length && !right.length) return;
|
|
2076
|
+
this.pinSeedApplied = true;
|
|
2077
|
+
// Nothing was pinned (invariant 2 above), so this is a plain assignment, not a merge — the
|
|
2078
|
+
// only ids that could be in `live` are the structural chrome rail, which must never be
|
|
2079
|
+
// persisted (invariant 3). Writes the uncontrolled holder AND the two-way model so both
|
|
2080
|
+
// binding modes observe the pinning that is actually in effect; deliberately does NOT route
|
|
2081
|
+
// through writeColumnPinning(), whose job is to emit `pin-change` for real user actions.
|
|
2082
|
+
// Annotated `any` for the same reason currentState() is (see its comment below): the
|
|
2083
|
+
// uncontrolled holder's initial value is the empty literal `{ left: [], right: [] }`, so the
|
|
2084
|
+
// emitted per-target state holder infers `{ left: never[]; right: never[] }` and a genuinely
|
|
2085
|
+
// populated `{ left: string[]; right: string[] }` will not assign to it under the strict
|
|
2086
|
+
// bundled-leaf tsc (react TS2345 on setColumnPinningDefault, solid TS2769, lit TS2322 — one
|
|
2087
|
+
// per target, all the same never[] inference). The runtime shape is correct; this is the
|
|
2088
|
+
// established idiom here rather than widening three per-leaf strict baselines for it.
|
|
2089
|
+
// writeColumnPinning() never hits this because its payload flows through `strip()`, which
|
|
2090
|
+
// returns `any[]`.
|
|
2091
|
+
const seeded: any = {
|
|
2092
|
+
left,
|
|
2093
|
+
right
|
|
2094
|
+
};
|
|
2095
|
+
this._columnPinningDefault.value = seeded;
|
|
2096
|
+
this._columnPinningControllable.write(seeded);
|
|
2097
|
+
};
|
|
2098
|
+
|
|
1736
2099
|
// Assemble the live state object from bound r-model slices (?? uncontrolled fallback).
|
|
1737
2100
|
// All NINE slices are wired (each ?? its own $data.<slice>Default). table-core reads
|
|
1738
2101
|
// this whole object as `state`. Return type annotated `any`: the inferred object-literal
|
|
@@ -1786,6 +2149,56 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1786
2149
|
// ({ id?, header, columns: [...] }) maps to a multi-level header GROUP column
|
|
1787
2150
|
// whose children are built recursively (B12 — grouped/multi-level column headers).
|
|
1788
2151
|
// Returns null for an unusable entry (no id/field, unsafe key, empty group).
|
|
2152
|
+
/**
|
|
2153
|
+
* A-01 — translate the public per-column `width` into table-core's `size`.
|
|
2154
|
+
*
|
|
2155
|
+
* `width` was written onto the ColumnDef in both build branches and read by NOTHING:
|
|
2156
|
+
* table-core resolves column width from `size` (a NUMBER of px), which only the two
|
|
2157
|
+
* chrome columns ever set. So the documented API was completely inert — the exact
|
|
2158
|
+
* sibling of the inert `pinned` that 0.5.0 closed.
|
|
2159
|
+
*
|
|
2160
|
+
* `size` is numeric px by construction, so only a number or a px string can map onto
|
|
2161
|
+
* it; any other CSS length (`12rem`, `20%`, `auto`) has no numeric px value at build
|
|
2162
|
+
* time and is deliberately NOT guessed — it returns null and the column keeps
|
|
2163
|
+
* table-core's default. The `width` prop's docs say exactly this.
|
|
2164
|
+
*
|
|
2165
|
+
* Returns null (not undefined) for "no usable width" so callers can test `!= null`
|
|
2166
|
+
* without tripping over a legitimate 0.
|
|
2167
|
+
*/
|
|
2168
|
+
parseWidthToSize = (w: any) => {
|
|
2169
|
+
if (typeof w === 'number') return Number.isFinite(w) && w > 0 ? w : null;
|
|
2170
|
+
if (typeof w !== 'string') return null;
|
|
2171
|
+
const t = w.trim();
|
|
2172
|
+
if (t === '') return null;
|
|
2173
|
+
const m = /^(\d+(?:\.\d+)?)(px)?$/i.exec(t);
|
|
2174
|
+
if (!m) return null;
|
|
2175
|
+
const n = Number.parseFloat(m[1]);
|
|
2176
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
2177
|
+
};
|
|
2178
|
+
|
|
2179
|
+
// ── E-03 ────────────────────────────────────────────────────────────────────────────────
|
|
2180
|
+
// The union check itself is `editorKindWarning` in helpers/columnDefUtils.ts (pure, so the
|
|
2181
|
+
// contract is unit-testable). Here we own the two things that are not pure: WHEN it runs and
|
|
2182
|
+
// how often it speaks.
|
|
2183
|
+
//
|
|
2184
|
+
// WHEN: at column-BUILD time, not in `editorTypeOf`, which is called per cell per render (D-23).
|
|
2185
|
+
// HOW OFTEN: latched per `id:value`, so a rebuild — the memo misses whenever the column set
|
|
2186
|
+
// changes identity — cannot turn one misconfiguration into a console flood.
|
|
2187
|
+
// Unguarded console.warn, matching the D-07/D-08 virtual-misconfig warns in DataTable.rozie:
|
|
2188
|
+
// a process.env guard is not bundler-portable across six targets, and always-warn-on-misconfig
|
|
2189
|
+
// is the established call in this component.
|
|
2190
|
+
editorWarned = Object.create(null);
|
|
2191
|
+
|
|
2192
|
+
checkEditorKind = (id: any, editor: any) => {
|
|
2193
|
+
const msg = editorKindWarning(id, editor);
|
|
2194
|
+
if (!msg) return editor;
|
|
2195
|
+
const key = id + '\u0000' + String(editor);
|
|
2196
|
+
if (this.editorWarned[key]) return editor;
|
|
2197
|
+
this.editorWarned[key] = true;
|
|
2198
|
+
console.warn(msg);
|
|
2199
|
+
return editor;
|
|
2200
|
+
};
|
|
2201
|
+
|
|
1789
2202
|
buildConfigDef = (c: any) => {
|
|
1790
2203
|
if (!c) return null;
|
|
1791
2204
|
// Grouped (multi-level) header column: an entry carrying a `columns` array. table-core's
|
|
@@ -1841,11 +2254,17 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1841
2254
|
aggregationFn: wrapAggregationFn(c.aggregationFn),
|
|
1842
2255
|
pinned: c.pinned != null ? c.pinned : '',
|
|
1843
2256
|
width: c.width != null ? c.width : '',
|
|
2257
|
+
// A-01 — `width` alone is inert; table-core sizes from `size`. Spread so an
|
|
2258
|
+
// unusable width leaves `size` ABSENT rather than explicitly undefined, which
|
|
2259
|
+
// would override a consumer's `defaultColumn.size`.
|
|
2260
|
+
...(this.parseWidthToSize(c.width) != null ? {
|
|
2261
|
+
size: this.parseWidthToSize(c.width)
|
|
2262
|
+
} : {}),
|
|
1844
2263
|
// Editable-cell config (Phase 51) → ColumnDef.meta, the table-core per-column
|
|
1845
2264
|
// metadata carrier the display↔editor branch + runValidator read. Off by default.
|
|
1846
2265
|
meta: {
|
|
1847
2266
|
editable: c.editable === true,
|
|
1848
|
-
editor: c.editor != null ? c.editor : 'text',
|
|
2267
|
+
editor: c.editor != null ? this.checkEditorKind(id, c.editor) : 'text',
|
|
1849
2268
|
editorOptions: c.editorOptions != null ? c.editorOptions : [],
|
|
1850
2269
|
validate: typeof c.validate === 'function' ? c.validate : null
|
|
1851
2270
|
}
|
|
@@ -1930,10 +2349,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
1930
2349
|
aggregationFn: wrapAggregationFn(spec.aggregationFn),
|
|
1931
2350
|
pinned: spec.pinned != null ? spec.pinned : '',
|
|
1932
2351
|
width: spec.width != null ? spec.width : '',
|
|
2352
|
+
// A-01 — see buildConfigDef; same mapping for the <Column> registry branch.
|
|
2353
|
+
...(this.parseWidthToSize(spec.width) != null ? {
|
|
2354
|
+
size: this.parseWidthToSize(spec.width)
|
|
2355
|
+
} : {}),
|
|
1933
2356
|
// Editable-cell config (Phase 51) → ColumnDef.meta from the <Column> registry spec.
|
|
1934
2357
|
meta: {
|
|
1935
2358
|
editable: spec.editable === true,
|
|
1936
|
-
editor: spec.editor != null ? spec.editor : 'text',
|
|
2359
|
+
editor: spec.editor != null ? this.checkEditorKind(id, spec.editor) : 'text',
|
|
1937
2360
|
editorOptions: spec.editorOptions != null ? spec.editorOptions : [],
|
|
1938
2361
|
validate: typeof spec.validate === 'function' ? spec.validate : null
|
|
1939
2362
|
}
|
|
@@ -2467,9 +2890,43 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2467
2890
|
this.writeColumnPinning(applyUpdater(updater, this.currentState().columnPinning));
|
|
2468
2891
|
};
|
|
2469
2892
|
|
|
2893
|
+
// columnSizingInfo is the ONLY slice table-core drives as a rapid, multi-call gesture: a resize
|
|
2894
|
+
// drag fires this updater once on pointerdown (seeding startOffset/startSize/isResizingColumn)
|
|
2895
|
+
// and then again on EVERY mousemove, each call reading the values the previous one wrote.
|
|
2896
|
+
//
|
|
2897
|
+
// That read-your-own-last-write requirement is what makes a plain `$data` read wrong here on
|
|
2898
|
+
// React. These callbacks are handed to createTable in $onMount, so on React the closure binds a
|
|
2899
|
+
// MOUNT-TIME `$data.columnSizingInfo` and never sees its own writes. Measured on the shipped
|
|
2900
|
+
// leaf (quick 260909, instrumented build): pointerdown correctly wrote
|
|
2901
|
+
// `{ startOffset: 163, isResizingColumn: 'col0' }`, and then every single mousemove still saw
|
|
2902
|
+
// `startOffset: null, isResizingColumn: false` — so it computed deltaOffset from 0 instead of
|
|
2903
|
+
// 163 (179, 195, 211… instead of 16, 32, 48…) AND wrote `isResizingColumn: false` back, wiping
|
|
2904
|
+
// the gesture on the first move. Net effect: column resizing did nothing at all on React while
|
|
2905
|
+
// working on the other five. It was masked because the one VR case that drives a real drag was
|
|
2906
|
+
// `test.fixme`'d on React as a presumed harness limitation (87-05 Task 3) — a real CDP drag
|
|
2907
|
+
// with intermediate steps fails there too, which is what proved it a product bug.
|
|
2908
|
+
//
|
|
2909
|
+
// Why not just read `currentState().columnSizingInfo` like the eleven siblings above: that
|
|
2910
|
+
// idiom relies on the re-feed $watch re-passing fresh callbacks into setOptions, and that watch
|
|
2911
|
+
// fires on data/column changes — NOT on every render, and never mid-gesture. It is sufficient
|
|
2912
|
+
// for one-shot updaters (a sort toggle, a selection click) and insufficient here, where
|
|
2913
|
+
// table-core issues a burst of updaters between any two re-feeds.
|
|
2914
|
+
//
|
|
2915
|
+
// Fix: a synchronous module-scope mirror, written on the same tick as the state. Reads prefer
|
|
2916
|
+
// the mirror, so each updater in a gesture composes on the previous one's result regardless of
|
|
2917
|
+
// when the host framework commits. Behaviourally identical on the five targets whose state read
|
|
2918
|
+
// was already synchronous (mirror and state agree), and the slice is transient gesture state
|
|
2919
|
+
// owned entirely by table-core — nothing else writes it — so the mirror cannot drift.
|
|
2920
|
+
// Same trick as the `committedThisSession` commit latch (editCellLifecycle.rzts), for the same
|
|
2921
|
+
// reason: a sync latch survives React's async window.
|
|
2922
|
+
columnSizingInfoSync: any = null;
|
|
2923
|
+
|
|
2470
2924
|
onColumnSizingInfoChangeCb = (updater: any) => {
|
|
2471
|
-
const
|
|
2472
|
-
|
|
2925
|
+
const base = this.columnSizingInfoSync != null ? this.columnSizingInfoSync : this._columnSizingInfo.value;
|
|
2926
|
+
const next = applyUpdater(updater, base);
|
|
2927
|
+
if (next == null) return;
|
|
2928
|
+
this.columnSizingInfoSync = next;
|
|
2929
|
+
this._columnSizingInfo.value = next;
|
|
2473
2930
|
};
|
|
2474
2931
|
|
|
2475
2932
|
// ══ Vertical row windowing (phase 53, req-1/2/3/6/9/10) — the virtual-core bridge ════════
|
|
@@ -2581,9 +3038,18 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2581
3038
|
}
|
|
2582
3039
|
}
|
|
2583
3040
|
// Active cell's column (D-10): required or focusActiveCell()'s [data-col-index="N"]
|
|
2584
|
-
// querySelector cannot resolve for an off-window active cell.
|
|
2585
|
-
//
|
|
2586
|
-
|
|
3041
|
+
// querySelector cannot resolve for an off-window active cell.
|
|
3042
|
+
//
|
|
3043
|
+
// B-03: this used to be body-cells-only, on the stated grounds that "a header-active cell
|
|
3044
|
+
// has no column-axis rendering dependency here (headers window separately)". That is false —
|
|
3045
|
+
// `windowedHeadersFor()` slices the header row from `windowedColIndices()`, the very set this
|
|
3046
|
+
// function feeds, so a header cell has EXACTLY the same dependency a body cell does. With the
|
|
3047
|
+
// active header column unforced, focusing a leaf header in a 60-column windowed grid and
|
|
3048
|
+
// pressing End left `resolveCellEl` with nothing to find: focus froze on the old `<th>` while
|
|
3049
|
+
// the roving model moved on, and `cellTabindex` handed `tabindex="0"` to a cell that does not
|
|
3050
|
+
// exist — so the grid had no tab stop at all. The gate is gone; the column is forced for
|
|
3051
|
+
// whichever axis the active cell is on.
|
|
3052
|
+
if (this._activeColIndex.value >= 0 && out.indexOf(this._activeColIndex.value) === -1) {
|
|
2587
3053
|
out.push(this._activeColIndex.value);
|
|
2588
3054
|
}
|
|
2589
3055
|
// Editing column (D-10): required or an open editor unmounts mid-keystroke when a
|
|
@@ -2660,21 +3126,65 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2660
3126
|
// committed yet), so the rAF pass is what observes React's recycled rows.
|
|
2661
3127
|
// Each pass only OBSERVES + measures the live window; measureElement is idempotent on an
|
|
2662
3128
|
// already-observed node, so running both is cheap and loop-free.
|
|
3129
|
+
// Upper bound on the frames scheduleRemeasure() will wait for the framework to commit a recycled
|
|
3130
|
+
// window (N-05). Measured commit lag is 1 frame on React/Angular; 10 is generous headroom.
|
|
3131
|
+
REMEASURE_MAX_FRAMES = 10;
|
|
3132
|
+
|
|
2663
3133
|
scheduleRemeasure = () => {
|
|
2664
3134
|
// 87-13: never arm a new sweep after teardown — an onChange can still fire from a
|
|
2665
3135
|
// virtualizer whose own ResizeObserver cleanup has run but whose queued tick was already
|
|
2666
3136
|
// in flight.
|
|
2667
3137
|
if (this.remeasureDisposed) return;
|
|
3138
|
+
// D-19 (quick 260923-rrr): remember whether the view is at the END — but only when the scroll
|
|
3139
|
+
// offset MOVED since the last record. A measurement-driven onChange (rows growing) arrives
|
|
3140
|
+
// with the offset unchanged, and it is exactly that growth that moves the view off the end,
|
|
3141
|
+
// so it must not clear the pin. (virtual-core's isScrolling cannot tell the two apart: it
|
|
3142
|
+
// stays true for 150ms after the user's scroll, spanning the growth.)
|
|
3143
|
+
if (this.virtualizer && this.gridScrollEl && !this.virtualizer.scrollState && this.gridScrollEl.scrollTop !== this.scrollEndPinnedTop) {
|
|
3144
|
+
this.scrollEndPinnedTop = this.gridScrollEl.scrollTop;
|
|
3145
|
+
// Judged on virtual-core's MODEL (total size − viewport − offset), not DOM geometry: its own
|
|
3146
|
+
// above-viewport adjustments move scrollTop before the framework commits the matching
|
|
3147
|
+
// spacer, so the DOM transiently reads "not at the end" for a view that is (measured on
|
|
3148
|
+
// Angular: a 12px adjustment cleared a DOM-judged pin and left the view 12px short).
|
|
3149
|
+
this.scrollEndPinned = this.virtualizer.getVirtualDistanceFromEnd() <= 1;
|
|
3150
|
+
this.scrollEndPinnedCount = this.windowSource().length;
|
|
3151
|
+
}
|
|
2668
3152
|
if (this.remeasurePending) return;
|
|
2669
3153
|
this.remeasurePending = true;
|
|
2670
3154
|
let ranMicro = false;
|
|
2671
3155
|
const microPass = () => {
|
|
2672
3156
|
this.remeasureWindow();
|
|
2673
3157
|
};
|
|
3158
|
+
// N-05 (quick 260923-rrr): the rAF pass keys on the OUTCOME, not on "a frame went by". React
|
|
3159
|
+
// and Angular commit the recycled window AFTER that first rAF, so a single pass measured the
|
|
3160
|
+
// OLD rows and the new ones were only handed to measureElement when virtual-core's 150ms
|
|
3161
|
+
// scrolling-ended tick fired an onChange — the user saw the un-adjusted layout, then a 1–3 row
|
|
3162
|
+
// jump (measured). While the committed <tr> set does not yet cover the virtualizer's window,
|
|
3163
|
+
// re-run next frame, bounded so a window that can never be covered cannot spin forever.
|
|
3164
|
+
let rafAttempts = 0;
|
|
2674
3165
|
const rafPass = () => {
|
|
2675
3166
|
this.remeasureRaf = null;
|
|
3167
|
+
const covered = this.remeasureWindow();
|
|
3168
|
+
rafAttempts = rafAttempts + 1;
|
|
3169
|
+
if (!covered && rafAttempts < this.REMEASURE_MAX_FRAMES && !this.remeasureDisposed && typeof requestAnimationFrame === 'function') {
|
|
3170
|
+
this.remeasureRaf = requestAnimationFrame(rafPass);
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
3173
|
+
// D-19: keep a user who scrolled to the END at the end. When rows at/above the viewport
|
|
3174
|
+
// measure taller than their estimate, virtual-core compensates with a scroll adjustment — but
|
|
3175
|
+
// on React/Angular that adjustment is written BEFORE the framework commits the taller spacer,
|
|
3176
|
+
// so the browser clamps it to the OLD maximum and it is lost (measured: the view ended
|
|
3177
|
+
// 593–1025px above the bottom on React, 30px on Angular). This pass runs after the commit, so
|
|
3178
|
+
// the real maximum is known. Not when the row count changed: appending rows must not
|
|
3179
|
+
// auto-follow them.
|
|
3180
|
+
if (this.scrollEndPinned && this.gridScrollEl && !this.remeasureDisposed && this.windowSource().length === this.scrollEndPinnedCount) {
|
|
3181
|
+
const maxTop = this.gridScrollEl.scrollHeight - this.gridScrollEl.clientHeight;
|
|
3182
|
+
if (maxTop - this.gridScrollEl.scrollTop > 1) {
|
|
3183
|
+
this.gridScrollEl.scrollTop = maxTop;
|
|
3184
|
+
this.scrollEndPinnedTop = this.gridScrollEl.scrollTop;
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
2676
3187
|
this.remeasurePending = false;
|
|
2677
|
-
this.remeasureWindow();
|
|
2678
3188
|
};
|
|
2679
3189
|
if (typeof queueMicrotask !== 'undefined') {
|
|
2680
3190
|
ranMicro = true;
|
|
@@ -2708,9 +3218,25 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2708
3218
|
// unioned into the windowed slice when it scrolls off-window and its height is subtracted from
|
|
2709
3219
|
// the appropriate spacer so the total stays exactly getTotalSize() (the 51-01-proven mechanism).
|
|
2710
3220
|
// This is the data-table half of the D-05 windowing.rzts pin-extension hook (listbox provides none).
|
|
3221
|
+
//
|
|
3222
|
+
// B-06 widens this from "the editing row" to "the row that must not recycle out of the
|
|
3223
|
+
// window", which now includes the ACTIVE row in grid mode. The active cell is the grid's ONLY
|
|
3224
|
+
// `tabindex="0"` — `cellTabindex()` returns 0 for it and -1 for every other cell — so once a
|
|
3225
|
+
// wheel-scroll carried it out of the window the grid had NO tab stop at all: Tab could not
|
|
3226
|
+
// re-enter it, and the roving model still pointed at a cell with no DOM node, so
|
|
3227
|
+
// `resolveCellEl` returned null and a subsequent arrow key moved state with nothing to focus.
|
|
3228
|
+
// The column axis has forced the active-cell COLUMN since D-10 (`forcedColumns`); this is the
|
|
3229
|
+
// row-axis twin of that decision, and it costs at most one extra rendered row, in flow, with
|
|
3230
|
+
// its height subtracted from the spacer by the same 51-01 mechanism the editing pin uses.
|
|
3231
|
+
//
|
|
3232
|
+
// `$props.interactionMode` is read directly rather than through `isGrid()` (gridFocusNav.rzts):
|
|
3233
|
+
// that partial is imported AFTER this one, and this file's header documents the
|
|
3234
|
+
// forward-cross-partial-reference TDZ on React that the 87-02 lesson established. A prop read
|
|
3235
|
+
// has no such hazard.
|
|
2711
3236
|
pinnedEditIndex = () => {
|
|
2712
3237
|
if (this._editingRow.value >= 0) return this._editingRow.value;
|
|
2713
3238
|
if (this._editingRowIndex.value != null) return this._editingRowIndex.value;
|
|
3239
|
+
if (this.interactionMode === 'grid' && !this._activeIsHeader.value && this._activeRow.value >= 0) return this._activeRow.value;
|
|
2714
3240
|
return -1;
|
|
2715
3241
|
};
|
|
2716
3242
|
|
|
@@ -2738,8 +3264,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2738
3264
|
// cover unmount — virtualizer/gridRoot are assigned in $onMount and never nulled — so this
|
|
2739
3265
|
// is the only thing standing between a pass queued pre-unmount and refineRowEstimate()'s
|
|
2740
3266
|
// setOptions/scrollTop/$data.windowVer writes against a torn-down instance.
|
|
2741
|
-
if (this.remeasureDisposed) return;
|
|
2742
|
-
if (!this.virtualizer || !this.gridRoot) return;
|
|
3267
|
+
if (this.remeasureDisposed) return true;
|
|
3268
|
+
if (!this.virtualizer || !this.gridRoot) return true;
|
|
2743
3269
|
// Bail ONLY while a PROGRAMMATIC scroll is in flight: virtualizer.scrollState is non-null
|
|
2744
3270
|
// exclusively during scrollToIndex / scrollToOffset (the D-12 scroll-then-focus seam) and
|
|
2745
3271
|
// null for ordinary user/scrollTop-driven scrolling (verified virtual-core@3.17.1: set in
|
|
@@ -2747,9 +3273,23 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2747
3273
|
// resizeItem nudge the offset and starve the scroll target (the Solid off-window focus
|
|
2748
3274
|
// regression); the next settled onChange re-measures the stable window. Manual-scroll
|
|
2749
3275
|
// recycling (the CR-01 case) has scrollState === null, so it measures normally.
|
|
2750
|
-
if (this.virtualizer.scrollState) return;
|
|
3276
|
+
if (this.virtualizer.scrollState) return true;
|
|
2751
3277
|
const trs = this.gridRoot.querySelectorAll('tbody.rdt-tbody > tr[data-index]');
|
|
2752
|
-
|
|
3278
|
+
const rendered = new Set();
|
|
3279
|
+
for (const tr of trs as any) {
|
|
3280
|
+
this.virtualizer.measureElement(tr);
|
|
3281
|
+
rendered.add(tr.getAttribute('data-index'));
|
|
3282
|
+
}
|
|
3283
|
+
// N-05: did the committed rows cover the virtualizer's CURRENT window? False while the
|
|
3284
|
+
// framework has not yet committed the recycled set (see scheduleRemeasure).
|
|
3285
|
+
let covered = true;
|
|
3286
|
+
const items = this.virtualizer.getVirtualItems();
|
|
3287
|
+
for (let i = 0; i < items.length; i++) {
|
|
3288
|
+
if (!rendered.has(String(items[i].index))) {
|
|
3289
|
+
covered = false;
|
|
3290
|
+
break;
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
2753
3293
|
// D-15 (Phase 87 87-07): trigger the shared engine's post-measurement fold + hysteresis
|
|
2754
3294
|
// re-feed via the afterRowRemeasure mutable-let hook (DataTable.rozie's own script,
|
|
2755
3295
|
// assigned to windowing.rzts's refineRowEstimate()) — NEVER a direct call from this file
|
|
@@ -2758,6 +3298,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2758
3298
|
// documents for columnVirtualizerOptions()). Inert (no-op) until the host assigns it; the
|
|
2759
3299
|
// off/pre-mount path never reaches this line since virtualizer is null there.
|
|
2760
3300
|
if (this.afterRowRemeasure) this.afterRowRemeasure();
|
|
3301
|
+
return covered;
|
|
2761
3302
|
};
|
|
2762
3303
|
|
|
2763
3304
|
// D-04: this shell exports ONLY the impure, data-table-specific host pieces. The pure windowing
|
|
@@ -2766,6 +3307,119 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
2766
3307
|
// `@rozie-ui/headless-core/windowing.rzts` via bare specifier — the P0-proven cross-package inline
|
|
2767
3308
|
// path that DISSOLVES the partial into the leaf (a re-export-from THROUGH this shell would survive as
|
|
2768
3309
|
// a runtime import, not inline — verified). The math closes over these host symbols by convention.
|
|
3310
|
+
// ── Lazy rows (quick 260925-dtl part 2, oinbox dogfooding) ─────────────────────────────────────
|
|
3311
|
+
// `virtual` + `manual` + `rowCount`: the row space is rowCount long and `data` may be SPARSE —
|
|
3312
|
+
// an `undefined`/`null` entry (or anything past data.length) is a row not loaded yet. Such holes
|
|
3313
|
+
// are fed to table-core as placeholder sentinels (one cached object per index, so a re-feed does
|
|
3314
|
+
// not churn them) that render as placeholder rows and can't be selected, expanded, edited or
|
|
3315
|
+
// activated. ONLY the table-core feed is padded: every write-back derives from currentData(), the
|
|
3316
|
+
// consumer's own array, so a sentinel can never leak into `data`.
|
|
3317
|
+
LAZY_PLACEHOLDER_KEY = '__rdtLazyPlaceholder';
|
|
3318
|
+
|
|
3319
|
+
// LAZY_PLACEHOLDER_ROW_ID_PREFIX: the table-core row-id table.rowIdOption() (DataTable.rozie)
|
|
3320
|
+
// assigns to a placeholder ('__rdt_ph_' + index — no consumer row to ask, so it keys by
|
|
3321
|
+
// position). Shared here (not re-literaled) because migratePlaceholderSizeCache() below and
|
|
3322
|
+
// rowIdOption() must agree byte-for-byte on the prefix or the migration's key scan silently
|
|
3323
|
+
// finds nothing.
|
|
3324
|
+
LAZY_PLACEHOLDER_ROW_ID_PREFIX = '__rdt_ph_';
|
|
3325
|
+
|
|
3326
|
+
lazyPlaceholders: Record<number, any> = {};
|
|
3327
|
+
|
|
3328
|
+
lazyRowsActive = () => this.rowsWindowed() && !!this.manual && this.rowCount != null && this.rowCount > 0;
|
|
3329
|
+
|
|
3330
|
+
isLazyPlaceholder = (orig: any): boolean => !!orig && orig[this.LAZY_PLACEHOLDER_KEY] === true;
|
|
3331
|
+
|
|
3332
|
+
rowIsLazyPlaceholder = (row: any): boolean => !!row && this.isLazyPlaceholder(row.original);
|
|
3333
|
+
|
|
3334
|
+
feedData = () => {
|
|
3335
|
+
const d: any[] = this.currentData() || [];
|
|
3336
|
+
if (!this.lazyRowsActive()) return d;
|
|
3337
|
+
const n = Math.max(d.length, Number(this.rowCount));
|
|
3338
|
+
const out: any[] = new Array(n);
|
|
3339
|
+
for (let i = 0; i < n; i++) {
|
|
3340
|
+
const v = d[i];
|
|
3341
|
+
if (v != null) {
|
|
3342
|
+
out[i] = v;
|
|
3343
|
+
} else {
|
|
3344
|
+
if (!this.lazyPlaceholders[i]) this.lazyPlaceholders[i] = {
|
|
3345
|
+
[this.LAZY_PLACEHOLDER_KEY]: true,
|
|
3346
|
+
index: i
|
|
3347
|
+
};
|
|
3348
|
+
out[i] = this.lazyPlaceholders[i];
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
return out;
|
|
3352
|
+
};
|
|
3353
|
+
|
|
3354
|
+
// migratePlaceholderSizeCache() (quick 260926, VR probe on data-table-lazy [react]): a filled
|
|
3355
|
+
// placeholder's row id changes ('__rdt_ph_' + i -> the real getRowId id), and virtual-core's
|
|
3356
|
+
// getItemKey re-keys by it — but virtual-core's itemSizeCache (dist/esm/index.js@3.17.1) is a
|
|
3357
|
+
// bare Map keyed by the OLD key, so the new key has no entry. The next getMeasurements() recompute
|
|
3358
|
+
// then estimates that row at $props.estimateRowHeight instead of its already-known measured size;
|
|
3359
|
+
// the CR-01 sweep re-measures it moments later and each delta fires one applyScrollAdjustment,
|
|
3360
|
+
// walking the window's anchor off by their sum (measured: 9 rows x -4px = -36px, row 5000 -> 4999
|
|
3361
|
+
// on React — the other 5 targets happened to keep reading the stale pre-refeed measurements array
|
|
3362
|
+
// and never saw the estimate at all). Fix: carry the cached size across the rekey, keyed off
|
|
3363
|
+
// table-core's OWN freshly-assigned id (table.getCoreRowModel().rows[i].id, NOT a locally
|
|
3364
|
+
// recomputed getRowId() call — must be byte-identical to what virtualItemKey(i) reads next) so
|
|
3365
|
+
// the cache is already correct before the caller's virtualizer.setOptions()/_willUpdate() re-feed.
|
|
3366
|
+
// Invalidating the memo needs BOTH strategies below, not just one: getMeasurements() is
|
|
3367
|
+
// memoized on [getMeasurementOptions(), itemSizeCacheVersion] on 3.15+ (confirmed from the
|
|
3368
|
+
// installed source; the same fact windowing.rzts's refineRowEstimate() comment documents) —
|
|
3369
|
+
// but our peer range is `^3`, and versions <=3.14 have NO itemSizeCacheVersion at all and key
|
|
3370
|
+
// the SAME memo on `this.itemSizeCache` (Map IDENTITY) instead — verified against the 3.14.0
|
|
3371
|
+
// tarball, whose own resizeItem() invalidates by reassigning `this.itemSizeCache = new Map(...)`,
|
|
3372
|
+
// never by bumping a version counter. Mutating the Map in place would be silently invisible to
|
|
3373
|
+
// that memo on <=3.14 (a stray `itemSizeCacheVersion++` there just creates a NaN-valued own
|
|
3374
|
+
// property nobody reads). So this reassigns the Map (a public field on every 3.x we support —
|
|
3375
|
+
// harmless on 3.15+, whose memo body re-reads `this.itemSizeCache` fresh) AND bumps the version
|
|
3376
|
+
// counter only where it actually exists. Scans the CACHE's own placeholder-prefixed keys
|
|
3377
|
+
// (bounded by rows actually measured so far), never rowCount (unbounded — 10,000+ here).
|
|
3378
|
+
migratePlaceholderSizeCache = (): void => {
|
|
3379
|
+
if (!this.virtualizer || !this.virtualizer.itemSizeCache || !this.table) return;
|
|
3380
|
+
const cache = this.virtualizer.itemSizeCache;
|
|
3381
|
+
const coreRows = this.table.getCoreRowModel().rows;
|
|
3382
|
+
let migrated = false;
|
|
3383
|
+
for (const oldKey of Array.from(cache.keys()) as any) {
|
|
3384
|
+
if (typeof oldKey !== 'string' || oldKey.indexOf(this.LAZY_PLACEHOLDER_ROW_ID_PREFIX) !== 0) continue;
|
|
3385
|
+
const i = Number(oldKey.slice(this.LAZY_PLACEHOLDER_ROW_ID_PREFIX.length));
|
|
3386
|
+
const row = coreRows[i];
|
|
3387
|
+
if (!row || this.rowIsLazyPlaceholder(row) || row.id === oldKey) continue;
|
|
3388
|
+
cache.set(row.id, cache.get(oldKey));
|
|
3389
|
+
cache.delete(oldKey);
|
|
3390
|
+
migrated = true;
|
|
3391
|
+
}
|
|
3392
|
+
if (!migrated) return;
|
|
3393
|
+
this.virtualizer.itemSizeCache = new Map(cache);
|
|
3394
|
+
if (typeof this.virtualizer.itemSizeCacheVersion === 'number') this.virtualizer.itemSizeCacheVersion++;
|
|
3395
|
+
};
|
|
3396
|
+
|
|
3397
|
+
// `visible-range-change { start, end }` — the RENDERED row window (overscan included), `end`
|
|
3398
|
+
// exclusive, over the full row space. Driven by the window-version bump every virtualizer change
|
|
3399
|
+
// routes through, deduped here so a scroll that doesn't move the window emits nothing.
|
|
3400
|
+
lastRangeStart = -1;
|
|
3401
|
+
|
|
3402
|
+
lastRangeEnd = -1;
|
|
3403
|
+
|
|
3404
|
+
emitVisibleRangeIfChanged = () => {
|
|
3405
|
+
if (!this.rowsWindowed() || !this.virtualizer) return;
|
|
3406
|
+
const items = this.virtualizer.getVirtualItems();
|
|
3407
|
+
if (!items.length) return;
|
|
3408
|
+
const start = items[0].index;
|
|
3409
|
+
const end = items[items.length - 1].index + 1;
|
|
3410
|
+
if (start === this.lastRangeStart && end === this.lastRangeEnd) return;
|
|
3411
|
+
this.lastRangeStart = start;
|
|
3412
|
+
this.lastRangeEnd = end;
|
|
3413
|
+
this.dispatchEvent(new CustomEvent("visible-range-change", {
|
|
3414
|
+
detail: {
|
|
3415
|
+
start,
|
|
3416
|
+
end
|
|
3417
|
+
},
|
|
3418
|
+
bubbles: true,
|
|
3419
|
+
composed: true
|
|
3420
|
+
}));
|
|
3421
|
+
};
|
|
3422
|
+
|
|
2769
3423
|
// ══ Generic vertical windowing math (Phase 64, D-04) — the target-agnostic virtual-core bridge ══
|
|
2770
3424
|
// Lifted verbatim from the DataTable virtualization.rzts (the Phase 53/63 B13 baseline). This partial
|
|
2771
3425
|
// holds ONLY the PURE windowing math; every DOM/refs/virtualizer-instance impurity stays per-consumer
|
|
@@ -3147,6 +3801,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3147
3801
|
// makes the eventual render land strictly AFTER this fold + re-feed has fully landed, on
|
|
3148
3802
|
// every target (the 87-10 fix for the confirmed Solid-specific rendering gap).
|
|
3149
3803
|
this.bumpWindowVer();
|
|
3804
|
+
// D-19 (quick 260923-rrr): a re-feed changes every unmeasured row's size, but — unlike a
|
|
3805
|
+
// measurement — it does not go through virtual-core's onChange, so the host's post-commit
|
|
3806
|
+
// pass (which restores an at-the-end view the growth moved off) never ran for it. Schedule it.
|
|
3807
|
+
this.scheduleRemeasure();
|
|
3150
3808
|
};
|
|
3151
3809
|
|
|
3152
3810
|
// The FULL virtualizer options. virtual-core's setOptions REPLACES options with
|
|
@@ -3545,6 +4203,25 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3545
4203
|
return '';
|
|
3546
4204
|
};
|
|
3547
4205
|
|
|
4206
|
+
// The table-core `getRowId` option (quick 260925-rew). Without it table-core keys
|
|
4207
|
+
// rowSelection / expanded by position, so a row inserted above a selected one moves the
|
|
4208
|
+
// selection onto another row. The prop is captured into a local so the null check narrows
|
|
4209
|
+
// inside the arrow (TS2721 on the Vue leaf otherwise), and String() because table-core
|
|
4210
|
+
// compares ids as strings.
|
|
4211
|
+
rowIdOption = () => {
|
|
4212
|
+
const f = this.getRowId;
|
|
4213
|
+
// A lazy placeholder has no consumer row to ask — key it by its index (see feedData()).
|
|
4214
|
+
// Prefix shared with migratePlaceholderSizeCache() (virtualization.rzts) — see its own comment.
|
|
4215
|
+
return f ? (originalRow: any, index: any, parent: any) => this.isLazyPlaceholder(originalRow) ? this.LAZY_PLACEHOLDER_ROW_ID_PREFIX + originalRow.index : String(f(originalRow, index, parent)) : undefined;
|
|
4216
|
+
};
|
|
4217
|
+
|
|
4218
|
+
// table-core `enableRowSelection`: off for selectionMode 'none'; with lazy rows a placeholder
|
|
4219
|
+
// (a row not loaded yet) is never selectable, so select-all takes the loaded rows only.
|
|
4220
|
+
rowSelectionOption = () => {
|
|
4221
|
+
if (this.selectionMode === 'none') return false;
|
|
4222
|
+
return this.lazyRowsActive() ? (row: any) => !this.rowIsLazyPlaceholder(row) : true;
|
|
4223
|
+
};
|
|
4224
|
+
|
|
3548
4225
|
// Push fresh options into table-core + re-pull the row model. Extracted so BOTH the
|
|
3549
4226
|
// re-feed $watch (above) and the Lit data-change $onUpdate (below) call it.
|
|
3550
4227
|
reFeed = () => {
|
|
@@ -3557,10 +4234,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3557
4234
|
// below (`maybeClearHistoryOnExternalSwap`), which runs on all six targets.
|
|
3558
4235
|
this.table.setOptions((prev: any) => ({
|
|
3559
4236
|
...prev,
|
|
3560
|
-
data: this.
|
|
4237
|
+
data: this.feedData(),
|
|
3561
4238
|
columns: this.tableColumns(),
|
|
3562
4239
|
state: this.currentState(),
|
|
3563
|
-
enableRowSelection: this.
|
|
4240
|
+
enableRowSelection: this.rowSelectionOption(),
|
|
3564
4241
|
enableMultiRowSelection: this.selectionMode === 'multiple',
|
|
3565
4242
|
// Re-pass the server-side page-count sources (#2) so a RUNTIME rowCount/pageCount change
|
|
3566
4243
|
// takes effect: setOptions REPLACES via `...prev`, which holds the value captured at
|
|
@@ -3573,7 +4250,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3573
4250
|
// onExpandedChange callback must re-capture fresh currentState each cycle, F6).
|
|
3574
4251
|
getExpandedRowModel: getExpandedRowModel(),
|
|
3575
4252
|
getSubRows: (this.getSubRows || undefined) as any,
|
|
3576
|
-
|
|
4253
|
+
// Row identity (quick 260925-rew) — see rowIdOption(). Re-passed on re-feed like the
|
|
4254
|
+
// other row-model fns.
|
|
4255
|
+
getRowId: this.rowIdOption(),
|
|
4256
|
+
getRowCanExpand: this.expandable === true && this.getSubRows == null ? (row: any) => !this.rowIsLazyPlaceholder(row) : undefined,
|
|
3577
4257
|
onExpandedChange: this.onExpandedChangeCb,
|
|
3578
4258
|
// Grouping auto-expand (phase 50 req-4): table-core's autoResetExpanded defaults TRUE, so a
|
|
3579
4259
|
// POST-MOUNT setGrouping (the consumer #groupBar / applyGrouping verb) auto-fires
|
|
@@ -3913,9 +4593,17 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
3913
4593
|
// read header.getSize() — the group-aware size (see headerWidth's own comment). pinStyle still
|
|
3914
4594
|
// keys off the plain column id (pinning is a leaf-column-only concern; a grouped header never
|
|
3915
4595
|
// pins as a unit in this codebase's model).
|
|
3916
|
-
|
|
4596
|
+
// A-03: `widthPx` is the in-window leaf-width sum windowedHeadersFor() computed alongside the
|
|
4597
|
+
// clamped colspan. When it is present it WINS over headerWidth(), which reports
|
|
4598
|
+
// Header.getSize() — every descendant leaf, in-window or not — and so over-declares a straddling
|
|
4599
|
+
// group by exactly the leaves the colspan already excluded. Null on the unwindowed path and from
|
|
4600
|
+
// the non-windowed header body, which both keep headerWidth() verbatim.
|
|
4601
|
+
// `widthPx` carries an EXPLICIT `= null` default for the same reason setRangeFocus's anchor
|
|
4602
|
+
// params do: the emitted signature types a declared param as required, and the non-windowed
|
|
4603
|
+
// header body calls `thStyle(header)` with one argument — TS2554 on the strict leaf typecheck.
|
|
4604
|
+
thStyle = (header: any, widthPx = null) => {
|
|
3917
4605
|
let s = '';
|
|
3918
|
-
const w = this.headerWidth(header);
|
|
4606
|
+
const w = widthPx != null && widthPx > 0 ? widthPx + 'px' : this.headerWidth(header);
|
|
3919
4607
|
if (w) s += 'width:' + w + ';';
|
|
3920
4608
|
s += this.pinStyle(header && header.column ? header.column.id : null, 2);
|
|
3921
4609
|
return s;
|
|
@@ -4301,7 +4989,6 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4301
4989
|
if (this.selectAllBox) this.selectAllBox.indeterminate = this.isSomeRowsSelected() && !this.isAllRowsSelected();
|
|
4302
4990
|
};
|
|
4303
4991
|
|
|
4304
|
-
// The registry API handed to <Column> children (whole-object-replace — T-48-PP guard).
|
|
4305
4992
|
// Imperative handle (consumer-callable). Each verb is a PRE-DECLARED top-level
|
|
4306
4993
|
// `const` (the canonical $expose contract — `$expose({ name })` references a
|
|
4307
4994
|
// binding ALREADY in scope; an INLINE-defined verb `$expose({ name: () => {} })`
|
|
@@ -4422,6 +5109,23 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4422
5109
|
// headerColIndexOf: a header cell's position in its header group's leaf headers.
|
|
4423
5110
|
headerColIndexOf = (hg: any, header: any) => (hg && hg.headers ? hg.headers : []).indexOf(header);
|
|
4424
5111
|
|
|
5112
|
+
// E-05 — a header's ABSOLUTE LEAF-column start index, which is NOT the same as its position
|
|
5113
|
+
// in the group's header array: a grouped header occupies ONE array slot but spans colSpan leaf
|
|
5114
|
+
// columns, so positions and leaf indices diverge the moment a header group exists. Walk the
|
|
5115
|
+
// group accumulating colSpan to the target. Window-independent by construction (a header's
|
|
5116
|
+
// leaf start is a full-model property), so both the windowed grid header and the plain
|
|
5117
|
+
// non-grid header can share it.
|
|
5118
|
+
headerLeafStart = (hg: any, header: any) => {
|
|
5119
|
+
const list = hg && hg.headers ? hg.headers : [];
|
|
5120
|
+
let leaf = 0;
|
|
5121
|
+
for (let i = 0; i < list.length; i++) {
|
|
5122
|
+
if (list[i] === header) return leaf;
|
|
5123
|
+
const h = list[i];
|
|
5124
|
+
leaf = leaf + (h && h.colSpan > 1 ? h.colSpan : 1);
|
|
5125
|
+
}
|
|
5126
|
+
return -1;
|
|
5127
|
+
};
|
|
5128
|
+
|
|
4425
5129
|
// ── C1 (phase 63 wave-6) absolute-index bridge ─────────────────────────────────────────
|
|
4426
5130
|
// The PUBLIC active-cell rowIndex (focusCell/getActiveCell/activecell-change) is the ABSOLUTE
|
|
4427
5131
|
// display-order position in getPrePaginationRowModel().rows (filter+sort+expand applied, BEFORE
|
|
@@ -4507,7 +5211,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4507
5211
|
// post-mount root. Uniform on all six, shadow-safe (the query runs from inside the
|
|
4508
5212
|
// component's own scope). rowKey is the literal '__header' or a String(integer index) and
|
|
4509
5213
|
// colIndex is an integer — NO consumer string is interpolated into the selector (T-49-01).
|
|
4510
|
-
|
|
5214
|
+
// `level` is ANNOTATED, not just defaulted: an untyped `level = null` infers the param type as
|
|
5215
|
+
// `null`, so every header call site passing a real level (`lvl`, activeHeaderLevel) was a
|
|
5216
|
+
// TS2345 under the strict leaf typecheck (quick 260922-mkb — B-03 added one more such call).
|
|
5217
|
+
resolveCellEl = (rowKey: any, colIndex: any, level: number | null = null) => {
|
|
4511
5218
|
if (!this.gridRoot) return null;
|
|
4512
5219
|
// B12: a grouped multi-level header has MULTIPLE cells sharing data-row="__header" at the
|
|
4513
5220
|
// same data-col-index across levels (parent vs leaf). Disambiguate header lookups by the
|
|
@@ -4567,10 +5274,18 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4567
5274
|
// both are out, and the ONE existing poll below is reused unchanged — it already resolves the
|
|
4568
5275
|
// target cell by BOTH (row, col) via resolveCellEl(String(r), c), so a cell that becomes
|
|
4569
5276
|
// reachable via the column axis needs no new poll logic (per the plan's own key_links note).
|
|
5277
|
+
// B-03: the COLUMN axis applies to HEADER cells too. This guard used to be `!header &&
|
|
5278
|
+
// (rowOut || colOut)`, so a header-active move to an off-window column never scrolled it in:
|
|
5279
|
+
// `resolveCellEl` returned null, focus froze on the old `<th>` while the state and the ring
|
|
5280
|
+
// ran away to a column with no DOM node, and `cellTabindex` then reported 0 for an unrendered
|
|
5281
|
+
// cell — leaving the grid with ZERO `tabindex="0"` cells and no way back in with Tab. Header
|
|
5282
|
+
// rows genuinely have no ROW-axis window (headers are not row-virtualized), so the row half
|
|
5283
|
+
// stays body-only; only the column half widens.
|
|
4570
5284
|
const rowOut = this.rowsWindowed() && this.virtualizer && this.rowIsOutsideWindow(r);
|
|
4571
5285
|
const colOut = this.colsWindowed() && this.colVirtualizer && this.colIsOutsideWindow(c);
|
|
4572
|
-
|
|
4573
|
-
|
|
5286
|
+
const rowScroll = !header && rowOut;
|
|
5287
|
+
if (rowScroll || colOut) {
|
|
5288
|
+
if (rowScroll) this.virtualizer.scrollToIndex(r, {
|
|
4574
5289
|
align: 'center'
|
|
4575
5290
|
});
|
|
4576
5291
|
if (colOut) this.colVirtualizer.scrollToIndex(c, {
|
|
@@ -4594,7 +5309,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4594
5309
|
// A newer focus intent superseded this poll — abort WITHOUT focusing (the user has since
|
|
4595
5310
|
// navigated / clicked elsewhere; re-focusing this off-window target would yank focus back).
|
|
4596
5311
|
if (this.focusIntentEpoch !== myEpoch) return;
|
|
4597
|
-
|
|
5312
|
+
// B-03: resolve with the SAME key the synchronous path below uses, so the poll can
|
|
5313
|
+
// find a header cell. `String(r)` alone addresses body rows only.
|
|
5314
|
+
const el = this.resolveCellEl(header ? '__header' : String(r), c, header ? lvl : null);
|
|
4598
5315
|
if (el) {
|
|
4599
5316
|
el.focus();
|
|
4600
5317
|
return;
|
|
@@ -4626,8 +5343,23 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4626
5343
|
// HTMLElement ARIA-reflected property (`Element.ariaRowCount: string`), so a same-named method
|
|
4627
5344
|
// becomes a class field that shadows it on Lit → TS2416 cascades to EVERY @property decorator
|
|
4628
5345
|
// (the `valueOf`/`nodeType` inherited-DOM-member collision class, authoring playbook §6).
|
|
5346
|
+
// A-04: under `manual` the filtered row model is NOT the total — table-core holds only the
|
|
5347
|
+
// page the consumer handed it, so getFilteredRowModel().rows.length is the PAGE SIZE. The
|
|
5348
|
+
// server-side total lives in the props the consumer passes for exactly this purpose
|
|
5349
|
+
// (`rowCount`, else `pageCount`), which is also table-core's own precedence for getPageCount()
|
|
5350
|
+
// (explicit pageCount wins, else ⌈rowCount / pageSize⌉). Taking them here keeps aria-rowcount
|
|
5351
|
+
// consistent with bodyAriaRowIndex, which already offsets by pageIndex * pageSize and would
|
|
5352
|
+
// otherwise hand a screen reader an index LARGER than the advertised count on any page past
|
|
5353
|
+
// the first. `pageCount * pageSize` is an upper bound (the last page is usually partial) —
|
|
5354
|
+
// an over-count is the safe direction: an aria-rowindex can never exceed it.
|
|
5355
|
+
// With `manual` and NEITHER count set, table-core pins getCanNextPage() to false, so pageIndex
|
|
5356
|
+
// stays 0, the offset is 0 and page-slice-as-total is self-consistent; we leave it alone.
|
|
4629
5357
|
totalRowCount = () => {
|
|
4630
5358
|
if (!this.table) return (this._rows.value || []).length;
|
|
5359
|
+
if (this.manual === true) {
|
|
5360
|
+
if (this.rowCount != null) return this.rowCount;
|
|
5361
|
+
if (this.pageCount != null) return this.pageCount * this.pageSize();
|
|
5362
|
+
}
|
|
4631
5363
|
const fm = this.table.getFilteredRowModel();
|
|
4632
5364
|
return fm && fm.rows ? fm.rows.length : (this._rows.value || []).length;
|
|
4633
5365
|
};
|
|
@@ -4661,6 +5393,25 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4661
5393
|
// `headerRowCount() + wr.vi.index + 1` inline (wr.vi.index is already the absolute full-model index).
|
|
4662
5394
|
bodyAriaRowIndex = (row: any) => this.headerRowCount() + this.rowIndexOf(row) + this.ariaPageOffset() + 1;
|
|
4663
5395
|
|
|
5396
|
+
// ── E-05: the COLUMN-axis twin of gridAriaRowCount / bodyAriaRowIndex ────────────────
|
|
5397
|
+
// WAI-ARIA: when aria-colcount is set, every cell must carry an aria-colindex and the count
|
|
5398
|
+
// must equal the largest index any cell declares. The row axis has had this since #13; the
|
|
5399
|
+
// column axis had NOTHING repo-wide (0 occurrences of aria-colcount/aria-colindex), which
|
|
5400
|
+
// matters most under column windowing — the rendered <td>s are a WINDOW, so without an
|
|
5401
|
+
// explicit index a screen reader counts the window and announces "column 3 of 6" while the
|
|
5402
|
+
// user sits in absolute column 41.
|
|
5403
|
+
//
|
|
5404
|
+
// The index space is `visibleCellsFor(row)` / the header group's leaf headers — the same one
|
|
5405
|
+
// `colIndexOf` and `data-col-index` already use, INCLUDING the auto-injected select/expander
|
|
5406
|
+
// chrome columns (they are real columns in that list). `visibleColCount()` counts the same
|
|
5407
|
+
// list, so count and indices are mutually consistent by construction.
|
|
5408
|
+
//
|
|
5409
|
+
// NB named gridAriaColCount, NOT ariaColCount: `Element.ariaColCount` / `.ariaColIndex` are
|
|
5410
|
+
// inherited ARIA-reflected properties, so a same-named method becomes a Lit class field that
|
|
5411
|
+
// shadows them → TS2416 cascading to every @property decorator. Exactly the rename discipline
|
|
5412
|
+
// totalRowCount / gridAriaRowCount / bodyAriaRowIndex already document above.
|
|
5413
|
+
gridAriaColCount = () => this.visibleColCount();
|
|
5414
|
+
|
|
4664
5415
|
// Column count = the visible cell list length (uniform header+body in a flat grid). Reads
|
|
4665
5416
|
// $data.rows (reactive) so it is fine-grained-correct on Solid/Lit; falls back to the
|
|
4666
5417
|
// header leaf count when there are no body rows.
|
|
@@ -4921,6 +5672,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
4921
5672
|
// Enter/F2 → enter interaction mode: focus the active cell's FIRST interactive control
|
|
4922
5673
|
// (D-07 — uniform for header sort buttons and body controls; Enter does NOT sort directly).
|
|
4923
5674
|
// No-op (stay in navigation mode) if the cell has no focusable control.
|
|
5675
|
+
// True when the active cell holds its own focusable controls (a link, a button, a drop-in
|
|
5676
|
+
// widget). Enter then ENTERS them (enterControl) rather than activating the row (row-activate).
|
|
5677
|
+
activeCellHasControls = () => focusables(this.currentCellEl()).length > 0;
|
|
5678
|
+
|
|
4924
5679
|
enterControl = () => {
|
|
4925
5680
|
const cellEl = this.currentCellEl();
|
|
4926
5681
|
const list = focusables(cellEl);
|
|
@@ -5013,7 +5768,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5013
5768
|
// nextRow/nextCol locals and FALLS THROUGH to the shared focus seam (like Ctrl+Home/End). ──
|
|
5014
5769
|
if ((e.ctrlKey || e.metaKey) && e.shiftKey && !this._activeIsHeader.value && (key === 'ArrowUp' || key === 'ArrowDown' || key === 'ArrowLeft' || key === 'ArrowRight')) {
|
|
5015
5770
|
e.preventDefault();
|
|
5016
|
-
|
|
5771
|
+
// B-04: measured from the range's FOCUS corner, which is what extendRange moves — not
|
|
5772
|
+
// from the active cell, which a drag-select or Ctrl+A leaves somewhere else entirely.
|
|
5773
|
+
if (key === 'ArrowUp') this.extendRange(-this.rangeFocusRow(), 0);else if (key === 'ArrowDown') this.extendRange(this.bodyRowCount() - 1 - this.rangeFocusRow(), 0);else if (key === 'ArrowLeft') this.extendRange(0, -this.rangeFocusCol());else this.extendRange(0, this.visibleColCount() - 1 - this.rangeFocusCol());
|
|
5017
5774
|
return;
|
|
5018
5775
|
} else if ((e.ctrlKey || e.metaKey) && !this._activeIsHeader.value && (key === 'ArrowUp' || key === 'ArrowDown' || key === 'ArrowLeft' || key === 'ArrowRight')) {
|
|
5019
5776
|
e.preventDefault();
|
|
@@ -5111,11 +5868,17 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5111
5868
|
// gated by clipboardActiveAllowed() (== !activeIsHeader) so a header-active Ctrl+C/Ctrl+V
|
|
5112
5869
|
// falls through to NATIVE behavior — never preventDefault'd, never a silent body mutation
|
|
5113
5870
|
// (copyRange/pasteRange also self-guard; the verb guard is what plan 63-09's Cut reuses). ──
|
|
5114
|
-
|
|
5871
|
+
// B-07: `clipboardWriteAvailable()` / `clipboardReadAvailable()` are part of the BRANCH
|
|
5872
|
+
// CONDITION, not a guard inside the verb. preventDefault ran first and copyRange/pasteRange
|
|
5873
|
+
// then returned early when `navigator.clipboard` was absent — an insecure `http://` origin, a
|
|
5874
|
+
// permissions policy, an older embedded webview — so the browser's own copy/paste was
|
|
5875
|
+
// suppressed and nothing replaced it: Ctrl+C did nothing whatsoever. Falling through leaves
|
|
5876
|
+
// the native default in charge, which is the only correct behaviour when we cannot act.
|
|
5877
|
+
else if ((key === 'c' || key === 'C') && (e.ctrlKey || e.metaKey) && this.clipboardActiveAllowed() && this.clipboardWriteAvailable()) {
|
|
5115
5878
|
e.preventDefault();
|
|
5116
5879
|
this.copyRange();
|
|
5117
5880
|
return;
|
|
5118
|
-
} else if ((key === 'v' || key === 'V') && (e.ctrlKey || e.metaKey) && this.clipboardActiveAllowed()) {
|
|
5881
|
+
} else if ((key === 'v' || key === 'V') && (e.ctrlKey || e.metaKey) && this.clipboardActiveAllowed() && this.clipboardReadAvailable()) {
|
|
5119
5882
|
e.preventDefault();
|
|
5120
5883
|
this.pasteRange();
|
|
5121
5884
|
return;
|
|
@@ -5125,7 +5888,9 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5125
5888
|
// Ctrl+C/Ctrl+V (clipboardActiveAllowed) so a header-active Ctrl+X falls through to NATIVE cut
|
|
5126
5889
|
// and never silently clears a body cell (cutRange also self-guards). Placed beside the C/V
|
|
5127
5890
|
// shortcuts, BEFORE the printable-key edit-entry branch (which excludes ctrl/meta). ──
|
|
5128
|
-
|
|
5891
|
+
// B-07 applies to Cut too, and gating it on WRITE availability is deliberate: a cut that
|
|
5892
|
+
// cannot copy is data destruction with nothing on the clipboard to paste back.
|
|
5893
|
+
else if ((key === 'x' || key === 'X') && (e.ctrlKey || e.metaKey) && this.clipboardActiveAllowed() && this.clipboardWriteAvailable()) {
|
|
5129
5894
|
e.preventDefault();
|
|
5130
5895
|
this.cutRange();
|
|
5131
5896
|
return;
|
|
@@ -5177,6 +5942,44 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5177
5942
|
if (!this._activeIsHeader.value) this.selectAllBody();
|
|
5178
5943
|
return;
|
|
5179
5944
|
}
|
|
5945
|
+
// ── B-15: the APG grid pattern's row/column selection keys, and Escape in NAVIGATION mode.
|
|
5946
|
+
// All three were simply absent. They are placed HERE, before every `key === ' '` branch
|
|
5947
|
+
// below, because the boolean in-place toggle matches a bare `' '` without excluding
|
|
5948
|
+
// modifiers — so a Shift+Space on a checkbox cell would have toggled the checkbox instead
|
|
5949
|
+
// of selecting the row, and Ctrl+Space likewise.
|
|
5950
|
+
//
|
|
5951
|
+
// Shift+Space selects the ROW (table-core's own rowSelection slice, through the row's
|
|
5952
|
+
// toggleSelected — the same funnel the per-row checkbox uses, so the change event and the
|
|
5953
|
+
// two-way model behave identically). A no-op when selectionMode is 'none', which is the
|
|
5954
|
+
// honest behaviour: there is no row selection to make.
|
|
5955
|
+
//
|
|
5956
|
+
// Ctrl+Space selects the COLUMN as a full-height CELL RANGE rather than inventing a third
|
|
5957
|
+
// selection concept — the same corners Ctrl+A and shift+arrow drive, so Ctrl+C / Ctrl+X /
|
|
5958
|
+
// Delete / the fill handle all act on it unchanged.
|
|
5959
|
+
//
|
|
5960
|
+
// Escape in navigation mode collapses the range. In-control mode already consumed Escape at
|
|
5961
|
+
// the top of this handler; in navigation mode it did nothing at all, leaving a user who had
|
|
5962
|
+
// shift-arrowed out a rectangle with no keyboard way to drop it short of an arrow key, which
|
|
5963
|
+
// also MOVES. Only preventDefault'd when there is a range to clear, so a consumer's own
|
|
5964
|
+
// Escape handling on a table with no selection is untouched.
|
|
5965
|
+
else if (key === ' ' && e.shiftKey && !e.ctrlKey && !e.metaKey && !this._activeIsHeader.value) {
|
|
5966
|
+
e.preventDefault();
|
|
5967
|
+
if (this.selectionMode !== 'none') {
|
|
5968
|
+
const selRow = (this._rows.value || [])[this._activeRow.value];
|
|
5969
|
+
if (selRow && selRow.toggleSelected) selRow.toggleSelected(!this.rowIsSelected(selRow));
|
|
5970
|
+
}
|
|
5971
|
+
return;
|
|
5972
|
+
} else if (key === ' ' && (e.ctrlKey || e.metaKey) && !e.shiftKey && !this._activeIsHeader.value) {
|
|
5973
|
+
e.preventDefault();
|
|
5974
|
+
this.selectActiveColumn();
|
|
5975
|
+
return;
|
|
5976
|
+
} else if (key === 'Escape') {
|
|
5977
|
+
if (this.rangeActive || this._rangeAnchor.value != null) {
|
|
5978
|
+
e.preventDefault();
|
|
5979
|
+
this.clearRange();
|
|
5980
|
+
}
|
|
5981
|
+
return;
|
|
5982
|
+
}
|
|
5180
5983
|
// ── Full-row edit entry (phase 51 req-6 / D-06) — Shift+F2 on an editable active cell puts
|
|
5181
5984
|
// EVERY editable cell in the active row into edit at once. Tested BEFORE the plain F2 branch
|
|
5182
5985
|
// (a Shift+F2 must NOT fall through to single-cell F2). Shift+F2 was chosen for the lowest
|
|
@@ -5256,6 +6059,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5256
6059
|
// rAF poll must not steal focus back after the user has already ArrowDown'd to another row.
|
|
5257
6060
|
this.recoverGridFocus(String(grpRow), grpCol, null, true);
|
|
5258
6061
|
return;
|
|
6062
|
+
}
|
|
6063
|
+
// ── row-activate (quick 260925-dtl): Enter on a NON-editable body cell with no controls of
|
|
6064
|
+
// its own opens the row (a list-style "open this item"). Reached only after every edit-entry
|
|
6065
|
+
// and group-toggle branch above has declined; a cell WITH controls keeps entering them.
|
|
6066
|
+
else if (key === 'Enter' && !this._activeIsHeader.value && !this.activeCellHasControls()) {
|
|
6067
|
+
e.preventDefault();
|
|
6068
|
+
this.activateRowAt(this._activeRow.value, 'keyboard');
|
|
6069
|
+
return;
|
|
5259
6070
|
} else if (key === 'Enter' || key === 'F2') {
|
|
5260
6071
|
e.preventDefault();
|
|
5261
6072
|
this.enterControl();
|
|
@@ -5354,11 +6165,18 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5354
6165
|
// focusin that follows a Shift+Click (rangeClickPending): @mousedown already set the range
|
|
5355
6166
|
// BEFORE this focusin fires, and a focusin carries no reliable shiftKey, so the @mousedown
|
|
5356
6167
|
// path owns the shift case and flags it here so the collapse is skipped.
|
|
6168
|
+
// B-11: `rangeClickPending` carries the CELL the Shift+Click landed on, not a bare boolean.
|
|
6169
|
+
// A boolean leaks: Shift+Click on the cell that ALREADY holds focus fires no focusin at all,
|
|
6170
|
+
// so nothing consumed the flag, and the next ordinary navigation click (or Tab) hit this
|
|
6171
|
+
// branch instead and skipped its collapse — leaving a stale rectangle painted while the
|
|
6172
|
+
// active cell moved away. Matching on the coordinates makes a leak self-healing: a focusin on
|
|
6173
|
+
// any OTHER cell falls through to clearRange() and clears the flag on the way past.
|
|
5357
6174
|
if (this.rangeTransition) {
|
|
5358
6175
|
this.rangeTransition = false;
|
|
5359
|
-
} else if (this.rangeClickPending) {
|
|
5360
|
-
this.rangeClickPending =
|
|
6176
|
+
} else if (this.rangeClickPending && this.rangeClickPending.isHeader === isHeader && this.rangeClickPending.r === movedRow && this.rangeClickPending.c === col) {
|
|
6177
|
+
this.rangeClickPending = null;
|
|
5361
6178
|
} else {
|
|
6179
|
+
this.rangeClickPending = null;
|
|
5362
6180
|
this.clearRange();
|
|
5363
6181
|
}
|
|
5364
6182
|
// The cell box (not an inner control) receiving focus = navigation mode.
|
|
@@ -5396,13 +6214,26 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5396
6214
|
this._activeIsHeader.value = false;
|
|
5397
6215
|
this._activeRow.value = row;
|
|
5398
6216
|
this._activeColIndex.value = col;
|
|
5399
|
-
|
|
6217
|
+
// B-11: arm the skip for THIS cell only (see syncActiveFromEvent). Body cell → isHeader
|
|
6218
|
+
// false; the header path never reaches here (rowAttr === '__header' returned above).
|
|
6219
|
+
this.rangeClickPending = {
|
|
6220
|
+
isHeader: false,
|
|
6221
|
+
r: row,
|
|
6222
|
+
c: col
|
|
6223
|
+
};
|
|
5400
6224
|
return;
|
|
5401
6225
|
}
|
|
5402
|
-
//
|
|
5403
|
-
//
|
|
5404
|
-
//
|
|
5405
|
-
//
|
|
6226
|
+
// B-16: a mousedown INSIDE the cell that is currently in edit belongs to the editor — it is
|
|
6227
|
+
// the user placing a caret or drag-selecting TEXT. Starting a cell drag-select from it paints
|
|
6228
|
+
// a grid range over the very cell being typed into, and (with `undoable`) a drag that leaves
|
|
6229
|
+
// the cell then lets Delete/Ctrl+X act on a rectangle the user was never selecting. The
|
|
6230
|
+
// editor owns the gesture; return before any document listener is attached.
|
|
6231
|
+
if (this.isEditing(row, col)) return;
|
|
6232
|
+
// §6 plain mousedown → begin a document-level drag-select anchored at this cell. The anchor is
|
|
6233
|
+
// passed EXPLICITLY into setRangeFocus by beginRangeDrag (B-01) rather than seeded from the
|
|
6234
|
+
// active cell, which is still the PRE-mousedown one at this point (native focus is a default
|
|
6235
|
+
// action that runs after this handler). A mousedown with no move collapses to a single active
|
|
6236
|
+
// cell (no range).
|
|
5406
6237
|
this.beginRangeDrag(row, col);
|
|
5407
6238
|
};
|
|
5408
6239
|
|
|
@@ -5459,7 +6290,57 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5459
6290
|
// editor). A plain click on an EDITABLE body cell opens its editor via the SAME beginEdit funnel;
|
|
5460
6291
|
// shift+click (range extend) and non-editable cells are unaffected. Same closest/parse/header-skip
|
|
5461
6292
|
// /finite guards as onGridDblClick. isGrid()-gated so 'table' mode never runs it.
|
|
6293
|
+
// ── row-activate (quick 260925-dtl, oinbox dogfooding) ─────────────────────────────────────
|
|
6294
|
+
// `row-activate { row, index, trigger }`: `row` = the original data object, `index` = its
|
|
6295
|
+
// position in the rendered model ($data.rows — page-relative, or the full model when virtual).
|
|
6296
|
+
// Group-header rows never activate; detail rows carry no data-row so a click there never
|
|
6297
|
+
// resolves a row.
|
|
6298
|
+
activateRowAt = (index: any, trigger: any) => {
|
|
6299
|
+
const row = (this._rows.value || [])[index];
|
|
6300
|
+
if (!row || this.rowIsGrouped(row) || this.rowIsLazyPlaceholder(row)) return;
|
|
6301
|
+
this.dispatchEvent(new CustomEvent("row-activate", {
|
|
6302
|
+
detail: {
|
|
6303
|
+
row: row.original,
|
|
6304
|
+
index,
|
|
6305
|
+
trigger
|
|
6306
|
+
},
|
|
6307
|
+
bubbles: true,
|
|
6308
|
+
composed: true
|
|
6309
|
+
}));
|
|
6310
|
+
};
|
|
6311
|
+
|
|
6312
|
+
// Controls a click must not activate THROUGH — the selection checkbox, the expander, links,
|
|
6313
|
+
// drop-in widgets and an open editor. Matched only INSIDE the clicked cell, so a host page that
|
|
6314
|
+
// happens to wrap the table in one of these does not silence every row.
|
|
6315
|
+
ROW_ACTIVATE_CONTROLS = 'input, button, a[href], select, textarea, label, summary, [contenteditable=""], [contenteditable="true"], [role="button"], [role="checkbox"], [role="link"], [data-editing-cell]';
|
|
6316
|
+
|
|
6317
|
+
// Both interaction modes: a plain click on a body data cell, outside any control. Not a
|
|
6318
|
+
// modified click (shift/ctrl/meta are selection gestures), not the end of a drag-select, and
|
|
6319
|
+
// not a singleClickEdit gesture on an editable grid cell (that click opens the editor instead).
|
|
6320
|
+
maybeActivateRowFromClick = (e: any) => {
|
|
6321
|
+
if (!e || e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
6322
|
+
if (this.rangeDragMoved) return;
|
|
6323
|
+
const tgt = e.target;
|
|
6324
|
+
if (!tgt || !tgt.closest) return;
|
|
6325
|
+
const cellEl = tgt.closest('[data-row]');
|
|
6326
|
+
if (!cellEl) return;
|
|
6327
|
+
const control = tgt.closest(this.ROW_ACTIVATE_CONTROLS);
|
|
6328
|
+
if (control && cellEl.contains(control)) return;
|
|
6329
|
+
const rowAttr = cellEl.getAttribute('data-row');
|
|
6330
|
+
if (rowAttr == null || rowAttr === '__header') return;
|
|
6331
|
+
const index = parseInt(rowAttr, 10);
|
|
6332
|
+
if (!Number.isFinite(index)) return;
|
|
6333
|
+
if (this.isGrid() && this.singleClickEdit) {
|
|
6334
|
+
const colAttr = cellEl.getAttribute('data-col-index');
|
|
6335
|
+
const col = colAttr == null ? NaN : parseInt(colAttr, 10);
|
|
6336
|
+
const colId = Number.isFinite(col) ? this.columnIdAt(index, col) : null;
|
|
6337
|
+
if (colId != null && this.columnEditable(colId)) return;
|
|
6338
|
+
}
|
|
6339
|
+
this.activateRowAt(index, 'click');
|
|
6340
|
+
};
|
|
6341
|
+
|
|
5462
6342
|
onGridClick = (e: any) => {
|
|
6343
|
+
this.maybeActivateRowFromClick(e);
|
|
5463
6344
|
if (!this.isGrid() || !e) return;
|
|
5464
6345
|
if (!this.singleClickEdit) return;
|
|
5465
6346
|
if (e.shiftKey) return;
|
|
@@ -5655,12 +6536,26 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5655
6536
|
// UNCHANGED (span = the header's own colSpan, or 1) when columns are not windowed — the
|
|
5656
6537
|
// off-path the template's `:colspan="wh.span > 1 ? wh.span : null"` binding preserves
|
|
5657
6538
|
// byte-identically (a bare colspan="1" would change the emitted markup on the unwindowed path).
|
|
6539
|
+
//
|
|
6540
|
+
// A-03: the emitted entry also carries the WIDTH of the in-window leaves, because the colspan
|
|
6541
|
+
// clamp above is only half the job. `thStyle` declared `Header.getSize()` — the sum of ALL
|
|
6542
|
+
// descendant leaves — against a colspan clamped to the leaves actually rendered. Under
|
|
6543
|
+
// `table-layout: fixed` a first-row spanning cell's declared width is divided across the
|
|
6544
|
+
// columns it spans, and only the first row establishes widths, so a group straddling the window
|
|
6545
|
+
// edge inflated every column it still rendered: measured on "Group A" (cols 10-14 @150px,
|
|
6546
|
+
// getSize() 750), each rendered group column went 150 -> 187.5 -> 250 -> 375 -> 750px as the
|
|
6547
|
+
// group scrolled out, and scrollWidth with it, 9000 -> 9600. Identical on all five light-DOM
|
|
6548
|
+
// targets. This is the SAME getSize()-vs-colspan mismatch the earlier Column.getSize() ->
|
|
6549
|
+
// Header.getSize() fix cured for the fully-in-window case, inverted: that one was a 5x
|
|
6550
|
+
// COLLAPSE, this is a 5x EXPANSION for the partial case. Summing the same leaves the clamp
|
|
6551
|
+
// counted keeps the declared width and the colspan describing the same set by construction.
|
|
5658
6552
|
windowedHeadersFor = (hg: any, hgLevel: any) => {
|
|
5659
6553
|
const headers = hg && hg.headers || [];
|
|
5660
6554
|
if (!this.colsWindowed()) {
|
|
5661
6555
|
return headers.map((h: any) => ({
|
|
5662
6556
|
header: h,
|
|
5663
|
-
span: h && h.colSpan > 1 ? h.colSpan : 1
|
|
6557
|
+
span: h && h.colSpan > 1 ? h.colSpan : 1,
|
|
6558
|
+
width: null
|
|
5664
6559
|
}));
|
|
5665
6560
|
}
|
|
5666
6561
|
const idx = this.windowedColIndices();
|
|
@@ -5675,10 +6570,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5675
6570
|
const end = cursor + leafCount;
|
|
5676
6571
|
cursor = end;
|
|
5677
6572
|
let span = 0;
|
|
5678
|
-
|
|
6573
|
+
let width = 0;
|
|
6574
|
+
for (let c = start; c < end; c++) if (idxSet[c]) {
|
|
6575
|
+
span = span + 1;
|
|
6576
|
+
width = width + this.columnSize(c);
|
|
6577
|
+
}
|
|
5679
6578
|
if (span > 0) out.push({
|
|
5680
6579
|
header: h,
|
|
5681
|
-
span
|
|
6580
|
+
span,
|
|
6581
|
+
width
|
|
5682
6582
|
});
|
|
5683
6583
|
}
|
|
5684
6584
|
return out;
|
|
@@ -5691,6 +6591,37 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5691
6591
|
// (byte-identical).
|
|
5692
6592
|
windowedColSpan = () => this.colsWindowed() ? this.windowedColIndices().length + 2 : this.visibleColCount();
|
|
5693
6593
|
|
|
6594
|
+
// ── A-02: a column RESIZE must invalidate the column virtualizer's measurement cache ──────
|
|
6595
|
+
// `setOptions` is not enough, and this is a property of virtual-core, not of how we call it.
|
|
6596
|
+
// `getMeasurements()` is memoized on `[getMeasurementOptions(), itemSizeCacheVersion]`, and
|
|
6597
|
+
// `getMeasurementOptions()` is itself memoized on count / paddingStart / scrollMargin /
|
|
6598
|
+
// getItemKey / enabled / lanes / laneAssignmentMode — `estimateSize` is NOT among them. The
|
|
6599
|
+
// column axis feeds its widths ENTIRELY through `estimateSize: (i) => columnSize(i)` (it never
|
|
6600
|
+
// calls `measureElement`), so after a resize the memo returns the array built from the OLD
|
|
6601
|
+
// widths: measured on a 60x150px grid, dragging one column +1200px left `scrollWidth`
|
|
6602
|
+
// oscillating 9000 <-> 10200 as that column entered and left the window, and at scrollLeft 6000
|
|
6603
|
+
// the window started at index 37 where fresh geometry predicts 29. `measure()` is the only API
|
|
6604
|
+
// that bumps `itemSizeCacheVersion`, and it appeared ZERO times in this package or in
|
|
6605
|
+
// headless-core. Same defect for `resetColumnSizing()` and any programmatic `columnSizing` write.
|
|
6606
|
+
//
|
|
6607
|
+
// Guarded on a SIGNATURE rather than called unconditionally. `columnResizeMode` is 'onChange',
|
|
6608
|
+
// so `writeColumnSizing` fires on every pointermove of a drag; `measure()` clears the size cache
|
|
6609
|
+
// and calls `notify(false)`, which bumps `$data.windowVer` and therefore re-renders. Hashing the
|
|
6610
|
+
// widths (order-sensitive, so a pure A<->B swap that preserves the sum still trips it) makes the
|
|
6611
|
+
// steady state a ~N-integer-op no-op and guarantees the notify cannot feed itself: the second
|
|
6612
|
+
// pass sees an unchanged signature and returns.
|
|
6613
|
+
lastColSizeSig = 0;
|
|
6614
|
+
|
|
6615
|
+
remeasureColumnSizes = () => {
|
|
6616
|
+
if (!this.colsWindowed() || !this.colVirtualizer || !this.colVirtualizer.measure) return;
|
|
6617
|
+
const n = this.columnCount();
|
|
6618
|
+
let sig = n;
|
|
6619
|
+
for (let i = 0; i < n; i++) sig = Math.imul(sig, 31) + this.columnSize(i) | 0;
|
|
6620
|
+
if (sig === this.lastColSizeSig) return;
|
|
6621
|
+
this.lastColSizeSig = sig;
|
|
6622
|
+
this.colVirtualizer.measure();
|
|
6623
|
+
};
|
|
6624
|
+
|
|
5694
6625
|
// remeasureColumnWindow() (Phase 87 87-04): push a fresh column count/size into the SECOND
|
|
5695
6626
|
// (horizontal) virtualizer on every row-model refresh — mirrors the row axis's
|
|
5696
6627
|
// virtualizer.setOptions(virtualizerOptions()) + _willUpdate() re-feed in refreshRowModel, so a
|
|
@@ -5704,6 +6635,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5704
6635
|
remeasureColumnWindow = () => {
|
|
5705
6636
|
if (!this.colsWindowed() || !this.colVirtualizer) return;
|
|
5706
6637
|
this.colVirtualizer.setOptions(this.columnVirtualizerOptions());
|
|
6638
|
+
this.remeasureColumnSizes();
|
|
5707
6639
|
this.colVirtualizer._willUpdate();
|
|
5708
6640
|
};
|
|
5709
6641
|
|
|
@@ -5740,8 +6672,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5740
6672
|
|
|
5741
6673
|
// rangeClickPending: set by onGridMouseDown on a Shift+Click (the range is set off the
|
|
5742
6674
|
// pointer event's shiftKey BEFORE the cell's focusin fires); the follow-up focusin reads it
|
|
5743
|
-
// to SKIP the range-collapse (a focusin carries no reliable shiftKey).
|
|
5744
|
-
|
|
6675
|
+
// to SKIP the range-collapse (a focusin carries no reliable shiftKey).
|
|
6676
|
+
// B-11: it holds the CELL `{ isHeader, r, c }` the Shift+Click landed on, not a boolean, and
|
|
6677
|
+
// is null when idle. Shift+Click on the already-focused cell fires no focusin, so a boolean
|
|
6678
|
+
// was left armed and the NEXT ordinary click/Tab consumed it and skipped its own collapse,
|
|
6679
|
+
// stranding a painted rectangle. Coordinate-matched, that leak is self-healing: a focusin on
|
|
6680
|
+
// any other cell clears it and collapses normally. Cleared on BOTH branches of the consume.
|
|
6681
|
+
rangeClickPending: any = null;
|
|
5745
6682
|
|
|
5746
6683
|
// B19: a SYNCHRONOUS mirror of "a range currently exists" — extendRange/setRangeFocus set it
|
|
5747
6684
|
// true, clearRange/clampRange-to-empty set it false. clearRange is invoked TWICE in one plain-
|
|
@@ -5812,7 +6749,27 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5812
6749
|
// pre-write value (ROZ138). extendRange/setRangeFocus thread the just-computed locals through
|
|
5813
6750
|
// here so the emitted payload matches the write. The single call site keeps the count
|
|
5814
6751
|
// predictable (React multi-emit dedup, D-07). One-way notification.
|
|
6752
|
+
// B-10 — announce the selection alongside the emit. This is the SINGLE site every range
|
|
6753
|
+
// mutation funnels through (shift+arrow via extendRange, shift+click and drag-select and
|
|
6754
|
+
// fill-drag via setRangeFocus, Ctrl+A via selectAllBody), so one announce here covers all of
|
|
6755
|
+
// them; guarding the callers individually would be five chances to miss one.
|
|
6756
|
+
//
|
|
6757
|
+
// Writes $data.rangeAnnounce directly rather than importing clipboardFill's announce(): that
|
|
6758
|
+
// helper drives pasteAnnounce, and a range move must not overwrite a paste/validation message
|
|
6759
|
+
// the user has not heard yet (hence the separate key + region). `polite` + `aria-atomic` means
|
|
6760
|
+
// a fast drag replaces the pending text instead of queueing every intermediate rectangle.
|
|
6761
|
+
rangeSummary = (anchor: any, focus: any) => {
|
|
6762
|
+
if (!anchor || !focus) return '';
|
|
6763
|
+
const rows = Math.abs(focus.rowIndex - anchor.rowIndex) + 1;
|
|
6764
|
+
const cols = Math.abs(focus.colIndex - anchor.colIndex) + 1;
|
|
6765
|
+
if (rows === 1 && cols === 1) return '1 cell selected';
|
|
6766
|
+
const rowPart = rows + (rows === 1 ? ' row' : ' rows');
|
|
6767
|
+
const colPart = cols + (cols === 1 ? ' column' : ' columns');
|
|
6768
|
+
return rowPart + ' by ' + colPart + ' selected, ' + rows * cols + ' cells';
|
|
6769
|
+
};
|
|
6770
|
+
|
|
5815
6771
|
emitRangeChange = (anchor: any, focus: any) => {
|
|
6772
|
+
this._rangeAnnounce.value = this.rangeSummary(anchor, focus);
|
|
5816
6773
|
this.dispatchEvent(new CustomEvent("range-change", {
|
|
5817
6774
|
detail: {
|
|
5818
6775
|
anchor,
|
|
@@ -5872,14 +6829,39 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5872
6829
|
}
|
|
5873
6830
|
};
|
|
5874
6831
|
|
|
5875
|
-
// setRangeFocus(rIdx, cIdx): set the moving corner to an explicit cell
|
|
5876
|
-
//
|
|
5877
|
-
// Emits range-change from this single site.
|
|
5878
|
-
|
|
6832
|
+
// setRangeFocus(rIdx, cIdx, anchorR, anchorC): set the moving corner to an explicit cell
|
|
6833
|
+
// (Shift+Click, or a drag-select's pointermove), seeding the anchor when no range exists yet.
|
|
6834
|
+
// Clamped to bounds. Emits range-change from this single site.
|
|
6835
|
+
//
|
|
6836
|
+
// B-01: the anchor seed is EXPLICIT when the caller knows it. Seeding it from
|
|
6837
|
+
// `$data.activeRow`/`$data.activeColIndex` is only correct if that state is live at the moment
|
|
6838
|
+
// the seed happens — and inside a drag-select it is not on React, where the document
|
|
6839
|
+
// pointermove closure captures the render-scoped consts from the render that ran BEFORE the
|
|
6840
|
+
// mousedown. Native focus moves as mousedown's DEFAULT ACTION, after handlers return, so at
|
|
6841
|
+
// `beginRangeDrag` time the active cell is still the PRE-mousedown one and React can never
|
|
6842
|
+
// show that closure a newer value. Dragging (5,2)→(7,3) on a fresh grid anchored at (0,0) and
|
|
6843
|
+
// selected the whole rectangle from there — which Ctrl+X and Delete then destroy.
|
|
6844
|
+
// The other five targets read `$data.activeRow` LIVE inside `move` (by then focusin has synced
|
|
6845
|
+
// it), so they were accidentally correct; taking the anchor from the caller removes the read
|
|
6846
|
+
// entirely and makes all six correct for the same reason. Falls back to the active cell when no
|
|
6847
|
+
// explicit anchor is passed — Shift+Click, where "the anchor is where you already were" IS the
|
|
6848
|
+
// active cell and the read happens synchronously in the same handler.
|
|
6849
|
+
// The two anchor params carry EXPLICIT `= null` defaults, and that is load-bearing, not style:
|
|
6850
|
+
// the emitted per-target signature types every declared param as REQUIRED, so the Shift+Click
|
|
6851
|
+
// call site's two-argument `setRangeFocus(row, col)` fails the strict leaf typecheck with
|
|
6852
|
+
// TS2554 "Expected 4 arguments, but got 2" on react and solid. Same rule the `focusActiveCell`
|
|
6853
|
+
// comment in gridKeydownHandlers records for its own optional params.
|
|
6854
|
+
setRangeFocus = (rIdx: any, cIdx: any, anchorR = null, anchorC = null) => {
|
|
5879
6855
|
const maxRow = this.bodyRowCount() - 1;
|
|
5880
6856
|
const maxCol = this.visibleColCount() - 1;
|
|
5881
6857
|
if (maxRow < 0 || maxCol < 0) return;
|
|
5882
6858
|
let anchor = this._rangeAnchor.value;
|
|
6859
|
+
if (!anchor && anchorR != null && anchorC != null) {
|
|
6860
|
+
anchor = {
|
|
6861
|
+
rowIndex: clamp(Math.trunc(Number(anchorR)) || 0, 0, maxRow),
|
|
6862
|
+
colIndex: clamp(Math.trunc(Number(anchorC)) || 0, 0, maxCol)
|
|
6863
|
+
};
|
|
6864
|
+
}
|
|
5883
6865
|
if (!anchor) anchor = {
|
|
5884
6866
|
rowIndex: this._activeRow.value,
|
|
5885
6867
|
colIndex: this._activeColIndex.value
|
|
@@ -5920,6 +6902,54 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
5920
6902
|
this.emitRangeChange(anchor, focus);
|
|
5921
6903
|
};
|
|
5922
6904
|
|
|
6905
|
+
// ── B-04: where does a range EXTENSION measure its delta from? ──────────────────────────
|
|
6906
|
+
// `extendRange(dRow, dCol)` applies its delta to the range's FOCUS corner, not to the active
|
|
6907
|
+
// cell. For shift+arrow the two always agree (extendRange writes the active cell to the new
|
|
6908
|
+
// focus on every step), but two paths deliberately move the focus corner WITHOUT syncing the
|
|
6909
|
+
// active cell: a drag-select (`setRangeFocus`, which leaves the active cell on the mousedown
|
|
6910
|
+
// anchor) and Ctrl+A (`selectAllBody`, which sets both corners outright). After either,
|
|
6911
|
+
// Ctrl/Cmd+Shift+Arrow computed "distance from the active cell to the edge" and then added it
|
|
6912
|
+
// to a DIFFERENT cell. Drag (5,2)->(7,3) then Ctrl+Shift+ArrowUp extended to row 2 instead of
|
|
6913
|
+
// row 0; Ctrl+Shift+ArrowLeft to column 1 instead of column 0 — an extend-to-edge that stops
|
|
6914
|
+
// short of the edge by exactly the gap between the two.
|
|
6915
|
+
//
|
|
6916
|
+
// These report the corner extendRange will actually move, so the delta and its target are the
|
|
6917
|
+
// same cell by construction. No range yet -> the active cell IS the focus corner.
|
|
6918
|
+
rangeFocusRow = () => {
|
|
6919
|
+
const f = this._rangeFocus.value;
|
|
6920
|
+
return f && f.rowIndex != null ? f.rowIndex : this._activeRow.value;
|
|
6921
|
+
};
|
|
6922
|
+
|
|
6923
|
+
rangeFocusCol = () => {
|
|
6924
|
+
const f = this._rangeFocus.value;
|
|
6925
|
+
return f && f.colIndex != null ? f.colIndex : this._activeColIndex.value;
|
|
6926
|
+
};
|
|
6927
|
+
|
|
6928
|
+
// B-15: the APG grid pattern's Ctrl+Space — select the COLUMN containing the focused cell.
|
|
6929
|
+
// Expressed as a full-height rectangle over the SAME range corners shift+arrow and Ctrl+A
|
|
6930
|
+
// drive, so Ctrl+C / Ctrl+X / Delete / the fill handle all act on it with no new machinery
|
|
6931
|
+
// and no second selection concept for a consumer to reason about. Emits through the single
|
|
6932
|
+
// emitRangeChange funnel (fresh corners, never a $data re-read). No-op on an empty grid, and
|
|
6933
|
+
// header-active is gated OUT by the caller, exactly as selectAllBody is.
|
|
6934
|
+
selectActiveColumn = () => {
|
|
6935
|
+
const maxRow = this.bodyRowCount() - 1;
|
|
6936
|
+
const maxCol = this.visibleColCount() - 1;
|
|
6937
|
+
if (maxRow < 0 || maxCol < 0) return;
|
|
6938
|
+
const c = clamp(Math.trunc(Number(this._activeColIndex.value)) || 0, 0, maxCol);
|
|
6939
|
+
const anchor = {
|
|
6940
|
+
rowIndex: 0,
|
|
6941
|
+
colIndex: c
|
|
6942
|
+
};
|
|
6943
|
+
const focus = {
|
|
6944
|
+
rowIndex: maxRow,
|
|
6945
|
+
colIndex: c
|
|
6946
|
+
};
|
|
6947
|
+
this._rangeAnchor.value = anchor;
|
|
6948
|
+
this._rangeFocus.value = focus;
|
|
6949
|
+
this.rangeActive = true;
|
|
6950
|
+
this.emitRangeChange(anchor, focus);
|
|
6951
|
+
};
|
|
6952
|
+
|
|
5923
6953
|
// clearRange(): drop the rectangle (a non-shift navigation / edit-entry collapses any
|
|
5924
6954
|
// range back to a single active cell). Cheap no-op when no range is set (the guard keeps a
|
|
5925
6955
|
// plain navigation with no active range from emitting). B19: when a range DID exist, emit
|
|
@@ -6054,6 +7084,18 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6054
7084
|
|
|
6055
7085
|
// copyRange(): write the current range as TSV to the clipboard (async). No-op when the
|
|
6056
7086
|
// async Clipboard API is unavailable (older/insecure contexts) — a copy is best-effort.
|
|
7087
|
+
// B-07: is the async Clipboard API actually usable? Ctrl+C / Ctrl+X / Ctrl+V used to
|
|
7088
|
+
// `e.preventDefault()` FIRST and only then call a verb that self-guards on
|
|
7089
|
+
// `navigator.clipboard`, so on any page where the API is absent — an insecure `http://` origin,
|
|
7090
|
+
// a permissions policy, an older embedded webview — the browser's OWN copy/cut/paste was
|
|
7091
|
+
// suppressed and nothing took its place. The keystroke did nothing at all. These predicates
|
|
7092
|
+
// move the check into the keymap's branch CONDITION (gridKeydownHandlers.rzts), so an
|
|
7093
|
+
// unavailable API means the branch does not match and the native default runs. Separate
|
|
7094
|
+
// read/write predicates because a context can grant one and not the other.
|
|
7095
|
+
clipboardWriteAvailable = () => typeof navigator !== 'undefined' && !!navigator.clipboard && typeof navigator.clipboard.writeText === 'function';
|
|
7096
|
+
|
|
7097
|
+
clipboardReadAvailable = () => typeof navigator !== 'undefined' && !!navigator.clipboard && typeof navigator.clipboard.readText === 'function';
|
|
7098
|
+
|
|
6057
7099
|
copyRange = () => {
|
|
6058
7100
|
// B11: never copy from a header-active state (the reusable clipboard guard).
|
|
6059
7101
|
if (!this.clipboardActiveAllowed()) return;
|
|
@@ -6072,15 +7114,33 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6072
7114
|
// running fresh array (replaceRowValue) and record the committed cell. After the walk: ONE
|
|
6073
7115
|
// writeData (the single r-model:data write), ONE cell-edit-commit per COMMITTED cell, and the
|
|
6074
7116
|
// N-of-M aria-live announce. Returns { wrote, total }.
|
|
6075
|
-
|
|
7117
|
+
// B-09 — `verb` names the OPERATION in the aria-live summary. This funnel serves paste, cut,
|
|
7118
|
+
// range-delete AND fill, so hardcoding "pasted" made a Cut announce "3 of 3 cells pasted" and a
|
|
7119
|
+
// Delete announce the same — actively wrong for a screen-reader user, who has no other signal
|
|
7120
|
+
// that the destructive operation they invoked actually happened. Defaults to 'pasted' so the
|
|
7121
|
+
// paste path's wording is unchanged.
|
|
7122
|
+
applyGridToRange = (grid: any, originRow: any, originCol: any, verb: any) => {
|
|
7123
|
+
const opVerb = typeof verb === 'string' && verb !== '' ? verb : 'pasted';
|
|
6076
7124
|
const maxRow = this.bodyRowCount() - 1;
|
|
6077
7125
|
const maxCol = this.visibleColCount() - 1;
|
|
6078
7126
|
if (maxRow < 0 || maxCol < 0) return {
|
|
6079
7127
|
wrote: 0,
|
|
7128
|
+
changed: 0,
|
|
6080
7129
|
total: 0
|
|
6081
7130
|
};
|
|
7131
|
+
// B-08: `applied` and `committed` are DIFFERENT counts and used to be one.
|
|
7132
|
+
// applied — targets whose value now equals what was pasted, INCLUDING the ones that
|
|
7133
|
+
// already did. This is the announce's numerator: from the user's point of
|
|
7134
|
+
// view a cell that already held the pasted value was pasted successfully, and
|
|
7135
|
+
// reporting "3 of 5" for a 5-cell paste where 2 matched would be a lie.
|
|
7136
|
+
// committed — targets whose value actually CHANGED. This drives the write, the per-cell
|
|
7137
|
+
// `cell-edit-commit` emits and (through writeData) the undo step.
|
|
7138
|
+
// Conflating them meant an unchanged cell emitted an edit event and recorded an undo entry:
|
|
7139
|
+
// a 1->5 fill fired 5 commits when 4 cells already held the value, and a Delete over an
|
|
7140
|
+
// already-empty cell pushed an undo step that undoes nothing. Consumers persisting on
|
|
7141
|
+
// `cell-edit-commit` were issuing writes for data that had not moved.
|
|
6082
7142
|
let total = 0;
|
|
6083
|
-
let
|
|
7143
|
+
let applied = 0;
|
|
6084
7144
|
const committed = [];
|
|
6085
7145
|
// Build the fresh data array incrementally so the whole paste is ONE writeData.
|
|
6086
7146
|
let next = this.currentData();
|
|
@@ -6094,15 +7154,25 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6094
7154
|
// it to this single funnel, so guarding the four callers individually would be four
|
|
6095
7155
|
// chances to miss one.
|
|
6096
7156
|
const rowGrouped = this.rowIndexIsGrouped(r);
|
|
7157
|
+
// Debug 260926-dtl-edit-guard: a LAZY PLACEHOLDER row (virtual+manual+rowCount, a row not
|
|
7158
|
+
// loaded yet) has no real underlying record — rowOriginalAt(r) returns the placeholder
|
|
7159
|
+
// sentinel ({__rdtLazyPlaceholder:true,index:i}, virtualization.rzts), which is never IN
|
|
7160
|
+
// currentData() (only the consumer's own array is), so sourceIndexOfRow's data.indexOf(...)
|
|
7161
|
+
// is always -1 and falls back to the VISIBLE index — either a no-op write with a FABRICATED
|
|
7162
|
+
// cell-edit-commit, or, inside a sparse hole, the hole gets clobbered with a partial row via
|
|
7163
|
+
// replaceRowValue. Hoisted per-ROW exactly like rowGrouped above (the SAME single placement
|
|
7164
|
+
// closes pasteRange/cutRange/clearActiveRange/fillRange all at once).
|
|
7165
|
+
const rowPlaceholder = this.rowIsLazyPlaceholder((this._rows.value || [])[r]);
|
|
6097
7166
|
const cols = grid[gr] || [];
|
|
6098
7167
|
for (let gc = 0; gc < cols.length; gc++) {
|
|
6099
7168
|
const c = originCol + gc;
|
|
6100
7169
|
if (c > maxCol) break;
|
|
6101
|
-
// A group-row cell still counts toward `total` — it is a skipped
|
|
6102
|
-
// a non-editable or validator-rejected cell — so the "N of M cells
|
|
6103
|
-
// pasted" announce keeps its existing semantics (never silently
|
|
7170
|
+
// A group-row / placeholder-row cell still counts toward `total` — it is a skipped
|
|
7171
|
+
// TARGET, exactly like a non-editable or validator-rejected cell — so the "N of M cells
|
|
7172
|
+
// pasted" / "no cells pasted" announce keeps its existing semantics (never silently
|
|
7173
|
+
// under-reports M).
|
|
6104
7174
|
total = total + 1;
|
|
6105
|
-
if (rowGrouped) continue;
|
|
7175
|
+
if (rowGrouped || rowPlaceholder) continue;
|
|
6106
7176
|
const colId = this.columnIdAt(r, c);
|
|
6107
7177
|
if (colId == null || !this.columnEditable(colId)) continue;
|
|
6108
7178
|
const rowObj = this.rowOriginalAt(r);
|
|
@@ -6116,6 +7186,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6116
7186
|
const field = this.fieldOfColId(colId);
|
|
6117
7187
|
const srcIndex = this.sourceIndexOfRow(r);
|
|
6118
7188
|
const oldValue = rowObj ? rowObj[field] : null;
|
|
7189
|
+
applied = applied + 1;
|
|
7190
|
+
// B-08: the cell already holds this value — it counts as applied, but there is nothing
|
|
7191
|
+
// to write, nothing to emit and nothing to undo.
|
|
7192
|
+
if (oldValue === value) continue;
|
|
6119
7193
|
next = replaceRowValue(next, srcIndex, field, value);
|
|
6120
7194
|
committed.push({
|
|
6121
7195
|
rowId: this.rowIdAt(r),
|
|
@@ -6123,26 +7197,41 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6123
7197
|
oldValue,
|
|
6124
7198
|
newValue: value
|
|
6125
7199
|
});
|
|
6126
|
-
wrote = wrote + 1;
|
|
6127
7200
|
}
|
|
6128
7201
|
}
|
|
6129
|
-
if (
|
|
7202
|
+
if (committed.length > 0) {
|
|
6130
7203
|
this.editTransition = true;
|
|
6131
7204
|
this.writeData(next);
|
|
6132
7205
|
this.editTransition = false;
|
|
6133
7206
|
// One cell-edit-commit per COMMITTED cell (the per-cell event contract, D-03).
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
7207
|
+
// A-08: each emit is guarded INDIVIDUALLY. These listeners are consumer code, and a single
|
|
7208
|
+
// throwing one used to abort the loop — so on a 40-cell paste, cells 0..k notified and
|
|
7209
|
+
// k+1..39 silently never did, leaving a consumer persisting on this event with a partial
|
|
7210
|
+
// write of a model that had already been fully updated. Measured while building this: a
|
|
7211
|
+
// throwing VALIDATOR is NOT the reachable vehicle here — `runValidator`
|
|
7212
|
+
// (editCellLifecycle.rzts) already catches and reports 'Invalid value' — so the listener
|
|
7213
|
+
// is the first-party code that actually escapes.
|
|
7214
|
+
for (let i = 0; i < committed.length; i++) {
|
|
7215
|
+
try {
|
|
7216
|
+
this.dispatchEvent(new CustomEvent("cell-edit-commit", {
|
|
7217
|
+
detail: committed[i],
|
|
7218
|
+
bubbles: true,
|
|
7219
|
+
composed: true
|
|
7220
|
+
}));
|
|
7221
|
+
} catch (err: any) {
|
|
7222
|
+
console.error('[rozie-data-table] paste failed partway: a cell-edit-commit listener threw. The ' + 'model was already written; the remaining cells still notify.', err);
|
|
7223
|
+
}
|
|
7224
|
+
}
|
|
6139
7225
|
}
|
|
6140
7226
|
// WR-02: announce the N-of-M summary only when at least one cell was written. When the paste
|
|
6141
7227
|
// targeted real cells but every one was skipped (validation-failed / non-editable), announce a
|
|
6142
7228
|
// distinct validation-failed message instead of a misleading "0 of M cells pasted".
|
|
6143
|
-
if (
|
|
7229
|
+
if (applied > 0) this.announce(applied + ' of ' + total + ' cells ' + opVerb);else if (total > 0) this.announce('No cells ' + opVerb + ' — ' + total + ' cells were invalid or read-only');
|
|
7230
|
+
// `wrote` stays the ANNOUNCED count (targets satisfied), not the written one, so every
|
|
7231
|
+
// existing caller and assertion keeps its meaning; `changed` is the new, narrower number.
|
|
6144
7232
|
return {
|
|
6145
|
-
wrote,
|
|
7233
|
+
wrote: applied,
|
|
7234
|
+
changed: committed.length,
|
|
6146
7235
|
total
|
|
6147
7236
|
};
|
|
6148
7237
|
};
|
|
@@ -6190,14 +7279,26 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6190
7279
|
return;
|
|
6191
7280
|
}
|
|
6192
7281
|
if (!p || !p.then) return;
|
|
7282
|
+
// A-08: the two failure modes are separated. A REJECTED clipboard read (permission denied,
|
|
7283
|
+
// insecure context, an empty selection) is a legitimate silent no-op — that is the rejection
|
|
7284
|
+
// handler, the second argument. Anything the fulfilment handler throws is FIRST-PARTY: a
|
|
7285
|
+
// consumer's row accessor, a `data`-model setter, or a `cell-edit-commit` listener (which
|
|
7286
|
+
// `applyGridToRange` now also guards per-emit). A single trailing `.catch(() => {})`
|
|
7287
|
+
// swallowed both, so consumer code failing inside a paste produced no error, no warning and
|
|
7288
|
+
// nothing in the console to search for. NOT a column validator: `runValidator` already
|
|
7289
|
+
// catches a throwing one and reports 'Invalid value' — checked rather than assumed.
|
|
6193
7290
|
p.then((text: any) => {
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
7291
|
+
try {
|
|
7292
|
+
const grid = parseTsv(text);
|
|
7293
|
+
if (!grid.length) return;
|
|
7294
|
+
// C3: tile the clipboard block to fill the destination range (single→range fill,
|
|
7295
|
+
// smaller-tiles-into-larger); a clipboard larger than the box pastes its full block.
|
|
7296
|
+
const tiled = tileGridToBox(grid, destBox);
|
|
7297
|
+
this.applyGridToRange(tiled, anchorRow, anchorCol, 'pasted');
|
|
7298
|
+
} catch (err: any) {
|
|
7299
|
+
console.error('[rozie-data-table] paste failed: a row accessor, the data model setter or other ' + 'consumer code threw. The paste was abandoned.', err);
|
|
7300
|
+
}
|
|
7301
|
+
}, () => {/* clipboard read rejected (permission / insecure context) — silent by design */});
|
|
6201
7302
|
};
|
|
6202
7303
|
|
|
6203
7304
|
// cutRange(): C3 Cut — copy the current range to the clipboard (rangeToTsv — the SAME escaped
|
|
@@ -6231,7 +7332,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6231
7332
|
for (let c = c0; c <= c1; c++) cols.push('');
|
|
6232
7333
|
grid.push(cols);
|
|
6233
7334
|
}
|
|
6234
|
-
this.applyGridToRange(grid, r0, c0);
|
|
7335
|
+
this.applyGridToRange(grid, r0, c0, 'cut');
|
|
6235
7336
|
};
|
|
6236
7337
|
|
|
6237
7338
|
// clearActiveRange(): the §7 (260709-3qt) Delete/Backspace clear — cutRange() MINUS the clipboard
|
|
@@ -6257,7 +7358,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6257
7358
|
for (let c = c0; c <= c1; c++) cols.push('');
|
|
6258
7359
|
grid.push(cols);
|
|
6259
7360
|
}
|
|
6260
|
-
this.applyGridToRange(grid, r0, c0);
|
|
7361
|
+
this.applyGridToRange(grid, r0, c0, 'cleared');
|
|
6261
7362
|
};
|
|
6262
7363
|
|
|
6263
7364
|
// fillRange(sourceBox): drag-fill (D-04 — VALUE-COPY ONLY, no series detection). B7: the fill
|
|
@@ -6312,7 +7413,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6312
7413
|
}
|
|
6313
7414
|
grid.push(cols);
|
|
6314
7415
|
}
|
|
6315
|
-
this.applyGridToRange(grid, box.r0, box.c0);
|
|
7416
|
+
this.applyGridToRange(grid, box.r0, box.c0, 'filled');
|
|
6316
7417
|
};
|
|
6317
7418
|
|
|
6318
7419
|
// onFillHandlePointerDown: begin a fill-handle drag (req-8 / D-04). The handle sits on the
|
|
@@ -6600,10 +7701,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6600
7701
|
this.rangeDragging = false;
|
|
6601
7702
|
};
|
|
6602
7703
|
|
|
6603
|
-
// beginRangeDrag(anchorR, anchorC): start a drag-select anchored at the mousedown cell.
|
|
6604
|
-
//
|
|
6605
|
-
//
|
|
6606
|
-
//
|
|
7704
|
+
// beginRangeDrag(anchorR, anchorC): start a drag-select anchored at the mousedown cell.
|
|
7705
|
+
// B-01: the anchor is handed to setRangeFocus EXPLICITLY, never left to be seeded from the
|
|
7706
|
+
// active cell. The mousedown's native focus is a DEFAULT ACTION that runs after this handler
|
|
7707
|
+
// returns, so at this point $data.activeRow/activeColIndex still hold the PRE-mousedown cell;
|
|
7708
|
+
// on React the pointermove closure below is frozen at that render and can never observe the
|
|
7709
|
+
// post-focusin value, so the whole gesture anchored at wherever the user happened to be
|
|
7710
|
+
// (a fresh grid: (0,0)). We still never WRITE $data.rangeAnchor directly (React-stale,
|
|
7711
|
+
// ROZ138) — setRangeFocus owns that write. rangeDragMoved starts
|
|
6607
7712
|
// false and flips true only once the pointer reaches a DIFFERENT cell, so a mousedown-with-no-move
|
|
6608
7713
|
// leaves a single active cell + no range (a normal click). lastCell dedups the many pointermove
|
|
6609
7714
|
// events per cell (setRangeFocus emits range-change — only extend on a NEW cell, mirroring fillDrag's
|
|
@@ -6628,7 +7733,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6628
7733
|
if (cell && (cell.r !== lastCell.r || cell.c !== lastCell.c)) {
|
|
6629
7734
|
lastCell = cell;
|
|
6630
7735
|
this.rangeDragMoved = true;
|
|
6631
|
-
this.setRangeFocus(cell.r, cell.c);
|
|
7736
|
+
this.setRangeFocus(cell.r, cell.c, anchorR, anchorC);
|
|
6632
7737
|
}
|
|
6633
7738
|
};
|
|
6634
7739
|
const up = () => {
|
|
@@ -6666,6 +7771,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6666
7771
|
// row reaches the group-toggle branch below them instead of opening an editor.
|
|
6667
7772
|
isActiveCellEditable = () => {
|
|
6668
7773
|
if (this.rowIndexIsGrouped(this._activeRow.value)) return false;
|
|
7774
|
+
// A lazy placeholder (a row not loaded yet) has nothing to edit (quick 260925-dtl part 2).
|
|
7775
|
+
if (this.rowIsLazyPlaceholder((this._rows.value || [])[this._activeRow.value])) return false;
|
|
6669
7776
|
const colId = this.activeCellColumnId();
|
|
6670
7777
|
return colId != null && this.columnEditable(colId);
|
|
6671
7778
|
};
|
|
@@ -6900,6 +8007,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6900
8007
|
// calls beginEdit directly gated ONLY on columnEditable — isActiveCellEditable (Layer 1)
|
|
6901
8008
|
// does not run on that path at all.
|
|
6902
8009
|
if (this.rowIndexIsGrouped(rowIndex)) return;
|
|
8010
|
+
if (this.rowIsLazyPlaceholder((this._rows.value || [])[rowIndex])) return;
|
|
6903
8011
|
const colId = this.columnIdAt(rowIndex, colIndex);
|
|
6904
8012
|
if (colId == null || !this.columnEditable(colId)) return;
|
|
6905
8013
|
// A new edit session starts — reset the sync idempotency latch so THIS session's eventual
|
|
@@ -6910,6 +8018,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6910
8018
|
// editor clears any row-edit state so isEditing never resolves both modes for one cell.
|
|
6911
8019
|
this._editingRowIndex.value = null;
|
|
6912
8020
|
this._rowDraft.value = {};
|
|
8021
|
+
this.rowDraftSync = {};
|
|
6913
8022
|
this._editingRow.value = rowIndex;
|
|
6914
8023
|
this._editingCol.value = colIndex;
|
|
6915
8024
|
this._draftValue.value = seed != null ? seed : this.cellValueAt(rowIndex, colIndex);
|
|
@@ -6938,6 +8047,21 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6938
8047
|
const tryFocus = () => {
|
|
6939
8048
|
const el = this.resolveCellEl(String(row), col);
|
|
6940
8049
|
if (el) {
|
|
8050
|
+
// B-05 / F-07: do NOT stomp focus a later interaction has already placed on a DIFFERENT
|
|
8051
|
+
// grid cell. This poll runs up to 30 frames after commit/cancel, so without this guard a
|
|
8052
|
+
// user (or a test) who moves to the next cell in that window gets yanked BACK to the cell
|
|
8053
|
+
// they just left — and because the steal depends on rAF timing it presents as a flake that
|
|
8054
|
+
// rotates across targets rather than as a reproducible bug. Measured 2026-09-12: the
|
|
8055
|
+
// data-table-dropins editor case failed with the active cell pinned to the PREVIOUS column
|
|
8056
|
+
// (want 0/2, active 0/1; want 0/1, active 0/0) with no editor open.
|
|
8057
|
+
// focusEditorWhenReady already carries the equivalent guard against the same class of
|
|
8058
|
+
// theft; this is its missing twin. Compare the OWNING CELL (not node identity) so a
|
|
8059
|
+
// node-replacing re-render on Solid still resolves a genuinely dropped focus as recoverable.
|
|
8060
|
+
const ae = this.gridRoot && this.gridRoot.getRootNode ? this.gridRoot.getRootNode().activeElement : null;
|
|
8061
|
+
if (ae && ae !== el && this.gridRoot.contains && this.gridRoot.contains(ae) && ae.closest) {
|
|
8062
|
+
const aeCell = ae.closest('[data-grid-cell]');
|
|
8063
|
+
if (aeCell && aeCell !== el) return;
|
|
8064
|
+
}
|
|
6941
8065
|
el.focus();
|
|
6942
8066
|
return;
|
|
6943
8067
|
}
|
|
@@ -6967,6 +8091,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6967
8091
|
endRowEdit = () => {
|
|
6968
8092
|
this._editingRowIndex.value = null;
|
|
6969
8093
|
this._rowDraft.value = {};
|
|
8094
|
+
this.rowDraftSync = {};
|
|
6970
8095
|
this._invalidMsg.value = '';
|
|
6971
8096
|
this._activeInControl.value = false;
|
|
6972
8097
|
this._editVer.value = this._editVer.value + 1;
|
|
@@ -6995,7 +8120,28 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
6995
8120
|
// other editor type commits the value verbatim. Idempotent for the #editor drop-in path
|
|
6996
8121
|
// (an already-numeric override passes through; an explicit null stays null).
|
|
6997
8122
|
coerceCellValue = (colId: any, raw: any) => {
|
|
6998
|
-
|
|
8123
|
+
const kind = this.editorTypeOf(colId);
|
|
8124
|
+
// ── C-13 ────────────────────────────────────────────────────────────────────────────
|
|
8125
|
+
// This funnel exists so "no mixed/garbage types ever reach the model" (B9, T-63-03-01), and
|
|
8126
|
+
// it covered ONE of the four built-in editor types. Every path that writes without opening
|
|
8127
|
+
// the built-in control — paste, Cut, Delete/Backspace over a range, a fill-drag — arrives
|
|
8128
|
+
// here with a plain TSV STRING, so a `checkbox` column took `''` from a Delete and the
|
|
8129
|
+
// literal text `'true'` from a paste, straight into a field the consumer declared boolean.
|
|
8130
|
+
// `select` is deliberately NOT coerced: its model type IS the option string, so a string is
|
|
8131
|
+
// already correct there, and an out-of-range option is a validation question (C-03's class),
|
|
8132
|
+
// not a type one.
|
|
8133
|
+
//
|
|
8134
|
+
// Recognised spellings both ways; ANYTHING else — including '' and unrecognised text —
|
|
8135
|
+
// becomes `false`. That is lossy by construction and deterministic by design: the
|
|
8136
|
+
// alternative is letting a non-boolean reach a boolean field, which is the defect. It is
|
|
8137
|
+
// documented in the editing page's emptied-cell table.
|
|
8138
|
+
if (kind === 'checkbox') {
|
|
8139
|
+
if (typeof raw === 'boolean') return raw;
|
|
8140
|
+
if (raw == null) return false;
|
|
8141
|
+
if (typeof raw === 'number') return raw !== 0;
|
|
8142
|
+
return /^\s*(true|1|yes|y|on)\s*$/i.test(String(raw));
|
|
8143
|
+
}
|
|
8144
|
+
if (kind !== 'number') return raw;
|
|
6999
8145
|
if (raw == null) return null;
|
|
7000
8146
|
if (typeof raw === 'number') return Number.isNaN(raw) ? null : raw;
|
|
7001
8147
|
const s = String(raw).trim();
|
|
@@ -7119,11 +8265,17 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
7119
8265
|
// checkbox branch, so this line is redundant AT HEAD — it earns its place by making "no
|
|
7120
8266
|
// write ever targets a group row" a property of the write funnels themselves.
|
|
7121
8267
|
if (this.rowIndexIsGrouped(this._activeRow.value)) return;
|
|
7122
|
-
const colId = this.columnIdAt(this._activeRow.value, this._activeColIndex.value);
|
|
7123
|
-
if (colId == null || !this.columnEditable(colId)) return;
|
|
7124
8268
|
const rowList = this._rows.value || [];
|
|
7125
8269
|
const row = rowList[this._activeRow.value];
|
|
7126
8270
|
if (!row) return;
|
|
8271
|
+
// Debug 260926-dtl-edit-guard: defense-in-depth, matching the redundant rowIndexIsGrouped
|
|
8272
|
+
// check just above (Layer 4 is reachable ONLY through the Layer-1-gated checkbox branch in
|
|
8273
|
+
// onGridKeyDown, whose isActiveCellEditable() already excludes a placeholder row — this makes
|
|
8274
|
+
// "no write ever targets a placeholder row" a property of the write funnel itself, not just of
|
|
8275
|
+
// its one caller).
|
|
8276
|
+
if (this.rowIsLazyPlaceholder(row)) return;
|
|
8277
|
+
const colId = this.columnIdAt(this._activeRow.value, this._activeColIndex.value);
|
|
8278
|
+
if (colId == null || !this.columnEditable(colId)) return;
|
|
7127
8279
|
const rowOriginal = row.original;
|
|
7128
8280
|
const rowId = row.id;
|
|
7129
8281
|
const oldValue = this.cellValueAt(this._activeRow.value, this._activeColIndex.value);
|
|
@@ -7260,6 +8412,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
7260
8412
|
// redundant at HEAD; kept for the same reason as Layer 4 — it is a mutation entry point,
|
|
7261
8413
|
// and the natural call site for a future #rowActions "Edit" drop-in button.
|
|
7262
8414
|
if (this.rowIndexIsGrouped(rowIndex)) return;
|
|
8415
|
+
// Debug 260926-dtl-edit-guard: a LAZY PLACEHOLDER row (a row not loaded yet, virtual+manual+
|
|
8416
|
+
// rowCount) has nothing to edit — beginEdit (editCellLifecycle.rzts) already carries this
|
|
8417
|
+
// guard for single-cell entry; this full-row twin was missing it, so the public editRow()
|
|
8418
|
+
// verb (and, redundantly with isActiveCellEditable's own placeholder check, Shift+F2) could
|
|
8419
|
+
// seed a row editor from a placeholder's undefined fields and commit through the same
|
|
8420
|
+
// sourceIndexOfRow fallback bug applyGridToRange's placeholder skip closes.
|
|
8421
|
+
if (this.rowIsLazyPlaceholder((this._rows.value || [])[rowIndex])) return;
|
|
7263
8422
|
const editable = this.editableColumnsForRow(rowIndex);
|
|
7264
8423
|
if (editable.length === 0) return;
|
|
7265
8424
|
// A new edit session starts — reset the sync idempotency latch (see editCellLifecycle.rzts).
|
|
@@ -7279,6 +8438,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
7279
8438
|
draft[ec.colId] = orig ? orig[ec.field] : null;
|
|
7280
8439
|
}
|
|
7281
8440
|
this._rowDraft.value = draft;
|
|
8441
|
+
this.rowDraftSync = draft;
|
|
7282
8442
|
this._editingRowIndex.value = rowIndex;
|
|
7283
8443
|
this._activeInControl.value = true;
|
|
7284
8444
|
this._editVer.value = this._editVer.value + 1;
|
|
@@ -7309,7 +8469,8 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
7309
8469
|
const r = rowList[rowIndex];
|
|
7310
8470
|
const rowOriginal = r ? r.original : null;
|
|
7311
8471
|
const rowId = r ? r.id : null;
|
|
7312
|
-
|
|
8472
|
+
// C-05: the SYNC mirror, not `$data.rowDraft` — see rowDraftSync in DataTable.rozie.
|
|
8473
|
+
const draft = this.rowDraftSync || {};
|
|
7313
8474
|
// Validate every edited column FIRST (D-01: a single failure blocks the whole row commit).
|
|
7314
8475
|
// B3 (Rule 1): coerce each draft by the column's editor type BEFORE validation + write — a
|
|
7315
8476
|
// 'number' editor must commit a real Number/null, never the raw editor STRING (the single-cell
|
|
@@ -7505,6 +8666,25 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
7505
8666
|
// A top-level `let` (React hoists to useRef → persists).
|
|
7506
8667
|
committedThisSession = false;
|
|
7507
8668
|
|
|
8669
|
+
// rowDraftSync (quick 260922-mkb, C-05): a SYNCHRONOUS mirror of `$data.rowDraft`, written at
|
|
8670
|
+
// every rowDraft write and read by commitRow. A custom #editor drop-in stages its value with
|
|
8671
|
+
// `commit(v)` → setRowDraft in the SAME keydown that then commits the row (the host's drop-in
|
|
8672
|
+
// wrapper runs in the bubble phase right after the drop-in's own Enter handler). On React
|
|
8673
|
+
// `$data.rowDraft` is a `useState` read, so commitRow in that same tick saw the pre-write draft,
|
|
8674
|
+
// found no change and wrote nothing. The built-in editors never hit this only because they stage
|
|
8675
|
+
// on every `input` event, long before Enter. Same top-level-`let` technique as
|
|
8676
|
+
// committedThisSession above; on the five synchronous targets it always equals $data.rowDraft.
|
|
8677
|
+
rowDraftSync: Record<string, any> = {};
|
|
8678
|
+
|
|
8679
|
+
// dropinTabFlush (quick 260922-mkb, C-10): non-null only for the duration of the drop-in
|
|
8680
|
+
// wrapper's Tab flush (see onEditorDropinKeyDown). While set, a drop-in's `commit(v)` records
|
|
8681
|
+
// that it ran and whether it committed, and commits WITHOUT the focus return (the Tab handler
|
|
8682
|
+
// moves focus on to the next editor itself).
|
|
8683
|
+
dropinTabFlush: {
|
|
8684
|
+
called: boolean;
|
|
8685
|
+
committed: boolean;
|
|
8686
|
+
} | null = null;
|
|
8687
|
+
|
|
7508
8688
|
// ── Per-cell editor draft source (req-6) ──────────────────────────────────────────────
|
|
7509
8689
|
// In single-cell mode every editor binds the shared $data.draftValue. In full-row mode
|
|
7510
8690
|
// (editingRowIndex != null) each editable cell owns its OWN draft keyed by columnId in
|
|
@@ -7528,6 +8708,16 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
7528
8708
|
this.setRowDraft(colId, value);
|
|
7529
8709
|
return;
|
|
7530
8710
|
}
|
|
8711
|
+
// C-10: during the drop-in wrapper's Tab flush, record the outcome and skip the focus
|
|
8712
|
+
// return — the Tab handler advances focus to the next editor itself.
|
|
8713
|
+
if (this.dropinTabFlush) {
|
|
8714
|
+
this.dropinTabFlush.called = true;
|
|
8715
|
+
// STICKY: only a successful commit may set the outcome. A re-entrant commit within the
|
|
8716
|
+
// same flush (any blur the write itself provokes) is latched to false by commitEdit and
|
|
8717
|
+
// must not clear it. Measured: before the Solid slot-fill fix this re-entry was real.
|
|
8718
|
+
if (this.commitEdit(value, true)) this.dropinTabFlush.committed = true;
|
|
8719
|
+
return;
|
|
8720
|
+
}
|
|
7531
8721
|
this.commitEdit(value);
|
|
7532
8722
|
};
|
|
7533
8723
|
|
|
@@ -7565,10 +8755,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
7565
8755
|
// setRowDraft: write ONE key into a FRESH rowDraft object (whole-object replace — an
|
|
7566
8756
|
// in-place mutation is silently dropped on React/Solid; the family immutable rule).
|
|
7567
8757
|
setRowDraft = (colId: any, value: any) => {
|
|
7568
|
-
|
|
8758
|
+
// C-05: build from the SYNC mirror, so two same-tick writes (and a same-tick commitRow) never
|
|
8759
|
+
// see a React-stale `$data.rowDraft`.
|
|
8760
|
+
const src = this.rowDraftSync || {};
|
|
7569
8761
|
const next = {};
|
|
7570
8762
|
for (const k in src) next[k] = src[k];
|
|
7571
8763
|
next[colId] = value;
|
|
8764
|
+
this.rowDraftSync = next;
|
|
7572
8765
|
this._rowDraft.value = next;
|
|
7573
8766
|
};
|
|
7574
8767
|
|
|
@@ -7658,6 +8851,105 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
7658
8851
|
}
|
|
7659
8852
|
};
|
|
7660
8853
|
|
|
8854
|
+
// DECLARED AFTER onEditorKeyDown ON PURPOSE. It delegates to it, and the React emitter lowers
|
|
8855
|
+
// both to `useCallback`s whose DEPENDENCY ARRAY is evaluated EAGERLY at declaration time — so
|
|
8856
|
+
// with this one first, its deps array referenced `onEditorKeyDown` before the const existed and
|
|
8857
|
+
// every React cell in the family died with `ReferenceError: Cannot access 'ze' before
|
|
8858
|
+
// initialization` at mount. That is the 87-02 useCallback-deps-array ordering lesson, and it is
|
|
8859
|
+
// invisible on the other five targets. Keep this block below.
|
|
8860
|
+
// ── C-10 + C-05: the grid keymap for a CUSTOM #editor drop-in ───────────────────────────
|
|
8861
|
+
// The built-in editors bind `onEditorKeyDown` directly, so Tab commits-and-advances and, in
|
|
8862
|
+
// row mode, Enter commits the row. A drop-in binds nothing of ours: it is consumer markup
|
|
8863
|
+
// inside the `#editor` slot. Its own keydown handler covers Enter and Escape (the drop-in
|
|
8864
|
+
// contract) and stops there, and the keydown then bubbles to the table root where
|
|
8865
|
+
// `onGridKeyDown` early-returns because an editor is open. Result:
|
|
8866
|
+
// C-10 — Tab in a drop-in did not commit and advance. It fell through to NATIVE tab order
|
|
8867
|
+
// and walked focus out of the grid, leaving the editor open behind it.
|
|
8868
|
+
// C-05 — in FULL-ROW mode a drop-in's `commit(v)` is draft-only BY DESIGN (committing per
|
|
8869
|
+
// cell would split the row into N writes and N events), and the row commits only
|
|
8870
|
+
// through handlers bound on the built-in inputs — which a row of drop-ins does not
|
|
8871
|
+
// have. So Shift+F2, documented as "Enter commits the whole row in one
|
|
8872
|
+
// `r-model:data` write", wrote a draft and nothing else, `editingRowIndex` stayed
|
|
8873
|
+
// set, and `onGridKeyDown`'s early-return left arrow nav dead. The only exits were
|
|
8874
|
+
// Escape (reverting the whole row) and the imperative verb.
|
|
8875
|
+
//
|
|
8876
|
+
// Bound on the `display: contents` span that wraps the slot, so it sees the bubbled event
|
|
8877
|
+
// from whatever the consumer rendered — including a bespoke editor that never heard of this
|
|
8878
|
+
// component. The host owning the GRID keymap while the drop-in owns its own CONTROL is the
|
|
8879
|
+
// same division the built-in path already has.
|
|
8880
|
+
//
|
|
8881
|
+
// Row mode ignores `defaultPrevented` for Enter/Escape on purpose: a drop-in preventDefaults
|
|
8882
|
+
// Enter to mean "do not insert a newline", not "the row is handled". Its own handler has
|
|
8883
|
+
// already run (target phase before bubble), so the draft is staged by the time we commit.
|
|
8884
|
+
// Single-cell mode honours `defaultPrevented` for Tab, so a drop-in that genuinely implements
|
|
8885
|
+
// its own Tab (a multi-field editor) keeps it.
|
|
8886
|
+
//
|
|
8887
|
+
// Single-cell Tab FLUSHES the drop-in before advancing (quick 260922-mkb). A drop-in owns its
|
|
8888
|
+
// draft: the host's `draftValue` still holds the value the editor opened with, so routing Tab
|
|
8889
|
+
// straight into onEditorKeyDown committed that stale value, opened the next cell's editor, and
|
|
8890
|
+
// then the old drop-in's unmount blur ran `commit(typed)` against the NOW-editing cell — the
|
|
8891
|
+
// typed text landed in the NEXT column (measured: `status=Zeta` from a Name edit, on four
|
|
8892
|
+
// targets). Instead the handler blurs the drop-in while its own cell is still the editing one,
|
|
8893
|
+
// so the drop-in's blur-commit (the drop-in contract) writes the right cell, and only then
|
|
8894
|
+
// advances. `dropinTabFlush` tells editorCommitFor to report the outcome and skip the focus
|
|
8895
|
+
// return. A drop-in that does not commit on blur (EditorCheckbox commits on change, so it holds
|
|
8896
|
+
// nothing pending) falls back to the host-draft path, which is exactly right in that case.
|
|
8897
|
+
onEditorDropinKeyDown = (e: any) => {
|
|
8898
|
+
if (!e) return;
|
|
8899
|
+
const key = e.key;
|
|
8900
|
+
if (this.inRowEdit()) {
|
|
8901
|
+
if (key === 'Enter') {
|
|
8902
|
+
e.preventDefault();
|
|
8903
|
+
this.commitRow();
|
|
8904
|
+
return;
|
|
8905
|
+
}
|
|
8906
|
+
if (key === 'Escape') {
|
|
8907
|
+
e.preventDefault();
|
|
8908
|
+
this.cancelRow();
|
|
8909
|
+
return;
|
|
8910
|
+
}
|
|
8911
|
+
if (key === 'Tab') {
|
|
8912
|
+
e.preventDefault();
|
|
8913
|
+
this.rowEditTab(e.target, e.shiftKey);
|
|
8914
|
+
return;
|
|
8915
|
+
}
|
|
8916
|
+
return;
|
|
8917
|
+
}
|
|
8918
|
+
if (key !== 'Tab' || e.defaultPrevented) return;
|
|
8919
|
+
e.preventDefault();
|
|
8920
|
+
const fromRow = this._editingRow.value;
|
|
8921
|
+
const fromCol = this._editingCol.value;
|
|
8922
|
+
const target = e.shiftKey ? this.prevEditableCell(fromRow, fromCol) : this.nextEditableCell(fromRow, fromCol);
|
|
8923
|
+
const el = e.target;
|
|
8924
|
+
// Move focus to the drop-in's OWN cell rather than calling blur(): that still fires the
|
|
8925
|
+
// drop-in's blur-commit, but keeps focus inside the grid instead of dropping it to <body>.
|
|
8926
|
+
const ownCell = el && el.closest ? el.closest('[data-grid-cell]') : null;
|
|
8927
|
+
const flush = {
|
|
8928
|
+
called: false,
|
|
8929
|
+
committed: false
|
|
8930
|
+
};
|
|
8931
|
+
this.dropinTabFlush = flush;
|
|
8932
|
+
if (ownCell && ownCell.focus) ownCell.focus();else if (el && el.blur) el.blur();
|
|
8933
|
+
this.dropinTabFlush = null;
|
|
8934
|
+
if (!flush.called) {
|
|
8935
|
+
this.onEditorKeyDown(e);
|
|
8936
|
+
return;
|
|
8937
|
+
}
|
|
8938
|
+
// A validation failure keeps the editor open: give the drop-in its focus back.
|
|
8939
|
+
if (!flush.committed) {
|
|
8940
|
+
if (el && el.focus) el.focus();
|
|
8941
|
+
return;
|
|
8942
|
+
}
|
|
8943
|
+
if (target) {
|
|
8944
|
+
this._activeRow.value = target.row;
|
|
8945
|
+
this._activeColIndex.value = target.col;
|
|
8946
|
+
this.beginEdit(target.row, target.col, null);
|
|
8947
|
+
} else {
|
|
8948
|
+
// B5 twin: no editable cell in the Tab direction — keep focus INSIDE the grid.
|
|
8949
|
+
this.focusCellWhenReady(fromRow, fromCol);
|
|
8950
|
+
}
|
|
8951
|
+
};
|
|
8952
|
+
|
|
7661
8953
|
// onEditorBlur: commit on a genuine click/focus-away (D-01 — an invalid value keeps the
|
|
7662
8954
|
// editor open via commitEdit's reject path). SKIP when:
|
|
7663
8955
|
// - editTransition is set (a synchronous commit/cancel teardown is unmounting the editor), or
|
|
@@ -7694,11 +8986,52 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
7694
8986
|
}
|
|
7695
8987
|
if (this._editingRow.value < 0 || this.editTransition) return;
|
|
7696
8988
|
const next = e ? e.relatedTarget : null;
|
|
7697
|
-
|
|
7698
|
-
//
|
|
7699
|
-
//
|
|
7700
|
-
//
|
|
7701
|
-
|
|
8989
|
+
const fromCell = e && e.target && e.target.closest ? e.target.closest('[data-grid-cell]') : null;
|
|
8990
|
+
// ── B-02 ────────────────────────────────────────────────────────────────────────────
|
|
8991
|
+
// A null relatedTarget used to return unconditionally. That is correct for the WR-04 case
|
|
8992
|
+
// the comment cited — a Tab-advance fires the OLD editor's blur with a TRANSIENT null
|
|
8993
|
+
// relatedTarget while it unmounts, and committing there double-counts — but it is NOT the
|
|
8994
|
+
// only shape. Clicking any non-focusable region of the page (the body, a heading, whitespace)
|
|
8995
|
+
// also blurs the editor with a null relatedTarget, and that returned WITHOUT committing and
|
|
8996
|
+
// WITHOUT endEdit(): the typed value was silently discarded, the editor stayed open, and
|
|
8997
|
+
// `$data.editingRow` stayed >= 0 — which gridKeydownHandlers' `if ($data.editingRow >= 0)
|
|
8998
|
+
// return` reads as "an editor owns the keyboard", so arrows/Enter/F2/Home/Ctrl+C were all
|
|
8999
|
+
// dead until the user double-clicked somewhere else. There is no document-level click-away
|
|
9000
|
+
// listener anywhere in the family to catch it afterwards.
|
|
9001
|
+
//
|
|
9002
|
+
// The two shapes ARE separable, with the discriminator the branch below already uses: on a
|
|
9003
|
+
// Tab-advance the keyboard path has already moved the editing pair to the NEXT cell, so the
|
|
9004
|
+
// blurring editor's own cell no longer matches ($data.editingRow/editingCol), while a
|
|
9005
|
+
// click-away leaves the pair still sitting on it. The row-edit branch twenty lines above
|
|
9006
|
+
// reaches the same conclusion by the same reasoning and commits on null; this is that branch's
|
|
9007
|
+
// single-cell twin.
|
|
9008
|
+
if (next == null) {
|
|
9009
|
+
// Not a click-away unless a pointerdown OUTSIDE the grid just said so. `outsidePointerDown`
|
|
9010
|
+
// is the discriminator, and nothing weaker works: an editor recycled out of the virtual
|
|
9011
|
+
// window (or replaced by Solid's keyed-row reconcile) blurs with a null relatedTarget too,
|
|
9012
|
+
// and `closest()` still resolves an owning `<td>` inside a DETACHED subtree, so every
|
|
9013
|
+
// synchronous DOM test reads it as a click-away — measured, it closed the pinned editor in
|
|
9014
|
+
// data-table-edit's "open editor + index-based range survive virtualization recycle" case on
|
|
9015
|
+
// solid. Deferring a frame does not help either: by then the node is reattached, focus sits
|
|
9016
|
+
// on <body>, and the state is indistinguishable from a real click-away.
|
|
9017
|
+
//
|
|
9018
|
+
// The flag is a plain module-let set by the document pointerdown listener in DataTable.rozie
|
|
9019
|
+
// (NOT a $data field, and NOT a closure over one): a document listener is attached once, so
|
|
9020
|
+
// on React any `$data` it closes over is frozen at attach time, and a commit driven from
|
|
9021
|
+
// that closure would write the draft as of editor-OPEN and lose everything typed. Routing
|
|
9022
|
+
// the decision back through THIS handler — template-bound, so React re-binds it every
|
|
9023
|
+
// render — keeps the commit on the always-fresh path. Consume it here so an unmount blur
|
|
9024
|
+
// arriving later can never inherit it.
|
|
9025
|
+
if (!this.outsidePointerDown) return;
|
|
9026
|
+
this.outsidePointerDown = false;
|
|
9027
|
+
if (!fromCell) return;
|
|
9028
|
+
if (fromCell.getAttribute('data-row') !== String(this._editingRow.value) || fromCell.getAttribute('data-col-index') !== String(this._editingCol.value)) return;
|
|
9029
|
+
// Genuine click-away onto a non-focusable region outside the grid → commit + close (D-01's
|
|
9030
|
+
// reject path keeps the editor open on an invalid value, exactly as for a click onto another
|
|
9031
|
+
// cell). skipFocusReturn: the user deliberately put focus elsewhere; do not yank it back.
|
|
9032
|
+
this.commitEdit(undefined, true);
|
|
9033
|
+
return;
|
|
9034
|
+
}
|
|
7702
9035
|
// Focus moving OUTSIDE the grid (a click into another widget) → commit (D-01 reject keeps
|
|
7703
9036
|
// the editor open on an invalid value).
|
|
7704
9037
|
if (!(this.gridRoot && this.gridRoot.contains && this.gridRoot.contains(next))) {
|
|
@@ -7709,7 +9042,6 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
|
|
|
7709
9042
|
// keyboard handler already committed) from a genuine click-away to ANOTHER grid cell
|
|
7710
9043
|
// (which must commit + close so the grid is not wedged with an open editor).
|
|
7711
9044
|
const nextCell = next.closest ? next.closest('[data-grid-cell]') : null;
|
|
7712
|
-
const fromCell = e && e.target && e.target.closest ? e.target.closest('[data-grid-cell]') : null;
|
|
7713
9045
|
// Same cell (an inner control / the editing cell itself on an Enter focus-return) → a
|
|
7714
9046
|
// controlled move; skip. Also skip when either cell can't be resolved (an unmounting
|
|
7715
9047
|
// editor has no owning cell — the Tab-advance remount-blur path, never a click-away).
|