argus-table 0.3.0 → 0.4.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.
Files changed (63) hide show
  1. package/dist/argus-column-group.js +66 -0
  2. package/dist/argus-column-group.js.map +1 -0
  3. package/dist/argus-column.js +4 -25
  4. package/dist/argus-column.js.map +1 -1
  5. package/dist/argus-table.js +553 -200
  6. package/dist/argus-table.js.map +1 -1
  7. package/dist/attributes.js +21 -0
  8. package/dist/attributes.js.map +1 -0
  9. package/dist/cdn/argus-table.js +92 -57
  10. package/dist/cdn/argus-table.js.map +1 -1
  11. package/dist/column-registry.js +23 -0
  12. package/dist/column-registry.js.map +1 -0
  13. package/dist/controllers/auto-width.js +6 -5
  14. package/dist/controllers/auto-width.js.map +1 -1
  15. package/dist/controllers/column-children.js +32 -9
  16. package/dist/controllers/column-children.js.map +1 -1
  17. package/dist/controllers/column-window.js +0 -4
  18. package/dist/controllers/column-window.js.map +1 -1
  19. package/dist/controllers/plugins.js +25 -6
  20. package/dist/controllers/plugins.js.map +1 -1
  21. package/dist/controllers/reorder-drag.js +12 -9
  22. package/dist/controllers/reorder-drag.js.map +1 -1
  23. package/dist/controllers/row-measure.js +2 -1
  24. package/dist/controllers/row-measure.js.map +1 -1
  25. package/dist/controllers/virtual-controller.js +25 -32
  26. package/dist/controllers/virtual-controller.js.map +1 -1
  27. package/dist/core/src/columns/column-groups.js +129 -0
  28. package/dist/core/src/columns/column-groups.js.map +1 -0
  29. package/dist/core/src/columns/column-order.js +125 -31
  30. package/dist/core/src/columns/column-order.js.map +1 -1
  31. package/dist/core/src/columns/resolve-columns.js +0 -1
  32. package/dist/core/src/columns/resolve-columns.js.map +1 -1
  33. package/dist/core/src/columns/spread-width.js +43 -0
  34. package/dist/core/src/columns/spread-width.js.map +1 -0
  35. package/dist/core/src/filters/filter.js +6 -8
  36. package/dist/core/src/filters/filter.js.map +1 -1
  37. package/dist/core/src/grid/row-layout.js +19 -11
  38. package/dist/core/src/grid/row-layout.js.map +1 -1
  39. package/dist/core/src/state/selection.js +2 -1
  40. package/dist/core/src/state/selection.js.map +1 -1
  41. package/dist/core/src/table/create-table.js +57 -22
  42. package/dist/core/src/table/create-table.js.map +1 -1
  43. package/dist/core/src/table/sorted-row-model.js.map +1 -1
  44. package/dist/core/src/virtual/row-sizes.js +0 -4
  45. package/dist/core/src/virtual/row-sizes.js.map +1 -1
  46. package/dist/custom-elements.json +489 -245
  47. package/dist/events.js +16 -16
  48. package/dist/events.js.map +1 -1
  49. package/dist/index.d.ts +273 -127
  50. package/dist/index.js +3 -2
  51. package/dist/render/body.js +2 -2
  52. package/dist/render/body.js.map +1 -1
  53. package/dist/render/cells.js +11 -3
  54. package/dist/render/cells.js.map +1 -1
  55. package/dist/render/content-directive.js +1 -1
  56. package/dist/render/content-directive.js.map +1 -1
  57. package/dist/render/content.js +15 -3
  58. package/dist/render/content.js.map +1 -1
  59. package/dist/render/header.js +82 -19
  60. package/dist/render/header.js.map +1 -1
  61. package/dist/styles/base.js +1 -1
  62. package/dist/styles/base.js.map +1 -1
  63. package/package.json +1 -1
@@ -1,6 +1,9 @@
1
1
  import { clickSelection, sameSelection, selectAll, selectRange, toggleRow } from "./core/src/state/selection.js";
2
2
  import { controllable } from "./core/src/state/controllable.js";
3
+ import { groupKey } from "./core/src/columns/column-groups.js";
3
4
  import { RowLayout } from "./core/src/grid/row-layout.js";
5
+ import { sameUnit } from "./core/src/columns/column-order.js";
6
+ import { spreadWidth } from "./core/src/columns/spread-width.js";
4
7
  import "./core/src/table/create-table.js";
5
8
  import { announce } from "./announce.js";
6
9
  import { AutoWidths, measureColumns } from "./controllers/auto-width.js";
@@ -14,15 +17,16 @@ import { canStartReorder, dropTarget, startReorderDrag } from "./controllers/reo
14
17
  import { startResizeDrag } from "./controllers/resize-drag.js";
15
18
  import { TableController } from "./controllers/table-controller.js";
16
19
  import { VirtualController } from "./controllers/virtual-controller.js";
17
- import { CELL_CLICK, COLUMN_FILTER, COLUMN_HIDE, COLUMN_PIN, COLUMN_REORDER, COLUMN_RESIZE, LOAD_ERROR, RANGE_CHANGE, ROW_CLICK, ROW_SELECT, SORT_CHANGE, fire } from "./events.js";
20
+ import { CELL_CLICK, COLUMN_FILTERS_CHANGE, COLUMN_ORDER_CHANGE, COLUMN_PINNING_CHANGE, COLUMN_SIZING_CHANGE, COLUMN_VISIBILITY_CHANGE, LOAD_ERROR, RANGE_CHANGE, ROW_CLICK, SELECTION_CHANGE, SORTING_CHANGE, fire } from "./events.js";
18
21
  import { fromInteractive } from "./interactive.js";
19
22
  import { bodyCap, watchMaxElementHeight } from "./max-height.js";
20
23
  import { renderBody } from "./render/body.js";
21
24
  import { renderLightDom } from "./render/cells.js";
22
25
  import { content, flushContent } from "./render/content-directive.js";
23
- import { renderHeader } from "./render/header.js";
26
+ import { columnKey, renderHeader } from "./render/header.js";
24
27
  import { renderPinnedRows } from "./render/pinned-rows.js";
25
28
  import { baseStyles } from "./styles/base.js";
29
+ import { switchConverter } from "./attributes.js";
26
30
  import __decorate from "./_virtual/_@oxc-project_runtime@0.156.0/helpers/esm/decorate.js";
27
31
  import { LitElement, html, nothing } from "lit";
28
32
  import { customElement, property } from "lit/decorators.js";
@@ -50,7 +54,7 @@ function sortColumnOf(event) {
50
54
  if (target.dataset.resizeCol !== void 0) return void 0;
51
55
  if (target.matches("button,input,select,textarea,a[href],label,[contenteditable],[popover],dialog")) return void 0;
52
56
  if (target.dataset.sortCol !== void 0) return target.dataset.sortCol;
53
- if (target.getAttribute("part") === "header-row") return void 0;
57
+ if (target.getAttribute("role") === "row") return void 0;
54
58
  }
55
59
  }
56
60
  var RESIZE_STEP = 10;
@@ -62,13 +66,13 @@ var rowHeightConverter = { fromAttribute(value) {
62
66
  const px = Number(text.replace(/px$/, ""));
63
67
  return text !== "" && px > 0 ? px : void 0;
64
68
  } };
65
- var HEADER = {
66
- section: "header",
67
- index: 0
68
- };
69
- /** Where the rows sit in the grid: header, pinned top, body, pinned bottom. */
69
+ /** Where the rows sit in the grid: header rows, pinned top, body, pinned bottom. */
70
70
  function layoutOf(model) {
71
- return new RowLayout(model.pinnedCount("top"), model.rowCount, model.pinnedCount("bottom"));
71
+ return new RowLayout(model.pinnedCount("top"), model.rowCount, model.pinnedCount("bottom"), model.headerRows);
72
+ }
73
+ /** The drop slot and applied-move key of a column or group (`HeaderDrag.key`). */
74
+ function unitKey(unit) {
75
+ return unit.kind === "column" ? columnKey(unit.id) : groupKey(unit.id, unit.pin);
72
76
  }
73
77
  /** The grid cell or column header an element is, if any. */
