argus-table 0.2.2 → 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 (83) hide show
  1. package/README.md +1 -1
  2. package/dist/argus-column-group.js +66 -0
  3. package/dist/argus-column-group.js.map +1 -0
  4. package/dist/argus-column.js +28 -26
  5. package/dist/argus-column.js.map +1 -1
  6. package/dist/argus-table.js +1497 -285
  7. package/dist/argus-table.js.map +1 -1
  8. package/dist/attributes.js +21 -0
  9. package/dist/attributes.js.map +1 -0
  10. package/dist/cdn/argus-table.js +120 -61
  11. package/dist/cdn/argus-table.js.map +1 -1
  12. package/dist/column-registry.js +23 -0
  13. package/dist/column-registry.js.map +1 -0
  14. package/dist/controllers/auto-width.js +16 -4
  15. package/dist/controllers/auto-width.js.map +1 -1
  16. package/dist/controllers/column-children.js +32 -9
  17. package/dist/controllers/column-children.js.map +1 -1
  18. package/dist/controllers/column-window.js +135 -0
  19. package/dist/controllers/column-window.js.map +1 -0
  20. package/dist/controllers/data-controller.js +9 -4
  21. package/dist/controllers/data-controller.js.map +1 -1
  22. package/dist/controllers/plugins.js +121 -0
  23. package/dist/controllers/plugins.js.map +1 -0
  24. package/dist/controllers/reorder-drag.js +19 -17
  25. package/dist/controllers/reorder-drag.js.map +1 -1
  26. package/dist/controllers/row-measure.js +122 -0
  27. package/dist/controllers/row-measure.js.map +1 -0
  28. package/dist/controllers/virtual-controller.js +315 -21
  29. package/dist/controllers/virtual-controller.js.map +1 -1
  30. package/dist/core/src/columns/column-groups.js +129 -0
  31. package/dist/core/src/columns/column-groups.js.map +1 -0
  32. package/dist/core/src/columns/column-order.js +125 -27
  33. package/dist/core/src/columns/column-order.js.map +1 -1
  34. package/dist/core/src/columns/resolve-columns.js +18 -5
  35. package/dist/core/src/columns/resolve-columns.js.map +1 -1
  36. package/dist/core/src/columns/spread-width.js +43 -0
  37. package/dist/core/src/columns/spread-width.js.map +1 -0
  38. package/dist/core/src/data/page-cache.js +4 -0
  39. package/dist/core/src/data/page-cache.js.map +1 -1
  40. package/dist/core/src/filters/filter.js +183 -0
  41. package/dist/core/src/filters/filter.js.map +1 -0
  42. package/dist/core/src/grid/row-layout.js +19 -11
  43. package/dist/core/src/grid/row-layout.js.map +1 -1
  44. package/dist/core/src/state/selection.js +71 -0
  45. package/dist/core/src/state/selection.js.map +1 -0
  46. package/dist/core/src/table/create-table.js +276 -66
  47. package/dist/core/src/table/create-table.js.map +1 -1
  48. package/dist/core/src/table/filtered-row-model.js +48 -0
  49. package/dist/core/src/table/filtered-row-model.js.map +1 -0
  50. package/dist/core/src/table/sorted-row-model.js +13 -2
  51. package/dist/core/src/table/sorted-row-model.js.map +1 -1
  52. package/dist/core/src/virtual/column-window.js +72 -0
  53. package/dist/core/src/virtual/column-window.js.map +1 -0
  54. package/dist/core/src/virtual/row-pool.js +2 -1
  55. package/dist/core/src/virtual/row-pool.js.map +1 -1
  56. package/dist/core/src/virtual/row-sizes.js +152 -0
  57. package/dist/core/src/virtual/row-sizes.js.map +1 -0
  58. package/dist/core/src/virtual/scroll-scale.js +41 -3
  59. package/dist/core/src/virtual/scroll-scale.js.map +1 -1
  60. package/dist/custom-elements.json +1683 -211
  61. package/dist/events.js +16 -8
  62. package/dist/events.js.map +1 -1
  63. package/dist/index.d.ts +808 -127
  64. package/dist/index.js +4 -2
  65. package/dist/interactive.js +22 -0
  66. package/dist/interactive.js.map +1 -0
  67. package/dist/max-height.js +22 -8
  68. package/dist/max-height.js.map +1 -1
  69. package/dist/render/body.js +48 -8
  70. package/dist/render/body.js.map +1 -1
  71. package/dist/render/cells.js +22 -11
  72. package/dist/render/cells.js.map +1 -1
  73. package/dist/render/content-directive.js +115 -0
  74. package/dist/render/content-directive.js.map +1 -0
  75. package/dist/render/content.js +33 -10
  76. package/dist/render/content.js.map +1 -1
  77. package/dist/render/header.js +104 -22
  78. package/dist/render/header.js.map +1 -1
  79. package/dist/render/pinned-rows.js +1 -1
  80. package/dist/render/pinned-rows.js.map +1 -1
  81. package/dist/styles/base.js +1 -1
  82. package/dist/styles/base.js.map +1 -1
  83. package/package.json +1 -1
@@ -1,21 +1,32 @@
1
+ import { clickSelection, sameSelection, selectAll, selectRange, toggleRow } from "./core/src/state/selection.js";
1
2
  import { controllable } from "./core/src/state/controllable.js";
3
+ import { groupKey } from "./core/src/columns/column-groups.js";
2
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";
7
+ import "./core/src/table/create-table.js";
3
8
  import { announce } from "./announce.js";
4
9
  import { AutoWidths, measureColumns } from "./controllers/auto-width.js";
5
10
  import { ColumnChildrenController } from "./controllers/column-children.js";
11
+ import { ColumnWindowController } from "./controllers/column-window.js";
6
12
  import { DataController } from "./controllers/data-controller.js";
7
13
  import { PinScrollController } from "./controllers/pin-scroll-controller.js";
14
+ import { PluginController, sameItems } from "./controllers/plugins.js";
15
+ import { RowMeasure } from "./controllers/row-measure.js";
8
16
  import { canStartReorder, dropTarget, startReorderDrag } from "./controllers/reorder-drag.js";
9
17
  import { startResizeDrag } from "./controllers/resize-drag.js";
10
18
  import { TableController } from "./controllers/table-controller.js";
11
19
  import { VirtualController } from "./controllers/virtual-controller.js";
12
- import { CELL_CLICK, COLUMN_REORDER, COLUMN_RESIZE, LOAD_ERROR, RANGE_CHANGE, ROW_CLICK, 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";
21
+ import { fromInteractive } from "./interactive.js";
13
22
  import { bodyCap, watchMaxElementHeight } from "./max-height.js";
14
23
  import { renderBody } from "./render/body.js";
15
24
  import { renderLightDom } from "./render/cells.js";
16
- import { renderHeader } from "./render/header.js";
25
+ import { content, flushContent } from "./render/content-directive.js";
26
+ import { columnKey, renderHeader } from "./render/header.js";
17
27
  import { renderPinnedRows } from "./render/pinned-rows.js";
18
28
  import { baseStyles } from "./styles/base.js";
29
+ import { switchConverter } from "./attributes.js";
19
30
  import __decorate from "./_virtual/_@oxc-project_runtime@0.156.0/helpers/esm/decorate.js";
20
31
  import { LitElement, html, nothing } from "lit";
21
32
  import { customElement, property } from "lit/decorators.js";
@@ -34,24 +45,34 @@ function attachGridInternals(host) {
34
45
  }
35
46
  /**
36
47
  * The sortable column a header click came from, if any. Works for slotted content
37
- * too. Clicks on a resize handle never sort, so a click that ends a drag doesn't.
48
+ * too. Clicks on a resize handle never sort, so a click that ends a drag doesn't,
49
+ * and neither do clicks on interactive content in the header (a button, an input).
38
50
  */
39
51
  function sortColumnOf(event) {
40
52
  for (const target of event.composedPath()) {
41
53
  if (!(target instanceof HTMLElement)) continue;
42
54
  if (target.dataset.resizeCol !== void 0) return void 0;
55
+ if (target.matches("button,input,select,textarea,a[href],label,[contenteditable],[popover],dialog")) return void 0;
43
56
  if (target.dataset.sortCol !== void 0) return target.dataset.sortCol;
44
- if (target.getAttribute("part") === "header-row") return void 0;
57
+ if (target.getAttribute("role") === "row") return void 0;
45
58
  }
46
59
  }
47
60
  var RESIZE_STEP = 10;
48
- var HEADER = {
49
- section: "header",
50
- index: 0
51
- };
52
- /** Where the rows sit in the grid: header, pinned top, body, pinned bottom. */
61
+ /** `row-height`: px (`48` or `48px`) or `auto`; anything else is unset. */
62
+ var rowHeightConverter = { fromAttribute(value) {
63
+ if (value === null) return void 0;
64
+ const text = value.trim().toLowerCase();
65
+ if (text === "auto") return "auto";
66
+ const px = Number(text.replace(/px$/, ""));
67
+ return text !== "" && px > 0 ? px : void 0;
68
+ } };
69
+ /** Where the rows sit in the grid: header rows, pinned top, body, pinned bottom. */
53
70
  function layoutOf(model) {
54
- 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);
55
76
  }
56
77
  /** The grid cell or column header an element is, if any. */
