argus-table 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +1 -1
  2. package/dist/argus-column.js +24 -1
  3. package/dist/argus-column.js.map +1 -1
  4. package/dist/argus-table.js +1024 -165
  5. package/dist/argus-table.js.map +1 -1
  6. package/dist/cdn/argus-table.js +74 -50
  7. package/dist/cdn/argus-table.js.map +1 -1
  8. package/dist/controllers/auto-width.js +11 -0
  9. package/dist/controllers/auto-width.js.map +1 -1
  10. package/dist/controllers/column-window.js +139 -0
  11. package/dist/controllers/column-window.js.map +1 -0
  12. package/dist/controllers/data-controller.js +9 -4
  13. package/dist/controllers/data-controller.js.map +1 -1
  14. package/dist/controllers/plugins.js +102 -0
  15. package/dist/controllers/plugins.js.map +1 -0
  16. package/dist/controllers/reorder-drag.js +9 -10
  17. package/dist/controllers/reorder-drag.js.map +1 -1
  18. package/dist/controllers/row-measure.js +121 -0
  19. package/dist/controllers/row-measure.js.map +1 -0
  20. package/dist/controllers/virtual-controller.js +311 -10
  21. package/dist/controllers/virtual-controller.js.map +1 -1
  22. package/dist/core/src/columns/column-order.js +14 -10
  23. package/dist/core/src/columns/column-order.js.map +1 -1
  24. package/dist/core/src/columns/resolve-columns.js +18 -4
  25. package/dist/core/src/columns/resolve-columns.js.map +1 -1
  26. package/dist/core/src/data/page-cache.js +4 -0
  27. package/dist/core/src/data/page-cache.js.map +1 -1
  28. package/dist/core/src/filters/filter.js +185 -0
  29. package/dist/core/src/filters/filter.js.map +1 -0
  30. package/dist/core/src/state/selection.js +70 -0
  31. package/dist/core/src/state/selection.js.map +1 -0
  32. package/dist/core/src/table/create-table.js +239 -64
  33. package/dist/core/src/table/create-table.js.map +1 -1
  34. package/dist/core/src/table/filtered-row-model.js +48 -0
  35. package/dist/core/src/table/filtered-row-model.js.map +1 -0
  36. package/dist/core/src/table/sorted-row-model.js +13 -2
  37. package/dist/core/src/table/sorted-row-model.js.map +1 -1
  38. package/dist/core/src/virtual/column-window.js +72 -0
  39. package/dist/core/src/virtual/column-window.js.map +1 -0
  40. package/dist/core/src/virtual/row-pool.js +2 -1
  41. package/dist/core/src/virtual/row-pool.js.map +1 -1
  42. package/dist/core/src/virtual/row-sizes.js +156 -0
  43. package/dist/core/src/virtual/row-sizes.js.map +1 -0
  44. package/dist/core/src/virtual/scroll-scale.js +41 -3
  45. package/dist/core/src/virtual/scroll-scale.js.map +1 -1
  46. package/dist/custom-elements.json +1273 -45
  47. package/dist/events.js +9 -1
  48. package/dist/events.js.map +1 -1
  49. package/dist/index.d.ts +609 -74
  50. package/dist/index.js +3 -2
  51. package/dist/interactive.js +22 -0
  52. package/dist/interactive.js.map +1 -0
  53. package/dist/max-height.js +22 -8
  54. package/dist/max-height.js.map +1 -1
  55. package/dist/render/body.js +47 -7
  56. package/dist/render/body.js.map +1 -1
  57. package/dist/render/cells.js +14 -11
  58. package/dist/render/cells.js.map +1 -1
  59. package/dist/render/content-directive.js +115 -0
  60. package/dist/render/content-directive.js.map +1 -0
  61. package/dist/render/content.js +20 -9
  62. package/dist/render/content.js.map +1 -1
  63. package/dist/render/header.js +24 -5
  64. package/dist/render/header.js.map +1 -1
  65. package/dist/render/pinned-rows.js +1 -1
  66. package/dist/render/pinned-rows.js.map +1 -1
  67. package/dist/styles/base.js +1 -1
  68. package/dist/styles/base.js.map +1 -1
  69. package/package.json +1 -1
