argus-table 0.2.1 → 0.3.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 (69) hide show
  1. package/README.md +1 -1
  2. package/dist/argus-column.js +24 -1
  3. package/dist/argus-column.js.map +1 -1
  4. package/dist/argus-table.js +1026 -167
  5. package/dist/argus-table.js.map +1 -1
  6. package/dist/cdn/argus-table.js +74 -50
  7. package/dist/cdn/argus-table.js.map +1 -1
  8. package/dist/controllers/auto-width.js +11 -0
  9. package/dist/controllers/auto-width.js.map +1 -1
  10. package/dist/controllers/column-window.js +139 -0
  11. package/dist/controllers/column-window.js.map +1 -0
  12. package/dist/controllers/data-controller.js +9 -4
  13. package/dist/controllers/data-controller.js.map +1 -1
  14. package/dist/controllers/plugins.js +102 -0
  15. package/dist/controllers/plugins.js.map +1 -0
  16. package/dist/controllers/reorder-drag.js +9 -10
  17. package/dist/controllers/reorder-drag.js.map +1 -1
  18. package/dist/controllers/row-measure.js +121 -0
  19. package/dist/controllers/row-measure.js.map +1 -0
  20. package/dist/controllers/virtual-controller.js +316 -10
  21. package/dist/controllers/virtual-controller.js.map +1 -1
  22. package/dist/core/src/columns/column-order.js +14 -10
  23. package/dist/core/src/columns/column-order.js.map +1 -1
  24. package/dist/core/src/columns/resolve-columns.js +18 -4
  25. package/dist/core/src/columns/resolve-columns.js.map +1 -1
  26. package/dist/core/src/data/page-cache.js +4 -0
  27. package/dist/core/src/data/page-cache.js.map +1 -1
  28. package/dist/core/src/filters/filter.js +185 -0
  29. package/dist/core/src/filters/filter.js.map +1 -0
  30. package/dist/core/src/state/selection.js +70 -0
  31. package/dist/core/src/state/selection.js.map +1 -0
  32. package/dist/core/src/table/create-table.js +239 -64
  33. package/dist/core/src/table/create-table.js.map +1 -1
  34. package/dist/core/src/table/filtered-row-model.js +48 -0
  35. package/dist/core/src/table/filtered-row-model.js.map +1 -0
  36. package/dist/core/src/table/sorted-row-model.js +13 -2
  37. package/dist/core/src/table/sorted-row-model.js.map +1 -1
  38. package/dist/core/src/virtual/column-window.js +72 -0
  39. package/dist/core/src/virtual/column-window.js.map +1 -0
  40. package/dist/core/src/virtual/row-pool.js +2 -1
  41. package/dist/core/src/virtual/row-pool.js.map +1 -1
  42. package/dist/core/src/virtual/row-sizes.js +156 -0
  43. package/dist/core/src/virtual/row-sizes.js.map +1 -0
  44. package/dist/core/src/virtual/scroll-scale.js +41 -3
  45. package/dist/core/src/virtual/scroll-scale.js.map +1 -1
  46. package/dist/custom-elements.json +1320 -74
  47. package/dist/events.js +9 -1
  48. package/dist/events.js.map +1 -1
  49. package/dist/index.d.ts +609 -74
  50. package/dist/index.js +3 -2
  51. package/dist/interactive.js +22 -0
  52. package/dist/interactive.js.map +1 -0
  53. package/dist/max-height.js +22 -8
  54. package/dist/max-height.js.map +1 -1
  55. package/dist/render/body.js +47 -7
  56. package/dist/render/body.js.map +1 -1
  57. package/dist/render/cells.js +14 -11
  58. package/dist/render/cells.js.map +1 -1
  59. package/dist/render/content-directive.js +115 -0
  60. package/dist/render/content-directive.js.map +1 -0
  61. package/dist/render/content.js +20 -9
  62. package/dist/render/content.js.map +1 -1
  63. package/dist/render/header.js +24 -5
  64. package/dist/render/header.js.map +1 -1
  65. package/dist/render/pinned-rows.js +1 -1
  66. package/dist/render/pinned-rows.js.map +1 -1
  67. package/dist/styles/base.js +1 -1
  68. package/dist/styles/base.js.map +1 -1
  69. package/package.json +1 -1
@@ -1,18 +1,25 @@
1
+ import { clickSelection, sameSelection, selectAll, selectRange, toggleRow } from "./core/src/state/selection.js";
1
2
  import { controllable } from "./core/src/state/controllable.js";
2
3
  import { RowLayout } from "./core/src/grid/row-layout.js";
4
+ import "./core/src/table/create-table.js";
3
5
  import { announce } from "./announce.js";
4
6
  import { AutoWidths, measureColumns } from "./controllers/auto-width.js";
5
7
  import { ColumnChildrenController } from "./controllers/column-children.js";
8
+ import { ColumnWindowController } from "./controllers/column-window.js";
6
9
  import { DataController } from "./controllers/data-controller.js";
7
10
  import { PinScrollController } from "./controllers/pin-scroll-controller.js";
11
+ import { PluginController, sameItems } from "./controllers/plugins.js";
12
+ import { RowMeasure } from "./controllers/row-measure.js";
8
13
  import { canStartReorder, dropTarget, startReorderDrag } from "./controllers/reorder-drag.js";
9
14
  import { startResizeDrag } from "./controllers/resize-drag.js";
10
15
  import { TableController } from "./controllers/table-controller.js";
11
16
  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";
17
+ import { CELL_CLICK, COLUMN_FILTER, COLUMN_HIDE, COLUMN_PIN, COLUMN_REORDER, COLUMN_RESIZE, LOAD_ERROR, RANGE_CHANGE, ROW_CLICK, ROW_SELECT, SORT_CHANGE, fire } from "./events.js";
18
+ import { fromInteractive } from "./interactive.js";
13
19
  import { bodyCap, watchMaxElementHeight } from "./max-height.js";
14
20
  import { renderBody } from "./render/body.js";
15
21
  import { renderLightDom } from "./render/cells.js";
22
+ import { content, flushContent } from "./render/content-directive.js";
16
23
  import { renderHeader } from "./render/header.js";
17
24
  import { renderPinnedRows } from "./render/pinned-rows.js";
18
25
  import { baseStyles } from "./styles/base.js";
@@ -34,17 +41,27 @@ function attachGridInternals(host) {
34
41
  }
35
42
  /**
36
43
  * 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.
44
+ * too. Clicks on a resize handle never sort, so a click that ends a drag doesn't,
45
+ * and neither do clicks on interactive content in the header (a button, an input).
38
46
  */
39
47
  function sortColumnOf(event) {
40
48
  for (const target of event.composedPath()) {
41
49
  if (!(target instanceof HTMLElement)) continue;
42
50
  if (target.dataset.resizeCol !== void 0) return void 0;
51
+ if (target.matches("button,input,select,textarea,a[href],label,[contenteditable],[popover],dialog")) return void 0;
43
52
  if (target.dataset.sortCol !== void 0) return target.dataset.sortCol;
44
53
  if (target.getAttribute("part") === "header-row") return void 0;
45
54
  }
46
55
  }
47
56
  var RESIZE_STEP = 10;