57
78
  function gridCell(target) {
@@ -59,18 +80,26 @@ function gridCell(target) {
59
80
  const role = target.getAttribute("role");
60
81
  return role === "gridcell" || role === "columnheader" ? target : void 0;
61
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
+ }
62
87
  /** The grid row a rendered row element is, from its `aria-rowindex`. */
63
88
  function gridRowOf(row, layout) {
64
89
  const index = row?.getAttribute("aria-rowindex");
65
90
  return index ? layout.fromAriaRowIndex(Number(index)) : void 0;
66
91
  }
92
+ /** The grid row a cell or column header is in. */
93
+ function gridRowOfCell(cell, layout) {
94
+ return gridRowOf(cell.parentElement, layout);
95
+ }
67
96
  /**
68
97
  * The pinned edge's shadow, drawn once per side over the scroller rather than
69
98
  * per row: a shadow on every pinned cell repaints on every scroll frame. It
70
99
  * covers the pinned columns and casts `--argus-pinned-shadow` onto the rest;
71
100
  * PinScrollController sizes it to the visible area.
72
101
  */
73
- function pinShadow(side, width, rows, rowPx) {
102
+ function pinShadow(side, width, contentPx) {
74
103
  if (width === 0) return nothing;
75
104
  return html`<div
76
105
  class="pin-shadow"
@@ -78,7 +107,7 @@ function pinShadow(side, width, rows, rowPx) {
78
107
  aria-hidden="true"
79
108
  style=${styleMap({
80
109
  inlineSize: `${String(width)}px`,
81
- "--_content-h": `${String((rows + 1) * rowPx)}px`
110
+ "--_content-h": `${String(contentPx)}px`
82
111
  })}
83
112
  ></div>`;
84
113
  }
@@ -86,18 +115,308 @@ function pinShadow(side, width, rows, rowPx) {
86
115
  function track(column) {
87
116
  return column.flex === void 0 ? `${String(column.width)}px` : `minmax(${String(column.minWidth)}px, ${String(column.flex)}fr)`;
88
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
+ }
89
122
  var ArgusTable = class ArgusTable extends LitElement {
90
- constructor(..._args) {
91
- super(..._args);
123
+ static {
124
+ this.styles = baseStyles;
125
+ }
126
+ /**
127
+ * Column definitions. Property only: functions can't be attributes. While empty,
128
+ * the table reads its `<argus-column>` children instead. `null` or `undefined`
129
+ * means `[]`.
130
+ */
131
+ get columns() {
132
+ return this.#columnsProp;
133
+ }
134
+ set columns(value) {
135
+ this.#columnsProp = value ?? [];
136
+ }
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
+ /**
153
+ * The rows to show. Replace the array to update; changes inside it aren't
154
+ * detected. `null` or `undefined` means `[]`.
155
+ */
156
+ get data() {
157
+ return this.#dataProp;
158
+ }
159
+ set data(value) {
160
+ this.#dataProp = value ?? [];
161
+ }
162
+ /** Rows rendered above and below the visible ones. `null` or `undefined` means the default, 6. */
163
+ get overscan() {
164
+ return this.#overscan;
165
+ }
166
+ set overscan(value) {
167
+ this.#overscan = value ?? 6;
168
+ }
169
+ /**
170
+ * The sort order, e.g. `[{ id: 'total', desc: true }]`; `[]` when unsorted. Unset,
171
+ * the table keeps it itself and header clicks change it. Once set, it is
172
+ * controlled: a click only fires `argus-sorting-change` with the proposed order,
173
+ * and nothing changes until you set the property. Set `undefined` to hand it back.
174
+ */
175
+ get sorting() {
176
+ return this.#sorting.value;
177
+ }
178
+ set sorting(value) {
179
+ this.#sorting.set(value);
180
+ }
181
+ /**
182
+ * Resized widths in px by column id, e.g. `{ name: 240 }`. A resized flex column
183
+ * becomes fixed. Uncontrolled until set, like `sorting`: once set, a resize only
184
+ * fires `argus-column-sizing-change` with the proposed widths. `undefined` hands it back.
185
+ */
186
+ get columnSizing() {
187
+ return this.#sizing.value;
188
+ }
189
+ set columnSizing(value) {
190
+ this.#sizing.set(value);
191
+ }
192
+ /**
193
+ * The column order: column ids as the table shows them, e.g.
194
+ * `['id', 'total', 'customer']`. `[]` (the default) keeps the order of `columns`.
195
+ * Each pin group (start-pinned, scrolling, end-pinned) takes its columns in this
196
+ * order; unknown ids are ignored and columns it doesn't list follow the listed
197
+ * ones of their group. After a move it holds every column id. Uncontrolled
198
+ * until set, like `sorting`: once set, a move only fires `argus-column-order-change`
199
+ * with the proposed order. `undefined` hands it back.
200
+ */
201
+ get columnOrder() {
202
+ return this.#order.value;
203
+ }
204
+ set columnOrder(value) {
205
+ this.#order.set(value);
206
+ }
207
+ /**
208
+ * Pins by column id, over the columns' own `pin`: `'start'` or `'end'` pins a
209
+ * column, `false` unpins one whose definition pins it, e.g.
210
+ * `{ total: 'end', id: false }`. Columns it doesn't list keep their `pin`. A pinned
211
+ * column joins the edge of its group nearest its place in `columnOrder`, and stops
212
+ * flexing. Uncontrolled until set, like `sorting`: once set, pinning from a plugin
213
+ * only fires `argus-column-pinning-change` with the proposed pins. `undefined` hands it back.
214
+ */
215
+ get columnPinning() {
216
+ return this.#pinning.value;
217
+ }
218
+ set columnPinning(value) {
219
+ this.#pinning.set(value);
220
+ }
221
+ /**
222
+ * Visibility by column id, over the columns' own `hidden`: `false` hides a column,
223
+ * `true` shows one defined as `hidden`, e.g. `{ email: false }`. Columns it
224
+ * doesn't list keep their `hidden`. A hidden column isn't rendered, but sorting by
225
+ * it still works and it keeps its place in `columnOrder`. Uncontrolled until set,
226
+ * like `sorting`: once set, hiding or showing from a plugin only fires
227
+ * `argus-column-visibility-change` with the proposed visibility. `undefined` hands it back.
228
+ */
229
+ get columnVisibility() {
230
+ return this.#visibility.value;
231
+ }
232
+ set columnVisibility(value) {
233
+ this.#visibility.set(value);
234
+ }
235
+ /**
236
+ * Filters by column id, e.g. `{ city: { kind: 'set', values: ['Berlin'] } }` or
237
+ * `{ total: { kind: 'number', op: 'gt', value: 100 } }`. A row shows when it passes
238
+ * every filter and `quickFilter`. Filters on hidden columns still apply; unknown ids
239
+ * are ignored. A column's `filterFn` replaces the built-in matching. With a
240
+ * `dataSource`, the filters go to `getRows()` and the server filters. Users set
241
+ * filters through plugins (the column menu) on columns with a `filter` kind.
242
+ * Uncontrolled until set, like `sorting`: once set, a change from a plugin only
243
+ * fires `argus-column-filters-change` with the proposed filters. `undefined` hands it back.
244
+ */
245
+ get columnFilters() {
246
+ return this.#filters.value;
247
+ }
248
+ set columnFilters(value) {
249
+ this.#filters.set(value);
250
+ }
251
+ /**
252
+ * Shows only rows that contain each word of this text in one of their shown
253
+ * columns, ignoring case, e.g. from a search box. Values are matched as text
254
+ * (dates as `2026-11-10`); objects aren't searched. With a `dataSource`, it goes
255
+ * to `getRows()` and the server filters. `''` (the default) shows every row.
256
+ */
257
+ get quickFilter() {
258
+ return this.#quickFilter;
259
+ }
260
+ set quickFilter(value) {
261
+ this.#quickFilter = value ?? "";
262
+ }
263
+ /**
264
+ * The selected rows, as row ids (from `getRowId`, or the row's index as a
265
+ * string), e.g. `['p4', 'p7']`. Uncontrolled until set, like `sorting`: once
266
+ * set, a click only fires `argus-selection-change` with the proposed ids.
267
+ * `undefined` hands it back. The table never drops ids on its own: they stay
268
+ * through sorting, a new `data` or reloaded pages. With a `dataSource`,
269
+ * Ctrl/Cmd+A selects the loaded rows only.
270
+ */
271
+ get selection() {
272
+ return this.#selection.value;
273
+ }
274
+ set selection(value) {
275
+ this.#selection.set(value);
276
+ }
277
+ /**
278
+ * Add-ons, such as `headerMenu()` from `@argus-table/header-menu`. Each one can
279
+ * change the column definitions, handle keys on a focused header or cell, and
280
+ * use the table through a small API. They run in array order. Replace the
281
+ * array to change them; an array with the same plugins in the same order
282
+ * changes nothing. `null` or `undefined` means `[]`.
283
+ */
284
+ get plugins() {
285
+ return this.#pluginsProp;
286
+ }
287
+ set plugins(value) {
288
+ const next = value ?? [];
289
+ if (!sameItems(next, this.#pluginsProp)) this.#pluginsProp = next;
290
+ }
291
+ #columnsProp;
292
+ #columnGroupsProp;
293
+ /** The groups last passed to the model: `columnGroups`, or the `<argus-column-group>` children. */
294
+ #groups;
295
+ #groupsChanged;
296
+ #pluginsProp;
297
+ #dataProp;
298
+ #overscan;
299
+ #sorting;
300
+ #sizing;
301
+ #order;
302
+ #pinning;
303
+ #visibility;
304
+ #filters;
305
+ #quickFilter;
306
+ /** Filters the table applied itself, by column; their events fire once the update is done. */
307
+ #appliedFilters;
308
+ /** Announce the row count after the update: the filters changed. */
309
+ #announceRows;
310
+ /** Pins the table applied itself, by column; their events fire once the update is done. */
311
+ #appliedPins;
312
+ /** Hidden or shown columns the table applied itself; their events fire once the update is done. */
313
+ #appliedHides;
314
+ #selection;
315
+ /** `selection` as a set, for the rows rendered; rebuilt when `selection` changes. */
316
+ #selectedIds;
317
+ /** The row a Shift range starts from: the last row clicked or toggled without Shift. */
318
+ #anchor;
319
+ /** A selection the table applied itself; its event fires once the update is done. */
320
+ #appliedSelection;
321
+ /** Whether the checkbox column was last shown; a change re-measures the pinned columns. */
322
+ #selectionColumn;
323
+ #selectionColumnChanged;
324
+ /** The select-all checkbox's state, kept until the selection or the shown rows change. */
325
+ #checkAll;
326
+ /** Moves the table applied itself, by column or group; their events fire once the update is done. */
327
+ #appliedReorders;
328
+ /** The header being dragged to a new position, if any. */
329
+ #drag;
330
+ /** A move from the keyboard, announced once it has rendered: what moved and its old position. */
331
+ #announceMove;
332
+ /** Resizes the table applied itself, by column; their events fire once the update is done. */
333
+ #appliedResizes;
334
+ /** Columns `autosizeColumns()` renders out of view for its measurement. */
335
+ #measureKeep;
336
+ /** Measured widths of `width: 'auto'` columns. */
337
+ #autoWidths;
338
+ /** The auto widths last passed to the model; a change re-measures the pinned columns' offsets. */
339
+ #pushedAutoWidths;
340
+ #autoWidthsChanged;
341
+ /** A sort the table applied itself; its event fires once the update is done. */
342
+ #appliedSort;
343
+ #internals;
344
+ #table;
345
+ #children;
346
+ /** What plugins get: closures over the table's private state. */
347
+ #pluginHost;
348
+ #plugins;
349
+ #columnsById;
350
+ /** The columns last passed to the model: `columns`, or the `<argus-column>` children. */
351
+ #columns;
352
+ #columnsChanged;
353
+ #warnedColumns;
354
+ /** The header row of column headers at the last reconcile (`#reconcileActive`). */
355
+ #leafLevel;
356
+ /** End of the light-DOM part holding cell content, so later children stay outside it. */
357
+ #contentEnd;
358
+ /** The light-DOM part: disconnected with the table, so foreign content is released. */
359
+ #lightPart;
360
+ #lightConnected;
361
+ #data;
362
+ /** Scroll back to the top after the update (a new sort order for a data source). */
363
+ #scrollToTop;
364
+ #virtual;
365
+ #pinScroll;
366
+ #columnWindow;
367
+ /** The columns the last render rendered (all of them, unless `virtualizeColumns`). */
368
+ #rendered;
369
+ #rowPx;
370
+ #probeObserver;
371
+ /** The column template and row height the rows were last read for. */
372
+ #rowsKey;
373
+ /** Rows of different heights: reads the rendered rows (`rowHeight: 'auto'` only). */
374
+ #rowMeasure;
375
+ #window;
376
+ #active;
377
+ /** Move focus to the active cell after the next update (keyboard navigation). */
378
+ #focusPending;
379
+ #lastRange;
380
+ #unwatchMaxHeight;
381
+ /**
382
+ * The row `setState()` still has to scroll to: a data source's rows that far
383
+ * aren't counted yet. Scrolled to again whenever the row count changes.
384
+ */
385
+ #scrollTarget;
386
+ /** The scroll container (`part="table"`); `null` before the first render. */
387
+ #scroller() {
388
+ return this.renderRoot.querySelector("[part~=\"table\"]");
389
+ }
390
+ constructor() {
391
+ super();
92
392
  this.density = "comfortable";
393
+ this.virtualizeColumns = false;
394
+ this.selectionMode = "none";
395
+ this.selectionCheckboxes = false;
93
396
  this.#columnsProp = [];
397
+ this.#columnGroupsProp = [];
398
+ this.#groups = [];
399
+ this.#groupsChanged = false;
400
+ this.#pluginsProp = [];
94
401
  this.#dataProp = [];
95
402
  this.#overscan = 6;
96
403
  this.#sorting = controllable([]);
97
404
  this.#sizing = controllable({});
98
405
  this.#order = controllable([]);
406
+ this.#pinning = controllable({});
407
+ this.#visibility = controllable({});
408
+ this.#filters = controllable({});
409
+ this.#quickFilter = "";
410
+ this.#appliedFilters = /* @__PURE__ */ new Map();
411
+ this.#announceRows = false;
412
+ this.#appliedPins = /* @__PURE__ */ new Map();
413
+ this.#appliedHides = /* @__PURE__ */ new Map();
414
+ this.#selection = controllable([]);
415
+ this.#selectionColumn = false;
416
+ this.#selectionColumnChanged = false;
99
417
  this.#appliedReorders = /* @__PURE__ */ new Map();
100
418
  this.#appliedResizes = /* @__PURE__ */ new Map();
419
+ this.#measureKeep = [];
101
420
  this.#autoWidths = new AutoWidths(() => {
102
421
  this.requestUpdate();
103
422
  });
@@ -105,14 +424,67 @@ var ArgusTable = class ArgusTable extends LitElement {
105
424
  this.#internals = attachGridInternals(this);
106
425
  this.#table = new TableController();
107
426
  this.#children = new ColumnChildrenController(this);
427
+ this.#pluginHost = {
428
+ element: this,
429
+ column: (id) => this.#columnInfo(id),
430
+ columns: () => this.#table.model.allColumns.flatMap((c) => {
431
+ const info = this.#columnInfo(c.id);
432
+ return info ? [info] : [];
433
+ }),
434
+ setPin: (columnId, pin) => {
435
+ this.#requestPin(columnId, pin);
436
+ },
437
+ setHidden: (columnId, hidden) => {
438
+ this.#requestHide(columnId, hidden);
439
+ },
440
+ setFilter: (columnId, filter) => {
441
+ this.#requestFilter(columnId, filter);
442
+ },
443
+ filterValues: (columnId, limit) => this.#table.model.filterValues(columnId, limit),
444
+ filteredIndexes: (except) => this.#table.model.indexesPassing(except),
445
+ cellOf: (event) => this.#cellTarget(event),
446
+ setSort: (columnId, direction) => {
447
+ this.#setSort(columnId, direction);
448
+ },
449
+ focusHeader: (columnId) => {
450
+ const model = this.#table.model;
451
+ this.#moveTo(layoutOf(model).leafHeader, columnId, model);
452
+ },
453
+ announce: (message) => {
454
+ announce(this.ownerDocument, message);
455
+ },
456
+ invalidateColumns: () => {
457
+ this.#plugins.invalidate();
458
+ this.requestUpdate();
459
+ },
460
+ requestUpdate: () => {
461
+ this.requestUpdate();
462
+ },
463
+ row: (index) => this.#table.model.rowAt(index)?.original,
464
+ content: (value) => content(value)
465
+ };
466
+ this.#plugins = new PluginController(this, this.#pluginHost);
108
467
  this.#columns = [];
109
468
  this.#columnsChanged = false;
110
469
  this.#warnedColumns = false;
470
+ this.#leafLevel = 0;
471
+ this.#lightConnected = true;
111
472
  this.#data = new DataController(this);
112
473
  this.#scrollToTop = false;
113
- this.#virtual = new VirtualController(this, () => this.renderRoot.querySelector("[part~=\"table\"]"));
114
- this.#pinScroll = new PinScrollController(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());
477
+ this.#rendered = [];
115
478
  this.#rowPx = 40;
479
+ this.#rowsKey = "";
480
+ this.#rowMeasure = new RowMeasure(this, {
481
+ bodyIndex: (row) => {
482
+ const at = gridRowOf(row, layoutOf(this.#table.model));
483
+ return at?.section === "body" && this.#data.stateAt(at.index) === "loaded" ? at.index : void 0;
484
+ },
485
+ sizeOf: (index) => this.#virtual.sizeOf(index),
486
+ minimum: () => this.#rowPx
487
+ });
116
488
  this.#window = {
117
489
  rows: [],
118
490
  totalSize: 0,
@@ -120,6 +492,9 @@ var ArgusTable = class ArgusTable extends LitElement {
120
492
  };
121
493
  this.#focusPending = false;
122
494
  this.#lastRange = "";
495
+ this.#dropScrollTarget = () => {
496
+ this.#scrollTarget = void 0;
497
+ };
123
498
  this.#onRetry = (event) => {
124
499
  const button = event.currentTarget;
125
500
  if (button.matches(":focus")) button.parentElement?.focus({ preventScroll: true });
@@ -134,6 +509,15 @@ var ArgusTable = class ArgusTable extends LitElement {
134
509
  if (event.button !== 0) return;
135
510
  const handle = event.composedPath()[0];
136
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
+ }
137
521
  const id = handle.dataset.resizeCol;
138
522
  const cell = handle.parentElement;
139
523
  if (id !== void 0 && cell) {
@@ -147,15 +531,23 @@ var ArgusTable = class ArgusTable extends LitElement {
147
531
  /** A double-click on a resize handle fits the column to its rendered content. */
148
532
  dblclick: (event) => {
149
533
  const handle = event.composedPath()[0];
150
- 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;
151
541
  if (id === void 0) return;
152
542
  this.#requestResizes(measureColumns(this.renderRoot, [id]));
153
543
  }
154
544
  };
155
545
  this.#onKeydown = (event) => {
156
- 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;
157
549
  const id = cell?.dataset.col;
158
- if (!cell || id === void 0) return;
550
+ if (!cell || id === void 0 && !group) return;
159
551
  const scroller = event.currentTarget;
160
552
  if (event.key === "Tab" && event.shiftKey) {
161
553
  scroller.tabIndex = -1;
@@ -164,19 +556,22 @@ var ArgusTable = class ArgusTable extends LitElement {
164
556
  }, { once: true });
165
557
  return;
166
558
  }
167
- const inHeader = cell.getAttribute("role") === "columnheader";
168
- const { key } = event;
169
- const arrow = key === "ArrowLeft" || key === "ArrowRight";
170
- if (inHeader && arrow && event.altKey && event.shiftKey) {
171
- event.preventDefault();
172
- const towardEnd = key === "ArrowRight" === (this.#direction() === 1);
173
- this.#moveByKey(id, towardEnd ? 1 : -1);
559
+ if (group || id === void 0) {
560
+ this.#groupKey(event, cell, scroller);
174
561
  return;
175
562
  }
176
- if (inHeader && event.altKey && arrow) {
563
+ const pluginTarget = id === "--argus-selection" ? void 0 : this.#cellTarget(event);
564
+ if (pluginTarget && this.#plugins.keydown(event, pluginTarget)) return;
565
+ const inHeader = cell.getAttribute("role") === "columnheader";
566
+ const { key } = event;
567
+ if (inHeader && (key === "ArrowLeft" || key === "ArrowRight") && event.altKey) {
177
568
  event.preventDefault();
178
569
  const towardEnd = key === "ArrowRight" === (this.#direction() === 1);
179
- 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));
180
575
  return;
181
576
  }
182
577
  if (inHeader && (key === "Enter" || key === " ") && cell.dataset.sortCol !== void 0) {
@@ -184,11 +579,12 @@ var ArgusTable = class ArgusTable extends LitElement {
184
579
  this.#requestSort(id);
185
580
  return;
186
581
  }
582
+ if (this.#selectionKey(event, cell, id, inHeader, scroller)) return;
187
583
  const corner = event.ctrlKey || event.metaKey;
188
584
  if (event.altKey || event.shiftKey || corner && key !== "Home" && key !== "End") return;
189
585
  const model = this.#table.model;
190
586
  const layout = layoutOf(model);
191
- const from = inHeader ? HEADER : gridRowOf(cell.parentElement, layout);
587
+ const from = gridRowOfCell(cell, layout);
192
588
  if (!from) return;
193
589
  if (!inHeader && key === "Enter") {
194
590
  event.preventDefault();
@@ -199,26 +595,13 @@ var ArgusTable = class ArgusTable extends LitElement {
199
595
  const columns = model.columns;
200
596
  let pos = layout.position(from);
201
597
  const last = layout.total - 1;
202
- const bodyFirst = layout.position({
203
- section: "body",
204
- index: 0
205
- });
206
- const bodyLast = bodyFirst + layout.body - 1;
207
- const fit = Math.floor(scroller.clientHeight / this.#rowPx + .01);
208
- const page = Math.max(1, fit - 1 - layout.top - layout.bottom);
209
598
  let col = columns.findIndex((c) => c.id === id);
210
599
  switch (key) {
211
600
  case "PageDown":
212
- if (layout.body > 0 && pos <= bodyLast) pos = Math.min(Math.max(pos, bodyFirst - 1) + page, bodyLast);
213
- break;
214
601
  case "PageUp":
215
- if (layout.body > 0 && pos > bodyFirst) pos = Math.max(Math.min(pos, bodyLast + 1) - page, bodyFirst);
216
- break;
217
602
  case "ArrowDown":
218
- pos = Math.min(pos + 1, last);
219
- break;
220
603
  case "ArrowUp":
221
- pos = Math.max(pos - 1, 0);
604
+ pos = this.#verticalStep(key, pos, layout, scroller);
222
605
  break;
223
606
  case "ArrowRight":
224
607
  col += this.#direction();
@@ -229,7 +612,7 @@ var ArgusTable = class ArgusTable extends LitElement {
229
612
  case "Home":
230
613
  col = 0;
231
614
  if (corner) {
232
- pos = 0;
615
+ pos = this.#headerLevelDown(model, 0, columns[0]?.id ?? id);
233
616
  this.#virtual.scrollToEdge("start");
234
617
  }
235
618
  break;
@@ -240,11 +623,20 @@ var ArgusTable = class ArgusTable extends LitElement {
240
623
  default: return;
241
624
  }
242
625
  event.preventDefault();
243
- const row = layout.at(pos) ?? HEADER;
244
- if (key.startsWith("Page") && from.section === "body" && row.section === "body") this.#virtual.scrollBy((row.index - from.index) * this.#rowPx);
245
- if (corner && key === "End" && row.section === "bottom") this.#virtual.scrollToEdge("end");
626
+ let row = layout.at(pos) ?? layout.leafHeader;
246
627
  const target = columns[Math.min(Math.max(col, 0), columns.length - 1)];
247
- if (target) this.#moveTo(row, target.id, model);
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
+ }
637
+ if (key.startsWith("Page") && from.section === "body" && row.section === "body") this.#virtual.scrollBy(this.#virtual.startOf(row.index) - this.#virtual.startOf(from.index));
638
+ if (corner && key === "End" && row.section === "bottom") this.#virtual.scrollToEdge("end");
639
+ this.#moveTo(row, target.id, model);
248
640
  };
249
641
  this.#onFocusin = (event) => {
250
642
  const scroller = event.currentTarget;
@@ -257,152 +649,99 @@ var ArgusTable = class ArgusTable extends LitElement {
257
649
  return;
258
650
  }
259
651
  for (const target of event.composedPath()) {
260
- const cell = gridCell(target);
261
- if (cell?.dataset.col !== void 0) {
262
- const model = this.#table.model;
263
- const row = cell.getAttribute("role") === "columnheader" ? HEADER : gridRowOf(cell.parentElement, layoutOf(model));
264
- const { col } = cell.dataset;
265
- const prev = this.#active;
266
- if (row && (row.section !== prev?.section || row.index !== prev.index || col !== prev.col)) {
267
- this.#active = {
268
- ...row,
269
- rowId: this.#gridRowId(model, row),
270
- col
271
- };
272
- this.requestUpdate();
273
- }
652
+ const group = groupCell(target);
653
+ if (group) {
654
+ this.#focusGroup(group);
274
655
  return;
275
656
  }
276
- if (target === event.currentTarget) return;
277
- }
278
- };
279
- this.#onClick = (event) => {
280
- this.#activate(event.composedPath());
281
- };
282
- }
283
- static {
284
- this.styles = baseStyles;
285
- }
286
- /**
287
- * Column definitions. Property only: functions can't be attributes. While empty,
288
- * the table reads its `<argus-column>` children instead. `null` or `undefined`
289
- * means `[]`.
290
- */
291
- get columns() {
292
- return this.#columnsProp;
293
- }
294
- set columns(value) {
295
- this.#columnsProp = value ?? [];
296
- }
297
- /**
298
- * The rows to show. Replace the array to update; changes inside it aren't
299
- * detected. `null` or `undefined` means `[]`.
300
- */
301
- get data() {
302
- return this.#dataProp;
303
- }
304
- set data(value) {
305
- this.#dataProp = value ?? [];
306
- }
307
- /** Rows rendered above and below the visible ones. `null` or `undefined` means the default, 6. */
308
- get overscan() {
309
- return this.#overscan;
310
- }
311
- set overscan(value) {
312
- this.#overscan = value ?? 6;
313
- }
314
- /**
315
- * The sort order, e.g. `[{ id: 'total', desc: true }]`; `[]` when unsorted. Unset,
316
- * the table keeps it itself and header clicks change it. Once set, it is
317
- * controlled: a click only fires `argus-sort-change` with the proposed order,
318
- * and nothing changes until you set the property. Set `undefined` to hand it back.
319
- */
320
- get sorting() {
321
- return this.#sorting.value;
322
- }
323
- set sorting(value) {
324
- this.#sorting.set(value);
325
- }
326
- /**
327
- * Resized widths in px by column id, e.g. `{ name: 240 }`. A resized flex column
328
- * becomes fixed. Uncontrolled until set, like `sorting`: once set, a resize only
329
- * fires `argus-column-resize` with the proposed widths. `undefined` hands it back.
330
- */
331
- get columnSizing() {
332
- return this.#sizing.value;
333
- }
334
- set columnSizing(value) {
335
- this.#sizing.set(value);
336
- }
337
- /**
338
- * The column order: column ids as the table shows them, e.g.
339
- * `['id', 'total', 'customer']`. `[]` (the default) keeps the order of `columns`.
340
- * Each pin group (start-pinned, scrolling, end-pinned) takes its columns in this
341
- * order; unknown ids are ignored and columns it doesn't list follow the listed
342
- * ones of their group. After a move it holds every column id. Uncontrolled
343
- * until set, like `sorting`: once set, a move only fires `argus-column-reorder`
344
- * with the proposed order. `undefined` hands it back.
345
- */
346
- get columnOrder() {
347
- return this.#order.value;
348
- }
349
- set columnOrder(value) {
350
- this.#order.set(value);
351
- }
352
- #columnsProp;
353
- #dataProp;
354
- #overscan;
355
- #sorting;
356
- #sizing;
357
- #order;
358
- /** Moves the table applied itself, by column; their events fire once the update is done. */
359
- #appliedReorders;
360
- /** The header being dragged to a new position, if any. */
361
- #drag;
362
- /** A move from the keyboard, announced once it has rendered: the column and its old position. */
363
- #announceMove;
364
- /** Resizes the table applied itself, by column; their events fire once the update is done. */
365
- #appliedResizes;
366
- /** Measured widths of `width: 'auto'` columns. */
367
- #autoWidths;
368
- /** The auto widths last passed to the model; a change re-measures the pinned columns' offsets. */
369
- #pushedAutoWidths;
370
- #autoWidthsChanged;
371
- /** A sort the table applied itself; its event fires once the update is done. */
372
- #appliedSort;
373
- #internals;
374
- #table;
375
- #children;
376
- /** The columns last passed to the model: `columns`, or the `<argus-column>` children. */
377
- #columns;
378
- #columnsChanged;
379
- #warnedColumns;
380
- /** End of the light-DOM part holding cell content, so later children stay outside it. */
381
- #contentEnd;
382
- #data;
383
- /** Scroll back to the top after the update (a new sort order for a data source). */
384
- #scrollToTop;
385
- #virtual;
386
- #pinScroll;
387
- #rowPx;
388
- #probeObserver;
389
- #window;
390
- #active;
391
- /** Move focus to the active cell after the next update (keyboard navigation). */
392
- #focusPending;
393
- #lastRange;
394
- #unwatchMaxHeight;
657
+ const cell = gridCell(target);
658
+ if (cell?.dataset.col !== void 0) {
659
+ const model = this.#table.model;
660
+ const row = gridRowOfCell(cell, layoutOf(model));
661
+ const { col } = cell.dataset;
662
+ const prev = this.#active;
663
+ if (row && (row.section !== prev?.section || row.index !== prev.index || col !== prev.col)) {
664
+ this.#active = {
665
+ ...row,
666
+ rowId: this.#gridRowId(model, row),
667
+ col
668
+ };
669
+ this.requestUpdate();
670
+ }
671
+ return;
672
+ }
673
+ if (target === event.currentTarget) return;
674
+ }
675
+ };
676
+ this.#onClick = (event) => {
677
+ const path = event.composedPath();
678
+ this.#activate(path);
679
+ const target = this.#mode() === "none" ? void 0 : this.#selectableRowOf(path);
680
+ if (!target) return;
681
+ const inSelectionCell = path.some((t) => t instanceof HTMLElement && t.dataset.col === "--argus-selection");
682
+ this.#clickRow(target, event, inSelectionCell || event.ctrlKey || event.metaKey);
683
+ };
684
+ this.#checkboxHandlers = {
685
+ row: (event) => {
686
+ const index = Number(event.currentTarget.dataset.row);
687
+ const id = this.#table.model.rowAt(index)?.id;
688
+ if (id !== void 0) this.#clickRow({
689
+ id,
690
+ index
691
+ }, event, true);
692
+ this.requestUpdate();
693
+ },
694
+ all: () => {
695
+ this.#toggleAll();
696
+ this.requestUpdate();
697
+ },
698
+ press: (event) => {
699
+ event.preventDefault();
700
+ event.currentTarget.parentElement?.focus({ preventScroll: true });
701
+ }
702
+ };
703
+ this.#onMousedown = (event) => {
704
+ if (!event.shiftKey || event.button !== 0 || this.#mode() !== "multi") return;
705
+ const path = event.composedPath();
706
+ if (!this.#selectableRowOf(path)) return;
707
+ event.preventDefault();
708
+ for (const target of path) {
709
+ const cell = gridCell(target);
710
+ if (cell) {
711
+ cell.focus({ preventScroll: true });
712
+ return;
713
+ }
714
+ }
715
+ };
716
+ for (const type of [
717
+ "wheel",
718
+ "touchstart",
719
+ "pointerdown",
720
+ "keydown"
721
+ ]) this.addEventListener(type, this.#dropScrollTarget, { passive: true });
722
+ }
723
+ #dropScrollTarget;
395
724
  connectedCallback() {
396
725
  super.connectedCallback();
397
726
  if (!this.hasAttribute("role")) this.setAttribute("role", "grid");
398
727
  if (this.hasUpdated) this.#observeProbe();
728
+ this.#setLightConnected(true);
399
729
  this.#unwatchMaxHeight = watchMaxElementHeight(() => {
400
730
  this.requestUpdate();
401
731
  });