74
78
  function gridCell(target) {
@@ -76,11 +80,19 @@ function gridCell(target) {
76
80
  const role = target.getAttribute("role");
77
81
  return role === "gridcell" || role === "columnheader" ? target : void 0;
78
82
  }
83
+ /** The column group header an element is, if any. */
84
+ function groupCell(target) {
85
+ return target instanceof HTMLElement && target.dataset.groupKey !== void 0 ? target : void 0;
86
+ }
79
87
  /** The grid row a rendered row element is, from its `aria-rowindex`. */
80
88
  function gridRowOf(row, layout) {
81
89
  const index = row?.getAttribute("aria-rowindex");
82
90
  return index ? layout.fromAriaRowIndex(Number(index)) : void 0;
83
91
  }
92
+ /** The grid row a cell or column header is in. */
93
+ function gridRowOfCell(cell, layout) {
94
+ return gridRowOf(cell.parentElement, layout);
95
+ }
84
96
  /**
85
97
  * The pinned edge's shadow, drawn once per side over the scroller rather than
86
98
  * per row: a shadow on every pinned cell repaints on every scroll frame. It
@@ -103,6 +115,10 @@ function pinShadow(side, width, contentPx) {
103
115
  function track(column) {
104
116
  return column.flex === void 0 ? `${String(column.width)}px` : `minmax(${String(column.minWidth)}px, ${String(column.flex)}fr)`;
105
117
  }
118
+ /** Total width of the columns pinned to one side. */
119
+ function pinnedWidth(columns, side) {
120
+ return columns.reduce((sum, c) => c.pin === side ? sum + c.width : sum, 0);
121
+ }
106
122
  var ArgusTable = class ArgusTable extends LitElement {
107
123
  static {
108
124
  this.styles = baseStyles;
@@ -119,6 +135,21 @@ var ArgusTable = class ArgusTable extends LitElement {
119
135
  this.#columnsProp = value ?? [];
120
136
  }
121
137
  /**
138
+ * Headers over several columns: `{ id, header, columns }`, where `columns` lists
139
+ * column ids and nested groups. Each level of nesting adds a header row. A group's
140
+ * columns stay next to each other: they move within the group, and the group
141
+ * moves as a whole (drag its header, or Alt+Shift+Arrow). A group whose columns
142
+ * are pinned to different sides shows on each side. Property only. While empty
143
+ * and `columns` is too, the table reads its `<argus-column-group>` children.
144
+ * `null` or `undefined` means `[]`.
145
+ */
146
+ get columnGroups() {
147
+ return this.#columnGroupsProp;
148
+ }
149
+ set columnGroups(value) {
150
+ this.#columnGroupsProp = value ?? [];
151
+ }
152
+ /**
122
153
  * The rows to show. Replace the array to update; changes inside it aren't
123
154
  * detected. `null` or `undefined` means `[]`.
124
155
  */
@@ -138,7 +169,7 @@ var ArgusTable = class ArgusTable extends LitElement {
138
169
  /**
139
170
  * The sort order, e.g. `[{ id: 'total', desc: true }]`; `[]` when unsorted. Unset,
140
171
  * the table keeps it itself and header clicks change it. Once set, it is
141
- * controlled: a click only fires `argus-sort-change` with the proposed order,
172
+ * controlled: a click only fires `argus-sorting-change` with the proposed order,
142
173
  * and nothing changes until you set the property. Set `undefined` to hand it back.
143
174
  */
144
175
  get sorting() {
@@ -150,7 +181,7 @@ var ArgusTable = class ArgusTable extends LitElement {
150
181
  /**
151
182
  * Resized widths in px by column id, e.g. `{ name: 240 }`. A resized flex column
152
183
  * becomes fixed. Uncontrolled until set, like `sorting`: once set, a resize only
153
- * fires `argus-column-resize` with the proposed widths. `undefined` hands it back.
184
+ * fires `argus-column-sizing-change` with the proposed widths. `undefined` hands it back.
154
185
  */
155
186
  get columnSizing() {
156
187
  return this.#sizing.value;
@@ -164,7 +195,7 @@ var ArgusTable = class ArgusTable extends LitElement {
164
195
  * Each pin group (start-pinned, scrolling, end-pinned) takes its columns in this
165
196
  * order; unknown ids are ignored and columns it doesn't list follow the listed
166
197
  * ones of their group. After a move it holds every column id. Uncontrolled
167
- * until set, like `sorting`: once set, a move only fires `argus-column-reorder`
198
+ * until set, like `sorting`: once set, a move only fires `argus-column-order-change`
168
199
  * with the proposed order. `undefined` hands it back.
169
200
  */
170
201
  get columnOrder() {
@@ -179,7 +210,7 @@ var ArgusTable = class ArgusTable extends LitElement {
179
210
  * `{ total: 'end', id: false }`. Columns it doesn't list keep their `pin`. A pinned
180
211
  * column joins the edge of its group nearest its place in `columnOrder`, and stops
181
212
  * flexing. Uncontrolled until set, like `sorting`: once set, pinning from a plugin
182
- * only fires `argus-column-pin` with the proposed pins. `undefined` hands it back.
213
+ * only fires `argus-column-pinning-change` with the proposed pins. `undefined` hands it back.
183
214
  */
184
215
  get columnPinning() {
185
216
  return this.#pinning.value;
@@ -193,7 +224,7 @@ var ArgusTable = class ArgusTable extends LitElement {
193
224
  * doesn't list keep their `hidden`. A hidden column isn't rendered, but sorting by
194
225
  * it still works and it keeps its place in `columnOrder`. Uncontrolled until set,
195
226
  * like `sorting`: once set, hiding or showing from a plugin only fires
196
- * `argus-column-hide` with the proposed visibility. `undefined` hands it back.
227
+ * `argus-column-visibility-change` with the proposed visibility. `undefined` hands it back.
197
228
  */
198
229
  get columnVisibility() {
199
230
  return this.#visibility.value;
@@ -209,7 +240,7 @@ var ArgusTable = class ArgusTable extends LitElement {
209
240
  * `dataSource`, the filters go to `getRows()` and the server filters. Users set
210
241
  * filters through plugins (the column menu) on columns with a `filter` kind.
211
242
  * Uncontrolled until set, like `sorting`: once set, a change from a plugin only
212
- * fires `argus-column-filter` with the proposed filters. `undefined` hands it back.
243
+ * fires `argus-column-filters-change` with the proposed filters. `undefined` hands it back.
213
244
  */
214
245
  get columnFilters() {
215
246
  return this.#filters.value;
@@ -232,7 +263,7 @@ var ArgusTable = class ArgusTable extends LitElement {
232
263
  /**
233
264
  * The selected rows, as row ids (from `getRowId`, or the row's index as a
234
265
  * string), e.g. `['p4', 'p7']`. Uncontrolled until set, like `sorting`: once
235
- * set, a click only fires `argus-row-select` with the proposed ids.
266
+ * set, a click only fires `argus-selection-change` with the proposed ids.
236
267
  * `undefined` hands it back. The table never drops ids on its own: they stay
237
268
  * through sorting, a new `data` or reloaded pages. With a `dataSource`,
238
269
  * Ctrl/Cmd+A selects the loaded rows only.
@@ -258,6 +289,10 @@ var ArgusTable = class ArgusTable extends LitElement {
258
289
  if (!sameItems(next, this.#pluginsProp)) this.#pluginsProp = next;
259
290
  }
260
291
  #columnsProp;
292
+ #columnGroupsProp;
293
+ /** The groups last passed to the model: `columnGroups`, or the `<argus-column-group>` children. */
294
+ #groups;
295
+ #groupsChanged;
261
296
  #pluginsProp;
262
297
  #dataProp;
263
298
  #overscan;
@@ -286,13 +321,13 @@ var ArgusTable = class ArgusTable extends LitElement {
286
321
  /** Whether the checkbox column was last shown; a change re-measures the pinned columns. */
287
322
  #selectionColumn;
288
323
  #selectionColumnChanged;
289
- /** The select-all checkbox's state, kept until `selection`, `data` or `getRowId` change. */
324
+ /** The select-all checkbox's state, kept until the selection or the shown rows change. */
290
325
  #checkAll;
291
- /** Moves the table applied itself, by column; their events fire once the update is done. */
326
+ /** Moves the table applied itself, by column or group; their events fire once the update is done. */
292
327
  #appliedReorders;
293
328
  /** The header being dragged to a new position, if any. */
294
329
  #drag;
295
- /** A move from the keyboard, announced once it has rendered: the column and its old position. */
330
+ /** A move from the keyboard, announced once it has rendered: what moved and its old position. */
296
331
  #announceMove;
297
332
  /** Resizes the table applied itself, by column; their events fire once the update is done. */
298
333
  #appliedResizes;
@@ -316,6 +351,8 @@ var ArgusTable = class ArgusTable extends LitElement {
316
351
  #columns;
317
352
  #columnsChanged;
318
353
  #warnedColumns;
354
+ /** The header row of column headers at the last reconcile (`#reconcileActive`). */
355
+ #leafLevel;
319
356
  /** End of the light-DOM part holding cell content, so later children stay outside it. */
320
357
  #contentEnd;
321
358
  /** The light-DOM part: disconnected with the table, so foreign content is released. */
@@ -346,6 +383,10 @@ var ArgusTable = class ArgusTable extends LitElement {
346
383
  * aren't counted yet. Scrolled to again whenever the row count changes.
347
384
  */
348
385
  #scrollTarget;
386
+ /** The scroll container (`part="table"`); `null` before the first render. */
387
+ #scroller() {
388
+ return this.renderRoot.querySelector("[part~=\"table\"]");
389
+ }
349
390
  constructor() {
350
391
  super();
351
392
  this.density = "comfortable";
@@ -353,6 +394,9 @@ var ArgusTable = class ArgusTable extends LitElement {
353
394
  this.selectionMode = "none";
354
395
  this.selectionCheckboxes = false;
355
396
  this.#columnsProp = [];
397
+ this.#columnGroupsProp = [];
398
+ this.#groups = [];
399
+ this.#groupsChanged = false;
356
400
  this.#pluginsProp = [];
357
401
  this.#dataProp = [];
358
402
  this.#overscan = 6;
@@ -397,12 +441,14 @@ var ArgusTable = class ArgusTable extends LitElement {
397
441
  this.#requestFilter(columnId, filter);
398
442
  },
399
443
  filterValues: (columnId, limit) => this.#table.model.filterValues(columnId, limit),
444
+ filteredIndexes: (except) => this.#table.model.indexesPassing(except),
400
445
  cellOf: (event) => this.#cellTarget(event),
401
446
  setSort: (columnId, direction) => {
402
447
  this.#setSort(columnId, direction);
403
448
  },
404
449
  focusHeader: (columnId) => {
405
- this.#moveTo(HEADER, columnId, this.#table.model);
450
+ const model = this.#table.model;
451
+ this.#moveTo(layoutOf(model).leafHeader, columnId, model);
406
452
  },
407
453
  announce: (message) => {
408
454
  announce(this.ownerDocument, message);
@@ -421,12 +467,13 @@ var ArgusTable = class ArgusTable extends LitElement {
421
467
  this.#columns = [];
422
468
  this.#columnsChanged = false;
423
469
  this.#warnedColumns = false;
470
+ this.#leafLevel = 0;
424
471
  this.#lightConnected = true;
425
472
  this.#data = new DataController(this);
426
473
  this.#scrollToTop = false;
427
- this.#virtual = new VirtualController(this, () => this.renderRoot.querySelector("[part~=\"table\"]"));
428
- this.#pinScroll = new PinScrollController(this, () => this.renderRoot.querySelector("[part~=\"table\"]"));
429
- this.#columnWindow = new ColumnWindowController(this, () => this.renderRoot.querySelector("[part~=\"table\"]"));
474
+ this.#virtual = new VirtualController(this, () => this.#scroller());
475
+ this.#pinScroll = new PinScrollController(this, () => this.#scroller());
476
+ this.#columnWindow = new ColumnWindowController(this, () => this.#scroller());
430
477
  this.#rendered = [];
431
478
  this.#rowPx = 40;
432
479
  this.#rowsKey = "";
@@ -462,6 +509,15 @@ var ArgusTable = class ArgusTable extends LitElement {
462
509
  if (event.button !== 0) return;
463
510
  const handle = event.composedPath()[0];
464
511
  if (!(handle instanceof HTMLElement)) return;
512
+ const group = this.#groupCellOf(handle.dataset.resizeGroup);
513
+ if (group) {
514
+ const starts = this.#groupWidths(group.columns);
515
+ const total = [...starts.values()].reduce((sum, w) => sum + w, 0);
516
+ startResizeDrag(handle, event, total, this.#direction(), (w) => {
517
+ this.#requestGroupResize(group.columns, starts, w);
518
+ });
519
+ return;
520
+ }
465
521
  const id = handle.dataset.resizeCol;
466
522
  const cell = handle.parentElement;
467
523
  if (id !== void 0 && cell) {
@@ -475,15 +531,23 @@ var ArgusTable = class ArgusTable extends LitElement {
475
531
  /** A double-click on a resize handle fits the column to its rendered content. */
476
532
  dblclick: (event) => {
477
533
  const handle = event.composedPath()[0];
478
- const id = handle instanceof HTMLElement ? handle.dataset.resizeCol : void 0;
534
+ if (!(handle instanceof HTMLElement)) return;
535
+ const group = this.#groupCellOf(handle.dataset.resizeGroup);
536
+ if (group) {
537
+ this.autosizeColumns([...group.columns]);
538
+ return;
539
+ }
540
+ const id = handle.dataset.resizeCol;
479
541
  if (id === void 0) return;
480
542
  this.#requestResizes(measureColumns(this.renderRoot, [id]));
481
543
  }
482
544
  };
483
545
  this.#onKeydown = (event) => {
484
- const cell = gridCell(event.composedPath()[0] ?? event.target ?? this);
546
+ const origin = event.composedPath()[0] ?? event.target ?? this;
547
+ const group = groupCell(origin);
548
+ const cell = gridCell(origin) ?? group;
485
549
  const id = cell?.dataset.col;
486
- if (!cell || id === void 0) return;
550
+ if (!cell || id === void 0 && !group) return;
487
551
  const scroller = event.currentTarget;
488
552
  if (event.key === "Tab" && event.shiftKey) {
489
553
  scroller.tabIndex = -1;
@@ -492,21 +556,22 @@ var ArgusTable = class ArgusTable extends LitElement {
492
556
  }, { once: true });
493
557
  return;
494
558
  }
559
+ if (group || id === void 0) {
560
+ this.#groupKey(event, cell, scroller);
561
+ return;
562
+ }
495
563
  const pluginTarget = id === "--argus-selection" ? void 0 : this.#cellTarget(event);
496
564
  if (pluginTarget && this.#plugins.keydown(event, pluginTarget)) return;
497
565
  const inHeader = cell.getAttribute("role") === "columnheader";
498
566
  const { key } = event;
499
- const arrow = key === "ArrowLeft" || key === "ArrowRight";
500
- if (inHeader && arrow && event.altKey && event.shiftKey) {
501
- event.preventDefault();
502
- const towardEnd = key === "ArrowRight" === (this.#direction() === 1);
503
- this.#moveByKey(id, towardEnd ? 1 : -1);
504
- return;
505
- }
506
- if (inHeader && event.altKey && arrow) {
567
+ if (inHeader && (key === "ArrowLeft" || key === "ArrowRight") && event.altKey) {
507
568
  event.preventDefault();
508
569
  const towardEnd = key === "ArrowRight" === (this.#direction() === 1);
509
- this.#requestResize(id, this.#currentWidth(id, cell) + (towardEnd ? RESIZE_STEP : -10));
570
+ if (event.shiftKey) this.#moveByKey({
571
+ kind: "column",
572
+ id
573
+ }, towardEnd ? 1 : -1);
574
+ else this.#requestResize(id, this.#currentWidth(id, cell) + (towardEnd ? RESIZE_STEP : -10));
510
575
  return;
511
576
  }
512
577
  if (inHeader && (key === "Enter" || key === " ") && cell.dataset.sortCol !== void 0) {
@@ -519,7 +584,7 @@ var ArgusTable = class ArgusTable extends LitElement {
519
584
  if (event.altKey || event.shiftKey || corner && key !== "Home" && key !== "End") return;
520
585
  const model = this.#table.model;
521
586
  const layout = layoutOf(model);
522
- const from = inHeader ? HEADER : gridRowOf(cell.parentElement, layout);
587
+ const from = gridRowOfCell(cell, layout);
523
588
  if (!from) return;
524
589
  if (!inHeader && key === "Enter") {
525
590
  event.preventDefault();
@@ -547,7 +612,7 @@ var ArgusTable = class ArgusTable extends LitElement {
547
612
  case "Home":
548
613
  col = 0;
549
614
  if (corner) {
550
- pos = 0;
615
+ pos = this.#headerLevelDown(model, 0, columns[0]?.id ?? id);
551
616
  this.#virtual.scrollToEdge("start");
552
617
  }
553
618
  break;
@@ -558,11 +623,20 @@ var ArgusTable = class ArgusTable extends LitElement {
558
623
  default: return;
559
624
  }
560
625
  event.preventDefault();
561
- const row = layout.at(pos) ?? HEADER;
626
+ let row = layout.at(pos) ?? layout.leafHeader;
627
+ const target = columns[Math.min(Math.max(col, 0), columns.length - 1)];
628
+ if (!target) return;
629
+ if (row.section === "header" && row.index < layout.header - 1 && key === "ArrowUp") {
630
+ const level = this.#headerLevelUp(model, row.index, target.id);
631
+ if (level === void 0) return;
632
+ row = {
633
+ section: "header",
634
+ index: level
635
+ };
636
+ }
562
637
  if (key.startsWith("Page") && from.section === "body" && row.section === "body") this.#virtual.scrollBy(this.#virtual.startOf(row.index) - this.#virtual.startOf(from.index));
563
638
  if (corner && key === "End" && row.section === "bottom") this.#virtual.scrollToEdge("end");
564
- const target = columns[Math.min(Math.max(col, 0), columns.length - 1)];
565
- if (target) this.#moveTo(row, target.id, model);
639
+ this.#moveTo(row, target.id, model);
566
640
  };
567
641
  this.#onFocusin = (event) => {
568
642
  const scroller = event.currentTarget;
@@ -575,10 +649,15 @@ var ArgusTable = class ArgusTable extends LitElement {
575
649
  return;
576
650
  }
577
651
  for (const target of event.composedPath()) {
652
+ const group = groupCell(target);
653
+ if (group) {
654
+ this.#focusGroup(group);
655
+ return;
656
+ }
578
657
  const cell = gridCell(target);
579
658
  if (cell?.dataset.col !== void 0) {
580
659
  const model = this.#table.model;
581
- const row = cell.getAttribute("role") === "columnheader" ? HEADER : gridRowOf(cell.parentElement, layoutOf(model));
660
+ const row = gridRowOfCell(cell, layoutOf(model));
582
661
  const { col } = cell.dataset;
583
662
  const prev = this.#active;
584
663
  if (row && (row.section !== prev?.section || row.index !== prev.index || col !== prev.col)) {
@@ -646,24 +725,23 @@ var ArgusTable = class ArgusTable extends LitElement {
646
725
  super.connectedCallback();
647
726
  if (!this.hasAttribute("role")) this.setAttribute("role", "grid");
648
727
  if (this.hasUpdated) this.#observeProbe();
649
- if (this.#lightPart && !this.#lightConnected) {
650
- this.#lightConnected = true;
651
- this.#lightPart.setConnected(true);
652
- flushContent(this);
653
- }
728
+ this.#setLightConnected(true);
654
729
  this.#unwatchMaxHeight = watchMaxElementHeight(() => {
655
730
  this.requestUpdate();
656
731
  });
657
732
  }
733
+ /** Connects or disconnects the cell content (framework content mounts and unmounts with it). */
734
+ #setLightConnected(connected) {
735
+ if (!this.#lightPart || this.#lightConnected === connected) return;
736
+ this.#lightConnected = connected;
737
+ this.#lightPart.setConnected(connected);
738
+ flushContent(this);
739
+ }
658
740
  disconnectedCallback() {
659
741
  super.disconnectedCallback();
660
742
  this.#probeObserver?.disconnect();
661
743
  this.#rowMeasure.stop();
662
- if (this.#lightPart && this.#lightConnected) {
663
- this.#lightConnected = false;
664
- this.#lightPart.setConnected(false);
665
- flushContent(this);
666
- }
744
+ this.#setLightConnected(false);
667
745
  this.#unwatchMaxHeight?.();
668
746
  this.#unwatchMaxHeight = void 0;
669
747
  }
@@ -680,6 +758,14 @@ var ArgusTable = class ArgusTable extends LitElement {
680
758
  return this.columns;
681
759
  }
682
760
  /**
761
+ * `columnGroups` when it has any or `columns` is set (which turns off all
762
+ * children), otherwise the `<argus-column-group>` children.
763
+ */
764
+ #effectiveGroups() {
765
+ if (this.columnGroups.length > 0 || this.columns.length > 0) return this.columnGroups;
766
+ return this.#children.groups;
767
+ }
768
+ /**
683
769
  * The node the light-DOM content is rendered before. Lit's part then ends there,
684
770
  * so children appended later (an `<argus-column>`) never land inside it. If the
685
771
  * marker was removed (say by `innerHTML`), a new one starts a fresh part.
@@ -705,7 +791,7 @@ var ArgusTable = class ArgusTable extends LitElement {
705
791
  /**
706
792
  * Fits columns to their content: the header, the pinned rows and the body rows
707
793
  * rendered now (rows out of view aren't measured). It resizes them like a drag
708
- * would, so `argus-column-resize` fires for each column that changes, and with a
794
+ * would, so `argus-column-sizing-change` fires for each column that changes, and with a
709
795
  * controlled `columnSizing` nothing changes until you set it. Without `ids`, fits
710
796
  * every resizable column; columns with `resizable: false` keep their width.
711
797
  * With `virtualizeColumns`, columns out of view render for the measurement.
@@ -745,13 +831,14 @@ var ArgusTable = class ArgusTable extends LitElement {
745
831
  * The state users change, as one object of plain JSON for saving a user's
746
832
  * layout: `sorting`, `columnSizing`, `columnOrder`, `columnPinning`,
747
833
  * `columnVisibility`, `columnFilters`, `selection`, `quickFilter`, and `scroll`
748
- * (the row at the top and the horizontal offset). Controlled properties are
834
+ * (the row at the top and the inline offset), with `version: 1`. Controlled properties are
749
835
  * included with the values you set. The object is a copy: changing it changes
750
836
  * nothing in the table.
751
837
  */
752
838
  getState() {
753
- const scroller = this.renderRoot.querySelector("[part~=\"table\"]");
839
+ const scroller = this.#scroller();
754
840
  return structuredClone({
841
+ version: 1,
755
842
  sorting: this.#sorting.value,
756
843
  columnSizing: this.#sizing.value,
757
844
  columnOrder: this.#order.value,
@@ -762,14 +849,14 @@ var ArgusTable = class ArgusTable extends LitElement {
762
849
  quickFilter: this.#quickFilter,
763
850
  scroll: {
764
851
  row: this.#virtual.topRow,
765
- left: Math.abs(scroller?.scrollLeft ?? 0)
852
+ inline: Math.abs(scroller?.scrollLeft ?? 0)
766
853
  }
767
854
  });
768
855
  }
769
856
  /**
770
857
  * Restores state from `getState()`, for example a layout saved in
771
858
  * `localStorage`. Only the keys given change; ids of columns that no longer
772
- * exist are ignored. Properties you control are left alone (set them
859
+ * exist are ignored. State of another `version` is ignored as a whole. Properties you control are left alone (set them
773
860
  * yourself), and the others stay uncontrolled. No change events fire.
774
861
  * `quickFilter` is set like the property: a search box of yours shows it only
775
862
  * if you read it back. `scroll` is applied after the rest, so with a
@@ -777,6 +864,7 @@ var ArgusTable = class ArgusTable extends LitElement {
777
864
  * has re-rendered there.
778
865
  */
779
866
  async setState(state) {
867
+ if ((state.version ?? 1) !== 1) return;
780
868
  this.#restore("sorting", this.#sorting, state.sorting);
781
869
  this.#restore("columnSizing", this.#sizing, state.columnSizing);
782
870
  this.#restore("columnOrder", this.#order, state.columnOrder);
@@ -795,8 +883,8 @@ var ArgusTable = class ArgusTable extends LitElement {
795
883
  row: scroll.row,
796
884
  count
797
885
  };
798
- const scroller = this.renderRoot.querySelector("[part~=\"table\"]");
799
- if (scroller) scroller.scrollLeft = scroll.left * this.#direction();
886
+ const scroller = this.#scroller();
887
+ if (scroller && scroll.inline >= 0) scroller.scrollLeft = scroll.inline * this.#direction();
800
888
  await new Promise(requestAnimationFrame);
801
889
  await this.#settled();
802
890
  }
@@ -822,9 +910,13 @@ var ArgusTable = class ArgusTable extends LitElement {
822
910
  }
823
911
  willUpdate(changed) {
824
912
  if (changed.has("plugins")) this.#plugins.set(this.plugins);
913
+ this.#plugins.willUpdate(new Set(changed.keys()));
825
914
  const columns = this.#plugins.transform(this.#effectiveColumns());
826
915
  this.#columnsChanged = columns !== this.#columns;
827
916
  this.#columns = columns;
917
+ const groups = this.#plugins.transformGroups(this.#effectiveGroups());
918
+ this.#groupsChanged = groups !== this.#groups;
919
+ this.#groups = groups;
828
920
  const filtersChanged = changed.has("columnFilters") || changed.has("quickFilter");
829
921
  if (changed.has("dataSource") || changed.has("sorting") || changed.has("getRowId") || filtersChanged) {
830
922
  this.#data.configure({
@@ -838,10 +930,11 @@ var ArgusTable = class ArgusTable extends LitElement {
838
930
  this.#scrollTarget = void 0;
839
931
  }
840
932
  if (filtersChanged && this.hasUpdated) this.#announceRows = true;
841
- if (this.#columnsChanged || changed.has("data") || changed.has("getRowId") || changed.has("sorting") || changed.has("columnSizing") || changed.has("columnOrder") || changed.has("columnPinning") || changed.has("columnVisibility") || changed.has("columnFilters") || changed.has("quickFilter") || changed.has("pinnedRows")) {
933
+ if (this.#columnsChanged || this.#groupsChanged || changed.has("data") || changed.has("getRowId") || changed.has("sorting") || changed.has("columnSizing") || changed.has("columnOrder") || changed.has("columnPinning") || changed.has("columnVisibility") || changed.has("columnFilters") || changed.has("quickFilter") || changed.has("pinnedRows")) {
842
934
  if (this.#columnsChanged) this.#autoWidths.sync(columns.filter((c) => c.width === "auto").map((c) => c.id));
843
935
  this.#table.update({
844
936
  columns,
937
+ columnGroups: groups,
845
938
  data: this.data,
846
939
  getRowId: this.getRowId,
847
940
  sorting: this.#sorting.value,
@@ -873,7 +966,7 @@ var ArgusTable = class ArgusTable extends LitElement {
873
966
  this.#internals.ariaRowCount = unknownTotal ? "-1" : String(layout.total);
874
967
  this.#internals.ariaColCount = String(model.columns.length);
875
968
  this.#internals.ariaMultiSelectable = this.#mode() === "multi" ? "true" : null;
876
- const extra = this.#rowPx + this.#pinnedPx("top", layout) + this.#pinnedPx("bottom", layout);
969
+ const extra = this.#headerAndPinnedPx(layout);
877
970
  const auto = this.rowHeight === "auto";
878
971
  if (!auto) this.#rowMeasure.stop();
879
972
  else if (changed.size > 0 || this.#columnsChanged) this.#rowMeasure.invalidate();
@@ -899,20 +992,22 @@ var ArgusTable = class ArgusTable extends LitElement {
899
992
  this.#window = this.#virtual.computeWindow();
900
993
  const rendered = this.#columnWindow.compute(columns, this.virtualizeColumns, this.virtualizeColumns ? this.#keptColumns(model) : []);
901
994
  this.#rendered = rendered;
902
- const pinnedWidth = (side) => columns.reduce((sum, c) => c.pin === side ? sum + c.width : sum, 0);
903
- const pinned = (side) => `${String(pinnedWidth(side))}px`;
995
+ const pinStart = pinnedWidth(columns, "start");
996
+ const pinEnd = pinnedWidth(columns, "end");
997
+ const cols = columns.map(track).join(" ");
904
998
  const layout = layoutOf(model);
905
999
  const style = {
906
- "--_cols": columns.map(track).join(" "),
1000
+ "--_cols": cols,
907
1001
  "--_width": `${String(model.totalWidth)}px`,
908
- "--_pin-start": pinned("start"),
909
- "--_pin-end": pinned("end"),
1002
+ "--_pin-start": `${String(pinStart)}px`,
1003
+ "--_pin-end": `${String(pinEnd)}px`,
1004
+ "--_header-rows": String(layout.header),
910
1005
  "--_pin-top-rows": String(layout.top),
911
1006
  "--_pin-bottom-rows": String(layout.bottom)
912
1007
  };
913
1008
  const auto = this.rowHeight === "auto";
914
1009
  if (auto) {
915
- const key = `${style["--_cols"] ?? ""}|${String(this.#rowPx)}`;
1010
+ const key = `${cols}|${String(this.#rowPx)}`;
916
1011
  if (key !== this.#rowsKey) {
917
1012
  this.#rowsKey = key;
918
1013
  this.#rowMeasure.invalidate();
@@ -930,7 +1025,7 @@ var ArgusTable = class ArgusTable extends LitElement {
930
1025
  index: 0
931
1026
  });
932
1027
  if (typeof this.rowHeight === "number" && this.rowHeight > 0) style["--_row-height"] = `${String(this.rowHeight)}px`;
933
- const contentPx = this.#rowPx + this.#pinnedPx("top", layout) + this.#pinnedPx("bottom", layout) + (model.rowCount === 0 ? 0 : this.#window.totalSize);
1028
+ const contentPx = this.#headerAndPinnedPx(layout) + (model.rowCount === 0 ? 0 : this.#window.totalSize);
934
1029
  return html`<div
935
1030
  part="table"
936
1031
  role="presentation"
@@ -942,14 +1037,20 @@ var ArgusTable = class ArgusTable extends LitElement {
942
1037
  @mousedown=${this.#onMousedown}
943
1038
  @click=${this.#onClick}
944
1039
  >
945
- ${renderHeader(rendered, this.#headerHandlers, activeIn("header")?.col, this.#drag, this.#checkAllOf(model))}
1040
+ ${renderHeader({
1041
+ columns: rendered,
1042
+ groups: model.headerGroups,
1043
+ handlers: this.#headerHandlers,
1044
+ active: activeIn("header"),
1045
+ drag: this.#drag,
1046
+ checkAll: this.#checkAllOf(model)
1047
+ })}
946
1048
  ${renderPinnedRows("top", layout.top, rendered, firstIndex("top"), activeIn("top"))}
947
1049
  ${model.rowCount === 0 ? html`<div part="empty"><slot name="empty">No rows</slot></div>` : renderBody(this.#window.rows, rendered, this.#window.totalSize, activeIn("body"), firstIndex("body"), this.#loading(), this.#selectedAt(model), this.#selectionColumn ? this.#checkboxHandlers : void 0, auto)}
948
1050
  ${renderPinnedRows("bottom", layout.bottom, rendered, firstIndex("bottom"), activeIn("bottom"))}
949
1051
  <div class="probe" aria-hidden="true"></div>
950
1052
  </div>
951
- ${pinShadow("start", pinnedWidth("start"), contentPx)}
952
- ${pinShadow("end", pinnedWidth("end"), contentPx)}`;
1053
+ ${pinShadow("start", pinStart, contentPx)} ${pinShadow("end", pinEnd, contentPx)}`;
953
1054
  }
954
1055
  firstUpdated() {
955
1056
  this.#observeProbe();
@@ -957,7 +1058,7 @@ var ArgusTable = class ArgusTable extends LitElement {
957
1058
  }
958
1059
  updated(changed) {
959
1060
  const model = this.#table.model;
960
- const part = renderLightDom(this, model, this.#window.rows, this.#rendered, this.#lightDomEnd());
1061
+ const part = renderLightDom(this, model, this.#window.rows, this.#rendered, model.headerGroups, this.#lightDomEnd());
961
1062
  if (this.#lightPart && this.#lightPart !== part) this.#lightPart.setConnected(false);
962
1063
  this.#lightPart = part;
963
1064
  flushContent(this);
@@ -965,45 +1066,25 @@ var ArgusTable = class ArgusTable extends LitElement {
965
1066
  const resized = this.rowHeight === "auto" && this.#measureRows();
966
1067
  if (this.#scrollToTop) {
967
1068
  this.#scrollToTop = false;
968
- const scroller = this.renderRoot.querySelector("[part~=\"table\"]");
1069
+ const scroller = this.#scroller();
969
1070
  if (scroller) scroller.scrollTop = 0;
970
1071
  }
971
1072
  this.#chaseScrollTarget(model.rowCount);
972
- if (this.#columnsChanged || changed.has("columnSizing") || changed.has("columnOrder") || changed.has("columnPinning") || changed.has("columnVisibility") || this.#autoWidthsChanged || this.#selectionColumnChanged) this.#pinScroll.measure();
1073
+ if (this.#columnsChanged || this.#groupsChanged || changed.has("columnSizing") || changed.has("columnOrder") || changed.has("columnPinning") || changed.has("columnVisibility") || this.#autoWidthsChanged || this.#selectionColumnChanged) this.#pinScroll.measure();
973
1074
  if (this.#focusPending && !resized) {
974
1075
  this.#focusPending = false;
975
1076
  this.#focusActive();
976
1077
  }
977
1078
  if (changed.has("density") || changed.has("rowHeight")) this.#measureRowHeight();
978
- if (this.#appliedResizes.size > 0) {
979
- const details = [...this.#appliedResizes.values()];
980
- this.#appliedResizes.clear();
981
- for (const detail of details) fire(this, COLUMN_RESIZE, detail);
982
- }
1079
+ this.#fireApplied(this.#appliedResizes, COLUMN_SIZING_CHANGE);
983
1080
  if (this.#announceMove) {
984
1081
  this.#announceMoved(model, this.#announceMove);
985
1082
  this.#announceMove = void 0;
986
1083
  }
987
- if (this.#appliedReorders.size > 0) {
988
- const details = [...this.#appliedReorders.values()];
989
- this.#appliedReorders.clear();
990
- for (const detail of details) fire(this, COLUMN_REORDER, detail);
991
- }
992
- if (this.#appliedPins.size > 0) {
993
- const details = [...this.#appliedPins.values()];
994
- this.#appliedPins.clear();
995
- for (const detail of details) fire(this, COLUMN_PIN, detail);
996
- }
997
- if (this.#appliedHides.size > 0) {
998
- const details = [...this.#appliedHides.values()];
999
- this.#appliedHides.clear();
1000
- for (const detail of details) fire(this, COLUMN_HIDE, detail);
1001
- }
1002
- if (this.#appliedFilters.size > 0) {
1003
- const details = [...this.#appliedFilters.values()];
1004
- this.#appliedFilters.clear();
1005
- for (const detail of details) fire(this, COLUMN_FILTER, detail);
1006
- }
1084
+ this.#fireApplied(this.#appliedReorders, COLUMN_ORDER_CHANGE);
1085
+ this.#fireApplied(this.#appliedPins, COLUMN_PINNING_CHANGE);
1086
+ this.#fireApplied(this.#appliedHides, COLUMN_VISIBILITY_CHANGE);
1087
+ this.#fireApplied(this.#appliedFilters, COLUMN_FILTERS_CHANGE);
1007
1088
  if (this.#announceRows) {
1008
1089
  this.#announceRows = false;
1009
1090
  this.#announceRowCount(model);
@@ -1011,20 +1092,20 @@ var ArgusTable = class ArgusTable extends LitElement {
1011
1092
  if (this.#appliedSort) {
1012
1093
  const sorting = this.#appliedSort;
1013
1094
  this.#appliedSort = void 0;
1014
- fire(this, SORT_CHANGE, { sorting });
1095
+ fire(this, SORTING_CHANGE, { sorting });
1015
1096
  }
1016
1097
  if (this.#appliedSelection) {
1017
1098
  const selection = this.#appliedSelection;
1018
1099
  this.#appliedSelection = void 0;
1019
- fire(this, ROW_SELECT, { selection });
1100
+ fire(this, SELECTION_CHANGE, { selection });
1020
1101
  }
1021
1102
  const { range } = this.#window;
1022
1103
  const key = range ? `${String(range.start)}:${String(range.end)}` : "";
1023
1104
  if (range && key !== this.#lastRange) {
1024
1105
  this.#lastRange = key;
1025
1106
  fire(this, RANGE_CHANGE, {
1026
- start: range.start,
1027
- end: range.end
1107
+ first: range.start,
1108
+ last: range.end
1028
1109
  });
1029
1110
  }
1030
1111
  if (range) {
@@ -1043,12 +1124,19 @@ var ArgusTable = class ArgusTable extends LitElement {
1043
1124
  #keptColumns(model) {
1044
1125
  return [
1045
1126
  this.#active?.col,
1046
- this.#drag?.columnId,
1127
+ ...this.#drag?.key.startsWith("c:") ? this.#drag.columns : [],
1047
1128
  this.dataSource ? model.columns.find((c) => c.kind === "data")?.id : void 0,
1048
1129
  ...this.#autoWidths.pending(model.columns),
1049
1130
  ...this.#measureKeep
1050
1131
  ];
1051
1132
  }
1133
+ /** Fires the events of changes the table applied itself, once the update is done. */
1134
+ #fireApplied(applied, name) {
1135
+ if (applied.size === 0) return;
1136
+ const details = [...applied.values()];
1137
+ applied.clear();
1138
+ for (const detail of details) fire(this, name, detail);
1139
+ }
1052
1140
  /**
1053
1141
  * Measures the `width: 'auto'` columns once loaded body rows render, or right
1054
1142
  * away when there are no rows (the header and pinned rows only). While a data
@@ -1113,6 +1201,129 @@ var ArgusTable = class ArgusTable extends LitElement {
1113
1201
  */
1114
1202
  #onKeydown;
1115
1203
  /**
1204
+ * The header row to go to from header row `level` down, over a column: the first
1205
+ * group row from `level` with a group over it, else the column headers.
1206
+ */
1207
+ #headerLevelDown(model, level, columnId) {
1208
+ const leaf = model.headerGroups.length;
1209
+ for (let l = level; l < leaf; l++) if (model.headerCell(l, columnId)?.kind === "group") return l;
1210
+ return leaf;
1211
+ }
1212
+ /** The first group row from `level` up with a group over a column, or `undefined`. */
1213
+ #headerLevelUp(model, level, columnId) {
1214
+ for (let l = Math.min(level, model.headerGroups.length - 1); l >= 0; l--) if (model.headerCell(l, columnId)?.kind === "group") return l;
1215
+ }
1216
+ /**
1217
+ * Keys on a focused group header. Arrows move between group headers in its row
1218
+ * (fillers are skipped) and to the headers above and below it; ArrowDown ends on
1219
+ * the column headers. Home and End go to the row's first and last group,
1220
+ * PageDown into the body, Ctrl+Home and Ctrl+End to the corners, and
1221
+ * Alt+Shift+Arrow moves the group. Groups don't sort or resize.
1222
+ */
1223
+ #groupKey(event, cell, scroller) {
1224
+ const model = this.#table.model;
1225
+ const layout = layoutOf(model);
1226
+ const at = gridRowOf(cell.parentElement, layout);
1227
+ const row = at?.section === "header" ? model.headerGroups[at.index] : void 0;
1228
+ const here = row?.find((c) => c.key === cell.dataset.groupKey);
1229
+ if (!at || !row || here?.groupId === void 0) return;
1230
+ const level = at.index;
1231
+ const active = this.#active;
1232
+ const anchor = active?.section === "header" && active.index === level && here.columns.includes(active.col) ? active.col : here.columns[0];
1233
+ if (anchor === void 0) return;
1234
+ const { key } = event;
1235
+ const arrow = key === "ArrowLeft" || key === "ArrowRight";
1236
+ const towardEnd = key === "ArrowRight" === (this.#direction() === 1);
1237
+ if (arrow && event.altKey) {
1238
+ event.preventDefault();
1239
+ if (event.shiftKey) this.#moveByKey({
1240
+ kind: "group",
1241
+ id: here.groupId,
1242
+ pin: here.pin
1243
+ }, towardEnd ? 1 : -1);
1244
+ else if (here.resizable) {
1245
+ const starts = this.#groupWidths(here.columns);
1246
+ const total = [...starts.values()].reduce((sum, w) => sum + w, 0);
1247
+ this.#requestGroupResize(here.columns, starts, total + (towardEnd ? RESIZE_STEP : -10));
1248
+ }
1249
+ return;
1250
+ }
1251
+ if (key === "Enter" || key === " ") {
1252
+ event.preventDefault();
1253
+ return;
1254
+ }
1255
+ if (this.#selectionKey(event, cell, anchor, true, scroller)) return;
1256
+ const corner = event.ctrlKey || event.metaKey;
1257
+ if (event.altKey || event.shiftKey || corner && key !== "Home" && key !== "End") return;
1258
+ const groups = row.filter((c) => c.kind === "group");
1259
+ const columns = model.columns;
1260
+ let to;
1261
+ switch (key) {
1262
+ case "ArrowLeft":
1263
+ case "ArrowRight": {
1264
+ const col = groups[groups.indexOf(here) + (towardEnd ? 1 : -1)]?.columns[0];
1265
+ if (col !== void 0) to = {
1266
+ row: at,
1267
+ col
1268
+ };
1269
+ break;
1270
+ }
1271
+ case "Home":
1272
+ case "End":
1273
+ if (corner) {
1274
+ const first = key === "Home";
1275
+ const col = (first ? columns[0] : columns.at(-1))?.id;
1276
+ if (col === void 0) break;
1277
+ if (first) this.#virtual.scrollToEdge("start");
1278
+ else if (layout.bottom > 0) this.#virtual.scrollToEdge("end");
1279
+ const pos = first ? this.#headerLevelDown(model, 0, col) : layout.total - 1;
1280
+ to = {
1281
+ row: layout.at(pos) ?? layout.leafHeader,
1282
+ col
1283
+ };
1284
+ } else {
1285
+ const col = (key === "Home" ? groups[0] : groups.at(-1))?.columns[0];
1286
+ if (col !== void 0) to = {
1287
+ row: at,
1288
+ col
1289
+ };
1290
+ }
1291
+ break;
1292
+ case "ArrowDown":
1293
+ to = {
1294
+ row: {
1295
+ section: "header",
1296
+ index: this.#headerLevelDown(model, level + 1, anchor)
1297
+ },
1298
+ col: anchor
1299
+ };
1300
+ break;
1301
+ case "ArrowUp": {
1302
+ const up = this.#headerLevelUp(model, level - 1, anchor);
1303
+ if (up !== void 0) to = {
1304
+ row: {
1305
+ section: "header",
1306
+ index: up
1307
+ },
1308
+ col: anchor
1309
+ };
1310
+ break;
1311
+ }
1312
+ case "PageDown": {
1313
+ const pos = this.#verticalStep(key, layout.position(at), layout, scroller);
1314
+ to = {
1315
+ row: layout.at(pos) ?? at,
1316
+ col: anchor
1317
+ };
1318
+ break;
1319
+ }
1320
+ case "PageUp": break;
1321
+ default: return;
1322
+ }
1323
+ event.preventDefault();
1324
+ if (to) this.#moveTo(to.row, to.col, model);
1325
+ }
1326
+ /**
1116
1327
  * The grid position ArrowUp/ArrowDown (one row) or PageUp/PageDown (a page of
1117
1328
  * the body) moves to from `pos`. PageDown from above the body lands a page
1118
1329
  * into it and stops at its last row; PageUp stays in the body.
@@ -1135,9 +1346,9 @@ var ArgusTable = class ArgusTable extends LitElement {
1135
1346
  #pageRows(key, pos, layout, scroller) {
1136
1347
  if (this.rowHeight !== "auto") {
1137
1348
  const fit = Math.floor(scroller.clientHeight / this.#rowPx + .01);
1138
- return Math.max(1, fit - 1 - layout.top - layout.bottom);
1349
+ return Math.max(1, fit - layout.header - layout.top - layout.bottom);
1139
1350
  }
1140
- const view = scroller.clientHeight - this.#rowPx - this.#pinnedPx("top", layout) - this.#pinnedPx("bottom", layout);
1351
+ const view = scroller.clientHeight - this.#headerAndPinnedPx(layout);
1141
1352
  const bodyFirst = layout.position({
1142
1353
  section: "body",
1143
1354
  index: 0
@@ -1207,6 +1418,22 @@ var ArgusTable = class ArgusTable extends LitElement {
1207
1418
  }
1208
1419
  /** Focus that lands in a cell (a click, or content inside it) makes that cell active. */
1209
1420
  #onFocusin;
1421
+ /** Focus on a group header makes it active, on the column it was active on, else its first. */
1422
+ #focusGroup(cell) {
1423
+ const model = this.#table.model;
1424
+ const row = gridRowOf(cell.parentElement, layoutOf(model));
1425
+ const group = row && model.headerGroups[row.index]?.find((c) => c.key === cell.dataset.groupKey);
1426
+ const first = group?.columns[0];
1427
+ if (!row || !group || first === void 0) return;
1428
+ const prev = this.#active;
1429
+ if (prev?.section === "header" && prev.index === row.index && group.columns.includes(prev.col)) return;
1430
+ this.#active = {
1431
+ ...row,
1432
+ rowId: void 0,
1433
+ col: first
1434
+ };
1435
+ this.requestUpdate();
1436
+ }
1210
1437
  /**
1211
1438
  * `argus-cell-click`, then `argus-row-click`, for clicks in the body; then,
1212
1439
  * with a `selectionMode`, the click selects its row.
@@ -1255,7 +1482,7 @@ var ArgusTable = class ArgusTable extends LitElement {
1255
1482
  }
1256
1483
  /**
1257
1484
  * Whether all, some or none of the selectable rows are selected. With `data`
1258
- * it's worked out once per `selection`, `data` and `getRowId`, not on scroll;
1485
+ * it's worked out once per selection and rows shown, not on scroll;
1259
1486
  * with a `dataSource` every time, over the loaded rows, which eviction keeps few.
1260
1487
  */
1261
1488
  #checkAllState(model) {
@@ -1277,11 +1504,13 @@ var ArgusTable = class ArgusTable extends LitElement {
1277
1504
  this.#visibility.value
1278
1505
  ];
1279
1506
  const cached = this.#checkAll;
1280
- if (cached?.key.length !== key.length || key.some((k, i) => k !== cached.key[i])) this.#checkAll = {
1507
+ if (cached?.key.length === key.length && key.every((k, i) => k === cached.key[i])) return cached.state;
1508
+ const state = count();
1509
+ this.#checkAll = {
1281
1510
  key,
1282
- state: count()
1511
+ state
1283
1512
  };
1284
- return this.#checkAll?.state ?? "none";
1513
+ return state;
1285
1514
  }
1286
1515
  /**
1287
1516
  * A Shift+press on a row in `multi` mode selects a range of rows, not the
@@ -1332,8 +1561,9 @@ var ArgusTable = class ArgusTable extends LitElement {
1332
1561
  /** The anchor row and where it is now; `undefined` if it isn't in the rows (or not loaded). */
1333
1562
  #anchorAt(model) {
1334
1563
  const id = this.#anchor;
1335
- const index = id === void 0 ? void 0 : model.indexOf(id);
1336
- return id === void 0 || index === void 0 ? void 0 : {
1564
+ if (id === void 0) return void 0;
1565
+ const index = model.indexOf(id);
1566
+ return index === void 0 ? void 0 : {
1337
1567
  id,
1338
1568
  index
1339
1569
  };
@@ -1351,7 +1581,7 @@ var ArgusTable = class ArgusTable extends LitElement {
1351
1581
  if (applied) {
1352
1582
  this.#appliedSelection = value;
1353
1583
  this.requestUpdate("selection", old);
1354
- } else fire(this, ROW_SELECT, { selection: value });
1584
+ } else fire(this, SELECTION_CHANGE, { selection: value });
1355
1585
  }
1356
1586
  /**
1357
1587
  * Fires `argus-cell-click` (when a cell is on the path), then `argus-row-click`,
@@ -1375,6 +1605,8 @@ var ArgusTable = class ArgusTable extends LitElement {
1375
1605
  if (!row) return;
1376
1606
  if (columnId !== void 0) fire(this, CELL_CLICK, {
1377
1607
  row: row.original,
1608
+ rowId: row.id,
1609
+ index,
1378
1610
  columnId,
1379
1611
  value: model.valueAt(index, columnId)
1380
1612
  });
@@ -1390,6 +1622,8 @@ var ArgusTable = class ArgusTable extends LitElement {
1390
1622
  if (!row) return;
1391
1623
  if (columnId !== void 0) fire(this, CELL_CLICK, {
1392
1624
  row: row.original,
1625
+ rowId: row.id,
1626
+ index,
1393
1627
  columnId,
1394
1628
  value: model.pinnedValueAt(pinned, index, columnId),
1395
1629
  pinned
@@ -1425,13 +1659,27 @@ var ArgusTable = class ArgusTable extends LitElement {
1425
1659
  const active = this.#active;
1426
1660
  if (!active) return;
1427
1661
  const col = CSS.escape(active.col);
1428
- const index = layoutOf(this.#table.model).ariaRowIndex(active);
1429
- const cell = this.renderRoot.querySelector(`[role='row'][aria-rowindex="${String(index)}"] > [data-col="${col}"]`);
1662
+ const model = this.#table.model;
1663
+ const layout = layoutOf(model);
1664
+ const index = layout.ariaRowIndex(active);
1665
+ const row = `[role='row'][aria-rowindex="${String(index)}"]`;
1666
+ if (active.section === "header" && active.index < layout.header - 1) {
1667
+ const key = model.headerCell(active.index, active.col)?.key ?? "";
1668
+ const group = this.renderRoot.querySelector(`${row} > [data-group-key="${CSS.escape(key)}"]`);
1669
+ if (!group) return;
1670
+ group.focus({ preventScroll: true });
1671
+ const leaf = `[role='row'][aria-rowindex="${String(layout.header)}"] > [data-col="${col}"]`;
1672
+ this.renderRoot.querySelector(leaf)?.scrollIntoView({
1673
+ block: "nearest",
1674
+ inline: "nearest"
1675
+ });
1676
+ return;
1677
+ }
1678
+ const cell = this.renderRoot.querySelector(`${row} > [data-col="${col}"]`);
1430
1679
  if (!cell) return;
1431
1680
  cell.focus({ preventScroll: true });
1432
1681
  if (active.section === "body" && (this.#virtual.scaled || this.rowHeight === "auto")) {
1433
- const layout = layoutOf(this.#table.model);
1434
- const before = this.#rowPx + this.#pinnedPx("top", layout);
1682
+ const before = layout.header * this.#rowPx + this.#pinnedPx("top", layout);
1435
1683
  if (this.#virtual.reveal(active.index, before, this.#pinnedPx("bottom", layout))) {
1436
1684
  this.#focusPending = true;
1437
1685
  return;
@@ -1457,14 +1705,24 @@ var ArgusTable = class ArgusTable extends LitElement {
1457
1705
  this.#active = void 0;
1458
1706
  return;
1459
1707
  }
1708
+ const layout = layoutOf(model);
1709
+ const leaf = layout.header - 1;
1460
1710
  const prev = this.#active ?? {
1461
- ...HEADER,
1711
+ ...layout.leafHeader,
1462
1712
  rowId: void 0,
1463
1713
  col: first.id
1464
1714
  };
1465
1715
  const col = columns.some((c) => c.id === prev.col) ? prev.col : this.#columnInPlaceOf(model, prev.col, first.id);
1466
1716
  let { section, index, rowId } = prev;
1467
1717
  let moved = col !== prev.col && this.#active !== void 0;
1718
+ if (section === "header") {
1719
+ if (!this.#active || index === this.#leafLevel) index = leaf;
1720
+ else if (index >= leaf || model.headerCell(index, col)?.kind !== "group") {
1721
+ index = leaf;
1722
+ moved = true;
1723
+ }
1724
+ }
1725
+ this.#leafLevel = leaf;
1468
1726
  if (section === "body" && rowId !== void 0) {
1469
1727
  const found = model.indexOf(rowId);
1470
1728
  const unloaded = index < model.rowCount && model.rowAt(index) === void 0;
@@ -1483,7 +1741,7 @@ var ArgusTable = class ArgusTable extends LitElement {
1483
1741
  rowId = index >= 0 ? model.pinnedRowAt(section, index)?.id : void 0;
1484
1742
  }
1485
1743
  if (index < 0) {
1486
- ({section, index} = HEADER);
1744
+ ({section, index} = layout.leafHeader);
1487
1745
  rowId = void 0;
1488
1746
  }
1489
1747
  if (moved && this.matches(":focus-within")) this.#focusPending = true;
@@ -1517,6 +1775,49 @@ var ArgusTable = class ArgusTable extends LitElement {
1517
1775
  const view = this.#table.model.columns.find((c) => c.id === columnId);
1518
1776
  return view && view.flex === void 0 ? view.width : cell.getBoundingClientRect().width;
1519
1777
  }
1778
+ /** The group header cell with a key, from the model. */
1779
+ #groupCellOf(key) {
1780
+ if (key === void 0) return void 0;
1781
+ return this.#table.model.headerGroups.flat().find((c) => c.key === key);
1782
+ }
1783
+ /**
1784
+ * The widths a group resize starts from, by column: the latest requested ones
1785
+ * (key presses can come faster than renders), else the model's, else the
1786
+ * rendered width of a flex column.
1787
+ */
1788
+ #groupWidths(columnIds) {
1789
+ const widths = /* @__PURE__ */ new Map();
1790
+ for (const id of columnIds) {
1791
+ const header = this.renderRoot.querySelector(`[part~='header-cell'][data-col="${CSS.escape(id)}"]`);
1792
+ const view = this.#table.model.columns.find((c) => c.id === id);
1793
+ const sized = this.#sizing.value[id];
1794
+ widths.set(id, sized ?? (header ? this.#currentWidth(id, header) : view?.width ?? 0));
1795
+ }
1796
+ return widths;
1797
+ }
1798
+ /**
1799
+ * Resizes a group to `total` px: the change is spread over its resizable columns
1800
+ * in proportion to their widths, within their bounds (one `argus-column-sizing-change`
1801
+ * per column that changes). The other columns keep their width.
1802
+ */
1803
+ #requestGroupResize(columnIds, starts, total) {
1804
+ const views = new Map(this.#table.model.columns.map((c) => [c.id, c]));
1805
+ let fixed = 0;
1806
+ const spread = [];
1807
+ for (const id of columnIds) {
1808
+ const view = views.get(id);
1809
+ const width = starts.get(id) ?? view?.width ?? 0;
1810
+ if (view?.resizable) spread.push({
1811
+ id,
1812
+ width,
1813
+ minWidth: view.minWidth,
1814
+ maxWidth: view.maxWidth
1815
+ });
1816
+ else fixed += width;
1817
+ }
1818
+ if (spread.length === 0) return;
1819
+ this.#requestResizes(spreadWidth(spread, total - fixed));
1820
+ }
1520
1821
  #direction() {
1521
1822
  return getComputedStyle(this).direction === "rtl" ? -1 : 1;
1522
1823
  }
@@ -1526,8 +1827,8 @@ var ArgusTable = class ArgusTable extends LitElement {
1526
1827
  }
1527
1828
  /**
1528
1829
  * Sets column widths (clamped by core) in one change, through the
1529
- * controlled/uncontrolled rules. One `argus-column-resize` per column that
1530
- * changes, each with the whole new sizing.
1830
+ * controlled/uncontrolled rules. One `argus-column-sizing-change` per column
1831
+ * that changes, each with the whole new sizing.
1531
1832
  */
1532
1833
  #requestResizes(widths) {
1533
1834
  const model = this.#table.model;
@@ -1543,15 +1844,15 @@ var ArgusTable = class ArgusTable extends LitElement {
1543
1844
  ...Object.fromEntries(targets)
1544
1845
  });
1545
1846
  const details = targets.map(([columnId, width]) => ({
1847
+ columnSizing: value,
1546
1848
  columnId,
1547
- width,
1548
- sizing: value
1849
+ width
1549
1850
  }));
1550
1851
  if (applied) {
1551
- for (const detail of this.#appliedResizes.values()) detail.sizing = value;
1852
+ for (const detail of this.#appliedResizes.values()) detail.columnSizing = value;
1552
1853
  for (const detail of details) this.#appliedResizes.set(detail.columnId, detail);
1553
1854
  this.requestUpdate("columnSizing", old);
1554
- } else for (const detail of details) fire(this, COLUMN_RESIZE, detail);
1855
+ } else for (const detail of details) fire(this, COLUMN_SIZING_CHANGE, detail);
1555
1856
  }
1556
1857
  /**
1557
1858
  * Pins or unpins a column through the controlled/uncontrolled rules. Starts
@@ -1563,17 +1864,17 @@ var ArgusTable = class ArgusTable extends LitElement {
1563
1864
  if (!next) return;
1564
1865
  const { value, applied } = this.#pinning.request(next);
1565
1866
  if (applied) {
1566
- for (const detail of this.#appliedPins.values()) detail.pinning = value;
1867
+ for (const detail of this.#appliedPins.values()) detail.columnPinning = value;
1567
1868
  this.#appliedPins.set(columnId, {
1869
+ columnPinning: value,
1568
1870
  columnId,
1569
- pin,
1570
- pinning: value
1871
+ pin
1571
1872
  });
1572
1873
  this.requestUpdate("columnPinning", old);
1573
- } else fire(this, COLUMN_PIN, {
1874
+ } else fire(this, COLUMN_PINNING_CHANGE, {
1875
+ columnPinning: value,
1574
1876
  columnId,
1575
- pin,
1576
- pinning: value
1877
+ pin
1577
1878
  });
1578
1879
  }
1579
1880
  /** Hides or shows a column through the controlled/uncontrolled rules. */
@@ -1583,17 +1884,17 @@ var ArgusTable = class ArgusTable extends LitElement {
1583
1884
  if (!next) return;
1584
1885
  const { value, applied } = this.#visibility.request(next);
1585
1886
  if (applied) {
1586
- for (const detail of this.#appliedHides.values()) detail.visibility = value;
1887
+ for (const detail of this.#appliedHides.values()) detail.columnVisibility = value;
1587
1888
  this.#appliedHides.set(columnId, {
1889
+ columnVisibility: value,
1588
1890
  columnId,
1589
- hidden,
1590
- visibility: value
1891
+ visible: !hidden
1591
1892
  });
1592
1893
  this.requestUpdate("columnVisibility", old);
1593
- } else fire(this, COLUMN_HIDE, {
1894
+ } else fire(this, COLUMN_VISIBILITY_CHANGE, {
1895
+ columnVisibility: value,
1594
1896
  columnId,
1595
- hidden,
1596
- visibility: value
1897
+ visible: !hidden
1597
1898
  });
1598
1899
  }
1599
1900
  /** Sets or clears a column's filter through the controlled/uncontrolled rules. */
@@ -1603,17 +1904,17 @@ var ArgusTable = class ArgusTable extends LitElement {
1603
1904
  if (!next) return;
1604
1905
  const { value, applied } = this.#filters.request(next);
1605
1906
  if (applied) {
1606
- for (const detail of this.#appliedFilters.values()) detail.filters = value;
1907
+ for (const detail of this.#appliedFilters.values()) detail.columnFilters = value;
1607
1908
  this.#appliedFilters.set(columnId, {
1909
+ columnFilters: value,
1608
1910
  columnId,
1609
- filter,
1610
- filters: value
1911
+ columnFilter: filter
1611
1912
  });
1612
1913
  this.requestUpdate("columnFilters", old);
1613
- } else fire(this, COLUMN_FILTER, {
1914
+ } else fire(this, COLUMN_FILTERS_CHANGE, {
1915
+ columnFilters: value,
1614
1916
  columnId,
1615
- filter,
1616
- filters: value
1917
+ columnFilter: filter
1617
1918
  });
1618
1919
  }
1619
1920
  /**
@@ -1626,80 +1927,127 @@ var ArgusTable = class ArgusTable extends LitElement {
1626
1927
  announce(this.ownerDocument, n === 1 ? "1 row" : `${String(n)} rows`);
1627
1928
  }
1628
1929
  /**
1629
- * Moves a column to position `to` among the shown data columns (within its pin group),
1630
- * through the controlled/uncontrolled rules. Starts from the latest requested
1631
- * order, so moves can come faster than renders. `false` if it can't move there.
1930
+ * Moves a column or group to position `to` among its siblings (the columns and
1931
+ * groups of its parent group on its pin side), through the controlled/uncontrolled
1932
+ * rules. Starts from the latest requested order, so moves can come faster than
1933
+ * renders. `false` if it can't move there.
1632
1934
  */
1633
- #requestReorder(columnId, to) {
1935
+ #requestReorder(unit, to) {
1634
1936
  const old = this.#order.value;
1635
- const next = this.#table.model.nextOrder(columnId, to, old);
1636
- if (!next) return false;
1937
+ const model = this.#table.model;
1938
+ const moving = model.siblingUnits(unit, old).find((s) => sameUnit(s.unit, unit));
1939
+ const next = model.nextMove(unit, to, old);
1940
+ const columnId = moving?.columns[0];
1941
+ if (!next || columnId === void 0) return false;
1942
+ const detail = (columnOrder) => unit.kind === "group" ? {
1943
+ columnOrder,
1944
+ columnId,
1945
+ groupId: unit.id
1946
+ } : {
1947
+ columnOrder,
1948
+ columnId
1949
+ };
1637
1950
  const { value, applied } = this.#order.request(next);
1638
1951
  if (applied) {
1639
- for (const detail of this.#appliedReorders.values()) detail.order = value;
1640
- this.#appliedReorders.set(columnId, {
1641
- columnId,
1642
- order: value
1643
- });
1952
+ for (const pending of this.#appliedReorders.values()) pending.columnOrder = value;
1953
+ this.#appliedReorders.set(unitKey(unit), detail(value));
1644
1954
  this.requestUpdate("columnOrder", old);
1645
- } else fire(this, COLUMN_REORDER, {
1646
- columnId,
1647
- order: value
1648
- });
1955
+ } else fire(this, COLUMN_ORDER_CHANGE, detail(value));
1649
1956
  return true;
1650
1957
  }
1651
- /** Alt+Shift+Arrow on a header: one step toward the start (-1) or the end (1). */
1652
- #moveByKey(columnId, step) {
1958
+ /** Alt+Shift+Arrow on a column or group header: one step toward the start (-1) or the end (1). */
1959
+ #moveByKey(unit, step) {
1653
1960
  const model = this.#table.model;
1654
- const shown = new Set(model.columns.map((c) => c.id));
1655
- const from = model.orderOf(this.#order.value).filter((id) => shown.has(id)).indexOf(columnId);
1656
- const view = model.columns.find((c) => c.id === columnId);
1657
- if (from < 0 || !view) return;
1658
- if (!this.#requestReorder(columnId, from + step)) return;
1961
+ const from = model.siblingUnits(unit, this.#order.value).findIndex((s) => sameUnit(s.unit, unit));
1962
+ const at = this.#colIndexOf(model, unit);
1963
+ if (from < 0 || at === void 0) return;
1964
+ if (!this.#requestReorder(unit, from + step)) return;
1659
1965
  this.#focusPending = true;
1660
1966
  this.#announceMove ??= {
1661
- id: columnId,
1662
- from: view.colIndex
1967
+ unit,
1968
+ from: at
1663
1969
  };
1664
1970
  this.requestUpdate();
1665
1971
  }
1666
- /** Tells screen readers where a column moved from the keyboard went. */
1667
- #announceMoved(model, move) {
1668
- const view = model.columns.find((c) => c.id === move.id);
1669
- if (!view || view.colIndex === move.from) return;
1670
- const header = this.renderRoot.querySelector(`[role='columnheader'][data-col="${CSS.escape(move.id)}"] > slot`);
1671
- const name = header instanceof HTMLSlotElement ? header.assignedNodes({ flatten: true }).map((n) => n.textContent).join("").trim() : "";
1672
- announce(this.ownerDocument, `${name || move.id} moved to column ${String(view.colIndex)} of ${String(model.columns.length)}`);
1972
+ /** `aria-colindex` of a column's header, or of a group's first column on its side. */
1973
+ #colIndexOf(model, unit) {
1974
+ if (unit.kind === "column") return model.columns.find((c) => c.id === unit.id)?.colIndex;
1975
+ const key = unitKey(unit);
1976
+ return model.headerGroups.flat().find((c) => c.key === key)?.colIndex;
1673
1977
  }
1674
- /** A press on a reorderable header: becomes a drag once the pointer moves. */
1978
+ /** Tells screen readers where a column or group moved from the keyboard went. */
1979
+ #announceMoved(model, move) {
1980
+ const { unit } = move;
1981
+ const total = String(model.columns.length);
1982
+ if (unit.kind === "column") {
1983
+ const view = model.columns.find((c) => c.id === unit.id);
1984
+ if (!view || view.colIndex === move.from) return;
1985
+ const name = this.#slotText(`[role='columnheader'][data-col="${CSS.escape(unit.id)}"] > slot`);
1986
+ announce(this.ownerDocument, `${name || unit.id} moved to column ${String(view.colIndex)} of ${total}`);
1987
+ return;
1988
+ }
1989
+ const key = unitKey(unit);
1990
+ const cell = model.headerGroups.flat().find((c) => c.key === key);
1991
+ if (!cell || cell.colIndex === move.from) return;
1992
+ const name = this.#slotText(`[data-group-key="${CSS.escape(key)}"] slot`);
1993
+ const last = cell.colIndex + cell.span - 1;
1994
+ const where = cell.span === 1 ? `column ${String(cell.colIndex)}` : `columns ${String(cell.colIndex)} to ${String(last)}`;
1995
+ announce(this.ownerDocument, `${name || unit.id} moved to ${where} of ${total}`);
1996
+ }
1997
+ /** The text slotted into a header, read through the slot's assigned nodes. */
1998
+ #slotText(selector) {
1999
+ const slot = this.renderRoot.querySelector(selector);
2000
+ return slot instanceof HTMLSlotElement ? slot.assignedNodes({ flatten: true }).map((n) => n.textContent).join("").trim() : "";
2001
+ }
2002
+ /** A press on a reorderable column or group header: becomes a drag once the pointer moves. */
1675
2003
  #startReorder(down) {
1676
2004
  const cell = down.composedPath().find((t) => t instanceof HTMLElement && t.getAttribute("role") === "columnheader");
1677
- const columnId = cell?.dataset.moveCol;
1678
- if (!cell || columnId === void 0 || !canStartReorder(down, cell)) return;
1679
- const scroller = this.renderRoot.querySelector("[part~=\"table\"]");
1680
- if (!scroller) return;
1681
- const pin = this.#table.model.columns.find((c) => c.id === columnId)?.pin;
2005
+ if (!cell || !canStartReorder(down, cell)) return;
2006
+ const model = this.#table.model;
2007
+ let unit;
2008
+ if (cell.dataset.moveCol !== void 0) unit = {
2009
+ kind: "column",
2010
+ id: cell.dataset.moveCol
2011
+ };
2012
+ else {
2013
+ const key = cell.dataset.moveGroup;
2014
+ const group = model.headerGroups.flat().find((c) => c.key === key);
2015
+ if (group?.groupId === void 0) return;
2016
+ unit = {
2017
+ kind: "group",
2018
+ id: group.groupId,
2019
+ pin: group.pin
2020
+ };
2021
+ }
2022
+ const scroller = this.#scroller();
2023
+ const moving = model.siblingUnits(unit).find((s) => sameUnit(s.unit, unit));
2024
+ if (!scroller || !moving) return;
2025
+ const key = unitKey(unit);
2026
+ const columns = new Set(moving.columns);
2027
+ const pin = unit.kind === "group" ? unit.pin : model.columns.find((c) => c.id === unit.id)?.pin;
1682
2028
  startReorderDrag(cell, down, {
1683
2029
  scroller,
1684
- targetAt: (x) => dropTarget(this.#dropSlots(pin), columnId, x, this.#direction()),
2030
+ targetAt: (x) => dropTarget(this.#dropSlots(unit), key, x, this.#direction()),
1685
2031
  scrollArea: () => pin === void 0 ? this.#scrollArea(scroller) : void 0,
1686
2032
  onStart: () => {
1687
2033
  this.#drag = {
1688
- columnId,
2034
+ key,
2035
+ columns,
1689
2036
  target: void 0
1690
2037
  };
1691
2038
  this.requestUpdate();
1692
2039
  },
1693
2040
  onTarget: (target) => {
1694
2041
  this.#drag = {
1695
- columnId,
2042
+ key,
2043
+ columns,
1696
2044
  target
1697
2045
  };
1698
2046
  this.requestUpdate();
1699
2047
  },
1700
2048
  onDrop: (target) => {
1701
2049
  this.#drag = void 0;
1702
- if (target && this.#requestReorder(columnId, target.to)) {
2050
+ if (target && this.#requestReorder(unit, target.to)) {
1703
2051
  if (this.matches(":focus-within")) this.#focusPending = true;
1704
2052
  }
1705
2053
  this.requestUpdate();
@@ -1711,16 +2059,17 @@ var ArgusTable = class ArgusTable extends LitElement {
1711
2059
  });
1712
2060
  }
1713
2061
  /**
1714
- * The rendered headers of one pin group, with their positions in the whole
1715
- * order (which leaves out the checkbox column).
2062
+ * The rendered headers of what a column or group moves among (see
2063
+ * `siblingUnits`), with their positions there. With `virtualizeColumns`, columns
2064
+ * out of view have no header and no slot.
1716
2065
  */
1717
- #dropSlots(pin) {
2066
+ #dropSlots(unit) {
1718
2067
  const slots = [];
1719
- this.#table.model.columns.filter((c) => c.kind === "data").forEach((c, index) => {
1720
- if (c.pin !== pin) return;
1721
- const cell = this.renderRoot.querySelector(`[role='columnheader'][data-col="${CSS.escape(c.id)}"]`);
2068
+ this.#table.model.siblingUnits(unit).forEach((sibling, index) => {
2069
+ const id = unitKey(sibling.unit);
2070
+ const cell = this.renderRoot.querySelector(sibling.unit.kind === "column" ? `[role='columnheader'][data-col="${CSS.escape(sibling.unit.id)}"]` : `[data-group-key="${CSS.escape(id)}"]`);
1722
2071
  if (cell) slots.push({
1723
- id: c.id,
2072
+ id,
1724
2073
  index,
1725
2074
  rect: cell.getBoundingClientRect()
1726
2075
  });
@@ -1729,8 +2078,7 @@ var ArgusTable = class ArgusTable extends LitElement {
1729
2078
  }
1730
2079
  /** Where the scrolling columns show: the scroller's inside, less the pinned columns. */
1731
2080
  #scrollArea(scroller) {
1732
- const columns = this.#table.model.columns;
1733
- const width = (side) => columns.reduce((sum, c) => c.pin === side ? sum + c.width : sum, 0);
2081
+ const width = (side) => pinnedWidth(this.#table.model.columns, side);
1734
2082
  const ltr = this.#direction() === 1;
1735
2083
  const left = scroller.getBoundingClientRect().left + scroller.clientLeft;
1736
2084
  return {
@@ -1750,8 +2098,8 @@ var ArgusTable = class ArgusTable extends LitElement {
1750
2098
  #setSort(columnId, direction) {
1751
2099
  const view = this.#table.model.columns.find((c) => c.id === columnId);
1752
2100
  if (view?.kind !== "data" || !view.sortable) return;
1753
- if (direction === (view.sorted === false ? null : view.sorted)) return;
1754
- if (direction === null) this.#applySorting([]);
2101
+ if (direction === (view.sorted === false ? void 0 : view.sorted)) return;
2102
+ if (direction === void 0) this.#applySorting([]);
1755
2103
  else this.#applySorting([{
1756
2104
  id: columnId,
1757
2105
  desc: direction === "desc"
@@ -1764,7 +2112,7 @@ var ArgusTable = class ArgusTable extends LitElement {
1764
2112
  if (applied) {
1765
2113
  this.#appliedSort = value;
1766
2114
  this.requestUpdate("sorting", old);
1767
- } else fire(this, SORT_CHANGE, { sorting: value });
2115
+ } else fire(this, SORTING_CHANGE, { sorting: value });
1768
2116
  }
1769
2117
  /**
1770
2118
  * A data column as plugins see it. Header functions call this on every
@@ -1789,7 +2137,7 @@ var ArgusTable = class ArgusTable extends LitElement {
1789
2137
  width: view.width,
1790
2138
  sorted: view.sorted === false ? void 0 : view.sorted,
1791
2139
  filter: view.filter,
1792
- filterValue: this.#filters.value[id]
2140
+ columnFilter: this.#filters.value[id]
1793
2141
  };
1794
2142
  }
1795
2143
  /** The header or cell an event happened in, for plugins. */
@@ -1799,12 +2147,12 @@ var ArgusTable = class ArgusTable extends LitElement {
1799
2147
  const columnId = cell?.dataset.col;
1800
2148
  if (!cell || columnId === void 0) continue;
1801
2149
  const model = this.#table.model;
1802
- const row = cell.getAttribute("role") === "columnheader" ? HEADER : gridRowOf(cell.parentElement, layoutOf(model));
2150
+ const row = gridRowOfCell(cell, layoutOf(model));
1803
2151
  if (!row) return void 0;
1804
2152
  return {
1805
2153
  section: row.section,
1806
2154
  columnId,
1807
- rowIndex: row.index,
2155
+ rowIndex: row.section === "header" ? 0 : row.index,
1808
2156
  rowId: this.#gridRowId(model, row)
1809
2157
  };
1810
2158
  }
@@ -1823,6 +2171,10 @@ var ArgusTable = class ArgusTable extends LitElement {
1823
2171
  if (this.rowHeight !== "auto" || count === 0) return count * this.#rowPx;
1824
2172
  return Math.max(this.#rowMeasure.pinned[side], count * this.#rowPx);
1825
2173
  }
2174
+ /** The header rows and the pinned rows: the rows in the scroller besides the body. */
2175
+ #headerAndPinnedPx(layout) {
2176
+ return layout.header * this.#rowPx + this.#pinnedPx("top", layout) + this.#pinnedPx("bottom", layout);
2177
+ }
1826
2178
  /**
1827
2179
  * `rowHeight: 'auto'`: reads the heights of the rendered rows, after their
1828
2180
  * content has rendered. Returns whether any changed (an update is requested).
@@ -1843,6 +2195,7 @@ var ArgusTable = class ArgusTable extends LitElement {
1843
2195
  }
1844
2196
  };
1845
2197
  __decorate([property({ attribute: false })], ArgusTable.prototype, "columns", null);
2198
+ __decorate([property({ attribute: false })], ArgusTable.prototype, "columnGroups", null);
1846
2199
  __decorate([property({ attribute: false })], ArgusTable.prototype, "data", null);
1847
2200
  __decorate([property({ attribute: false })], ArgusTable.prototype, "dataSource", void 0);
1848
2201
  __decorate([property({ attribute: false })], ArgusTable.prototype, "pinnedRows", void 0);
@@ -1855,7 +2208,7 @@ __decorate([property({ reflect: true })], ArgusTable.prototype, "density", void
1855
2208
  __decorate([property({ type: Number })], ArgusTable.prototype, "overscan", null);
1856
2209
  __decorate([property({
1857
2210
  attribute: "virtualize-columns",
1858
- type: Boolean
2211
+ converter: switchConverter
1859
2212
  })], ArgusTable.prototype, "virtualizeColumns", void 0);
1860
2213
  __decorate([property({ attribute: false })], ArgusTable.prototype, "sorting", null);
1861
2214
  __decorate([property({ attribute: false })], ArgusTable.prototype, "columnSizing", null);
@@ -1867,7 +2220,7 @@ __decorate([property({ attribute: "quick-filter" })], ArgusTable.prototype, "qui
1867
2220
  __decorate([property({ attribute: "selection-mode" })], ArgusTable.prototype, "selectionMode", void 0);
1868
2221
  __decorate([property({
1869
2222
  attribute: "selection-checkboxes",
1870
- type: Boolean
2223
+ converter: switchConverter
1871
2224
  })], ArgusTable.prototype, "selectionCheckboxes", void 0);
1872
2225
  __decorate([property({ attribute: false })], ArgusTable.prototype, "selection", null);
1873
2226
  __decorate([property({ attribute: false })], ArgusTable.prototype, "contentRenderer", void 0);