57
+ /** `row-height`: px (`48` or `48px`) or `auto`; anything else is unset. */
58
+ var rowHeightConverter = { fromAttribute(value) {
59
+ if (value === null) return void 0;
60
+ const text = value.trim().toLowerCase();
61
+ if (text === "auto") return "auto";
62
+ const px = Number(text.replace(/px$/, ""));
63
+ return text !== "" && px > 0 ? px : void 0;
64
+ } };
48
65
  var HEADER = {
49
66
  section: "header",
50
67
  index: 0
@@ -70,7 +87,7 @@ function gridRowOf(row, layout) {
70
87
  * covers the pinned columns and casts `--argus-pinned-shadow` onto the rest;
71
88
  * PinScrollController sizes it to the visible area.
72
89
  */
73
- function pinShadow(side, width, rows, rowPx) {
90
+ function pinShadow(side, width, contentPx) {
74
91
  if (width === 0) return nothing;
75
92
  return html`<div
76
93
  class="pin-shadow"
@@ -78,7 +95,7 @@ function pinShadow(side, width, rows, rowPx) {
78
95
  aria-hidden="true"
79
96
  style=${styleMap({
80
97
  inlineSize: `${String(width)}px`,
81
- "--_content-h": `${String((rows + 1) * rowPx)}px`
98
+ "--_content-h": `${String(contentPx)}px`
82
99
  })}
83
100
  ></div>`;
84
101
  }
@@ -87,17 +104,275 @@ function track(column) {
87
104
  return column.flex === void 0 ? `${String(column.width)}px` : `minmax(${String(column.minWidth)}px, ${String(column.flex)}fr)`;
88
105
  }
89
106
  var ArgusTable = class ArgusTable extends LitElement {
90
- constructor(..._args) {
91
- super(..._args);
107
+ static {
108
+ this.styles = baseStyles;
109
+ }
110
+ /**
111
+ * Column definitions. Property only: functions can't be attributes. While empty,
112
+ * the table reads its `<argus-column>` children instead. `null` or `undefined`
113
+ * means `[]`.
114
+ */
115
+ get columns() {
116
+ return this.#columnsProp;
117
+ }
118
+ set columns(value) {
119
+ this.#columnsProp = value ?? [];
120
+ }
121
+ /**
122
+ * The rows to show. Replace the array to update; changes inside it aren't
123
+ * detected. `null` or `undefined` means `[]`.
124
+ */
125
+ get data() {
126
+ return this.#dataProp;
127
+ }
128
+ set data(value) {
129
+ this.#dataProp = value ?? [];
130
+ }
131
+ /** Rows rendered above and below the visible ones. `null` or `undefined` means the default, 6. */
132
+ get overscan() {
133
+ return this.#overscan;
134
+ }
135
+ set overscan(value) {
136
+ this.#overscan = value ?? 6;
137
+ }
138
+ /**
139
+ * The sort order, e.g. `[{ id: 'total', desc: true }]`; `[]` when unsorted. Unset,
140
+ * the table keeps it itself and header clicks change it. Once set, it is
141
+ * controlled: a click only fires `argus-sort-change` with the proposed order,
142
+ * and nothing changes until you set the property. Set `undefined` to hand it back.
143
+ */
144
+ get sorting() {
145
+ return this.#sorting.value;
146
+ }
147
+ set sorting(value) {
148
+ this.#sorting.set(value);
149
+ }
150
+ /**
151
+ * Resized widths in px by column id, e.g. `{ name: 240 }`. A resized flex column
152
+ * becomes fixed. Uncontrolled until set, like `sorting`: once set, a resize only
153
+ * fires `argus-column-resize` with the proposed widths. `undefined` hands it back.
154
+ */
155
+ get columnSizing() {
156
+ return this.#sizing.value;
157
+ }
158
+ set columnSizing(value) {
159
+ this.#sizing.set(value);
160
+ }
161
+ /**
162
+ * The column order: column ids as the table shows them, e.g.
163
+ * `['id', 'total', 'customer']`. `[]` (the default) keeps the order of `columns`.
164
+ * Each pin group (start-pinned, scrolling, end-pinned) takes its columns in this
165
+ * order; unknown ids are ignored and columns it doesn't list follow the listed
166
+ * ones of their group. After a move it holds every column id. Uncontrolled
167
+ * until set, like `sorting`: once set, a move only fires `argus-column-reorder`
168
+ * with the proposed order. `undefined` hands it back.
169
+ */
170
+ get columnOrder() {
171
+ return this.#order.value;
172
+ }
173
+ set columnOrder(value) {
174
+ this.#order.set(value);
175
+ }
176
+ /**
177
+ * Pins by column id, over the columns' own `pin`: `'start'` or `'end'` pins a
178
+ * column, `false` unpins one whose definition pins it, e.g.
179
+ * `{ total: 'end', id: false }`. Columns it doesn't list keep their `pin`. A pinned
180
+ * column joins the edge of its group nearest its place in `columnOrder`, and stops
181
+ * flexing. Uncontrolled until set, like `sorting`: once set, pinning from a plugin
182
+ * only fires `argus-column-pin` with the proposed pins. `undefined` hands it back.
183
+ */
184
+ get columnPinning() {
185
+ return this.#pinning.value;
186
+ }
187
+ set columnPinning(value) {
188
+ this.#pinning.set(value);
189
+ }
190
+ /**
191
+ * Visibility by column id, over the columns' own `hidden`: `false` hides a column,
192
+ * `true` shows one defined as `hidden`, e.g. `{ email: false }`. Columns it
193
+ * doesn't list keep their `hidden`. A hidden column isn't rendered, but sorting by
194
+ * it still works and it keeps its place in `columnOrder`. Uncontrolled until set,
195
+ * like `sorting`: once set, hiding or showing from a plugin only fires
196
+ * `argus-column-hide` with the proposed visibility. `undefined` hands it back.
197
+ */
198
+ get columnVisibility() {
199
+ return this.#visibility.value;
200
+ }
201
+ set columnVisibility(value) {
202
+ this.#visibility.set(value);
203
+ }
204
+ /**
205
+ * Filters by column id, e.g. `{ city: { kind: 'set', values: ['Berlin'] } }` or
206
+ * `{ total: { kind: 'number', op: 'gt', value: 100 } }`. A row shows when it passes
207
+ * every filter and `quickFilter`. Filters on hidden columns still apply; unknown ids
208
+ * are ignored. A column's `filterFn` replaces the built-in matching. With a
209
+ * `dataSource`, the filters go to `getRows()` and the server filters. Users set
210
+ * filters through plugins (the column menu) on columns with a `filter` kind.
211
+ * Uncontrolled until set, like `sorting`: once set, a change from a plugin only
212
+ * fires `argus-column-filter` with the proposed filters. `undefined` hands it back.
213
+ */
214
+ get columnFilters() {
215
+ return this.#filters.value;
216
+ }
217
+ set columnFilters(value) {
218
+ this.#filters.set(value);
219
+ }
220
+ /**
221
+ * Shows only rows that contain each word of this text in one of their shown
222
+ * columns, ignoring case, e.g. from a search box. Values are matched as text
223
+ * (dates as `2026-11-10`); objects aren't searched. With a `dataSource`, it goes
224
+ * to `getRows()` and the server filters. `''` (the default) shows every row.
225
+ */
226
+ get quickFilter() {
227
+ return this.#quickFilter;
228
+ }
229
+ set quickFilter(value) {
230
+ this.#quickFilter = value ?? "";
231
+ }
232
+ /**
233
+ * The selected rows, as row ids (from `getRowId`, or the row's index as a
234
+ * string), e.g. `['p4', 'p7']`. Uncontrolled until set, like `sorting`: once
235
+ * set, a click only fires `argus-row-select` with the proposed ids.
236
+ * `undefined` hands it back. The table never drops ids on its own: they stay
237
+ * through sorting, a new `data` or reloaded pages. With a `dataSource`,
238
+ * Ctrl/Cmd+A selects the loaded rows only.
239
+ */
240
+ get selection() {
241
+ return this.#selection.value;
242
+ }
243
+ set selection(value) {
244
+ this.#selection.set(value);
245
+ }
246
+ /**
247
+ * Add-ons, such as `headerMenu()` from `@argus-table/header-menu`. Each one can
248
+ * change the column definitions, handle keys on a focused header or cell, and
249
+ * use the table through a small API. They run in array order. Replace the
250
+ * array to change them; an array with the same plugins in the same order
251
+ * changes nothing. `null` or `undefined` means `[]`.
252
+ */
253
+ get plugins() {
254
+ return this.#pluginsProp;
255
+ }
256
+ set plugins(value) {
257
+ const next = value ?? [];
258
+ if (!sameItems(next, this.#pluginsProp)) this.#pluginsProp = next;
259
+ }
260
+ #columnsProp;
261
+ #pluginsProp;
262
+ #dataProp;
263
+ #overscan;
264
+ #sorting;
265
+ #sizing;
266
+ #order;
267
+ #pinning;
268
+ #visibility;
269
+ #filters;
270
+ #quickFilter;
271
+ /** Filters the table applied itself, by column; their events fire once the update is done. */
272
+ #appliedFilters;
273
+ /** Announce the row count after the update: the filters changed. */
274
+ #announceRows;
275
+ /** Pins the table applied itself, by column; their events fire once the update is done. */
276
+ #appliedPins;
277
+ /** Hidden or shown columns the table applied itself; their events fire once the update is done. */
278
+ #appliedHides;
279
+ #selection;
280
+ /** `selection` as a set, for the rows rendered; rebuilt when `selection` changes. */
281
+ #selectedIds;
282
+ /** The row a Shift range starts from: the last row clicked or toggled without Shift. */
283
+ #anchor;
284
+ /** A selection the table applied itself; its event fires once the update is done. */
285
+ #appliedSelection;
286
+ /** Whether the checkbox column was last shown; a change re-measures the pinned columns. */
287
+ #selectionColumn;
288
+ #selectionColumnChanged;
289
+ /** The select-all checkbox's state, kept until `selection`, `data` or `getRowId` change. */
290
+ #checkAll;
291
+ /** Moves the table applied itself, by column; their events fire once the update is done. */
292
+ #appliedReorders;
293
+ /** The header being dragged to a new position, if any. */
294
+ #drag;
295
+ /** A move from the keyboard, announced once it has rendered: the column and its old position. */
296
+ #announceMove;
297
+ /** Resizes the table applied itself, by column; their events fire once the update is done. */
298
+ #appliedResizes;
299
+ /** Columns `autosizeColumns()` renders out of view for its measurement. */
300
+ #measureKeep;
301
+ /** Measured widths of `width: 'auto'` columns. */
302
+ #autoWidths;
303
+ /** The auto widths last passed to the model; a change re-measures the pinned columns' offsets. */
304
+ #pushedAutoWidths;
305
+ #autoWidthsChanged;
306
+ /** A sort the table applied itself; its event fires once the update is done. */
307
+ #appliedSort;
308
+ #internals;
309
+ #table;
310
+ #children;
311
+ /** What plugins get: closures over the table's private state. */
312
+ #pluginHost;
313
+ #plugins;
314
+ #columnsById;
315
+ /** The columns last passed to the model: `columns`, or the `<argus-column>` children. */
316
+ #columns;
317
+ #columnsChanged;
318
+ #warnedColumns;
319
+ /** End of the light-DOM part holding cell content, so later children stay outside it. */
320
+ #contentEnd;
321
+ /** The light-DOM part: disconnected with the table, so foreign content is released. */
322
+ #lightPart;
323
+ #lightConnected;
324
+ #data;
325
+ /** Scroll back to the top after the update (a new sort order for a data source). */
326
+ #scrollToTop;
327
+ #virtual;
328
+ #pinScroll;
329
+ #columnWindow;
330
+ /** The columns the last render rendered (all of them, unless `virtualizeColumns`). */
331
+ #rendered;
332
+ #rowPx;
333
+ #probeObserver;
334
+ /** The column template and row height the rows were last read for. */
335
+ #rowsKey;
336
+ /** Rows of different heights: reads the rendered rows (`rowHeight: 'auto'` only). */
337
+ #rowMeasure;
338
+ #window;
339
+ #active;
340
+ /** Move focus to the active cell after the next update (keyboard navigation). */
341
+ #focusPending;
342
+ #lastRange;
343
+ #unwatchMaxHeight;
344
+ /**
345
+ * The row `setState()` still has to scroll to: a data source's rows that far
346
+ * aren't counted yet. Scrolled to again whenever the row count changes.
347
+ */
348
+ #scrollTarget;
349
+ constructor() {
350
+ super();
92
351
  this.density = "comfortable";
352
+ this.virtualizeColumns = false;
353
+ this.selectionMode = "none";
354
+ this.selectionCheckboxes = false;
93
355
  this.#columnsProp = [];
356
+ this.#pluginsProp = [];
94
357
  this.#dataProp = [];
95
358
  this.#overscan = 6;
96
359
  this.#sorting = controllable([]);
97
360
  this.#sizing = controllable({});
98
361
  this.#order = controllable([]);
362
+ this.#pinning = controllable({});
363
+ this.#visibility = controllable({});
364
+ this.#filters = controllable({});
365
+ this.#quickFilter = "";
366
+ this.#appliedFilters = /* @__PURE__ */ new Map();
367
+ this.#announceRows = false;
368
+ this.#appliedPins = /* @__PURE__ */ new Map();
369
+ this.#appliedHides = /* @__PURE__ */ new Map();
370
+ this.#selection = controllable([]);
371
+ this.#selectionColumn = false;
372
+ this.#selectionColumnChanged = false;
99
373
  this.#appliedReorders = /* @__PURE__ */ new Map();
100
374
  this.#appliedResizes = /* @__PURE__ */ new Map();
375
+ this.#measureKeep = [];
101
376
  this.#autoWidths = new AutoWidths(() => {
102
377
  this.requestUpdate();
103
378
  });
@@ -105,14 +380,64 @@ var ArgusTable = class ArgusTable extends LitElement {
105
380
  this.#internals = attachGridInternals(this);
106
381
  this.#table = new TableController();
107
382
  this.#children = new ColumnChildrenController(this);
383
+ this.#pluginHost = {
384
+ element: this,
385
+ column: (id) => this.#columnInfo(id),
386
+ columns: () => this.#table.model.allColumns.flatMap((c) => {
387
+ const info = this.#columnInfo(c.id);
388
+ return info ? [info] : [];
389
+ }),
390
+ setPin: (columnId, pin) => {
391
+ this.#requestPin(columnId, pin);
392
+ },
393
+ setHidden: (columnId, hidden) => {
394
+ this.#requestHide(columnId, hidden);
395
+ },
396
+ setFilter: (columnId, filter) => {
397
+ this.#requestFilter(columnId, filter);
398
+ },
399
+ filterValues: (columnId, limit) => this.#table.model.filterValues(columnId, limit),
400
+ cellOf: (event) => this.#cellTarget(event),
401
+ setSort: (columnId, direction) => {
402
+ this.#setSort(columnId, direction);
403
+ },
404
+ focusHeader: (columnId) => {
405
+ this.#moveTo(HEADER, columnId, this.#table.model);
406
+ },
407
+ announce: (message) => {
408
+ announce(this.ownerDocument, message);
409
+ },
410
+ invalidateColumns: () => {
411
+ this.#plugins.invalidate();
412
+ this.requestUpdate();
413
+ },
414
+ requestUpdate: () => {
415
+ this.requestUpdate();
416
+ },
417
+ row: (index) => this.#table.model.rowAt(index)?.original,
418
+ content: (value) => content(value)
419
+ };
420
+ this.#plugins = new PluginController(this, this.#pluginHost);
108
421
  this.#columns = [];
109
422
  this.#columnsChanged = false;
110
423
  this.#warnedColumns = false;
424
+ this.#lightConnected = true;
111
425
  this.#data = new DataController(this);
112
426
  this.#scrollToTop = false;
113
427
  this.#virtual = new VirtualController(this, () => this.renderRoot.querySelector("[part~=\"table\"]"));
114
428
  this.#pinScroll = new PinScrollController(this, () => this.renderRoot.querySelector("[part~=\"table\"]"));
429
+ this.#columnWindow = new ColumnWindowController(this, () => this.renderRoot.querySelector("[part~=\"table\"]"));
430
+ this.#rendered = [];
115
431
  this.#rowPx = 40;
432
+ this.#rowsKey = "";
433
+ this.#rowMeasure = new RowMeasure(this, {
434
+ bodyIndex: (row) => {
435
+ const at = gridRowOf(row, layoutOf(this.#table.model));
436
+ return at?.section === "body" && this.#data.stateAt(at.index) === "loaded" ? at.index : void 0;
437
+ },
438
+ sizeOf: (index) => this.#virtual.sizeOf(index),
439
+ minimum: () => this.#rowPx
440
+ });
116
441
  this.#window = {
117
442
  rows: [],
118
443
  totalSize: 0,
@@ -120,6 +445,9 @@ var ArgusTable = class ArgusTable extends LitElement {
120
445
  };
121
446
  this.#focusPending = false;
122
447
  this.#lastRange = "";
448
+ this.#dropScrollTarget = () => {
449
+ this.#scrollTarget = void 0;
450
+ };
123
451
  this.#onRetry = (event) => {
124
452
  const button = event.currentTarget;
125
453
  if (button.matches(":focus")) button.parentElement?.focus({ preventScroll: true });
@@ -164,6 +492,8 @@ var ArgusTable = class ArgusTable extends LitElement {
164
492
  }, { once: true });
165
493
  return;
166
494
  }
495
+ const pluginTarget = id === "--argus-selection" ? void 0 : this.#cellTarget(event);
496
+ if (pluginTarget && this.#plugins.keydown(event, pluginTarget)) return;
167
497
  const inHeader = cell.getAttribute("role") === "columnheader";
168
498
  const { key } = event;
169
499
  const arrow = key === "ArrowLeft" || key === "ArrowRight";
@@ -184,6 +514,7 @@ var ArgusTable = class ArgusTable extends LitElement {
184
514
  this.#requestSort(id);
185
515
  return;
186
516
  }
517
+ if (this.#selectionKey(event, cell, id, inHeader, scroller)) return;
187
518
  const corner = event.ctrlKey || event.metaKey;
188
519
  if (event.altKey || event.shiftKey || corner && key !== "Home" && key !== "End") return;
189
520
  const model = this.#table.model;
@@ -199,26 +530,13 @@ var ArgusTable = class ArgusTable extends LitElement {
199
530
  const columns = model.columns;
200
531
  let pos = layout.position(from);
201
532
  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
533
  let col = columns.findIndex((c) => c.id === id);
210
534
  switch (key) {
211
535
  case "PageDown":
212
- if (layout.body > 0 && pos <= bodyLast) pos = Math.min(Math.max(pos, bodyFirst - 1) + page, bodyLast);
213
- break;
214
536
  case "PageUp":
215
- if (layout.body > 0 && pos > bodyFirst) pos = Math.max(Math.min(pos, bodyLast + 1) - page, bodyFirst);
216
- break;
217
537
  case "ArrowDown":
218
- pos = Math.min(pos + 1, last);
219
- break;
220
538
  case "ArrowUp":
221
- pos = Math.max(pos - 1, 0);
539
+ pos = this.#verticalStep(key, pos, layout, scroller);
222
540
  break;
223
541
  case "ArrowRight":
224
542
  col += this.#direction();
@@ -230,7 +548,7 @@ var ArgusTable = class ArgusTable extends LitElement {
230
548
  col = 0;
231
549
  if (corner) {
232
550
  pos = 0;
233
- scroller.scrollTop = 0;
551
+ this.#virtual.scrollToEdge("start");
234
552
  }
235
553
  break;
236
554
  case "End":
@@ -241,8 +559,8 @@ var ArgusTable = class ArgusTable extends LitElement {
241
559
  }
242
560
  event.preventDefault();
243
561
  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") scroller.scrollTop = scroller.scrollHeight;
562
+ if (key.startsWith("Page") && from.section === "body" && row.section === "body") this.#virtual.scrollBy(this.#virtual.startOf(row.index) - this.#virtual.startOf(from.index));
563
+ if (corner && key === "End" && row.section === "bottom") this.#virtual.scrollToEdge("end");
246
564
  const target = columns[Math.min(Math.max(col, 0), columns.length - 1)];
247
565
  if (target) this.#moveTo(row, target.id, model);
248
566
  };
@@ -277,125 +595,62 @@ var ArgusTable = class ArgusTable extends LitElement {
277
595
  }
278
596
  };
279
597
  this.#onClick = (event) => {
280
- this.#activate(event.composedPath());
598
+ const path = event.composedPath();
599
+ this.#activate(path);
600
+ const target = this.#mode() === "none" ? void 0 : this.#selectableRowOf(path);
601
+ if (!target) return;
602
+ const inSelectionCell = path.some((t) => t instanceof HTMLElement && t.dataset.col === "--argus-selection");
603
+ this.#clickRow(target, event, inSelectionCell || event.ctrlKey || event.metaKey);
281
604
  };
605
+ this.#checkboxHandlers = {
606
+ row: (event) => {
607
+ const index = Number(event.currentTarget.dataset.row);
608
+ const id = this.#table.model.rowAt(index)?.id;
609
+ if (id !== void 0) this.#clickRow({
610
+ id,
611
+ index
612
+ }, event, true);
613
+ this.requestUpdate();
614
+ },
615
+ all: () => {
616
+ this.#toggleAll();
617
+ this.requestUpdate();
618
+ },
619
+ press: (event) => {
620
+ event.preventDefault();
621
+ event.currentTarget.parentElement?.focus({ preventScroll: true });
622
+ }
623
+ };
624
+ this.#onMousedown = (event) => {
625
+ if (!event.shiftKey || event.button !== 0 || this.#mode() !== "multi") return;
626
+ const path = event.composedPath();
627
+ if (!this.#selectableRowOf(path)) return;
628
+ event.preventDefault();
629
+ for (const target of path) {
630
+ const cell = gridCell(target);
631
+ if (cell) {
632
+ cell.focus({ preventScroll: true });
633
+ return;
634
+ }
635
+ }
636
+ };
637
+ for (const type of [
638
+ "wheel",
639
+ "touchstart",
640
+ "pointerdown",
641
+ "keydown"
642
+ ]) this.addEventListener(type, this.#dropScrollTarget, { passive: true });
282
643
  }
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;
644
+ #dropScrollTarget;
395
645
  connectedCallback() {
396
646
  super.connectedCallback();
397
647
  if (!this.hasAttribute("role")) this.setAttribute("role", "grid");
398
648
  if (this.hasUpdated) this.#observeProbe();
649
+ if (this.#lightPart && !this.#lightConnected) {
650
+ this.#lightConnected = true;
651
+ this.#lightPart.setConnected(true);
652
+ flushContent(this);
653
+ }
399
654
  this.#unwatchMaxHeight = watchMaxElementHeight(() => {
400
655
  this.requestUpdate();
401
656
  });