402
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
+ }
403
740
  disconnectedCallback() {
404
741
  super.disconnectedCallback();
405
742
  this.#probeObserver?.disconnect();
743
+ this.#rowMeasure.stop();
744
+ this.#setLightConnected(false);
406
745
  this.#unwatchMaxHeight?.();
407
746
  this.#unwatchMaxHeight = void 0;
408
747
  }
@@ -419,6 +758,14 @@ var ArgusTable = class ArgusTable extends LitElement {
419
758
  return this.columns;
420
759
  }
421
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
+ /**
422
769
  * The node the light-DOM content is rendered before. Lit's part then ends there,
423
770
  * so children appended later (an `<argus-column>`) never land inside it. If the
424
771
  * marker was removed (say by `innerHTML`), a new one starts a fresh part.
@@ -444,53 +791,172 @@ var ArgusTable = class ArgusTable extends LitElement {
444
791
  /**
445
792
  * Fits columns to their content: the header, the pinned rows and the body rows
446
793
  * rendered now (rows out of view aren't measured). It resizes them like a drag
447
- * 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
448
795
  * controlled `columnSizing` nothing changes until you set it. Without `ids`, fits
449
796
  * every resizable column; columns with `resizable: false` keep their width.
797
+ * With `virtualizeColumns`, columns out of view render for the measurement.
450
798
  * Resolves once the table has re-rendered.
451
799
  */
