argus-table 0.0.0-stage → 0.0.1
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 +492 -0
- package/dist/argus-table.js.map +1 -0
- package/dist/cdn/argus-table.js +4743 -0
- package/dist/cdn/argus-table.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 +1131 -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 +49 -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 +62 -0
- package/dist/render/header.js.map +1 -0
- package/dist/styles/base.js +235 -0
- package/dist/styles/base.js.map +1 -0
- package/package.json +61 -3
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// src/controllers/resize-drag.ts
|
|
2
|
+
/**
|
|
3
|
+
* One pointer drag on a resize handle. The pointer is captured, so the drag keeps
|
|
4
|
+
* working outside the handle, and moves are batched to one `onWidth` call per
|
|
5
|
+
* animation frame (TanStack's `onChange` mode: the width updates while dragging).
|
|
6
|
+
*
|
|
7
|
+
* `direction` is 1 in LTR and -1 in RTL: the handle sits on the inline-end edge,
|
|
8
|
+
* so moving toward the inline end always grows the column.
|
|
9
|
+
*/
|
|
10
|
+
function startResizeDrag(handle, down, startWidth, direction, onWidth) {
|
|
11
|
+
const startX = down.clientX;
|
|
12
|
+
let frame = 0;
|
|
13
|
+
let pending;
|
|
14
|
+
const flush = () => {
|
|
15
|
+
frame = 0;
|
|
16
|
+
if (pending !== void 0) onWidth(pending);
|
|
17
|
+
pending = void 0;
|
|
18
|
+
};
|
|
19
|
+
const move = (event) => {
|
|
20
|
+
pending = startWidth + (event.clientX - startX) * direction;
|
|
21
|
+
frame ||= requestAnimationFrame(flush);
|
|
22
|
+
};
|
|
23
|
+
const end = (event) => {
|
|
24
|
+
if (event.type === "pointerup") move(event);
|
|
25
|
+
if (frame) cancelAnimationFrame(frame);
|
|
26
|
+
flush();
|
|
27
|
+
handle.removeAttribute("data-active");
|
|
28
|
+
handle.removeEventListener("pointermove", move);
|
|
29
|
+
handle.removeEventListener("pointerup", end);
|
|
30
|
+
handle.removeEventListener("pointercancel", end);
|
|
31
|
+
if (handle.hasPointerCapture(event.pointerId)) handle.releasePointerCapture(event.pointerId);
|
|
32
|
+
};
|
|
33
|
+
down.preventDefault();
|
|
34
|
+
try {
|
|
35
|
+
handle.setPointerCapture(down.pointerId);
|
|
36
|
+
} catch {}
|
|
37
|
+
handle.setAttribute("data-active", "");
|
|
38
|
+
handle.addEventListener("pointermove", move);
|
|
39
|
+
handle.addEventListener("pointerup", end);
|
|
40
|
+
handle.addEventListener("pointercancel", end);
|
|
41
|
+
}
|
|
42
|
+
export { startResizeDrag };
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=resize-drag.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resize-drag.js","names":[],"sources":["../../src/controllers/resize-drag.ts"],"sourcesContent":["/**\n * One pointer drag on a resize handle. The pointer is captured, so the drag keeps\n * working outside the handle, and moves are batched to one `onWidth` call per\n * animation frame (TanStack's `onChange` mode: the width updates while dragging).\n *\n * `direction` is 1 in LTR and -1 in RTL: the handle sits on the inline-end edge,\n * so moving toward the inline end always grows the column.\n */\nexport function startResizeDrag(\n handle: HTMLElement,\n down: PointerEvent,\n startWidth: number,\n direction: 1 | -1,\n onWidth: (width: number) => void,\n): void {\n const startX = down.clientX;\n let frame = 0;\n let pending: number | undefined;\n\n const flush = () => {\n frame = 0;\n if (pending !== undefined) onWidth(pending);\n pending = undefined;\n };\n const move = (event: PointerEvent) => {\n pending = startWidth + (event.clientX - startX) * direction;\n frame ||= requestAnimationFrame(flush);\n };\n const end = (event: PointerEvent) => {\n if (event.type === 'pointerup') move(event);\n if (frame) cancelAnimationFrame(frame);\n flush();\n handle.removeAttribute('data-active');\n handle.removeEventListener('pointermove', move);\n handle.removeEventListener('pointerup', end);\n handle.removeEventListener('pointercancel', end);\n if (handle.hasPointerCapture(event.pointerId)) handle.releasePointerCapture(event.pointerId);\n };\n\n down.preventDefault();\n try {\n handle.setPointerCapture(down.pointerId);\n } catch {\n // Synthetic or already released pointers can't be captured; moves on the handle still work.\n }\n handle.setAttribute('data-active', '');\n handle.addEventListener('pointermove', move);\n handle.addEventListener('pointerup', end);\n handle.addEventListener('pointercancel', end);\n}\n"],"mappings":";;;;;;;;;AAQA,SAAgB,gBACd,QACA,MACA,YACA,WACA,SACM;CACN,MAAM,SAAS,KAAK;CACpB,IAAI,QAAQ;CACZ,IAAI;CAEJ,MAAM,cAAc;EAClB,QAAQ;EACR,IAAI,YAAY,KAAA,GAAW,QAAQ,OAAO;EAC1C,UAAU,KAAA;CACZ;CACA,MAAM,QAAQ,UAAwB;EACpC,UAAU,cAAc,MAAM,UAAU,UAAU;EAClD,UAAU,sBAAsB,KAAK;CACvC;CACA,MAAM,OAAO,UAAwB;EACnC,IAAI,MAAM,SAAS,aAAa,KAAK,KAAK;EAC1C,IAAI,OAAO,qBAAqB,KAAK;EACrC,MAAM;EACN,OAAO,gBAAgB,aAAa;EACpC,OAAO,oBAAoB,eAAe,IAAI;EAC9C,OAAO,oBAAoB,aAAa,GAAG;EAC3C,OAAO,oBAAoB,iBAAiB,GAAG;EAC/C,IAAI,OAAO,kBAAkB,MAAM,SAAS,GAAG,OAAO,sBAAsB,MAAM,SAAS;CAC7F;CAEA,KAAK,eAAe;CACpB,IAAI;EACF,OAAO,kBAAkB,KAAK,SAAS;CACzC,QAAQ,CAER;CACA,OAAO,aAAa,eAAe,EAAE;CACrC,OAAO,iBAAiB,eAAe,IAAI;CAC3C,OAAO,iBAAiB,aAAa,GAAG;CACxC,OAAO,iBAAiB,iBAAiB,GAAG;AAC9C"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createTableModel } from "../core/src/table/create-table.js";
|
|
2
|
+
// src/controllers/table-controller.ts
|
|
3
|
+
/**
|
|
4
|
+
* Owns the table model (core's TanStack adapter). The element pushes its
|
|
5
|
+
* properties in from `willUpdate`; render functions read `model`. It needs no
|
|
6
|
+
* lifecycle hooks yet: the model is pull-based and owns no state.
|
|
7
|
+
*/
|
|
8
|
+
var TableController = class {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.model = createTableModel({
|
|
11
|
+
columns: [],
|
|
12
|
+
data: [],
|
|
13
|
+
getRowId: void 0,
|
|
14
|
+
sorting: [],
|
|
15
|
+
columnSizing: {}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
update(options) {
|
|
19
|
+
this.model.update(options);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
export { TableController };
|
|
23
|
+
|
|
24
|
+
//# sourceMappingURL=table-controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"table-controller.js","names":[],"sources":["../../src/controllers/table-controller.ts"],"sourcesContent":["import { createTableModel, type TableModel, type TableModelOptions } from '@argus-table/core';\nimport type { CellContent } from '../types.js';\n\n/**\n * Owns the table model (core's TanStack adapter). The element pushes its\n * properties in from `willUpdate`; render functions read `model`. It needs no\n * lifecycle hooks yet: the model is pull-based and owns no state.\n */\nexport class TableController<T> {\n readonly model: TableModel<T, CellContent>;\n\n constructor() {\n this.model = createTableModel<T, CellContent>({\n columns: [],\n data: [],\n getRowId: undefined,\n sorting: [],\n columnSizing: {},\n });\n }\n\n update(options: Partial<TableModelOptions<T, CellContent>>): void {\n this.model.update(options);\n }\n}\n"],"mappings":";;;;;;;AAQA,IAAa,kBAAb,MAAgC;CAG9B,cAAc;EACZ,KAAK,QAAQ,iBAAiC;GAC5C,SAAS,CAAC;GACV,MAAM,CAAC;GACP,UAAU,KAAA;GACV,SAAS,CAAC;GACV,cAAc,CAAC;EACjB,CAAC;CACH;CAEA,OAAO,SAA2D;EAChE,KAAK,MAAM,OAAO,OAAO;CAC3B;AACF"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { RowPool } from "../core/src/virtual/row-pool.js";
|
|
2
|
+
import { VirtualizerController } from "@tanstack/lit-virtual";
|
|
3
|
+
// src/controllers/virtual-controller.ts
|
|
4
|
+
/**
|
|
5
|
+
* Row virtualization (TanStack Virtual) plus the row pool: which rows render,
|
|
6
|
+
* where, and which pool slot's DOM each one uses. The virtualizer registers itself
|
|
7
|
+
* with the host and requests updates on scroll.
|
|
8
|
+
*/
|
|
9
|
+
var VirtualController = class {
|
|
10
|
+
#virtual;
|
|
11
|
+
#pool = new RowPool();
|
|
12
|
+
#config = {
|
|
13
|
+
count: 0,
|
|
14
|
+
rowHeight: 40,
|
|
15
|
+
overscan: 6,
|
|
16
|
+
rowIdAt: String
|
|
17
|
+
};
|
|
18
|
+
constructor(host, getScrollElement) {
|
|
19
|
+
this.#virtual = new VirtualizerController(host, {
|
|
20
|
+
count: 0,
|
|
21
|
+
getScrollElement,
|
|
22
|
+
estimateSize: () => this.#config.rowHeight,
|
|
23
|
+
overscan: this.#config.overscan
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
configure(next) {
|
|
27
|
+
const prev = this.#config;
|
|
28
|
+
this.#config = next;
|
|
29
|
+
if (next.count !== prev.count || next.overscan !== prev.overscan || next.rowHeight !== prev.rowHeight) {
|
|
30
|
+
this.#virtual.setOptions({
|
|
31
|
+
count: next.count,
|
|
32
|
+
overscan: next.overscan
|
|
33
|
+
});
|
|
34
|
+
if (next.rowHeight !== prev.rowHeight) this.#virtual.getVirtualizer().measure();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Computes the rendered window and updates the pool. Call once per render.
|
|
39
|
+
*/
|
|
40
|
+
computeWindow() {
|
|
41
|
+
const v = this.#virtual.getVirtualizer();
|
|
42
|
+
const items = v.getVirtualItems();
|
|
43
|
+
const byId = /* @__PURE__ */ new Map();
|
|
44
|
+
for (const item of items) byId.set(this.#config.rowIdAt(item.index), {
|
|
45
|
+
index: item.index,
|
|
46
|
+
start: item.start
|
|
47
|
+
});
|
|
48
|
+
const { keep } = this.#config;
|
|
49
|
+
this.#pool.pin(keep?.id);
|
|
50
|
+
if (keep && !byId.has(keep.id)) byId.set(keep.id, {
|
|
51
|
+
index: keep.index,
|
|
52
|
+
start: keep.index * this.#config.rowHeight
|
|
53
|
+
});
|
|
54
|
+
const rows = [];
|
|
55
|
+
for (const { id, slot } of this.#pool.update(byId.keys())) {
|
|
56
|
+
const item = byId.get(id);
|
|
57
|
+
if (item) rows.push({
|
|
58
|
+
slot,
|
|
59
|
+
...item
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const range = v.range;
|
|
63
|
+
return {
|
|
64
|
+
rows,
|
|
65
|
+
totalSize: v.getTotalSize(),
|
|
66
|
+
range: range ? {
|
|
67
|
+
start: range.startIndex,
|
|
68
|
+
end: range.endIndex
|
|
69
|
+
} : null
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** Number of pool slots created so far: bounded by the most rows rendered at once. */
|
|
73
|
+
get poolSize() {
|
|
74
|
+
return this.#pool.size;
|
|
75
|
+
}
|
|
76
|
+
/** `'start'` puts the row at the top; `'auto'` scrolls only as far as needed to show it. */
|
|
77
|
+
scrollToIndex(index, align = "start") {
|
|
78
|
+
this.#virtual.getVirtualizer().scrollToIndex(index, { align });
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
export { VirtualController };
|
|
82
|
+
|
|
83
|
+
//# sourceMappingURL=virtual-controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"virtual-controller.js","names":[],"sources":["../../src/controllers/virtual-controller.ts"],"sourcesContent":["import type { ReactiveControllerHost } from 'lit';\nimport { VirtualizerController } from '@tanstack/lit-virtual';\nimport { RowPool } from '@argus-table/core';\n\n/** A rendered body row: its pool slot, its index in the row model and its offset. */\nexport interface WindowRow {\n slot: number;\n index: number;\n start: number;\n}\n\nexport interface VirtualWindow {\n /** Rendered rows, in pool order (ADR 0001 addendum). */\n rows: WindowRow[];\n /** Height of all rows in px. */\n totalSize: number;\n /** Visible rows, without overscan; `null` before the first layout. */\n range: { start: number; end: number } | null;\n}\n\nexport interface VirtualConfig {\n count: number;\n rowHeight: number;\n overscan: number;\n /** Row id at an index of the row model; pool slots follow row ids. */\n rowIdAt: (index: number) => string;\n /**\n * The row to keep rendered outside the range (the one holding the grid's tab\n * stop), so its DOM and focus survive scrolling. `undefined`: none.\n */\n keep?: { id: string; index: number } | undefined;\n}\n\n/**\n * Row virtualization (TanStack Virtual) plus the row pool: which rows render,\n * where, and which pool slot's DOM each one uses. The virtualizer registers itself\n * with the host and requests updates on scroll.\n */\nexport class VirtualController {\n readonly #virtual: VirtualizerController<HTMLElement, Element>;\n readonly #pool = new RowPool();\n #config: VirtualConfig = { count: 0, rowHeight: 40, overscan: 6, rowIdAt: String };\n\n constructor(host: ReactiveControllerHost, getScrollElement: () => HTMLElement | null) {\n this.#virtual = new VirtualizerController<HTMLElement, Element>(host, {\n count: 0,\n getScrollElement,\n estimateSize: () => this.#config.rowHeight,\n overscan: this.#config.overscan,\n });\n }\n\n configure(next: VirtualConfig): void {\n const prev = this.#config;\n this.#config = next;\n if (\n next.count !== prev.count ||\n next.overscan !== prev.overscan ||\n next.rowHeight !== prev.rowHeight\n ) {\n this.#virtual.setOptions({ count: next.count, overscan: next.overscan });\n if (next.rowHeight !== prev.rowHeight) this.#virtual.getVirtualizer().measure();\n }\n }\n\n /**\n * Computes the rendered window and updates the pool. Call once per render.\n */\n computeWindow(): VirtualWindow {\n const v = this.#virtual.getVirtualizer();\n const items = v.getVirtualItems();\n const byId = new Map<string, { index: number; start: number }>();\n for (const item of items) {\n byId.set(this.#config.rowIdAt(item.index), { index: item.index, start: item.start });\n }\n const { keep } = this.#config;\n this.#pool.pin(keep?.id);\n if (keep && !byId.has(keep.id)) {\n // Fixed row heights: the virtualizer would place it here too.\n byId.set(keep.id, { index: keep.index, start: keep.index * this.#config.rowHeight });\n }\n const rows: WindowRow[] = [];\n for (const { id, slot } of this.#pool.update(byId.keys())) {\n const item = byId.get(id);\n if (item) rows.push({ slot, ...item });\n }\n const range = v.range;\n return {\n rows,\n totalSize: v.getTotalSize(),\n range: range ? { start: range.startIndex, end: range.endIndex } : null,\n };\n }\n\n /** Number of pool slots created so far: bounded by the most rows rendered at once. */\n get poolSize(): number {\n return this.#pool.size;\n }\n\n /** `'start'` puts the row at the top; `'auto'` scrolls only as far as needed to show it. */\n scrollToIndex(index: number, align: 'start' | 'auto' = 'start'): void {\n this.#virtual.getVirtualizer().scrollToIndex(index, { align });\n }\n}\n"],"mappings":";;;;;;;;AAsCA,IAAa,oBAAb,MAA+B;CAC7B;CACA,QAAiB,IAAI,QAAQ;CAC7B,UAAyB;EAAE,OAAO;EAAG,WAAW;EAAI,UAAU;EAAG,SAAS;CAAO;CAEjF,YAAY,MAA8B,kBAA4C;EACpF,KAAK,WAAW,IAAI,sBAA4C,MAAM;GACpE,OAAO;GACP;GACA,oBAAoB,KAAK,QAAQ;GACjC,UAAU,KAAK,QAAQ;EACzB,CAAC;CACH;CAEA,UAAU,MAA2B;EACnC,MAAM,OAAO,KAAK;EAClB,KAAK,UAAU;EACf,IACE,KAAK,UAAU,KAAK,SACpB,KAAK,aAAa,KAAK,YACvB,KAAK,cAAc,KAAK,WACxB;GACA,KAAK,SAAS,WAAW;IAAE,OAAO,KAAK;IAAO,UAAU,KAAK;GAAS,CAAC;GACvE,IAAI,KAAK,cAAc,KAAK,WAAW,KAAK,SAAS,eAAe,CAAC,CAAC,QAAQ;EAChF;CACF;;;;CAKA,gBAA+B;EAC7B,MAAM,IAAI,KAAK,SAAS,eAAe;EACvC,MAAM,QAAQ,EAAE,gBAAgB;EAChC,MAAM,uBAAO,IAAI,IAA8C;EAC/D,KAAK,MAAM,QAAQ,OACjB,KAAK,IAAI,KAAK,QAAQ,QAAQ,KAAK,KAAK,GAAG;GAAE,OAAO,KAAK;GAAO,OAAO,KAAK;EAAM,CAAC;EAErF,MAAM,EAAE,SAAS,KAAK;EACtB,KAAK,MAAM,IAAI,MAAM,EAAE;EACvB,IAAI,QAAQ,CAAC,KAAK,IAAI,KAAK,EAAE,GAE3B,KAAK,IAAI,KAAK,IAAI;GAAE,OAAO,KAAK;GAAO,OAAO,KAAK,QAAQ,KAAK,QAAQ;EAAU,CAAC;EAErF,MAAM,OAAoB,CAAC;EAC3B,KAAK,MAAM,EAAE,IAAI,UAAU,KAAK,MAAM,OAAO,KAAK,KAAK,CAAC,GAAG;GACzD,MAAM,OAAO,KAAK,IAAI,EAAE;GACxB,IAAI,MAAM,KAAK,KAAK;IAAE;IAAM,GAAG;GAAK,CAAC;EACvC;EACA,MAAM,QAAQ,EAAE;EAChB,OAAO;GACL;GACA,WAAW,EAAE,aAAa;GAC1B,OAAO,QAAQ;IAAE,OAAO,MAAM;IAAY,KAAK,MAAM;GAAS,IAAI;EACpE;CACF;;CAGA,IAAI,WAAmB;EACrB,OAAO,KAAK,MAAM;CACpB;;CAGA,cAAc,OAAe,QAA0B,SAAe;EACpE,KAAK,SAAS,eAAe,CAAC,CAAC,cAAc,OAAO,EAAE,MAAM,CAAC;CAC/D;AACF"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
var pathCache = /* @__PURE__ */ new Map();
|
|
2
|
+
/**
|
|
3
|
+
* Reads a dotted property path such as `'customer.name'`. Returns `undefined`
|
|
4
|
+
* as soon as a step is `null` or `undefined`, like optional chaining.
|
|
5
|
+
*/
|
|
6
|
+
function readPath(source, path) {
|
|
7
|
+
let keys = pathCache.get(path);
|
|
8
|
+
if (!keys) {
|
|
9
|
+
keys = path.split(".");
|
|
10
|
+
pathCache.set(path, keys);
|
|
11
|
+
}
|
|
12
|
+
let value = source;
|
|
13
|
+
for (const key of keys) {
|
|
14
|
+
if (value === null || value === void 0) return void 0;
|
|
15
|
+
value = value[key];
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function finite(value) {
|
|
20
|
+
return value !== void 0 && Number.isFinite(value) ? value : void 0;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Normalizes `ColumnDef`s: checks ids, applies defaults and clamps widths.
|
|
24
|
+
*
|
|
25
|
+
* @throws if an id is empty or used twice, because state and events are keyed by id.
|
|
26
|
+
*/
|
|
27
|
+
function resolveColumns(defs) {
|
|
28
|
+
const seen = /* @__PURE__ */ new Set();
|
|
29
|
+
return defs.map((def, index) => {
|
|
30
|
+
if (typeof def.id !== "string" || def.id === "") throw new Error(`argus-table: column at index ${String(index)} needs a non-empty string id.`);
|
|
31
|
+
if (seen.has(def.id)) throw new Error(`argus-table: column id '${def.id}' is used more than once.`);
|
|
32
|
+
seen.add(def.id);
|
|
33
|
+
const minWidth = Math.max(0, finite(def.minWidth) ?? 40);
|
|
34
|
+
const maxWidth = Math.max(minWidth, finite(def.maxWidth) ?? Number.POSITIVE_INFINITY);
|
|
35
|
+
const clamp = (px) => Math.min(maxWidth, Math.max(minWidth, px));
|
|
36
|
+
const pin = def.pin === "start" || def.pin === "end" ? def.pin : void 0;
|
|
37
|
+
const { width } = def;
|
|
38
|
+
const flex = pin === void 0 && typeof width === "object" && Number.isFinite(width.flex) && width.flex > 0 ? width.flex : void 0;
|
|
39
|
+
const base = flex !== void 0 ? minWidth : clamp(finite(width) ?? 150);
|
|
40
|
+
const { accessor } = def;
|
|
41
|
+
const getValue = typeof accessor === "function" ? accessor : typeof accessor === "string" ? (row) => readPath(row, accessor) : () => void 0;
|
|
42
|
+
return {
|
|
43
|
+
id: def.id,
|
|
44
|
+
index,
|
|
45
|
+
header: def.header,
|
|
46
|
+
cell: def.cell,
|
|
47
|
+
getValue,
|
|
48
|
+
hasAccessor: accessor !== void 0,
|
|
49
|
+
width: base,
|
|
50
|
+
flex,
|
|
51
|
+
minWidth,
|
|
52
|
+
maxWidth,
|
|
53
|
+
pin,
|
|
54
|
+
sortable: def.sortable === true,
|
|
55
|
+
resizable: def.resizable !== false,
|
|
56
|
+
align: def.align ?? "start"
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export { readPath, resolveColumns };
|
|
61
|
+
|
|
62
|
+
//# sourceMappingURL=resolve-columns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-columns.js","names":[],"sources":["../../../../../core/src/columns/resolve-columns.ts"],"sourcesContent":["import type { ColumnAlign, ColumnDef, ColumnPin } from '../types.js';\n\n/** Width used when a column sets none. */\nexport const DEFAULT_WIDTH = 150;\n/** Lower bound used when a column sets no `minWidth`. */\nexport const DEFAULT_MIN_WIDTH = 40;\n\n/** A column after defaults, bounds and the accessor have been resolved. */\nexport interface ResolvedColumn<TData, TContent> {\n id: string;\n /** Position in the `columns` array. */\n index: number;\n header: ColumnDef<TData, TContent>['header'];\n cell: ColumnDef<TData, TContent>['cell'];\n /** Reads the cell value; returns `undefined` when the column has no accessor. */\n getValue: (row: TData, index: number) => unknown;\n hasAccessor: boolean;\n /** Base width in px. For a flex column this is its minimum track size. */\n width: number;\n /** Share of free space for `{ flex }` columns, otherwise `undefined`. Pinned columns never flex. */\n flex: number | undefined;\n minWidth: number;\n maxWidth: number;\n pin: ColumnPin | undefined;\n sortable: boolean;\n resizable: boolean;\n align: ColumnAlign;\n}\n\nconst pathCache = new Map<string, readonly string[]>();\n\n/**\n * Reads a dotted property path such as `'customer.name'`. Returns `undefined`\n * as soon as a step is `null` or `undefined`, like optional chaining.\n */\nexport function readPath(source: unknown, path: string): unknown {\n let keys = pathCache.get(path);\n if (!keys) {\n keys = path.split('.');\n pathCache.set(path, keys);\n }\n let value: unknown = source;\n for (const key of keys) {\n if (value === null || value === undefined) return undefined;\n value = (value as Record<string, unknown>)[key];\n }\n return value;\n}\n\nfunction finite(value: number | undefined): number | undefined {\n return value !== undefined && Number.isFinite(value) ? value : undefined;\n}\n\n/**\n * Normalizes `ColumnDef`s: checks ids, applies defaults and clamps widths.\n *\n * @throws if an id is empty or used twice, because state and events are keyed by id.\n */\nexport function resolveColumns<TData, TContent>(\n defs: readonly ColumnDef<TData, TContent>[],\n): ResolvedColumn<TData, TContent>[] {\n const seen = new Set<string>();\n return defs.map((def, index) => {\n if (typeof def.id !== 'string' || def.id === '') {\n throw new Error(`argus-table: column at index ${String(index)} needs a non-empty string id.`);\n }\n if (seen.has(def.id)) {\n throw new Error(`argus-table: column id '${def.id}' is used more than once.`);\n }\n seen.add(def.id);\n\n const minWidth = Math.max(0, finite(def.minWidth) ?? DEFAULT_MIN_WIDTH);\n const maxWidth = Math.max(minWidth, finite(def.maxWidth) ?? Number.POSITIVE_INFINITY);\n const clamp = (px: number) => Math.min(maxWidth, Math.max(minWidth, px));\n\n const pin = def.pin === 'start' || def.pin === 'end' ? def.pin : undefined;\n const { width } = def;\n // Pinned columns need a known width for their sticky offsets, so they never flex.\n const flex =\n pin === undefined &&\n typeof width === 'object' &&\n Number.isFinite(width.flex) &&\n width.flex > 0\n ? width.flex\n : undefined;\n const base = flex !== undefined ? minWidth : clamp(finite(width as number) ?? DEFAULT_WIDTH);\n\n const { accessor } = def;\n const getValue: ResolvedColumn<TData, TContent>['getValue'] =\n typeof accessor === 'function'\n ? accessor\n : typeof accessor === 'string'\n ? (row) => readPath(row, accessor)\n : () => undefined;\n\n return {\n id: def.id,\n index,\n header: def.header,\n cell: def.cell,\n getValue,\n hasAccessor: accessor !== undefined,\n width: base,\n flex,\n minWidth,\n maxWidth,\n pin,\n sortable: def.sortable === true,\n resizable: def.resizable !== false,\n align: def.align ?? 'start',\n };\n });\n}\n"],"mappings":"AA6BA,IAAM,4BAAY,IAAI,IAA+B;;;;;AAMrD,SAAgB,SAAS,QAAiB,MAAuB;CAC/D,IAAI,OAAO,UAAU,IAAI,IAAI;CAC7B,IAAI,CAAC,MAAM;EACT,OAAO,KAAK,MAAM,GAAG;EACrB,UAAU,IAAI,MAAM,IAAI;CAC1B;CACA,IAAI,QAAiB;CACrB,KAAK,MAAM,OAAO,MAAM;EACtB,IAAI,UAAU,QAAQ,UAAU,KAAA,GAAW,OAAO,KAAA;EAClD,QAAS,MAAkC;CAC7C;CACA,OAAO;AACT;AAEA,SAAS,OAAO,OAA+C;CAC7D,OAAO,UAAU,KAAA,KAAa,OAAO,SAAS,KAAK,IAAI,QAAQ,KAAA;AACjE;;;;;;AAOA,SAAgB,eACd,MACmC;CACnC,MAAM,uBAAO,IAAI,IAAY;CAC7B,OAAO,KAAK,KAAK,KAAK,UAAU;EAC9B,IAAI,OAAO,IAAI,OAAO,YAAY,IAAI,OAAO,IAC3C,MAAM,IAAI,MAAM,gCAAgC,OAAO,KAAK,EAAE,8BAA8B;EAE9F,IAAI,KAAK,IAAI,IAAI,EAAE,GACjB,MAAM,IAAI,MAAM,2BAA2B,IAAI,GAAG,0BAA0B;EAE9E,KAAK,IAAI,IAAI,EAAE;EAEf,MAAM,WAAW,KAAK,IAAI,GAAG,OAAO,IAAI,QAAQ,KAAA,EAAsB;EACtE,MAAM,WAAW,KAAK,IAAI,UAAU,OAAO,IAAI,QAAQ,KAAK,OAAO,iBAAiB;EACpF,MAAM,SAAS,OAAe,KAAK,IAAI,UAAU,KAAK,IAAI,UAAU,EAAE,CAAC;EAEvE,MAAM,MAAM,IAAI,QAAQ,WAAW,IAAI,QAAQ,QAAQ,IAAI,MAAM,KAAA;EACjE,MAAM,EAAE,UAAU;EAElB,MAAM,OACJ,QAAQ,KAAA,KACR,OAAO,UAAU,YACjB,OAAO,SAAS,MAAM,IAAI,KAC1B,MAAM,OAAO,IACT,MAAM,OACN,KAAA;EACN,MAAM,OAAO,SAAS,KAAA,IAAY,WAAW,MAAM,OAAO,KAAe,KAAA,GAAkB;EAE3F,MAAM,EAAE,aAAa;EACrB,MAAM,WACJ,OAAO,aAAa,aAChB,WACA,OAAO,aAAa,YACjB,QAAQ,SAAS,KAAK,QAAQ,UACzB,KAAA;EAEd,OAAO;GACL,IAAI,IAAI;GACR;GACA,QAAQ,IAAI;GACZ,MAAM,IAAI;GACV;GACA,aAAa,aAAa,KAAA;GAC1B,OAAO;GACP;GACA;GACA;GACA;GACA,UAAU,IAAI,aAAa;GAC3B,WAAW,IAAI,cAAc;GAC7B,OAAO,IAAI,SAAS;EACtB;CACF,CAAC;AACH"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// ../core/src/state/controllable.ts
|
|
2
|
+
function controllable(initial) {
|
|
3
|
+
let value = initial;
|
|
4
|
+
let controlled = false;
|
|
5
|
+
return {
|
|
6
|
+
get value() {
|
|
7
|
+
return value;
|
|
8
|
+
},
|
|
9
|
+
get controlled() {
|
|
10
|
+
return controlled;
|
|
11
|
+
},
|
|
12
|
+
set(next) {
|
|
13
|
+
if (next === void 0) {
|
|
14
|
+
controlled = false;
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
controlled = true;
|
|
18
|
+
value = next;
|
|
19
|
+
},
|
|
20
|
+
request(next) {
|
|
21
|
+
if (!controlled) value = next;
|
|
22
|
+
return {
|
|
23
|
+
value: next,
|
|
24
|
+
applied: !controlled
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export { controllable };
|
|
30
|
+
|
|
31
|
+
//# sourceMappingURL=controllable.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controllable.js","names":[],"sources":["../../../../../core/src/state/controllable.ts"],"sourcesContent":["/**\n * State that works like a form input: uncontrolled until the host sets it,\n * controlled from then on. See `docs/plan/code-architecture/04-state-and-events.md`.\n *\n * - **Uncontrolled:** `request()` applies the change. The caller then fires the change event.\n * - **Controlled:** `request()` changes nothing and returns the proposed value for the\n * event. The value only changes when the host calls `set()` again.\n * - `set(undefined)` hands ownership back to the table and keeps the last value.\n */\nexport interface Controllable<T> {\n /** The current value. */\n readonly value: T;\n /** `true` once the host has set a value (and not handed it back with `undefined`). */\n readonly controlled: boolean;\n /** Called when the host sets the property. */\n set(value: T | undefined): void;\n /**\n * Called when the table wants to change the state, for example after a header click.\n * Returns the proposed value and whether it was applied.\n */\n request(next: T): { value: T; applied: boolean };\n}\n\nexport function controllable<T>(initial: T): Controllable<T> {\n let value = initial;\n let controlled = false;\n return {\n get value() {\n return value;\n },\n get controlled() {\n return controlled;\n },\n set(next) {\n if (next === undefined) {\n controlled = false;\n return;\n }\n controlled = true;\n value = next;\n },\n request(next) {\n if (!controlled) value = next;\n return { value: next, applied: !controlled };\n },\n };\n}\n"],"mappings":";AAuBA,SAAgB,aAAgB,SAA6B;CAC3D,IAAI,QAAQ;CACZ,IAAI,aAAa;CACjB,OAAO;EACL,IAAI,QAAQ;GACV,OAAO;EACT;EACA,IAAI,aAAa;GACf,OAAO;EACT;EACA,IAAI,MAAM;GACR,IAAI,SAAS,KAAA,GAAW;IACtB,aAAa;IACb;GACF;GACA,aAAa;GACb,QAAQ;EACV;EACA,QAAQ,MAAM;GACZ,IAAI,CAAC,YAAY,QAAQ;GACzB,OAAO;IAAE,OAAO;IAAM,SAAS,CAAC;GAAW;EAC7C;CACF;AACF"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { resolveColumns } from "../columns/resolve-columns.js";
|
|
2
|
+
import { columnPinningFeature, columnSizingFeature, constructTable, createSortedRowModel, rowSortingFeature, sortFn_alphanumeric, sortFn_basic, sortFn_datetime, sortFn_text, tableFeatures } from "@tanstack/table-core";
|
|
3
|
+
import { storeReactivityBindings } from "@tanstack/table-core/store-reactivity-bindings";
|
|
4
|
+
// ../core/src/table/create-table.ts
|
|
5
|
+
var features = tableFeatures({
|
|
6
|
+
coreReactivityFeature: storeReactivityBindings(),
|
|
7
|
+
columnPinningFeature,
|
|
8
|
+
columnSizingFeature,
|
|
9
|
+
rowSortingFeature,
|
|
10
|
+
sortedRowModel: createSortedRowModel(),
|
|
11
|
+
sortFns: {
|
|
12
|
+
alphanumeric: sortFn_alphanumeric,
|
|
13
|
+
basic: sortFn_basic,
|
|
14
|
+
datetime: sortFn_datetime,
|
|
15
|
+
text: sortFn_text
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
var defaultRowId = (_row, index) => String(index);
|
|
19
|
+
function toTanStack(column) {
|
|
20
|
+
return {
|
|
21
|
+
id: column.id,
|
|
22
|
+
accessorFn: column.getValue,
|
|
23
|
+
size: column.width,
|
|
24
|
+
minSize: column.minWidth,
|
|
25
|
+
maxSize: column.maxWidth,
|
|
26
|
+
enableSorting: column.sortable,
|
|
27
|
+
sortUndefined: "last"
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Creates the table model over TanStack Table. It owns no state: sorting and sizing
|
|
32
|
+
* come in through `update()`, and `nextSorting()`/`nextSizing()` only propose
|
|
33
|
+
* changes, so the caller decides (controlled or uncontrolled) whether to apply them.
|
|
34
|
+
*/
|
|
35
|
+
function createTableModel(initial) {
|
|
36
|
+
let options = initial;
|
|
37
|
+
let resolved = resolveColumns(options.columns);
|
|
38
|
+
let byId = new Map(resolved.map((c) => [c.id, c]));
|
|
39
|
+
let tanStackColumns = resolved.map(toTanStack);
|
|
40
|
+
let views;
|
|
41
|
+
const rowViews = /* @__PURE__ */ new WeakMap();
|
|
42
|
+
let indexById;
|
|
43
|
+
const tableOptions = () => ({
|
|
44
|
+
features,
|
|
45
|
+
columns: tanStackColumns,
|
|
46
|
+
data: options.data,
|
|
47
|
+
getRowId: options.getRowId ?? defaultRowId,
|
|
48
|
+
state: {
|
|
49
|
+
sorting: options.sorting,
|
|
50
|
+
columnSizing: options.columnSizing,
|
|
51
|
+
columnPinning: {
|
|
52
|
+
start: resolved.filter((c) => c.pin === "start").map((c) => c.id),
|
|
53
|
+
end: resolved.filter((c) => c.pin === "end").map((c) => c.id)
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
onSortingChange: () => void 0,
|
|
57
|
+
onColumnSizingChange: () => void 0,
|
|
58
|
+
onColumnPinningChange: () => void 0
|
|
59
|
+
});
|
|
60
|
+
const table = constructTable(tableOptions());
|
|
61
|
+
const rows = () => table.getRowModel().rows;
|
|
62
|
+
const buildViews = () => {
|
|
63
|
+
const start = table.getStartLeafColumns();
|
|
64
|
+
const center = table.getCenterLeafColumns();
|
|
65
|
+
const end = table.getEndLeafColumns();
|
|
66
|
+
const sorting = options.sorting;
|
|
67
|
+
const view = (column, colIndex, offset, pinEdge) => {
|
|
68
|
+
const def = byId.get(column.id);
|
|
69
|
+
if (!def) throw new Error(`argus-table: unknown column '${column.id}'.`);
|
|
70
|
+
const sort = sorting.find((s) => s.id === column.id);
|
|
71
|
+
return {
|
|
72
|
+
id: def.id,
|
|
73
|
+
colIndex,
|
|
74
|
+
pin: def.pin,
|
|
75
|
+
offset,
|
|
76
|
+
pinEdge,
|
|
77
|
+
width: column.getSize(),
|
|
78
|
+
flex: options.columnSizing[def.id] === void 0 ? def.flex : void 0,
|
|
79
|
+
minWidth: def.minWidth,
|
|
80
|
+
maxWidth: def.maxWidth,
|
|
81
|
+
sortable: def.sortable,
|
|
82
|
+
resizable: def.resizable,
|
|
83
|
+
align: def.align,
|
|
84
|
+
sorted: sort ? sort.desc ? "desc" : "asc" : false,
|
|
85
|
+
header: def.header,
|
|
86
|
+
cell: def.cell,
|
|
87
|
+
hasAccessor: def.hasAccessor
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
return [
|
|
91
|
+
...start.map((c, i) => view(c, i + 1, c.getStart("start"), i === start.length - 1)),
|
|
92
|
+
...center.map((c, i) => view(c, start.length + i + 1, c.getStart("center"), false)),
|
|
93
|
+
...end.map((c, i) => view(c, start.length + center.length + i + 1, c.getAfter("end"), i === 0))
|
|
94
|
+
];
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
update(next) {
|
|
98
|
+
const merged = {
|
|
99
|
+
...options,
|
|
100
|
+
...next
|
|
101
|
+
};
|
|
102
|
+
if (!Object.keys(next).some((key) => next[key] !== options[key])) return;
|
|
103
|
+
if (next.columns && next.columns !== options.columns) {
|
|
104
|
+
resolved = resolveColumns(next.columns);
|
|
105
|
+
byId = new Map(resolved.map((c) => [c.id, c]));
|
|
106
|
+
tanStackColumns = resolved.map(toTanStack);
|
|
107
|
+
}
|
|
108
|
+
options = merged;
|
|
109
|
+
views = void 0;
|
|
110
|
+
table.setOptions((prev) => ({
|
|
111
|
+
...prev,
|
|
112
|
+
...tableOptions()
|
|
113
|
+
}));
|
|
114
|
+
},
|
|
115
|
+
get columns() {
|
|
116
|
+
return views ??= buildViews();
|
|
117
|
+
},
|
|
118
|
+
get rowCount() {
|
|
119
|
+
return rows().length;
|
|
120
|
+
},
|
|
121
|
+
get totalWidth() {
|
|
122
|
+
return table.getTotalSize();
|
|
123
|
+
},
|
|
124
|
+
rowAt(index) {
|
|
125
|
+
const row = rows()[index];
|
|
126
|
+
if (!row) return void 0;
|
|
127
|
+
let view = rowViews.get(row);
|
|
128
|
+
if (view?.index !== index) {
|
|
129
|
+
view = {
|
|
130
|
+
id: row.id,
|
|
131
|
+
index,
|
|
132
|
+
original: row.original
|
|
133
|
+
};
|
|
134
|
+
rowViews.set(row, view);
|
|
135
|
+
}
|
|
136
|
+
return view;
|
|
137
|
+
},
|
|
138
|
+
indexOf(rowId) {
|
|
139
|
+
const current = rows();
|
|
140
|
+
if (indexById?.rows !== current) indexById = {
|
|
141
|
+
rows: current,
|
|
142
|
+
map: new Map(current.map((r, i) => [r.id, i]))
|
|
143
|
+
};
|
|
144
|
+
return indexById.map.get(rowId);
|
|
145
|
+
},
|
|
146
|
+
valueAt(index, columnId) {
|
|
147
|
+
return rows()[index]?.getValue(columnId);
|
|
148
|
+
},
|
|
149
|
+
cellContext(index, columnId) {
|
|
150
|
+
const row = rows()[index];
|
|
151
|
+
if (!row || !byId.has(columnId)) return void 0;
|
|
152
|
+
return {
|
|
153
|
+
row: row.original,
|
|
154
|
+
rowId: row.id,
|
|
155
|
+
index,
|
|
156
|
+
columnId,
|
|
157
|
+
value: row.getValue(columnId)
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
nextSorting(columnId) {
|
|
161
|
+
if (byId.get(columnId)?.sortable !== true) return void 0;
|
|
162
|
+
const current = options.sorting.find((s) => s.id === columnId);
|
|
163
|
+
if (!current) return [{
|
|
164
|
+
id: columnId,
|
|
165
|
+
desc: false
|
|
166
|
+
}];
|
|
167
|
+
return current.desc ? [] : [{
|
|
168
|
+
id: columnId,
|
|
169
|
+
desc: true
|
|
170
|
+
}];
|
|
171
|
+
},
|
|
172
|
+
nextSizing(columnId, width) {
|
|
173
|
+
const column = byId.get(columnId);
|
|
174
|
+
if (!column?.resizable || !Number.isFinite(width)) return void 0;
|
|
175
|
+
const clamped = Math.round(Math.min(column.maxWidth, Math.max(column.minWidth, width)));
|
|
176
|
+
return {
|
|
177
|
+
...options.columnSizing,
|
|
178
|
+
[columnId]: clamped
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
export { createTableModel };
|
|
184
|
+
|
|
185
|
+
//# sourceMappingURL=create-table.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-table.js","names":[],"sources":["../../../../../core/src/table/create-table.ts"],"sourcesContent":["import {\n columnPinningFeature,\n columnSizingFeature,\n constructTable,\n createSortedRowModel,\n rowSortingFeature,\n sortFn_alphanumeric,\n sortFn_basic,\n sortFn_datetime,\n sortFn_text,\n tableFeatures,\n type Column,\n type ColumnDef as TanStackColumnDef,\n type Row,\n type Table,\n type TableOptions,\n} from '@tanstack/table-core';\nimport { storeReactivityBindings } from '@tanstack/table-core/store-reactivity-bindings';\nimport { resolveColumns, type ResolvedColumn } from '../columns/resolve-columns.js';\nimport type {\n CellContext,\n ColumnAlign,\n ColumnDef,\n ColumnPin,\n ColumnSizingState,\n SortState,\n} from '../types.js';\n\n// The TanStack adapter. Everything TanStack stays in this module: callers only see\n// the view types below, so TanStack can be upgraded or replaced without touching\n// `argus-table`'s public API.\n\nconst features = tableFeatures({\n // Vanilla (framework-free) reactivity; required by `constructTable`.\n coreReactivityFeature: storeReactivityBindings(),\n columnPinningFeature,\n columnSizingFeature,\n rowSortingFeature,\n sortedRowModel: createSortedRowModel(),\n // Registered by name so `sortFn: 'auto'` can pick one by value type.\n sortFns: {\n alphanumeric: sortFn_alphanumeric,\n basic: sortFn_basic,\n datetime: sortFn_datetime,\n text: sortFn_text,\n },\n});\ntype Features = typeof features;\ntype AnyRow = Record<string, unknown>;\n\n/** Sort direction of a column, or `false` when it isn't sorted. */\nexport type SortDirection = 'asc' | 'desc' | false;\n\n/** A column as the renderer sees it, in visual order: start-pinned, scrolling, end-pinned. */\nexport interface ColumnView<TData, TContent> {\n id: string;\n /** 1-based position in visual order, for `aria-colindex`. */\n colIndex: number;\n pin: ColumnPin | undefined;\n /**\n * Sticky inset in px: from the start edge for `'start'` columns, from the end edge\n * for `'end'` columns. For scrolling columns, the start within the scrolling area.\n */\n offset: number;\n /** `true` on the pinned column next to the scrolling area (one per pinned side). */\n pinEdge: boolean;\n /** Current width in px (a resized width wins over the column's own). */\n width: number;\n /** Share of free space, or `undefined` for a fixed width. A resized column stops flexing. */\n flex: number | undefined;\n minWidth: number;\n maxWidth: number;\n sortable: boolean;\n resizable: boolean;\n align: ColumnAlign;\n sorted: SortDirection;\n header: ColumnDef<TData, TContent>['header'];\n cell: ColumnDef<TData, TContent>['cell'];\n hasAccessor: boolean;\n}\n\n/** A row of the current (sorted) row model. */\nexport interface RowView<TData> {\n id: string;\n /** Index in the current row model, 0-based. */\n index: number;\n original: TData;\n}\n\nexport interface TableModelOptions<TData, TContent> {\n columns: readonly ColumnDef<TData, TContent>[];\n data: readonly TData[];\n /** Row id function. Default: the row's index in `data`, as a string. */\n getRowId: ((row: TData, index: number) => string) | undefined;\n /** Current sort state; its owner is the caller (see `controllable`). */\n sorting: SortState;\n /** Current resized widths; its owner is the caller. */\n columnSizing: ColumnSizingState;\n}\n\nexport interface TableModel<TData, TContent> {\n /** Updates any subset of the options. Unchanged references are ignored. */\n update(options: Partial<TableModelOptions<TData, TContent>>): void;\n /** Columns in visual order. */\n readonly columns: readonly ColumnView<TData, TContent>[];\n /** Number of rows in the current row model. */\n readonly rowCount: number;\n /** Sum of all column widths in px (flex columns count their minimum). */\n readonly totalWidth: number;\n /** The row at an index of the current row model. */\n rowAt(index: number): RowView<TData> | undefined;\n /** The index of a row id in the current row model (after sorting), or `undefined`. */\n indexOf(rowId: string): number | undefined;\n /** The value a column reads from the row at `index`. */\n valueAt(index: number, columnId: string): unknown;\n /** What a column's `cell` function receives for the row at `index`. */\n cellContext(index: number, columnId: string): CellContext<TData> | undefined;\n /**\n * The sort state that clicking this column's header would produce:\n * asc → desc → unsorted, one column at a time. `undefined` if it can't sort.\n */\n nextSorting(columnId: string): SortState | undefined;\n /**\n * The sizing state after setting a column to `width` px, clamped to its bounds.\n * `undefined` if the column can't be resized.\n */\n nextSizing(columnId: string, width: number): ColumnSizingState | undefined;\n}\n\nconst defaultRowId = (_row: unknown, index: number) => String(index);\n\nfunction toTanStack<TData, TContent>(\n column: ResolvedColumn<TData, TContent>,\n): TanStackColumnDef<Features, AnyRow> {\n return {\n id: column.id,\n accessorFn: column.getValue as (row: AnyRow, index: number) => unknown,\n size: column.width,\n minSize: column.minWidth,\n maxSize: column.maxWidth,\n enableSorting: column.sortable,\n sortUndefined: 'last',\n };\n}\n\n/**\n * Creates the table model over TanStack Table. It owns no state: sorting and sizing\n * come in through `update()`, and `nextSorting()`/`nextSizing()` only propose\n * changes, so the caller decides (controlled or uncontrolled) whether to apply them.\n */\nexport function createTableModel<TData, TContent>(\n initial: TableModelOptions<TData, TContent>,\n): TableModel<TData, TContent> {\n let options = initial;\n let resolved = resolveColumns(options.columns);\n let byId = new Map(resolved.map((c) => [c.id, c]));\n // Kept stable between updates: a new array would make TanStack rebuild columns and rows.\n let tanStackColumns = resolved.map(toTanStack);\n let views: ColumnView<TData, TContent>[] | undefined;\n const rowViews = new WeakMap<Row<Features, AnyRow>, RowView<TData>>();\n // Built lazily per row model (sorting or new data gives a new array).\n let indexById: { rows: readonly Row<Features, AnyRow>[]; map: Map<string, number> } | undefined;\n\n const tableOptions = (): TableOptions<Features, AnyRow> => ({\n features,\n columns: tanStackColumns,\n data: options.data as AnyRow[],\n getRowId: (options.getRowId ?? defaultRowId) as (row: AnyRow, index: number) => string,\n state: {\n sorting: options.sorting,\n columnSizing: options.columnSizing,\n columnPinning: {\n start: resolved.filter((c) => c.pin === 'start').map((c) => c.id),\n end: resolved.filter((c) => c.pin === 'end').map((c) => c.id),\n },\n },\n // State is owned by the caller; TanStack's own setters must not change it.\n onSortingChange: () => undefined,\n onColumnSizingChange: () => undefined,\n onColumnPinningChange: () => undefined,\n });\n\n const table: Table<Features, AnyRow> = constructTable(tableOptions());\n\n const rows = () => table.getRowModel().rows;\n\n const buildViews = (): ColumnView<TData, TContent>[] => {\n const start = table.getStartLeafColumns();\n const center = table.getCenterLeafColumns();\n const end = table.getEndLeafColumns();\n const sorting = options.sorting;\n const view = (\n column: Column<Features, AnyRow>,\n colIndex: number,\n offset: number,\n pinEdge: boolean,\n ): ColumnView<TData, TContent> => {\n const def = byId.get(column.id);\n if (!def) throw new Error(`argus-table: unknown column '${column.id}'.`);\n const sort = sorting.find((s) => s.id === column.id);\n return {\n id: def.id,\n colIndex,\n pin: def.pin,\n offset,\n pinEdge,\n width: column.getSize(),\n flex: options.columnSizing[def.id] === undefined ? def.flex : undefined,\n minWidth: def.minWidth,\n maxWidth: def.maxWidth,\n sortable: def.sortable,\n resizable: def.resizable,\n align: def.align,\n sorted: sort ? (sort.desc ? 'desc' : 'asc') : false,\n header: def.header,\n cell: def.cell,\n hasAccessor: def.hasAccessor,\n };\n };\n return [\n ...start.map((c, i) => view(c, i + 1, c.getStart('start'), i === start.length - 1)),\n ...center.map((c, i) => view(c, start.length + i + 1, c.getStart('center'), false)),\n ...end.map((c, i) =>\n view(c, start.length + center.length + i + 1, c.getAfter('end'), i === 0),\n ),\n ];\n };\n\n return {\n update(next) {\n const merged = { ...options, ...next };\n const changed = (Object.keys(next) as (keyof typeof next)[]).some(\n (key) => next[key] !== options[key],\n );\n if (!changed) return;\n if (next.columns && next.columns !== options.columns) {\n resolved = resolveColumns(next.columns);\n byId = new Map(resolved.map((c) => [c.id, c]));\n tanStackColumns = resolved.map(toTanStack);\n }\n options = merged;\n views = undefined;\n table.setOptions((prev) => ({ ...prev, ...tableOptions() }));\n },\n\n get columns() {\n return (views ??= buildViews());\n },\n\n get rowCount() {\n return rows().length;\n },\n\n get totalWidth() {\n return table.getTotalSize();\n },\n\n rowAt(index) {\n const row = rows()[index];\n if (!row) return undefined;\n let view = rowViews.get(row);\n if (view?.index !== index) {\n view = { id: row.id, index, original: row.original as TData };\n rowViews.set(row, view);\n }\n return view;\n },\n\n indexOf(rowId) {\n const current = rows();\n if (indexById?.rows !== current) {\n indexById = { rows: current, map: new Map(current.map((r, i) => [r.id, i])) };\n }\n return indexById.map.get(rowId);\n },\n\n valueAt(index, columnId) {\n return rows()[index]?.getValue(columnId);\n },\n\n cellContext(index, columnId) {\n const row = rows()[index];\n if (!row || !byId.has(columnId)) return undefined;\n return {\n row: row.original as TData,\n rowId: row.id,\n index,\n columnId,\n value: row.getValue(columnId),\n };\n },\n\n nextSorting(columnId) {\n if (byId.get(columnId)?.sortable !== true) return undefined;\n const current = options.sorting.find((s) => s.id === columnId);\n if (!current) return [{ id: columnId, desc: false }];\n return current.desc ? [] : [{ id: columnId, desc: true }];\n },\n\n nextSizing(columnId, width) {\n const column = byId.get(columnId);\n if (!column?.resizable || !Number.isFinite(width)) return undefined;\n const clamped = Math.round(Math.min(column.maxWidth, Math.max(column.minWidth, width)));\n return { ...options.columnSizing, [columnId]: clamped };\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// ../core/src/virtual/row-pool.ts
|
|
2
|
+
/**
|
|
3
|
+
* Assigns pool slots to rendered rows (ADR 0001).
|
|
4
|
+
*
|
|
5
|
+
* A row keeps its slot while it stays rendered. A row entering the window takes a
|
|
6
|
+
* slot freed by a row that left, so its DOM is updated in place rather than created.
|
|
7
|
+
* Rows are returned in slot order ("pool order"); the ADR 0001 addendum ruled out
|
|
8
|
+
* re-sorting them by row index, so assistive tech gets row positions from
|
|
9
|
+
* `aria-rowindex` instead.
|
|
10
|
+
*/
|
|
11
|
+
var RowPool = class {
|
|
12
|
+
#slots = /* @__PURE__ */ new Map();
|
|
13
|
+
#free = [];
|
|
14
|
+
#size = 0;
|
|
15
|
+
#pinned;
|
|
16
|
+
/** Number of slots created so far: the most rows rendered at once. */
|
|
17
|
+
get size() {
|
|
18
|
+
return this.#size;
|
|
19
|
+
}
|
|
20
|
+
/** The row id whose slot is kept while it is out of range, if any. */
|
|
21
|
+
get pinned() {
|
|
22
|
+
return this.#pinned;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Keeps one row's slot, and keeps it rendered, while it is outside the rendered
|
|
26
|
+
* range. Used for the row holding focus. `undefined` releases it.
|
|
27
|
+
*/
|
|
28
|
+
pin(id) {
|
|
29
|
+
this.#pinned = id;
|
|
30
|
+
}
|
|
31
|
+
/** The slot a row holds, or `undefined` when it isn't rendered. */
|
|
32
|
+
slotOf(id) {
|
|
33
|
+
return this.#slots.get(id);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Updates the pool for the rows that should be rendered and returns them, plus the
|
|
37
|
+
* pinned row if it isn't among them, in slot order.
|
|
38
|
+
*/
|
|
39
|
+
update(ids) {
|
|
40
|
+
const keep = new Set(ids);
|
|
41
|
+
if (this.#pinned !== void 0) keep.add(this.#pinned);
|
|
42
|
+
for (const [id, slot] of this.#slots) if (!keep.has(id)) {
|
|
43
|
+
this.#slots.delete(id);
|
|
44
|
+
this.#free.push(slot);
|
|
45
|
+
}
|
|
46
|
+
for (const id of keep) if (!this.#slots.has(id)) this.#slots.set(id, this.#free.pop() ?? this.#size++);
|
|
47
|
+
const entries = [];
|
|
48
|
+
for (const [id, slot] of this.#slots) entries.push({
|
|
49
|
+
id,
|
|
50
|
+
slot
|
|
51
|
+
});
|
|
52
|
+
return entries.sort((a, b) => a.slot - b.slot);
|
|
53
|
+
}
|
|
54
|
+
/** Frees every slot, for example when the data is replaced. */
|
|
55
|
+
clear() {
|
|
56
|
+
this.#slots.clear();
|
|
57
|
+
this.#free = [];
|
|
58
|
+
this.#size = 0;
|
|
59
|
+
this.#pinned = void 0;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
export { RowPool };
|
|
63
|
+
|
|
64
|
+
//# sourceMappingURL=row-pool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"row-pool.js","names":[],"sources":["../../../../../core/src/virtual/row-pool.ts"],"sourcesContent":["/** A rendered row and the pool slot whose DOM it uses. */\nexport interface PoolEntry {\n id: string;\n slot: number;\n}\n\n/**\n * Assigns pool slots to rendered rows (ADR 0001).\n *\n * A row keeps its slot while it stays rendered. A row entering the window takes a\n * slot freed by a row that left, so its DOM is updated in place rather than created.\n * Rows are returned in slot order (\"pool order\"); the ADR 0001 addendum ruled out\n * re-sorting them by row index, so assistive tech gets row positions from\n * `aria-rowindex` instead.\n */\nexport class RowPool {\n #slots = new Map<string, number>();\n #free: number[] = [];\n #size = 0;\n #pinned: string | undefined;\n\n /** Number of slots created so far: the most rows rendered at once. */\n get size(): number {\n return this.#size;\n }\n\n /** The row id whose slot is kept while it is out of range, if any. */\n get pinned(): string | undefined {\n return this.#pinned;\n }\n\n /**\n * Keeps one row's slot, and keeps it rendered, while it is outside the rendered\n * range. Used for the row holding focus. `undefined` releases it.\n */\n pin(id: string | undefined): void {\n this.#pinned = id;\n }\n\n /** The slot a row holds, or `undefined` when it isn't rendered. */\n slotOf(id: string): number | undefined {\n return this.#slots.get(id);\n }\n\n /**\n * Updates the pool for the rows that should be rendered and returns them, plus the\n * pinned row if it isn't among them, in slot order.\n */\n update(ids: Iterable<string>): PoolEntry[] {\n const keep = new Set(ids);\n if (this.#pinned !== undefined) keep.add(this.#pinned);\n\n for (const [id, slot] of this.#slots) {\n if (!keep.has(id)) {\n this.#slots.delete(id);\n this.#free.push(slot);\n }\n }\n for (const id of keep) {\n if (!this.#slots.has(id)) this.#slots.set(id, this.#free.pop() ?? this.#size++);\n }\n\n const entries: PoolEntry[] = [];\n for (const [id, slot] of this.#slots) entries.push({ id, slot });\n return entries.sort((a, b) => a.slot - b.slot);\n }\n\n /** Frees every slot, for example when the data is replaced. */\n clear(): void {\n this.#slots.clear();\n this.#free = [];\n this.#size = 0;\n this.#pinned = undefined;\n }\n}\n"],"mappings":";;;;;;;;;;AAeA,IAAa,UAAb,MAAqB;CACnB,yBAAS,IAAI,IAAoB;CACjC,QAAkB,CAAC;CACnB,QAAQ;CACR;;CAGA,IAAI,OAAe;EACjB,OAAO,KAAK;CACd;;CAGA,IAAI,SAA6B;EAC/B,OAAO,KAAK;CACd;;;;;CAMA,IAAI,IAA8B;EAChC,KAAK,UAAU;CACjB;;CAGA,OAAO,IAAgC;EACrC,OAAO,KAAK,OAAO,IAAI,EAAE;CAC3B;;;;;CAMA,OAAO,KAAoC;EACzC,MAAM,OAAO,IAAI,IAAI,GAAG;EACxB,IAAI,KAAK,YAAY,KAAA,GAAW,KAAK,IAAI,KAAK,OAAO;EAErD,KAAK,MAAM,CAAC,IAAI,SAAS,KAAK,QAC5B,IAAI,CAAC,KAAK,IAAI,EAAE,GAAG;GACjB,KAAK,OAAO,OAAO,EAAE;GACrB,KAAK,MAAM,KAAK,IAAI;EACtB;EAEF,KAAK,MAAM,MAAM,MACf,IAAI,CAAC,KAAK,OAAO,IAAI,EAAE,GAAG,KAAK,OAAO,IAAI,IAAI,KAAK,MAAM,IAAI,KAAK,KAAK,OAAO;EAGhF,MAAM,UAAuB,CAAC;EAC9B,KAAK,MAAM,CAAC,IAAI,SAAS,KAAK,QAAQ,QAAQ,KAAK;GAAE;GAAI;EAAK,CAAC;EAC/D,OAAO,QAAQ,MAAM,GAAG,MAAM,EAAE,OAAO,EAAE,IAAI;CAC/C;;CAGA,QAAc;EACZ,KAAK,OAAO,MAAM;EAClB,KAAK,QAAQ,CAAC;EACd,KAAK,QAAQ;EACb,KAAK,UAAU,KAAA;CACjB;AACF"}
|