@@ -403,6 +658,12 @@ var ArgusTable = class ArgusTable extends LitElement {
403
658
  disconnectedCallback() {
404
659
  super.disconnectedCallback();
405
660
  this.#probeObserver?.disconnect();
661
+ this.#rowMeasure.stop();
662
+ if (this.#lightPart && this.#lightConnected) {
663
+ this.#lightConnected = false;
664
+ this.#lightPart.setConnected(false);
665
+ flushContent(this);
666
+ }
406
667
  this.#unwatchMaxHeight?.();
407
668
  this.#unwatchMaxHeight = void 0;
408
669
  }
@@ -447,33 +708,137 @@ var ArgusTable = class ArgusTable extends LitElement {
447
708
  * would, so `argus-column-resize` fires for each column that changes, and with a
448
709
  * controlled `columnSizing` nothing changes until you set it. Without `ids`, fits
449
710
  * every resizable column; columns with `resizable: false` keep their width.
711
+ * With `virtualizeColumns`, columns out of view render for the measurement.
450
712
  * Resolves once the table has re-rendered.
451
713
  */
452
714
  async autosizeColumns(ids) {
453
715
  await this.updateComplete;
454
716
  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));
717
+ if (this.virtualizeColumns) {
718
+ this.#measureKeep = targets;
719
+ this.requestUpdate();
720
+ await this.updateComplete;
721
+ }
722
+ const widths = measureColumns(this.renderRoot, targets);
723
+ if (this.#measureKeep.length > 0) {
724
+ this.#measureKeep = [];
725
+ this.requestUpdate();
726
+ }
727
+ this.#requestResizes(widths);
456
728
  await this.updateComplete;
457
729
  }