452
800
  async autosizeColumns(ids) {
453
801
  await this.updateComplete;
454
802
  const targets = this.#table.model.columns.filter((c) => c.resizable && (ids === void 0 || ids.includes(c.id))).map((c) => c.id);
455
- this.#requestResizes(measureColumns(this.renderRoot, targets));
803
+ if (this.virtualizeColumns) {
804
+ this.#measureKeep = targets;
805
+ this.requestUpdate();
806
+ await this.updateComplete;
807
+ }
808
+ const widths = measureColumns(this.renderRoot, targets);
809
+ if (this.#measureKeep.length > 0) {
810
+ this.#measureKeep = [];
811
+ this.requestUpdate();
812
+ }
813
+ this.#requestResizes(widths);
456
814
  await this.updateComplete;
457
815
  }
458
816
  /** Scrolls so that the row at `index` (in the current row order) is at the top. */
459
817
  async scrollToRow(index) {
460
818
  await this.updateComplete;
461
819
  this.#virtual.scrollToIndex(index);
820
+ await this.#settled();
821
+ }
822
+ /**
823
+ * Waits for the update, and with `rowHeight: 'auto'` for the ones measured
824
+ * rows request (a few at most: heights don't depend on positions).
825
+ */
826
+ async #settled() {
827
+ let done = await this.updateComplete;
828
+ for (let i = 0; i < 8 && !done && this.rowHeight === "auto"; i++) done = await this.updateComplete;
829
+ }
830
+ /**
831
+ * The state users change, as one object of plain JSON for saving a user's
832
+ * layout: `sorting`, `columnSizing`, `columnOrder`, `columnPinning`,
833
+ * `columnVisibility`, `columnFilters`, `selection`, `quickFilter`, and `scroll`
834
+ * (the row at the top and the inline offset), with `version: 1`. Controlled properties are
835
+ * included with the values you set. The object is a copy: changing it changes
836
+ * nothing in the table.
837
+ */
838
+ getState() {
839
+ const scroller = this.#scroller();
840
+ return structuredClone({
841
+ version: 1,
842
+ sorting: this.#sorting.value,
843
+ columnSizing: this.#sizing.value,
844
+ columnOrder: this.#order.value,
845
+ columnPinning: this.#pinning.value,
846
+ columnVisibility: this.#visibility.value,
847
+ columnFilters: this.#filters.value,
848
+ selection: this.#selection.value,
849
+ quickFilter: this.#quickFilter,
850
+ scroll: {
851
+ row: this.#virtual.topRow,
852
+ inline: Math.abs(scroller?.scrollLeft ?? 0)
853
+ }
854
+ });
855
+ }
856
+ /**
857
+ * Restores state from `getState()`, for example a layout saved in
858
+ * `localStorage`. Only the keys given change; ids of columns that no longer
859
+ * exist are ignored. State of another `version` is ignored as a whole. Properties you control are left alone (set them
860
+ * yourself), and the others stay uncontrolled. No change events fire.
861
+ * `quickFilter` is set like the property: a search box of yours shows it only
862
+ * if you read it back. `scroll` is applied after the rest, so with a
863
+ * `dataSource` the restored rows load where it points. Resolves once the table
864
+ * has re-rendered there.
865
+ */
866
+ async setState(state) {
867
+ if ((state.version ?? 1) !== 1) return;
868
+ this.#restore("sorting", this.#sorting, state.sorting);
869
+ this.#restore("columnSizing", this.#sizing, state.columnSizing);
870
+ this.#restore("columnOrder", this.#order, state.columnOrder);
871
+ this.#restore("columnPinning", this.#pinning, state.columnPinning);
872
+ this.#restore("columnVisibility", this.#visibility, state.columnVisibility);
873
+ this.#restore("columnFilters", this.#filters, state.columnFilters);
874
+ if (this.#restore("selection", this.#selection, state.selection)) this.#anchor = void 0;
875
+ if (state.quickFilter !== void 0) this.quickFilter = state.quickFilter;
876
+ this.#scrollTarget = void 0;
462
877
  await this.updateComplete;
878
+ const { scroll } = state;
879
+ if (!scroll) return;
880
+ this.#virtual.scrollToIndex(scroll.row);
881
+ const count = this.#table.model.rowCount;
882
+ if (this.#data.rows?.totalKnown === false && scroll.row >= count) this.#scrollTarget = {
883
+ row: scroll.row,
884
+ count
885
+ };
886
+ const scroller = this.#scroller();
887
+ if (scroller && scroll.inline >= 0) scroller.scrollLeft = scroll.inline * this.#direction();
888
+ await new Promise(requestAnimationFrame);
889
+ await this.#settled();
890
+ }
891
+ /** Scrolls on toward the row `setState()` restored once more rows are counted. */
892
+ #chaseScrollTarget(count) {
893
+ const target = this.#scrollTarget;
894
+ if (!target || target.count === count) return;
895
+ target.count = count;
896
+ this.#virtual.scrollToIndex(target.row);
897
+ if (target.row < count || this.#data.rows?.totalKnown !== false) this.#scrollTarget = void 0;
898
+ }
899
+ /**
900
+ * Applies a restored value to uncontrolled state without an event, as a user
901
+ * change would without the event. Returns whether it changed.
902
+ */
903
+ #restore(name, state, value) {
904
+ if (value === void 0 || state.controlled) return false;
905
+ if (JSON.stringify(value) === JSON.stringify(state.value)) return false;
906
+ const old = state.value;
907
+ state.request(structuredClone(value));
908
+ this.requestUpdate(name, old);
909
+ return true;
463
910
  }