@@ -0,0 +1,121 @@
1
+ // src/controllers/row-measure.ts
2
+ /**
3
+ * Rows of different heights (`rowHeight: 'auto'`): reads the heights of the
4
+ * rendered body rows and of the pinned row groups once their content has
5
+ * rendered, and watches them for changes the table doesn't render (an image
6
+ * loading, a web font, a framework component updating), which request an
7
+ * update that reads them again. Pooled rows show other rows as the body
8
+ * scrolls, so every update reads every rendered row: one that now shows a row
9
+ * of the same height doesn't resize, and the observer wouldn't report it.
10
+ */
11
+ var RowMeasure = class {
12
+ #host;
13
+ #options;
14
+ #observer;
15
+ #observed = /* @__PURE__ */ new Set();
16
+ #pinned = {
17
+ top: 0,
18
+ bottom: 0
19
+ };
20
+ /** The row each body row element showed when it was last read. */
21
+ #read = /* @__PURE__ */ new WeakMap();
22
+ /** Read every row next time, not only those that show another row now. */
23
+ #all = true;
24
+ constructor(host, options) {
25
+ this.#host = host;
26
+ this.#options = options;
27
+ }
28
+ /** The pinned groups' heights from the last `read()`. */
29
+ get pinned() {
30
+ return this.#pinned;
31
+ }
32
+ /**
33
+ * The next `read()` reads every row: their content may have changed size
34
+ * (new data or columns, a resize, content that grew).
35
+ */
36
+ invalidate() {
37
+ this.#all = true;
38
+ }
39
+ /**
40
+ * Reads the rows in `root` (the shadow root): the body rows' heights as
41
+ * `[index, px]`, and whether the pinned groups changed height. Scrolling,
42
+ * only the pooled rows that now show another row are read.
43
+ */
44
+ read(root) {
45
+ const observer = this.#observer ??= new ResizeObserver(this.#onResize);
46
+ for (const element of this.#observed) if (!element.isConnected) {
47
+ observer.unobserve(element);
48
+ this.#observed.delete(element);
49
+ }
50
+ const rows = [];
51
+ const due = [];
52
+ for (const row of root.querySelectorAll("[part~=\"body\"] > .row")) {
53
+ this.#observe(row);
54
+ const index = this.#options.bodyIndex(row);
55
+ if (index === void 0) this.#read.delete(row);
56
+ else if (this.#all || this.#read.get(row) !== index) due.push([row, index]);
57
+ }
58
+ this.#all = false;
59
+ for (const [row] of due) row.setAttribute("data-measure-row", "");
60
+ for (const [row, index] of due) {
61
+ rows.push([index, row.getBoundingClientRect().height]);
62
+ this.#read.set(row, index);
63
+ }
64
+ for (const [row] of due) row.removeAttribute("data-measure-row");
65
+ const group = (side) => {
66
+ const element = root.querySelector(`.pinned[data-side="${side}"]`);
67
+ if (element) this.#observe(element);
68
+ return element ? element.getBoundingClientRect().height : 0;
69
+ };
70
+ const pinned = {
71
+ top: group("top"),
72
+ bottom: group("bottom")
73
+ };
74
+ const pinnedChanged = Math.abs(pinned.top - this.#pinned.top) > .01 || Math.abs(pinned.bottom - this.#pinned.bottom) > .01;
75
+ if (pinnedChanged) this.#pinned = pinned;
76
+ return {
77
+ rows,
78
+ pinnedChanged
79
+ };
80
+ }
81
+ /** Stops watching (fixed row heights again, or the table left the page). */
82
+ stop() {
83
+ this.#observer?.disconnect();
84
+ this.#observed.clear();
85
+ this.#pinned = {
86
+ top: 0,
87
+ bottom: 0
88
+ };
89
+ this.#all = true;
90
+ }
91
+ #observe(element) {
92
+ if (this.#observed.has(element)) return;
93
+ this.#observed.add(element);
94
+ this.#observer?.observe(element);
95
+ }
96
+ /** Requests an update only for a height the table doesn't have yet. */
97
+ #onResize = (entries) => {
98
+ const { bodyIndex, sizeOf, minimum } = this.#options;
99
+ for (const entry of entries) {
100
+ const target = entry.target;
101
+ if (!target.isConnected) continue;
102
+ const px = entry.borderBoxSize[0]?.blockSize ?? target.getBoundingClientRect().height;
103
+ let known;
104
+ if (target.classList.contains("pinned")) known = target.getAttribute("data-side") === "top" ? this.#pinned.top : this.#pinned.bottom;
105
+ else {
106
+ const index = bodyIndex(target);
107
+ if (index === void 0) continue;
108
+ known = sizeOf(index);
109
+ if (px < minimum() || px <= known + .01) continue;
110
+ }
111
+ if (Math.abs(px - known) > .01) {
112
+ this.#all = true;
113
+ this.#host.requestUpdate();
114
+ return;
115
+ }
116
+ }
117
+ };
118
+ };
119
+ export { RowMeasure };
120
+
121
+ //# sourceMappingURL=row-measure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"row-measure.js","names":[],"sources":["../../src/controllers/row-measure.ts"],"sourcesContent":["import type { ReactiveControllerHost } from 'lit';\n\n/** The heights of the pinned row groups, in px. */\nexport interface PinnedHeights {\n top: number;\n bottom: number;\n}\n\nexport interface RowMeasureOptions {\n /** A body row element's index in the row order; `undefined` to leave it out (a row still loading). */\n bodyIndex: (row: Element) => number | undefined;\n /** The height the virtualizer has for a body row now. */\n sizeOf: (index: number) => number;\n /** The least a row is (the row height). */\n minimum: () => number;\n}\n\n/**\n * Rows of different heights (`rowHeight: 'auto'`): reads the heights of the\n * rendered body rows and of the pinned row groups once their content has\n * rendered, and watches them for changes the table doesn't render (an image\n * loading, a web font, a framework component updating), which request an\n * update that reads them again. Pooled rows show other rows as the body\n * scrolls, so every update reads every rendered row: one that now shows a row\n * of the same height doesn't resize, and the observer wouldn't report it.\n */\nexport class RowMeasure {\n readonly #host: ReactiveControllerHost;\n readonly #options: RowMeasureOptions;\n #observer: ResizeObserver | undefined;\n readonly #observed = new Set<Element>();\n #pinned: PinnedHeights = { top: 0, bottom: 0 };\n /** The row each body row element showed when it was last read. */\n #read = new WeakMap<Element, number>();\n /** Read every row next time, not only those that show another row now. */\n #all = true;\n\n constructor(host: ReactiveControllerHost, options: RowMeasureOptions) {\n this.#host = host;\n this.#options = options;\n }\n\n /** The pinned groups' heights from the last `read()`. */\n get pinned(): PinnedHeights {\n return this.#pinned;\n }\n\n /**\n * The next `read()` reads every row: their content may have changed size\n * (new data or columns, a resize, content that grew).\n */\n invalidate(): void {\n this.#all = true;\n }\n\n /**\n * Reads the rows in `root` (the shadow root): the body rows' heights as\n * `[index, px]`, and whether the pinned groups changed height. Scrolling,\n * only the pooled rows that now show another row are read.\n */\n read(root: ParentNode): { rows: [number, number][]; pinnedChanged: boolean } {\n const observer = (this.#observer ??= new ResizeObserver(this.#onResize));\n for (const element of this.#observed) {\n if (!element.isConnected) {\n observer.unobserve(element);\n this.#observed.delete(element);\n }\n }\n const rows: [number, number][] = [];\n const due: [Element, number][] = [];\n for (const row of root.querySelectorAll('[part~=\"body\"] > .row')) {\n this.#observe(row);\n const index = this.#options.bodyIndex(row);\n if (index === undefined) {\n this.#read.delete(row);\n } else if (this.#all || this.#read.get(row) !== index) {\n due.push([row, index]);\n }\n }\n this.#all = false;\n // Body rows render at least their measured height (on the px grid): drop it\n // for one synchronous read, so rows can shrink too. Never painted.\n for (const [row] of due) row.setAttribute('data-measure-row', '');\n for (const [row, index] of due) {\n rows.push([index, row.getBoundingClientRect().height]);\n this.#read.set(row, index);\n }\n for (const [row] of due) row.removeAttribute('data-measure-row');\n const group = (side: 'top' | 'bottom') => {\n const element = root.querySelector(`.pinned[data-side=\"${side}\"]`);\n if (element) this.#observe(element);\n return element ? element.getBoundingClientRect().height : 0;\n };\n const pinned = { top: group('top'), bottom: group('bottom') };\n const pinnedChanged =\n Math.abs(pinned.top - this.#pinned.top) > 0.01 ||\n Math.abs(pinned.bottom - this.#pinned.bottom) > 0.01;\n if (pinnedChanged) this.#pinned = pinned;\n return { rows, pinnedChanged };\n }\n\n /** Stops watching (fixed row heights again, or the table left the page). */\n stop(): void {\n this.#observer?.disconnect();\n this.#observed.clear();\n this.#pinned = { top: 0, bottom: 0 };\n this.#all = true;\n }\n\n #observe(element: Element): void {\n if (this.#observed.has(element)) return;\n this.#observed.add(element);\n this.#observer?.observe(element);\n }\n\n /** Requests an update only for a height the table doesn't have yet. */\n readonly #onResize = (entries: ResizeObserverEntry[]) => {\n const { bodyIndex, sizeOf, minimum } = this.#options;\n for (const entry of entries) {\n const target = entry.target;\n if (!target.isConnected) continue;\n const px = entry.borderBoxSize[0]?.blockSize ?? target.getBoundingClientRect().height;\n let known: number | undefined;\n if (target.classList.contains('pinned')) {\n known = target.getAttribute('data-side') === 'top' ? this.#pinned.top : this.#pinned.bottom;\n } else {\n const index = bodyIndex(target);\n if (index === undefined) continue;\n known = sizeOf(index);\n // Rows render at least their known height: only growing shows here.\n if (px < minimum() || px <= known + 0.01) continue;\n }\n if (Math.abs(px - known) > 0.01) {\n this.#all = true;\n this.#host.requestUpdate();\n return;\n }\n }\n };\n}\n"],"mappings":"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"}
@@ -1,9 +1,18 @@
1
1
  import { RowPool } from "../core/src/virtual/row-pool.js";
