@rozie-ui/data-table-lit 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -24,6 +24,39 @@ const wrapAggregationFn = (fn) => {
24
24
  }
25
25
  };
26
26
  };
27
+ const collectNestedDefs = (list, out) => {
28
+ if (!Array.isArray(list)) return;
29
+ for (const d of list) {
30
+ if (!d || d.id == null) continue;
31
+ const id = String(d.id);
32
+ if (!(id in out)) out[id] = d;
33
+ if (Array.isArray(d.columns)) collectNestedDefs(d.columns, out);
34
+ }
35
+ };
36
+ const indexDefsById = (defs) => {
37
+ const out = Object.create(null);
38
+ if (!Array.isArray(defs)) return out;
39
+ for (const d of defs) {
40
+ if (!d || d.id == null) continue;
41
+ out[String(d.id)] = d;
42
+ }
43
+ for (const d of defs) if (d && Array.isArray(d.columns)) collectNestedDefs(d.columns, out);
44
+ return out;
45
+ };
46
+ const collectGroupableLeafDefs = (defs) => {
47
+ const out = [];
48
+ if (!Array.isArray(defs)) return out;
49
+ for (const d of defs) {
50
+ if (!d) continue;
51
+ if (Array.isArray(d.columns)) {
52
+ out.push(...collectGroupableLeafDefs(d.columns));
53
+ continue;
54
+ }
55
+ if (d.groupable === false) continue;
56
+ out.push(d);
57
+ }
58
+ return out;
59
+ };
27
60
  //#endregion
28
61
  //#region src/helpers/indexMath.ts
29
62
  const clamp = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