464
911
  willUpdate(changed) {
465
- const columns = this.#effectiveColumns();
912
+ if (changed.has("plugins")) this.#plugins.set(this.plugins);
913
+ this.#plugins.willUpdate(new Set(changed.keys()));
914
+ const columns = this.#plugins.transform(this.#effectiveColumns());
466
915
  this.#columnsChanged = columns !== this.#columns;
467
916
  this.#columns = columns;
468
- if (changed.has("dataSource") || changed.has("sorting") || changed.has("getRowId")) {
917
+ const groups = this.#plugins.transformGroups(this.#effectiveGroups());
918
+ this.#groupsChanged = groups !== this.#groups;
919
+ this.#groups = groups;
920
+ const filtersChanged = changed.has("columnFilters") || changed.has("quickFilter");
921
+ if (changed.has("dataSource") || changed.has("sorting") || changed.has("getRowId") || filtersChanged) {
469
922
  this.#data.configure({
470
923
  source: this.dataSource,
471
924
  sorting: this.#sorting.value,
925
+ filters: this.#filters.value,
926
+ quickFilter: this.#quickFilter,
472
927
  getRowId: this.getRowId
473
928
  });
474
- if (this.dataSource && this.hasUpdated && changed.has("sorting")) this.#scrollToTop = !changed.has("dataSource");
929
+ if (this.dataSource && this.hasUpdated && (changed.has("sorting") || filtersChanged)) this.#scrollToTop = !changed.has("dataSource");
930
+ this.#scrollTarget = void 0;
475
931
  }
476
- if (this.#columnsChanged || changed.has("data") || changed.has("getRowId") || changed.has("sorting") || changed.has("columnSizing") || changed.has("columnOrder") || changed.has("pinnedRows")) {
932
+ if (filtersChanged && this.hasUpdated) this.#announceRows = true;
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")) {
477
934
  if (this.#columnsChanged) this.#autoWidths.sync(columns.filter((c) => c.width === "auto").map((c) => c.id));
478
935
  this.#table.update({
479
936
  columns,
937
+ columnGroups: groups,
480
938
  data: this.data,
481
939
  getRowId: this.getRowId,
482
940
  sorting: this.#sorting.value,
483
941
  columnSizing: this.#sizing.value,
484
942
  columnOrder: this.#order.value,
943
+ columnPinning: this.#pinning.value,
944
+ columnVisibility: this.#visibility.value,
945
+ columnFilters: this.#filters.value,
946
+ quickFilter: this.#quickFilter,
485
947
  pinnedRows: this.pinnedRows
486
948
  });
487
949
  }
488
950
  const autoWidths = this.#autoWidths.widths;
489
951
  this.#autoWidthsChanged = autoWidths !== this.#pushedAutoWidths;
490
952
  this.#pushedAutoWidths = autoWidths;
953
+ const selectionColumn = this.selectionCheckboxes && this.#mode() !== "none";
954
+ this.#selectionColumnChanged = selectionColumn !== this.#selectionColumn;
955
+ this.#selectionColumn = selectionColumn;
491
956
  this.#table.update({
492
957
  remote: this.#data.rows,
493
- autoWidths
958
+ autoWidths,
959
+ selectionColumn
494
960
  });
495
961
  const model = this.#table.model;
496
962
  this.#reconcileActive(model);
