argus-table 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +66 -2
- package/dist/_virtual/_@oxc-project_runtime@0.156.0/helpers/esm/decorate.js +8 -0
- package/dist/argus-table.js +517 -0
- package/dist/argus-table.js.map +1 -0
- package/dist/cdn/argus-table.js +4569 -0
- package/dist/cdn/argus-table.js.map +1 -0
- package/dist/controllers/pin-scroll-controller.js +78 -0
- package/dist/controllers/pin-scroll-controller.js.map +1 -0
- package/dist/controllers/resize-drag.js +44 -0
- package/dist/controllers/resize-drag.js.map +1 -0
- package/dist/controllers/table-controller.js +24 -0
- package/dist/controllers/table-controller.js.map +1 -0
- package/dist/controllers/virtual-controller.js +83 -0
- package/dist/controllers/virtual-controller.js.map +1 -0
- package/dist/core/src/columns/resolve-columns.js +62 -0
- package/dist/core/src/columns/resolve-columns.js.map +1 -0
- package/dist/core/src/state/controllable.js +31 -0
- package/dist/core/src/state/controllable.js.map +1 -0
- package/dist/core/src/table/create-table.js +185 -0
- package/dist/core/src/table/create-table.js.map +1 -0
- package/dist/core/src/virtual/row-pool.js +64 -0
- package/dist/core/src/virtual/row-pool.js.map +1 -0
- package/dist/custom-elements.json +1191 -0
- package/dist/events.js +22 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +236 -0
- package/dist/index.js +3 -0
- package/dist/render/body.js +48 -0
- package/dist/render/body.js.map +1 -0
- package/dist/render/cells.js +27 -0
- package/dist/render/cells.js.map +1 -0
- package/dist/render/content.js +35 -0
- package/dist/render/content.js.map +1 -0
- package/dist/render/header.js +61 -0
- package/dist/render/header.js.map +1 -0
- package/dist/styles/base.js +11 -0
- package/dist/styles/base.js.map +1 -0
- package/package.json +62 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joessst-Dev and Argus Table contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,67 @@
|
|
|
1
|
-
#
|
|
1
|
+
# argus-table
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`<argus-table>` is a data table web component built with Lit and TanStack. It works in any framework, or none.
|
|
4
|
+
|
|
5
|
+
- **Virtualized rows:** 100,000 rows scroll smoothly, and the DOM stays the same size.
|
|
6
|
+
- **Pinned columns** at the start or end, which mirror in right-to-left layouts.
|
|
7
|
+
- **Sorting and resizing:** by mouse or keyboard, controlled or uncontrolled.
|
|
8
|
+
- **Accessible:** the WAI-ARIA grid pattern, with one tab stop and arrow-key navigation.
|
|
9
|
+
- **Plain CSS styling:** `--argus-*` tokens and `::part()` names. Cell content is light DOM, so your own CSS and components work inside cells.
|
|
10
|
+
|
|
11
|
+
**[Docs and live examples](https://joessst-dev.github.io/argus-table/)** · [GitHub](https://github.com/Joessst-Dev/argus-table)
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm i argus-table @argus-table/themes
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## A first table
|
|
20
|
+
|
|
21
|
+
```html
|
|
22
|
+
<argus-table id="orders" aria-label="Orders" style="height: 400px"></argus-table>
|
|
23
|
+
|
|
24
|
+
<script type="module">
|
|
25
|
+
import 'argus-table';
|
|
26
|
+
import '@argus-table/themes/default.css';
|
|
27
|
+
|
|
28
|
+
const table = document.querySelector('#orders');
|
|
29
|
+
table.columns = [
|
|
30
|
+
{ id: 'id', header: 'Order', accessor: 'id', width: 90, pin: 'start' },
|
|
31
|
+
{
|
|
32
|
+
id: 'customer',
|
|
33
|
+
header: 'Customer',
|
|
34
|
+
accessor: 'customer.name',
|
|
35
|
+
width: { flex: 1 },
|
|
36
|
+
sortable: true,
|
|
37
|
+
},
|
|
38
|
+
{ id: 'total', header: 'Total', accessor: 'total', width: 110, align: 'end', sortable: true },
|
|
39
|
+
];
|
|
40
|
+
table.data = await (await fetch('/api/orders')).json();
|
|
41
|
+
table.getRowId = (order) => order.id;
|
|
42
|
+
table.addEventListener('argus-row-click', (e) => console.log(e.detail.row));
|
|
43
|
+
</script>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- **Height:** the table virtualizes its rows, so it needs a bounded height. Its default is 400 px.
|
|
47
|
+
- **Label:** give it a name with `aria-label` or `aria-labelledby`.
|
|
48
|
+
- **Columns and data** are JavaScript properties, because they hold objects and functions.
|
|
49
|
+
- **Cells:** a column's `cell` function can return a string, a number, a DOM node or a Lit template.
|
|
50
|
+
|
|
51
|
+
## Without a bundler
|
|
52
|
+
|
|
53
|
+
The CDN build has every dependency bundled:
|
|
54
|
+
|
|
55
|
+
```html
|
|
56
|
+
<script
|
|
57
|
+
type="module"
|
|
58
|
+
src="https://cdn.jsdelivr.net/npm/argus-table/dist/cdn/argus-table.js"
|
|
59
|
+
></script>
|
|
60
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@argus-table/themes/dist/default.css" />
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Status
|
|
64
|
+
|
|
65
|
+
This is v0.1, an early release. Names can still change before 1.0. Infinite loading, pinned rows, auto column widths, column reordering and framework wrappers come in v0.2. See the [roadmap](https://github.com/Joessst-Dev/argus-table/blob/main/docs/plan/plan/12-roadmap.md).
|
|
66
|
+
|
|
67
|
+
MIT licensed.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// \0@oxc-project+runtime@0.156.0/helpers/esm/decorate.js
|
|
2
|
+
function __decorate(decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
}
|
|
8
|
+
export { __decorate as default };
|
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
import { controllable } from "./core/src/state/controllable.js";
|
|
2
|
+
import { PinScrollController } from "./controllers/pin-scroll-controller.js";
|
|
3
|
+
import { startResizeDrag } from "./controllers/resize-drag.js";
|
|
4
|
+
import { TableController } from "./controllers/table-controller.js";
|
|
5
|
+
import { VirtualController } from "./controllers/virtual-controller.js";
|
|
6
|
+
import { CELL_CLICK, COLUMN_RESIZE, RANGE_CHANGE, ROW_CLICK, SORT_CHANGE, fire } from "./events.js";
|
|
7
|
+
import { renderBody } from "./render/body.js";
|
|
8
|
+
import { renderLightDom } from "./render/cells.js";
|
|
9
|
+
import { renderHeader } from "./render/header.js";
|
|
10
|
+
import { baseStyles } from "./styles/base.js";
|
|
11
|
+
import __decorate from "./_virtual/_@oxc-project_runtime@0.156.0/helpers/esm/decorate.js";
|
|
12
|
+
import { LitElement, html, nothing } from "lit";
|
|
13
|
+
import { customElement, property } from "lit/decorators.js";
|
|
14
|
+
import { styleMap } from "lit/directives/style-map.js";
|
|
15
|
+
// src/argus-table.ts
|
|
16
|
+
/**
|
|
17
|
+
* The host carries the grid role, so rows rendered in the shadow root become
|
|
18
|
+
* its children in the accessibility tree. ElementInternals sets the default;
|
|
19
|
+
* `connectedCallback` also adds a `role` attribute, because tools such as
|
|
20
|
+
* axe-core can't see the ElementInternals role. A `role` the page sets wins.
|
|
21
|
+
*/
|
|
22
|
+
function attachGridInternals(host) {
|
|
23
|
+
const internals = host.attachInternals();
|
|
24
|
+
internals.role = "grid";
|
|
25
|
+
return internals;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The sortable column a header click came from, if any. Works for slotted content
|
|
29
|
+
* too. Clicks on a resize handle never sort, so a click that ends a drag doesn't.
|
|
30
|
+
*/
|
|
31
|
+
function sortColumnOf(event) {
|
|
32
|
+
for (const target of event.composedPath()) {
|
|
33
|
+
if (!(target instanceof HTMLElement)) continue;
|
|
34
|
+
if (target.dataset.resizeCol !== void 0) return void 0;
|
|
35
|
+
if (target.dataset.sortCol !== void 0) return target.dataset.sortCol;
|
|
36
|
+
if (target.getAttribute("part") === "header-row") return void 0;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
var RESIZE_STEP = 10;
|
|
40
|
+
/** The grid cell or column header an element is, if any. */
|
|
41
|
+
function gridCell(target) {
|
|
42
|
+
if (!(target instanceof HTMLElement) || target.dataset.col === void 0) return void 0;
|
|
43
|
+
const role = target.getAttribute("role");
|
|
44
|
+
return role === "gridcell" || role === "columnheader" ? target : void 0;
|
|
45
|
+
}
|
|
46
|
+
/** Row index of a rendered row element (`aria-rowindex` minus the header); `-1` for the header. */
|
|
47
|
+
function rowIndexOf(row) {
|
|
48
|
+
return Number(row?.getAttribute("aria-rowindex") ?? 1) - 2;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The pinned edge's shadow, drawn once per side over the scroller rather than
|
|
52
|
+
* per row: a shadow on every pinned cell repaints on every scroll frame. It
|
|
53
|
+
* covers the pinned columns and casts `--argus-pinned-shadow` onto the rest;
|
|
54
|
+
* PinScrollController sizes it to the visible area.
|
|
55
|
+
*/
|
|
56
|
+
function pinShadow(side, width, rows, rowPx) {
|
|
57
|
+
if (width === 0) return nothing;
|
|
58
|
+
return html`<div
|
|
59
|
+
class="pin-shadow"
|
|
60
|
+
data-side=${side}
|
|
61
|
+
aria-hidden="true"
|
|
62
|
+
style=${styleMap({
|
|
63
|
+
inlineSize: `${String(width)}px`,
|
|
64
|
+
"--_content-h": `${String((rows + 1) * rowPx)}px`
|
|
65
|
+
})}
|
|
66
|
+
></div>`;
|
|
67
|
+
}
|
|
68
|
+
/** CSS grid track for a column: fixed px, or a flex share with the min width as floor. */
|
|
69
|
+
function track(column) {
|
|
70
|
+
return column.flex === void 0 ? `${String(column.width)}px` : `minmax(${String(column.minWidth)}px, ${String(column.flex)}fr)`;
|
|
71
|
+
}
|
|
72
|
+
var ArgusTable = class ArgusTable extends LitElement {
|
|
73
|
+
constructor(..._args) {
|
|
74
|
+
super(..._args);
|
|
75
|
+
this.columns = [];
|
|
76
|
+
this.data = [];
|
|
77
|
+
this.density = "comfortable";
|
|
78
|
+
this.overscan = 6;
|
|
79
|
+
this.#sorting = controllable([]);
|
|
80
|
+
this.#sizing = controllable({});
|
|
81
|
+
this.#internals = attachGridInternals(this);
|
|
82
|
+
this.#table = new TableController();
|
|
83
|
+
this.#virtual = new VirtualController(this, () => this.renderRoot.querySelector("[part~=\"table\"]"));
|
|
84
|
+
this.#pinScroll = new PinScrollController(this, () => this.renderRoot.querySelector("[part~=\"table\"]"));
|
|
85
|
+
this.#rowPx = 40;
|
|
86
|
+
this.#window = {
|
|
87
|
+
rows: [],
|
|
88
|
+
totalSize: 0,
|
|
89
|
+
range: null
|
|
90
|
+
};
|
|
91
|
+
this.#focusPending = false;
|
|
92
|
+
this.#lastRange = "";
|
|
93
|
+
this.#headerHandlers = {
|
|
94
|
+
click: (event) => {
|
|
95
|
+
const id = sortColumnOf(event);
|
|
96
|
+
if (id !== void 0) this.#requestSort(id);
|
|
97
|
+
},
|
|
98
|
+
pointerdown: (event) => {
|
|
99
|
+
if (event.button !== 0) return;
|
|
100
|
+
const handle = event.composedPath()[0];
|
|
101
|
+
if (!(handle instanceof HTMLElement)) return;
|
|
102
|
+
const id = handle.dataset.resizeCol;
|
|
103
|
+
const cell = handle.parentElement;
|
|
104
|
+
if (id === void 0 || !cell) return;
|
|
105
|
+
startResizeDrag(handle, event, cell.getBoundingClientRect().width, this.#direction(), (w) => {
|
|
106
|
+
this.#requestResize(id, w);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
this.#onKeydown = (event) => {
|
|
111
|
+
const cell = gridCell(event.composedPath()[0] ?? event.target ?? this);
|
|
112
|
+
const id = cell?.dataset.col;
|
|
113
|
+
if (!cell || id === void 0) return;
|
|
114
|
+
if (event.key === "Tab" && event.shiftKey) {
|
|
115
|
+
const scroller = event.currentTarget;
|
|
116
|
+
scroller.tabIndex = -1;
|
|
117
|
+
cell.addEventListener("focusout", () => {
|
|
118
|
+
scroller.removeAttribute("tabindex");
|
|
119
|
+
}, { once: true });
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const inHeader = cell.getAttribute("role") === "columnheader";
|
|
123
|
+
const { key } = event;
|
|
124
|
+
if (inHeader && event.altKey && (key === "ArrowLeft" || key === "ArrowRight")) {
|
|
125
|
+
event.preventDefault();
|
|
126
|
+
const towardEnd = key === "ArrowRight" === (this.#direction() === 1);
|
|
127
|
+
this.#requestResize(id, this.#currentWidth(id, cell) + (towardEnd ? RESIZE_STEP : -10));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (inHeader && (key === "Enter" || key === " ") && cell.dataset.sortCol !== void 0) {
|
|
131
|
+
event.preventDefault();
|
|
132
|
+
this.#requestSort(id);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
|
|
136
|
+
if (!inHeader && key === "Enter") {
|
|
137
|
+
event.preventDefault();
|
|
138
|
+
this.#activate(event.composedPath());
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const model = this.#table.model;
|
|
142
|
+
const columns = model.columns;
|
|
143
|
+
let row = inHeader ? -1 : rowIndexOf(cell.parentElement);
|
|
144
|
+
let col = columns.findIndex((c) => c.id === id);
|
|
145
|
+
switch (key) {
|
|
146
|
+
case "ArrowDown":
|
|
147
|
+
row = Math.min(row + 1, model.rowCount - 1);
|
|
148
|
+
break;
|
|
149
|
+
case "ArrowUp":
|
|
150
|
+
row = Math.max(row - 1, -1);
|
|
151
|
+
break;
|
|
152
|
+
case "ArrowRight":
|
|
153
|
+
col += this.#direction();
|
|
154
|
+
break;
|
|
155
|
+
case "ArrowLeft":
|
|
156
|
+
col -= this.#direction();
|
|
157
|
+
break;
|
|
158
|
+
case "Home":
|
|
159
|
+
col = 0;
|
|
160
|
+
break;
|
|
161
|
+
case "End":
|
|
162
|
+
col = columns.length - 1;
|
|
163
|
+
break;
|
|
164
|
+
default: return;
|
|
165
|
+
}
|
|
166
|
+
event.preventDefault();
|
|
167
|
+
const target = columns[Math.min(Math.max(col, 0), columns.length - 1)];
|
|
168
|
+
if (target) this.#moveTo(row, target.id, model);
|
|
169
|
+
};
|
|
170
|
+
this.#onFocusin = (event) => {
|
|
171
|
+
const scroller = event.currentTarget;
|
|
172
|
+
if (event.composedPath()[0] === scroller) {
|
|
173
|
+
const from = event.relatedTarget;
|
|
174
|
+
if (!(from instanceof Node && scroller.contains(from))) {
|
|
175
|
+
this.#focusPending = true;
|
|
176
|
+
this.requestUpdate();
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
for (const target of event.composedPath()) {
|
|
181
|
+
const cell = gridCell(target);
|
|
182
|
+
if (cell?.dataset.col !== void 0) {
|
|
183
|
+
const index = cell.getAttribute("role") === "columnheader" ? -1 : rowIndexOf(cell.parentElement);
|
|
184
|
+
const { col } = cell.dataset;
|
|
185
|
+
if (index !== this.#active?.index || col !== this.#active.col) {
|
|
186
|
+
this.#active = {
|
|
187
|
+
index,
|
|
188
|
+
rowId: this.#table.model.rowAt(index)?.id,
|
|
189
|
+
col
|
|
190
|
+
};
|
|
191
|
+
this.requestUpdate();
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (target === event.currentTarget) return;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
this.#onClick = (event) => {
|
|
199
|
+
this.#activate(event.composedPath());
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
static {
|
|
203
|
+
this.styles = baseStyles;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* The sort order, e.g. `[{ id: 'total', desc: true }]`; `[]` when unsorted. Unset,
|
|
207
|
+
* the table keeps it itself and header clicks change it. Once set, it is
|
|
208
|
+
* controlled: a click only fires `argus-sort-change` with the proposed order,
|
|
209
|
+
* and nothing changes until you set the property. Set `undefined` to hand it back.
|
|
210
|
+
*/
|
|
211
|
+
get sorting() {
|
|
212
|
+
return this.#sorting.value;
|
|
213
|
+
}
|
|
214
|
+
set sorting(value) {
|
|
215
|
+
this.#sorting.set(value);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Resized widths in px by column id, e.g. `{ name: 240 }`. A resized flex column
|
|
219
|
+
* becomes fixed. Uncontrolled until set, like `sorting`: once set, a resize only
|
|
220
|
+
* fires `argus-column-resize` with the proposed widths. `undefined` hands it back.
|
|
221
|
+
*/
|
|
222
|
+
get columnSizing() {
|
|
223
|
+
return this.#sizing.value;
|
|
224
|
+
}
|
|
225
|
+
set columnSizing(value) {
|
|
226
|
+
this.#sizing.set(value);
|
|
227
|
+
}
|
|
228
|
+
#sorting;
|
|
229
|
+
#sizing;
|
|
230
|
+
/** A resize the table applied itself; its event fires once the update is done. */
|
|
231
|
+
#appliedResize;
|
|
232
|
+
/** A sort the table applied itself; its event fires once the update is done. */
|
|
233
|
+
#appliedSort;
|
|
234
|
+
#internals;
|
|
235
|
+
#table;
|
|
236
|
+
#virtual;
|
|
237
|
+
#pinScroll;
|
|
238
|
+
#rowPx;
|
|
239
|
+
#probeObserver;
|
|
240
|
+
#window;
|
|
241
|
+
#active;
|
|
242
|
+
/** Move focus to the active cell after the next update (keyboard navigation). */
|
|
243
|
+
#focusPending;
|
|
244
|
+
#lastRange;
|
|
245
|
+
connectedCallback() {
|
|
246
|
+
super.connectedCallback();
|
|
247
|
+
if (!this.hasAttribute("role")) this.setAttribute("role", "grid");
|
|
248
|
+
if (this.hasUpdated) this.#observeProbe();
|
|
249
|
+
}
|
|
250
|
+
disconnectedCallback() {
|
|
251
|
+
super.disconnectedCallback();
|
|
252
|
+
this.#probeObserver?.disconnect();
|
|
253
|
+
}
|
|
254
|
+
/** Scrolls so that the row at `index` (in the current row order) is at the top. */
|
|
255
|
+
async scrollToRow(index) {
|
|
256
|
+
await this.updateComplete;
|
|
257
|
+
this.#virtual.scrollToIndex(index);
|
|
258
|
+
await this.updateComplete;
|
|
259
|
+
}
|
|
260
|
+
willUpdate(changed) {
|
|
261
|
+
if (changed.has("columns") || changed.has("data") || changed.has("getRowId") || changed.has("sorting") || changed.has("columnSizing")) this.#table.update({
|
|
262
|
+
columns: this.columns,
|
|
263
|
+
data: this.data,
|
|
264
|
+
getRowId: this.getRowId,
|
|
265
|
+
sorting: this.#sorting.value,
|
|
266
|
+
columnSizing: this.#sizing.value
|
|
267
|
+
});
|
|
268
|
+
const model = this.#table.model;
|
|
269
|
+
this.#reconcileActive(model);
|
|
270
|
+
const active = this.#active;
|
|
271
|
+
this.#internals.ariaRowCount = String(model.rowCount + 1);
|
|
272
|
+
this.#internals.ariaColCount = String(model.columns.length);
|
|
273
|
+
this.#virtual.configure({
|
|
274
|
+
count: model.rowCount,
|
|
275
|
+
rowHeight: this.#rowPx,
|
|
276
|
+
overscan: Math.max(0, this.overscan),
|
|
277
|
+
rowIdAt: (index) => model.rowAt(index)?.id ?? String(index),
|
|
278
|
+
keep: active?.rowId === void 0 ? void 0 : {
|
|
279
|
+
id: active.rowId,
|
|
280
|
+
index: active.index
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
render() {
|
|
285
|
+
const model = this.#table.model;
|
|
286
|
+
const columns = model.columns;
|
|
287
|
+
this.#window = this.#virtual.computeWindow();
|
|
288
|
+
const pinnedWidth = (side) => columns.reduce((sum, c) => c.pin === side ? sum + c.width : sum, 0);
|
|
289
|
+
const pinned = (side) => `${String(pinnedWidth(side))}px`;
|
|
290
|
+
const style = {
|
|
291
|
+
"--_cols": columns.map(track).join(" "),
|
|
292
|
+
"--_width": `${String(model.totalWidth)}px`,
|
|
293
|
+
"--_pin-start": pinned("start"),
|
|
294
|
+
"--_pin-end": pinned("end")
|
|
295
|
+
};
|
|
296
|
+
const active = this.#active;
|
|
297
|
+
const bodyActive = active && active.index >= 0 ? {
|
|
298
|
+
index: active.index,
|
|
299
|
+
col: active.col
|
|
300
|
+
} : void 0;
|
|
301
|
+
if (this.rowHeight !== void 0 && this.rowHeight > 0) style["--_row-height"] = `${String(this.rowHeight)}px`;
|
|
302
|
+
return html`<div
|
|
303
|
+
part="table"
|
|
304
|
+
role="presentation"
|
|
305
|
+
style=${styleMap(style)}
|
|
306
|
+
@keydown=${this.#onKeydown}
|
|
307
|
+
@focusin=${this.#onFocusin}
|
|
308
|
+
@click=${this.#onClick}
|
|
309
|
+
>
|
|
310
|
+
${renderHeader(columns, this.#headerHandlers, active?.index === -1 ? active.col : void 0)}
|
|
311
|
+
${model.rowCount === 0 ? html`<div part="empty"><slot name="empty">No rows</slot></div>` : renderBody(this.#window.rows, columns, this.#window.totalSize, bodyActive)}
|
|
312
|
+
<div class="probe" aria-hidden="true"></div>
|
|
313
|
+
</div>
|
|
314
|
+
${pinShadow("start", pinnedWidth("start"), model.rowCount, this.#rowPx)}
|
|
315
|
+
${pinShadow("end", pinnedWidth("end"), model.rowCount, this.#rowPx)}`;
|
|
316
|
+
}
|
|
317
|
+
firstUpdated() {
|
|
318
|
+
this.#observeProbe();
|
|
319
|
+
this.#measureRowHeight();
|
|
320
|
+
}
|
|
321
|
+
updated(changed) {
|
|
322
|
+
renderLightDom(this, this.#table.model, this.#window.rows);
|
|
323
|
+
if (changed.has("columns") || changed.has("columnSizing")) this.#pinScroll.measure();
|
|
324
|
+
if (this.#focusPending) {
|
|
325
|
+
this.#focusPending = false;
|
|
326
|
+
this.#focusActive();
|
|
327
|
+
}
|
|
328
|
+
if (changed.has("density") || changed.has("rowHeight")) this.#measureRowHeight();
|
|
329
|
+
if (this.#appliedResize) {
|
|
330
|
+
const detail = this.#appliedResize;
|
|
331
|
+
this.#appliedResize = void 0;
|
|
332
|
+
fire(this, COLUMN_RESIZE, detail);
|
|
333
|
+
}
|
|
334
|
+
if (this.#appliedSort) {
|
|
335
|
+
const sorting = this.#appliedSort;
|
|
336
|
+
this.#appliedSort = void 0;
|
|
337
|
+
fire(this, SORT_CHANGE, { sorting });
|
|
338
|
+
}
|
|
339
|
+
const { range } = this.#window;
|
|
340
|
+
const key = range ? `${String(range.start)}:${String(range.end)}` : "";
|
|
341
|
+
if (range && key !== this.#lastRange) {
|
|
342
|
+
this.#lastRange = key;
|
|
343
|
+
fire(this, RANGE_CHANGE, {
|
|
344
|
+
start: range.start,
|
|
345
|
+
end: range.end
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
#headerHandlers;
|
|
350
|
+
/**
|
|
351
|
+
* Keys on a focused cell or header. Keys from content inside a cell (an input,
|
|
352
|
+
* a button) are left to that content.
|
|
353
|
+
*/
|
|
354
|
+
#onKeydown;
|
|
355
|
+
/** Focus that lands in a cell (a click, or content inside it) makes that cell active. */
|
|
356
|
+
#onFocusin;
|
|
357
|
+
/** `argus-cell-click`, then `argus-row-click`, for clicks in the body. */
|
|
358
|
+
#onClick;
|
|
359
|
+
/**
|
|
360
|
+
* Fires `argus-cell-click` (when a cell is on the path), then `argus-row-click`,
|
|
361
|
+
* for the body row on an event path. Clicks and Enter on a body cell use it.
|
|
362
|
+
*/
|
|
363
|
+
#activate(path) {
|
|
364
|
+
let columnId;
|
|
365
|
+
for (const target of path) {
|
|
366
|
+
if (!(target instanceof HTMLElement)) continue;
|
|
367
|
+
const role = target.getAttribute("role");
|
|
368
|
+
if (role === "gridcell") columnId = target.dataset.col;
|
|
369
|
+
if (role !== "row") continue;
|
|
370
|
+
const model = this.#table.model;
|
|
371
|
+
const index = rowIndexOf(target);
|
|
372
|
+
const row = index >= 0 ? model.rowAt(index) : void 0;
|
|
373
|
+
if (!row) return;
|
|
374
|
+
if (columnId !== void 0) fire(this, CELL_CLICK, {
|
|
375
|
+
row: row.original,
|
|
376
|
+
columnId,
|
|
377
|
+
value: model.valueAt(index, columnId)
|
|
378
|
+
});
|
|
379
|
+
fire(this, ROW_CLICK, {
|
|
380
|
+
row: row.original,
|
|
381
|
+
rowId: row.id,
|
|
382
|
+
index
|
|
383
|
+
});
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/** Makes a cell active and focuses it after the update. The keep-rendered rule renders its row. */
|
|
388
|
+
#moveTo(index, col, model) {
|
|
389
|
+
this.#active = {
|
|
390
|
+
index,
|
|
391
|
+
rowId: index >= 0 ? model.rowAt(index)?.id : void 0,
|
|
392
|
+
col
|
|
393
|
+
};
|
|
394
|
+
this.#focusPending = true;
|
|
395
|
+
this.requestUpdate();
|
|
396
|
+
}
|
|
397
|
+
#focusActive() {
|
|
398
|
+
const active = this.#active;
|
|
399
|
+
if (!active) return;
|
|
400
|
+
const col = CSS.escape(active.col);
|
|
401
|
+
const selector = active.index === -1 ? `[part~='header-row'] > [data-col="${col}"]` : `[part~='body'] > [aria-rowindex="${String(active.index + 2)}"] > [data-col="${col}"]`;
|
|
402
|
+
const cell = this.renderRoot.querySelector(selector);
|
|
403
|
+
if (!cell) return;
|
|
404
|
+
cell.scrollIntoView({
|
|
405
|
+
block: "nearest",
|
|
406
|
+
inline: "nearest"
|
|
407
|
+
});
|
|
408
|
+
cell.focus({ preventScroll: true });
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Keeps the active cell valid after columns or rows change: on the same row id
|
|
412
|
+
* when it still exists (it may have moved), else at the same index, clamped.
|
|
413
|
+
* Defaults to the first header. If its row went away while the grid had focus,
|
|
414
|
+
* focus follows to the replacement.
|
|
415
|
+
*/
|
|
416
|
+
#reconcileActive(model) {
|
|
417
|
+
const columns = model.columns;
|
|
418
|
+
const first = columns[0];
|
|
419
|
+
if (!first) {
|
|
420
|
+
this.#active = void 0;
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const prev = this.#active ?? {
|
|
424
|
+
index: -1,
|
|
425
|
+
rowId: void 0,
|
|
426
|
+
col: first.id
|
|
427
|
+
};
|
|
428
|
+
const col = columns.some((c) => c.id === prev.col) ? prev.col : first.id;
|
|
429
|
+
let { index, rowId } = prev;
|
|
430
|
+
if (rowId !== void 0) {
|
|
431
|
+
const found = model.indexOf(rowId);
|
|
432
|
+
if (found === void 0) {
|
|
433
|
+
index = Math.min(index, model.rowCount - 1);
|
|
434
|
+
rowId = model.rowAt(index)?.id;
|
|
435
|
+
if (this.matches(":focus-within")) this.#focusPending = true;
|
|
436
|
+
} else index = found;
|
|
437
|
+
}
|
|
438
|
+
if (index !== prev.index || rowId !== prev.rowId || col !== prev.col || !this.#active) this.#active = {
|
|
439
|
+
index,
|
|
440
|
+
rowId,
|
|
441
|
+
col
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* The width a resize starts from: the latest requested width (key presses can
|
|
446
|
+
* come faster than renders), else the model's fixed width, else the rendered
|
|
447
|
+
* width of a flex column.
|
|
448
|
+
*/
|
|
449
|
+
#currentWidth(columnId, cell) {
|
|
450
|
+
const sized = this.#sizing.value[columnId];
|
|
451
|
+
if (sized !== void 0) return sized;
|
|
452
|
+
const view = this.#table.model.columns.find((c) => c.id === columnId);
|
|
453
|
+
return view && view.flex === void 0 ? view.width : cell.getBoundingClientRect().width;
|
|
454
|
+
}
|
|
455
|
+
#direction() {
|
|
456
|
+
return getComputedStyle(this).direction === "rtl" ? -1 : 1;
|
|
457
|
+
}
|
|
458
|
+
/** Sets one column's width (clamped by core), through the controlled/uncontrolled rules. */
|
|
459
|
+
#requestResize(columnId, width) {
|
|
460
|
+
const next = this.#table.model.nextSizing(columnId, width);
|
|
461
|
+
const target = next?.[columnId];
|
|
462
|
+
if (!next || target === void 0 || target === this.#sizing.value[columnId]) return;
|
|
463
|
+
const old = this.#sizing.value;
|
|
464
|
+
const { value, applied } = this.#sizing.request(next);
|
|
465
|
+
const detail = {
|
|
466
|
+
columnId,
|
|
467
|
+
width: target,
|
|
468
|
+
sizing: value
|
|
469
|
+
};
|
|
470
|
+
if (applied) {
|
|
471
|
+
this.#appliedResize = detail;
|
|
472
|
+
this.requestUpdate("columnSizing", old);
|
|
473
|
+
} else fire(this, COLUMN_RESIZE, detail);
|
|
474
|
+
}
|
|
475
|
+
/** asc → desc → unsorted, through the controlled/uncontrolled rules. */
|
|
476
|
+
#requestSort(columnId) {
|
|
477
|
+
const next = this.#table.model.nextSorting(columnId);
|
|
478
|
+
if (!next) return;
|
|
479
|
+
const old = this.#sorting.value;
|
|
480
|
+
const { value, applied } = this.#sorting.request(next);
|
|
481
|
+
if (applied) {
|
|
482
|
+
this.#appliedSort = value;
|
|
483
|
+
this.requestUpdate("sorting", old);
|
|
484
|
+
} else fire(this, SORT_CHANGE, { sorting: value });
|
|
485
|
+
}
|
|
486
|
+
#observeProbe() {
|
|
487
|
+
const probe = this.renderRoot.querySelector(".probe");
|
|
488
|
+
if (!probe) return;
|
|
489
|
+
this.#probeObserver ??= new ResizeObserver(() => {
|
|
490
|
+
this.#measureRowHeight();
|
|
491
|
+
});
|
|
492
|
+
this.#probeObserver.observe(probe);
|
|
493
|
+
}
|
|
494
|
+
/** Reads the resolved row height, so themes and density change the virtualizer too. */
|
|
495
|
+
#measureRowHeight() {
|
|
496
|
+
const px = this.renderRoot.querySelector(".probe")?.getBoundingClientRect().height ?? 0;
|
|
497
|
+
if (px > 0 && Math.abs(px - this.#rowPx) > .01) {
|
|
498
|
+
this.#rowPx = px;
|
|
499
|
+
this.requestUpdate();
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
__decorate([property({ attribute: false })], ArgusTable.prototype, "columns", void 0);
|
|
504
|
+
__decorate([property({ attribute: false })], ArgusTable.prototype, "data", void 0);
|
|
505
|
+
__decorate([property({ attribute: false })], ArgusTable.prototype, "getRowId", void 0);
|
|
506
|
+
__decorate([property({
|
|
507
|
+
attribute: "row-height",
|
|
508
|
+
type: Number
|
|
509
|
+
})], ArgusTable.prototype, "rowHeight", void 0);
|
|
510
|
+
__decorate([property({ reflect: true })], ArgusTable.prototype, "density", void 0);
|
|
511
|
+
__decorate([property({ type: Number })], ArgusTable.prototype, "overscan", void 0);
|
|
512
|
+
__decorate([property({ attribute: false })], ArgusTable.prototype, "sorting", null);
|
|
513
|
+
__decorate([property({ attribute: false })], ArgusTable.prototype, "columnSizing", null);
|
|
514
|
+
ArgusTable = __decorate([customElement("argus-table")], ArgusTable);
|
|
515
|
+
export { ArgusTable };
|
|
516
|
+
|
|
517
|
+
//# sourceMappingURL=argus-table.js.map
|