458
730
  /** Scrolls so that the row at `index` (in the current row order) is at the top. */
459
731
  async scrollToRow(index) {
460
732
  await this.updateComplete;
461
733
  this.#virtual.scrollToIndex(index);
734
+ await this.#settled();
735
+ }
736
+ /**
737
+ * Waits for the update, and with `rowHeight: 'auto'` for the ones measured
738
+ * rows request (a few at most: heights don't depend on positions).
739
+ */
740
+ async #settled() {
741
+ let done = await this.updateComplete;
742
+ for (let i = 0; i < 8 && !done && this.rowHeight === "auto"; i++) done = await this.updateComplete;
743
+ }
744
+ /**
745
+ * The state users change, as one object of plain JSON for saving a user's
746
+ * layout: `sorting`, `columnSizing`, `columnOrder`, `columnPinning`,
747
+ * `columnVisibility`, `columnFilters`, `selection`, `quickFilter`, and `scroll`
748
+ * (the row at the top and the horizontal offset). Controlled properties are
749
+ * included with the values you set. The object is a copy: changing it changes
750
+ * nothing in the table.
751
+ */
752
+ getState() {
753
+ const scroller = this.renderRoot.querySelector("[part~=\"table\"]");
754
+ return structuredClone({
755
+ sorting: this.#sorting.value,
756
+ columnSizing: this.#sizing.value,
757
+ columnOrder: this.#order.value,
758
+ columnPinning: this.#pinning.value,
759
+ columnVisibility: this.#visibility.value,
760
+ columnFilters: this.#filters.value,
761
+ selection: this.#selection.value,
762
+ quickFilter: this.#quickFilter,
763
+ scroll: {
764
+ row: this.#virtual.topRow,
765
+ left: Math.abs(scroller?.scrollLeft ?? 0)
766
+ }
767
+ });
768
+ }
769
+ /**
770
+ * Restores state from `getState()`, for example a layout saved in
771
+ * `localStorage`. Only the keys given change; ids of columns that no longer
772
+ * exist are ignored. Properties you control are left alone (set them
773
+ * yourself), and the others stay uncontrolled. No change events fire.
774
+ * `quickFilter` is set like the property: a search box of yours shows it only
775
+ * if you read it back. `scroll` is applied after the rest, so with a
776
+ * `dataSource` the restored rows load where it points. Resolves once the table
777
+ * has re-rendered there.
778
+ */
779
+ async setState(state) {
780
+ this.#restore("sorting", this.#sorting, state.sorting);
781
+ this.#restore("columnSizing", this.#sizing, state.columnSizing);
782
+ this.#restore("columnOrder", this.#order, state.columnOrder);
783
+ this.#restore("columnPinning", this.#pinning, state.columnPinning);
784
+ this.#restore("columnVisibility", this.#visibility, state.columnVisibility);
785
+ this.#restore("columnFilters", this.#filters, state.columnFilters);
786
+ if (this.#restore("selection", this.#selection, state.selection)) this.#anchor = void 0;
787
+ if (state.quickFilter !== void 0) this.quickFilter = state.quickFilter;
788
+ this.#scrollTarget = void 0;
462
789
  await this.updateComplete;
790
+ const { scroll } = state;
791
+ if (!scroll) return;
792
+ this.#virtual.scrollToIndex(scroll.row);
793
+ const count = this.#table.model.rowCount;
794
+ if (this.#data.rows?.totalKnown === false && scroll.row >= count) this.#scrollTarget = {
795
+ row: scroll.row,
796
+ count
797
+ };
798
+ const scroller = this.renderRoot.querySelector("[part~=\"table\"]");
799
+ if (scroller) scroller.scrollLeft = scroll.left * this.#direction();
800
+ await new Promise(requestAnimationFrame);
801
+ await this.#settled();
802
+ }
803
+ /** Scrolls on toward the row `setState()` restored once more rows are counted. */
804
+ #chaseScrollTarget(count) {
805
+ const target = this.#scrollTarget;
806
+ if (!target || target.count === count) return;
807
+ target.count = count;
808
+ this.#virtual.scrollToIndex(target.row);
809
+ if (target.row < count || this.#data.rows?.totalKnown !== false) this.#scrollTarget = void 0;
810
+ }
811
+ /**
812
+ * Applies a restored value to uncontrolled state without an event, as a user
813
+ * change would without the event. Returns whether it changed.
814
+ */
815
+ #restore(name, state, value) {
816
+ if (value === void 0 || state.controlled) return false;
817
+ if (JSON.stringify(value) === JSON.stringify(state.value)) return false;
818
+ const old = state.value;
819
+ state.request(structuredClone(value));
820
+ this.requestUpdate(name, old);
821
+ return true;
463
822
  }
464
823
  willUpdate(changed) {
465
- const columns = this.#effectiveColumns();
824
+ if (changed.has("plugins")) this.#plugins.set(this.plugins);
825
+ const columns = this.#plugins.transform(this.#effectiveColumns());
466
826
  this.#columnsChanged = columns !== this.#columns;
467
827
  this.#columns = columns;
468
- if (changed.has("dataSource") || changed.has("sorting") || changed.has("getRowId")) {
828
+ const filtersChanged = changed.has("columnFilters") || changed.has("quickFilter");
829
+ if (changed.has("dataSource") || changed.has("sorting") || changed.has("getRowId") || filtersChanged) {
469
830
  this.#data.configure({
470
831
  source: this.dataSource,
471
832
  sorting: this.#sorting.value,
833
+ filters: this.#filters.value,
834
+ quickFilter: this.#quickFilter,
472
835
  getRowId: this.getRowId
473
836
  });
474
- if (this.dataSource && this.hasUpdated && changed.has("sorting")) this.#scrollToTop = !changed.has("dataSource");
837
+ if (this.dataSource && this.hasUpdated && (changed.has("sorting") || filtersChanged)) this.#scrollToTop = !changed.has("dataSource");
838
+ this.#scrollTarget = void 0;
475
839
  }
476
- if (this.#columnsChanged || changed.has("data") || changed.has("getRowId") || changed.has("sorting") || changed.has("columnSizing") || changed.has("columnOrder") || changed.has("pinnedRows")) {
840
+ if (filtersChanged && this.hasUpdated) this.#announceRows = true;
841
+ if (this.#columnsChanged || changed.has("data") || changed.has("getRowId") || changed.has("sorting") || changed.has("columnSizing") || changed.has("columnOrder") || changed.has("columnPinning") || changed.has("columnVisibility") || changed.has("columnFilters") || changed.has("quickFilter") || changed.has("pinnedRows")) {
477
842
  if (this.#columnsChanged) this.#autoWidths.sync(columns.filter((c) => c.width === "auto").map((c) => c.id));
478
843
  this.#table.update({
479
844
  columns,
@@ -482,15 +847,23 @@ var ArgusTable = class ArgusTable extends LitElement {
482
847
  sorting: this.#sorting.value,
483
848
  columnSizing: this.#sizing.value,
484
849
  columnOrder: this.#order.value,
850
+ columnPinning: this.#pinning.value,
851
+ columnVisibility: this.#visibility.value,
852
+ columnFilters: this.#filters.value,
853
+ quickFilter: this.#quickFilter,
485
854
  pinnedRows: this.pinnedRows
486
855
  });
487
856
  }
488
857
  const autoWidths = this.#autoWidths.widths;
489
858
  this.#autoWidthsChanged = autoWidths !== this.#pushedAutoWidths;
490
859
  this.#pushedAutoWidths = autoWidths;
860
+ const selectionColumn = this.selectionCheckboxes && this.#mode() !== "none";
861
+ this.#selectionColumnChanged = selectionColumn !== this.#selectionColumn;
862
+ this.#selectionColumn = selectionColumn;
491
863
  this.#table.update({
492
864
  remote: this.#data.rows,
493
- autoWidths
865
+ autoWidths,
866
+ selectionColumn
494
867
  });