@@ -499,11 +965,19 @@ var ArgusTable = class ArgusTable extends LitElement {
499
965
  const layout = layoutOf(model);
500
966
  this.#internals.ariaRowCount = unknownTotal ? "-1" : String(layout.total);
501
967
  this.#internals.ariaColCount = String(model.columns.length);
502
- const extra = (1 + layout.top + layout.bottom) * this.#rowPx;
968
+ this.#internals.ariaMultiSelectable = this.#mode() === "multi" ? "true" : null;
969
+ const extra = this.#headerAndPinnedPx(layout);
970
+ const auto = this.rowHeight === "auto";
971
+ if (!auto) this.#rowMeasure.stop();
972
+ else if (changed.size > 0 || this.#columnsChanged) this.#rowMeasure.invalidate();
503
973
  this.#virtual.configure({
504
974
  count: model.rowCount,
505
975
  rowHeight: this.#rowPx,
506
- cap: bodyCap(model.rowCount * this.#rowPx, extra),
976
+ cap: (logical) => bodyCap(logical, extra),
977
+ auto: auto ? {
978
+ order: model.rowOrder,
979
+ indexOf: (id) => model.indexOf(id)
980
+ } : void 0,
507
981
  overscan: Math.max(0, this.overscan),
508
982
  rowIdAt: (index) => this.#rowId(model, index),
509
983
  keep: active?.section !== "body" || active.rowId === void 0 ? void 0 : {
@@ -516,17 +990,31 @@ var ArgusTable = class ArgusTable extends LitElement {
516
990
  const model = this.#table.model;
517
991
  const columns = model.columns;
518
992
  this.#window = this.#virtual.computeWindow();
519
- const pinnedWidth = (side) => columns.reduce((sum, c) => c.pin === side ? sum + c.width : sum, 0);
520
- const pinned = (side) => `${String(pinnedWidth(side))}px`;
993
+ const rendered = this.#columnWindow.compute(columns, this.virtualizeColumns, this.virtualizeColumns ? this.#keptColumns(model) : []);
994
+ this.#rendered = rendered;
995
+ const pinStart = pinnedWidth(columns, "start");
996
+ const pinEnd = pinnedWidth(columns, "end");
997
+ const cols = columns.map(track).join(" ");
521
998
  const layout = layoutOf(model);
522
999
  const style = {
523
- "--_cols": columns.map(track).join(" "),
1000
+ "--_cols": cols,
524
1001
  "--_width": `${String(model.totalWidth)}px`,
525
- "--_pin-start": pinned("start"),
526
- "--_pin-end": pinned("end"),
1002
+ "--_pin-start": `${String(pinStart)}px`,
1003
+ "--_pin-end": `${String(pinEnd)}px`,
1004
+ "--_header-rows": String(layout.header),
527
1005
  "--_pin-top-rows": String(layout.top),
528
1006
  "--_pin-bottom-rows": String(layout.bottom)
529
1007
  };
1008
+ const auto = this.rowHeight === "auto";
1009
+ if (auto) {
1010
+ const key = `${cols}|${String(this.#rowPx)}`;
1011
+ if (key !== this.#rowsKey) {
1012
+ this.#rowsKey = key;
1013
+ this.#rowMeasure.invalidate();
1014
+ }
1015
+ style["--_pin-top-h"] = `${String(this.#pinnedPx("top", layout))}px`;
1016
+ style["--_pin-bottom-h"] = `${String(this.#pinnedPx("bottom", layout))}px`;
1017
+ }
530
1018
  const active = this.#active;
531
1019
  const activeIn = (section) => active?.section === section ? {
532
1020
  index: active.index,
@@ -536,24 +1024,33 @@ var ArgusTable = class ArgusTable extends LitElement {
536
1024
  section,
537
1025
  index: 0
538
1026
  });
539
- if (this.rowHeight !== void 0 && this.rowHeight > 0) style["--_row-height"] = `${String(this.rowHeight)}px`;
1027
+ if (typeof this.rowHeight === "number" && this.rowHeight > 0) style["--_row-height"] = `${String(this.rowHeight)}px`;
1028
+ const contentPx = this.#headerAndPinnedPx(layout) + (model.rowCount === 0 ? 0 : this.#window.totalSize);
540
1029
  return html`<div
541
1030
  part="table"
542
1031
  role="presentation"
543
1032
  style=${styleMap(style)}
544
1033
  ?data-dragging=${this.#drag !== void 0}
1034
+ ?data-auto-rows=${auto}
545
1035
  @keydown=${this.#onKeydown}
546
1036
  @focusin=${this.#onFocusin}
1037
+ @mousedown=${this.#onMousedown}
547
1038
  @click=${this.#onClick}
548
1039
  >
549
- ${renderHeader(columns, this.#headerHandlers, activeIn("header")?.col, this.#drag)}
550
- ${renderPinnedRows("top", layout.top, columns, firstIndex("top"), activeIn("top"))}
551
- ${model.rowCount === 0 ? html`<div part="empty"><slot name="empty">No rows</slot></div>` : renderBody(this.#window.rows, columns, this.#window.totalSize, activeIn("body"), firstIndex("body"), this.#loading())}
552
- ${renderPinnedRows("bottom", layout.bottom, columns, firstIndex("bottom"), activeIn("bottom"))}
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
+ })}
1048
+ ${renderPinnedRows("top", layout.top, rendered, firstIndex("top"), activeIn("top"))}
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)}
1050
+ ${renderPinnedRows("bottom", layout.bottom, rendered, firstIndex("bottom"), activeIn("bottom"))}
553
1051
  <div class="probe" aria-hidden="true"></div>
554
1052
  </div>
555
- ${pinShadow("start", pinnedWidth("start"), layout.total - 1, this.#rowPx)}
556
- ${pinShadow("end", pinnedWidth("end"), layout.total - 1, this.#rowPx)}`;
1053
+ ${pinShadow("start", pinStart, contentPx)} ${pinShadow("end", pinEnd, contentPx)}`;
557
1054
  }
558
1055
  firstUpdated() {
559
1056
  this.#observeProbe();
@@ -561,45 +1058,54 @@ var ArgusTable = class ArgusTable extends LitElement {
561
1058
  }
562
1059
  updated(changed) {
563
1060
  const model = this.#table.model;
564
- renderLightDom(this, model, this.#window.rows, this.#lightDomEnd());
1061
+ const part = renderLightDom(this, model, this.#window.rows, this.#rendered, model.headerGroups, this.#lightDomEnd());
1062
+ if (this.#lightPart && this.#lightPart !== part) this.#lightPart.setConnected(false);
1063
+ this.#lightPart = part;
1064
+ flushContent(this);
565
1065
  this.#measureAutoWidths(model);
1066
+ const resized = this.rowHeight === "auto" && this.#measureRows();
566
1067
  if (this.#scrollToTop) {
567
1068
  this.#scrollToTop = false;
568
- const scroller = this.renderRoot.querySelector("[part~=\"table\"]");
1069
+ const scroller = this.#scroller();
569
1070
  if (scroller) scroller.scrollTop = 0;
570
1071
  }
571
- if (this.#columnsChanged || changed.has("columnSizing") || changed.has("columnOrder") || this.#autoWidthsChanged) this.#pinScroll.measure();
572
- if (this.#focusPending) {
1072
+ this.#chaseScrollTarget(model.rowCount);
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();
1074
+ if (this.#focusPending && !resized) {
573
1075
  this.#focusPending = false;
574
1076
  this.#focusActive();
575
1077
  }
576
1078
  if (changed.has("density") || changed.has("rowHeight")) this.#measureRowHeight();
577
- if (this.#appliedResizes.size > 0) {
578
- const details = [...this.#appliedResizes.values()];
579
- this.#appliedResizes.clear();
580
- for (const detail of details) fire(this, COLUMN_RESIZE, detail);
581
- }
1079
+ this.#fireApplied(this.#appliedResizes, COLUMN_SIZING_CHANGE);
582
1080
  if (this.#announceMove) {
583
1081
  this.#announceMoved(model, this.#announceMove);
584
1082
  this.#announceMove = void 0;
585
1083
  }
586
- if (this.#appliedReorders.size > 0) {
587
- const details = [...this.#appliedReorders.values()];
588
- this.#appliedReorders.clear();
589
- for (const detail of details) fire(this, COLUMN_REORDER, detail);
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);
1088
+ if (this.#announceRows) {
1089
+ this.#announceRows = false;
1090
+ this.#announceRowCount(model);
590
1091
  }
591
1092
  if (this.#appliedSort) {
592
1093
  const sorting = this.#appliedSort;
593
1094
  this.#appliedSort = void 0;
594
- fire(this, SORT_CHANGE, { sorting });
1095
+ fire(this, SORTING_CHANGE, { sorting });
1096
+ }
1097
+ if (this.#appliedSelection) {
1098
+ const selection = this.#appliedSelection;
1099
+ this.#appliedSelection = void 0;
1100
+ fire(this, SELECTION_CHANGE, { selection });
595
1101
  }
596
1102
  const { range } = this.#window;
597
1103
  const key = range ? `${String(range.start)}:${String(range.end)}` : "";
598
1104
  if (range && key !== this.#lastRange) {
599
1105
  this.#lastRange = key;
600
1106
  fire(this, RANGE_CHANGE, {
601
- start: range.start,
602
- end: range.end
1107
+ first: range.start,
1108
+ last: range.end
603
1109
  });
604
1110
  }
605
1111
  if (range) {
@@ -609,6 +1115,29 @@ var ArgusTable = class ArgusTable extends LitElement {
609
1115
  for (const detail of this.#data.takeErrors()) fire(this, LOAD_ERROR, detail);
610
1116
  }
611
1117
  /**
1118
+ * Columns to render wherever they are, with `virtualizeColumns`: the active
1119
+ * cell's (its tab stop and focus survive scrolling sideways, and keyboard
1120
+ * moves land on a rendered cell), one being dragged, auto widths still to
1121
+ * measure, the targets of `autosizeColumns()`, and with a `dataSource` the
1122
+ * first data column, which holds the Retry buttons.
1123
+ */
1124
+ #keptColumns(model) {
1125
+ return [
1126
+ this.#active?.col,
1127
+ ...this.#drag?.key.startsWith("c:") ? this.#drag.columns : [],
1128
+ this.dataSource ? model.columns.find((c) => c.kind === "data")?.id : void 0,
1129
+ ...this.#autoWidths.pending(model.columns),
1130
+ ...this.#measureKeep
1131
+ ];
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
+ }
1140
+ /**
612
1141
  * Measures the `width: 'auto'` columns once loaded body rows render, or right
613
1142
  * away when there are no rows (the header and pinned rows only). While a data
614
1143
  * source's first rows load, they keep the default width. A changed width
@@ -671,14 +1200,393 @@ var ArgusTable = class ArgusTable extends LitElement {
671
1200
  * cell (an input, a button) are left to that content.
672
1201
  */
673
1202
  #onKeydown;
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
+ /**
1327
+ * The grid position ArrowUp/ArrowDown (one row) or PageUp/PageDown (a page of
1328
+ * the body) moves to from `pos`. PageDown from above the body lands a page
1329
+ * into it and stops at its last row; PageUp stays in the body.
1330
+ */
1331
+ #verticalStep(key, pos, layout, scroller) {
1332
+ const bodyFirst = layout.position({
1333
+ section: "body",
1334
+ index: 0
1335
+ });
1336
+ const bodyLast = bodyFirst + layout.body - 1;
1337
+ const page = this.#pageRows(key, pos, layout, scroller);
1338
+ switch (key) {
1339
+ case "PageDown": return layout.body > 0 && pos <= bodyLast ? Math.min(Math.max(pos, bodyFirst - 1) + page, bodyLast) : pos;
1340
+ case "PageUp": return layout.body > 0 && pos > bodyFirst ? Math.max(Math.min(pos, bodyLast + 1) - page, bodyFirst) : pos;
1341
+ case "ArrowDown": return Math.min(pos + 1, layout.total - 1);
1342
+ case "ArrowUp": return Math.max(pos - 1, 0);
1343
+ }
1344
+ }
1345
+ /** How many rows PageUp/PageDown move from grid position `pos`. */
1346
+ #pageRows(key, pos, layout, scroller) {
1347
+ if (this.rowHeight !== "auto") {
1348
+ const fit = Math.floor(scroller.clientHeight / this.#rowPx + .01);
1349
+ return Math.max(1, fit - layout.header - layout.top - layout.bottom);
1350
+ }
1351
+ const view = scroller.clientHeight - this.#headerAndPinnedPx(layout);
1352
+ const bodyFirst = layout.position({
1353
+ section: "body",
1354
+ index: 0
1355
+ });
1356
+ const from = Math.min(Math.max(pos - bodyFirst, 0), Math.max(layout.body - 1, 0));
1357
+ return this.#virtual.pageRows(from, key === "PageDown", view);
1358
+ }
1359
+ /**
1360
+ * Selection keys, with a `selectionMode`: Space (or Shift+Space) toggles the
1361
+ * focused body row; in `multi` mode, Shift+ArrowUp/ArrowDown and
1362
+ * Shift+PageUp/PageDown move focus through the body and select from the
1363
+ * anchor to it, and Ctrl/Cmd+A selects every row. `true` if it took the key.
1364
+ */
1365
+ #selectionKey(event, cell, col, inHeader, scroller) {
1366
+ const mode = this.#mode();
1367
+ if (mode === "none" || event.altKey) return false;
1368
+ const { key } = event;
1369
+ const command = event.ctrlKey || event.metaKey;
1370
+ const model = this.#table.model;
1371
+ if (command && !event.shiftKey && key.toLowerCase() === "a") {
1372
+ if (mode !== "multi") return false;
1373
+ event.preventDefault();
1374
+ this.#selectAll();
1375
+ return true;
1376
+ }
1377
+ if (command) return false;
1378
+ if (inHeader) {
1379
+ if (key !== " " || col !== "--argus-selection" || mode !== "multi") return false;
1380
+ event.preventDefault();
1381
+ this.#toggleAll();
1382
+ return true;
1383
+ }
1384
+ const layout = layoutOf(model);
1385
+ const from = gridRowOf(cell.parentElement, layout);
1386
+ if (from?.section !== "body") return false;
1387
+ if (key === " ") {
1388
+ event.preventDefault();
1389
+ const id = model.rowAt(from.index)?.id;
1390
+ if (id !== void 0) this.#requestSelection(toggleRow(this.#selection.value, mode, id));
1391
+ return true;
1392
+ }
1393
+ if (!event.shiftKey || mode !== "multi" || key !== "ArrowDown" && key !== "ArrowUp" && key !== "PageDown" && key !== "PageUp") return false;
1394
+ event.preventDefault();
1395
+ const bodyFirst = layout.position({
1396
+ section: "body",
1397
+ index: 0
1398
+ });
1399
+ const pos = this.#verticalStep(key, layout.position(from), layout, scroller);
1400
+ const to = layout.at(Math.min(Math.max(pos, bodyFirst), bodyFirst + layout.body - 1));
1401
+ if (to?.section !== "body" || to.index === from.index) return true;
1402
+ if (key.startsWith("Page")) this.#virtual.scrollBy(this.#virtual.startOf(to.index) - this.#virtual.startOf(from.index));
1403
+ const fromId = model.rowAt(from.index)?.id;
1404
+ const anchor = this.#anchorAt(model) ?? (fromId === void 0 ? void 0 : {
1405
+ id: fromId,
1406
+ index: from.index
1407
+ });
1408
+ if (anchor) {
1409
+ const idAt = (index) => model.rowAt(index)?.id;
1410
+ const selection = selectRange([], anchor.index, to.index, idAt, false);
1411
+ this.#requestSelection({
1412
+ selection,
1413
+ anchor: anchor.id
1414
+ });
1415
+ }
1416
+ this.#moveTo(to, col, model);
1417
+ return true;
1418
+ }
674
1419
  /** Focus that lands in a cell (a click, or content inside it) makes that cell active. */
675
1420
  #onFocusin;
676
- /** `argus-cell-click`, then `argus-row-click`, for clicks in the body. */
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
+ }
1437
+ /**
1438
+ * `argus-cell-click`, then `argus-row-click`, for clicks in the body; then,
1439
+ * with a `selectionMode`, the click selects its row.
1440
+ */
677
1441
  #onClick;
1442
+ /** Selects for a click on a row, through `clickSelection`. */
1443
+ #clickRow(target, event, toggle) {
1444
+ const model = this.#table.model;
1445
+ this.#requestSelection(clickSelection(this.#selection.value, this.#mode(), target, this.#anchorAt(model), {
1446
+ toggle,
1447
+ range: event.shiftKey
1448
+ }, (index) => model.rowAt(index)?.id));
1449
+ }
1450
+ /**
1451
+ * The checkboxes' handlers. The table owns their state: a click asks for a new
1452
+ * selection, and the render that always follows sets every box back to the
1453
+ * selection (`live()`), so a change a controlled host turns down unticks it
1454
+ * again. Not `preventDefault()`: for a real click the browser runs that render
1455
+ * inside the dispatch, then undoes the toggle after it, over the render. A
1456
+ * press leaves focus on the cell, so the grid's keys keep working.
1457
+ */
1458
+ #checkboxHandlers;
1459
+ /** Selects every selectable row, keeping the others (with a `dataSource`: the loaded ones). */
1460
+ #selectAll() {
1461
+ const ids = this.#table.model.selectableIds();
1462
+ this.#requestSelection({
1463
+ selection: selectAll(this.#selection.value, ids),
1464
+ anchor: this.#anchor
1465
+ });
1466
+ }
1467
+ /** The select-all checkbox: clears the selection when every row is selected, else selects all. */
1468
+ #toggleAll() {
1469
+ if (this.#checkAllState(this.#table.model) === "all") this.#requestSelection({
1470
+ selection: [],
1471
+ anchor: this.#anchor
1472
+ });
1473
+ else this.#selectAll();
1474
+ }
1475
+ /** The select-all checkbox, in the checkbox column of a `multi` table. */
1476
+ #checkAllOf(model) {
1477
+ if (!this.#selectionColumn || this.#mode() !== "multi") return void 0;
1478
+ return {
1479
+ state: this.#checkAllState(model),
1480
+ handlers: this.#checkboxHandlers
1481
+ };
1482
+ }
1483
+ /**
1484
+ * Whether all, some or none of the selectable rows are selected. With `data`
1485
+ * it's worked out once per selection and rows shown, not on scroll;
1486
+ * with a `dataSource` every time, over the loaded rows, which eviction keeps few.
1487
+ */
1488
+ #checkAllState(model) {
1489
+ const count = () => {
1490
+ const ids = model.selectableIds();
1491
+ const selected = this.#selectedSet();
1492
+ const n = ids.reduce((sum, id) => selected.has(id) ? sum + 1 : sum, 0);
1493
+ if (n === 0) return "none";
1494
+ return n === ids.length ? "all" : "some";
1495
+ };
1496
+ if (this.#data.rows) return count();
1497
+ const key = [
1498
+ this.#selection.value,
1499
+ this.data,
1500
+ this.getRowId,
1501
+ this.#columns,
1502
+ this.#filters.value,
1503
+ this.#quickFilter,
1504
+ this.#visibility.value
1505
+ ];
1506
+ const cached = 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 = {
1510
+ key,
1511
+ state
1512
+ };
1513
+ return state;
1514
+ }
1515
+ /**
1516
+ * A Shift+press on a row in `multi` mode selects a range of rows, not the
1517
+ * page's text from the last click: no text selection, and the cell still
1518
+ * takes focus.
1519
+ */
1520
+ #onMousedown;
1521
+ /** `selectionMode`, with anything unknown read as `none`. */
1522
+ #mode() {
1523
+ const mode = this.selectionMode;
1524
+ return mode === "single" || mode === "multi" ? mode : "none";
1525
+ }
1526
+ /** Whether each rendered body row is selected; `undefined` without a `selectionMode`. */
1527
+ #selectedAt(model) {
1528
+ if (this.#mode() === "none") return void 0;
1529
+ const ids = this.#selectedSet();
1530
+ return (index) => {
1531
+ const id = model.rowAt(index)?.id;
1532
+ return id === void 0 ? void 0 : ids.has(id);
1533
+ };
1534
+ }
1535
+ /** `selection` as a set, rebuilt only when `selection` changes. */
1536
+ #selectedSet() {
1537
+ const selection = this.#selection.value;
1538
+ if (this.#selectedIds?.of !== selection) this.#selectedIds = {
1539
+ of: selection,
1540
+ ids: new Set(selection)
1541
+ };
1542
+ return this.#selectedIds.ids;
1543
+ }
1544
+ /**
1545
+ * The loaded body row a pointer event is on, as a selection target. None for
1546
+ * the header, pinned rows, rows still loading, and presses on interactive
1547
+ * content in a cell (a button, a link, an input).
1548
+ */
1549
+ #selectableRowOf(path) {
1550
+ const row = path.find((t) => t instanceof HTMLElement && t.getAttribute("role") === "row");
1551
+ if (!row || fromInteractive(path, row)) return void 0;
1552
+ const model = this.#table.model;
1553
+ const at = gridRowOf(row, layoutOf(model));
1554
+ if (at?.section !== "body") return void 0;
1555
+ const id = model.rowAt(at.index)?.id;
1556
+ return id === void 0 ? void 0 : {
1557
+ id,
1558
+ index: at.index
1559
+ };
1560
+ }
1561
+ /** The anchor row and where it is now; `undefined` if it isn't in the rows (or not loaded). */
1562
+ #anchorAt(model) {
1563
+ const id = this.#anchor;
1564
+ if (id === void 0) return void 0;
1565
+ const index = model.indexOf(id);
1566
+ return index === void 0 ? void 0 : {
1567
+ id,
1568
+ index
1569
+ };
1570
+ }
1571
+ /**
1572
+ * Changes the selection through the controlled/uncontrolled rules, and moves
1573
+ * the anchor. A step that leaves the same rows selected fires nothing.
1574
+ */
1575
+ #requestSelection(step) {
1576
+ if (!step) return;
1577
+ this.#anchor = step.anchor;
1578
+ const old = this.#selection.value;
1579
+ if (sameSelection(step.selection, old)) return;
1580
+ const { value, applied } = this.#selection.request(step.selection);
1581
+ if (applied) {
1582
+ this.#appliedSelection = value;
1583
+ this.requestUpdate("selection", old);
1584
+ } else fire(this, SELECTION_CHANGE, { selection: value });
1585
+ }
678
1586
  /**
679
1587
  * Fires `argus-cell-click` (when a cell is on the path), then `argus-row-click`,
680
1588
  * for the body or pinned row on an event path. Clicks and Enter on a cell use
681
- * it. Rows that haven't loaded and the header fire nothing.
1589
+ * it. Rows that haven't loaded, the header and the checkbox column fire nothing.
682
1590
  */
683
1591
  #activate(path) {
684
1592
  let columnId;
@@ -686,6 +1594,7 @@ var ArgusTable = class ArgusTable extends LitElement {
686
1594
  if (!(target instanceof HTMLElement)) continue;
687
1595
  const role = target.getAttribute("role");
688
1596
  if (role === "gridcell") columnId = target.dataset.col;
1597
+ if (columnId === "--argus-selection") return;
689
1598
  if (role !== "row") continue;
690
1599
  const model = this.#table.model;
691
1600
  const at = gridRowOf(target, layoutOf(model));
@@ -696,6 +1605,8 @@ var ArgusTable = class ArgusTable extends LitElement {
696
1605
  if (!row) return;
697
1606
  if (columnId !== void 0) fire(this, CELL_CLICK, {
698
1607
  row: row.original,
1608
+ rowId: row.id,
1609
+ index,
699
1610
  columnId,
700
1611
  value: model.valueAt(index, columnId)
701
1612
  });
@@ -711,6 +1622,8 @@ var ArgusTable = class ArgusTable extends LitElement {
711
1622
  if (!row) return;
712
1623
  if (columnId !== void 0) fire(this, CELL_CLICK, {
713
1624
  row: row.original,
1625
+ rowId: row.id,
1626
+ index,
714
1627
  columnId,
715
1628
  value: model.pinnedValueAt(pinned, index, columnId),
716
1629
  pinned
@@ -746,14 +1659,28 @@ var ArgusTable = class ArgusTable extends LitElement {
746
1659
  const active = this.#active;
747
1660
  if (!active) return;
748
1661
  const col = CSS.escape(active.col);
749
- const index = layoutOf(this.#table.model).ariaRowIndex(active);
750
- 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}"]`);
751
1679
  if (!cell) return;
752
1680
  cell.focus({ preventScroll: true });
753
- if (active.section === "body" && this.#virtual.scaled) {
754
- const layout = layoutOf(this.#table.model);
755
- const before = (1 + layout.top) * this.#rowPx;
756
- if (this.#virtual.reveal(active.index, before, layout.bottom * this.#rowPx)) {
1681
+ if (active.section === "body" && (this.#virtual.scaled || this.rowHeight === "auto")) {
1682
+ const before = layout.header * this.#rowPx + this.#pinnedPx("top", layout);
1683
+ if (this.#virtual.reveal(active.index, before, this.#pinnedPx("bottom", layout))) {
757
1684
  this.#focusPending = true;
758
1685
  return;
759
1686
  }
@@ -778,14 +1705,24 @@ var ArgusTable = class ArgusTable extends LitElement {
778
1705
  this.#active = void 0;
779
1706
  return;
780
1707
  }
1708
+ const layout = layoutOf(model);
1709
+ const leaf = layout.header - 1;
781
1710
  const prev = this.#active ?? {
782
- ...HEADER,
1711
+ ...layout.leafHeader,
783
1712
  rowId: void 0,
784
1713
  col: first.id
785
1714
  };
786
- const col = columns.some((c) => c.id === prev.col) ? prev.col : first.id;
1715
+ const col = columns.some((c) => c.id === prev.col) ? prev.col : this.#columnInPlaceOf(model, prev.col, first.id);
787
1716
  let { section, index, rowId } = prev;
788
- let moved = false;
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;
789
1726
  if (section === "body" && rowId !== void 0) {
790
1727
  const found = model.indexOf(rowId);
791
1728
  const unloaded = index < model.rowCount && model.rowAt(index) === void 0;
@@ -804,7 +1741,7 @@ var ArgusTable = class ArgusTable extends LitElement {
804
1741
  rowId = index >= 0 ? model.pinnedRowAt(section, index)?.id : void 0;
805
1742
  }
806
1743
  if (index < 0) {
807
- ({section, index} = HEADER);
1744
+ ({section, index} = layout.leafHeader);
808
1745
  rowId = void 0;
809
1746
  }
810
1747
  if (moved && this.matches(":focus-within")) this.#focusPending = true;
@@ -816,6 +1753,18 @@ var ArgusTable = class ArgusTable extends LitElement {
816
1753
  };
817
1754
  }
818
1755
  /**
1756
+ * The shown column that takes a hidden column's place: the next one in the
1757
+ * column order, else the one before it. `fallback` for a column that is gone
1758
+ * altogether.
1759
+ */
1760
+ #columnInPlaceOf(model, id, fallback) {
1761
+ const all = model.allColumns;
1762
+ const at = all.findIndex((c) => c.id === id);
1763
+ if (at < 0) return fallback;
1764
+ const shown = (c) => !c.hidden;
1765
+ return (all.slice(at + 1).find(shown) ?? all.slice(0, at).findLast(shown))?.id ?? fallback;
1766
+ }
1767
+ /**
819
1768
  * The width a resize starts from: the latest requested width (key presses can
820
1769
  * come faster than renders), else the model's fixed width, else the rendered
821
1770
  * width of a flex column.
@@ -826,6 +1775,49 @@ var ArgusTable = class ArgusTable extends LitElement {
826
1775
  const view = this.#table.model.columns.find((c) => c.id === columnId);
827
1776
  return view && view.flex === void 0 ? view.width : cell.getBoundingClientRect().width;
828
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
+ }
829
1821
  #direction() {
830
1822
  return getComputedStyle(this).direction === "rtl" ? -1 : 1;
831
1823
  }
@@ -835,8 +1827,8 @@ var ArgusTable = class ArgusTable extends LitElement {
835
1827
  }
836
1828
  /**
837
1829
  * Sets column widths (clamped by core) in one change, through the
838
- * controlled/uncontrolled rules. One `argus-column-resize` per column that
839
- * 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.
840
1832
  */
841
1833
  #requestResizes(widths) {
842
1834
  const model = this.#table.model;
@@ -852,90 +1844,210 @@ var ArgusTable = class ArgusTable extends LitElement {
852
1844
  ...Object.fromEntries(targets)
853
1845
  });
854
1846
  const details = targets.map(([columnId, width]) => ({
1847
+ columnSizing: value,
855
1848
  columnId,
856
- width,
857
- sizing: value
1849
+ width
858
1850
  }));
859
1851
  if (applied) {
860
- for (const detail of this.#appliedResizes.values()) detail.sizing = value;
1852
+ for (const detail of this.#appliedResizes.values()) detail.columnSizing = value;
861
1853
  for (const detail of details) this.#appliedResizes.set(detail.columnId, detail);
862
1854
  this.requestUpdate("columnSizing", old);
863
- } else for (const detail of details) fire(this, COLUMN_RESIZE, detail);
1855
+ } else for (const detail of details) fire(this, COLUMN_SIZING_CHANGE, detail);
864
1856
  }
865
1857
  /**
866
- * Moves a column to position `to` in the whole order (within its pin group),
867
- * through the controlled/uncontrolled rules. Starts from the latest requested
868
- * order, so moves can come faster than renders. `false` if it can't move there.
1858
+ * Pins or unpins a column through the controlled/uncontrolled rules. Starts
1859
+ * from the latest requested pins, so requests can come faster than renders.
869
1860
  */
870
- #requestReorder(columnId, to) {
871
- const old = this.#order.value;
872
- const next = this.#table.model.nextOrder(columnId, to, old);
873
- if (!next) return false;
874
- const { value, applied } = this.#order.request(next);
1861
+ #requestPin(columnId, pin) {
1862
+ const old = this.#pinning.value;
1863
+ const next = this.#table.model.nextPinning(columnId, pin, old);
1864
+ if (!next) return;
1865
+ const { value, applied } = this.#pinning.request(next);
875
1866
  if (applied) {
876
- for (const detail of this.#appliedReorders.values()) detail.order = value;
877
- this.#appliedReorders.set(columnId, {
1867
+ for (const detail of this.#appliedPins.values()) detail.columnPinning = value;
1868
+ this.#appliedPins.set(columnId, {
1869
+ columnPinning: value,
878
1870
  columnId,
879
- order: value
1871
+ pin
880
1872
  });
881
- this.requestUpdate("columnOrder", old);
882
- } else fire(this, COLUMN_REORDER, {
1873
+ this.requestUpdate("columnPinning", old);
1874
+ } else fire(this, COLUMN_PINNING_CHANGE, {
1875
+ columnPinning: value,
1876
+ columnId,
1877
+ pin
1878
+ });
1879
+ }
1880
+ /** Hides or shows a column through the controlled/uncontrolled rules. */
1881
+ #requestHide(columnId, hidden) {
1882
+ const old = this.#visibility.value;
1883
+ const next = this.#table.model.nextVisibility(columnId, hidden, old);
1884
+ if (!next) return;
1885
+ const { value, applied } = this.#visibility.request(next);
1886
+ if (applied) {
1887
+ for (const detail of this.#appliedHides.values()) detail.columnVisibility = value;
1888
+ this.#appliedHides.set(columnId, {
1889
+ columnVisibility: value,
1890
+ columnId,
1891
+ visible: !hidden
1892
+ });
1893
+ this.requestUpdate("columnVisibility", old);
1894
+ } else fire(this, COLUMN_VISIBILITY_CHANGE, {
1895
+ columnVisibility: value,
883
1896
  columnId,
884
- order: value
1897
+ visible: !hidden
885
1898
  });
1899
+ }
1900
+ /** Sets or clears a column's filter through the controlled/uncontrolled rules. */
1901
+ #requestFilter(columnId, filter) {
1902
+ const old = this.#filters.value;
1903
+ const next = this.#table.model.nextFilters(columnId, filter, old);
1904
+ if (!next) return;
1905
+ const { value, applied } = this.#filters.request(next);
1906
+ if (applied) {
1907
+ for (const detail of this.#appliedFilters.values()) detail.columnFilters = value;
1908
+ this.#appliedFilters.set(columnId, {
1909
+ columnFilters: value,
1910
+ columnId,
1911
+ columnFilter: filter
1912
+ });
1913
+ this.requestUpdate("columnFilters", old);
1914
+ } else fire(this, COLUMN_FILTERS_CHANGE, {
1915
+ columnFilters: value,
1916
+ columnId,
1917
+ columnFilter: filter
1918
+ });
1919
+ }
1920
+ /**
1921
+ * Says how many rows the filters leave ("42 rows"). With a `dataSource` the count
1922
+ * isn't known until the first page answers, so nothing is said.
1923
+ */
1924
+ #announceRowCount(model) {
1925
+ if (this.#data.rows) return;
1926
+ const n = model.rowCount;
1927
+ announce(this.ownerDocument, n === 1 ? "1 row" : `${String(n)} rows`);
1928
+ }
1929
+ /**
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.
1934
+ */
1935
+ #requestReorder(unit, to) {
1936
+ const old = this.#order.value;
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
+ };
1950
+ const { value, applied } = this.#order.request(next);
1951
+ if (applied) {
1952
+ for (const pending of this.#appliedReorders.values()) pending.columnOrder = value;
1953
+ this.#appliedReorders.set(unitKey(unit), detail(value));
1954
+ this.requestUpdate("columnOrder", old);
1955
+ } else fire(this, COLUMN_ORDER_CHANGE, detail(value));
886
1956
  return true;
887
1957
  }
888
- /** Alt+Shift+Arrow on a header: one step toward the start (-1) or the end (1). */
889
- #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) {
890
1960
  const model = this.#table.model;
891
- const from = model.orderOf(this.#order.value).indexOf(columnId);
892
- const view = model.columns.find((c) => c.id === columnId);
893
- if (from < 0 || !view) return;
894
- 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;
895
1965
  this.#focusPending = true;
896
1966
  this.#announceMove ??= {
897
- id: columnId,
898
- from: view.colIndex
1967
+ unit,
1968
+ from: at
899
1969
  };
900
1970
  this.requestUpdate();
901
1971
  }