2
+ import { RowSizes } from "../core/src/virtual/row-sizes.js";
2
3
  import { ScrollScale } from "../core/src/virtual/scroll-scale.js";
3
4
  import { maxVisibleRows, steadyWindow } from "../core/src/virtual/steady-window.js";
4
5
  import { VirtualizerController } from "@tanstack/lit-virtual";
5
6
  // src/controllers/virtual-controller.ts
6
7
  /**
8
+ * Engines place elements in a body taller than this (2^24 px) only to 2 px:
9
+ * positions are 32-bit floats. Measured rows are whole px below it and an even
10
+ * number of px above it, so every row starts on the grid and they stay flush.
11
+ */
12
+ var COARSE_BODY = 16e6;
13
+ /** How long scrolling has to be quiet before a held correction is written (see `#hold`). */
14
+ var HOLD_MS = 150;
15
+ /**
7
16
  * Row virtualization (TanStack Virtual) plus the row pool: which rows render,
8
17
  * where, and which pool slot's DOM each one uses. The virtualizer registers itself
9
18
  * with the host and requests updates on scroll.
@@ -16,6 +25,16 @@ import { VirtualizerController } from "@tanstack/lit-virtual";
16
25
  * scroll position (`start` minus the difference between the logical and the
17
26
  * physical offset). Scroll through this controller (`scrollBy`, `reveal`,
18
27
  * `scrollToIndex`): physical px no longer match rows once the body is scaled.
28
+ *
29
+ * Rows of different heights (`auto`): TanStack's own measuring recomputes every
30
+ * row below a measured one, too slow for a million rows, so in this mode the
31
+ * virtualizer sits idle (no rows) and the controller does its part: the window
32
+ * from `RowSizes` (heights by index, O(log n) offsets), a scroll listener that
33
+ * re-renders when the window moves, and anchoring. Heights follow row ids, so
34
+ * a sort or filter keeps them. A measured row above the top row would move
35
+ * the rows in view: unscaled, the rows render shifted by the difference until
36
+ * scrolling stops (`#hold`); scaled, the mapping pivots at the current offset.
37
+ * Neither writes the scroll position while the user scrolls.
19
38
  */