@@ -468,6 +501,7 @@ let DataTable = class DataTable extends (0, _lit_labs_preact_signals.SignalWatch
468
501
  this.columnDefsCache = null;
469
502
  this.columnDefsCacheColumnsRef = void 0;
470
503
  this.columnDefsCacheColRegRef = void 0;
504
+ this.columnDefsIndexCache = null;
471
505
  this.columnDefs = () => {
472
506
  const cfg = this.columns || [];
473
507
  const reg = this._colReg.value || {};
@@ -511,8 +545,13 @@ let DataTable = class DataTable extends (0, _lit_labs_preact_signals.SignalWatch
511
545
  this.columnDefsCache = out;
512
546
  this.columnDefsCacheColumnsRef = this.columns;
513
547
  this.columnDefsCacheColRegRef = this._colReg.value;
548
+ this.columnDefsIndexCache = indexDefsById(out);
514
549
  return out;
515
550
  };
551
+ this.defIndex = () => {
552
+ this.columnDefs();
553
+ return this.columnDefsIndexCache || Object.create(null);
554
+ };
516
555
  this.SELECT_COL_ID = "__rdt_select";
517
556
  this.EXPANDER_COL_ID = "__rdt_expander";
518
557
  this.selectionEnabled = () => this.selectionMode === "single" || this.selectionMode === "multiple";
@@ -1254,9 +1293,9 @@ let DataTable = class DataTable extends (0, _lit_labs_preact_signals.SignalWatch
1254
1293
  return "";
1255
1294
  };
1256
1295
  this.defFor = (colId) => {
1257
- const defs = this.columnDefs();
1258
- for (const d of defs) if (d.id === colId) return d;
1259
- return null;
1296
+ if (colId == null) return null;
1297
+ const d = this.defIndex()[String(colId)];
1298
+ return d != null ? d : null;
1260
1299
  };
1261
1300
  this.visibleCellsFor = (row) => this._rowModelVer.value >= 0 ? row.getVisibleCells() : [];
1262
1301
  this.editMetaOf = (colId) => {
@@ -1433,22 +1472,56 @@ let DataTable = class DataTable extends (0, _lit_labs_preact_signals.SignalWatch
1433
1472
  return base;
1434
1473
  };
1435
1474
  this.rowIsGrouped = (row) => !!(this.tick() >= 0 && row && row.getIsGrouped && row.getIsGrouped());
1475
+ this.rowIndexIsGrouped = (rowIndex) => this.rowIsGrouped((this._rows.value || [])[rowIndex]);
1436
1476
  this.groupingActive = () => this.tick() >= 0 && (this.currentState().grouping || []).length > 0;
1437
1477
  this.cellIsGrouped = (cellCtx) => !!(this.tick() >= 0 && cellCtx && cellCtx.getIsGrouped && cellCtx.getIsGrouped());
1438
1478
  this.cellIsAggregated = (cellCtx) => !!(this.tick() >= 0 && cellCtx && cellCtx.getIsAggregated && cellCtx.getIsAggregated());
1439
1479
  this.cellIsPlaceholder = (cellCtx) => !!(this.tick() >= 0 && cellCtx && cellCtx.getIsPlaceholder && cellCtx.getIsPlaceholder());
1440
- this.groupSubRowCount = (row) => row && row.getLeafRows ? row.getLeafRows().length : row && row.subRows ? row.subRows.length : 0;
1480
+ this.groupSubRowCount = (row) => {
1481
+ if (row && row.getLeafRows) {
1482
+ const leaves = row.getLeafRows();
1483
+ let n = 0;
1484
+ for (let i = 0; i < leaves.length; i++) {
1485
+ const r = leaves[i];
1486
+ if (!(r && r.getIsGrouped && r.getIsGrouped())) n = n + 1;
1487
+ }
1488
+ return n;
1489
+ }
1490
+ return row && row.subRows ? row.subRows.length : 0;
1491
+ };
1492
+ this.groupRowDescriptorCache = null;
1493
+ this.groupRowDescriptorCacheVer = void 0;
1494
+ this.groupRowDescriptor = (row) => {
1495
+ const ver = this._rowModelVer.value;
1496
+ if (!this.groupRowDescriptorCache || this.groupRowDescriptorCacheVer !== ver) {
1497
+ this.groupRowDescriptorCache = Object.create(null);
1498
+ this.groupRowDescriptorCacheVer = ver;
1499
+ }
1500
+ const key = String(row.id);
1501
+ let d = this.groupRowDescriptorCache[key];
1502
+ if (!d) {
1503
+ const groupingColumnId = row.groupingColumnId != null ? row.groupingColumnId : "";
1504
+ const groupingValue = row.getGroupingValue ? row.getGroupingValue(groupingColumnId) : row.groupingValue;
1505
+ d = {
1506
+ isGroupRow: true,
1507
+ groupId: row.id,
1508
+ groupingColumnId,
1509
+ groupingValue,
1510
+ leafCount: this.groupSubRowCount(row)
1511
+ };
1512
+ this.groupRowDescriptorCache[key] = d;
1513
+ }
1514
+ return d;
1515
+ };
1516
+ this.cellSlotRow = (row) => this.rowIsGrouped(row) ? this.groupRowDescriptor(row) : row ? row.original : null;
1441
1517
  this.groupingKeys = () => this.currentState().grouping || [];
1442
1518
  this.groupableColumns = () => {
1443
1519
  const out = [];
1444
- const defs = this.columnDefs();
1445
- for (const d of defs) {
1446
- if (!d || d.groupable === false) continue;
1447
- out.push({
1448
- id: d.id,
1449
- label: d.header != null ? d.header : d.id
1450
- });
1451
- }
1520
+ const defs = collectGroupableLeafDefs(this.columnDefs());
1521
+ for (const d of defs) out.push({
1522
+ id: d.id,
1523
+ label: d.header != null ? d.header : d.id
1524
+ });
1452
1525
  return out;
1453
1526
  };
1454
1527
  this.stopEvent = (evt) => {
@@ -2446,11 +2519,13 @@ let DataTable = class DataTable extends (0, _lit_labs_preact_signals.SignalWatch
2446
2519
  for (let gr = 0; gr < grid.length; gr++) {
2447
2520
  const r = originRow + gr;
2448
2521
  if (r > maxRow) break;
2522
+ const rowGrouped = this.rowIndexIsGrouped(r);
2449
2523
  const cols = grid[gr] || [];
2450
2524
  for (let gc = 0; gc < cols.length; gc++) {
2451
2525
  const c = originCol + gc;
2452
2526
  if (c > maxCol) break;
2453
2527
  total = total + 1;
2528
+ if (rowGrouped) continue;
2454
2529
  const colId = this.columnIdAt(r, c);
2455
2530
  if (colId == null || !this.columnEditable(colId)) continue;
2456
2531
  const rowObj = this.rowOriginalAt(r);
@@ -2760,11 +2835,13 @@ let DataTable = class DataTable extends (0, _lit_labs_preact_signals.SignalWatch
2760
2835
  return cell && cell.column ? cell.column.id : null;
2761
2836
  };
2762
2837
  this.isActiveCellEditable = () => {
2838
+ if (this.rowIndexIsGrouped(this._activeRow.value)) return false;
2763
2839
  const colId = this.activeCellColumnId();
2764
2840
  return colId != null && this.columnEditable(colId);
2765
2841
  };
2766
2842
  this.isEditing = (rowIndex, colIndex) => {
2767
2843
  if (this._editVer.value < 0) return false;
2844
+ if (this.rowIndexIsGrouped(rowIndex)) return false;
2768
2845
  if (this._editingRowIndex.value != null && this._editingRowIndex.value === rowIndex) {
2769
2846
  const colId = this.columnIdAt(rowIndex, colIndex);
2770
2847
  return colId != null && this.columnEditable(colId);
@@ -2864,6 +2941,7 @@ let DataTable = class DataTable extends (0, _lit_labs_preact_signals.SignalWatch
2864
2941
  return cell ? cell.getValue() : null;
2865
2942
  };
2866
2943
  this.beginEdit = (rowIndex, colIndex, seed) => {
2944
+ if (this.rowIndexIsGrouped(rowIndex)) return;
2867
2945
  const colId = this.columnIdAt(rowIndex, colIndex);
2868
2946
  if (colId == null || !this.columnEditable(colId)) return;
2869
2947
  this.committedThisSession = false;
@@ -2981,6 +3059,7 @@ let DataTable = class DataTable extends (0, _lit_labs_preact_signals.SignalWatch
2981
3059
  return true;
2982
3060
  };
2983
3061
  this.toggleActiveBooleanCell = () => {
3062
+ if (this.rowIndexIsGrouped(this._activeRow.value)) return;
2984
3063
  const colId = this.columnIdAt(this._activeRow.value, this._activeColIndex.value);
2985
3064
  if (colId == null || !this.columnEditable(colId)) return;
2986
3065
  const row = (this._rows.value || [])[this._activeRow.value];
@@ -3070,6 +3149,7 @@ let DataTable = class DataTable extends (0, _lit_labs_preact_signals.SignalWatch
3070
3149
  this.beginRowEdit = (row) => {
3071
3150
  const rowIndex = this.rowIndexOf(row);
3072
3151
  if (rowIndex < 0) return;
3152
+ if (this.rowIndexIsGrouped(rowIndex)) return;
3073
3153
  const editable = this.editableColumnsForRow(rowIndex);
3074
3154
  if (editable.length === 0) return;
3075
3155
  this.committedThisSession = false;
@@ -4349,13 +4429,13 @@ ${this.groupable ? lit.html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4
4349
4429
  this.onToggleExpand(wr.row, $event);
4350
4430
  }} data-rozie-s-d5dcab4c>${(0, _rozie_runtime_lit.rozieDisplay)(this.rowIsExpanded(wr.row) ? "▾" : "▸")}</button>` : lit.nothing}</span>` : this.isSelectColumn(cell.column.id) ? lit.html`<span style="display:contents" data-rozie-s-d5dcab4c>
4351
4431
  ${this.selectCell !== void 0 ? this.selectCell({
4352
- row: wr.row.original,
4432
+ row: this.cellSlotRow(wr.row),
4353
4433
  checked: this.rowIsSelected(wr.row),
4354
4434
  toggle: (e) => this.onToggleRow(wr.row, e)
4355
4435
  }) : lit.html`<slot name="selectCell" data-rozie-params=${(() => {
4356
4436
  try {
4357
4437
  return JSON.stringify({
4358
- row: wr.row.original,
4438
+ row: this.cellSlotRow(wr.row),
4359
4439
  checked: this.rowIsSelected(wr.row)
4360
4440
  });
4361
4441
  } catch {
@@ -4374,14 +4454,14 @@ ${this.groupable ? lit.html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4
4374
4454
  ${this.cell !== void 0 ? this.cell({
4375
4455
  columnId: cell.column.id,
4376
4456
  column: cell.column,
4377
- row: wr.row.original,
4457
+ row: this.cellSlotRow(wr.row),
4378
4458
  value: cell.getValue()
4379
4459
  }) : lit.html`<slot name="cell" data-rozie-params=${(() => {
4380
4460
  try {
4381
4461
  return JSON.stringify({
4382
4462
  columnId: cell.column.id,
4383
4463
  column: cell.column,
4384
- row: wr.row.original,
4464
+ row: this.cellSlotRow(wr.row),
4385
4465
  value: cell.getValue()
4386
4466
  });
4387
4467
  } catch {
@@ -4395,7 +4475,7 @@ ${this.groupable ? lit.html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4
4395
4475
  ${this.editor !== void 0 ? this.editor({
4396
4476
  columnId: cell.column.id,
4397
4477
  column: cell.column,
4398
- row: wr.row.original,
4478
+ row: this.cellSlotRow(wr.row),
4399
4479
  value: this.editorValueFor(cell.column.id),
4400
4480
  commit: this.editorCommitFor(cell.column.id),
4401
4481
  cancel: this.editorCancelFor(),
@@ -4405,7 +4485,7 @@ ${this.groupable ? lit.html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4
4405
4485
  return JSON.stringify({
4406
4486
  columnId: cell.column.id,
4407
4487
  column: cell.column,
4408
- row: wr.row.original,
4488
+ row: this.cellSlotRow(wr.row),
4409
4489
  value: this.editorValueFor(cell.column.id),
4410
4490
  commit: this.editorCommitFor(cell.column.id),
4411
4491
  cancel: this.editorCancelFor(),
@@ -4445,14 +4525,14 @@ ${this.groupable ? lit.html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4
4445
4525
  ${this.cell !== void 0 ? this.cell({
4446
4526
  columnId: cell.column.id,
4447
4527
  column: cell.column,
4448
- row: wr.row.original,
4528
+ row: this.cellSlotRow(wr.row),
4449
4529
  value: cell.getValue()
4450
4530
  }) : lit.html`<slot name="cell" data-rozie-params=${(() => {
4451
4531
  try {
4452
4532
  return JSON.stringify({
4453
4533
  columnId: cell.column.id,
4454
4534
  column: cell.column,
4455
- row: wr.row.original,
4535
+ row: this.cellSlotRow(wr.row),
4456
4536
  value: cell.getValue()
4457
4537
  });
4458
4538
  } catch {
@@ -4643,13 +4723,13 @@ ${this.groupable ? lit.html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4
4643
4723
  this.onToggleExpand(row, $event);
4644
4724
  }} data-rozie-s-d5dcab4c>${(0, _rozie_runtime_lit.rozieDisplay)(this.rowIsExpanded(row) ? "▾" : "▸")}</button>` : lit.nothing}</span>` : this.isSelectColumn(cell.column.id) ? lit.html`<span style="display:contents" data-rozie-s-d5dcab4c>
4645
4725
  ${this.selectCell !== void 0 ? this.selectCell({
4646
- row: row.original,
4726
+ row: this.cellSlotRow(row),
4647
4727
  checked: this.rowIsSelected(row),
4648
4728
  toggle: (e) => this.onToggleRow(row, e)
4649
4729
  }) : lit.html`<slot name="selectCell" data-rozie-params=${(() => {
4650
4730
  try {
4651
4731
  return JSON.stringify({
4652
- row: row.original,
4732
+ row: this.cellSlotRow(row),
4653
4733
  checked: this.rowIsSelected(row)
4654
4734
  });
4655
4735
  } catch {
@@ -4668,14 +4748,14 @@ ${this.groupable ? lit.html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4
4668
4748
  ${this.cell !== void 0 ? this.cell({
4669
4749
  columnId: cell.column.id,
4670
4750
  column: cell.column,
4671
- row: row.original,
4751
+ row: this.cellSlotRow(row),
4672
4752
  value: cell.getValue()
4673
4753
  }) : lit.html`<slot name="cell" data-rozie-params=${(() => {
4674
4754
  try {
4675
4755
  return JSON.stringify({
4676
4756
  columnId: cell.column.id,
4677
4757
  column: cell.column,
4678
- row: row.original,
4758
+ row: this.cellSlotRow(row),
4679
4759
  value: cell.getValue()
4680
4760
  });
4681
4761
  } catch {
@@ -4689,7 +4769,7 @@ ${this.groupable ? lit.html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4
4689
4769
  ${this.editor !== void 0 ? this.editor({
4690
4770
  columnId: cell.column.id,
4691
4771
  column: cell.column,
4692
- row: row.original,
4772
+ row: this.cellSlotRow(row),
4693
4773
  value: this.editorValueFor(cell.column.id),
4694
4774
  commit: this.editorCommitFor(cell.column.id),
4695
4775
  cancel: this.editorCancelFor(),
@@ -4699,7 +4779,7 @@ ${this.groupable ? lit.html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4
4699
4779
  return JSON.stringify({
4700
4780
  columnId: cell.column.id,
4701
4781
  column: cell.column,
4702
- row: row.original,
4782
+ row: this.cellSlotRow(row),
4703
4783
  value: this.editorValueFor(cell.column.id),
4704
4784
  commit: this.editorCommitFor(cell.column.id),
4705
4785
  cancel: this.editorCancelFor(),
@@ -4739,14 +4819,14 @@ ${this.groupable ? lit.html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4
4739
4819
  ${this.cell !== void 0 ? this.cell({
4740
4820
  columnId: cell.column.id,
4741
4821
  column: cell.column,
4742
- row: row.original,
4822
+ row: this.cellSlotRow(row),
4743
4823
  value: cell.getValue()
4744
4824
  }) : lit.html`<slot name="cell" data-rozie-params=${(() => {
4745
4825
  try {
4746
4826
  return JSON.stringify({
4747
4827
  columnId: cell.column.id,
4748
4828
  column: cell.column,
4749
- row: row.original,
4829
+ row: this.cellSlotRow(row),
4750
4830
  value: cell.getValue()
4751
4831
  });
4752
4832
  } catch {
package/dist/index.d.cts CHANGED
@@ -308,7 +308,9 @@ declare class DataTable extends DataTable_base {
308
308
  columnDefsCache: any[] | null;
309
309
  columnDefsCacheColumnsRef: any;
310
310
  columnDefsCacheColRegRef: any;
311
+ columnDefsIndexCache: any;
311
312
  columnDefs: () => any[];
313
+ defIndex: () => any;
312
314
  SELECT_COL_ID: string;
313
315
  EXPANDER_COL_ID: string;
314
316
  selectionEnabled: () => boolean;
@@ -456,11 +458,16 @@ declare class DataTable extends DataTable_base {
456
458
  onToggleExpand: (row: any, evt: any) => void;
457
459
  bodyCellStyle: (row: any, colId: any) => string;
458
460
  rowIsGrouped: (row: any) => boolean;
461
+ rowIndexIsGrouped: (rowIndex: any) => boolean;
459
462
  groupingActive: () => boolean;
460
463
  cellIsGrouped: (cellCtx: any) => boolean;
461
464
  cellIsAggregated: (cellCtx: any) => boolean;
462
465
  cellIsPlaceholder: (cellCtx: any) => boolean;
463
466
  groupSubRowCount: (row: any) => any;
467
+ groupRowDescriptorCache: any;
468
+ groupRowDescriptorCacheVer: any;
469
+ groupRowDescriptor: (row: any) => any;
470
+ cellSlotRow: (row: any) => any;
464
471
  groupingKeys: () => any;
465
472
  groupableColumns: () => any[];
466
473
  stopEvent: (evt: any) => void;
package/dist/index.d.mts CHANGED
@@ -308,7 +308,9 @@ declare class DataTable extends DataTable_base {
308
308
  columnDefsCache: any[] | null;
309
309
  columnDefsCacheColumnsRef: any;
310
310
  columnDefsCacheColRegRef: any;
311
+ columnDefsIndexCache: any;
311
312
  columnDefs: () => any[];
313
+ defIndex: () => any;
312
314
  SELECT_COL_ID: string;
313
315
  EXPANDER_COL_ID: string;
314
316
  selectionEnabled: () => boolean;
@@ -456,11 +458,16 @@ declare class DataTable extends DataTable_base {
456
458
  onToggleExpand: (row: any, evt: any) => void;
457
459
  bodyCellStyle: (row: any, colId: any) => string;
458
460
  rowIsGrouped: (row: any) => boolean;
461
+ rowIndexIsGrouped: (rowIndex: any) => boolean;
459
462
  groupingActive: () => boolean;
460
463
  cellIsGrouped: (cellCtx: any) => boolean;
461
464
  cellIsAggregated: (cellCtx: any) => boolean;
462
465
  cellIsPlaceholder: (cellCtx: any) => boolean;
463
466
  groupSubRowCount: (row: any) => any;
467
+ groupRowDescriptorCache: any;
468
+ groupRowDescriptorCacheVer: any;
469
+ groupRowDescriptor: (row: any) => any;
470
+ cellSlotRow: (row: any) => any;
464
471
  groupingKeys: () => any;
465
472
  groupableColumns: () => any[];
466
473
  stopEvent: (evt: any) => void;
package/dist/index.mjs CHANGED
@@ -20,6 +20,39 @@ const wrapAggregationFn = (fn) => {
20
20
  }
21
21
  };
22
22
  };
23
+ const collectNestedDefs = (list, out) => {
24
+ if (!Array.isArray(list)) return;
25
+ for (const d of list) {
26
+ if (!d || d.id == null) continue;
27
+ const id = String(d.id);
28
+ if (!(id in out)) out[id] = d;
29
+ if (Array.isArray(d.columns)) collectNestedDefs(d.columns, out);
30
+ }
31
+ };
32
+ const indexDefsById = (defs) => {
33
+ const out = Object.create(null);
34
+ if (!Array.isArray(defs)) return out;
35
+ for (const d of defs) {
36
+ if (!d || d.id == null) continue;
37
+ out[String(d.id)] = d;
38
+ }
39
+ for (const d of defs) if (d && Array.isArray(d.columns)) collectNestedDefs(d.columns, out);
40
+ return out;
41
+ };
42
+ const collectGroupableLeafDefs = (defs) => {
43
+ const out = [];
44
+ if (!Array.isArray(defs)) return out;
45
+ for (const d of defs) {
46
+ if (!d) continue;
47
+ if (Array.isArray(d.columns)) {
48
+ out.push(...collectGroupableLeafDefs(d.columns));
49
+ continue;
50
+ }
51
+ if (d.groupable === false) continue;
52
+ out.push(d);
53
+ }
54
+ return out;
55
+ };
23
56
  //#endregion
24
57
  //#region src/helpers/indexMath.ts
25
58
  const clamp = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
@@ -464,6 +497,7 @@ let DataTable = class DataTable extends SignalWatcher(LitElement) {
464
497
  this.columnDefsCache = null;
465
498
  this.columnDefsCacheColumnsRef = void 0;
466
499
  this.columnDefsCacheColRegRef = void 0;
500
+ this.columnDefsIndexCache = null;
467
501
  this.columnDefs = () => {
468
502
  const cfg = this.columns || [];
469
503
  const reg = this._colReg.value || {};
@@ -507,8 +541,13 @@ let DataTable = class DataTable extends SignalWatcher(LitElement) {
507
541
  this.columnDefsCache = out;
508
542
  this.columnDefsCacheColumnsRef = this.columns;
509
543
  this.columnDefsCacheColRegRef = this._colReg.value;
544
+ this.columnDefsIndexCache = indexDefsById(out);
510
545
  return out;
511
546
  };
547
+ this.defIndex = () => {
548
+ this.columnDefs();
549
+ return this.columnDefsIndexCache || Object.create(null);
550
+ };
512
551
  this.SELECT_COL_ID = "__rdt_select";
513
552
  this.EXPANDER_COL_ID = "__rdt_expander";
514
553
  this.selectionEnabled = () => this.selectionMode === "single" || this.selectionMode === "multiple";
@@ -1250,9 +1289,9 @@ let DataTable = class DataTable extends SignalWatcher(LitElement) {
1250
1289
  return "";
1251
1290
  };
1252
1291
  this.defFor = (colId) => {
1253
- const defs = this.columnDefs();
1254
- for (const d of defs) if (d.id === colId) return d;
1255
- return null;
1292
+ if (colId == null) return null;
1293
+ const d = this.defIndex()[String(colId)];
1294
+ return d != null ? d : null;
1256
1295
  };
1257
1296
  this.visibleCellsFor = (row) => this._rowModelVer.value >= 0 ? row.getVisibleCells() : [];
1258
1297
  this.editMetaOf = (colId) => {
@@ -1429,22 +1468,56 @@ let DataTable = class DataTable extends SignalWatcher(LitElement) {
1429
1468
  return base;
1430
1469
  };
1431
1470
  this.rowIsGrouped = (row) => !!(this.tick() >= 0 && row && row.getIsGrouped && row.getIsGrouped());
1471
+ this.rowIndexIsGrouped = (rowIndex) => this.rowIsGrouped((this._rows.value || [])[rowIndex]);
1432
1472
  this.groupingActive = () => this.tick() >= 0 && (this.currentState().grouping || []).length > 0;
1433
1473
  this.cellIsGrouped = (cellCtx) => !!(this.tick() >= 0 && cellCtx && cellCtx.getIsGrouped && cellCtx.getIsGrouped());
1434
1474
  this.cellIsAggregated = (cellCtx) => !!(this.tick() >= 0 && cellCtx && cellCtx.getIsAggregated && cellCtx.getIsAggregated());
1435
1475
  this.cellIsPlaceholder = (cellCtx) => !!(this.tick() >= 0 && cellCtx && cellCtx.getIsPlaceholder && cellCtx.getIsPlaceholder());
1436
- this.groupSubRowCount = (row) => row && row.getLeafRows ? row.getLeafRows().length : row && row.subRows ? row.subRows.length : 0;
1476
+ this.groupSubRowCount = (row) => {
1477
+ if (row && row.getLeafRows) {
1478
+ const leaves = row.getLeafRows();
1479
+ let n = 0;
1480
+ for (let i = 0; i < leaves.length; i++) {
1481
+ const r = leaves[i];
1482
+ if (!(r && r.getIsGrouped && r.getIsGrouped())) n = n + 1;
1483
+ }
1484
+ return n;
1485
+ }
1486
+ return row && row.subRows ? row.subRows.length : 0;
1487
+ };
1488
+ this.groupRowDescriptorCache = null;
1489
+ this.groupRowDescriptorCacheVer = void 0;
1490
+ this.groupRowDescriptor = (row) => {
1491
+ const ver = this._rowModelVer.value;
1492
+ if (!this.groupRowDescriptorCache || this.groupRowDescriptorCacheVer !== ver) {
1493
+ this.groupRowDescriptorCache = Object.create(null);
1494
+ this.groupRowDescriptorCacheVer = ver;
1495
+ }
1496
+ const key = String(row.id);
1497
+ let d = this.groupRowDescriptorCache[key];
1498
+ if (!d) {
1499
+ const groupingColumnId = row.groupingColumnId != null ? row.groupingColumnId : "";
1500
+ const groupingValue = row.getGroupingValue ? row.getGroupingValue(groupingColumnId) : row.groupingValue;
1501
+ d = {
1502
+ isGroupRow: true,
1503
+ groupId: row.id,
1504
+ groupingColumnId,
1505
+ groupingValue,
1506
+ leafCount: this.groupSubRowCount(row)
1507
+ };
1508
+ this.groupRowDescriptorCache[key] = d;
1509
+ }
1510
+ return d;
1511
+ };
1512
+ this.cellSlotRow = (row) => this.rowIsGrouped(row) ? this.groupRowDescriptor(row) : row ? row.original : null;
1437
1513
  this.groupingKeys = () => this.currentState().grouping || [];
1438
1514
  this.groupableColumns = () => {
1439
1515
  const out = [];
1440
- const defs = this.columnDefs();
1441
- for (const d of defs) {
1442
- if (!d || d.groupable === false) continue;
1443
- out.push({
1444
- id: d.id,
1445
- label: d.header != null ? d.header : d.id
1446
- });
1447
- }
1516
+ const defs = collectGroupableLeafDefs(this.columnDefs());
1517
+ for (const d of defs) out.push({
1518
+ id: d.id,
1519
+ label: d.header != null ? d.header : d.id
1520
+ });
1448
1521
  return out;
1449
1522
  };
1450
1523
  this.stopEvent = (evt) => {
@@ -2442,11 +2515,13 @@ let DataTable = class DataTable extends SignalWatcher(LitElement) {
2442
2515
  for (let gr = 0; gr < grid.length; gr++) {
2443
2516
  const r = originRow + gr;
2444
2517
  if (r > maxRow) break;
2518
+ const rowGrouped = this.rowIndexIsGrouped(r);
2445
2519
  const cols = grid[gr] || [];
2446
2520
  for (let gc = 0; gc < cols.length; gc++) {
2447
2521
  const c = originCol + gc;
2448
2522
  if (c > maxCol) break;
2449
2523
  total = total + 1;
2524
+ if (rowGrouped) continue;
2450
2525
  const colId = this.columnIdAt(r, c);
2451
2526
  if (colId == null || !this.columnEditable(colId)) continue;
2452
2527
  const rowObj = this.rowOriginalAt(r);
@@ -2756,11 +2831,13 @@ let DataTable = class DataTable extends SignalWatcher(LitElement) {
2756
2831
  return cell && cell.column ? cell.column.id : null;
2757
2832
  };
2758
2833
  this.isActiveCellEditable = () => {
2834
+ if (this.rowIndexIsGrouped(this._activeRow.value)) return false;
2759
2835
  const colId = this.activeCellColumnId();
2760
2836
  return colId != null && this.columnEditable(colId);
2761
2837
  };
2762
2838
  this.isEditing = (rowIndex, colIndex) => {
2763
2839
  if (this._editVer.value < 0) return false;
2840
+ if (this.rowIndexIsGrouped(rowIndex)) return false;
2764
2841
  if (this._editingRowIndex.value != null && this._editingRowIndex.value === rowIndex) {
2765
2842
  const colId = this.columnIdAt(rowIndex, colIndex);
2766
2843
  return colId != null && this.columnEditable(colId);
@@ -2860,6 +2937,7 @@ let DataTable = class DataTable extends SignalWatcher(LitElement) {
2860
2937
  return cell ? cell.getValue() : null;
2861
2938
  };
2862
2939
  this.beginEdit = (rowIndex, colIndex, seed) => {
2940
+ if (this.rowIndexIsGrouped(rowIndex)) return;
2863
2941
  const colId = this.columnIdAt(rowIndex, colIndex);
2864
2942
  if (colId == null || !this.columnEditable(colId)) return;
2865
2943
  this.committedThisSession = false;
@@ -2977,6 +3055,7 @@ let DataTable = class DataTable extends SignalWatcher(LitElement) {
2977
3055
  return true;
2978
3056
  };
2979
3057
  this.toggleActiveBooleanCell = () => {
3058
+ if (this.rowIndexIsGrouped(this._activeRow.value)) return;
2980
3059
  const colId = this.columnIdAt(this._activeRow.value, this._activeColIndex.value);
2981
3060
  if (colId == null || !this.columnEditable(colId)) return;
2982
3061
  const row = (this._rows.value || [])[this._activeRow.value];
@@ -3066,6 +3145,7 @@ let DataTable = class DataTable extends SignalWatcher(LitElement) {
3066
3145
  this.beginRowEdit = (row) => {
3067
3146
  const rowIndex = this.rowIndexOf(row);
3068
3147
  if (rowIndex < 0) return;
3148
+ if (this.rowIndexIsGrouped(rowIndex)) return;
3069
3149
  const editable = this.editableColumnsForRow(rowIndex);
3070
3150
  if (editable.length === 0) return;
3071
3151
  this.committedThisSession = false;
@@ -4345,13 +4425,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
4345
4425
  this.onToggleExpand(wr.row, $event);
4346
4426
  }} 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>
4347
4427
  ${this.selectCell !== void 0 ? this.selectCell({
4348
- row: wr.row.original,
4428
+ row: this.cellSlotRow(wr.row),
4349
4429
  checked: this.rowIsSelected(wr.row),
4350
4430
  toggle: (e) => this.onToggleRow(wr.row, e)
4351
4431
  }) : html`<slot name="selectCell" data-rozie-params=${(() => {
4352
4432
  try {
4353
4433
  return JSON.stringify({
4354
- row: wr.row.original,
4434
+ row: this.cellSlotRow(wr.row),
4355
4435
  checked: this.rowIsSelected(wr.row)
4356
4436
  });
4357
4437
  } catch {
@@ -4370,14 +4450,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
4370
4450
  ${this.cell !== void 0 ? this.cell({
4371
4451
  columnId: cell.column.id,
4372
4452
  column: cell.column,
4373
- row: wr.row.original,
4453
+ row: this.cellSlotRow(wr.row),
4374
4454
  value: cell.getValue()
4375
4455
  }) : html`<slot name="cell" data-rozie-params=${(() => {
4376
4456
  try {
4377
4457
  return JSON.stringify({
4378
4458
  columnId: cell.column.id,
4379
4459
  column: cell.column,
4380
- row: wr.row.original,
4460
+ row: this.cellSlotRow(wr.row),
4381
4461
  value: cell.getValue()
4382
4462
  });
4383
4463
  } catch {
@@ -4391,7 +4471,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
4391
4471
  ${this.editor !== void 0 ? this.editor({
4392
4472
  columnId: cell.column.id,
4393
4473
  column: cell.column,
4394
- row: wr.row.original,
4474
+ row: this.cellSlotRow(wr.row),
4395
4475
  value: this.editorValueFor(cell.column.id),
4396
4476
  commit: this.editorCommitFor(cell.column.id),
4397
4477
  cancel: this.editorCancelFor(),
@@ -4401,7 +4481,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
4401
4481
  return JSON.stringify({
4402
4482
  columnId: cell.column.id,
4403
4483
  column: cell.column,
4404
- row: wr.row.original,
4484
+ row: this.cellSlotRow(wr.row),
4405
4485
  value: this.editorValueFor(cell.column.id),
4406
4486
  commit: this.editorCommitFor(cell.column.id),
4407
4487
  cancel: this.editorCancelFor(),
@@ -4441,14 +4521,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
4441
4521
  ${this.cell !== void 0 ? this.cell({
4442
4522
  columnId: cell.column.id,
4443
4523
  column: cell.column,
4444
- row: wr.row.original,
4524
+ row: this.cellSlotRow(wr.row),
4445
4525
  value: cell.getValue()
4446
4526
  }) : html`<slot name="cell" data-rozie-params=${(() => {
4447
4527
  try {
4448
4528
  return JSON.stringify({
4449
4529
  columnId: cell.column.id,
4450
4530
  column: cell.column,
4451
- row: wr.row.original,
4531
+ row: this.cellSlotRow(wr.row),
4452
4532
  value: cell.getValue()
4453
4533
  });
4454
4534
  } catch {
@@ -4639,13 +4719,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
4639
4719
  this.onToggleExpand(row, $event);
4640
4720
  }} 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>
4641
4721
  ${this.selectCell !== void 0 ? this.selectCell({
4642
- row: row.original,
4722
+ row: this.cellSlotRow(row),
4643
4723
  checked: this.rowIsSelected(row),
4644
4724
  toggle: (e) => this.onToggleRow(row, e)
4645
4725
  }) : html`<slot name="selectCell" data-rozie-params=${(() => {
4646
4726
  try {
4647
4727
  return JSON.stringify({
4648
- row: row.original,
4728
+ row: this.cellSlotRow(row),
4649
4729
  checked: this.rowIsSelected(row)
4650
4730
  });
4651
4731
  } catch {
@@ -4664,14 +4744,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
4664
4744
  ${this.cell !== void 0 ? this.cell({
4665
4745
  columnId: cell.column.id,
4666
4746
  column: cell.column,
4667
- row: row.original,
4747
+ row: this.cellSlotRow(row),
4668
4748
  value: cell.getValue()
4669
4749
  }) : html`<slot name="cell" data-rozie-params=${(() => {
4670
4750
  try {
4671
4751
  return JSON.stringify({
4672
4752
  columnId: cell.column.id,
4673
4753
  column: cell.column,
4674
- row: row.original,
4754
+ row: this.cellSlotRow(row),
4675
4755
  value: cell.getValue()
4676
4756
  });
4677
4757
  } catch {
@@ -4685,7 +4765,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
4685
4765
  ${this.editor !== void 0 ? this.editor({
4686
4766
  columnId: cell.column.id,
4687
4767
  column: cell.column,
4688
- row: row.original,
4768
+ row: this.cellSlotRow(row),
4689
4769
  value: this.editorValueFor(cell.column.id),
4690
4770
  commit: this.editorCommitFor(cell.column.id),
4691
4771
  cancel: this.editorCancelFor(),
@@ -4695,7 +4775,7 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
4695
4775
  return JSON.stringify({
4696
4776
  columnId: cell.column.id,
4697
4777
  column: cell.column,
4698
- row: row.original,
4778
+ row: this.cellSlotRow(row),
4699
4779
  value: this.editorValueFor(cell.column.id),
4700
4780
  commit: this.editorCommitFor(cell.column.id),
4701
4781
  cancel: this.editorCancelFor(),
@@ -4735,14 +4815,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
4735
4815
  ${this.cell !== void 0 ? this.cell({
4736
4816
  columnId: cell.column.id,
4737
4817
  column: cell.column,
4738
- row: row.original,
4818
+ row: this.cellSlotRow(row),
4739
4819
  value: cell.getValue()
4740
4820
  }) : html`<slot name="cell" data-rozie-params=${(() => {
4741
4821
  try {
4742
4822
  return JSON.stringify({
4743
4823
  columnId: cell.column.id,
4744
4824
  column: cell.column,
4745
- row: row.original,
4825
+ row: this.cellSlotRow(row),
4746
4826
  value: cell.getValue()
4747
4827
  });
4748
4828
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rozie-ui/data-table-lit",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "license": "MIT",
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 } from './helpers/columnDefUtils';
8
+ import { isSafeKey, wrapAggregationFn, indexDefsById, 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';
@@ -1335,22 +1335,22 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
1335
1335
 
1336
1336
  ${this.isExpanderColumn(cell.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
1337
1337
  ${this.rowCanExpand(wr.row) ? html`<button class="rdt-expander" type="button" data-expander="" aria-expanded=${!!this.rowIsExpanded(wr.row)} aria-label=${rozieAttr(this.rowIsExpanded(wr.row) ? 'Collapse row' : 'Expand row')} @click=${($event: MouseEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.onToggleExpand(wr.row, $event); }} data-rozie-s-d5dcab4c>${rozieDisplay(this.rowIsExpanded(wr.row) ? '▾' : '▸')}</button>` : nothing}</span>` : this.isSelectColumn(cell.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
1338
- ${this.selectCell !== undefined ? this.selectCell({row: wr.row.original, checked: this.rowIsSelected(wr.row), toggle: e => this.onToggleRow(wr.row, e)}) : html`<slot name="selectCell" data-rozie-params=${(() => { try { return JSON.stringify({row: wr.row.original, 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)}>
1338
+ ${this.selectCell !== undefined ? this.selectCell({row: this.cellSlotRow(wr.row), checked: this.rowIsSelected(wr.row), toggle: e => this.onToggleRow(wr.row, e)}) : html`<slot name="selectCell" data-rozie-params=${(() => { try { return JSON.stringify({row: this.cellSlotRow(wr.row), checked: this.rowIsSelected(wr.row)}); } catch { return '{}'; } })()} @rozie-select-cell-toggle=${($event: CustomEvent) => ((e => this.onToggleRow(wr.row, e)) as (...args: any[]) => any)($event.detail)}>
1339
1339
  <input class="rdt-select-row" type="checkbox" aria-label="Select row" ?checked=${this.rowIsSelected(wr.row)} @change=${($event: Event & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onToggleRow(wr.row, $event); }} data-rozie-s-d5dcab4c />
1340
1340
  </slot>`}
1341
1341
  </span>` : this.cellIsGrouped(cell) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
1342
1342
  <button class="rdt-expander rdt-group-toggle" type="button" data-expander="" aria-expanded=${!!this.rowIsExpanded(wr.row)} aria-label=${rozieAttr(this.rowIsExpanded(wr.row) ? 'Collapse group' : 'Expand group')} @click=${($event: MouseEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.onToggleExpand(wr.row, $event); }} data-rozie-s-d5dcab4c>${rozieDisplay(this.rowIsExpanded(wr.row) ? '▾' : '▸')}</button>
1343
1343
  <span class="rdt-group-value" data-rozie-s-d5dcab4c>
1344
- ${this.cell !== undefined ? this.cell({columnId: cell.column.id, column: cell.column, row: wr.row.original, value: cell.getValue()}) : html`<slot name="cell" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: wr.row.original, value: cell.getValue()}); } catch { return '{}'; } })()}>${rozieDisplay(cell.getValue())}</slot>`}
1344
+ ${this.cell !== undefined ? this.cell({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: cell.getValue()}) : html`<slot name="cell" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: cell.getValue()}); } catch { return '{}'; } })()}>${rozieDisplay(cell.getValue())}</slot>`}
1345
1345
  </span>
1346
1346
  <span class="rdt-group-count" data-rozie-s-d5dcab4c>${rozieDisplay('(' + this.groupSubRowCount(wr.row) + ')')}</span>
1347
1347
  </span>` : this.isEditing(wr.vi.index, this.colIndexOf(wr.row, cell)) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
1348
1348
  ${this.hasEditorSlot(cell.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
1349
- ${this.editor !== undefined ? this.editor({columnId: cell.column.id, column: cell.column, row: wr.row.original, 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: wr.row.original, value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index)}); } catch { return '{}'; } })()}></slot>`}
1349
+ ${this.editor !== undefined ? this.editor({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index)}) : html`<slot name="editor" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), autofocus: this.editorAutofocusFor(cell.column.id, wr.vi.index)}); } catch { return '{}'; } })()}></slot>`}
1350
1350
  </span>` : this.editorTypeOf(cell.column.id) === 'number' ? html`<input class="rdt-cell-editor" type="number" data-editing-cell="" .value=${this.editorValueFor(cell.column.id)} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />` : this.editorTypeOf(cell.column.id) === 'select' ? html`<select class="rdt-cell-editor" data-editing-cell="" .value=${this.editorValueFor(cell.column.id)} @change=${($event: Event & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c>
1351
1351
  ${repeat<any>(this.editorOptionsOf(cell.column.id), (opt, _idx) => opt.value, (opt, _idx) => html`<option value=${rozieAttr(opt.value)} data-rozie-s-d5dcab4c>${rozieDisplay(opt.label)}</option>`)}
1352
1352
  </select>` : this.editorTypeOf(cell.column.id) === 'checkbox' ? html`<input class="rdt-cell-editor" type="checkbox" data-editing-cell="" ?checked=${this.editorCheckedFor(cell.column.id)} @change=${($event: Event & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorCheckbox(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />` : html`<input class="rdt-cell-editor" type="text" data-editing-cell="" .value=${this.editorValueFor(cell.column.id)} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />`}</span>` : this.cellIsPlaceholder(cell) ? html`<span style="display:contents" data-rozie-s-d5dcab4c></span>` : html`<span class="rdt-cell-value" data-rozie-s-d5dcab4c>
1353
- ${this.cell !== undefined ? this.cell({columnId: cell.column.id, column: cell.column, row: wr.row.original, value: cell.getValue()}) : html`<slot name="cell" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: wr.row.original, value: cell.getValue()}); } catch { return '{}'; } })()}>${rozieDisplay(cell.getValue())}</slot>`}
1353
+ ${this.cell !== undefined ? this.cell({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: cell.getValue()}) : html`<slot name="cell" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(wr.row), value: cell.getValue()}); } catch { return '{}'; } })()}>${rozieDisplay(cell.getValue())}</slot>`}
1354
1354
  </span>`}${this.isFillHandleCell(wr.vi.index, this.colIndexOf(wr.row, cell)) ? html`<span class="rdt-fill-handle" data-fill-handle="" data-testid="fill-handle" aria-hidden="true" @pointerdown=${($event: PointerEvent & { currentTarget: HTMLSpanElement; target: HTMLSpanElement }) => { this.onFillHandlePointerDown($event); }} data-rozie-s-d5dcab4c></span>` : nothing}</td>`)}
1355
1355
 
1356
1356
  ${this.colsWindowed() ? html`<td class="rdt-col-spacer" aria-hidden="true" style=${rozieStyle('width:' + this.colPadRight() + 'px;padding:0;border:0')} data-rozie-s-d5dcab4c></td>` : nothing}</tr>
@@ -1416,22 +1416,22 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
1416
1416
 
1417
1417
  ${this.isExpanderColumn(cell.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
1418
1418
  ${this.rowCanExpand(row) ? html`<button class="rdt-expander" type="button" data-expander="" aria-expanded=${!!this.rowIsExpanded(row)} aria-label=${rozieAttr(this.rowIsExpanded(row) ? 'Collapse row' : 'Expand row')} @click=${($event: MouseEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.onToggleExpand(row, $event); }} data-rozie-s-d5dcab4c>${rozieDisplay(this.rowIsExpanded(row) ? '▾' : '▸')}</button>` : nothing}</span>` : this.isSelectColumn(cell.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
1419
- ${this.selectCell !== undefined ? this.selectCell({row: row.original, checked: this.rowIsSelected(row), toggle: e => this.onToggleRow(row, e)}) : html`<slot name="selectCell" data-rozie-params=${(() => { try { return JSON.stringify({row: row.original, checked: this.rowIsSelected(row)}); } catch { return '{}'; } })()} @rozie-select-cell-toggle=${($event: CustomEvent) => ((e => this.onToggleRow(row, e)) as (...args: any[]) => any)($event.detail)}>
1419
+ ${this.selectCell !== undefined ? this.selectCell({row: this.cellSlotRow(row), checked: this.rowIsSelected(row), toggle: e => this.onToggleRow(row, e)}) : html`<slot name="selectCell" data-rozie-params=${(() => { try { return JSON.stringify({row: this.cellSlotRow(row), checked: this.rowIsSelected(row)}); } catch { return '{}'; } })()} @rozie-select-cell-toggle=${($event: CustomEvent) => ((e => this.onToggleRow(row, e)) as (...args: any[]) => any)($event.detail)}>
1420
1420
  <input class="rdt-select-row" type="checkbox" aria-label="Select row" ?checked=${this.rowIsSelected(row)} @change=${($event: Event & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onToggleRow(row, $event); }} data-rozie-s-d5dcab4c />
1421
1421
  </slot>`}
1422
1422
  </span>` : this.cellIsGrouped(cell) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
1423
1423
  <button class="rdt-expander rdt-group-toggle" type="button" data-expander="" aria-expanded=${!!this.rowIsExpanded(row)} aria-label=${rozieAttr(this.rowIsExpanded(row) ? 'Collapse group' : 'Expand group')} @click=${($event: MouseEvent & { currentTarget: HTMLButtonElement; target: HTMLButtonElement }) => { this.onToggleExpand(row, $event); }} data-rozie-s-d5dcab4c>${rozieDisplay(this.rowIsExpanded(row) ? '▾' : '▸')}</button>
1424
1424
  <span class="rdt-group-value" data-rozie-s-d5dcab4c>
1425
- ${this.cell !== undefined ? this.cell({columnId: cell.column.id, column: cell.column, row: row.original, value: cell.getValue()}) : html`<slot name="cell" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: row.original, value: cell.getValue()}); } catch { return '{}'; } })()}>${rozieDisplay(cell.getValue())}</slot>`}
1425
+ ${this.cell !== undefined ? this.cell({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: cell.getValue()}) : html`<slot name="cell" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: cell.getValue()}); } catch { return '{}'; } })()}>${rozieDisplay(cell.getValue())}</slot>`}
1426
1426
  </span>
1427
1427
  <span class="rdt-group-count" data-rozie-s-d5dcab4c>${rozieDisplay('(' + this.groupSubRowCount(row) + ')')}</span>
1428
1428
  </span>` : this.isEditing(this.rowIndexOf(row), this.colIndexOf(row, cell)) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
1429
1429
  ${this.hasEditorSlot(cell.column.id) ? html`<span style="display:contents" data-rozie-s-d5dcab4c>
1430
- ${this.editor !== undefined ? this.editor({columnId: cell.column.id, column: cell.column, row: row.original, 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: row.original, value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row))}); } catch { return '{}'; } })()}></slot>`}
1430
+ ${this.editor !== undefined ? this.editor({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row))}) : html`<slot name="editor" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: this.editorValueFor(cell.column.id), commit: this.editorCommitFor(cell.column.id), cancel: this.editorCancelFor(), autofocus: this.editorAutofocusFor(cell.column.id, this.rowIndexOf(row))}); } catch { return '{}'; } })()}></slot>`}
1431
1431
  </span>` : this.editorTypeOf(cell.column.id) === 'number' ? html`<input class="rdt-cell-editor" type="number" data-editing-cell="" .value=${this.editorValueFor(cell.column.id)} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />` : this.editorTypeOf(cell.column.id) === 'select' ? html`<select class="rdt-cell-editor" data-editing-cell="" .value=${this.editorValueFor(cell.column.id)} @change=${($event: Event & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLSelectElement; target: HTMLSelectElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c>
1432
1432
  ${repeat<any>(this.editorOptionsOf(cell.column.id), (opt, _idx) => opt.value, (opt, _idx) => html`<option value=${rozieAttr(opt.value)} data-rozie-s-d5dcab4c>${rozieDisplay(opt.label)}</option>`)}
1433
1433
  </select>` : this.editorTypeOf(cell.column.id) === 'checkbox' ? html`<input class="rdt-cell-editor" type="checkbox" data-editing-cell="" ?checked=${this.editorCheckedFor(cell.column.id)} @change=${($event: Event & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorCheckbox(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />` : html`<input class="rdt-cell-editor" type="text" data-editing-cell="" .value=${this.editorValueFor(cell.column.id)} @input=${($event: InputEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onCellEditorInput(cell.column.id, $event); }} @keydown=${($event: KeyboardEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorKeyDown($event); }} @blur=${($event: FocusEvent & { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { this.onEditorBlur($event); }} data-rozie-s-d5dcab4c />`}</span>` : this.cellIsPlaceholder(cell) ? html`<span style="display:contents" data-rozie-s-d5dcab4c></span>` : html`<span class="rdt-cell-value" data-rozie-s-d5dcab4c>
1434
- ${this.cell !== undefined ? this.cell({columnId: cell.column.id, column: cell.column, row: row.original, value: cell.getValue()}) : html`<slot name="cell" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: row.original, value: cell.getValue()}); } catch { return '{}'; } })()}>${rozieDisplay(cell.getValue())}</slot>`}
1434
+ ${this.cell !== undefined ? this.cell({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: cell.getValue()}) : html`<slot name="cell" data-rozie-params=${(() => { try { return JSON.stringify({columnId: cell.column.id, column: cell.column, row: this.cellSlotRow(row), value: cell.getValue()}); } catch { return '{}'; } })()}>${rozieDisplay(cell.getValue())}</slot>`}
1435
1435
  </span>`}${this.isFillHandleCell(this.rowIndexOf(row), this.colIndexOf(row, cell)) ? html`<span class="rdt-fill-handle" data-fill-handle="" data-testid="fill-handle" aria-hidden="true" @pointerdown=${($event: PointerEvent & { currentTarget: HTMLSpanElement; target: HTMLSpanElement }) => { this.onFillHandlePointerDown($event); }} data-rozie-s-d5dcab4c></span>` : nothing}</td>`)}
1436
1436
  </tr>
1437
1437
 
@@ -1770,6 +1770,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
1770
1770
 
1771
1771
  columnDefsCacheColRegRef: any = undefined;
1772
1772
 
1773
+ // B1 (quick 260906-afh) — the def-lookup index, populated INSIDE this SAME memo block on
1774
+ // a cache MISS only (never rebuilt per call — a per-call recursive scan would restore the
1775
+ // O(N^2 x M) blowup D-23 removed, T-AFH-03). A top-level `let` lowers to a per-instance
1776
+ // `useRef` on React (verified in the emitted leaf), so this is instance-scoped like
1777
+ // columnDefsCache above.
1778
+ columnDefsIndexCache: any = null;
1779
+
1773
1780
  columnDefs = () => {
1774
1781
  const cfg = this.columns || [];
1775
1782
  const reg = this._colReg.value || {};
@@ -1818,9 +1825,20 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
1818
1825
  this.columnDefsCache = out;
1819
1826
  this.columnDefsCacheColumnsRef = this.columns;
1820
1827
  this.columnDefsCacheColRegRef = this._colReg.value;
1828
+ this.columnDefsIndexCache = indexDefsById(out);
1821
1829
  return out;
1822
1830
  };
1823
1831
 
1832
+ // defIndex: the def-lookup map for the CURRENT columnDefs() array. Calling columnDefs()
1833
+ // first is deliberate — either an O(1) reference-equality cache hit (columnDefsIndexCache
1834
+ // is already fresh) or a miss that repopulates BOTH slots together, so the index can never
1835
+ // go stale relative to the array. Falls back to an empty null-prototype map (never null/
1836
+ // undefined) so a caller can read it unconditionally.
1837
+ defIndex = () => {
1838
+ this.columnDefs();
1839
+ return this.columnDefsIndexCache || Object.create(null);
1840
+ };
1841
+
1824
1842
  // The constant id of the auto-injected leading checkbox column (D-04). Distinct from
1825
1843
  // any consumer column id (the registry/config guard never produces a leading "__").
1826
1844
  SELECT_COL_ID = '__rdt_select';
@@ -3560,10 +3578,18 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
3560
3578
 
3561
3579
  // Template helpers reading the resolved column-def metadata by id (plain fns — used
3562
3580
  // in template predicates + interpolation; uniform on all 6, no $computed alias trap).
3581
+ // B1 (quick 260906-afh): reads the O(1) def-lookup index (defIndex(), columnBuilders.rzts)
3582
+ // instead of linearly scanning columnDefs() — which only ever held TOP-LEVEL entries, so a
3583
+ // NESTED group leaf (a `columns:` group child) was invisible to every caller (headerLabel,
3584
+ // columnIsFilterable, editMetaOf/columnEditable/editorTypeOf/editorOptionsOf) and silently
3585
+ // took the "no def" fallback. defIndex() resolves both top-level AND nested leaf ids. This
3586
+ // also removes the per-cell O(N) scan D-23 left in place — defIndex() is memoized in the
3587
+ // SAME cache-invalidation block as columnDefs() itself, never rebuilt per call.
3563
3588
  defFor = (colId: any) => {
3564
- const defs = this.columnDefs();
3565
- for (const d of defs as any) if (d.id === colId) return d;
3566
- return null;
3589
+ if (colId == null) return null;
3590
+ const idx = this.defIndex();
3591
+ const d = idx[String(colId)];
3592
+ return d != null ? d : null;
3567
3593
  };
3568
3594
 
3569
3595
  // Per-row visible cells for the body loop. table-core memoizes row objects by id,
@@ -3925,6 +3951,15 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
3925
3951
  // rowIsGrouped: this flattened row is a group-header row.
3926
3952
  rowIsGrouped = (row: any) => !!(this.tick() >= 0 && row && row.getIsGrouped && row.getIsGrouped());
3927
3953
 
3954
+ // rowIndexIsGrouped (quick 260906-cvo, C1/C2): THE single row-level editability/writability
3955
+ // predicate — a group-header row is a SYNTHETIC row whose `original` is table-core's
3956
+ // `leafRows[0].original` (the same fact cellSlotRow's header comment documents), so any
3957
+ // edit or bulk write resolved through it silently lands on an UNRELATED record. Every edit/
3958
+ // write guard in this component calls THIS predicate — never a re-implemented inline
3959
+ // `getIsGrouped()` check — so the invariant has one definition. Indexes into the SAME
3960
+ // `$data.rows` flattened row list rowIsGrouped reads.
3961
+ rowIndexIsGrouped = (rowIndex: any) => this.rowIsGrouped((this._rows.value || [])[rowIndex]);
3962
+
3928
3963
  // groupingActive: grouping is currently engaged (a non-empty ordered key list). Drives the
3929
3964
  // data-group-leaf marker so it is ABSENT when ungrouped (byte-identical-off, req-10).
3930
3965
  groupingActive = () => this.tick() >= 0 && (this.currentState().grouping || []).length > 0;
@@ -3937,34 +3972,119 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
3937
3972
 
3938
3973
  cellIsAggregated = (cellCtx: any) => !!(this.tick() >= 0 && cellCtx && cellCtx.getIsAggregated && cellCtx.getIsAggregated());
3939
3974
 
3940
- // cellIsPlaceholder: a PLACEHOLDER cell on a group-header row — a non-grouped, non-aggregated
3941
- // cell that table-core fills with the FIRST leaf row's value (cell.getValue() leaks e.g.
3942
- // "Services"/"Edsger Dijkstra" onto the group line). Renders BLANK via a dedicated empty
3943
- // template branch so the leaked leaf value never paints. Tick-gated exactly like cellIsGrouped
3944
- // so the group chrome re-derives on a re-pull on the fine-grained targets (Solid/Lit).
3975
+ // cellIsPlaceholder: VERIFIED against @tanstack/table-core@8.21.3 (ColumnGrouping.js:120-121,
3976
+ // quick 260906-afh premise correction). A placeholder cell is a cell IN A GROUPING COLUMN
3977
+ // that is NOT the row's OWN grouping column — `!cell.getIsGrouped() && column.getIsGrouped()`.
3978
+ // That is true both on a group-header row (for every OTHER grouping column) and on every leaf
3979
+ // row (for every grouping column) — it is NOT "never true on a group-header row" and it is NOT
3980
+ // "any non-grouped, non-aggregated cell". The operative consequence for this component: a
3981
+ // placeholder can only EVER be a grouping-column cell, so this branch can never reach a
3982
+ // non-grouping column — it does NOT and CANNOT guard the AGGREGATED-cell `row` leak
3983
+ // (cellSlotRow, below) fixes. Where it DOES apply (e.g. under multi-level grouping, the
3984
+ // OTHER grouping column on a group-header row), table-core still fills cell.getValue() with
3985
+ // the FIRST leaf row's value (cell.getValue() leaks e.g. "Oslo" onto a region-level group
3986
+ // line whose grouping is 'region', not 'city'); this branch renders BLANK via a dedicated
3987
+ // empty template so that leaked value never paints. Tick-gated exactly like cellIsGrouped so
3988
+ // the group chrome re-derives on a re-pull on the fine-grained targets (Solid/Lit).
3945
3989
  cellIsPlaceholder = (cellCtx: any) => !!(this.tick() >= 0 && cellCtx && cellCtx.getIsPlaceholder && cellCtx.getIsPlaceholder());
3946
3990
 
3947
- // groupSubRowCount: the number of underlying LEAF RECORDS under a group-header row (the count
3948
- // shown in the header, e.g. "North (40)"). row.subRows is the IMMEDIATE members — for MULTI-LEVEL
3949
- // grouping those are sub-GROUPS, not records, so "North" with 2 categories / 40 records would show
3950
- // "North (2)". getLeafRows() returns all leaf descendants (the actual record count); keep the
3951
- // subRows fallback for safety. Single-level grouping is unchanged (getLeafRows == subRows when the
3952
- // children are already leaves).
3953
- groupSubRowCount = (row: any) => row && row.getLeafRows ? row.getLeafRows().length : row && row.subRows ? row.subRows.length : 0;
3991
+ // groupSubRowCount: the number of underlying TRUE LEAF RECORDS under a group-header row (the
3992
+ // count shown in the header, e.g. "North (40)"). row.subRows is the IMMEDIATE members — for
3993
+ // MULTI-LEVEL grouping those are sub-GROUPS, not records, so "North" with 2 categories / 40
3994
+ // records would show "North (2)" off subRows alone. row.getLeafRows() is table-core's
3995
+ // `flattenBy(row.subRows, d => d.subRows)` (ColumnGrouping.js / utils.js, verified against
3996
+ // @tanstack/table-core@8.21.3) — it pushes EVERY node it walks, INCLUDING intermediate group
3997
+ // nodes, so under multi-level grouping it over-counts by exactly the number of sub-group
3998
+ // nodes between the row and its true leaves (C4, quick 260906-cvo: North with 2 city
3999
+ // sub-groups + 3 records reported 5, not 3). `row.getIsGrouped()` is `!!row.groupingColumnId`
4000
+ // — precisely the predicate that separates a group node from a real record — so filter the
4001
+ // flattened list on it rather than hand-rolling a recursive `subRows` walk (getLeafRows()
4002
+ // already flattens; the defect is purely that it does not filter). Single-level grouping is
4003
+ // numerically UNCHANGED: a leaf row has no `subRows`, so no node was ever miscounted there.
4004
+ groupSubRowCount = (row: any) => {
4005
+ if (row && row.getLeafRows) {
4006
+ const leaves = row.getLeafRows();
4007
+ let n = 0;
4008
+ for (let i = 0; i < leaves.length; i++) {
4009
+ const r = leaves[i];
4010
+ if (!(r && r.getIsGrouped && r.getIsGrouped())) n = n + 1;
4011
+ }
4012
+ return n;
4013
+ }
4014
+ return row && row.subRows ? row.subRows.length : 0;
4015
+ };
4016
+
4017
+ // ── B2 (quick 260906-afh) — group-header row #cell slot descriptor ─────────────────────
4018
+ // PROBLEM: table-core builds a flattened group-header row via
4019
+ // `createRow(table, id, leafRows[0].original, …)` (getGroupedRowModel.js), so `row.original`
4020
+ // on a group row IS the FIRST LEAF's record. The #cell scoped slot's existing 4 call sites
4021
+ // all bound `:row="row.original"` (or `wr.row.original`), so an AGGREGATED cell (a
4022
+ // non-grouping column on a group row — cellIsGrouped()===false, cellIsPlaceholder()===false,
4023
+ // so it takes the #cell r-else branch, same as any ordinary leaf cell) silently handed a
4024
+ // consumer template the WRONG record's fields (T-AFH-05). The grouped cell itself (the
4025
+ // group's own column, .rdt-group-value) has the same #cell binding and the same leak.
4026
+ //
4027
+ // FIX: `cellSlotRow(row)` returns a STABLE GROUP DESCRIPTOR — never `row.original` — for a
4028
+ // group-header row, and `row.original` unchanged for every other row. The descriptor is an
4029
+ // OBJECT, never `null` (T-AFH-06 — a consumer already reading `row.foo` would throw on
4030
+ // Vue/Angular if handed null).
4031
+ //
4032
+ // Descriptor cache (T-AFH-02/T-AFH-04): a null-prototype map keyed on `String(row.id)`,
4033
+ // invalidated wholesale whenever `$data.rowModelVer` (bumped by every refreshRowModel, the
4034
+ // SAME reactive re-derivation discipline visibleCellsFor/rowIsGrouped use) differs from the
4035
+ // generation the map was built for — so the descriptor's REFERENCE IDENTITY is stable
4036
+ // across every cell of one group row within a row-model generation (an unstable reference
4037
+ // would thrash the fine-grained targets, Solid/Lit) and refreshed the moment the grouping
4038
+ // changes. Group row ids are built from consumer grouping VALUES, so the map MUST be
4039
+ // null-prototype — a `__proto__` grouping value must not reach Object.prototype.
4040
+ groupRowDescriptorCache: any = null;
4041
+
4042
+ groupRowDescriptorCacheVer: any = undefined;
4043
+
4044
+ groupRowDescriptor = (row: any) => {
4045
+ const ver = this._rowModelVer.value;
4046
+ if (!this.groupRowDescriptorCache || this.groupRowDescriptorCacheVer !== ver) {
4047
+ this.groupRowDescriptorCache = Object.create(null);
4048
+ this.groupRowDescriptorCacheVer = ver;
4049
+ }
4050
+ const key = String(row.id);
4051
+ let d = this.groupRowDescriptorCache[key];
4052
+ if (!d) {
4053
+ const groupingColumnId = row.groupingColumnId != null ? row.groupingColumnId : '';
4054
+ const groupingValue = row.getGroupingValue ? row.getGroupingValue(groupingColumnId) : row.groupingValue;
4055
+ d = {
4056
+ isGroupRow: true,
4057
+ groupId: row.id,
4058
+ groupingColumnId,
4059
+ groupingValue,
4060
+ leafCount: this.groupSubRowCount(row)
4061
+ };
4062
+ this.groupRowDescriptorCache[key] = d;
4063
+ }
4064
+ return d;
4065
+ };
4066
+
4067
+ // cellSlotRow: the #cell slot's `:row=` value. A group-header row gets the stable
4068
+ // descriptor above (never the leaked first-leaf record); every other row is unchanged.
4069
+ cellSlotRow = (row: any) => this.rowIsGrouped(row) ? this.groupRowDescriptor(row) : row ? row.original : null;
3954
4070
 
3955
4071
  // groupingKeys: the live ordered grouping array — slot prop for the headless #groupBar + the
3956
4072
  // default styled-token reflection. Reads currentState() ($props.grouping ?? $data.groupingDefault),
3957
4073
  // both reactive sources, so the bar re-renders on a grouping change across all six targets.
3958
4074
  groupingKeys = () => this.currentState().grouping || [];
3959
4075
 
3960
- // groupableColumns: the data columns OFFERED to the headless #groupBar (those whose Column/config
3961
- // `groupable` is not false) — `[{ id, label }]`. Excludes the chrome columns (select/expander are
3962
- // not in columnDefs()). The consumer builds any bar/drag UI from this; the component ships none.
4076
+ // groupableColumns: the data columns OFFERED to the headless #groupBar — `[{ id, label }]`.
4077
+ // Excludes the chrome columns (select/expander are not in columnDefs()). C5+C6 (quick
4078
+ // 260906-cvo): delegates the actual walk to collectGroupableLeafDefs (columnDefUtils.ts) —
4079
+ // a GROUP column (a `columns:` group's own entry) is NEVER offered (it has no accessor and
4080
+ // cannot produce a grouping value, regardless of any `groupable` flag on it), and every
4081
+ // groupable NESTED leaf (a `columns:` group child, opted out via `groupable: false` like any
4082
+ // top-level leaf) IS offered, in declaration order. The consumer builds any bar/drag UI from
4083
+ // this; the component ships none.
3963
4084
  groupableColumns = () => {
3964
4085
  const out = [];
3965
- const defs = this.columnDefs();
4086
+ const defs = collectGroupableLeafDefs(this.columnDefs());
3966
4087
  for (const d of defs as any) {
3967
- if (!d || d.groupable === false) continue;
3968
4088
  out.push({
3969
4089
  id: d.id,
3970
4090
  label: d.header != null ? d.header : d.id
@@ -4976,11 +5096,18 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
4976
5096
  return;
4977
5097
  }
4978
5098
  // ── C2 (phase 63 wave-8): Enter on a GROUP-HEADER cell toggles that group's collapse/
4979
- // expand (APG treegrid). A group cell is NON-editable (isActiveCellEditable=false, the
4980
- // verified invariant) so it never hits the edit branches above and would otherwise fall to
4981
- // enterControl() — which merely FOCUSES the group-toggle button (requiring a second key).
4982
- // Route it to the SAME onToggleExpand path the chevron uses (group rows ride the expand
4983
- // model) so one Enter toggles the group. Body cells only (a header-active Enter is unchanged);
5099
+ // expand (APG treegrid). C1 (quick 260906-cvo) CORRECTION: the comment here used to claim
5100
+ // "a group cell is non-editable" as "the verified invariant" — that was FALSE AS WRITTEN.
5101
+ // It held only for the grouping column's OWN cell (cellIsGrouped's branch renders before
5102
+ // the editor branch); a non-grouping EDITABLE column's cell on a group row fell straight
5103
+ // through the branch chain to isEditing(...) and opened an editor (the C1 defect this
5104
+ // quick task closed). What is now TRUE, and WHY: isActiveCellEditable() carries an
5105
+ // explicit rowIndexIsGrouped(...) row guard (filterPaginationRowChrome.rzts), so EVERY
5106
+ // editable column's cell on a group row is non-editable — that is what makes the four
5107
+ // edit-entry branches above fall through to this one, not an incidental property of the
5108
+ // grouping column alone. Route it to the SAME onToggleExpand path the chevron uses (group
5109
+ // rows ride the expand model) so one Enter toggles the group. Body cells only (a
5110
+ // header-active Enter is unchanged);
4984
5111
  // ($data.rows || [])[$data.activeRow] is the active flattened row (page-relative non-virtual /
4985
5112
  // full-model virtual — both index $data.rows). Placed BEFORE the reserved enterControl branch.
4986
5113
  else if (key === 'Enter' && !this._activeIsHeader.value && this.rowIsGrouped((this._rows.value || [])[this._activeRow.value])) {
@@ -5777,6 +5904,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
5777
5904
  // rangeToTsv(): serialize the current range to TSV — rows joined by '\n', cells by '\t',
5778
5905
  // reading each cell's value off the visible model by index (cellValueAt). A single active
5779
5906
  // cell (no range) serializes that one cell. Each field is B10-escaped. Pure read — never writes.
5907
+ // C2 (quick 260906-cvo) — DELIBERATE NON-CHANGE: this is a READ, unlike applyGridToRange's
5908
+ // write funnel, and stays UNGUARDED for a group-header row on purpose. cellValueAt reads the
5909
+ // column's DISPLAYED aggregate value on a group row (table-core's cell.getValue()), which is
5910
+ // exactly what the user sees — serializing it is correct behavior, not a leak. Do not "fix"
5911
+ // this asymmetry; it is intentional.
5780
5912
  rangeToTsv = () => {
5781
5913
  const box = this.normalizedRange();
5782
5914
  const r0 = box ? box.r0 : this._activeRow.value;
@@ -5830,11 +5962,22 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
5830
5962
  for (let gr = 0; gr < grid.length; gr++) {
5831
5963
  const r = originRow + gr;
5832
5964
  if (r > maxRow) break;
5965
+ // C2 (quick 260906-cvo) — hoisted ONCE per ROW (never per cell): a group-header row's
5966
+ // `original` is table-core's `leafRows[0].original` (a synthetic row), so a write
5967
+ // resolved through it silently lands on an unrelated record. This ONE placement closes
5968
+ // pasteRange, cutRange, clearActiveRange AND fillRange — all four build a grid and hand
5969
+ // it to this single funnel, so guarding the four callers individually would be four
5970
+ // chances to miss one.
5971
+ const rowGrouped = this.rowIndexIsGrouped(r);
5833
5972
  const cols = grid[gr] || [];
5834
5973
  for (let gc = 0; gc < cols.length; gc++) {
5835
5974
  const c = originCol + gc;
5836
5975
  if (c > maxCol) break;
5976
+ // A group-row cell still counts toward `total` — it is a skipped TARGET, exactly like
5977
+ // a non-editable or validator-rejected cell — so the "N of M cells pasted" / "no cells
5978
+ // pasted" announce keeps its existing semantics (never silently under-reports M).
5837
5979
  total = total + 1;
5980
+ if (rowGrouped) continue;
5838
5981
  const colId = this.columnIdAt(r, c);
5839
5982
  if (colId == null || !this.columnEditable(colId)) continue;
5840
5983
  const rowObj = this.rowOriginalAt(r);
@@ -6356,7 +6499,13 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
6356
6499
  // isActiveCellEditable: the active cell sits in an editable column AND is a body cell
6357
6500
  // (req-1). Gates the F2/Enter/printable edit-entry branches in onGridKeyDown; a
6358
6501
  // non-editable active cell falls through to the reserved enterControl path.
6502
+ // C1 (quick 260906-cvo) — Layer 1: a group-header row is NEVER editable, regardless of
6503
+ // column. This is the single highest-value guard in the cluster: it closes Shift+F2, the
6504
+ // checkbox toggle, and the F2/Enter/printable edit-entry branches in ONE edit (they are all
6505
+ // `else if` branches gated on this predicate), AND restores fallthrough so Enter on a group
6506
+ // row reaches the group-toggle branch below them instead of opening an editor.
6359
6507
  isActiveCellEditable = () => {
6508
+ if (this.rowIndexIsGrouped(this._activeRow.value)) return false;
6360
6509
  const colId = this.activeCellColumnId();
6361
6510
  return colId != null && this.columnEditable(colId);
6362
6511
  };
@@ -6374,6 +6523,14 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
6374
6523
  // non-virtual, wr.vi.index virtual).
6375
6524
  isEditing = (rowIndex: any, colIndex: any) => {
6376
6525
  if (this._editVer.value < 0) return false;
6526
+ // C1 (quick 260906-cvo) — Layer 6: the ONLY layer whose reachability does not depend on a
6527
+ // keystroke. Editing state is INDEX-based over the visible model, and nothing clears it
6528
+ // when the row model re-derives — a consumer that changes the grouping model WHILE a cell
6529
+ // editor is open can move a group-header row under the index $data.editingRow/
6530
+ // editingRowIndex still holds, rendering the editor on a group row with no beginEdit
6531
+ // involved. Placed AFTER the editVer read (preserves the Svelte/Solid re-derivation
6532
+ // discipline the header comment above documents).
6533
+ if (this.rowIndexIsGrouped(rowIndex)) return false;
6377
6534
  if (this._editingRowIndex.value != null && this._editingRowIndex.value === rowIndex) {
6378
6535
  const colId = this.columnIdAt(rowIndex, colIndex);
6379
6536
  return colId != null && this.columnEditable(colId);
@@ -6551,6 +6708,10 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
6551
6708
  // $data) so a Tab-advance that just setState'd activeRow/Col works on React. Clears any
6552
6709
  // prior invalid state. Focus moves into the editor.
6553
6710
  beginEdit = (rowIndex: any, colIndex: any, seed: any) => {
6711
+ // C1 (quick 260906-cvo) — Layer 2: closes the `onGridClick` (singleClickEdit) path, which
6712
+ // calls beginEdit directly gated ONLY on columnEditable — isActiveCellEditable (Layer 1)
6713
+ // does not run on that path at all.
6714
+ if (this.rowIndexIsGrouped(rowIndex)) return;
6554
6715
  const colId = this.columnIdAt(rowIndex, colIndex);
6555
6716
  if (colId == null || !this.columnEditable(colId)) return;
6556
6717
  // A new edit session starts — reset the sync idempotency latch so THIS session's eventual
@@ -6761,6 +6922,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
6761
6922
  // editor:'checkbox' columns only (Space/Enter/F2), full-row edit mode is unaffected (the
6762
6923
  // editingRowIndex early return in onGridKeyDown already excludes it).
6763
6924
  toggleActiveBooleanCell = () => {
6925
+ // C1 (quick 260906-cvo) — Layer 4: the only write funnel bypassing both beginEdit/
6926
+ // commitEdit and applyGridToRange. Reachable today only through the Layer-1-gated
6927
+ // checkbox branch, so this line is redundant AT HEAD — it earns its place by making "no
6928
+ // write ever targets a group row" a property of the write funnels themselves.
6929
+ if (this.rowIndexIsGrouped(this._activeRow.value)) return;
6764
6930
  const colId = this.columnIdAt(this._activeRow.value, this._activeColIndex.value);
6765
6931
  if (colId == null || !this.columnEditable(colId)) return;
6766
6932
  const rowList = this._rows.value || [];
@@ -6892,6 +7058,11 @@ ${this.groupable ? html`<div class="rdt-group-bar-host" data-rozie-s-d5dcab4c>
6892
7058
  beginRowEdit = (row: any) => {
6893
7059
  const rowIndex = this.rowIndexOf(row);
6894
7060
  if (rowIndex < 0) return;
7061
+ // C1 (quick 260906-cvo) — Layer 5: a second, independent editing-state funnel that takes a
7062
+ // row OBJECT from its caller. Reachable today only via Shift+F2 (Layer 1), so also
7063
+ // redundant at HEAD; kept for the same reason as Layer 4 — it is a mutation entry point,
7064
+ // and the natural call site for a future #rowActions "Edit" drop-in button.
7065
+ if (this.rowIndexIsGrouped(rowIndex)) return;
6895
7066
  const editable = this.editableColumnsForRow(rowIndex);
6896
7067
  if (editable.length === 0) return;
6897
7068
  // A new edit session starts — reset the sync idempotency latch (see editCellLifecycle.rzts).
@@ -32,4 +32,80 @@ const wrapAggregationFn = (fn: any) => {
32
32
  }
33
33
  }
34
34
 
35
- export { isSafeKey, wrapAggregationFn }
35
+ // ── B1 (quick 260906-afh) — nested group leaf def index ─────────────────────────────
36
+ // LOOKUP-ONLY. This map is never fed to table-core (it does not replace/reshape the
37
+ // columnDefs() array table-core consumes for createTable/setOptions — that stays
38
+ // byte-identical); it exists purely so `defFor(colId)` (columnChrome.rzts) can resolve
39
+ // a NESTED leaf's def (a `columns:` group child) in O(1), the same as a top-level leaf.
40
+ //
41
+ // collectNestedDefs: depth-first walk of a `columns` array (each entry may itself carry
42
+ // a nested `columns` array — 3+ levels deep is supported). Writes `out[String(d.id)] = d`
43
+ // ONLY when the id is ABSENT from `out` — first-write-wins so a caller can pre-seed `out`
44
+ // with the higher-precedence top-level defs and this pass only ever fills GAPS.
45
+ const collectNestedDefs = (list: any, out: any) => {
46
+ if (!Array.isArray(list)) return
47
+ for (const d of list) {
48
+ if (!d || d.id == null) continue
49
+ const id = String(d.id)
50
+ if (!(id in out)) out[id] = d
51
+ if (Array.isArray(d.columns)) collectNestedDefs(d.columns, out)
52
+ }
53
+ }
54
+
55
+ // indexDefsById: builds a null-prototype `{ id: def }` map from a `columnDefs()`-shaped
56
+ // array in TWO passes. Pass 1 registers every TOP-LEVEL def unconditionally — so any id
57
+ // that already resolves at HEAD (a top-level leaf OR a top-level group's OWN id) keeps
58
+ // resolving to the EXACT SAME def, the byte-identical-lookup guarantee for every
59
+ // pre-existing caller. Pass 2 walks each top-level entry's `columns` (if any) via
60
+ // collectNestedDefs, so a nested leaf only fills a GAP — it can never shadow a
61
+ // same-named top-level id (top-level wins regardless of array order). Object.create(null)
62
+ // (T-AFH-01): a consumer column id of "__proto__"/"constructor" cannot pollute
63
+ // Object.prototype through this map — mirrors the existing `byId` T-48-PP guard in
64
+ // columnBuilders.rzts.
65
+ const indexDefsById = (defs: any): any => {
66
+ const out = Object.create(null)
67
+ if (!Array.isArray(defs)) return out
68
+ for (const d of defs) {
69
+ if (!d || d.id == null) continue
70
+ out[String(d.id)] = d
71
+ }
72
+ for (const d of defs) {
73
+ if (d && Array.isArray(d.columns)) collectNestedDefs(d.columns, out)
74
+ }
75
+ return out
76
+ }
77
+
78
+ // ── C5+C6 (quick 260906-cvo) — groupable-leaf collection for the #groupBar ──────────────
79
+ // C5: buildConfigDef's GROUP branch (columnBuilders.rzts) returns `{ id, header, columns }`
80
+ // with NO `groupable` key and no accessor, so the OLD `groupableColumns()` loop's
81
+ // `d.groupable === false` skip never fired for a group column — it was wrongly OFFERED to
82
+ // the bar even though a group column can never produce a grouping value.
83
+ // C6: that same loop walked TOP-LEVEL `columnDefs()` entries only, so a genuinely groupable
84
+ // NESTED leaf (a `columns:` group child) was never offered at all.
85
+ //
86
+ // collectGroupableLeafDefs: a DEDICATED depth-first walk, NOT a reuse of `indexDefsById`.
87
+ // `indexDefsById` is a LOOKUP index — it deliberately registers group columns too (a group
88
+ // id must keep resolving for `defFor`), and its insertion order is all-top-level-first-then-
89
+ // nested, which would both offer group columns AND hand the group bar an order that does not
90
+ // match the rendered header order. This walk instead: recurses into a `columns:` array
91
+ // WITHOUT ever pushing the group entry itself (closes C5 — a group column has no accessor
92
+ // and cannot be a grouping target, so an explicit `groupable: true` on a group entry is
93
+ // meaningless and is likewise never pushed); pushes every other (leaf) entry unless it opts
94
+ // out via `groupable === false` (closes C6 — nested leaves are now reachable); walks in
95
+ // DECLARATION order, depth-first, matching the rendered header order.
96
+ const collectGroupableLeafDefs = (defs: any): any[] => {
97
+ const out: any[] = []
98
+ if (!Array.isArray(defs)) return out
99
+ for (const d of defs) {
100
+ if (!d) continue
101
+ if (Array.isArray(d.columns)) {
102
+ out.push(...collectGroupableLeafDefs(d.columns))
103
+ continue
104
+ }
105
+ if (d.groupable === false) continue
106
+ out.push(d)
107
+ }
108
+ return out
109
+ }
110
+
111
+ export { isSafeKey, wrapAggregationFn, collectNestedDefs, indexDefsById, collectGroupableLeafDefs }