902
- /** Tells screen readers where a column moved from the keyboard went. */
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;
1977
+ }
1978
+ /** Tells screen readers where a column or group moved from the keyboard went. */
903
1979
  #announceMoved(model, move) {
904
- const view = model.columns.find((c) => c.id === move.id);
905
- if (!view || view.colIndex === move.from) return;
906
- const header = this.renderRoot.querySelector(`[role='columnheader'][data-col="${CSS.escape(move.id)}"] > slot`);
907
- const name = header instanceof HTMLSlotElement ? header.assignedNodes({ flatten: true }).map((n) => n.textContent).join("").trim() : "";
908
- announce(this.ownerDocument, `${name || move.id} moved to column ${String(view.colIndex)} of ${String(model.columns.length)}`);
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}`);
909
1996
  }
910
- /** A press on a reorderable header: becomes a drag once the pointer moves. */
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. */
911
2003
  #startReorder(down) {
912
2004
  const cell = down.composedPath().find((t) => t instanceof HTMLElement && t.getAttribute("role") === "columnheader");
913
- const columnId = cell?.dataset.moveCol;
914
- if (!cell || columnId === void 0 || !canStartReorder(down, cell)) return;
915
- const scroller = this.renderRoot.querySelector("[part~=\"table\"]");
916
- if (!scroller) return;
917
- 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;
918
2028
  startReorderDrag(cell, down, {
919
2029
  scroller,
920
- targetAt: (x) => dropTarget(this.#dropSlots(pin), columnId, x, this.#direction()),
2030
+ targetAt: (x) => dropTarget(this.#dropSlots(unit), key, x, this.#direction()),
921
2031
  scrollArea: () => pin === void 0 ? this.#scrollArea(scroller) : void 0,
922
2032
  onStart: () => {
923
2033
  this.#drag = {
924
- columnId,
2034
+ key,
2035
+ columns,
925
2036
  target: void 0
926
2037
  };
927
2038
  this.requestUpdate();
928
2039
  },
929
2040
  onTarget: (target) => {
930
2041
  this.#drag = {
931
- columnId,
2042
+ key,
2043
+ columns,
932
2044
  target
933
2045
  };
934
2046
  this.requestUpdate();
935
2047
  },
936
2048
  onDrop: (target) => {
937
2049
  this.#drag = void 0;
938
- if (target && this.#requestReorder(columnId, target.to)) {
2050
+ if (target && this.#requestReorder(unit, target.to)) {
939
2051
  if (this.matches(":focus-within")) this.#focusPending = true;
940
2052
  }
941
2053
  this.requestUpdate();
@@ -946,14 +2058,18 @@ var ArgusTable = class ArgusTable extends LitElement {
946
2058
  }
947
2059
  });
948
2060
  }
949
- /** The rendered headers of one pin group, with their positions in the whole order. */
950
- #dropSlots(pin) {
2061
+ /**
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.
2065
+ */
2066
+ #dropSlots(unit) {
951
2067
  const slots = [];
952
- this.#table.model.columns.forEach((c, index) => {
953
- if (c.pin !== pin) return;
954
- 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)}"]`);
955
2071
  if (cell) slots.push({
956
- id: c.id,
2072
+ id,
957
2073
  index,
958
2074
  rect: cell.getBoundingClientRect()
959
2075
  });
