argus-table 0.1.0 → 0.2.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 +25 -2
- package/dist/announce.js +27 -0
- package/dist/announce.js.map +1 -0
- package/dist/argus-column.js +121 -0
- package/dist/argus-column.js.map +1 -0
- package/dist/argus-table.js +585 -83
- package/dist/argus-table.js.map +1 -1
- package/dist/cdn/argus-table.js +79 -4533
- package/dist/cdn/argus-table.js.map +1 -1
- package/dist/controllers/auto-width.js +89 -0
- package/dist/controllers/auto-width.js.map +1 -0
- package/dist/controllers/column-children.js +41 -0
- package/dist/controllers/column-children.js.map +1 -0
- package/dist/controllers/data-controller.js +177 -0
- package/dist/controllers/data-controller.js.map +1 -0
- package/dist/controllers/reorder-drag.js +160 -0
- package/dist/controllers/reorder-drag.js.map +1 -0
- package/dist/controllers/resize-drag.js +3 -0
- package/dist/controllers/resize-drag.js.map +1 -1
- package/dist/controllers/table-controller.js +2 -1
- package/dist/controllers/table-controller.js.map +1 -1
- package/dist/controllers/virtual-controller.js +146 -9
- package/dist/controllers/virtual-controller.js.map +1 -1
- package/dist/core/src/columns/column-order.js +50 -0
- package/dist/core/src/columns/column-order.js.map +1 -0
- package/dist/core/src/columns/resolve-columns.js +5 -1
- package/dist/core/src/columns/resolve-columns.js.map +1 -1
- package/dist/core/src/data/page-cache.js +143 -0
- package/dist/core/src/data/page-cache.js.map +1 -0
- package/dist/core/src/grid/row-layout.js +58 -0
- package/dist/core/src/grid/row-layout.js.map +1 -0
- package/dist/core/src/table/create-table.js +112 -14
- package/dist/core/src/table/create-table.js.map +1 -1
- package/dist/core/src/table/sorted-row-model.js +86 -0
- package/dist/core/src/table/sorted-row-model.js.map +1 -0
- package/dist/core/src/virtual/scroll-scale.js +74 -0
- package/dist/core/src/virtual/scroll-scale.js.map +1 -0
- package/dist/custom-elements.json +1463 -62
- package/dist/events.js +5 -1
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +252 -27
- package/dist/index.js +3 -2
- package/dist/max-height.js +68 -0
- package/dist/max-height.js.map +1 -0
- package/dist/render/body.js +53 -20
- package/dist/render/body.js.map +1 -1
- package/dist/render/cells.js +21 -4
- package/dist/render/cells.js.map +1 -1
- package/dist/render/content.js +12 -5
- package/dist/render/content.js.map +1 -1
- package/dist/render/header.js +21 -4
- package/dist/render/header.js.map +1 -1
- package/dist/render/pinned-rows.js +29 -0
- package/dist/render/pinned-rows.js.map +1 -0
- package/dist/styles/base.js +1 -1
- package/dist/styles/base.js.map +1 -1
- package/package.json +2 -1
|
@@ -1,27 +1,111 @@
|
|
|
1
1
|
import { RowPool } from "../core/src/virtual/row-pool.js";
|
|
2
|
+
import { ScrollScale } from "../core/src/virtual/scroll-scale.js";
|
|
2
3
|
import { VirtualizerController } from "@tanstack/lit-virtual";
|
|
3
4
|
// src/controllers/virtual-controller.ts
|
|
4
5
|
/**
|
|
5
6
|
* Row virtualization (TanStack Virtual) plus the row pool: which rows render,
|
|
6
7
|
* where, and which pool slot's DOM each one uses. The virtualizer registers itself
|
|
7
8
|
* with the host and requests updates on scroll.
|
|
9
|
+
*
|
|
10
|
+
* Scroll scaling: engines limit an element's height, so a body taller than the
|
|
11
|
+
* cap renders at the cap and its scroll position maps linearly onto the rows
|
|
12
|
+
* (`ScrollScale`). The virtualizer works in logical offsets: it gets a stand-in
|
|
13
|
+
* for the scroller whose `scrollTop`, `scrollHeight` and `scrollTo()` are
|
|
14
|
+
* logical, and everything else passes through. Rows render relative to the
|
|
15
|
+
* scroll position (`start` minus the difference between the logical and the
|
|
16
|
+
* physical offset). Scroll through this controller (`scrollBy`, `reveal`,
|
|
17
|
+
* `scrollToIndex`): physical px no longer match rows once the body is scaled.
|
|
8
18
|
*/
|
|
9
19
|
var VirtualController = class {
|
|
10
20
|
#virtual;
|
|
11
21
|
#pool = new RowPool();
|
|
22
|
+
#scale = new ScrollScale();
|
|
23
|
+
#host;
|
|
24
|
+
#getScrollElement;
|
|
25
|
+
#element;
|
|
26
|
+
#standIn;
|
|
12
27
|
#config = {
|
|
13
28
|
count: 0,
|
|
14
29
|
rowHeight: 40,
|
|
15
30
|
overscan: 6,
|
|
16
|
-
rowIdAt: String
|
|
31
|
+
rowIdAt: String,
|
|
32
|
+
cap: Infinity
|
|
17
33
|
};
|
|
34
|
+
/** A logical offset to restore after the update, once the body has its new height. */
|
|
35
|
+
#anchor;
|
|
18
36
|
constructor(host, getScrollElement) {
|
|
37
|
+
this.#host = host;
|
|
38
|
+
this.#getScrollElement = getScrollElement;
|
|
19
39
|
this.#virtual = new VirtualizerController(host, {
|
|
20
40
|
count: 0,
|
|
21
|
-
getScrollElement,
|
|
41
|
+
getScrollElement: () => this.#scroller(),
|
|
42
|
+
observeElementRect: (_instance, cb) => {
|
|
43
|
+
const element = this.#element;
|
|
44
|
+
if (!element) return void 0;
|
|
45
|
+
const report = () => {
|
|
46
|
+
cb({
|
|
47
|
+
width: element.clientWidth,
|
|
48
|
+
height: element.clientHeight
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
report();
|
|
52
|
+
const observer = new ResizeObserver(report);
|
|
53
|
+
observer.observe(element);
|
|
54
|
+
return () => {
|
|
55
|
+
observer.disconnect();
|
|
56
|
+
};
|
|
57
|
+
},
|
|
22
58
|
estimateSize: () => this.#config.rowHeight,
|
|
23
59
|
overscan: this.#config.overscan
|
|
24
60
|
});
|
|
61
|
+
host.addController(this);
|
|
62
|
+
}
|
|
63
|
+
/** The scroller's stand-in for the virtualizer: vertical offsets and height in logical px. */
|
|
64
|
+
#scroller() {
|
|
65
|
+
const element = this.#getScrollElement();
|
|
66
|
+
if (!element) return null;
|
|
67
|
+
if (this.#element !== element || !this.#standIn) {
|
|
68
|
+
this.#element = element;
|
|
69
|
+
this.#standIn = new Proxy(element, { get: (target, key) => {
|
|
70
|
+
if (key === "scrollTop") return this.#top(target);
|
|
71
|
+
if (key === "scrollHeight") return target.scrollHeight + this.#scale.excess;
|
|
72
|
+
if (key === "scrollTo") return (options) => {
|
|
73
|
+
if (options.top === void 0) target.scrollTo(options);
|
|
74
|
+
else this.#write(target, options.top, options.behavior);
|
|
75
|
+
};
|
|
76
|
+
const value = Reflect.get(target, key);
|
|
77
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
78
|
+
} });
|
|
79
|
+
}
|
|
80
|
+
return this.#standIn;
|
|
81
|
+
}
|
|
82
|
+
/** The scroller's logical offset. */
|
|
83
|
+
#top(element) {
|
|
84
|
+
const top = element.scrollTop;
|
|
85
|
+
const scale = this.#scale;
|
|
86
|
+
return scale.scaled ? scale.toLogical(top, element.scrollHeight - element.clientHeight) : top;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Scrolls to a logical offset. `round` rounds the physical one, so that a
|
|
90
|
+
* scaled offset moves the scroller at all.
|
|
91
|
+
*/
|
|
92
|
+
#write(element, logical, behavior, round) {
|
|
93
|
+
const scale = this.#scale;
|
|
94
|
+
if (!scale.scaled) {
|
|
95
|
+
element.scrollTo({
|
|
96
|
+
top: logical,
|
|
97
|
+
behavior
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const range = element.scrollHeight - element.clientHeight;
|
|
102
|
+
const target = Math.min(Math.max(logical, 0), range + scale.excess);
|
|
103
|
+
const physical = scale.toPhysical(target, range);
|
|
104
|
+
element.scrollTo({
|
|
105
|
+
top: round ? round(physical) : physical,
|
|
106
|
+
behavior
|
|
107
|
+
});
|
|
108
|
+
if (behavior !== "smooth") scale.land(target, element.scrollTop);
|
|
25
109
|
}
|
|
26
110
|
configure(next) {
|
|
27
111
|
const prev = this.#config;
|
|
@@ -33,6 +117,18 @@ var VirtualController = class {
|
|
|
33
117
|
});
|
|
34
118
|
if (next.rowHeight !== prev.rowHeight) this.#virtual.getVirtualizer().measure();
|
|
35
119
|
}
|
|
120
|
+
const wasScaled = this.#scale.excess > 0;
|
|
121
|
+
const offset = this.#virtual.getVirtualizer().scrollOffset;
|
|
122
|
+
if (this.#scale.configure(next.count * next.rowHeight, next.cap)) {
|
|
123
|
+
if ((wasScaled || this.#scale.scaled) && offset !== null && offset > 0) this.#anchor = offset;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
hostUpdated() {
|
|
127
|
+
const anchor = this.#anchor;
|
|
128
|
+
if (anchor === void 0) return;
|
|
129
|
+
this.#anchor = void 0;
|
|
130
|
+
if (this.#element) this.#write(this.#element, anchor);
|
|
131
|
+
this.#host.requestUpdate();
|
|
36
132
|
}
|
|
37
133
|
/**
|
|
38
134
|
* Computes the rendered window and updates the pool. Call once per render.
|
|
@@ -40,17 +136,23 @@ var VirtualController = class {
|
|
|
40
136
|
computeWindow() {
|
|
41
137
|
const v = this.#virtual.getVirtualizer();
|
|
42
138
|
const items = v.getVirtualItems();
|
|
139
|
+
const scaled = this.#scale.scaled;
|
|
140
|
+
const size = this.#scale.size;
|
|
141
|
+
const shift = scaled ? (v.scrollOffset ?? 0) - (this.#element?.scrollTop ?? 0) : 0;
|
|
43
142
|
const byId = /* @__PURE__ */ new Map();
|
|
44
143
|
for (const item of items) byId.set(this.#config.rowIdAt(item.index), {
|
|
45
144
|
index: item.index,
|
|
46
|
-
start: item.start
|
|
145
|
+
start: item.start - shift
|
|
47
146
|
});
|
|
48
|
-
const { keep } = this.#config;
|
|
147
|
+
const { keep, rowHeight } = this.#config;
|
|
49
148
|
this.#pool.pin(keep?.id);
|
|
50
|
-
if (keep && !byId.has(keep.id))
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
149
|
+
if (keep && !byId.has(keep.id)) {
|
|
150
|
+
const start = keep.index * rowHeight - shift;
|
|
151
|
+
byId.set(keep.id, {
|
|
152
|
+
index: keep.index,
|
|
153
|
+
start: scaled ? Math.min(Math.max(start, -rowHeight), size) : start
|
|
154
|
+
});
|
|
155
|
+
}
|
|
54
156
|
const rows = [];
|
|
55
157
|
for (const { id, slot } of this.#pool.update(byId.keys())) {
|
|
56
158
|
const item = byId.get(id);
|
|
@@ -62,7 +164,7 @@ var VirtualController = class {
|
|
|
62
164
|
const range = v.range;
|
|
63
165
|
return {
|
|
64
166
|
rows,
|
|
65
|
-
totalSize: v.getTotalSize(),
|
|
167
|
+
totalSize: scaled ? size : v.getTotalSize(),
|
|
66
168
|
range: range ? {
|
|
67
169
|
start: range.startIndex,
|
|
68
170
|
end: range.endIndex
|
|
@@ -73,10 +175,45 @@ var VirtualController = class {
|
|
|
73
175
|
get poolSize() {
|
|
74
176
|
return this.#pool.size;
|
|
75
177
|
}
|
|
178
|
+
/** Whether the body is taller than the cap and scrolls scaled. */
|
|
179
|
+
get scaled() {
|
|
180
|
+
return this.#scale.scaled;
|
|
181
|
+
}
|
|
76
182
|
/** `'start'` puts the row at the top; `'auto'` scrolls only as far as needed to show it. */
|
|
77
183
|
scrollToIndex(index, align = "start") {
|
|
78
184
|
this.#virtual.getVirtualizer().scrollToIndex(index, { align });
|
|
79
185
|
}
|
|
186
|
+
/** Scrolls by `delta` px of rows (logical px). */
|
|
187
|
+
scrollBy(delta) {
|
|
188
|
+
const element = this.#element;
|
|
189
|
+
if (element) this.#write(element, this.#top(element) + delta);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Scrolls the least distance that shows row `index` in full, between the
|
|
193
|
+
* content above the body (`before` px: the header and top pinned rows) and
|
|
194
|
+
* below it (`after` px). Returns whether the rows are yet to move there (with
|
|
195
|
+
* the update after the scroll event): until then the row's DOM isn't where it
|
|
196
|
+
* will be.
|
|
197
|
+
*/
|
|
198
|
+
reveal(index, before, after) {
|
|
199
|
+
const element = this.#element;
|
|
200
|
+
if (!element) return false;
|
|
201
|
+
const { rowHeight } = this.#config;
|
|
202
|
+
const offset = this.#top(element);
|
|
203
|
+
const view = element.clientHeight - before - after;
|
|
204
|
+
const start = index * rowHeight;
|
|
205
|
+
let target;
|
|
206
|
+
let round;
|
|
207
|
+
if (start < offset) {
|
|
208
|
+
target = start;
|
|
209
|
+
round = Math.floor;
|
|
210
|
+
} else if (start + rowHeight > offset + view) {
|
|
211
|
+
target = start + rowHeight - view;
|
|
212
|
+
round = Math.ceil;
|
|
213
|
+
} else target = offset;
|
|
214
|
+
if (target !== offset) this.#write(element, target, void 0, round);
|
|
215
|
+
return this.#virtual.getVirtualizer().scrollOffset !== this.#top(element);
|
|
216
|
+
}
|
|
80
217
|
};
|
|
81
218
|
export { VirtualController };
|
|
82
219
|
|
|
@@ -1 +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"}
|
|
1
|
+
{"version":3,"file":"virtual-controller.js","names":[],"sources":["../../src/controllers/virtual-controller.ts"],"sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from 'lit';\nimport { VirtualizerController } from '@tanstack/lit-virtual';\nimport { RowPool, ScrollScale } 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 /** Offset in the rendered body, in px (scaled bodies: relative to the scroll position). */\n start: number;\n}\n\nexport interface VirtualWindow {\n /** Rendered rows, in pool order (ADR 0001 addendum). */\n rows: WindowRow[];\n /** Rendered height of the body in px: all rows, or the cap. */\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 /** The tallest the body may render (`bodyCap()`); a taller one scrolls scaled. */\n cap: number;\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 *\n * Scroll scaling: engines limit an element's height, so a body taller than the\n * cap renders at the cap and its scroll position maps linearly onto the rows\n * (`ScrollScale`). The virtualizer works in logical offsets: it gets a stand-in\n * for the scroller whose `scrollTop`, `scrollHeight` and `scrollTo()` are\n * logical, and everything else passes through. Rows render relative to the\n * scroll position (`start` minus the difference between the logical and the\n * physical offset). Scroll through this controller (`scrollBy`, `reveal`,\n * `scrollToIndex`): physical px no longer match rows once the body is scaled.\n */\nexport class VirtualController implements ReactiveController {\n readonly #virtual: VirtualizerController<HTMLElement, Element>;\n readonly #pool = new RowPool();\n readonly #scale = new ScrollScale();\n readonly #host: ReactiveControllerHost;\n readonly #getScrollElement: () => HTMLElement | null;\n #element: HTMLElement | undefined;\n #standIn: HTMLElement | undefined;\n #config: VirtualConfig = {\n count: 0,\n rowHeight: 40,\n overscan: 6,\n rowIdAt: String,\n cap: Infinity,\n };\n /** A logical offset to restore after the update, once the body has its new height. */\n #anchor: number | undefined;\n\n constructor(host: ReactiveControllerHost, getScrollElement: () => HTMLElement | null) {\n this.#host = host;\n this.#getScrollElement = getScrollElement;\n this.#virtual = new VirtualizerController<HTMLElement, Element>(host, {\n count: 0,\n getScrollElement: () => this.#scroller(),\n // The real element: a ResizeObserver can't observe the stand-in.\n observeElementRect: (_instance, cb) => {\n const element = this.#element;\n if (!element) return undefined;\n const report = () => {\n cb({ width: element.clientWidth, height: element.clientHeight });\n };\n report();\n const observer = new ResizeObserver(report);\n observer.observe(element);\n return () => {\n observer.disconnect();\n };\n },\n estimateSize: () => this.#config.rowHeight,\n overscan: this.#config.overscan,\n });\n host.addController(this);\n }\n\n /** The scroller's stand-in for the virtualizer: vertical offsets and height in logical px. */\n #scroller(): HTMLElement | null {\n const element = this.#getScrollElement();\n if (!element) return null;\n if (this.#element !== element || !this.#standIn) {\n this.#element = element;\n this.#standIn = new Proxy(element, {\n get: (target, key) => {\n if (key === 'scrollTop') return this.#top(target);\n if (key === 'scrollHeight') return target.scrollHeight + this.#scale.excess;\n if (key === 'scrollTo') {\n return (options: ScrollToOptions) => {\n if (options.top === undefined) target.scrollTo(options);\n else this.#write(target, options.top, options.behavior);\n };\n }\n const value: unknown = Reflect.get(target, key);\n // Methods run on the element itself (DOM methods reject the proxy).\n return typeof value === 'function'\n ? (value as (...args: unknown[]) => unknown).bind(target)\n : value;\n },\n });\n }\n return this.#standIn;\n }\n\n /** The scroller's logical offset. */\n #top(element: HTMLElement): number {\n const top = element.scrollTop;\n const scale = this.#scale;\n return scale.scaled ? scale.toLogical(top, element.scrollHeight - element.clientHeight) : top;\n }\n\n /**\n * Scrolls to a logical offset. `round` rounds the physical one, so that a\n * scaled offset moves the scroller at all.\n */\n #write(\n element: HTMLElement,\n logical: number,\n behavior?: ScrollBehavior,\n round?: (px: number) => number,\n ): void {\n const scale = this.#scale;\n if (!scale.scaled) {\n element.scrollTo({ top: logical, behavior });\n return;\n }\n const range = element.scrollHeight - element.clientHeight;\n const target = Math.min(Math.max(logical, 0), range + scale.excess);\n const physical = scale.toPhysical(target, range);\n element.scrollTo({ top: round ? round(physical) : physical, behavior });\n if (behavior !== 'smooth') scale.land(target, element.scrollTop);\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 // Not `scaled`: TypeScript would take it for unchanged after `configure()`.\n const wasScaled = this.#scale.excess > 0;\n const offset = this.#virtual.getVirtualizer().scrollOffset;\n if (this.#scale.configure(next.count * next.rowHeight, next.cap)) {\n // A new mapping would move the rows under a scaled scroll position (a data\n // source growing, a zoom change): keep the logical offset once the body\n // has its new height.\n if ((wasScaled || this.#scale.scaled) && offset !== null && offset > 0) {\n this.#anchor = offset;\n }\n }\n }\n\n hostUpdated(): void {\n const anchor = this.#anchor;\n if (anchor === undefined) return;\n this.#anchor = undefined;\n if (this.#element) this.#write(this.#element, anchor);\n // The logical offset is the same, so the virtualizer sees no change; the\n // rows still have to move to the new physical one.\n this.#host.requestUpdate();\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 // Scaled: rows render relative to the scroll position. The physical offset is\n // read now, so rows for the virtualizer's offset fill the view even if the\n // scroller has moved on since its last scroll event.\n const scaled = this.#scale.scaled;\n const size = this.#scale.size;\n const shift = scaled ? (v.scrollOffset ?? 0) - (this.#element?.scrollTop ?? 0) : 0;\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 - shift });\n }\n const { keep, rowHeight } = 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. Scaled, it is\n // out of view: keep it inside the body (or just above it, under the header),\n // so it doesn't stretch the scroll range past the cap.\n const start = keep.index * rowHeight - shift;\n byId.set(keep.id, {\n index: keep.index,\n start: scaled ? Math.min(Math.max(start, -rowHeight), size) : start,\n });\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: scaled ? size : 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 /** Whether the body is taller than the cap and scrolls scaled. */\n get scaled(): boolean {\n return this.#scale.scaled;\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 /** Scrolls by `delta` px of rows (logical px). */\n scrollBy(delta: number): void {\n const element = this.#element;\n if (element) this.#write(element, this.#top(element) + delta);\n }\n\n /**\n * Scrolls the least distance that shows row `index` in full, between the\n * content above the body (`before` px: the header and top pinned rows) and\n * below it (`after` px). Returns whether the rows are yet to move there (with\n * the update after the scroll event): until then the row's DOM isn't where it\n * will be.\n */\n reveal(index: number, before: number, after: number): boolean {\n const element = this.#element;\n if (!element) return false;\n const { rowHeight } = this.#config;\n const offset = this.#top(element);\n const view = element.clientHeight - before - after;\n const start = index * rowHeight;\n let target: number;\n let round: ((px: number) => number) | undefined;\n if (start < offset) {\n target = start;\n round = Math.floor;\n } else if (start + rowHeight > offset + view) {\n target = start + rowHeight - view;\n round = Math.ceil;\n } else {\n target = offset;\n }\n if (target !== offset) this.#write(element, target, undefined, round);\n // Rendered for an offset the scroller has since left: they move with the\n // update after the scroll event.\n return this.#virtual.getVirtualizer().scrollOffset !== this.#top(element);\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// ../core/src/columns/column-order.ts
|
|
2
|
+
var groupOf = (column) => column.pin ?? "center";
|
|
3
|
+
/**
|
|
4
|
+
* The order the table shows: start-pinned, scrolling, end-pinned columns, each
|
|
5
|
+
* group in the sequence of `order`. Unknown and repeated ids are dropped; columns
|
|
6
|
+
* the order doesn't list follow the listed ones of their group, in `columns` order.
|
|
7
|
+
*/
|
|
8
|
+
function normalizeOrder(order, columns) {
|
|
9
|
+
const byId = new Map(columns.map((c) => [c.id, c]));
|
|
10
|
+
const listed = /* @__PURE__ */ new Set();
|
|
11
|
+
const groups = {
|
|
12
|
+
start: [],
|
|
13
|
+
center: [],
|
|
14
|
+
end: []
|
|
15
|
+
};
|
|
16
|
+
for (const id of order) {
|
|
17
|
+
const column = byId.get(id);
|
|
18
|
+
if (!column || listed.has(id)) continue;
|
|
19
|
+
listed.add(id);
|
|
20
|
+
groups[groupOf(column)].push(id);
|
|
21
|
+
}
|
|
22
|
+
for (const column of columns) if (!listed.has(column.id)) groups[groupOf(column)].push(column.id);
|
|
23
|
+
return [
|
|
24
|
+
...groups.start,
|
|
25
|
+
...groups.center,
|
|
26
|
+
...groups.end
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The order after moving a column to position `to` (0-based, in the whole order
|
|
31
|
+
* as the table shows it, counted after the move). `undefined` if the column can't
|
|
32
|
+
* move, `to` is outside the column's group, or nothing changes.
|
|
33
|
+
*/
|
|
34
|
+
function moveColumn(order, columns, columnId, to) {
|
|
35
|
+
const column = columns.find((c) => c.id === columnId);
|
|
36
|
+
if (!column?.reorderable || !Number.isInteger(to)) return void 0;
|
|
37
|
+
const current = normalizeOrder(order, columns);
|
|
38
|
+
if (to === current.indexOf(columnId)) return void 0;
|
|
39
|
+
const pinOf = new Map(columns.map((c) => [c.id, groupOf(c)]));
|
|
40
|
+
const group = groupOf(column);
|
|
41
|
+
const first = current.findIndex((id) => pinOf.get(id) === group);
|
|
42
|
+
const last = current.findLastIndex((id) => pinOf.get(id) === group);
|
|
43
|
+
if (to < first || to > last) return void 0;
|
|
44
|
+
const next = current.filter((id) => id !== columnId);
|
|
45
|
+
next.splice(to, 0, columnId);
|
|
46
|
+
return next;
|
|
47
|
+
}
|
|
48
|
+
export { moveColumn, normalizeOrder };
|
|
49
|
+
|
|
50
|
+
//# sourceMappingURL=column-order.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"column-order.js","names":[],"sources":["../../../../../core/src/columns/column-order.ts"],"sourcesContent":["import type { ColumnOrderState, ColumnPin } from '../types.js';\n\n/** What ordering needs to know about a column. */\nexport interface OrderableColumn {\n id: string;\n pin: ColumnPin | undefined;\n reorderable: boolean;\n}\n\ntype Group = ColumnPin | 'center';\n\nconst groupOf = (column: OrderableColumn): Group => column.pin ?? 'center';\n\n/**\n * The order the table shows: start-pinned, scrolling, end-pinned columns, each\n * group in the sequence of `order`. Unknown and repeated ids are dropped; columns\n * the order doesn't list follow the listed ones of their group, in `columns` order.\n */\nexport function normalizeOrder(\n order: readonly string[],\n columns: readonly OrderableColumn[],\n): ColumnOrderState {\n const byId = new Map(columns.map((c) => [c.id, c]));\n const listed = new Set<string>();\n const groups: Record<Group, string[]> = { start: [], center: [], end: [] };\n for (const id of order) {\n const column = byId.get(id);\n if (!column || listed.has(id)) continue;\n listed.add(id);\n groups[groupOf(column)].push(id);\n }\n for (const column of columns) {\n if (!listed.has(column.id)) groups[groupOf(column)].push(column.id);\n }\n return [...groups.start, ...groups.center, ...groups.end];\n}\n\n/**\n * The order after moving a column to position `to` (0-based, in the whole order\n * as the table shows it, counted after the move). `undefined` if the column can't\n * move, `to` is outside the column's group, or nothing changes.\n */\nexport function moveColumn(\n order: readonly string[],\n columns: readonly OrderableColumn[],\n columnId: string,\n to: number,\n): ColumnOrderState | undefined {\n const column = columns.find((c) => c.id === columnId);\n if (!column?.reorderable || !Number.isInteger(to)) return undefined;\n const current = normalizeOrder(order, columns);\n const from = current.indexOf(columnId);\n if (to === from) return undefined;\n const pinOf = new Map(columns.map((c) => [c.id, groupOf(c)]));\n const group = groupOf(column);\n const first = current.findIndex((id) => pinOf.get(id) === group);\n const last = current.findLastIndex((id) => pinOf.get(id) === group);\n if (to < first || to > last) return undefined;\n const next = current.filter((id) => id !== columnId);\n next.splice(to, 0, columnId);\n return next;\n}\n"],"mappings":";AAWA,IAAM,WAAW,WAAmC,OAAO,OAAO;;;;;;AAOlE,SAAgB,eACd,OACA,SACkB;CAClB,MAAM,OAAO,IAAI,IAAI,QAAQ,KAAK,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;CAClD,MAAM,yBAAS,IAAI,IAAY;CAC/B,MAAM,SAAkC;EAAE,OAAO,CAAC;EAAG,QAAQ,CAAC;EAAG,KAAK,CAAC;CAAE;CACzE,KAAK,MAAM,MAAM,OAAO;EACtB,MAAM,SAAS,KAAK,IAAI,EAAE;EAC1B,IAAI,CAAC,UAAU,OAAO,IAAI,EAAE,GAAG;EAC/B,OAAO,IAAI,EAAE;EACb,OAAO,QAAQ,MAAM,EAAE,CAAC,KAAK,EAAE;CACjC;CACA,KAAK,MAAM,UAAU,SACnB,IAAI,CAAC,OAAO,IAAI,OAAO,EAAE,GAAG,OAAO,QAAQ,MAAM,EAAE,CAAC,KAAK,OAAO,EAAE;CAEpE,OAAO;EAAC,GAAG,OAAO;EAAO,GAAG,OAAO;EAAQ,GAAG,OAAO;CAAG;AAC1D;;;;;;AAOA,SAAgB,WACd,OACA,SACA,UACA,IAC8B;CAC9B,MAAM,SAAS,QAAQ,MAAM,MAAM,EAAE,OAAO,QAAQ;CACpD,IAAI,CAAC,QAAQ,eAAe,CAAC,OAAO,UAAU,EAAE,GAAG,OAAO,KAAA;CAC1D,MAAM,UAAU,eAAe,OAAO,OAAO;CAE7C,IAAI,OADS,QAAQ,QAAQ,QAClB,GAAM,OAAO,KAAA;CACxB,MAAM,QAAQ,IAAI,IAAI,QAAQ,KAAK,MAAM,CAAC,EAAE,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC;CAC5D,MAAM,QAAQ,QAAQ,MAAM;CAC5B,MAAM,QAAQ,QAAQ,WAAW,OAAO,MAAM,IAAI,EAAE,MAAM,KAAK;CAC/D,MAAM,OAAO,QAAQ,eAAe,OAAO,MAAM,IAAI,EAAE,MAAM,KAAK;CAClE,IAAI,KAAK,SAAS,KAAK,MAAM,OAAO,KAAA;CACpC,MAAM,OAAO,QAAQ,QAAQ,OAAO,OAAO,QAAQ;CACnD,KAAK,OAAO,IAAI,GAAG,QAAQ;CAC3B,OAAO;AACT"}
|
|
@@ -36,7 +36,9 @@ function resolveColumns(defs) {
|
|
|
36
36
|
const pin = def.pin === "start" || def.pin === "end" ? def.pin : void 0;
|
|
37
37
|
const { width } = def;
|
|
38
38
|
const flex = pin === void 0 && typeof width === "object" && Number.isFinite(width.flex) && width.flex > 0 ? width.flex : void 0;
|
|
39
|
-
const
|
|
39
|
+
const auto = width === "auto";
|
|
40
|
+
const fixed = typeof width === "number" ? finite(width) : void 0;
|
|
41
|
+
const base = flex !== void 0 ? minWidth : clamp(fixed ?? 150);
|
|
40
42
|
const { accessor } = def;
|
|
41
43
|
const getValue = typeof accessor === "function" ? accessor : typeof accessor === "string" ? (row) => readPath(row, accessor) : () => void 0;
|
|
42
44
|
return {
|
|
@@ -48,11 +50,13 @@ function resolveColumns(defs) {
|
|
|
48
50
|
hasAccessor: accessor !== void 0,
|
|
49
51
|
width: base,
|
|
50
52
|
flex,
|
|
53
|
+
auto,
|
|
51
54
|
minWidth,
|
|
52
55
|
maxWidth,
|
|
53
56
|
pin,
|
|
54
57
|
sortable: def.sortable === true,
|
|
55
58
|
resizable: def.resizable !== false,
|
|
59
|
+
reorderable: def.reorderable !== false,
|
|
56
60
|
align: def.align ?? "start"
|
|
57
61
|
};
|
|
58
62
|
});
|
|
@@ -1 +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(
|
|
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 /** `true` for `width: 'auto'`: the table measures the content and passes the width in. */\n auto: boolean;\n minWidth: number;\n maxWidth: number;\n pin: ColumnPin | undefined;\n sortable: boolean;\n resizable: boolean;\n reorderable: 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 auto = width === 'auto';\n // An auto column shows the default width until it is measured.\n const fixed = typeof width === 'number' ? finite(width) : undefined;\n const base = flex !== undefined ? minWidth : clamp(fixed ?? 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 auto,\n minWidth,\n maxWidth,\n pin,\n sortable: def.sortable === true,\n resizable: def.resizable !== false,\n reorderable: def.reorderable !== false,\n align: def.align ?? 'start',\n };\n });\n}\n"],"mappings":"AAgCA,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,UAAU;EAEvB,MAAM,QAAQ,OAAO,UAAU,WAAW,OAAO,KAAK,IAAI,KAAA;EAC1D,MAAM,OAAO,SAAS,KAAA,IAAY,WAAW,MAAM,SAAA,GAAsB;EAEzE,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;GACA,UAAU,IAAI,aAAa;GAC3B,WAAW,IAAI,cAAc;GAC7B,aAAa,IAAI,gBAAgB;GACjC,OAAO,IAAI,SAAS;EACtB;CACF,CAAC;AACH"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The row cache behind `dataSource`: which pages are loaded, in flight or failed,
|
|
3
|
+
* and how many rows the table has. Bookkeeping only: no timers and no fetching,
|
|
4
|
+
* so the caller (the element's DataController) decides when to load.
|
|
5
|
+
*
|
|
6
|
+
* Without a known total, the row count stays one page past the last loaded row,
|
|
7
|
+
* so the next page shows as skeletons and loads when it scrolls into view. A page
|
|
8
|
+
* shorter than asked for marks the end.
|
|
9
|
+
*/
|
|
10
|
+
var PageCache = class {
|
|
11
|
+
#getRowId;
|
|
12
|
+
#pages = /* @__PURE__ */ new Map();
|
|
13
|
+
/** In-flight pages and the `end` they asked for. */
|
|
14
|
+
#inFlight = /* @__PURE__ */ new Map();
|
|
15
|
+
#failed = /* @__PURE__ */ new Set();
|
|
16
|
+
#indexById = /* @__PURE__ */ new Map();
|
|
17
|
+
#rowCount;
|
|
18
|
+
#totalKnown;
|
|
19
|
+
constructor(options) {
|
|
20
|
+
this.pageSize = Math.max(1, Math.floor(options.pageSize));
|
|
21
|
+
this.#getRowId = options.getRowId ?? ((_row, index) => String(index));
|
|
22
|
+
this.#rowCount = Math.max(0, Math.floor(options.rowCount ?? this.pageSize));
|
|
23
|
+
this.#totalKnown = options.totalKnown ?? false;
|
|
24
|
+
}
|
|
25
|
+
/** Number of rows, loaded or not. */
|
|
26
|
+
get rowCount() {
|
|
27
|
+
return this.#rowCount;
|
|
28
|
+
}
|
|
29
|
+
/** `true` once the total is known (given by the server, or found from a short page). */
|
|
30
|
+
get totalKnown() {
|
|
31
|
+
return this.#totalKnown;
|
|
32
|
+
}
|
|
33
|
+
pageOf(index) {
|
|
34
|
+
return Math.floor(index / this.pageSize);
|
|
35
|
+
}
|
|
36
|
+
/** The loaded row at an index, with its id; `undefined` until its page has loaded. */
|
|
37
|
+
entryAt(index) {
|
|
38
|
+
if (index < 0 || index >= this.#rowCount) return void 0;
|
|
39
|
+
return this.#pages.get(this.pageOf(index))?.[index % this.pageSize];
|
|
40
|
+
}
|
|
41
|
+
/** The index of a loaded row's id, or `undefined`. */
|
|
42
|
+
indexOf(rowId) {
|
|
43
|
+
return this.#indexById.get(rowId);
|
|
44
|
+
}
|
|
45
|
+
stateAt(index) {
|
|
46
|
+
if (this.entryAt(index)) return "loaded";
|
|
47
|
+
return this.#failed.has(this.pageOf(index)) ? "error" : "loading";
|
|
48
|
+
}
|
|
49
|
+
/** Pages touching the rows `start` to `end` (exclusive) that still need a request. */
|
|
50
|
+
missingPages(start, end) {
|
|
51
|
+
const last = Math.min(end, this.#rowCount) - 1;
|
|
52
|
+
const pages = [];
|
|
53
|
+
if (last < 0) return pages;
|
|
54
|
+
for (let page = this.pageOf(Math.max(0, start)); page <= this.pageOf(last); page++) if (!this.#pages.has(page) && !this.#inFlight.has(page) && !this.#failed.has(page)) pages.push(page);
|
|
55
|
+
return pages;
|
|
56
|
+
}
|
|
57
|
+
/** Marks a page as in flight and returns the rows to ask for. */
|
|
58
|
+
begin(page) {
|
|
59
|
+
const start = page * this.pageSize;
|
|
60
|
+
let end = start + this.pageSize;
|
|
61
|
+
if (this.#totalKnown) end = Math.min(end, this.#rowCount);
|
|
62
|
+
this.#failed.delete(page);
|
|
63
|
+
this.#inFlight.set(page, end);
|
|
64
|
+
return {
|
|
65
|
+
start,
|
|
66
|
+
end
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Stores a page's rows and updates the row count. Ignored unless the page is in flight. */
|
|
70
|
+
resolve(page, rows, total) {
|
|
71
|
+
const end = this.#inFlight.get(page);
|
|
72
|
+
if (end === void 0) return false;
|
|
73
|
+
this.#inFlight.delete(page);
|
|
74
|
+
const start = page * this.pageSize;
|
|
75
|
+
const asked = end - start;
|
|
76
|
+
const entries = rows.slice(0, asked).map((row, i) => ({
|
|
77
|
+
id: this.#getRowId(row, start + i),
|
|
78
|
+
row
|
|
79
|
+
}));
|
|
80
|
+
this.#drop(page);
|
|
81
|
+
if (entries.length > 0) this.#pages.set(page, entries);
|
|
82
|
+
entries.forEach((entry, i) => this.#indexById.set(entry.id, start + i));
|
|
83
|
+
if (total !== void 0 && Number.isFinite(total) && total >= 0) this.#setCount(Math.floor(total), true);
|
|
84
|
+
else if (entries.length < asked) this.#setCount(start + entries.length, true);
|
|
85
|
+
else if (!this.#totalKnown) this.#setCount(Math.max(this.#rowCount, end + this.pageSize), false);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
/** Marks an in-flight page as failed. Ignored unless the page is in flight. */
|
|
89
|
+
fail(page) {
|
|
90
|
+
if (!this.#inFlight.delete(page)) return false;
|
|
91
|
+
this.#failed.add(page);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
/** Lets a failed page be requested again. */
|
|
95
|
+
retry(page) {
|
|
96
|
+
this.#failed.delete(page);
|
|
97
|
+
}
|
|
98
|
+
/** Forgets an in-flight page (its request was aborted), so it can be requested again. */
|
|
99
|
+
cancel(page) {
|
|
100
|
+
this.#inFlight.delete(page);
|
|
101
|
+
}
|
|
102
|
+
/** `true` if a page failed and hasn't been retried or evicted since. */
|
|
103
|
+
isFailed(page) {
|
|
104
|
+
return this.#failed.has(page);
|
|
105
|
+
}
|
|
106
|
+
/** In-flight pages entirely outside rows `start` to `end` (exclusive). */
|
|
107
|
+
inFlightOutside(start, end) {
|
|
108
|
+
const first = this.pageOf(Math.max(0, start));
|
|
109
|
+
const last = this.pageOf(Math.max(0, end - 1));
|
|
110
|
+
return [...this.#inFlight.keys()].filter((page) => page < first || page > last);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Drops loaded and failed pages more than `KEEP_PAGES` pages away from rows
|
|
114
|
+
* `start` to `end` (exclusive). Failed pages that come back into view load again.
|
|
115
|
+
*/
|
|
116
|
+
evictOutside(start, end) {
|
|
117
|
+
const first = this.pageOf(Math.max(0, start)) - 10;
|
|
118
|
+
const last = this.pageOf(Math.max(0, end - 1)) + 10;
|
|
119
|
+
for (const page of [...this.#pages.keys()]) if (page < first || page > last) this.#drop(page);
|
|
120
|
+
for (const page of [...this.#failed]) if (page < first || page > last) this.#failed.delete(page);
|
|
121
|
+
}
|
|
122
|
+
/** Number of pages holding rows: bounded by eviction. */
|
|
123
|
+
get loadedPages() {
|
|
124
|
+
return this.#pages.size;
|
|
125
|
+
}
|
|
126
|
+
#setCount(count, known) {
|
|
127
|
+
this.#rowCount = count;
|
|
128
|
+
this.#totalKnown = known;
|
|
129
|
+
for (const page of [...this.#pages.keys()]) if (page * this.pageSize >= count) this.#drop(page);
|
|
130
|
+
}
|
|
131
|
+
#drop(page) {
|
|
132
|
+
const entries = this.#pages.get(page);
|
|
133
|
+
if (!entries) return;
|
|
134
|
+
const start = page * this.pageSize;
|
|
135
|
+
entries.forEach((entry, i) => {
|
|
136
|
+
if (this.#indexById.get(entry.id) === start + i) this.#indexById.delete(entry.id);
|
|
137
|
+
});
|
|
138
|
+
this.#pages.delete(page);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
export { PageCache };
|
|
142
|
+
|
|
143
|
+
//# sourceMappingURL=page-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-cache.js","names":[],"sources":["../../../../../core/src/data/page-cache.ts"],"sourcesContent":["/** Whether a row's data is there: loaded, still to come, or its page failed. */\nexport type RowState = 'loaded' | 'loading' | 'error';\n\n/** A loaded row and its id. */\nexport interface CachedRow<TData> {\n id: string;\n row: TData;\n}\n\nexport interface PageCacheOptions<TData> {\n /** Rows per page, at least 1. */\n pageSize: number;\n /** Row id function, given the absolute index. Default: the index as a string. */\n getRowId?: ((row: TData, index: number) => string) | undefined;\n /** Row count to start with (carried over from a previous cache). Default: one page. */\n rowCount?: number;\n /** Whether `rowCount` is the real total. Default: `false`. */\n totalKnown?: boolean;\n}\n\n/**\n * Pages kept loaded on each side of the requested range; pages further away are\n * dropped, so memory stays bounded however far the table scrolls.\n */\nexport const KEEP_PAGES = 10;\n\n/**\n * The row cache behind `dataSource`: which pages are loaded, in flight or failed,\n * and how many rows the table has. Bookkeeping only: no timers and no fetching,\n * so the caller (the element's DataController) decides when to load.\n *\n * Without a known total, the row count stays one page past the last loaded row,\n * so the next page shows as skeletons and loads when it scrolls into view. A page\n * shorter than asked for marks the end.\n */\nexport class PageCache<TData> {\n readonly pageSize: number;\n readonly #getRowId: (row: TData, index: number) => string;\n readonly #pages = new Map<number, CachedRow<TData>[]>();\n /** In-flight pages and the `end` they asked for. */\n readonly #inFlight = new Map<number, number>();\n readonly #failed = new Set<number>();\n readonly #indexById = new Map<string, number>();\n #rowCount: number;\n #totalKnown: boolean;\n\n constructor(options: PageCacheOptions<TData>) {\n this.pageSize = Math.max(1, Math.floor(options.pageSize));\n this.#getRowId = options.getRowId ?? ((_row, index) => String(index));\n this.#rowCount = Math.max(0, Math.floor(options.rowCount ?? this.pageSize));\n this.#totalKnown = options.totalKnown ?? false;\n }\n\n /** Number of rows, loaded or not. */\n get rowCount(): number {\n return this.#rowCount;\n }\n\n /** `true` once the total is known (given by the server, or found from a short page). */\n get totalKnown(): boolean {\n return this.#totalKnown;\n }\n\n pageOf(index: number): number {\n return Math.floor(index / this.pageSize);\n }\n\n /** The loaded row at an index, with its id; `undefined` until its page has loaded. */\n entryAt(index: number): CachedRow<TData> | undefined {\n if (index < 0 || index >= this.#rowCount) return undefined;\n return this.#pages.get(this.pageOf(index))?.[index % this.pageSize];\n }\n\n /** The index of a loaded row's id, or `undefined`. */\n indexOf(rowId: string): number | undefined {\n return this.#indexById.get(rowId);\n }\n\n stateAt(index: number): RowState {\n if (this.entryAt(index)) return 'loaded';\n return this.#failed.has(this.pageOf(index)) ? 'error' : 'loading';\n }\n\n /** Pages touching the rows `start` to `end` (exclusive) that still need a request. */\n missingPages(start: number, end: number): number[] {\n const last = Math.min(end, this.#rowCount) - 1;\n const pages: number[] = [];\n if (last < 0) return pages;\n for (let page = this.pageOf(Math.max(0, start)); page <= this.pageOf(last); page++) {\n if (!this.#pages.has(page) && !this.#inFlight.has(page) && !this.#failed.has(page)) {\n pages.push(page);\n }\n }\n return pages;\n }\n\n /** Marks a page as in flight and returns the rows to ask for. */\n begin(page: number): { start: number; end: number } {\n const start = page * this.pageSize;\n let end = start + this.pageSize;\n if (this.#totalKnown) end = Math.min(end, this.#rowCount);\n this.#failed.delete(page);\n this.#inFlight.set(page, end);\n return { start, end };\n }\n\n /** Stores a page's rows and updates the row count. Ignored unless the page is in flight. */\n resolve(page: number, rows: readonly TData[], total?: number): boolean {\n const end = this.#inFlight.get(page);\n if (end === undefined) return false;\n this.#inFlight.delete(page);\n const start = page * this.pageSize;\n const asked = end - start;\n const entries = rows.slice(0, asked).map((row, i) => ({\n id: this.#getRowId(row, start + i),\n row,\n }));\n this.#drop(page);\n if (entries.length > 0) this.#pages.set(page, entries);\n entries.forEach((entry, i) => this.#indexById.set(entry.id, start + i));\n\n if (total !== undefined && Number.isFinite(total) && total >= 0) {\n this.#setCount(Math.floor(total), true);\n } else if (entries.length < asked) {\n this.#setCount(start + entries.length, true);\n } else if (!this.#totalKnown) {\n this.#setCount(Math.max(this.#rowCount, end + this.pageSize), false);\n }\n return true;\n }\n\n /** Marks an in-flight page as failed. Ignored unless the page is in flight. */\n fail(page: number): boolean {\n if (!this.#inFlight.delete(page)) return false;\n this.#failed.add(page);\n return true;\n }\n\n /** Lets a failed page be requested again. */\n retry(page: number): void {\n this.#failed.delete(page);\n }\n\n /** Forgets an in-flight page (its request was aborted), so it can be requested again. */\n cancel(page: number): void {\n this.#inFlight.delete(page);\n }\n\n /** `true` if a page failed and hasn't been retried or evicted since. */\n isFailed(page: number): boolean {\n return this.#failed.has(page);\n }\n\n /** In-flight pages entirely outside rows `start` to `end` (exclusive). */\n inFlightOutside(start: number, end: number): number[] {\n const first = this.pageOf(Math.max(0, start));\n const last = this.pageOf(Math.max(0, end - 1));\n return [...this.#inFlight.keys()].filter((page) => page < first || page > last);\n }\n\n /**\n * Drops loaded and failed pages more than `KEEP_PAGES` pages away from rows\n * `start` to `end` (exclusive). Failed pages that come back into view load again.\n */\n evictOutside(start: number, end: number): void {\n const first = this.pageOf(Math.max(0, start)) - KEEP_PAGES;\n const last = this.pageOf(Math.max(0, end - 1)) + KEEP_PAGES;\n for (const page of [...this.#pages.keys()]) {\n if (page < first || page > last) this.#drop(page);\n }\n for (const page of [...this.#failed]) {\n if (page < first || page > last) this.#failed.delete(page);\n }\n }\n\n /** Number of pages holding rows: bounded by eviction. */\n get loadedPages(): number {\n return this.#pages.size;\n }\n\n #setCount(count: number, known: boolean): void {\n this.#rowCount = count;\n this.#totalKnown = known;\n // Rows past the end are gone; drop pages that start there.\n for (const page of [...this.#pages.keys()]) {\n if (page * this.pageSize >= count) this.#drop(page);\n }\n }\n\n #drop(page: number): void {\n const entries = this.#pages.get(page);\n if (!entries) return;\n const start = page * this.pageSize;\n entries.forEach((entry, i) => {\n if (this.#indexById.get(entry.id) === start + i) this.#indexById.delete(entry.id);\n });\n this.#pages.delete(page);\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// ../core/src/grid/row-layout.ts
|
|
2
|
+
/**
|
|
3
|
+
* Where each row sits in the grid as a whole: the header row, then the rows
|
|
4
|
+
* pinned to the top, the body rows, and the rows pinned to the bottom. The
|
|
5
|
+
* position is 0-based and `aria-rowindex` is one more. Keyboard navigation
|
|
6
|
+
* steps through positions, so it crosses sections without special cases.
|
|
7
|
+
*/
|
|
8
|
+
var RowLayout = class {
|
|
9
|
+
constructor(top, body, bottom) {
|
|
10
|
+
this.top = top;
|
|
11
|
+
this.body = body;
|
|
12
|
+
this.bottom = bottom;
|
|
13
|
+
}
|
|
14
|
+
/** Number of rows, the header included (`aria-rowcount`). */
|
|
15
|
+
get total() {
|
|
16
|
+
return 1 + this.top + this.body + this.bottom;
|
|
17
|
+
}
|
|
18
|
+
/** 0-based position of a row in the grid. */
|
|
19
|
+
position({ section, index }) {
|
|
20
|
+
switch (section) {
|
|
21
|
+
case "header": return 0;
|
|
22
|
+
case "top": return 1 + index;
|
|
23
|
+
case "body": return 1 + this.top + index;
|
|
24
|
+
case "bottom": return 1 + this.top + this.body + index;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** The row at a 0-based position, or `undefined` outside the grid. */
|
|
28
|
+
at(position) {
|
|
29
|
+
if (!Number.isInteger(position) || position < 0 || position >= this.total) return void 0;
|
|
30
|
+
if (position === 0) return {
|
|
31
|
+
section: "header",
|
|
32
|
+
index: 0
|
|
33
|
+
};
|
|
34
|
+
let index = position - 1;
|
|
35
|
+
if (index < this.top) return {
|
|
36
|
+
section: "top",
|
|
37
|
+
index
|
|
38
|
+
};
|
|
39
|
+
index -= this.top;
|
|
40
|
+
if (index < this.body) return {
|
|
41
|
+
section: "body",
|
|
42
|
+
index
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
section: "bottom",
|
|
46
|
+
index: index - this.body
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
ariaRowIndex(row) {
|
|
50
|
+
return this.position(row) + 1;
|
|
51
|
+
}
|
|
52
|
+
fromAriaRowIndex(ariaRowIndex) {
|
|
53
|
+
return this.at(ariaRowIndex - 1);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
export { RowLayout };
|
|
57
|
+
|
|
58
|
+
//# sourceMappingURL=row-layout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"row-layout.js","names":[],"sources":["../../../../../core/src/grid/row-layout.ts"],"sourcesContent":["/** The row groups of the grid, top to bottom. */\nexport type GridSection = 'header' | 'top' | 'body' | 'bottom';\n\n/** A row of the grid: its section and its index within it (the header is index 0). */\nexport interface GridRow {\n section: GridSection;\n index: number;\n}\n\n/**\n * Where each row sits in the grid as a whole: the header row, then the rows\n * pinned to the top, the body rows, and the rows pinned to the bottom. The\n * position is 0-based and `aria-rowindex` is one more. Keyboard navigation\n * steps through positions, so it crosses sections without special cases.\n */\nexport class RowLayout {\n readonly top: number;\n readonly body: number;\n readonly bottom: number;\n\n constructor(top: number, body: number, bottom: number) {\n this.top = top;\n this.body = body;\n this.bottom = bottom;\n }\n\n /** Number of rows, the header included (`aria-rowcount`). */\n get total(): number {\n return 1 + this.top + this.body + this.bottom;\n }\n\n /** 0-based position of a row in the grid. */\n position({ section, index }: GridRow): number {\n switch (section) {\n case 'header':\n return 0;\n case 'top':\n return 1 + index;\n case 'body':\n return 1 + this.top + index;\n case 'bottom':\n return 1 + this.top + this.body + index;\n }\n }\n\n /** The row at a 0-based position, or `undefined` outside the grid. */\n at(position: number): GridRow | undefined {\n if (!Number.isInteger(position) || position < 0 || position >= this.total) return undefined;\n if (position === 0) return { section: 'header', index: 0 };\n let index = position - 1;\n if (index < this.top) return { section: 'top', index };\n index -= this.top;\n if (index < this.body) return { section: 'body', index };\n return { section: 'bottom', index: index - this.body };\n }\n\n ariaRowIndex(row: GridRow): number {\n return this.position(row) + 1;\n }\n\n fromAriaRowIndex(ariaRowIndex: number): GridRow | undefined {\n return this.at(ariaRowIndex - 1);\n }\n}\n"],"mappings":";;;;;;;AAeA,IAAa,YAAb,MAAuB;CAKrB,YAAY,KAAa,MAAc,QAAgB;EACrD,KAAK,MAAM;EACX,KAAK,OAAO;EACZ,KAAK,SAAS;CAChB;;CAGA,IAAI,QAAgB;EAClB,OAAO,IAAI,KAAK,MAAM,KAAK,OAAO,KAAK;CACzC;;CAGA,SAAS,EAAE,SAAS,SAA0B;EAC5C,QAAQ,SAAR;GACE,KAAK,UACH,OAAO;GACT,KAAK,OACH,OAAO,IAAI;GACb,KAAK,QACH,OAAO,IAAI,KAAK,MAAM;GACxB,KAAK,UACH,OAAO,IAAI,KAAK,MAAM,KAAK,OAAO;EACtC;CACF;;CAGA,GAAG,UAAuC;EACxC,IAAI,CAAC,OAAO,UAAU,QAAQ,KAAK,WAAW,KAAK,YAAY,KAAK,OAAO,OAAO,KAAA;EAClF,IAAI,aAAa,GAAG,OAAO;GAAE,SAAS;GAAU,OAAO;EAAE;EACzD,IAAI,QAAQ,WAAW;EACvB,IAAI,QAAQ,KAAK,KAAK,OAAO;GAAE,SAAS;GAAO;EAAM;EACrD,SAAS,KAAK;EACd,IAAI,QAAQ,KAAK,MAAM,OAAO;GAAE,SAAS;GAAQ;EAAM;EACvD,OAAO;GAAE,SAAS;GAAU,OAAO,QAAQ,KAAK;EAAK;CACvD;CAEA,aAAa,KAAsB;EACjC,OAAO,KAAK,SAAS,GAAG,IAAI;CAC9B;CAEA,iBAAiB,cAA2C;EAC1D,OAAO,KAAK,GAAG,eAAe,CAAC;CACjC;AACF"}
|