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.
- package/README.md +1 -1
- package/dist/argus-column.js +24 -1
- package/dist/argus-column.js.map +1 -1
- package/dist/argus-table.js +1026 -167
- package/dist/argus-table.js.map +1 -1
- package/dist/cdn/argus-table.js +74 -50
- package/dist/cdn/argus-table.js.map +1 -1
- package/dist/controllers/auto-width.js +11 -0
- package/dist/controllers/auto-width.js.map +1 -1
- package/dist/controllers/column-window.js +139 -0
- package/dist/controllers/column-window.js.map +1 -0
- package/dist/controllers/data-controller.js +9 -4
- package/dist/controllers/data-controller.js.map +1 -1
- package/dist/controllers/plugins.js +102 -0
- package/dist/controllers/plugins.js.map +1 -0
- package/dist/controllers/reorder-drag.js +9 -10
- package/dist/controllers/reorder-drag.js.map +1 -1
- package/dist/controllers/row-measure.js +121 -0
- package/dist/controllers/row-measure.js.map +1 -0
- package/dist/controllers/virtual-controller.js +316 -10
- package/dist/controllers/virtual-controller.js.map +1 -1
- package/dist/core/src/columns/column-order.js +14 -10
- package/dist/core/src/columns/column-order.js.map +1 -1
- package/dist/core/src/columns/resolve-columns.js +18 -4
- package/dist/core/src/columns/resolve-columns.js.map +1 -1
- package/dist/core/src/data/page-cache.js +4 -0
- package/dist/core/src/data/page-cache.js.map +1 -1
- package/dist/core/src/filters/filter.js +185 -0
- package/dist/core/src/filters/filter.js.map +1 -0
- package/dist/core/src/state/selection.js +70 -0
- package/dist/core/src/state/selection.js.map +1 -0
- package/dist/core/src/table/create-table.js +239 -64
- package/dist/core/src/table/create-table.js.map +1 -1
- package/dist/core/src/table/filtered-row-model.js +48 -0
- package/dist/core/src/table/filtered-row-model.js.map +1 -0
- package/dist/core/src/table/sorted-row-model.js +13 -2
- package/dist/core/src/table/sorted-row-model.js.map +1 -1
- package/dist/core/src/virtual/column-window.js +72 -0
- package/dist/core/src/virtual/column-window.js.map +1 -0
- package/dist/core/src/virtual/row-pool.js +2 -1
- package/dist/core/src/virtual/row-pool.js.map +1 -1
- package/dist/core/src/virtual/row-sizes.js +156 -0
- package/dist/core/src/virtual/row-sizes.js.map +1 -0
- package/dist/core/src/virtual/scroll-scale.js +41 -3
- package/dist/core/src/virtual/scroll-scale.js.map +1 -1
- package/dist/custom-elements.json +1320 -74
- package/dist/events.js +9 -1
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +609 -74
- package/dist/index.js +3 -2
- package/dist/interactive.js +22 -0
- package/dist/interactive.js.map +1 -0
- package/dist/max-height.js +22 -8
- package/dist/max-height.js.map +1 -1
- package/dist/render/body.js +47 -7
- package/dist/render/body.js.map +1 -1
- package/dist/render/cells.js +14 -11
- package/dist/render/cells.js.map +1 -1
- package/dist/render/content-directive.js +115 -0
- package/dist/render/content-directive.js.map +1 -0
- package/dist/render/content.js +20 -9
- package/dist/render/content.js.map +1 -1
- package/dist/render/header.js +24 -5
- package/dist/render/header.js.map +1 -1
- package/dist/render/pinned-rows.js +1 -1
- package/dist/render/pinned-rows.js.map +1 -1
- package/dist/styles/base.js +1 -1
- package/dist/styles/base.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ArgusColumn } from "./argus-column.js";
|
|
2
2
|
import { ArgusTable } from "./argus-table.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import { content } from "./render/content-directive.js";
|
|
4
|
+
import { CELL_CLICK, COLUMN_FILTER, COLUMN_HIDE, COLUMN_PIN, COLUMN_REORDER, COLUMN_RESIZE, LOAD_ERROR, RANGE_CHANGE, ROW_CLICK, ROW_SELECT, SORT_CHANGE } from "./events.js";
|
|
5
|
+
export { ArgusColumn, ArgusTable, CELL_CLICK, COLUMN_FILTER, COLUMN_HIDE, COLUMN_PIN, COLUMN_REORDER, COLUMN_RESIZE, LOAD_ERROR, RANGE_CHANGE, ROW_CLICK, ROW_SELECT, SORT_CHANGE, content };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// src/interactive.ts
|
|
2
|
+
/**
|
|
3
|
+
* Interactive content a page may put in a header or cell. A press on it belongs
|
|
4
|
+
* to that content: it doesn't sort, start a column drag or select a row. A
|
|
5
|
+
* popover or a dialog (a menu or an editor a plugin opens from a header) counts
|
|
6
|
+
* as a whole, padding included.
|
|
7
|
+
*/
|
|
8
|
+
var INTERACTIVE = "button,input,select,textarea,a[href],label,[contenteditable],[popover],dialog";
|
|
9
|
+
/**
|
|
10
|
+
* Whether an event path reaches interactive content before `stop` (the cell or
|
|
11
|
+
* row the table handles the event for).
|
|
12
|
+
*/
|
|
13
|
+
function fromInteractive(path, stop) {
|
|
14
|
+
for (const target of path) {
|
|
15
|
+
if (target === stop) return false;
|
|
16
|
+
if (target instanceof Element && target.matches("button,input,select,textarea,a[href],label,[contenteditable],[popover],dialog")) return true;
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
export { INTERACTIVE, fromInteractive };
|
|
21
|
+
|
|
22
|
+
//# sourceMappingURL=interactive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interactive.js","names":[],"sources":["../src/interactive.ts"],"sourcesContent":["/**\n * Interactive content a page may put in a header or cell. A press on it belongs\n * to that content: it doesn't sort, start a column drag or select a row. A\n * popover or a dialog (a menu or an editor a plugin opens from a header) counts\n * as a whole, padding included.\n */\nexport const INTERACTIVE =\n 'button,input,select,textarea,a[href],label,[contenteditable],[popover],dialog';\n\n/**\n * Whether an event path reaches interactive content before `stop` (the cell or\n * row the table handles the event for).\n */\nexport function fromInteractive(path: readonly EventTarget[], stop: Element): boolean {\n for (const target of path) {\n if (target === stop) return false;\n if (target instanceof Element && target.matches(INTERACTIVE)) return true;\n }\n return false;\n}\n"],"mappings":";;;;;;;AAMA,IAAa,cACX;;;;;AAMF,SAAgB,gBAAgB,MAA8B,MAAwB;CACpF,KAAK,MAAM,UAAU,MAAM;EACzB,IAAI,WAAW,MAAM,OAAO;EAC5B,IAAI,kBAAkB,WAAW,OAAO,QAAA,+EAAmB,GAAG,OAAO;CACvE;CACA,OAAO;AACT"}
|
package/dist/max-height.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
// src/max-height.ts
|
|
2
2
|
/**
|
|
3
|
-
* The tallest
|
|
3
|
+
* The tallest body the engine handles, measured once per page.
|
|
4
4
|
*
|
|
5
5
|
* Chromium and WebKit clamp a taller element at about 33.5M px (fewer CSS px
|
|
6
6
|
* when the page is zoomed in), and Firefox drops anything over about 17.9M px
|
|
7
|
-
* to 0.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* to 0. Firefox also lets go of an element stuck to the top of a scroller (the
|
|
8
|
+
* header, the top pinned rows) once the scroll offset passes about 8.95M px
|
|
9
|
+
* (2^29 of its 1/60 px units). A hidden probe tries a few heights and keeps the
|
|
10
|
+
* first that renders at full size and keeps a sticky element in place when
|
|
11
|
+
* scrolled that far: 32M px in Chromium and WebKit, 8M px in Firefox. A zoom
|
|
12
|
+
* change (a new `devicePixelRatio` on resize) measures again and tells the
|
|
13
|
+
* tables, so a scaled body stays within the new limit.
|
|
10
14
|
*/
|
|
11
15
|
var HEIGHTS = [
|
|
12
16
|
32e6,
|
|
@@ -28,16 +32,26 @@ function onResize() {
|
|
|
28
32
|
measured = void 0;
|
|
29
33
|
for (const listener of listeners) listener();
|
|
30
34
|
}
|
|
31
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* The tallest body height in px the engine renders and scrolls with its header
|
|
37
|
+
* in place.
|
|
38
|
+
*/
|
|
32
39
|
function maxElementHeight() {
|
|
33
40
|
if (measured !== void 0) return measured;
|
|
34
41
|
const probe = document.createElement("div");
|
|
35
42
|
probe.setAttribute("aria-hidden", "true");
|
|
36
|
-
probe.style.cssText = "position:fixed;inset-block-start:0;inset-inline-start:0;inline-size:1px;visibility:hidden;pointer-events:none";
|
|
43
|
+
probe.style.cssText = "position:fixed;inset-block-start:0;inset-inline-start:0;inline-size:1px;block-size:10px;overflow:hidden;visibility:hidden;pointer-events:none";
|
|
44
|
+
const sticky = document.createElement("div");
|
|
45
|
+
sticky.style.cssText = "position:sticky;inset-block-start:0;block-size:1px";
|
|
46
|
+
const filler = document.createElement("div");
|
|
47
|
+
probe.append(sticky, filler);
|
|
37
48
|
document.body.append(probe);
|
|
38
49
|
measured = HEIGHTS.find((height) => {
|
|
39
|
-
|
|
40
|
-
|
|
50
|
+
filler.style.blockSize = `${String(height)}px`;
|
|
51
|
+
if (filler.offsetHeight < height - 1) return false;
|
|
52
|
+
probe.scrollTop = height;
|
|
53
|
+
const held = sticky.getBoundingClientRect().top - probe.getBoundingClientRect().top;
|
|
54
|
+
return Math.abs(held) < 1;
|
|
41
55
|
}) ?? SAFE_HEIGHT;
|
|
42
56
|
probe.remove();
|
|
43
57
|
return measured;
|
package/dist/max-height.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"max-height.js","names":[],"sources":["../src/max-height.ts"],"sourcesContent":["/**\n * The tallest
|
|
1
|
+
{"version":3,"file":"max-height.js","names":[],"sources":["../src/max-height.ts"],"sourcesContent":["/**\n * The tallest body the engine handles, measured once per page.\n *\n * Chromium and WebKit clamp a taller element at about 33.5M px (fewer CSS px\n * when the page is zoomed in), and Firefox drops anything over about 17.9M px\n * to 0. Firefox also lets go of an element stuck to the top of a scroller (the\n * header, the top pinned rows) once the scroll offset passes about 8.95M px\n * (2^29 of its 1/60 px units). A hidden probe tries a few heights and keeps the\n * first that renders at full size and keeps a sticky element in place when\n * scrolled that far: 32M px in Chromium and WebKit, 8M px in Firefox. A zoom\n * change (a new `devicePixelRatio` on resize) measures again and tells the\n * tables, so a scaled body stays within the new limit.\n */\nconst HEIGHTS = [32_000_000, 16_000_000, 8_000_000, 4_000_000, 2_000_000];\n\n/** Content below this never needs the probe: every engine renders it. */\nconst SAFE_HEIGHT = 2_000_000;\n/** Room left for the scrollbar and rounding. */\nconst MARGIN = 1000;\n\nlet measured: number | undefined;\nlet forced: number | undefined;\nlet dpr = 0;\nconst listeners = new Set<() => void>();\n\nfunction onResize() {\n if (window.devicePixelRatio === dpr) return;\n dpr = window.devicePixelRatio;\n measured = undefined;\n for (const listener of listeners) listener();\n}\n\n/**\n * The tallest body height in px the engine renders and scrolls with its header\n * in place.\n */\nfunction maxElementHeight(): number {\n if (forced !== undefined) return forced;\n if (measured !== undefined) return measured;\n // Fixed, so the probe never makes the page scroll. A scroller (overflow:\n // hidden still scrolls from script) with a sticky element and a filler.\n const probe = document.createElement('div');\n probe.setAttribute('aria-hidden', 'true');\n probe.style.cssText =\n 'position:fixed;inset-block-start:0;inset-inline-start:0;inline-size:1px;block-size:10px;overflow:hidden;visibility:hidden;pointer-events:none';\n const sticky = document.createElement('div');\n sticky.style.cssText = 'position:sticky;inset-block-start:0;block-size:1px';\n const filler = document.createElement('div');\n probe.append(sticky, filler);\n document.body.append(probe);\n measured =\n HEIGHTS.find((height) => {\n filler.style.blockSize = `${String(height)}px`;\n if (filler.offsetHeight < height - 1) return false;\n probe.scrollTop = height;\n const held = sticky.getBoundingClientRect().top - probe.getBoundingClientRect().top;\n return Math.abs(held) < 1;\n }) ?? SAFE_HEIGHT;\n probe.remove();\n return measured;\n}\n\n/**\n * The tallest the body may render: the engine's maximum less what scrolls with\n * it (`extra`: the header and pinned rows). `Infinity` for a body of `logical`\n * px that is far from any limit, without measuring.\n */\nexport function bodyCap(logical: number, extra: number): number {\n if (forced === undefined && logical + extra < SAFE_HEIGHT) return Infinity;\n return Math.max(maxElementHeight() - extra - MARGIN, 0);\n}\n\n/** Calls `listener` when the maximum may have changed (the page was zoomed). Returns the unsubscribe. */\nexport function watchMaxElementHeight(listener: () => void): () => void {\n if (listeners.size === 0) {\n dpr = window.devicePixelRatio;\n window.addEventListener('resize', onResize, { passive: true });\n }\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n if (listeners.size === 0) window.removeEventListener('resize', onResize);\n };\n}\n\n/** Tests: pretend the engine renders at most `height` px (`undefined` measures again). */\nexport function setMaxElementHeightForTests(height: number | undefined): void {\n forced = height;\n}\n"],"mappings":";;;;;;;;;;;;;;AAaA,IAAM,UAAU;CAAC;CAAY;CAAY;CAAW;CAAW;AAAS;;AAGxE,IAAM,cAAc;;AAEpB,IAAM,SAAS;AAEf,IAAI;AAEJ,IAAI,MAAM;AACV,IAAM,4BAAY,IAAI,IAAgB;AAEtC,SAAS,WAAW;CAClB,IAAI,OAAO,qBAAqB,KAAK;CACrC,MAAM,OAAO;CACb,WAAW,KAAA;CACX,KAAK,MAAM,YAAY,WAAW,SAAS;AAC7C;;;;;AAMA,SAAS,mBAA2B;CAElC,IAAI,aAAa,KAAA,GAAW,OAAO;CAGnC,MAAM,QAAQ,SAAS,cAAc,KAAK;CAC1C,MAAM,aAAa,eAAe,MAAM;CACxC,MAAM,MAAM,UACV;CACF,MAAM,SAAS,SAAS,cAAc,KAAK;CAC3C,OAAO,MAAM,UAAU;CACvB,MAAM,SAAS,SAAS,cAAc,KAAK;CAC3C,MAAM,OAAO,QAAQ,MAAM;CAC3B,SAAS,KAAK,OAAO,KAAK;CAC1B,WACE,QAAQ,MAAM,WAAW;EACvB,OAAO,MAAM,YAAY,GAAG,OAAO,MAAM,EAAE;EAC3C,IAAI,OAAO,eAAe,SAAS,GAAG,OAAO;EAC7C,MAAM,YAAY;EAClB,MAAM,OAAO,OAAO,sBAAsB,CAAC,CAAC,MAAM,MAAM,sBAAsB,CAAC,CAAC;EAChF,OAAO,KAAK,IAAI,IAAI,IAAI;CAC1B,CAAC,KAAK;CACR,MAAM,OAAO;CACb,OAAO;AACT;;;;;;AAOA,SAAgB,QAAQ,SAAiB,OAAuB;CAC9D,IAA4B,UAAU,QAAQ,aAAa,OAAO;CAClE,OAAO,KAAK,IAAI,iBAAiB,IAAI,QAAQ,QAAQ,CAAC;AACxD;;AAGA,SAAgB,sBAAsB,UAAkC;CACtE,IAAI,UAAU,SAAS,GAAG;EACxB,MAAM,OAAO;EACb,OAAO,iBAAiB,UAAU,UAAU,EAAE,SAAS,KAAK,CAAC;CAC/D;CACA,UAAU,IAAI,QAAQ;CACtB,aAAa;EACX,UAAU,OAAO,QAAQ;EACzB,IAAI,UAAU,SAAS,GAAG,OAAO,oBAAoB,UAAU,QAAQ;CACzE;AACF"}
|
package/dist/render/body.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { cellParts, cellSlot,
|
|
1
|
+
import { cellParts, cellSlot, cellStyle } from "./content.js";
|
|
2
2
|
import { html, nothing } from "lit";
|
|
3
3
|
import { styleMap } from "lit/directives/style-map.js";
|
|
4
|
+
import { live } from "lit/directives/live.js";
|
|
4
5
|
import { repeat } from "lit/directives/repeat.js";
|
|
5
6
|
// src/render/body.ts
|
|
6
7
|
/**
|
|
@@ -8,35 +9,46 @@ import { repeat } from "lit/directives/repeat.js";
|
|
|
8
9
|
* reuses the DOM of one that left (ADR 0001), and they stay in pool order: screen
|
|
9
10
|
* readers get the position from `aria-rowindex`. `firstRowIndex` is the
|
|
10
11
|
* `aria-rowindex` of row 0: after the header and the rows pinned to the top.
|
|
12
|
+
* `inset`: rows of different heights (`rowHeight: 'auto'`) are placed with
|
|
13
|
+
* `inset-block-start` and render at least their measured `size`, which is on
|
|
14
|
+
* the controller's px grid, so they stay flush however far down they are.
|
|
11
15
|
*/
|
|
12
|
-
function renderBody(rows, columns, totalSize, active, firstRowIndex, loading) {
|
|
16
|
+
function renderBody(rows, columns, totalSize, active, firstRowIndex, loading, selectedAt, checkboxes, inset = false) {
|
|
13
17
|
return html`<div
|
|
14
18
|
part="body"
|
|
15
19
|
role="rowgroup"
|
|
16
20
|
style=${styleMap({ blockSize: `${String(totalSize)}px` })}
|
|
17
21
|
>
|
|
18
|
-
${repeat(rows, (r) => r.slot, (r) => renderRow(r, columns, r.index === active?.index ? active.col : void 0, firstRowIndex, loading))}
|
|
22
|
+
${repeat(rows, (r) => r.slot, (r) => renderRow(r, columns, r.index === active?.index ? active.col : void 0, firstRowIndex, loading, selectedAt?.(r.index), checkboxes, inset))}
|
|
19
23
|
</div>`;
|
|
20
24
|
}
|
|
21
25
|
/**
|
|
22
26
|
* One body row. Every cell is focusable (`tabindex=-1`) so clicks focus it; the
|
|
23
27
|
* active one is the grid's single tab stop (roving tabindex). A row whose data
|
|
24
28
|
* isn't loaded shows skeleton bars, and a Retry button if its page failed.
|
|
29
|
+
* `selected` is `undefined` for a row that can't be selected: it then has no
|
|
30
|
+
* `aria-selected`, and no checkbox in the selection column.
|
|
25
31
|
*/
|
|
26
|
-
function renderRow(row, columns, activeCol, firstRowIndex, loading) {
|
|
32
|
+
function renderRow(row, columns, activeCol, firstRowIndex, loading, selected, checkboxes, inset) {
|
|
27
33
|
const state = loading?.stateAt(row.index) ?? "loaded";
|
|
28
34
|
let parts = row.index % 2 === 0 ? "row row-even" : "row row-odd";
|
|
29
35
|
if (state !== "loaded") parts += " row-loading";
|
|
36
|
+
if (selected) parts += " row-selected";
|
|
30
37
|
const retry = state === "error" ? loading?.retry.get(row.index) : void 0;
|
|
38
|
+
const retryCol = firstDataColumn(columns);
|
|
31
39
|
return html`<div
|
|
32
40
|
part=${parts}
|
|
33
41
|
class="row"
|
|
34
42
|
role="row"
|
|
35
43
|
aria-rowindex=${row.index + firstRowIndex}
|
|
36
44
|
aria-busy=${state === "loading" ? "true" : nothing}
|
|
37
|
-
|
|
45
|
+
aria-selected=${selected === void 0 ? nothing : String(selected)}
|
|
46
|
+
style=${styleMap(inset ? {
|
|
47
|
+
insetBlockStart: `${String(row.start)}px`,
|
|
48
|
+
minBlockSize: row.size === void 0 ? void 0 : `${String(row.size)}px`
|
|
49
|
+
} : { transform: `translateY(${String(row.start)}px)` })}
|
|
38
50
|
>
|
|
39
|
-
${repeat(columns, (c) => c.
|
|
51
|
+
${repeat(columns, (c) => c.key, (c) => renderCell(c, c.id === activeCol, cellSlot(row.slot, c.slot), false, c.kind === "selection" ? rowCheckbox(row.index, selected, checkboxes) : placeholder(state, c.id === retryCol ? retry : void 0, row.index, loading)))}
|
|
40
52
|
</div>`;
|
|
41
53
|
}
|
|
42
54
|
/**
|
|
@@ -52,12 +64,40 @@ function renderCell(c, tabStop, slot, pinnedRow, before = nothing) {
|
|
|
52
64
|
tabindex=${tabStop ? 0 : -1}
|
|
53
65
|
data-col=${c.id}
|
|
54
66
|
data-align=${c.align}
|
|
67
|
+
data-wrap=${c.wrap ? "" : nothing}
|
|
55
68
|
data-pin=${c.pin ?? nothing}
|
|
56
|
-
style=${
|
|
69
|
+
style=${cellStyle(c) ?? nothing}
|
|
57
70
|
>
|
|
58
71
|
${before}<slot name=${slot}></slot>
|
|
59
72
|
</div>`;
|
|
60
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* The id of the leftmost rendered data column (in reading order). Rendered
|
|
76
|
+
* columns are in pool order with `virtualizeColumns`.
|
|
77
|
+
*/
|
|
78
|
+
function firstDataColumn(columns) {
|
|
79
|
+
let first;
|
|
80
|
+
for (const c of columns) if (c.kind === "data" && (!first || c.colIndex < first.colIndex)) first = c;
|
|
81
|
+
return first?.id;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A row's checkbox in the selection column, for a row that can be selected.
|
|
85
|
+
* It isn't focusable: the cell holds focus. `live()` keeps it showing the
|
|
86
|
+
* selection when a controlled host turns a change down.
|
|
87
|
+
*/
|
|
88
|
+
function rowCheckbox(index, selected, checkboxes) {
|
|
89
|
+
if (selected === void 0 || !checkboxes) return nothing;
|
|
90
|
+
return html`<input
|
|
91
|
+
type="checkbox"
|
|
92
|
+
part="selection-checkbox"
|
|
93
|
+
tabindex="-1"
|
|
94
|
+
aria-label="Select row"
|
|
95
|
+
data-row=${index}
|
|
96
|
+
.checked=${live(selected)}
|
|
97
|
+
@click=${checkboxes.row}
|
|
98
|
+
@mousedown=${checkboxes.press}
|
|
99
|
+
/>`;
|
|
100
|
+
}
|
|
61
101
|
/** What an unloaded cell shows: a skeleton bar, or the Retry button in a failed page's first cell. */
|
|
62
102
|
function placeholder(state, retryLabel, index, loading) {
|
|
63
103
|
if (state === "loaded") return nothing;
|
package/dist/render/body.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"body.js","names":[],"sources":["../../src/render/body.ts"],"sourcesContent":["import { html, nothing } from 'lit';\nimport { repeat } from 'lit/directives/repeat.js';\nimport { styleMap } from 'lit/directives/style-map.js';\nimport type {
|
|
1
|
+
{"version":3,"file":"body.js","names":[],"sources":["../../src/render/body.ts"],"sourcesContent":["import { html, nothing } from 'lit';\nimport { live } from 'lit/directives/live.js';\nimport { repeat } from 'lit/directives/repeat.js';\nimport { styleMap } from 'lit/directives/style-map.js';\nimport type { RowState } from '@argus-table/core';\nimport type { RenderedColumn } from '../controllers/column-window.js';\nimport type { WindowRow } from '../controllers/virtual-controller.js';\nimport { cellParts, cellSlot, cellStyle, type CheckboxHandlers } from './content.js';\n\ntype Columns<T> = readonly RenderedColumn<T>[];\n\n/** The cell holding the grid's tab stop: a row index and a column id. */\nexport interface ActiveCell {\n index: number;\n col: string;\n}\n\n/** Row loading state for `dataSource` tables. */\nexport interface Loading {\n /** Whether a row's data is loaded, still loading, or its page failed. */\n stateAt(index: number): RowState;\n /** Rows that show a Retry button (one per failed page in view), with its accessible label. */\n retry: ReadonlyMap<number, string>;\n /** Click handler of the Retry buttons; the button carries the row index in `data-row`. */\n onRetry: (event: Event) => void;\n}\n\n/**\n * Whether the row at an index is selected; `undefined` when it can't be\n * selected (selection is off, or the row hasn't loaded).\n */\nexport type SelectedAt = (index: number) => boolean | undefined;\n\n/**\n * The virtualized body. Rows are keyed by pool slot, so a row entering the window\n * reuses the DOM of one that left (ADR 0001), and they stay in pool order: screen\n * readers get the position from `aria-rowindex`. `firstRowIndex` is the\n * `aria-rowindex` of row 0: after the header and the rows pinned to the top.\n * `inset`: rows of different heights (`rowHeight: 'auto'`) are placed with\n * `inset-block-start` and render at least their measured `size`, which is on\n * the controller's px grid, so they stay flush however far down they are.\n */\nexport function renderBody<T>(\n rows: readonly WindowRow[],\n columns: Columns<T>,\n totalSize: number,\n active: ActiveCell | undefined,\n firstRowIndex: number,\n loading?: Loading,\n selectedAt?: SelectedAt,\n checkboxes?: CheckboxHandlers,\n inset = false,\n) {\n return html`<div\n part=\"body\"\n role=\"rowgroup\"\n style=${styleMap({ blockSize: `${String(totalSize)}px` })}\n >\n ${repeat(\n rows,\n (r) => r.slot,\n (r) =>\n renderRow(\n r,\n columns,\n r.index === active?.index ? active.col : undefined,\n firstRowIndex,\n loading,\n selectedAt?.(r.index),\n checkboxes,\n inset,\n ),\n )}\n </div>`;\n}\n\n/**\n * One body row. Every cell is focusable (`tabindex=-1`) so clicks focus it; the\n * active one is the grid's single tab stop (roving tabindex). A row whose data\n * isn't loaded shows skeleton bars, and a Retry button if its page failed.\n * `selected` is `undefined` for a row that can't be selected: it then has no\n * `aria-selected`, and no checkbox in the selection column.\n */\nfunction renderRow<T>(\n row: WindowRow,\n columns: Columns<T>,\n activeCol: string | undefined,\n firstRowIndex: number,\n loading: Loading | undefined,\n selected: boolean | undefined,\n checkboxes: CheckboxHandlers | undefined,\n inset: boolean,\n) {\n const state = loading?.stateAt(row.index) ?? 'loaded';\n let parts = row.index % 2 === 0 ? 'row row-even' : 'row row-odd';\n if (state !== 'loaded') parts += ' row-loading';\n if (selected) parts += ' row-selected';\n const retry = state === 'error' ? loading?.retry.get(row.index) : undefined;\n // The Retry button goes in the first data column, after a selection column.\n const retryCol = firstDataColumn(columns);\n return html`<div\n part=${parts}\n class=\"row\"\n role=\"row\"\n aria-rowindex=${row.index + firstRowIndex}\n aria-busy=${state === 'loading' ? 'true' : nothing}\n aria-selected=${selected === undefined ? nothing : String(selected)}\n style=${styleMap(\n inset\n ? {\n insetBlockStart: `${String(row.start)}px`,\n minBlockSize: row.size === undefined ? undefined : `${String(row.size)}px`,\n }\n : { transform: `translateY(${String(row.start)}px)` },\n )}\n >\n ${repeat(\n columns,\n (c) => c.key,\n (c) =>\n renderCell(\n c,\n c.id === activeCol,\n cellSlot(row.slot, c.slot),\n false,\n c.kind === 'selection'\n ? rowCheckbox(row.index, selected, checkboxes)\n : placeholder(state, c.id === retryCol ? retry : undefined, row.index, loading),\n ),\n )}\n </div>`;\n}\n\n/**\n * A body or pinned-row cell. Every cell is focusable (`tabindex=-1`) so clicks\n * focus it; `tabStop` marks the grid's single tab stop. `before` goes ahead of\n * the slot (a loading row's skeleton bar or Retry button).\n */\nexport function renderCell<T>(\n c: RenderedColumn<T>,\n tabStop: boolean,\n slot: string,\n pinnedRow: boolean,\n before: unknown = nothing,\n) {\n return html`<div\n part=${cellParts(c, pinnedRow)}\n role=\"gridcell\"\n aria-colindex=${c.colIndex}\n tabindex=${tabStop ? 0 : -1}\n data-col=${c.id}\n data-align=${c.align}\n data-wrap=${c.wrap ? '' : nothing}\n data-pin=${c.pin ?? nothing}\n style=${cellStyle(c) ?? nothing}\n >\n ${before}<slot name=${slot}></slot>\n </div>`;\n}\n\n/**\n * The id of the leftmost rendered data column (in reading order). Rendered\n * columns are in pool order with `virtualizeColumns`.\n */\nfunction firstDataColumn<T>(columns: Columns<T>): string | undefined {\n let first: RenderedColumn<T> | undefined;\n for (const c of columns) {\n if (c.kind === 'data' && (!first || c.colIndex < first.colIndex)) first = c;\n }\n return first?.id;\n}\n\n/**\n * A row's checkbox in the selection column, for a row that can be selected.\n * It isn't focusable: the cell holds focus. `live()` keeps it showing the\n * selection when a controlled host turns a change down.\n */\nfunction rowCheckbox(\n index: number,\n selected: boolean | undefined,\n checkboxes: CheckboxHandlers | undefined,\n) {\n if (selected === undefined || !checkboxes) return nothing;\n return html`<input\n type=\"checkbox\"\n part=\"selection-checkbox\"\n tabindex=\"-1\"\n aria-label=\"Select row\"\n data-row=${index}\n .checked=${live(selected)}\n @click=${checkboxes.row}\n @mousedown=${checkboxes.press}\n />`;\n}\n\n/** What an unloaded cell shows: a skeleton bar, or the Retry button in a failed page's first cell. */\nfunction placeholder(\n state: RowState,\n retryLabel: string | undefined,\n index: number,\n loading: Loading | undefined,\n) {\n if (state === 'loaded') return nothing;\n if (retryLabel !== undefined && loading) {\n return html`<button\n part=\"retry\"\n type=\"button\"\n tabindex=\"-1\"\n data-row=${index}\n aria-label=${retryLabel}\n @click=${loading.onRetry}\n >\n Retry\n </button>`;\n }\n return html`<div\n part=${state === 'error' ? 'skeleton skeleton-error' : 'skeleton'}\n aria-hidden=\"true\"\n ></div>`;\n}\n"],"mappings":";;;;;;;;;;;;;;;AA0CA,SAAgB,WACd,MACA,SACA,WACA,QACA,eACA,SACA,YACA,YACA,QAAQ,OACR;CACA,OAAO,IAAI;;;YAGD,SAAS,EAAE,WAAW,GAAG,OAAO,SAAS,EAAE,IAAI,CAAC,EAAE;;MAExD,OACA,OACC,MAAM,EAAE,OACR,MACC,UACE,GACA,SACA,EAAE,UAAU,QAAQ,QAAQ,OAAO,MAAM,KAAA,GACzC,eACA,SACA,aAAa,EAAE,KAAK,GACpB,YACA,KACF,CACJ,EAAE;;AAEN;;;;;;;;AASA,SAAS,UACP,KACA,SACA,WACA,eACA,SACA,UACA,YACA,OACA;CACA,MAAM,QAAQ,SAAS,QAAQ,IAAI,KAAK,KAAK;CAC7C,IAAI,QAAQ,IAAI,QAAQ,MAAM,IAAI,iBAAiB;CACnD,IAAI,UAAU,UAAU,SAAS;CACjC,IAAI,UAAU,SAAS;CACvB,MAAM,QAAQ,UAAU,UAAU,SAAS,MAAM,IAAI,IAAI,KAAK,IAAI,KAAA;CAElE,MAAM,WAAW,gBAAgB,OAAO;CACxC,OAAO,IAAI;WACF,MAAM;;;oBAGG,IAAI,QAAQ,cAAc;gBAC9B,UAAU,YAAY,SAAS,QAAQ;oBACnC,aAAa,KAAA,IAAY,UAAU,OAAO,QAAQ,EAAE;YAC5D,SACN,QACI;EACE,iBAAiB,GAAG,OAAO,IAAI,KAAK,EAAE;EACtC,cAAc,IAAI,SAAS,KAAA,IAAY,KAAA,IAAY,GAAG,OAAO,IAAI,IAAI,EAAE;CACzE,IACA,EAAE,WAAW,cAAc,OAAO,IAAI,KAAK,EAAE,KAAK,CACxD,EAAE;;MAEA,OACA,UACC,MAAM,EAAE,MACR,MACC,WACE,GACA,EAAE,OAAO,WACT,SAAS,IAAI,MAAM,EAAE,IAAI,GACzB,OACA,EAAE,SAAS,cACP,YAAY,IAAI,OAAO,UAAU,UAAU,IAC3C,YAAY,OAAO,EAAE,OAAO,WAAW,QAAQ,KAAA,GAAW,IAAI,OAAO,OAAO,CAClF,CACJ,EAAE;;AAEN;;;;;;AAOA,SAAgB,WACd,GACA,SACA,MACA,WACA,SAAkB,SAClB;CACA,OAAO,IAAI;WACF,UAAU,GAAG,SAAS,EAAE;;oBAEf,EAAE,SAAS;eAChB,UAAU,IAAI,GAAG;eACjB,EAAE,GAAG;iBACH,EAAE,MAAM;gBACT,EAAE,OAAO,KAAK,QAAQ;eACvB,EAAE,OAAO,QAAQ;YACpB,UAAU,CAAC,KAAK,QAAQ;;MAE9B,OAAO,aAAa,KAAK;;AAE/B;;;;;AAMA,SAAS,gBAAmB,SAAyC;CACnE,IAAI;CACJ,KAAK,MAAM,KAAK,SACd,IAAI,EAAE,SAAS,WAAW,CAAC,SAAS,EAAE,WAAW,MAAM,WAAW,QAAQ;CAE5E,OAAO,OAAO;AAChB;;;;;;AAOA,SAAS,YACP,OACA,UACA,YACA;CACA,IAAI,aAAa,KAAA,KAAa,CAAC,YAAY,OAAO;CAClD,OAAO,IAAI;;;;;eAKE,MAAM;eACN,KAAK,QAAQ,EAAE;aACjB,WAAW,IAAI;iBACX,WAAW,MAAM;;AAElC;;AAGA,SAAS,YACP,OACA,YACA,OACA,SACA;CACA,IAAI,UAAU,UAAU,OAAO;CAC/B,IAAI,eAAe,KAAA,KAAa,SAC9B,OAAO,IAAI;;;;iBAIE,MAAM;mBACJ,WAAW;eACf,QAAQ,QAAQ;;;;CAK7B,OAAO,IAAI;WACF,UAAU,UAAU,4BAA4B,WAAW;;;AAGtE"}
|
package/dist/render/cells.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { cellSlot, formatValue, headerSlot, pinnedSlot
|
|
1
|
+
import { cellSlot, formatValue, headerSlot, pinnedSlot } from "./content.js";
|
|
2
|
+
import { content } from "./content-directive.js";
|
|
2
3
|
import { html, render } from "lit";
|
|
3
4
|
import { repeat } from "lit/directives/repeat.js";
|
|
4
5
|
// src/render/cells.ts
|
|
@@ -7,35 +8,37 @@ import { repeat } from "lit/directives/repeat.js";
|
|
|
7
8
|
* (a slot only takes the host's own children). Keyed and ordered by pool slot,
|
|
8
9
|
* so recycled content updates in place instead of moving (ADR 0001). The part
|
|
9
10
|
* ends at `renderBefore`, so the host's other children (`<argus-column>`) stay
|
|
10
|
-
* outside it, wherever they are added.
|
|
11
|
+
* outside it, wherever they are added. `rendered` are the columns the shadow
|
|
12
|
+
* root renders, with their slots and keys. What `header` and `cell` return goes
|
|
13
|
+
* through `content()`, so foreign content reaches the table's `contentRenderer`.
|
|
14
|
+
* Returns the part, which the table disconnects when it is disconnected.
|
|
11
15
|
*/
|
|
12
|
-
function renderLightDom(host, model, rows, renderBefore) {
|
|
13
|
-
const columns =
|
|
14
|
-
render(html`${repeat(columns, (c) => c.
|
|
16
|
+
function renderLightDom(host, model, rows, rendered, renderBefore) {
|
|
17
|
+
const columns = rendered.filter((c) => c.kind === "data");
|
|
18
|
+
return render(html`${repeat(columns, (c) => c.key, (c) => html`<div slot=${headerSlot(c.slot)}>${headerContent(c)}</div>`)}${pinnedContent(model, columns, "top")}${repeat(rows, (r) => r.slot, (r) => repeat(columns, (c) => c.key, (c) => html`<div slot=${cellSlot(r.slot, c.slot)}>${cellContent(model, r.index, c)}</div>`))}${pinnedContent(model, columns, "bottom")}`, host, {
|
|
15
19
|
host,
|
|
16
20
|
renderBefore
|
|
17
21
|
});
|
|
18
22
|
}
|
|
19
23
|
function headerContent(column) {
|
|
20
24
|
const { header } = column;
|
|
21
|
-
return
|
|
25
|
+
return content(typeof header === "function" ? header() : header);
|
|
22
26
|
}
|
|
23
27
|
/** Content of the rows pinned to one side, slotted by side, index and column. */
|
|
24
|
-
function pinnedContent(model, side) {
|
|
25
|
-
|
|
26
|
-
return Array.from({ length: model.pinnedCount(side) }, (_, i) => i).map((i) => repeat(columns, (c) => c.id, (c) => html`<div slot=${pinnedSlot(side, i, c.colIndex)}>${pinnedCell(model, side, i, c)}</div>`));
|
|
28
|
+
function pinnedContent(model, columns, side) {
|
|
29
|
+
return Array.from({ length: model.pinnedCount(side) }, (_, i) => i).map((i) => repeat(columns, (c) => c.key, (c) => html`<div slot=${pinnedSlot(side, i, c.slot)}>${pinnedCell(model, side, i, c)}</div>`));
|
|
27
30
|
}
|
|
28
31
|
function pinnedCell(model, side, index, column) {
|
|
29
32
|
if (column.cell) {
|
|
30
33
|
const context = model.pinnedCellContext(side, index, column.id);
|
|
31
|
-
return context ?
|
|
34
|
+
return context ? content(column.cell(context.row, context)) : "";
|
|
32
35
|
}
|
|
33
36
|
return column.hasAccessor ? formatValue(model.pinnedValueAt(side, index, column.id)) : "";
|
|
34
37
|
}
|
|
35
38
|
function cellContent(model, index, column) {
|
|
36
39
|
if (column.cell) {
|
|
37
40
|
const context = model.cellContext(index, column.id);
|
|
38
|
-
return context ?
|
|
41
|
+
return context ? content(column.cell(context.row, context)) : "";
|
|
39
42
|
}
|
|
40
43
|
return column.hasAccessor ? formatValue(model.valueAt(index, column.id)) : "";
|
|
41
44
|
}
|
package/dist/render/cells.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cells.js","names":[],"sources":["../../src/render/cells.ts"],"sourcesContent":["import { html, render } from 'lit';\nimport { repeat } from 'lit/directives/repeat.js';\nimport type { ColumnView, RowPin, TableModel } from '@argus-table/core';\nimport type { WindowRow } from '../controllers/virtual-controller.js';\nimport type { CellContent } from '../types.js';\nimport { cellSlot, formatValue, headerSlot, pinnedSlot
|
|
1
|
+
{"version":3,"file":"cells.js","names":[],"sources":["../../src/render/cells.ts"],"sourcesContent":["import { html, render, type RootPart } from 'lit';\nimport { repeat } from 'lit/directives/repeat.js';\nimport type { ColumnView, RowPin, TableModel } from '@argus-table/core';\nimport type { RenderedColumn } from '../controllers/column-window.js';\nimport type { WindowRow } from '../controllers/virtual-controller.js';\nimport type { CellContent } from '../types.js';\nimport { cellSlot, formatValue, headerSlot, pinnedSlot } from './content.js';\nimport { content } from './content-directive.js';\n\ntype Column<T> = ColumnView<T, CellContent>;\n\n/**\n * Renders header and cell content into the host's light DOM, as direct children\n * (a slot only takes the host's own children). Keyed and ordered by pool slot,\n * so recycled content updates in place instead of moving (ADR 0001). The part\n * ends at `renderBefore`, so the host's other children (`<argus-column>`) stay\n * outside it, wherever they are added. `rendered` are the columns the shadow\n * root renders, with their slots and keys. What `header` and `cell` return goes\n * through `content()`, so foreign content reaches the table's `contentRenderer`.\n * Returns the part, which the table disconnects when it is disconnected.\n */\nexport function renderLightDom<T>(\n host: HTMLElement,\n model: TableModel<T, CellContent>,\n rows: readonly WindowRow[],\n rendered: readonly RenderedColumn<T>[],\n renderBefore: ChildNode,\n): RootPart {\n // The selection column renders its checkboxes in the shadow root: no content here.\n const columns = rendered.filter((c) => c.kind === 'data');\n return render(\n html`${repeat(\n columns,\n (c) => c.key,\n (c) => html`<div slot=${headerSlot(c.slot)}>${headerContent(c)}</div>`,\n )}${pinnedContent(model, columns, 'top')}${repeat(\n rows,\n (r) => r.slot,\n (r) =>\n repeat(\n columns,\n (c) => c.key,\n (c) =>\n html`<div slot=${cellSlot(r.slot, c.slot)}>${cellContent(model, r.index, c)}</div>`,\n ),\n )}${pinnedContent(model, columns, 'bottom')}`,\n host,\n { host, renderBefore },\n );\n}\n\nfunction headerContent<T>(column: Column<T>): unknown {\n const { header } = column;\n return content(typeof header === 'function' ? header() : header);\n}\n\n/** Content of the rows pinned to one side, slotted by side, index and column. */\nfunction pinnedContent<T>(\n model: TableModel<T, CellContent>,\n columns: readonly RenderedColumn<T>[],\n side: RowPin,\n) {\n const rows = Array.from({ length: model.pinnedCount(side) }, (_, i) => i);\n return rows.map((i) =>\n repeat(\n columns,\n (c) => c.key,\n (c) => html`<div slot=${pinnedSlot(side, i, c.slot)}>${pinnedCell(model, side, i, c)}</div>`,\n ),\n );\n}\n\nfunction pinnedCell<T>(\n model: TableModel<T, CellContent>,\n side: RowPin,\n index: number,\n column: Column<T>,\n) {\n if (column.cell) {\n const context = model.pinnedCellContext(side, index, column.id);\n return context ? content(column.cell(context.row, context)) : '';\n }\n return column.hasAccessor ? formatValue(model.pinnedValueAt(side, index, column.id)) : '';\n}\n\nfunction cellContent<T>(model: TableModel<T, CellContent>, index: number, column: Column<T>) {\n if (column.cell) {\n const context = model.cellContext(index, column.id);\n return context ? content(column.cell(context.row, context)) : '';\n }\n return column.hasAccessor ? formatValue(model.valueAt(index, column.id)) : '';\n}\n"],"mappings":";;;;;;;;;;;;;;;AAqBA,SAAgB,eACd,MACA,OACA,MACA,UACA,cACU;CAEV,MAAM,UAAU,SAAS,QAAQ,MAAM,EAAE,SAAS,MAAM;CACxD,OAAO,OACL,IAAI,GAAG,OACL,UACC,MAAM,EAAE,MACR,MAAM,IAAI,aAAa,WAAW,EAAE,IAAI,EAAE,GAAG,cAAc,CAAC,EAAE,OACjE,IAAI,cAAc,OAAO,SAAS,KAAK,IAAI,OACzC,OACC,MAAM,EAAE,OACR,MACC,OACE,UACC,MAAM,EAAE,MACR,MACC,IAAI,aAAa,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,YAAY,OAAO,EAAE,OAAO,CAAC,EAAE,OAChF,CACJ,IAAI,cAAc,OAAO,SAAS,QAAQ,KAC1C,MACA;EAAE;EAAM;CAAa,CACvB;AACF;AAEA,SAAS,cAAiB,QAA4B;CACpD,MAAM,EAAE,WAAW;CACnB,OAAO,QAAQ,OAAO,WAAW,aAAa,OAAO,IAAI,MAAM;AACjE;;AAGA,SAAS,cACP,OACA,SACA,MACA;CAEA,OADa,MAAM,KAAK,EAAE,QAAQ,MAAM,YAAY,IAAI,EAAE,IAAI,GAAG,MAAM,CAChE,CAAA,CAAK,KAAK,MACf,OACE,UACC,MAAM,EAAE,MACR,MAAM,IAAI,aAAa,WAAW,MAAM,GAAG,EAAE,IAAI,EAAE,GAAG,WAAW,OAAO,MAAM,GAAG,CAAC,EAAE,OACvF,CACF;AACF;AAEA,SAAS,WACP,OACA,MACA,OACA,QACA;CACA,IAAI,OAAO,MAAM;EACf,MAAM,UAAU,MAAM,kBAAkB,MAAM,OAAO,OAAO,EAAE;EAC9D,OAAO,UAAU,QAAQ,OAAO,KAAK,QAAQ,KAAK,OAAO,CAAC,IAAI;CAChE;CACA,OAAO,OAAO,cAAc,YAAY,MAAM,cAAc,MAAM,OAAO,OAAO,EAAE,CAAC,IAAI;AACzF;AAEA,SAAS,YAAe,OAAmC,OAAe,QAAmB;CAC3F,IAAI,OAAO,MAAM;EACf,MAAM,UAAU,MAAM,YAAY,OAAO,OAAO,EAAE;EAClD,OAAO,UAAU,QAAQ,OAAO,KAAK,QAAQ,KAAK,OAAO,CAAC,IAAI;CAChE;CACA,OAAO,OAAO,cAAc,YAAY,MAAM,QAAQ,OAAO,OAAO,EAAE,CAAC,IAAI;AAC7E"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { toChild } from "./content.js";
|
|
2
|
+
import { nothing } from "lit";
|
|
3
|
+
import { AsyncDirective, PartType, directive } from "lit/async-directive.js";
|
|
4
|
+
// src/render/content-directive.ts
|
|
5
|
+
/** Renderers that rendered or released since the last flush, per table. */
|
|
6
|
+
var dirty = /* @__PURE__ */ new WeakMap();
|
|
7
|
+
var warned = false;
|
|
8
|
+
function markDirty(host, renderer) {
|
|
9
|
+
let set = dirty.get(host);
|
|
10
|
+
if (!set) dirty.set(host, set = /* @__PURE__ */ new Set());
|
|
11
|
+
set.add(renderer);
|
|
12
|
+
}
|
|
13
|
+
/** Calls `flush()` on the renderers that rendered or released for `host` since the last call. */
|
|
14
|
+
function flushContent(host) {
|
|
15
|
+
const set = dirty.get(host);
|
|
16
|
+
if (!set?.size) return;
|
|
17
|
+
const renderers = [...set];
|
|
18
|
+
set.clear();
|
|
19
|
+
for (const renderer of renderers) renderer.flush?.();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Whether the table renders `value` itself: strings, numbers, Lit templates and
|
|
23
|
+
* directive results, DOM nodes, `nothing` and empty values, and arrays of those.
|
|
24
|
+
* Anything else is foreign: it goes to the `contentRenderer`.
|
|
25
|
+
*/
|
|
26
|
+
function isForeign(value) {
|
|
27
|
+
if (value === null || typeof value !== "object") return false;
|
|
28
|
+
if (Array.isArray(value)) return value.some(isForeign);
|
|
29
|
+
if (value instanceof Node) return false;
|
|
30
|
+
return !("_$litType$" in value) && !("_$litDirective$" in value);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Header and cell content. Foreign content renders through the table's
|
|
34
|
+
* `contentRenderer` into a target element kept while this part lives, so a pooled
|
|
35
|
+
* cell updates its framework content in place as it shows other rows.
|
|
36
|
+
*/
|
|
37
|
+
var ContentDirective = class extends AsyncDirective {
|
|
38
|
+
#target;
|
|
39
|
+
/** The renderer that rendered into the target, and the table it belongs to. */
|
|
40
|
+
#renderer;
|
|
41
|
+
#host;
|
|
42
|
+
/** The last foreign content, rendered again on reconnect. */
|
|
43
|
+
#content;
|
|
44
|
+
constructor(part) {
|
|
45
|
+
super(part);
|
|
46
|
+
if (part.type !== PartType.CHILD) throw new Error("argus-table: content() only works in child positions.");
|
|
47
|
+
}
|
|
48
|
+
render(value) {
|
|
49
|
+
return isForeign(value) ? nothing : toChild(value);
|
|
50
|
+
}
|
|
51
|
+
update(part, [value]) {
|
|
52
|
+
if (!isForeign(value)) {
|
|
53
|
+
this.#release();
|
|
54
|
+
this.#content = void 0;
|
|
55
|
+
return this.render(value);
|
|
56
|
+
}
|
|
57
|
+
const host = part.options?.host;
|
|
58
|
+
const renderer = host?.contentRenderer;
|
|
59
|
+
if (!host || !renderer) {
|
|
60
|
+
this.#release();
|
|
61
|
+
if (!warned) {
|
|
62
|
+
warned = true;
|
|
63
|
+
console.warn("argus-table: a header or cell returned content the table can’t render (set contentRenderer, or use a framework wrapper).");
|
|
64
|
+
}
|
|
65
|
+
return nothing;
|
|
66
|
+
}
|
|
67
|
+
if (renderer !== this.#renderer) this.#release();
|
|
68
|
+
const target = this.#target ??= targetElement();
|
|
69
|
+
this.#renderer = renderer;
|
|
70
|
+
this.#host = host;
|
|
71
|
+
this.#content = value;
|
|
72
|
+
renderer.render(target, value);
|
|
73
|
+
markDirty(host, renderer);
|
|
74
|
+
return target;
|
|
75
|
+
}
|
|
76
|
+
disconnected() {
|
|
77
|
+
const content = this.#content;
|
|
78
|
+
this.#release();
|
|
79
|
+
this.#content = content;
|
|
80
|
+
}
|
|
81
|
+
reconnected() {
|
|
82
|
+
const host = this.#host;
|
|
83
|
+
const renderer = host?.contentRenderer;
|
|
84
|
+
if (!host || !renderer || this.#content === void 0 || !this.#target) return;
|
|
85
|
+
this.#renderer = renderer;
|
|
86
|
+
renderer.render(this.#target, this.#content);
|
|
87
|
+
markDirty(host, renderer);
|
|
88
|
+
}
|
|
89
|
+
#release() {
|
|
90
|
+
const renderer = this.#renderer;
|
|
91
|
+
const host = this.#host;
|
|
92
|
+
if (!renderer || !host || !this.#target) return;
|
|
93
|
+
this.#renderer = void 0;
|
|
94
|
+
renderer.release(this.#target);
|
|
95
|
+
markDirty(host, renderer);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
/** A target: invisible to layout, so the content lays out as the cell's own. */
|
|
99
|
+
function targetElement() {
|
|
100
|
+
const target = document.createElement("div");
|
|
101
|
+
target.className = "argus-content";
|
|
102
|
+
target.style.display = "contents";
|
|
103
|
+
return target;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Header or cell content for a template of your own. The table renders what
|
|
107
|
+
* `header` and `cell` return through it already; a plugin that nests a column's
|
|
108
|
+
* content in its own template (`html\`<b>${content(own)}</b>\``) wraps it, so
|
|
109
|
+
* framework content (React elements, Vue VNodes) still reaches the
|
|
110
|
+
* `contentRenderer`. Only in templates the table renders.
|
|
111
|
+
*/
|
|
112
|
+
var content = directive(ContentDirective);
|
|
113
|
+
export { content, flushContent, isForeign };
|
|
114
|
+
|
|
115
|
+
//# sourceMappingURL=content-directive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-directive.js","names":[],"sources":["../../src/render/content-directive.ts"],"sourcesContent":["import { nothing, type ChildPart } from 'lit';\nimport { AsyncDirective, directive, PartType, type PartInfo } from 'lit/async-directive.js';\nimport { toChild } from './content.js';\nimport type { CellContent } from '../types.js';\n\n/**\n * Renders content the table doesn't render itself, such as React elements, Vue\n * VNodes or Angular templates, into a target element the table keeps. Set it as the\n * table's `contentRenderer`; the framework wrappers do.\n */\nexport interface ContentRenderer {\n /**\n * Renders `content` into `target`, or updates what is there. Called on every update\n * for each header and cell showing foreign content. A target is reused: when the\n * table scrolls, the same target shows other rows.\n */\n render(target: HTMLElement, content: unknown): void;\n /**\n * `target` shows no foreign content any more: its cell scrolled away and wasn't\n * reused, its row isn't loaded, its column left, or the table was disconnected.\n * Unmount what `render` put there.\n */\n release(target: HTMLElement): void;\n /**\n * Called once after each update in which `render` or `release` ran, before the\n * table measures content (`width: 'auto'`). Commit batched work here, synchronously.\n */\n flush?(): void;\n}\n\n/** What the directive reads from the table it renders into. */\ninterface ContentHost {\n readonly contentRenderer?: ContentRenderer | undefined;\n}\n\n/** Renderers that rendered or released since the last flush, per table. */\nconst dirty = new WeakMap<object, Set<ContentRenderer>>();\nlet warned = false;\n\nfunction markDirty(host: object, renderer: ContentRenderer): void {\n let set = dirty.get(host);\n if (!set) dirty.set(host, (set = new Set()));\n set.add(renderer);\n}\n\n/** Calls `flush()` on the renderers that rendered or released for `host` since the last call. */\nexport function flushContent(host: object): void {\n const set = dirty.get(host);\n if (!set?.size) return;\n const renderers = [...set];\n set.clear();\n for (const renderer of renderers) renderer.flush?.();\n}\n\n/**\n * Whether the table renders `value` itself: strings, numbers, Lit templates and\n * directive results, DOM nodes, `nothing` and empty values, and arrays of those.\n * Anything else is foreign: it goes to the `contentRenderer`.\n */\nexport function isForeign(value: unknown): boolean {\n if (value === null || typeof value !== 'object') return false;\n if (Array.isArray(value)) return value.some(isForeign);\n if (value instanceof Node) return false;\n return !('_$litType$' in value) && !('_$litDirective$' in value);\n}\n\n/**\n * Header and cell content. Foreign content renders through the table's\n * `contentRenderer` into a target element kept while this part lives, so a pooled\n * cell updates its framework content in place as it shows other rows.\n */\nclass ContentDirective extends AsyncDirective {\n #target: HTMLElement | undefined;\n /** The renderer that rendered into the target, and the table it belongs to. */\n #renderer: ContentRenderer | undefined;\n #host: object | undefined;\n /** The last foreign content, rendered again on reconnect. */\n #content: unknown;\n\n constructor(part: PartInfo) {\n super(part);\n if (part.type !== PartType.CHILD) {\n throw new Error('argus-table: content() only works in child positions.');\n }\n }\n\n render(value: unknown): unknown {\n return isForeign(value) ? nothing : toChild(value as CellContent | null | undefined);\n }\n\n override update(part: ChildPart, [value]: [unknown]): unknown {\n if (!isForeign(value)) {\n this.#release();\n this.#content = undefined;\n return this.render(value);\n }\n const host = part.options?.host;\n const renderer = (host as ContentHost | undefined)?.contentRenderer;\n if (!host || !renderer) {\n this.#release();\n if (!warned) {\n warned = true;\n console.warn(\n 'argus-table: a header or cell returned content the table can’t render (set contentRenderer, or use a framework wrapper).',\n );\n }\n return nothing;\n }\n if (renderer !== this.#renderer) this.#release();\n const target = (this.#target ??= targetElement());\n this.#renderer = renderer;\n this.#host = host;\n this.#content = value;\n renderer.render(target, value);\n markDirty(host, renderer);\n return target;\n }\n\n protected override disconnected(): void {\n const content = this.#content;\n this.#release();\n // Kept for reconnected(): the part is the same, and so is its content.\n this.#content = content;\n }\n\n protected override reconnected(): void {\n const host = this.#host;\n const renderer = (host as ContentHost | undefined)?.contentRenderer;\n if (!host || !renderer || this.#content === undefined || !this.#target) return;\n this.#renderer = renderer;\n renderer.render(this.#target, this.#content);\n markDirty(host, renderer);\n }\n\n #release(): void {\n const renderer = this.#renderer;\n const host = this.#host;\n if (!renderer || !host || !this.#target) return;\n this.#renderer = undefined;\n renderer.release(this.#target);\n markDirty(host, renderer);\n }\n}\n\n/** A target: invisible to layout, so the content lays out as the cell's own. */\nfunction targetElement(): HTMLElement {\n const target = document.createElement('div');\n target.className = 'argus-content';\n target.style.display = 'contents';\n return target;\n}\n\n/**\n * Header or cell content for a template of your own. The table renders what\n * `header` and `cell` return through it already; a plugin that nests a column's\n * content in its own template (`html\\`<b>${content(own)}</b>\\``) wraps it, so\n * framework content (React elements, Vue VNodes) still reaches the\n * `contentRenderer`. Only in templates the table renders.\n */\nexport const content = directive(ContentDirective);\n"],"mappings":";;;;;AAoCA,IAAM,wBAAQ,IAAI,QAAsC;AACxD,IAAI,SAAS;AAEb,SAAS,UAAU,MAAc,UAAiC;CAChE,IAAI,MAAM,MAAM,IAAI,IAAI;CACxB,IAAI,CAAC,KAAK,MAAM,IAAI,MAAO,sBAAM,IAAI,IAAI,CAAE;CAC3C,IAAI,IAAI,QAAQ;AAClB;;AAGA,SAAgB,aAAa,MAAoB;CAC/C,MAAM,MAAM,MAAM,IAAI,IAAI;CAC1B,IAAI,CAAC,KAAK,MAAM;CAChB,MAAM,YAAY,CAAC,GAAG,GAAG;CACzB,IAAI,MAAM;CACV,KAAK,MAAM,YAAY,WAAW,SAAS,QAAQ;AACrD;;;;;;AAOA,SAAgB,UAAU,OAAyB;CACjD,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU,OAAO;CACxD,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO,MAAM,KAAK,SAAS;CACrD,IAAI,iBAAiB,MAAM,OAAO;CAClC,OAAO,EAAE,gBAAgB,UAAU,EAAE,qBAAqB;AAC5D;;;;;;AAOA,IAAM,mBAAN,cAA+B,eAAe;CAC5C;;CAEA;CACA;;CAEA;CAEA,YAAY,MAAgB;EAC1B,MAAM,IAAI;EACV,IAAI,KAAK,SAAS,SAAS,OACzB,MAAM,IAAI,MAAM,uDAAuD;CAE3E;CAEA,OAAO,OAAyB;EAC9B,OAAO,UAAU,KAAK,IAAI,UAAU,QAAQ,KAAuC;CACrF;CAEA,OAAgB,MAAiB,CAAC,QAA4B;EAC5D,IAAI,CAAC,UAAU,KAAK,GAAG;GACrB,KAAK,SAAS;GACd,KAAK,WAAW,KAAA;GAChB,OAAO,KAAK,OAAO,KAAK;EAC1B;EACA,MAAM,OAAO,KAAK,SAAS;EAC3B,MAAM,WAAY,MAAkC;EACpD,IAAI,CAAC,QAAQ,CAAC,UAAU;GACtB,KAAK,SAAS;GACd,IAAI,CAAC,QAAQ;IACX,SAAS;IACT,QAAQ,KACN,0HACF;GACF;GACA,OAAO;EACT;EACA,IAAI,aAAa,KAAK,WAAW,KAAK,SAAS;EAC/C,MAAM,SAAU,KAAK,YAAY,cAAc;EAC/C,KAAK,YAAY;EACjB,KAAK,QAAQ;EACb,KAAK,WAAW;EAChB,SAAS,OAAO,QAAQ,KAAK;EAC7B,UAAU,MAAM,QAAQ;EACxB,OAAO;CACT;CAEA,eAAwC;EACtC,MAAM,UAAU,KAAK;EACrB,KAAK,SAAS;EAEd,KAAK,WAAW;CAClB;CAEA,cAAuC;EACrC,MAAM,OAAO,KAAK;EAClB,MAAM,WAAY,MAAkC;EACpD,IAAI,CAAC,QAAQ,CAAC,YAAY,KAAK,aAAa,KAAA,KAAa,CAAC,KAAK,SAAS;EACxE,KAAK,YAAY;EACjB,SAAS,OAAO,KAAK,SAAS,KAAK,QAAQ;EAC3C,UAAU,MAAM,QAAQ;CAC1B;CAEA,WAAiB;EACf,MAAM,WAAW,KAAK;EACtB,MAAM,OAAO,KAAK;EAClB,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,KAAK,SAAS;EACzC,KAAK,YAAY,KAAA;EACjB,SAAS,QAAQ,KAAK,OAAO;EAC7B,UAAU,MAAM,QAAQ;CAC1B;AACF;;AAGA,SAAS,gBAA6B;CACpC,MAAM,SAAS,SAAS,cAAc,KAAK;CAC3C,OAAO,YAAY;CACnB,OAAO,MAAM,UAAU;CACvB,OAAO;AACT;;;;;;;;AASA,IAAa,UAAU,UAAU,gBAAgB"}
|
package/dist/render/content.js
CHANGED
|
@@ -12,18 +12,20 @@ function formatValue(value) {
|
|
|
12
12
|
return typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
|
-
* Private slot names (ADR 0001): cells by pool slot and column,
|
|
16
|
-
* column, pinned rows by side, index and column.
|
|
15
|
+
* Private slot names (ADR 0001): cells by row pool slot and column slot,
|
|
16
|
+
* headers by column slot, pinned rows by side, index and column slot. A
|
|
17
|
+
* column's slot is its `colIndex`, or its pool slot with `virtualizeColumns`.
|
|
17
18
|
*/
|
|
18
|
-
var cellSlot = (slot,
|
|
19
|
-
var headerSlot = (
|
|
20
|
-
var pinnedSlot = (side, index,
|
|
19
|
+
var cellSlot = (slot, colSlot) => `c-${String(slot)}-${String(colSlot)}`;
|
|
20
|
+
var headerSlot = (colSlot) => `h-${String(colSlot)}`;
|
|
21
|
+
var pinnedSlot = (side, index, colSlot) => `${side === "top" ? "t" : "b"}-${String(index)}-${String(colSlot)}`;
|
|
21
22
|
/**
|
|
22
23
|
* Part list of a body or pinned-row cell: state is in the part name (`::part()`
|
|
23
24
|
* can't take attribute selectors).
|
|
24
25
|
*/
|
|
25
26
|
function cellParts(column, pinnedRow = false) {
|
|
26
27
|
let parts = pinnedRow ? "cell cell-pinned-row" : "cell";
|
|
28
|
+
if (column.kind === "selection") parts += " cell-selection";
|
|
27
29
|
if (column.pin) parts += column.pinEdge ? " cell-pinned cell-pinned-edge" : " cell-pinned";
|
|
28
30
|
if (column.sorted) parts += " cell-sorted";
|
|
29
31
|
return parts;
|
|
@@ -33,10 +35,19 @@ function ariaSort(sorted) {
|
|
|
33
35
|
if (!sorted) return void 0;
|
|
34
36
|
return sorted === "asc" ? "ascending" : "descending";
|
|
35
37
|
}
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Inline style of a cell or header: a pinned one's sticky inset, from the
|
|
40
|
+
* start or the end edge, and with `virtualizeColumns` the grid cell it is
|
|
41
|
+
* placed in. Cells are in pool order there, not in column order, so the row is
|
|
42
|
+
* explicit too: auto-placement would wrap a cell that comes after one of a
|
|
43
|
+
* later column into a new grid row.
|
|
44
|
+
*/
|
|
45
|
+
function cellStyle(column) {
|
|
46
|
+
const pin = column.pin ? `--_pin-offset: ${String(column.offset)}px` : void 0;
|
|
47
|
+
if (column.place === void 0) return pin;
|
|
48
|
+
const place = `grid-area: 1 / ${String(column.place)}`;
|
|
49
|
+
return pin ? `${pin}; ${place}` : place;
|
|
39
50
|
}
|
|
40
|
-
export { ariaSort, cellParts, cellSlot, formatValue, headerSlot,
|
|
51
|
+
export { ariaSort, cellParts, cellSlot, cellStyle, formatValue, headerSlot, pinnedSlot, toChild };
|
|
41
52
|
|
|
42
53
|
//# sourceMappingURL=content.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content.js","names":[],"sources":["../../src/render/content.ts"],"sourcesContent":["import { nothing } from 'lit';\nimport type { CellContent } from '../types.js';\n\n/** Turns header or cell content into a Lit child. */\nexport function toChild(content: CellContent | null | undefined): unknown {\n if (content === null || content === undefined) return nothing;\n return typeof content === 'number' ? String(content) : content;\n}\n\n/** Default cell text for an accessor value. */\nexport function formatValue(value: unknown): string {\n if (value === null || value === undefined) return '';\n if (value instanceof Date) return value.toLocaleString();\n // eslint-disable-next-line @typescript-eslint/no-base-to-string -- objects are handled above\n return typeof value === 'object' ? JSON.stringify(value) : String(value);\n}\n\n/**\n * Private slot names (ADR 0001): cells by pool slot and column
|
|
1
|
+
{"version":3,"file":"content.js","names":[],"sources":["../../src/render/content.ts"],"sourcesContent":["import { nothing } from 'lit';\nimport type { CellContent } from '../types.js';\n\n/** Turns header or cell content into a Lit child. */\nexport function toChild(content: CellContent | null | undefined): unknown {\n if (content === null || content === undefined) return nothing;\n return typeof content === 'number' ? String(content) : content;\n}\n\n/** Default cell text for an accessor value. */\nexport function formatValue(value: unknown): string {\n if (value === null || value === undefined) return '';\n if (value instanceof Date) return value.toLocaleString();\n // eslint-disable-next-line @typescript-eslint/no-base-to-string -- objects are handled above\n return typeof value === 'object' ? JSON.stringify(value) : String(value);\n}\n\n/**\n * Private slot names (ADR 0001): cells by row pool slot and column slot,\n * headers by column slot, pinned rows by side, index and column slot. A\n * column's slot is its `colIndex`, or its pool slot with `virtualizeColumns`.\n */\nexport const cellSlot = (slot: number, colSlot: number) => `c-${String(slot)}-${String(colSlot)}`;\nexport const headerSlot = (colSlot: number) => `h-${String(colSlot)}`;\nexport const pinnedSlot = (side: 'top' | 'bottom', index: number, colSlot: number) =>\n `${side === 'top' ? 't' : 'b'}-${String(index)}-${String(colSlot)}`;\n\ninterface Pinnable {\n pin: 'start' | 'end' | undefined;\n pinEdge: boolean;\n}\n\n/**\n * Part list of a body or pinned-row cell: state is in the part name (`::part()`\n * can't take attribute selectors).\n */\nexport function cellParts(\n column: Pinnable & { sorted: 'asc' | 'desc' | false; kind: 'data' | 'selection' },\n pinnedRow = false,\n): string {\n let parts = pinnedRow ? 'cell cell-pinned-row' : 'cell';\n if (column.kind === 'selection') parts += ' cell-selection';\n if (column.pin) parts += column.pinEdge ? ' cell-pinned cell-pinned-edge' : ' cell-pinned';\n if (column.sorted) parts += ' cell-sorted';\n return parts;\n}\n\n/** Handlers of the selection checkboxes (`selectionCheckboxes`). */\nexport interface CheckboxHandlers {\n /** A row's checkbox; it carries the row index in `data-row`. */\n row: (event: MouseEvent) => void;\n /** The header's select-all checkbox. */\n all: (event: MouseEvent) => void;\n /** A press on any of them: keeps focus on its cell, not the checkbox. */\n press: (event: MouseEvent) => void;\n}\n\n/** `aria-sort` for a header: only the sorted column carries it (WAI-ARIA APG). */\nexport function ariaSort(sorted: 'asc' | 'desc' | false): 'ascending' | 'descending' | undefined {\n if (!sorted) return undefined;\n return sorted === 'asc' ? 'ascending' : 'descending';\n}\n\n/**\n * Inline style of a cell or header: a pinned one's sticky inset, from the\n * start or the end edge, and with `virtualizeColumns` the grid cell it is\n * placed in. Cells are in pool order there, not in column order, so the row is\n * explicit too: auto-placement would wrap a cell that comes after one of a\n * later column into a new grid row.\n */\nexport function cellStyle(\n column: Pinnable & { offset: number; place: number | undefined },\n): string | undefined {\n const pin = column.pin ? `--_pin-offset: ${String(column.offset)}px` : undefined;\n if (column.place === undefined) return pin;\n const place = `grid-area: 1 / ${String(column.place)}`;\n return pin ? `${pin}; ${place}` : place;\n}\n"],"mappings":";;;AAIA,SAAgB,QAAQ,SAAkD;CACxE,IAAI,YAAY,QAAQ,YAAY,KAAA,GAAW,OAAO;CACtD,OAAO,OAAO,YAAY,WAAW,OAAO,OAAO,IAAI;AACzD;;AAGA,SAAgB,YAAY,OAAwB;CAClD,IAAI,UAAU,QAAQ,UAAU,KAAA,GAAW,OAAO;CAClD,IAAI,iBAAiB,MAAM,OAAO,MAAM,eAAe;CAEvD,OAAO,OAAO,UAAU,WAAW,KAAK,UAAU,KAAK,IAAI,OAAO,KAAK;AACzE;;;;;;AAOA,IAAa,YAAY,MAAc,YAAoB,KAAK,OAAO,IAAI,EAAE,GAAG,OAAO,OAAO;AAC9F,IAAa,cAAc,YAAoB,KAAK,OAAO,OAAO;AAClE,IAAa,cAAc,MAAwB,OAAe,YAChE,GAAG,SAAS,QAAQ,MAAM,IAAI,GAAG,OAAO,KAAK,EAAE,GAAG,OAAO,OAAO;;;;;AAWlE,SAAgB,UACd,QACA,YAAY,OACJ;CACR,IAAI,QAAQ,YAAY,yBAAyB;CACjD,IAAI,OAAO,SAAS,aAAa,SAAS;CAC1C,IAAI,OAAO,KAAK,SAAS,OAAO,UAAU,kCAAkC;CAC5E,IAAI,OAAO,QAAQ,SAAS;CAC5B,OAAO;AACT;;AAaA,SAAgB,SAAS,QAAwE;CAC/F,IAAI,CAAC,QAAQ,OAAO,KAAA;CACpB,OAAO,WAAW,QAAQ,cAAc;AAC1C;;;;;;;;AASA,SAAgB,UACd,QACoB;CACpB,MAAM,MAAM,OAAO,MAAM,kBAAkB,OAAO,OAAO,MAAM,EAAE,MAAM,KAAA;CACvE,IAAI,OAAO,UAAU,KAAA,GAAW,OAAO;CACvC,MAAM,QAAQ,kBAAkB,OAAO,OAAO,KAAK;CACnD,OAAO,MAAM,GAAG,IAAI,IAAI,UAAU;AACpC"}
|
package/dist/render/header.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { ariaSort,
|
|
1
|
+
import { ariaSort, cellStyle, headerSlot } from "./content.js";
|
|
2
2
|
import { html, nothing } from "lit";
|
|
3
|
+
import { live } from "lit/directives/live.js";
|
|
3
4
|
import { repeat } from "lit/directives/repeat.js";
|
|
4
5
|
// src/render/header.ts
|
|
5
6
|
/**
|
|
@@ -16,7 +17,7 @@ import { repeat } from "lit/directives/repeat.js";
|
|
|
16
17
|
* column. While one is dragged (`drag`), its header adds `header-cell-dragging`
|
|
17
18
|
* and the target header shows a `drop-indicator` on the edge it would land at.
|
|
18
19
|
*/
|
|
19
|
-
function renderHeader(columns, handlers, activeCol, drag) {
|
|
20
|
+
function renderHeader(columns, handlers, activeCol, drag, checkAll) {
|
|
20
21
|
return html`<div part="header" role="rowgroup">
|
|
21
22
|
<div
|
|
22
23
|
part="header-row"
|
|
@@ -27,21 +28,23 @@ function renderHeader(columns, handlers, activeCol, drag) {
|
|
|
27
28
|
@pointerdown=${handlers.pointerdown}
|
|
28
29
|
@dblclick=${handlers.dblclick}
|
|
29
30
|
>
|
|
30
|
-
${repeat(columns, (c) => c.
|
|
31
|
+
${repeat(columns, (c) => c.key, (c) => html`<div
|
|
31
32
|
part=${headerParts(c, drag)}
|
|
32
33
|
role="columnheader"
|
|
33
34
|
aria-colindex=${c.colIndex}
|
|
34
35
|
aria-sort=${ariaSort(c.sorted) ?? nothing}
|
|
36
|
+
aria-description=${c.filtered ? "Filtered" : nothing}
|
|
35
37
|
data-col=${c.id}
|
|
36
38
|
data-sort-col=${c.sortable ? c.id : nothing}
|
|
37
39
|
data-move-col=${c.reorderable ? c.id : nothing}
|
|
38
40
|
tabindex=${c.id === activeCol ? 0 : -1}
|
|
39
41
|
data-align=${c.align}
|
|
40
42
|
data-pin=${c.pin ?? nothing}
|
|
41
|
-
style=${
|
|
43
|
+
style=${cellStyle(c) ?? nothing}
|
|
42
44
|
>
|
|
43
|
-
|
|
45
|
+
${c.kind === "selection" ? selectAllCheckbox(checkAll) : html`<slot name=${headerSlot(c.slot)}></slot>`}
|
|
44
46
|
${c.sortable ? html`<span class="sort-indicator" aria-hidden="true"></span>` : nothing}
|
|
47
|
+
${c.filtered ? html`<span part="filter-indicator" aria-hidden="true"></span>` : nothing}
|
|
45
48
|
${c.resizable ? resizeHandle(c) : nothing}
|
|
46
49
|
${drag?.target?.columnId === c.id ? html`<span
|
|
47
50
|
part="drop-indicator"
|
|
@@ -54,9 +57,25 @@ function renderHeader(columns, handlers, activeCol, drag) {
|
|
|
54
57
|
}
|
|
55
58
|
function headerParts(c, drag) {
|
|
56
59
|
let parts = c.sorted ? "header-cell header-cell-sorted" : "header-cell";
|
|
60
|
+
if (c.filtered) parts += " header-cell-filtered";
|
|
61
|
+
if (c.kind === "selection") parts += " header-cell-selection";
|
|
57
62
|
if (drag?.columnId === c.id) parts += " header-cell-dragging";
|
|
58
63
|
return parts;
|
|
59
64
|
}
|
|
65
|
+
/** The header's select-all checkbox: not focusable, like the rows' (the header cell holds focus). */
|
|
66
|
+
function selectAllCheckbox(checkAll) {
|
|
67
|
+
if (!checkAll) return nothing;
|
|
68
|
+
return html`<input
|
|
69
|
+
type="checkbox"
|
|
70
|
+
part="selection-checkbox"
|
|
71
|
+
tabindex="-1"
|
|
72
|
+
aria-label="Select all rows"
|
|
73
|
+
.checked=${live(checkAll.state === "all")}
|
|
74
|
+
.indeterminate=${live(checkAll.state === "some")}
|
|
75
|
+
@click=${checkAll.handlers.all}
|
|
76
|
+
@mousedown=${checkAll.handlers.press}
|
|
77
|
+
/>`;
|
|
78
|
+
}
|
|
60
79
|
/**
|
|
61
80
|
* A vertical separator with the column's width as its value. A flex column has no
|
|
62
81
|
* fixed width until it is resized, so it reports none.
|