@@ -962,8 +2078,7 @@ var ArgusTable = class ArgusTable extends LitElement {
962
2078
  }
963
2079
  /** Where the scrolling columns show: the scroller's inside, less the pinned columns. */
964
2080
  #scrollArea(scroller) {
965
- const columns = this.#table.model.columns;
966
- 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);
967
2082
  const ltr = this.#direction() === 1;
968
2083
  const left = scroller.getBoundingClientRect().left + scroller.clientLeft;
969
2084
  return {
@@ -974,13 +2089,73 @@ var ArgusTable = class ArgusTable extends LitElement {
974
2089
  /** asc → desc → unsorted, through the controlled/uncontrolled rules. */
975
2090
  #requestSort(columnId) {
976
2091
  const next = this.#table.model.nextSorting(columnId);
977
- if (!next) return;
2092
+ if (next) this.#applySorting(next);
2093
+ }
2094
+ /**
2095
+ * A plugin's sort: by a column in a direction, or (`null`) cleared if it is by
2096
+ * that column. Only sortable columns, and only a change.
2097
+ */
2098
+ #setSort(columnId, direction) {
2099
+ const view = this.#table.model.columns.find((c) => c.id === columnId);
2100
+ if (view?.kind !== "data" || !view.sortable) return;
2101
+ if (direction === (view.sorted === false ? void 0 : view.sorted)) return;
2102
+ if (direction === void 0) this.#applySorting([]);
2103
+ else this.#applySorting([{
2104
+ id: columnId,
2105
+ desc: direction === "desc"
2106
+ }]);
2107
+ }
2108
+ /** A new sort order, through the controlled/uncontrolled rules. */
2109
+ #applySorting(next) {
978
2110
  const old = this.#sorting.value;
979
2111
  const { value, applied } = this.#sorting.request(next);
980
2112
  if (applied) {
981
2113
  this.#appliedSort = value;
982
2114
  this.requestUpdate("sorting", old);
983
- } else fire(this, SORT_CHANGE, { sorting: value });
2115
+ } else fire(this, SORTING_CHANGE, { sorting: value });
2116
+ }
2117
+ /**
2118
+ * A data column as plugins see it. Header functions call this on every
2119
+ * render, so the views are indexed by id once per new column list.
2120
+ */
2121
+ #columnInfo(id) {
2122
+ const columns = this.#table.model.allColumns;
2123
+ if (this.#columnsById?.of !== columns) this.#columnsById = {
2124
+ of: columns,
2125
+ byId: new Map(columns.map((c) => [c.id, c]))
2126
+ };
2127
+ const view = this.#columnsById.byId.get(id);
2128
+ if (view?.kind !== "data") return void 0;
2129
+ return {
2130
+ id,
2131
+ sortable: view.sortable,
2132
+ resizable: view.resizable,
2133
+ pinnable: view.pinnable,
2134
+ hideable: view.hideable,
2135
+ pin: view.pin,
2136
+ hidden: view.hidden,
2137
+ width: view.width,
2138
+ sorted: view.sorted === false ? void 0 : view.sorted,
2139
+ filter: view.filter,
2140
+ columnFilter: this.#filters.value[id]
2141
+ };
2142
+ }
2143
+ /** The header or cell an event happened in, for plugins. */
2144
+ #cellTarget(event) {
2145
+ for (const target of event.composedPath()) {
2146
+ const cell = gridCell(target);
2147
+ const columnId = cell?.dataset.col;
2148
+ if (!cell || columnId === void 0) continue;
2149
+ const model = this.#table.model;
2150
+ const row = gridRowOfCell(cell, layoutOf(model));
2151
+ if (!row) return void 0;
2152
+ return {
2153
+ section: row.section,
2154
+ columnId,
2155
+ rowIndex: row.section === "header" ? 0 : row.index,
2156
+ rowId: this.#gridRowId(model, row)
2157
+ };
2158
+ }
984
2159
  }
985
2160
  #observeProbe() {
986
2161
  const probe = this.renderRoot.querySelector(".probe");
@@ -990,6 +2165,26 @@ var ArgusTable = class ArgusTable extends LitElement {
990
2165
  });
991
2166
  this.#probeObserver.observe(probe);
992
2167
  }
2168
+ /** The height of the rows pinned to one side: one row each, or measured with `rowHeight: 'auto'`. */
2169
+ #pinnedPx(side, layout) {
2170
+ const count = side === "top" ? layout.top : layout.bottom;
2171
+ if (this.rowHeight !== "auto" || count === 0) return count * this.#rowPx;
2172
+ return Math.max(this.#rowMeasure.pinned[side], count * this.#rowPx);
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
+ }
2178
+ /**
2179
+ * `rowHeight: 'auto'`: reads the heights of the rendered rows, after their
2180
+ * content has rendered. Returns whether any changed (an update is requested).
2181
+ */
2182
+ #measureRows() {
2183
+ const { rows, pinnedChanged } = this.#rowMeasure.read(this.renderRoot);
2184
+ const resized = this.#virtual.measureRows(rows);
2185
+ if (pinnedChanged) this.requestUpdate();
2186
+ return resized || pinnedChanged;
2187
+ }
993
2188
  /** Reads the resolved row height, so themes and density change the virtualizer too. */
994
2189
  #measureRowHeight() {
995
2190
  const px = this.renderRoot.querySelector(".probe")?.getBoundingClientRect().height ?? 0;
@@ -1000,19 +2195,36 @@ var ArgusTable = class ArgusTable extends LitElement {
1000
2195
  }
1001
2196
  };
1002
2197
  __decorate([property({ attribute: false })], ArgusTable.prototype, "columns", null);
2198
+ __decorate([property({ attribute: false })], ArgusTable.prototype, "columnGroups", null);
1003
2199
  __decorate([property({ attribute: false })], ArgusTable.prototype, "data", null);
1004
2200
  __decorate([property({ attribute: false })], ArgusTable.prototype, "dataSource", void 0);
1005
2201
  __decorate([property({ attribute: false })], ArgusTable.prototype, "pinnedRows", void 0);
1006
2202
  __decorate([property({ attribute: false })], ArgusTable.prototype, "getRowId", void 0);
1007
2203
  __decorate([property({
1008
2204
  attribute: "row-height",
1009
- type: Number
2205
+ converter: rowHeightConverter
1010
2206
  })], ArgusTable.prototype, "rowHeight", void 0);
1011
2207
  __decorate([property({ reflect: true })], ArgusTable.prototype, "density", void 0);
1012
2208
  __decorate([property({ type: Number })], ArgusTable.prototype, "overscan", null);
2209
+ __decorate([property({
2210
+ attribute: "virtualize-columns",
2211
+ converter: switchConverter
2212
+ })], ArgusTable.prototype, "virtualizeColumns", void 0);
1013
2213
  __decorate([property({ attribute: false })], ArgusTable.prototype, "sorting", null);
1014
2214
  __decorate([property({ attribute: false })], ArgusTable.prototype, "columnSizing", null);
1015
2215
  __decorate([property({ attribute: false })], ArgusTable.prototype, "columnOrder", null);
2216
+ __decorate([property({ attribute: false })], ArgusTable.prototype, "columnPinning", null);
2217
+ __decorate([property({ attribute: false })], ArgusTable.prototype, "columnVisibility", null);
2218
+ __decorate([property({ attribute: false })], ArgusTable.prototype, "columnFilters", null);
2219
+ __decorate([property({ attribute: "quick-filter" })], ArgusTable.prototype, "quickFilter", null);
2220
+ __decorate([property({ attribute: "selection-mode" })], ArgusTable.prototype, "selectionMode", void 0);
2221
+ __decorate([property({
2222
+ attribute: "selection-checkboxes",
2223
+ converter: switchConverter
2224
+ })], ArgusTable.prototype, "selectionCheckboxes", void 0);
2225
+ __decorate([property({ attribute: false })], ArgusTable.prototype, "selection", null);
2226
+ __decorate([property({ attribute: false })], ArgusTable.prototype, "contentRenderer", void 0);
2227
+ __decorate([property({ attribute: false })], ArgusTable.prototype, "plugins", null);
1016
2228
  ArgusTable = __decorate([customElement("argus-table")], ArgusTable);
1017
2229
  export { ArgusTable };
1018
2230