495
868
  const model = this.#table.model;
496
869
  this.#reconcileActive(model);
@@ -499,11 +872,19 @@ var ArgusTable = class ArgusTable extends LitElement {
499
872
  const layout = layoutOf(model);
500
873
  this.#internals.ariaRowCount = unknownTotal ? "-1" : String(layout.total);
501
874
  this.#internals.ariaColCount = String(model.columns.length);
502
- const extra = (1 + layout.top + layout.bottom) * this.#rowPx;
875
+ this.#internals.ariaMultiSelectable = this.#mode() === "multi" ? "true" : null;
876
+ const extra = this.#rowPx + this.#pinnedPx("top", layout) + this.#pinnedPx("bottom", layout);
877
+ const auto = this.rowHeight === "auto";
878
+ if (!auto) this.#rowMeasure.stop();
879
+ else if (changed.size > 0 || this.#columnsChanged) this.#rowMeasure.invalidate();
503
880
  this.#virtual.configure({
504
881
  count: model.rowCount,
505
882
  rowHeight: this.#rowPx,
506
- cap: bodyCap(model.rowCount * this.#rowPx, extra),
883
+ cap: (logical) => bodyCap(logical, extra),
884
+ auto: auto ? {
885
+ order: model.rowOrder,
886
+ indexOf: (id) => model.indexOf(id)
887
+ } : void 0,
507
888
  overscan: Math.max(0, this.overscan),
508
889
  rowIdAt: (index) => this.#rowId(model, index),
509
890
  keep: active?.section !== "body" || active.rowId === void 0 ? void 0 : {
@@ -516,6 +897,8 @@ var ArgusTable = class ArgusTable extends LitElement {
516
897
  const model = this.#table.model;
517
898
  const columns = model.columns;
518
899
  this.#window = this.#virtual.computeWindow();
900
+ const rendered = this.#columnWindow.compute(columns, this.virtualizeColumns, this.virtualizeColumns ? this.#keptColumns(model) : []);
901
+ this.#rendered = rendered;
519
902
  const pinnedWidth = (side) => columns.reduce((sum, c) => c.pin === side ? sum + c.width : sum, 0);
520
903
  const pinned = (side) => `${String(pinnedWidth(side))}px`;
521
904
  const layout = layoutOf(model);
@@ -527,6 +910,16 @@ var ArgusTable = class ArgusTable extends LitElement {
527
910
  "--_pin-top-rows": String(layout.top),
528
911
  "--_pin-bottom-rows": String(layout.bottom)
529
912
  };
913
+ const auto = this.rowHeight === "auto";
914
+ if (auto) {
915
+ const key = `${style["--_cols"] ?? ""}|${String(this.#rowPx)}`;
916
+ if (key !== this.#rowsKey) {
917
+ this.#rowsKey = key;
918
+ this.#rowMeasure.invalidate();
919
+ }
920
+ style["--_pin-top-h"] = `${String(this.#pinnedPx("top", layout))}px`;
921
+ style["--_pin-bottom-h"] = `${String(this.#pinnedPx("bottom", layout))}px`;
922
+ }
530
923
  const active = this.#active;
531
924
  const activeIn = (section) => active?.section === section ? {
532
925
  index: active.index,
@@ -536,24 +929,27 @@ var ArgusTable = class ArgusTable extends LitElement {
536
929
  section,
537
930
  index: 0
538
931
  });
539
- if (this.rowHeight !== void 0 && this.rowHeight > 0) style["--_row-height"] = `${String(this.rowHeight)}px`;
932
+ if (typeof this.rowHeight === "number" && this.rowHeight > 0) style["--_row-height"] = `${String(this.rowHeight)}px`;
933
+ const contentPx = this.#rowPx + this.#pinnedPx("top", layout) + this.#pinnedPx("bottom", layout) + (model.rowCount === 0 ? 0 : this.#window.totalSize);
540
934
  return html`<div
541
935
  part="table"
542
936
  role="presentation"
543
937
  style=${styleMap(style)}
544
938
  ?data-dragging=${this.#drag !== void 0}
939
+ ?data-auto-rows=${auto}
545
940
  @keydown=${this.#onKeydown}
546
941
  @focusin=${this.#onFocusin}
942
+ @mousedown=${this.#onMousedown}
547
943
  @click=${this.#onClick}
548
944
  >
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"))}
945
+ ${renderHeader(rendered, this.#headerHandlers, activeIn("header")?.col, this.#drag, this.#checkAllOf(model))}
946
+ ${renderPinnedRows("top", layout.top, rendered, firstIndex("top"), activeIn("top"))}
947
+ ${model.rowCount === 0 ? html`<div part="empty"><slot name="empty">No rows</slot></div>` : renderBody(this.#window.rows, rendered, this.#window.totalSize, activeIn("body"), firstIndex("body"), this.#loading(), this.#selectedAt(model), this.#selectionColumn ? this.#checkboxHandlers : void 0, auto)}
948
+ ${renderPinnedRows("bottom", layout.bottom, rendered, firstIndex("bottom"), activeIn("bottom"))}
553
949
  <div class="probe" aria-hidden="true"></div>
554
950
  </div>
555
- ${pinShadow("start", pinnedWidth("start"), layout.total - 1, this.#rowPx)}
556
- ${pinShadow("end", pinnedWidth("end"), layout.total - 1, this.#rowPx)}`;
951
+ ${pinShadow("start", pinnedWidth("start"), contentPx)}
952
+ ${pinShadow("end", pinnedWidth("end"), contentPx)}`;
557
953
  }
558
954
  firstUpdated() {
559
955
  this.#observeProbe();
@@ -561,15 +957,20 @@ var ArgusTable = class ArgusTable extends LitElement {
561
957
  }
562
958
  updated(changed) {
563
959
  const model = this.#table.model;
564
- renderLightDom(this, model, this.#window.rows, this.#lightDomEnd());
960
+ const part = renderLightDom(this, model, this.#window.rows, this.#rendered, this.#lightDomEnd());
961
+ if (this.#lightPart && this.#lightPart !== part) this.#lightPart.setConnected(false);
962
+ this.#lightPart = part;
963
+ flushContent(this);
565
964
  this.#measureAutoWidths(model);
965
+ const resized = this.rowHeight === "auto" && this.#measureRows();
566
966
  if (this.#scrollToTop) {
567
967
  this.#scrollToTop = false;
568
968
  const scroller = this.renderRoot.querySelector("[part~=\"table\"]");
569
969
  if (scroller) scroller.scrollTop = 0;
570
970
  }
571
- if (this.#columnsChanged || changed.has("columnSizing") || changed.has("columnOrder") || this.#autoWidthsChanged) this.#pinScroll.measure();
572
- if (this.#focusPending) {
971
+ this.#chaseScrollTarget(model.rowCount);
972
+ if (this.#columnsChanged || changed.has("columnSizing") || changed.has("columnOrder") || changed.has("columnPinning") || changed.has("columnVisibility") || this.#autoWidthsChanged || this.#selectionColumnChanged) this.#pinScroll.measure();
973
+ if (this.#focusPending && !resized) {
573
974
  this.#focusPending = false;
574
975
  this.#focusActive();
575
976
  }
@@ -588,11 +989,35 @@ var ArgusTable = class ArgusTable extends LitElement {
588
989
  this.#appliedReorders.clear();
589
990
  for (const detail of details) fire(this, COLUMN_REORDER, detail);
590
991
  }
992
+ if (this.#appliedPins.size > 0) {
993
+ const details = [...this.#appliedPins.values()];
994
+ this.#appliedPins.clear();
995
+ for (const detail of details) fire(this, COLUMN_PIN, detail);
996
+ }
997
+ if (this.#appliedHides.size > 0) {
998
+ const details = [...this.#appliedHides.values()];
999
+ this.#appliedHides.clear();
1000
+ for (const detail of details) fire(this, COLUMN_HIDE, detail);
1001
+ }
1002
+ if (this.#appliedFilters.size > 0) {
1003
+ const details = [...this.#appliedFilters.values()];
1004
+ this.#appliedFilters.clear();
1005
+ for (const detail of details) fire(this, COLUMN_FILTER, detail);
1006
+ }
1007
+ if (this.#announceRows) {
1008
+ this.#announceRows = false;
1009
+ this.#announceRowCount(model);
1010
+ }
591
1011
  if (this.#appliedSort) {
592
1012
  const sorting = this.#appliedSort;
593
1013
  this.#appliedSort = void 0;
594
1014
  fire(this, SORT_CHANGE, { sorting });
595
1015
  }
1016
+ if (this.#appliedSelection) {
1017
+ const selection = this.#appliedSelection;
1018
+ this.#appliedSelection = void 0;
1019
+ fire(this, ROW_SELECT, { selection });
1020
+ }
596
1021
  const { range } = this.#window;
597
1022
  const key = range ? `${String(range.start)}:${String(range.end)}` : "";
598
1023
  if (range && key !== this.#lastRange) {
@@ -609,6 +1034,22 @@ var ArgusTable = class ArgusTable extends LitElement {
609
1034
  for (const detail of this.#data.takeErrors()) fire(this, LOAD_ERROR, detail);
610
1035
  }
611
1036
  /**
1037
+ * Columns to render wherever they are, with `virtualizeColumns`: the active
1038
+ * cell's (its tab stop and focus survive scrolling sideways, and keyboard
1039
+ * moves land on a rendered cell), one being dragged, auto widths still to
1040
+ * measure, the targets of `autosizeColumns()`, and with a `dataSource` the
1041
+ * first data column, which holds the Retry buttons.
1042
+ */
1043
+ #keptColumns(model) {
1044
+ return [
1045
+ this.#active?.col,
1046
+ this.#drag?.columnId,
1047
+ this.dataSource ? model.columns.find((c) => c.kind === "data")?.id : void 0,
1048
+ ...this.#autoWidths.pending(model.columns),
1049
+ ...this.#measureKeep
1050
+ ];
1051
+ }
1052
+ /**
612
1053
  * Measures the `width: 'auto'` columns once loaded body rows render, or right
613
1054
  * away when there are no rows (the header and pinned rows only). While a data
614
1055
  * source's first rows load, they keep the default width. A changed width
@@ -671,14 +1112,251 @@ var ArgusTable = class ArgusTable extends LitElement {
671
1112
  * cell (an input, a button) are left to that content.
672
1113
  */
673
1114
  #onKeydown;
1115
+ /**
1116
+ * The grid position ArrowUp/ArrowDown (one row) or PageUp/PageDown (a page of
1117
+ * the body) moves to from `pos`. PageDown from above the body lands a page
1118
+ * into it and stops at its last row; PageUp stays in the body.
1119
+ */
1120
+ #verticalStep(key, pos, layout, scroller) {
1121
+ const bodyFirst = layout.position({
1122
+ section: "body",
1123
+ index: 0
1124
+ });
1125
+ const bodyLast = bodyFirst + layout.body - 1;
1126
+ const page = this.#pageRows(key, pos, layout, scroller);
1127
+ switch (key) {
1128
+ case "PageDown": return layout.body > 0 && pos <= bodyLast ? Math.min(Math.max(pos, bodyFirst - 1) + page, bodyLast) : pos;
1129
+ case "PageUp": return layout.body > 0 && pos > bodyFirst ? Math.max(Math.min(pos, bodyLast + 1) - page, bodyFirst) : pos;
1130
+ case "ArrowDown": return Math.min(pos + 1, layout.total - 1);
1131
+ case "ArrowUp": return Math.max(pos - 1, 0);
1132
+ }
1133
+ }
1134
+ /** How many rows PageUp/PageDown move from grid position `pos`. */
1135
+ #pageRows(key, pos, layout, scroller) {
1136
+ if (this.rowHeight !== "auto") {
1137
+ const fit = Math.floor(scroller.clientHeight / this.#rowPx + .01);
1138
+ return Math.max(1, fit - 1 - layout.top - layout.bottom);
1139
+ }
1140
+ const view = scroller.clientHeight - this.#rowPx - this.#pinnedPx("top", layout) - this.#pinnedPx("bottom", layout);
1141
+ const bodyFirst = layout.position({
1142
+ section: "body",
1143
+ index: 0
1144
+ });
1145
+ const from = Math.min(Math.max(pos - bodyFirst, 0), Math.max(layout.body - 1, 0));
1146
+ return this.#virtual.pageRows(from, key === "PageDown", view);
1147
+ }
1148
+ /**
1149
+ * Selection keys, with a `selectionMode`: Space (or Shift+Space) toggles the
1150
+ * focused body row; in `multi` mode, Shift+ArrowUp/ArrowDown and
1151
+ * Shift+PageUp/PageDown move focus through the body and select from the
1152
+ * anchor to it, and Ctrl/Cmd+A selects every row. `true` if it took the key.
1153
+ */
1154
+ #selectionKey(event, cell, col, inHeader, scroller) {
1155
+ const mode = this.#mode();
1156
+ if (mode === "none" || event.altKey) return false;
1157
+ const { key } = event;
1158
+ const command = event.ctrlKey || event.metaKey;
1159
+ const model = this.#table.model;
1160
+ if (command && !event.shiftKey && key.toLowerCase() === "a") {
1161
+ if (mode !== "multi") return false;
1162
+ event.preventDefault();
1163
+ this.#selectAll();
1164
+ return true;
1165
+ }
1166
+ if (command) return false;
1167
+ if (inHeader) {
1168
+ if (key !== " " || col !== "--argus-selection" || mode !== "multi") return false;
1169
+ event.preventDefault();
1170
+ this.#toggleAll();
1171
+ return true;
1172
+ }
1173
+ const layout = layoutOf(model);
1174
+ const from = gridRowOf(cell.parentElement, layout);
1175
+ if (from?.section !== "body") return false;
1176
+ if (key === " ") {
1177
+ event.preventDefault();
1178
+ const id = model.rowAt(from.index)?.id;
1179
+ if (id !== void 0) this.#requestSelection(toggleRow(this.#selection.value, mode, id));
1180
+ return true;
1181
+ }
1182
+ if (!event.shiftKey || mode !== "multi" || key !== "ArrowDown" && key !== "ArrowUp" && key !== "PageDown" && key !== "PageUp") return false;
1183
+ event.preventDefault();
1184
+ const bodyFirst = layout.position({
1185
+ section: "body",
1186
+ index: 0
1187
+ });
1188
+ const pos = this.#verticalStep(key, layout.position(from), layout, scroller);
1189
+ const to = layout.at(Math.min(Math.max(pos, bodyFirst), bodyFirst + layout.body - 1));
1190
+ if (to?.section !== "body" || to.index === from.index) return true;
1191
+ if (key.startsWith("Page")) this.#virtual.scrollBy(this.#virtual.startOf(to.index) - this.#virtual.startOf(from.index));
1192
+ const fromId = model.rowAt(from.index)?.id;
1193
+ const anchor = this.#anchorAt(model) ?? (fromId === void 0 ? void 0 : {
1194
+ id: fromId,
1195
+ index: from.index
1196
+ });
1197
+ if (anchor) {
1198
+ const idAt = (index) => model.rowAt(index)?.id;
1199
+ const selection = selectRange([], anchor.index, to.index, idAt, false);
1200
+ this.#requestSelection({
1201
+ selection,
1202
+ anchor: anchor.id
1203
+ });
1204
+ }
1205
+ this.#moveTo(to, col, model);
1206
+ return true;
1207
+ }
674
1208
  /** Focus that lands in a cell (a click, or content inside it) makes that cell active. */
675
1209
  #onFocusin;
676
- /** `argus-cell-click`, then `argus-row-click`, for clicks in the body. */
1210
+ /**
1211
+ * `argus-cell-click`, then `argus-row-click`, for clicks in the body; then,
1212
+ * with a `selectionMode`, the click selects its row.
1213
+ */
677
1214
  #onClick;
1215
+ /** Selects for a click on a row, through `clickSelection`. */
1216
+ #clickRow(target, event, toggle) {
1217
+ const model = this.#table.model;
1218
+ this.#requestSelection(clickSelection(this.#selection.value, this.#mode(), target, this.#anchorAt(model), {
1219
+ toggle,
1220
+ range: event.shiftKey
1221
+ }, (index) => model.rowAt(index)?.id));
1222
+ }
1223
+ /**
1224
+ * The checkboxes' handlers. The table owns their state: a click asks for a new
1225
+ * selection, and the render that always follows sets every box back to the
1226
+ * selection (`live()`), so a change a controlled host turns down unticks it
1227
+ * again. Not `preventDefault()`: for a real click the browser runs that render
1228
+ * inside the dispatch, then undoes the toggle after it, over the render. A
1229
+ * press leaves focus on the cell, so the grid's keys keep working.
1230
+ */
1231
+ #checkboxHandlers;
1232
+ /** Selects every selectable row, keeping the others (with a `dataSource`: the loaded ones). */
1233
+ #selectAll() {
1234
+ const ids = this.#table.model.selectableIds();
1235
+ this.#requestSelection({
1236
+ selection: selectAll(this.#selection.value, ids),
1237
+ anchor: this.#anchor
1238
+ });
1239
+ }
1240
+ /** The select-all checkbox: clears the selection when every row is selected, else selects all. */
1241
+ #toggleAll() {
1242
+ if (this.#checkAllState(this.#table.model) === "all") this.#requestSelection({
1243
+ selection: [],
1244
+ anchor: this.#anchor
1245
+ });
1246
+ else this.#selectAll();
1247
+ }
1248
+ /** The select-all checkbox, in the checkbox column of a `multi` table. */
1249
+ #checkAllOf(model) {
1250
+ if (!this.#selectionColumn || this.#mode() !== "multi") return void 0;
1251
+ return {
1252
+ state: this.#checkAllState(model),
1253
+ handlers: this.#checkboxHandlers
1254
+ };
1255
+ }
1256
+ /**
1257
+ * Whether all, some or none of the selectable rows are selected. With `data`
1258
+ * it's worked out once per `selection`, `data` and `getRowId`, not on scroll;
1259
+ * with a `dataSource` every time, over the loaded rows, which eviction keeps few.
1260
+ */
1261
+ #checkAllState(model) {
1262
+ const count = () => {
1263
+ const ids = model.selectableIds();
1264
+ const selected = this.#selectedSet();
1265
+ const n = ids.reduce((sum, id) => selected.has(id) ? sum + 1 : sum, 0);
1266
+ if (n === 0) return "none";
1267
+ return n === ids.length ? "all" : "some";
1268
+ };
1269
+ if (this.#data.rows) return count();
1270
+ const key = [
1271
+ this.#selection.value,
1272
+ this.data,
1273
+ this.getRowId,
1274
+ this.#columns,
1275
+ this.#filters.value,
1276
+ this.#quickFilter,
1277
+ this.#visibility.value
1278
+ ];
1279
+ const cached = this.#checkAll;
1280
+ if (cached?.key.length !== key.length || key.some((k, i) => k !== cached.key[i])) this.#checkAll = {
1281
+ key,
1282
+ state: count()
1283
+ };
1284
+ return this.#checkAll?.state ?? "none";
1285
+ }
1286
+ /**
1287
+ * A Shift+press on a row in `multi` mode selects a range of rows, not the
1288
+ * page's text from the last click: no text selection, and the cell still
1289
+ * takes focus.
1290
+ */
1291
+ #onMousedown;
1292
+ /** `selectionMode`, with anything unknown read as `none`. */
1293
+ #mode() {
1294
+ const mode = this.selectionMode;
1295
+ return mode === "single" || mode === "multi" ? mode : "none";
1296
+ }
1297
+ /** Whether each rendered body row is selected; `undefined` without a `selectionMode`. */
1298
+ #selectedAt(model) {
1299
+ if (this.#mode() === "none") return void 0;
1300
+ const ids = this.#selectedSet();
1301
+ return (index) => {
1302
+ const id = model.rowAt(index)?.id;
1303
+ return id === void 0 ? void 0 : ids.has(id);
1304
+ };
1305
+ }
1306
+ /** `selection` as a set, rebuilt only when `selection` changes. */
1307
+ #selectedSet() {
1308
+ const selection = this.#selection.value;
1309
+ if (this.#selectedIds?.of !== selection) this.#selectedIds = {
1310
+ of: selection,
1311
+ ids: new Set(selection)
1312
+ };
1313
+ return this.#selectedIds.ids;
1314
+ }
1315
+ /**
1316
+ * The loaded body row a pointer event is on, as a selection target. None for
1317
+ * the header, pinned rows, rows still loading, and presses on interactive
1318
+ * content in a cell (a button, a link, an input).
1319
+ */
1320
+ #selectableRowOf(path) {
1321
+ const row = path.find((t) => t instanceof HTMLElement && t.getAttribute("role") === "row");
1322
+ if (!row || fromInteractive(path, row)) return void 0;
1323
+ const model = this.#table.model;
1324
+ const at = gridRowOf(row, layoutOf(model));
1325
+ if (at?.section !== "body") return void 0;
1326
+ const id = model.rowAt(at.index)?.id;
1327
+ return id === void 0 ? void 0 : {
1328
+ id,
1329
+ index: at.index
1330
+ };
1331
+ }
1332
+ /** The anchor row and where it is now; `undefined` if it isn't in the rows (or not loaded). */
1333
+ #anchorAt(model) {
1334
+ const id = this.#anchor;
1335
+ const index = id === void 0 ? void 0 : model.indexOf(id);
1336
+ return id === void 0 || index === void 0 ? void 0 : {
1337
+ id,
1338
+ index
1339
+ };
1340
+ }
1341
+ /**
1342
+ * Changes the selection through the controlled/uncontrolled rules, and moves
1343
+ * the anchor. A step that leaves the same rows selected fires nothing.
1344
+ */
1345
+ #requestSelection(step) {
1346
+ if (!step) return;
1347
+ this.#anchor = step.anchor;
1348
+ const old = this.#selection.value;
1349
+ if (sameSelection(step.selection, old)) return;
1350
+ const { value, applied } = this.#selection.request(step.selection);
1351
+ if (applied) {
1352
+ this.#appliedSelection = value;
1353
+ this.requestUpdate("selection", old);
1354
+ } else fire(this, ROW_SELECT, { selection: value });
1355
+ }
678
1356
  /**
679
1357
  * Fires `argus-cell-click` (when a cell is on the path), then `argus-row-click`,
680
1358
  * 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.
1359
+ * it. Rows that haven't loaded, the header and the checkbox column fire nothing.
682
1360
  */
683
1361
  #activate(path) {
684
1362
  let columnId;
@@ -686,6 +1364,7 @@ var ArgusTable = class ArgusTable extends LitElement {
686
1364
  if (!(target instanceof HTMLElement)) continue;
687
1365
  const role = target.getAttribute("role");
688
1366
  if (role === "gridcell") columnId = target.dataset.col;
1367
+ if (columnId === "--argus-selection") return;
689
1368
  if (role !== "row") continue;
690
1369
  const model = this.#table.model;
691
1370
  const at = gridRowOf(target, layoutOf(model));
@@ -750,10 +1429,10 @@ var ArgusTable = class ArgusTable extends LitElement {
750
1429
  const cell = this.renderRoot.querySelector(`[role='row'][aria-rowindex="${String(index)}"] > [data-col="${col}"]`);
751
1430
  if (!cell) return;
752
1431
  cell.focus({ preventScroll: true });
753
- if (active.section === "body" && this.#virtual.scaled) {
1432
+ if (active.section === "body" && (this.#virtual.scaled || this.rowHeight === "auto")) {
754
1433
  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)) {
1434
+ const before = this.#rowPx + this.#pinnedPx("top", layout);
1435
+ if (this.#virtual.reveal(active.index, before, this.#pinnedPx("bottom", layout))) {
757
1436
  this.#focusPending = true;
758
1437
  return;
759
1438
  }
@@ -783,9 +1462,9 @@ var ArgusTable = class ArgusTable extends LitElement {
783
1462
  rowId: void 0,
784
1463
  col: first.id
785
1464
  };
786
- const col = columns.some((c) => c.id === prev.col) ? prev.col : first.id;
1465
+ const col = columns.some((c) => c.id === prev.col) ? prev.col : this.#columnInPlaceOf(model, prev.col, first.id);
787
1466
  let { section, index, rowId } = prev;
788
- let moved = false;
1467
+ let moved = col !== prev.col && this.#active !== void 0;
789
1468
  if (section === "body" && rowId !== void 0) {
790
1469
  const found = model.indexOf(rowId);
791
1470
  const unloaded = index < model.rowCount && model.rowAt(index) === void 0;
@@ -816,6 +1495,18 @@ var ArgusTable = class ArgusTable extends LitElement {
816
1495
  };
817
1496
  }
818
1497
  /**
1498
+ * The shown column that takes a hidden column's place: the next one in the
1499
+ * column order, else the one before it. `fallback` for a column that is gone
1500
+ * altogether.
1501
+ */
1502
+ #columnInPlaceOf(model, id, fallback) {
1503
+ const all = model.allColumns;
1504
+ const at = all.findIndex((c) => c.id === id);
1505
+ if (at < 0) return fallback;
1506
+ const shown = (c) => !c.hidden;
1507
+ return (all.slice(at + 1).find(shown) ?? all.slice(0, at).findLast(shown))?.id ?? fallback;
1508
+ }
1509
+ /**
819
1510
  * The width a resize starts from: the latest requested width (key presses can
820
1511
  * come faster than renders), else the model's fixed width, else the rendered
821
1512
  * width of a flex column.
@@ -863,7 +1554,79 @@ var ArgusTable = class ArgusTable extends LitElement {
863
1554
  } else for (const detail of details) fire(this, COLUMN_RESIZE, detail);
864
1555
  }
865
1556
  /**
866
- * Moves a column to position `to` in the whole order (within its pin group),
1557
+ * Pins or unpins a column through the controlled/uncontrolled rules. Starts
1558
+ * from the latest requested pins, so requests can come faster than renders.
1559
+ */
1560
+ #requestPin(columnId, pin) {
1561
+ const old = this.#pinning.value;
1562
+ const next = this.#table.model.nextPinning(columnId, pin, old);
1563
+ if (!next) return;
1564
+ const { value, applied } = this.#pinning.request(next);
1565
+ if (applied) {
1566
+ for (const detail of this.#appliedPins.values()) detail.pinning = value;
1567
+ this.#appliedPins.set(columnId, {
1568
+ columnId,
1569
+ pin,
1570
+ pinning: value
1571
+ });
1572
+ this.requestUpdate("columnPinning", old);
1573
+ } else fire(this, COLUMN_PIN, {
1574
+ columnId,
1575
+ pin,
1576
+ pinning: value
1577
+ });
1578
+ }
1579
+ /** Hides or shows a column through the controlled/uncontrolled rules. */
1580
+ #requestHide(columnId, hidden) {
1581
+ const old = this.#visibility.value;
1582
+ const next = this.#table.model.nextVisibility(columnId, hidden, old);
1583
+ if (!next) return;
1584
+ const { value, applied } = this.#visibility.request(next);
1585
+ if (applied) {
1586
+ for (const detail of this.#appliedHides.values()) detail.visibility = value;
1587
+ this.#appliedHides.set(columnId, {
1588
+ columnId,
1589
+ hidden,
1590
+ visibility: value
1591
+ });
1592
+ this.requestUpdate("columnVisibility", old);
1593
+ } else fire(this, COLUMN_HIDE, {
1594
+ columnId,
1595
+ hidden,
1596
+ visibility: value
1597
+ });
1598
+ }
1599
+ /** Sets or clears a column's filter through the controlled/uncontrolled rules. */
1600
+ #requestFilter(columnId, filter) {
1601
+ const old = this.#filters.value;
1602
+ const next = this.#table.model.nextFilters(columnId, filter, old);
1603
+ if (!next) return;
1604
+ const { value, applied } = this.#filters.request(next);
1605
+ if (applied) {
1606
+ for (const detail of this.#appliedFilters.values()) detail.filters = value;
1607
+ this.#appliedFilters.set(columnId, {
1608
+ columnId,
1609
+ filter,
1610
+ filters: value
1611
+ });
1612
+ this.requestUpdate("columnFilters", old);
1613
+ } else fire(this, COLUMN_FILTER, {
1614
+ columnId,
1615
+ filter,
1616
+ filters: value
1617
+ });
1618
+ }
1619
+ /**
1620
+ * Says how many rows the filters leave ("42 rows"). With a `dataSource` the count
1621
+ * isn't known until the first page answers, so nothing is said.
1622
+ */
1623
+ #announceRowCount(model) {
1624
+ if (this.#data.rows) return;
1625
+ const n = model.rowCount;
1626
+ announce(this.ownerDocument, n === 1 ? "1 row" : `${String(n)} rows`);
1627
+ }
1628
+ /**
1629
+ * Moves a column to position `to` among the shown data columns (within its pin group),
867
1630
  * through the controlled/uncontrolled rules. Starts from the latest requested
868
1631
  * order, so moves can come faster than renders. `false` if it can't move there.
869
1632
  */
@@ -888,7 +1651,8 @@ var ArgusTable = class ArgusTable extends LitElement {
888
1651
  /** Alt+Shift+Arrow on a header: one step toward the start (-1) or the end (1). */
889
1652
  #moveByKey(columnId, step) {
890
1653
  const model = this.#table.model;
891
- const from = model.orderOf(this.#order.value).indexOf(columnId);
1654
+ const shown = new Set(model.columns.map((c) => c.id));
1655
+ const from = model.orderOf(this.#order.value).filter((id) => shown.has(id)).indexOf(columnId);
892
1656
  const view = model.columns.find((c) => c.id === columnId);
893
1657
  if (from < 0 || !view) return;
894
1658
  if (!this.#requestReorder(columnId, from + step)) return;
@@ -946,10 +1710,13 @@ var ArgusTable = class ArgusTable extends LitElement {
946
1710
  }
947
1711
  });
948
1712
  }
949
- /** The rendered headers of one pin group, with their positions in the whole order. */
1713
+ /**
1714
+ * The rendered headers of one pin group, with their positions in the whole
1715
+ * order (which leaves out the checkbox column).
1716
+ */
950
1717
  #dropSlots(pin) {
951
1718
  const slots = [];
952
- this.#table.model.columns.forEach((c, index) => {
1719
+ this.#table.model.columns.filter((c) => c.kind === "data").forEach((c, index) => {
953
1720
  if (c.pin !== pin) return;
954
1721
  const cell = this.renderRoot.querySelector(`[role='columnheader'][data-col="${CSS.escape(c.id)}"]`);
955
1722
  if (cell) slots.push({
@@ -974,7 +1741,24 @@ var ArgusTable = class ArgusTable extends LitElement {
974
1741
  /** asc → desc → unsorted, through the controlled/uncontrolled rules. */
975
1742
  #requestSort(columnId) {
976
1743
  const next = this.#table.model.nextSorting(columnId);
977
- if (!next) return;
1744
+ if (next) this.#applySorting(next);
1745
+ }
1746
+ /**
1747
+ * A plugin's sort: by a column in a direction, or (`null`) cleared if it is by
1748
+ * that column. Only sortable columns, and only a change.
1749
+ */
1750
+ #setSort(columnId, direction) {
1751
+ const view = this.#table.model.columns.find((c) => c.id === columnId);
1752
+ if (view?.kind !== "data" || !view.sortable) return;
1753
+ if (direction === (view.sorted === false ? null : view.sorted)) return;
1754
+ if (direction === null) this.#applySorting([]);
1755
+ else this.#applySorting([{
1756
+ id: columnId,
1757
+ desc: direction === "desc"
1758
+ }]);
1759
+ }
1760
+ /** A new sort order, through the controlled/uncontrolled rules. */
1761
+ #applySorting(next) {
978
1762
  const old = this.#sorting.value;
979
1763
  const { value, applied } = this.#sorting.request(next);
980
1764
  if (applied) {
@@ -982,6 +1766,49 @@ var ArgusTable = class ArgusTable extends LitElement {
982
1766
  this.requestUpdate("sorting", old);
983
1767
  } else fire(this, SORT_CHANGE, { sorting: value });
984
1768
  }
1769
+ /**
1770
+ * A data column as plugins see it. Header functions call this on every
1771
+ * render, so the views are indexed by id once per new column list.
1772
+ */
1773
+ #columnInfo(id) {
1774
+ const columns = this.#table.model.allColumns;
1775
+ if (this.#columnsById?.of !== columns) this.#columnsById = {
1776
+ of: columns,
1777
+ byId: new Map(columns.map((c) => [c.id, c]))
1778
+ };
1779
+ const view = this.#columnsById.byId.get(id);
1780
+ if (view?.kind !== "data") return void 0;
1781
+ return {
1782
+ id,
1783
+ sortable: view.sortable,
1784
+ resizable: view.resizable,
1785
+ pinnable: view.pinnable,
1786
+ hideable: view.hideable,
1787
+ pin: view.pin,
1788
+ hidden: view.hidden,
1789
+ width: view.width,
1790
+ sorted: view.sorted === false ? void 0 : view.sorted,
1791
+ filter: view.filter,
1792
+ filterValue: this.#filters.value[id]
1793
+ };
1794
+ }
1795
+ /** The header or cell an event happened in, for plugins. */
1796
+ #cellTarget(event) {
1797
+ for (const target of event.composedPath()) {
1798
+ const cell = gridCell(target);
1799
+ const columnId = cell?.dataset.col;
1800
+ if (!cell || columnId === void 0) continue;
1801
+ const model = this.#table.model;
1802
+ const row = cell.getAttribute("role") === "columnheader" ? HEADER : gridRowOf(cell.parentElement, layoutOf(model));
1803
+ if (!row) return void 0;
1804
+ return {
1805
+ section: row.section,
1806
+ columnId,
1807
+ rowIndex: row.index,
1808
+ rowId: this.#gridRowId(model, row)
1809
+ };
1810
+ }
1811
+ }
985
1812
  #observeProbe() {
986
1813
  const probe = this.renderRoot.querySelector(".probe");
987
1814
  if (!probe) return;
@@ -990,6 +1817,22 @@ var ArgusTable = class ArgusTable extends LitElement {
990
1817
  });
991
1818
  this.#probeObserver.observe(probe);
992
1819
  }
1820
+ /** The height of the rows pinned to one side: one row each, or measured with `rowHeight: 'auto'`. */
1821
+ #pinnedPx(side, layout) {
1822
+ const count = side === "top" ? layout.top : layout.bottom;
1823
+ if (this.rowHeight !== "auto" || count === 0) return count * this.#rowPx;
1824
+ return Math.max(this.#rowMeasure.pinned[side], count * this.#rowPx);
1825
+ }
1826
+ /**
1827
+ * `rowHeight: 'auto'`: reads the heights of the rendered rows, after their
1828
+ * content has rendered. Returns whether any changed (an update is requested).
1829
+ */
1830
+ #measureRows() {
1831
+ const { rows, pinnedChanged } = this.#rowMeasure.read(this.renderRoot);
1832
+ const resized = this.#virtual.measureRows(rows);
1833
+ if (pinnedChanged) this.requestUpdate();
1834
+ return resized || pinnedChanged;
1835
+ }
993
1836
  /** Reads the resolved row height, so themes and density change the virtualizer too. */
994
1837
  #measureRowHeight() {
995
1838
  const px = this.renderRoot.querySelector(".probe")?.getBoundingClientRect().height ?? 0;
@@ -1006,13 +1849,29 @@ __decorate([property({ attribute: false })], ArgusTable.prototype, "pinnedRows",
1006
1849
  __decorate([property({ attribute: false })], ArgusTable.prototype, "getRowId", void 0);
1007
1850
  __decorate([property({
1008
1851
  attribute: "row-height",
1009
- type: Number
1852
+ converter: rowHeightConverter
1010
1853
  })], ArgusTable.prototype, "rowHeight", void 0);
1011
1854
  __decorate([property({ reflect: true })], ArgusTable.prototype, "density", void 0);
1012
1855
  __decorate([property({ type: Number })], ArgusTable.prototype, "overscan", null);
1856
+ __decorate([property({
1857
+ attribute: "virtualize-columns",
1858
+ type: Boolean
1859
+ })], ArgusTable.prototype, "virtualizeColumns", void 0);
1013
1860
  __decorate([property({ attribute: false })], ArgusTable.prototype, "sorting", null);
1014
1861
  __decorate([property({ attribute: false })], ArgusTable.prototype, "columnSizing", null);
1015
1862
  __decorate([property({ attribute: false })], ArgusTable.prototype, "columnOrder", null);
1863
+ __decorate([property({ attribute: false })], ArgusTable.prototype, "columnPinning", null);
1864
+ __decorate([property({ attribute: false })], ArgusTable.prototype, "columnVisibility", null);
1865
+ __decorate([property({ attribute: false })], ArgusTable.prototype, "columnFilters", null);
1866
+ __decorate([property({ attribute: "quick-filter" })], ArgusTable.prototype, "quickFilter", null);
1867
+ __decorate([property({ attribute: "selection-mode" })], ArgusTable.prototype, "selectionMode", void 0);
1868
+ __decorate([property({
1869
+ attribute: "selection-checkboxes",
1870
+ type: Boolean
1871
+ })], ArgusTable.prototype, "selectionCheckboxes", void 0);
1872
+ __decorate([property({ attribute: false })], ArgusTable.prototype, "selection", null);
1873
+ __decorate([property({ attribute: false })], ArgusTable.prototype, "contentRenderer", void 0);
1874
+ __decorate([property({ attribute: false })], ArgusTable.prototype, "plugins", null);
1016
1875
  ArgusTable = __decorate([customElement("argus-table")], ArgusTable);
1017
1876
  export { ArgusTable };
1018
1877