20
39
  var VirtualController = class {
21
40
  #virtual;
@@ -30,10 +49,31 @@ var VirtualController = class {
30
49
  rowHeight: 40,
31
50
  overscan: 6,
32
51
  rowIdAt: String,
33
- cap: Infinity
52
+ cap: () => Infinity
34
53
  };
35
54
  /** A logical offset to restore after the update, once the body has its new height. */
36
55
  #anchor;
56
+ #sizes = new RowSizes();
57
+ /** Measured heights by row id (as read), so they follow rows through a sort or filter. */
58
+ #heights = /* @__PURE__ */ new Map();
59
+ /** Rows are whole multiples of this many px (see `COARSE_BODY`). */
60
+ #quantum = 1;
61
+ /**
62
+ * Unscaled: the logical offset less the scroller's. Rows measured above the
63
+ * top row would move the rows in view; instead of writing the scroll position
64
+ * (which cancels a wheel or touch scroll in progress, in WebKit at once) the
65
+ * rows render shifted by this much, and it is written once scrolling has been
66
+ * quiet for `HOLD_MS`.
67
+ */
68
+ #hold = 0;
69
+ #holdTimer;
70
+ /** The `order` the sizes are indexed for. */
71
+ #order;
72
+ /** The visible rows last rendered, and the logical offset they were rendered for. */
73
+ #rendered;
74
+ /** The scroller this controller listens to (auto mode). */
75
+ #listening;
76
+ #resizeObserver;
37
77
  constructor(host, getScrollElement) {
38
78
  this.#host = host;
39
79
  this.#getScrollElement = getScrollElement;
@@ -85,7 +125,7 @@ var VirtualController = class {
85
125
  #top(element) {
86
126
  const top = element.scrollTop;
87
127
  const scale = this.#scale;
88
- return scale.scaled ? scale.toLogical(top, element.scrollHeight - element.clientHeight) : top;
128
+ return scale.scaled ? scale.toLogical(top, element.scrollHeight - element.clientHeight) : top + this.#hold;
89
129
  }
90
130
  /**
91
131
  * Scrolls to a logical offset. `round` rounds the physical one, so that a
@@ -94,6 +134,7 @@ var VirtualController = class {
94
134
  #write(element, logical, behavior, round) {
95
135
  const scale = this.#scale;
96
136
  if (!scale.scaled) {
137
+ this.#dropHold();
97
138
  element.scrollTo({
98
139
  top: logical,
99
140
  behavior
@@ -112,7 +153,22 @@ var VirtualController = class {
112
153
  configure(next) {
113
154
  const prev = this.#config;
114
155
  this.#config = next;
115
- if (next.count !== prev.count || next.overscan !== prev.overscan || next.rowHeight !== prev.rowHeight) {
156
+ if (next.auto) {
157
+ this.#configureAuto(next.auto, prev);
158
+ return;
159
+ }
160
+ if (prev.auto) {
161
+ this.#heights.clear();
162
+ this.#sizes.reset(0, next.rowHeight);
163
+ this.#order = void 0;
164
+ this.#rendered = void 0;
165
+ this.#dropHold();
166
+ this.#virtual.setOptions({
167
+ count: next.count,
168
+ overscan: next.overscan
169
+ });
170
+ this.#virtual.getVirtualizer().measure();
171
+ } else if (next.count !== prev.count || next.overscan !== prev.overscan || next.rowHeight !== prev.rowHeight) {
116
172
  this.#virtual.setOptions({
117
173
  count: next.count,
118
174
  overscan: next.overscan
@@ -121,21 +177,135 @@ var VirtualController = class {
121
177
  }
122
178
  const wasScaled = this.#scale.excess > 0;
123
179
  const offset = this.#virtual.getVirtualizer().scrollOffset;
124
- if (this.#scale.configure(next.count * next.rowHeight, next.cap)) {
180
+ const logical = next.count * next.rowHeight;
181
+ if (this.#scale.configure(logical, next.cap(logical))) {
125
182
  if ((wasScaled || this.#scale.scaled) && offset !== null && offset > 0) this.#anchor = offset;
126
183
  }
127
184
  }
185
+ #configureAuto(auto, prev) {
186
+ const next = this.#config;
187
+ if (!prev.auto) this.#virtual.setOptions({ count: 0 });
188
+ const sizes = this.#sizes;
189
+ if (!prev.auto || next.rowHeight !== prev.rowHeight) {
190
+ this.#heights.clear();
191
+ this.#order = void 0;
192
+ }
193
+ const reset = () => {
194
+ sizes.reset(next.count, this.#fit(0), this.#known(auto, next.count));
195
+ };
196
+ if (auto.order !== this.#order) {
197
+ this.#order = auto.order;
198
+ reset();
199
+ } else if (next.count !== sizes.count) sizes.resize(next.count);
200
+ const quantum = this.#quantumFor(sizes.total);
201
+ if (quantum !== this.#quantum) {
202
+ this.#quantum = quantum;
203
+ reset();
204
+ }
205
+ const element = this.#element;
206
+ const wasScaled = this.#scale.excess > 0;
207
+ const offset = element ? this.#top(element) : 0;
208
+ const logical = sizes.total;
209
+ if (this.#scale.configure(logical, next.cap(logical))) {
210
+ if ((wasScaled || this.#scale.scaled) && offset > 0) this.#anchor = offset;
211
+ }
212
+ }
213
+ /** The measured heights by index in the current order. Forgets rows long gone. */
214
+ *#known(auto, count) {
215
+ const heights = this.#heights;
216
+ const prune = heights.size > 2 * count + 1e3;
217
+ for (const [id, px] of heights) {
218
+ const index = auto.indexOf(id);
219
+ if (index !== void 0 && index < count) yield [index, this.#fit(px)];
220
+ else if (prune) heights.delete(id);
221
+ }
222
+ }
223
+ /** The px grid rows are on for a body `logical` px tall (see `COARSE_BODY`). */
224
+ #quantumFor(logical) {
225
+ return Math.min(logical, this.#config.cap(logical)) > COARSE_BODY ? 2 : 1;
226
+ }
227
+ /** A measured height as a row renders it: on the grid, at least the row height. */
228
+ #fit(px) {
229
+ const q = this.#quantum;
230
+ const up = (value) => Math.ceil(value / q - .01) * q;
231
+ return Math.max(up(px), up(this.#config.rowHeight));
232
+ }
128
233
  hostUpdated() {
234
+ this.#listen();
129
235
  const anchor = this.#anchor;
130
236
  if (anchor === void 0) return;
131
237
  this.#anchor = void 0;
132
238
  if (this.#element) this.#write(this.#element, anchor);
133
239
  this.#host.requestUpdate();
134
240
  }
241
+ hostDisconnected() {
242
+ this.#unlisten();
243
+ }
244
+ /**
245
+ * Auto mode: the virtualizer has no rows, so it reports neither scrolling nor
246
+ * a new viewport size. Listen to the scroller here.
247
+ */
248
+ #listen() {
249
+ const element = this.#config.auto ? this.#getScrollElement() : null;
250
+ if (element === (this.#listening ?? null)) return;
251
+ this.#unlisten();
252
+ if (!element) return;
253
+ this.#scroller();
254
+ this.#listening = element;
255
+ element.addEventListener("scroll", this.#onScroll, { passive: true });
256
+ this.#resizeObserver ??= new ResizeObserver(this.#onScroll);
257
+ this.#resizeObserver.observe(element);
258
+ }
259
+ #unlisten() {
260
+ this.#listening?.removeEventListener("scroll", this.#onScroll);
261
+ this.#resizeObserver?.disconnect();
262
+ this.#listening = void 0;
263
+ this.#dropHold();
264
+ }
265
+ #dropHold() {
266
+ clearTimeout(this.#holdTimer);
267
+ this.#holdTimer = void 0;
268
+ this.#hold = 0;
269
+ }
270
+ /** Writes the held correction: the rows stay where they are on screen. */
271
+ #flushHold = () => {
272
+ this.#holdTimer = void 0;
273
+ const element = this.#element;
274
+ if (!element || this.#hold === 0) return;
275
+ const logical = this.#top(element);
276
+ this.#hold = 0;
277
+ element.scrollTo({ top: logical });
278
+ this.#host.requestUpdate();
279
+ };
280
+ /** Re-renders when the visible rows have changed. */
281
+ #onScroll = () => {
282
+ if (this.#holdTimer !== void 0) {
283
+ clearTimeout(this.#holdTimer);
284
+ this.#holdTimer = setTimeout(this.#flushHold, HOLD_MS);
285
+ }
286
+ const visible = this.#visible();
287
+ const rendered = this.#rendered;
288
+ if (visible?.start !== rendered?.start || visible?.end !== rendered?.end) this.#host.requestUpdate();
289
+ };
290
+ /** Auto mode: the visible rows (without overscan) and the logical offset, or `undefined` before layout. */
291
+ #visible() {
292
+ const element = this.#element;
293
+ const sizes = this.#sizes;
294
+ const view = element?.clientHeight ?? 0;
295
+ if (!element || sizes.count === 0 || view <= 0) return void 0;
296
+ const top = this.#top(element);
297
+ const start = sizes.indexAt(top);
298
+ return {
299
+ start,
300
+ end: Math.max(start, sizes.indexAt(top + view - .5)),
301
+ top
302
+ };
303
+ }
135
304
  /**
136
305
  * Computes the rendered window and updates the pool. Call once per render.
137
306
  */
138
307
  computeWindow() {
308
+ if (this.#config.auto) return this.#autoWindow();
139
309
  const v = this.#virtual.getVirtualizer();
140
310
  const items = v.getVirtualItems();
141
311
  const scaled = this.#scale.scaled;
@@ -173,6 +343,119 @@ var VirtualController = class {
173
343
  } : null
174
344
  };
175
345
  }
346
+ /** `computeWindow()` for rows of different heights. */
347
+ #autoWindow() {
348
+ const { count, overscan, rowHeight, keep, rowIdAt } = this.#config;
349
+ const sizes = this.#sizes;
350
+ const scale = this.#scale;
351
+ const element = this.#element;
352
+ const visible = this.#visible();
353
+ this.#rendered = visible;
354
+ const shift = visible && element ? visible.top - element.scrollTop : 0;
355
+ const byId = /* @__PURE__ */ new Map();
356
+ if (visible && element) {
357
+ const indexes = steadyWindow({
358
+ startIndex: visible.start,
359
+ endIndex: visible.end,
360
+ overscan,
361
+ count
362
+ }, maxVisibleRows(element.clientHeight, rowHeight));
363
+ const first = indexes[0];
364
+ let start = first === void 0 ? 0 : sizes.startOf(first);
365
+ for (const index of indexes) {
366
+ const size = sizes.sizeOf(index);
367
+ byId.set(rowIdAt(index), {
368
+ index,
369
+ start: start - shift,
370
+ size
371
+ });
372
+ start += size;
373
+ }
374
+ }
375
+ this.#pool.pin(keep?.id);
376
+ if (keep && !byId.has(keep.id) && keep.index < count) {
377
+ const size = sizes.sizeOf(keep.index);
378
+ const start = sizes.startOf(keep.index) - shift;
379
+ byId.set(keep.id, {
380
+ index: keep.index,
381
+ start: scale.scaled ? Math.min(Math.max(start, -size), scale.size) : start,
382
+ size
383
+ });
384
+ }
385
+ const rows = [];
386
+ for (const { id, slot } of this.#pool.update(byId.keys())) {
387
+ const item = byId.get(id);
388
+ if (item) rows.push({
389
+ slot,
390
+ ...item
391
+ });
392
+ }
393
+ return {
394
+ rows,
395
+ totalSize: scale.scaled ? scale.size : sizes.total,
396
+ range: visible ? {
397
+ start: visible.start,
398
+ end: visible.end
399
+ } : null
400
+ };
401
+ }
402
+ /**
403
+ * Auto mode: records the heights of rendered body rows (`[index, px]`), read
404
+ * once their content has rendered. Heights under the row height count as it.
405
+ * Returns whether any changed; the controller then requests the update that
406
+ * places the rows. Rows above the top row keep the rows in view in place.
407
+ */
408
+ measureRows(measured) {
409
+ const { auto, rowIdAt, cap } = this.#config;
410
+ if (!auto) return false;
411
+ const sizes = this.#sizes;
412
+ const element = this.#element;
413
+ const top = element ? this.#top(element) : 0;
414
+ const anchor = sizes.indexAt(top);
415
+ const within = top - sizes.startOf(anchor);
416
+ let changed = false;
417
+ for (const [index, px] of measured) {
418
+ if (index < 0 || index >= sizes.count || !(px > 0)) continue;
419
+ this.#heights.set(rowIdAt(index), px);
420
+ if (sizes.set(index, this.#fit(px)) !== 0) changed = true;
421
+ }
422
+ if (!changed) return false;
423
+ const target = sizes.startOf(anchor) + within;
424
+ const scale = this.#scale;
425
+ const wasScaled = scale.excess > 0;
426
+ const logical = sizes.total;
427
+ if (scale.configure(logical, cap(logical)) && element) {
428
+ if (wasScaled && scale.scaled) scale.pivot(element.scrollTop, target, element.scrollHeight - element.clientHeight);
429
+ else if (wasScaled !== scale.scaled) this.#anchor = target;
430
+ else if (Math.abs(target - top) > .01) {
431
+ this.#hold += target - top;
432
+ clearTimeout(this.#holdTimer);
433
+ this.#holdTimer = setTimeout(this.#flushHold, HOLD_MS);
434
+ }
435
+ }
436
+ this.#host.requestUpdate();
437
+ return true;
438
+ }
439
+ /** The height of the row at `index` (auto mode: measured, or the row height). */
440
+ sizeOf(index) {
441
+ return this.#config.auto ? this.#sizes.sizeOf(index) : this.#config.rowHeight;
442
+ }
443
+ /** The logical offset of the row at `index` in the body. */
444
+ startOf(index) {
445
+ return this.#config.auto ? this.#sizes.startOf(index) : index * this.#config.rowHeight;
446
+ }
447
+ /**
448
+ * Auto mode: how many rows a page moves from row `from`, the rows that fit in
449
+ * `view` px below it (`down`) or above it. At least one.
450
+ */
451
+ pageRows(from, down, view) {
452
+ const sizes = this.#sizes;
453
+ const start = sizes.startOf(from);
454
+ if (down) return Math.max(1, sizes.indexAt(start + view + .01) - from);
455
+ let to = sizes.indexAt(start - view);
456
+ if (sizes.startOf(to) < start - view - .01) to += 1;
457
+ return Math.max(1, from - to);
458
+ }
176
459
  /** Number of pool slots created so far: bounded by the most rows rendered at once. */
177
460
  get poolSize() {
178
461
  return this.#pool.size;
@@ -181,8 +464,25 @@ var VirtualController = class {
181
464
  get scaled() {
182
465
  return this.#scale.scaled;
183
466
  }
467
+ /** The index of the row at the top of the body (0 before the first layout). */
468
+ get topRow() {
469
+ const element = this.#element;
470
+ const { count, rowHeight } = this.#config;
471
+ if (!element || count === 0 || rowHeight <= 0) return 0;
472
+ if (this.#config.auto) return this.#sizes.indexAt(this.#top(element) + .5);
473
+ const row = Math.floor((this.#top(element) + .5) / rowHeight);
474
+ return Math.min(Math.max(row, 0), count - 1);
475
+ }
184
476
  /** `'start'` puts the row at the top; `'auto'` scrolls only as far as needed to show it. */
185
477
  scrollToIndex(index, align = "start") {
478
+ if (this.#config.auto) {
479
+ const element = this.#element;
480
+ if (!element) return;
481
+ if (align === "auto") this.reveal(index, 0, 0);
482
+ else this.#write(element, this.#sizes.startOf(Math.min(Math.max(index, 0), this.#sizes.count)));
483
+ this.#host.requestUpdate();
484
+ return;
485
+ }
186
486
  this.#virtual.getVirtualizer().scrollToIndex(index, { align });
187
487
  }
188
488
  /** Scrolls by `delta` px of rows (logical px). */
@@ -205,21 +505,22 @@ var VirtualController = class {
205
505
  reveal(index, before, after) {
206
506
  const element = this.#element;
207
507
  if (!element) return false;
208
- const { rowHeight } = this.#config;
508
+ const auto = this.#config.auto !== void 0;
209
509
  const offset = this.#top(element);
210
510
  const view = element.clientHeight - before - after;
211
- const start = index * rowHeight;
511
+ const start = this.startOf(index);
512
+ const size = this.sizeOf(index);
212
513
  let target;
213
514
  let round;
214
- if (start < offset) {
515
+ if (start < offset || auto && size > view) {
215
516
  target = start;
216
517
  round = Math.floor;
217
- } else if (start + rowHeight > offset + view) {
218
- target = start + rowHeight - view;
518
+ } else if (start + size > offset + view) {
519
+ target = start + size - view;
219
520
  round = Math.ceil;
220
521
  } else target = offset;
221
522
  if (target !== offset) this.#write(element, target, void 0, round);
222
- return this.#virtual.getVirtualizer().scrollOffset !== this.#top(element);
523
+ return (auto ? this.#rendered?.top : this.#virtual.getVirtualizer().scrollOffset) !== this.#top(element);
223
524
  }
224
525
  };
225
526
  export { VirtualController };