argus-table 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/argus-column-group.js +66 -0
- package/dist/argus-column-group.js.map +1 -0
- package/dist/argus-column.js +28 -26
- package/dist/argus-column.js.map +1 -1
- package/dist/argus-table.js +1497 -285
- package/dist/argus-table.js.map +1 -1
- package/dist/attributes.js +21 -0
- package/dist/attributes.js.map +1 -0
- package/dist/cdn/argus-table.js +120 -61
- package/dist/cdn/argus-table.js.map +1 -1
- package/dist/column-registry.js +23 -0
- package/dist/column-registry.js.map +1 -0
- package/dist/controllers/auto-width.js +16 -4
- package/dist/controllers/auto-width.js.map +1 -1
- package/dist/controllers/column-children.js +32 -9
- package/dist/controllers/column-children.js.map +1 -1
- package/dist/controllers/column-window.js +135 -0
- package/dist/controllers/column-window.js.map +1 -0
- package/dist/controllers/data-controller.js +9 -4
- package/dist/controllers/data-controller.js.map +1 -1
- package/dist/controllers/plugins.js +121 -0
- package/dist/controllers/plugins.js.map +1 -0
- package/dist/controllers/reorder-drag.js +19 -17
- package/dist/controllers/reorder-drag.js.map +1 -1
- package/dist/controllers/row-measure.js +122 -0
- package/dist/controllers/row-measure.js.map +1 -0
- package/dist/controllers/virtual-controller.js +315 -21
- package/dist/controllers/virtual-controller.js.map +1 -1
- package/dist/core/src/columns/column-groups.js +129 -0
- package/dist/core/src/columns/column-groups.js.map +1 -0
- package/dist/core/src/columns/column-order.js +125 -27
- package/dist/core/src/columns/column-order.js.map +1 -1
- package/dist/core/src/columns/resolve-columns.js +18 -5
- package/dist/core/src/columns/resolve-columns.js.map +1 -1
- package/dist/core/src/columns/spread-width.js +43 -0
- package/dist/core/src/columns/spread-width.js.map +1 -0
- package/dist/core/src/data/page-cache.js +4 -0
- package/dist/core/src/data/page-cache.js.map +1 -1
- package/dist/core/src/filters/filter.js +183 -0
- package/dist/core/src/filters/filter.js.map +1 -0
- package/dist/core/src/grid/row-layout.js +19 -11
- package/dist/core/src/grid/row-layout.js.map +1 -1
- package/dist/core/src/state/selection.js +71 -0
- package/dist/core/src/state/selection.js.map +1 -0
- package/dist/core/src/table/create-table.js +276 -66
- package/dist/core/src/table/create-table.js.map +1 -1
- package/dist/core/src/table/filtered-row-model.js +48 -0
- package/dist/core/src/table/filtered-row-model.js.map +1 -0
- package/dist/core/src/table/sorted-row-model.js +13 -2
- package/dist/core/src/table/sorted-row-model.js.map +1 -1
- package/dist/core/src/virtual/column-window.js +72 -0
- package/dist/core/src/virtual/column-window.js.map +1 -0
- package/dist/core/src/virtual/row-pool.js +2 -1
- package/dist/core/src/virtual/row-pool.js.map +1 -1
- package/dist/core/src/virtual/row-sizes.js +152 -0
- package/dist/core/src/virtual/row-sizes.js.map +1 -0
- package/dist/core/src/virtual/scroll-scale.js +41 -3
- package/dist/core/src/virtual/scroll-scale.js.map +1 -1
- package/dist/custom-elements.json +1683 -211
- package/dist/events.js +16 -8
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +808 -127
- package/dist/index.js +4 -2
- package/dist/interactive.js +22 -0
- package/dist/interactive.js.map +1 -0
- package/dist/max-height.js +22 -8
- package/dist/max-height.js.map +1 -1
- package/dist/render/body.js +48 -8
- package/dist/render/body.js.map +1 -1
- package/dist/render/cells.js +22 -11
- package/dist/render/cells.js.map +1 -1
- package/dist/render/content-directive.js +115 -0
- package/dist/render/content-directive.js.map +1 -0
- package/dist/render/content.js +33 -10
- package/dist/render/content.js.map +1 -1
- package/dist/render/header.js +104 -22
- package/dist/render/header.js.map +1 -1
- package/dist/render/pinned-rows.js +1 -1
- package/dist/render/pinned-rows.js.map +1 -1
- package/dist/styles/base.js +1 -1
- package/dist/styles/base.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reorder-drag.js","names":[],"sources":["../../src/controllers/reorder-drag.ts"],"sourcesContent":["/** Where a dragged column would land: its new position and the header that shows the drop line. */\nexport interface DropTarget {\n /** Position in the whole column order after the move, 0-based. */\n to: number;\n /** The header that draws the drop line. */\n columnId: string;\n /** The edge of that header the line is on, in logical terms. */\n edge: 'start' | 'end';\n}\n\n/** A header of the dragged column's group, in visual order. */\nexport interface DropSlot {\n id: string;\n /** Its position in the whole column order, 0-based. */\n index: number;\n rect: { left: number; right: number };\n}\n\n/**\n * The drop target for a pointer at `x`: the gap between headers nearest to it,\n * decided by each header's midpoint. `undefined` where the column would stay\n * put. `direction` is 1 in LTR and -1 in RTL, where the group runs right to left.\n */\nexport function dropTarget(\n slots: readonly DropSlot[],\n draggedId: string,\n x: number,\n direction: 1 | -1,\n): DropTarget | undefined {\n const from = slots.findIndex((s) => s.id === draggedId);\n const first = slots[0];\n const last = slots.at(-1);\n if (from < 0 || !first || !last) return undefined;\n // The gap before the first header whose middle is past the pointer, in reading order.\n let gap = slots.findIndex((s) => (x - (s.rect.left + s.rect.right) / 2) * direction < 0);\n if (gap < 0) gap = slots.length;\n if (gap === from || gap === from + 1) return undefined;\n const to = first.index + (gap > from ? gap - 1 : gap);\n const at = slots[gap];\n return at ? { to, columnId: at.id, edge: 'start' } : { to, columnId: last.id, edge: 'end' };\n}\n\nexport interface ReorderDragOptions {\n /** The scroll container: it scrolls while the pointer is near its edges. */\n scroller: HTMLElement;\n /** The drop target for a pointer at `x` (client coordinates). */\n targetAt: (x: number) => DropTarget | undefined;\n /**\n * Where the dragged column's group is visible, in client x, if it can scroll\n * (the scrolling columns). Near these edges the scroller scrolls with the drag.\n */\n scrollArea: () => { left: number; right: number } | undefined;\n onStart: () => void;\n /** The target changed (`undefined`: the column would stay put). */\n onTarget: (target: DropTarget | undefined) => void;\n /** The pointer was released over a target, or `undefined` when it would stay put. */\n onDrop: (target: DropTarget | undefined) => void;\n /** Escape, a cancelled pointer, or a release without a drag. */\n onCancel: () => void;\n}\n\n/** Movement before a press becomes a drag, so a click still sorts. */\nconst THRESHOLD = 4;\n/** Width of the band at the scroll area's edges that scrolls, and the top speed there. */\nconst EDGE = 40;\nconst MAX_SPEED = 16;\n\nconst INTERACTIVE = 'button,input,select,textarea,a[href],label,[contenteditable]';\n\n/**\n * Whether a press on a header may start a reorder drag: the primary button of a\n * mouse or pen (a touch pans the table instead), not on a resize handle, and not\n * in interactive content slotted into the header.\n */\nexport function canStartReorder(down: PointerEvent, cell: HTMLElement): boolean {\n if (down.button !== 0 || down.pointerType === 'touch') return false;\n for (const target of down.composedPath()) {\n if (target === cell) return true;\n if (!(target instanceof Element)) continue;\n if (target instanceof HTMLElement && target.dataset.resizeCol !== undefined) return false;\n if (target.matches(INTERACTIVE)) return false;\n }\n return false;\n}\n\n/**\n * One pointer drag of a column header. It becomes a drag after a few pixels of\n * movement; until then the press is a click. While dragging, the pointer is\n * captured by the header, the target follows the pointer once per frame, and the\n * scroll area scrolls near its edges. Escape cancels. The click that ends a drag\n * is swallowed, so it neither sorts nor counts as a cell click.\n */\nexport function startReorderDrag(\n cell: HTMLElement,\n down: PointerEvent,\n options: ReorderDragOptions,\n): void {\n const view = cell.ownerDocument.defaultView ?? window;\n const startX = down.clientX;\n const startY = down.clientY;\n let x = startX;\n let dragging = false;\n let frame = 0;\n let target: DropTarget | undefined;\n\n const same = (a: DropTarget | undefined, b: DropTarget | undefined) =>\n a?.to === b?.to && a?.columnId === b?.columnId && a?.edge === b?.edge;\n\n const tick = () => {\n frame = 0;\n if (!dragging) return;\n const area = options.scrollArea();\n if (area) {\n const speed =\n x < area.left + EDGE\n ? -Math.min(1, (area.left + EDGE - x) / EDGE)\n : x > area.right - EDGE\n ? Math.min(1, (x - (area.right - EDGE)) / EDGE)\n : 0;\n // A visual delta: scrollLeft runs the same way in LTR and RTL.\n if (speed !== 0) options.scroller.scrollLeft += Math.round(speed * MAX_SPEED);\n // Targets past the visible part would be hidden under pinned columns.\n x = Math.min(Math.max(x, area.left), area.right);\n }\n const next = options.targetAt(x);\n if (!same(next, target)) {\n target = next;\n options.onTarget(target);\n }\n // Keep running while dragging: the view may still be scrolling.\n frame = view.requestAnimationFrame(tick);\n };\n\n const listeners = new AbortController();\n const cleanup = (event?: PointerEvent) => {\n if (frame) view.cancelAnimationFrame(frame);\n frame = 0;\n listeners.abort();\n if (event && cell.hasPointerCapture(event.pointerId)) {\n cell.releasePointerCapture(event.pointerId);\n }\n };\n\n const move = (event: PointerEvent) => {\n if (event.pointerId !== down.pointerId) return;\n x = event.clientX;\n if (!dragging) {\n if (Math.hypot(x - startX, event.clientY - startY) < THRESHOLD) return;\n dragging = true;\n try {\n cell.setPointerCapture(event.pointerId);\n } catch {\n // Synthetic pointers can't be captured; the window listeners still see the moves.\n }\n options.onStart();\n }\n // No text selection while dragging.\n event.preventDefault();\n frame ||= view.requestAnimationFrame(tick);\n };\n\n const up = (event: PointerEvent) => {\n if (event.pointerId !== down.pointerId) return;\n cleanup(event);\n if (!dragging) return;\n x = event.clientX;\n const area = options.scrollArea();\n if (area) x = Math.min(Math.max(x, area.left), area.right);\n swallowClick(view);\n options.onDrop(options.targetAt(x));\n };\n\n const cancel = (event: PointerEvent) => {\n if (event.pointerId !== down.pointerId) return;\n cleanup(event);\n if (dragging) options.onCancel();\n };\n\n const key = (event: KeyboardEvent) => {\n if (!dragging || event.key !== 'Escape') return;\n event.preventDefault();\n event.stopPropagation();\n cleanup();\n // The pointer is still down: its release must not count as a click.\n swallowClick(view, false);\n options.onCancel();\n };\n\n const listen = { capture: true, signal: listeners.signal };\n view.addEventListener('pointermove', move, listen);\n view.addEventListener('pointerup', up, listen);\n view.addEventListener('pointercancel', cancel, listen);\n view.addEventListener('keydown', key, listen);\n}\n\n/**\n * Stops the click that follows the release of a drag, if one comes. It comes in\n * the same task as the release, so the guard goes once that task is over. After\n * Escape the pointer is still down (`released: false`): the guard waits for its release.\n */\nfunction swallowClick(view: Window, released = true): void {\n const stop = (event: MouseEvent) => {\n event.preventDefault();\n event.stopPropagation();\n };\n const remove = () => {\n setTimeout(() => {\n view.removeEventListener('click', stop, { capture: true });\n });\n };\n view.addEventListener('click', stop, { capture: true, once: true });\n if (released) remove();\n else view.addEventListener('pointerup', remove, { capture: true, once: true });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"reorder-drag.js","names":[],"sources":["../../src/controllers/reorder-drag.ts"],"sourcesContent":["import { INTERACTIVE } from '../interactive.js';\n\n/** Where a dragged column or group would land: its new position and the header that shows the drop line. */\nexport interface DropTarget {\n /** Position among its siblings after the move, 0-based. */\n to: number;\n /** The slot (a column or group header) that draws the drop line. */\n id: string;\n /** The edge of that header the line is on, in logical terms. */\n edge: 'start' | 'end';\n}\n\n/** A header of what the dragged column or group moves among, in visual order. */\nexport interface DropSlot {\n id: string;\n /** Its position among them, 0-based. */\n index: number;\n rect: { left: number; right: number };\n}\n\n/**\n * The drop target for a pointer at `x`: the gap between headers nearest to it,\n * decided by each header's midpoint. `undefined` where the column would stay\n * put. `direction` is 1 in LTR and -1 in RTL, where the group runs right to left.\n * The slots needn't be next to each other in the order: with\n * `virtualizeColumns`, headers out of view aren't rendered.\n */\nexport function dropTarget(\n slots: readonly DropSlot[],\n draggedId: string,\n x: number,\n direction: 1 | -1,\n): DropTarget | undefined {\n const dragged = slots.find((s) => s.id === draggedId);\n const last = slots.at(-1);\n if (!dragged || !last) return undefined;\n // The gap before the first header whose middle is past the pointer, in reading order.\n const at = slots.find((s) => (x - (s.rect.left + s.rect.right) / 2) * direction < 0);\n if (!at) {\n // Past the last header: after it.\n const to = last.index >= dragged.index ? last.index : last.index + 1;\n return to === dragged.index ? undefined : { to, id: last.id, edge: 'end' };\n }\n // Taking the column out shifts the ones after it one place back.\n const to = at.index > dragged.index ? at.index - 1 : at.index;\n return to === dragged.index ? undefined : { to, id: at.id, edge: 'start' };\n}\n\nexport interface ReorderDragOptions {\n /** The scroll container: it scrolls while the pointer is near its edges. */\n scroller: HTMLElement;\n /** The drop target for a pointer at `x` (client coordinates). */\n targetAt: (x: number) => DropTarget | undefined;\n /**\n * Where the dragged column's group is visible, in client x, if it can scroll\n * (the scrolling columns). Near these edges the scroller scrolls with the drag.\n */\n scrollArea: () => { left: number; right: number } | undefined;\n onStart: () => void;\n /** The target changed (`undefined`: the column would stay put). */\n onTarget: (target: DropTarget | undefined) => void;\n /** The pointer was released over a target, or `undefined` when it would stay put. */\n onDrop: (target: DropTarget | undefined) => void;\n /** Escape, a cancelled pointer, or a release without a drag. */\n onCancel: () => void;\n}\n\n/** Movement before a press becomes a drag, so a click still sorts. */\nconst THRESHOLD = 4;\n/** Width of the band at the scroll area's edges that scrolls, and the top speed there. */\nconst EDGE = 40;\nconst MAX_SPEED = 16;\n\n/**\n * Whether a press on a header may start a reorder drag: the primary button of a\n * mouse or pen (a touch pans the table instead), not on a resize handle, and not\n * in interactive content slotted into the header.\n */\nexport function canStartReorder(down: PointerEvent, cell: HTMLElement): boolean {\n if (down.button !== 0 || down.pointerType === 'touch') return false;\n for (const target of down.composedPath()) {\n if (target === cell) return true;\n if (!(target instanceof Element)) continue;\n if (target instanceof HTMLElement && target.dataset.resizeCol !== undefined) return false;\n if (target.matches(INTERACTIVE)) return false;\n }\n return false;\n}\n\n/**\n * One pointer drag of a column header. It becomes a drag after a few pixels of\n * movement; until then the press is a click. While dragging, the pointer is\n * captured by the header, the target follows the pointer once per frame, and the\n * scroll area scrolls near its edges. Escape cancels. The click that ends a drag\n * is swallowed, so it neither sorts nor counts as a cell click.\n */\nexport function startReorderDrag(\n cell: HTMLElement,\n down: PointerEvent,\n options: ReorderDragOptions,\n): void {\n const view = cell.ownerDocument.defaultView ?? window;\n const startX = down.clientX;\n const startY = down.clientY;\n let x = startX;\n let dragging = false;\n let frame = 0;\n let target: DropTarget | undefined;\n\n const same = (a: DropTarget | undefined, b: DropTarget | undefined) =>\n a?.to === b?.to && a?.id === b?.id && a?.edge === b?.edge;\n\n const tick = () => {\n frame = 0;\n if (!dragging) return;\n const area = options.scrollArea();\n if (area) {\n const speed =\n x < area.left + EDGE\n ? -Math.min(1, (area.left + EDGE - x) / EDGE)\n : x > area.right - EDGE\n ? Math.min(1, (x - (area.right - EDGE)) / EDGE)\n : 0;\n // A visual delta: scrollLeft runs the same way in LTR and RTL.\n if (speed !== 0) options.scroller.scrollLeft += Math.round(speed * MAX_SPEED);\n // Targets past the visible part would be hidden under pinned columns.\n x = Math.min(Math.max(x, area.left), area.right);\n }\n const next = options.targetAt(x);\n if (!same(next, target)) {\n target = next;\n options.onTarget(target);\n }\n // Keep running while dragging: the view may still be scrolling.\n frame = view.requestAnimationFrame(tick);\n };\n\n const listeners = new AbortController();\n const cleanup = (event?: PointerEvent) => {\n if (frame) view.cancelAnimationFrame(frame);\n frame = 0;\n listeners.abort();\n if (event && cell.hasPointerCapture(event.pointerId)) {\n cell.releasePointerCapture(event.pointerId);\n }\n };\n\n const move = (event: PointerEvent) => {\n if (event.pointerId !== down.pointerId) return;\n x = event.clientX;\n if (!dragging) {\n if (Math.hypot(x - startX, event.clientY - startY) < THRESHOLD) return;\n dragging = true;\n try {\n cell.setPointerCapture(event.pointerId);\n } catch {\n // Synthetic pointers can't be captured; the window listeners still see the moves.\n }\n options.onStart();\n }\n // No text selection while dragging.\n event.preventDefault();\n frame ||= view.requestAnimationFrame(tick);\n };\n\n const up = (event: PointerEvent) => {\n if (event.pointerId !== down.pointerId) return;\n cleanup(event);\n if (!dragging) return;\n x = event.clientX;\n const area = options.scrollArea();\n if (area) x = Math.min(Math.max(x, area.left), area.right);\n swallowClick(view);\n options.onDrop(options.targetAt(x));\n };\n\n const cancel = (event: PointerEvent) => {\n if (event.pointerId !== down.pointerId) return;\n cleanup(event);\n if (dragging) options.onCancel();\n };\n\n const key = (event: KeyboardEvent) => {\n if (!dragging || event.key !== 'Escape') return;\n event.preventDefault();\n event.stopPropagation();\n cleanup();\n // The pointer is still down: its release must not count as a click.\n swallowClick(view, false);\n options.onCancel();\n };\n\n const listen = { capture: true, signal: listeners.signal };\n view.addEventListener('pointermove', move, listen);\n view.addEventListener('pointerup', up, listen);\n view.addEventListener('pointercancel', cancel, listen);\n view.addEventListener('keydown', key, listen);\n}\n\n/**\n * Stops the click that follows the release of a drag, if one comes. It comes in\n * the same task as the release, so the guard goes once that task is over. After\n * Escape the pointer is still down (`released: false`): the guard waits for its release.\n */\nfunction swallowClick(view: Window, released = true): void {\n const stop = (event: MouseEvent) => {\n event.preventDefault();\n event.stopPropagation();\n };\n const remove = () => {\n setTimeout(() => {\n view.removeEventListener('click', stop, { capture: true });\n });\n };\n view.addEventListener('click', stop, { capture: true, once: true });\n if (released) remove();\n else view.addEventListener('pointerup', remove, { capture: true, once: true });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,122 @@
|
|
|
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 each update reads every rendered row that shows another row than
|
|
9
|
+
* at its last read (all of them after `invalidate()`): one that now shows a row
|
|
10
|
+
* of the same height doesn't resize, and the observer wouldn't report it.
|
|
11
|
+
*/
|
|
12
|
+
var RowMeasure = class {
|
|
13
|
+
#host;
|
|
14
|
+
#options;
|
|
15
|
+
#observer;
|
|
16
|
+
#observed = /* @__PURE__ */ new Set();
|
|
17
|
+
#pinned = {
|
|
18
|
+
top: 0,
|
|
19
|
+
bottom: 0
|
|
20
|
+
};
|
|
21
|
+
/** The row each body row element showed when it was last read. */
|
|
22
|
+
#read = /* @__PURE__ */ new WeakMap();
|
|
23
|
+
/** Read every row next time, not only those that show another row now. */
|
|
24
|
+
#all = true;
|
|
25
|
+
constructor(host, options) {
|
|
26
|
+
this.#host = host;
|
|
27
|
+
this.#options = options;
|
|
28
|
+
}
|
|
29
|
+
/** The pinned groups' heights from the last `read()`. */
|
|
30
|
+
get pinned() {
|
|
31
|
+
return this.#pinned;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The next `read()` reads every row: their content may have changed size
|
|
35
|
+
* (new data or columns, a resize, content that grew).
|
|
36
|
+
*/
|
|
37
|
+
invalidate() {
|
|
38
|
+
this.#all = true;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Reads the rows in `root` (the shadow root): the body rows' heights as
|
|
42
|
+
* `[index, px]`, and whether the pinned groups changed height. Scrolling,
|
|
43
|
+
* only the pooled rows that now show another row are read.
|
|
44
|
+
*/
|
|
45
|
+
read(root) {
|
|
46
|
+
const observer = this.#observer ??= new ResizeObserver(this.#onResize);
|
|
47
|
+
for (const element of this.#observed) if (!element.isConnected) {
|
|
48
|
+
observer.unobserve(element);
|
|
49
|
+
this.#observed.delete(element);
|
|
50
|
+
}
|
|
51
|
+
const rows = [];
|
|
52
|
+
const due = [];
|
|
53
|
+
for (const row of root.querySelectorAll("[part~=\"body\"] > .row")) {
|
|
54
|
+
this.#observe(row);
|
|
55
|
+
const index = this.#options.bodyIndex(row);
|
|
56
|
+
if (index === void 0) this.#read.delete(row);
|
|
57
|
+
else if (this.#all || this.#read.get(row) !== index) due.push([row, index]);
|
|
58
|
+
}
|
|
59
|
+
this.#all = false;
|
|
60
|
+
for (const [row] of due) row.setAttribute("data-measure-row", "");
|
|
61
|
+
for (const [row, index] of due) {
|
|
62
|
+
rows.push([index, row.getBoundingClientRect().height]);
|
|
63
|
+
this.#read.set(row, index);
|
|
64
|
+
}
|
|
65
|
+
for (const [row] of due) row.removeAttribute("data-measure-row");
|
|
66
|
+
const group = (side) => {
|
|
67
|
+
const element = root.querySelector(`.pinned[data-side="${side}"]`);
|
|
68
|
+
if (element) this.#observe(element);
|
|
69
|
+
return element ? element.getBoundingClientRect().height : 0;
|
|
70
|
+
};
|
|
71
|
+
const pinned = {
|
|
72
|
+
top: group("top"),
|
|
73
|
+
bottom: group("bottom")
|
|
74
|
+
};
|
|
75
|
+
const pinnedChanged = Math.abs(pinned.top - this.#pinned.top) > .01 || Math.abs(pinned.bottom - this.#pinned.bottom) > .01;
|
|
76
|
+
if (pinnedChanged) this.#pinned = pinned;
|
|
77
|
+
return {
|
|
78
|
+
rows,
|
|
79
|
+
pinnedChanged
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** Stops watching (fixed row heights again, or the table left the page). */
|
|
83
|
+
stop() {
|
|
84
|
+
this.#observer?.disconnect();
|
|
85
|
+
this.#observed.clear();
|
|
86
|
+
this.#pinned = {
|
|
87
|
+
top: 0,
|
|
88
|
+
bottom: 0
|
|
89
|
+
};
|
|
90
|
+
this.#all = true;
|
|
91
|
+
}
|
|
92
|
+
#observe(element) {
|
|
93
|
+
if (this.#observed.has(element)) return;
|
|
94
|
+
this.#observed.add(element);
|
|
95
|
+
this.#observer?.observe(element);
|
|
96
|
+
}
|
|
97
|
+
/** Requests an update only for a height the table doesn't have yet. */
|
|
98
|
+
#onResize = (entries) => {
|
|
99
|
+
const { bodyIndex, sizeOf, minimum } = this.#options;
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
const target = entry.target;
|
|
102
|
+
if (!target.isConnected) continue;
|
|
103
|
+
const px = entry.borderBoxSize[0]?.blockSize ?? target.getBoundingClientRect().height;
|
|
104
|
+
let known;
|
|
105
|
+
if (target.classList.contains("pinned")) known = target.getAttribute("data-side") === "top" ? this.#pinned.top : this.#pinned.bottom;
|
|
106
|
+
else {
|
|
107
|
+
const index = bodyIndex(target);
|
|
108
|
+
if (index === void 0) continue;
|
|
109
|
+
known = sizeOf(index);
|
|
110
|
+
if (px < minimum() || px <= known + .01) continue;
|
|
111
|
+
}
|
|
112
|
+
if (Math.abs(px - known) > .01) {
|
|
113
|
+
this.#all = true;
|
|
114
|
+
this.#host.requestUpdate();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
export { RowMeasure };
|
|
121
|
+
|
|
122
|
+
//# 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 each update reads every rendered row that shows another row than\n * at its last read (all of them after `invalidate()`): 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":";;;;;;;;;;;AA2BA,IAAa,aAAb,MAAwB;CACtB;CACA;CACA;CACA,4BAAqB,IAAI,IAAa;CACtC,UAAyB;EAAE,KAAK;EAAG,QAAQ;CAAE;;CAE7C,wBAAQ,IAAI,QAAyB;;CAErC,OAAO;CAEP,YAAY,MAA8B,SAA4B;EACpE,KAAK,QAAQ;EACb,KAAK,WAAW;CAClB;;CAGA,IAAI,SAAwB;EAC1B,OAAO,KAAK;CACd;;;;;CAMA,aAAmB;EACjB,KAAK,OAAO;CACd;;;;;;CAOA,KAAK,MAAwE;EAC3E,MAAM,WAAY,KAAK,cAAc,IAAI,eAAe,KAAK,SAAS;EACtE,KAAK,MAAM,WAAW,KAAK,WACzB,IAAI,CAAC,QAAQ,aAAa;GACxB,SAAS,UAAU,OAAO;GAC1B,KAAK,UAAU,OAAO,OAAO;EAC/B;EAEF,MAAM,OAA2B,CAAC;EAClC,MAAM,MAA2B,CAAC;EAClC,KAAK,MAAM,OAAO,KAAK,iBAAiB,yBAAuB,GAAG;GAChE,KAAK,SAAS,GAAG;GACjB,MAAM,QAAQ,KAAK,SAAS,UAAU,GAAG;GACzC,IAAI,UAAU,KAAA,GACZ,KAAK,MAAM,OAAO,GAAG;QAChB,IAAI,KAAK,QAAQ,KAAK,MAAM,IAAI,GAAG,MAAM,OAC9C,IAAI,KAAK,CAAC,KAAK,KAAK,CAAC;EAEzB;EACA,KAAK,OAAO;EAGZ,KAAK,MAAM,CAAC,QAAQ,KAAK,IAAI,aAAa,oBAAoB,EAAE;EAChE,KAAK,MAAM,CAAC,KAAK,UAAU,KAAK;GAC9B,KAAK,KAAK,CAAC,OAAO,IAAI,sBAAsB,CAAC,CAAC,MAAM,CAAC;GACrD,KAAK,MAAM,IAAI,KAAK,KAAK;EAC3B;EACA,KAAK,MAAM,CAAC,QAAQ,KAAK,IAAI,gBAAgB,kBAAkB;EAC/D,MAAM,SAAS,SAA2B;GACxC,MAAM,UAAU,KAAK,cAAc,sBAAsB,KAAK,GAAG;GACjE,IAAI,SAAS,KAAK,SAAS,OAAO;GAClC,OAAO,UAAU,QAAQ,sBAAsB,CAAC,CAAC,SAAS;EAC5D;EACA,MAAM,SAAS;GAAE,KAAK,MAAM,KAAK;GAAG,QAAQ,MAAM,QAAQ;EAAE;EAC5D,MAAM,gBACJ,KAAK,IAAI,OAAO,MAAM,KAAK,QAAQ,GAAG,IAAI,OAC1C,KAAK,IAAI,OAAO,SAAS,KAAK,QAAQ,MAAM,IAAI;EAClD,IAAI,eAAe,KAAK,UAAU;EAClC,OAAO;GAAE;GAAM;EAAc;CAC/B;;CAGA,OAAa;EACX,KAAK,WAAW,WAAW;EAC3B,KAAK,UAAU,MAAM;EACrB,KAAK,UAAU;GAAE,KAAK;GAAG,QAAQ;EAAE;EACnC,KAAK,OAAO;CACd;CAEA,SAAS,SAAwB;EAC/B,IAAI,KAAK,UAAU,IAAI,OAAO,GAAG;EACjC,KAAK,UAAU,IAAI,OAAO;EAC1B,KAAK,WAAW,QAAQ,OAAO;CACjC;;CAGA,aAAsB,YAAmC;EACvD,MAAM,EAAE,WAAW,QAAQ,YAAY,KAAK;EAC5C,KAAK,MAAM,SAAS,SAAS;GAC3B,MAAM,SAAS,MAAM;GACrB,IAAI,CAAC,OAAO,aAAa;GACzB,MAAM,KAAK,MAAM,cAAc,EAAE,EAAE,aAAa,OAAO,sBAAsB,CAAC,CAAC;GAC/E,IAAI;GACJ,IAAI,OAAO,UAAU,SAAS,QAAQ,GACpC,QAAQ,OAAO,aAAa,WAAW,MAAM,QAAQ,KAAK,QAAQ,MAAM,KAAK,QAAQ;QAChF;IACL,MAAM,QAAQ,UAAU,MAAM;IAC9B,IAAI,UAAU,KAAA,GAAW;IACzB,QAAQ,OAAO,KAAK;IAEpB,IAAI,KAAK,QAAQ,KAAK,MAAM,QAAQ,KAAM;GAC5C;GACA,IAAI,KAAK,IAAI,KAAK,KAAK,IAAI,KAAM;IAC/B,KAAK,OAAO;IACZ,KAAK,MAAM,cAAc;IACzB;GACF;EACF;CACF;AACF"}
|
|
@@ -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,30 +153,159 @@ var VirtualController = class {
|
|
|
112
153
|
configure(next) {
|
|
113
154
|
const prev = this.#config;
|
|
114
155
|
this.#config = next;
|
|
115
|
-
if (next.
|
|
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
|
|
119
175
|
});
|
|
120
176
|
if (next.rowHeight !== prev.rowHeight) this.#virtual.getVirtualizer().measure();
|
|
121
177
|
}
|
|
178
|
+
this.#rescale(next.count * next.rowHeight, this.#virtual.getVirtualizer().scrollOffset);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* A new logical body height. A new mapping would move the rows under a scaled
|
|
182
|
+
* scroll position (a data source growing, a zoom change): keep the logical
|
|
183
|
+
* offset (read before the new mapping) once the body has its new height.
|
|
184
|
+
*/
|
|
185
|
+
#rescale(logical, offset) {
|
|
122
186
|
const wasScaled = this.#scale.excess > 0;
|
|
123
|
-
|
|
124
|
-
if (this.#scale.
|
|
125
|
-
|
|
187
|
+
if (!this.#scale.configure(logical, this.#config.cap(logical))) return;
|
|
188
|
+
if ((wasScaled || this.#scale.scaled) && offset !== null && offset > 0) this.#anchor = offset;
|
|
189
|
+
}
|
|
190
|
+
#configureAuto(auto, prev) {
|
|
191
|
+
const next = this.#config;
|
|
192
|
+
if (!prev.auto) this.#virtual.setOptions({ count: 0 });
|
|
193
|
+
const sizes = this.#sizes;
|
|
194
|
+
if (!prev.auto || next.rowHeight !== prev.rowHeight) {
|
|
195
|
+
this.#heights.clear();
|
|
196
|
+
this.#order = void 0;
|
|
197
|
+
}
|
|
198
|
+
const reset = () => {
|
|
199
|
+
sizes.reset(next.count, this.#fit(0), this.#known(auto, next.count));
|
|
200
|
+
};
|
|
201
|
+
if (auto.order !== this.#order) {
|
|
202
|
+
this.#order = auto.order;
|
|
203
|
+
reset();
|
|
204
|
+
} else if (next.count !== sizes.count) sizes.resize(next.count);
|
|
205
|
+
const quantum = this.#quantumFor(sizes.total);
|
|
206
|
+
if (quantum !== this.#quantum) {
|
|
207
|
+
this.#quantum = quantum;
|
|
208
|
+
reset();
|
|
209
|
+
}
|
|
210
|
+
const element = this.#element;
|
|
211
|
+
this.#rescale(sizes.total, element ? this.#top(element) : 0);
|
|
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);
|
|
126
221
|
}
|
|
127
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;
|
|
@@ -155,6 +325,19 @@ var VirtualController = class {
|
|
|
155
325
|
start: scaled ? Math.min(Math.max(start, -rowHeight), size) : start
|
|
156
326
|
});
|
|
157
327
|
}
|
|
328
|
+
const rows = this.#pooled(byId);
|
|
329
|
+
const range = v.range;
|
|
330
|
+
return {
|
|
331
|
+
rows,
|
|
332
|
+
totalSize: scaled ? size : v.getTotalSize(),
|
|
333
|
+
range: range ? {
|
|
334
|
+
start: range.startIndex,
|
|
335
|
+
end: range.endIndex
|
|
336
|
+
} : null
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
/** The rows to render, in pool order: each holds its pool slot while in range. */
|
|
340
|
+
#pooled(byId) {
|
|
158
341
|
const rows = [];
|
|
159
342
|
for (const { id, slot } of this.#pool.update(byId.keys())) {
|
|
160
343
|
const item = byId.get(id);
|
|
@@ -163,26 +346,136 @@ var VirtualController = class {
|
|
|
163
346
|
...item
|
|
164
347
|
});
|
|
165
348
|
}
|
|
166
|
-
|
|
349
|
+
return rows;
|
|
350
|
+
}
|
|
351
|
+
/** `computeWindow()` for rows of different heights. */
|
|
352
|
+
#autoWindow() {
|
|
353
|
+
const { count, overscan, rowHeight, keep, rowIdAt } = this.#config;
|
|
354
|
+
const sizes = this.#sizes;
|
|
355
|
+
const scale = this.#scale;
|
|
356
|
+
const element = this.#element;
|
|
357
|
+
const visible = this.#visible();
|
|
358
|
+
this.#rendered = visible;
|
|
359
|
+
const shift = visible && element ? visible.top - element.scrollTop : 0;
|
|
360
|
+
const byId = /* @__PURE__ */ new Map();
|
|
361
|
+
if (visible && element) {
|
|
362
|
+
const indexes = steadyWindow({
|
|
363
|
+
startIndex: visible.start,
|
|
364
|
+
endIndex: visible.end,
|
|
365
|
+
overscan,
|
|
366
|
+
count
|
|
367
|
+
}, maxVisibleRows(element.clientHeight, rowHeight));
|
|
368
|
+
const first = indexes[0];
|
|
369
|
+
let start = first === void 0 ? 0 : sizes.startOf(first);
|
|
370
|
+
for (const index of indexes) {
|
|
371
|
+
const size = sizes.sizeOf(index);
|
|
372
|
+
byId.set(rowIdAt(index), {
|
|
373
|
+
index,
|
|
374
|
+
start: start - shift,
|
|
375
|
+
size
|
|
376
|
+
});
|
|
377
|
+
start += size;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
this.#pool.pin(keep?.id);
|
|
381
|
+
if (keep && !byId.has(keep.id) && keep.index < count) {
|
|
382
|
+
const size = sizes.sizeOf(keep.index);
|
|
383
|
+
const start = sizes.startOf(keep.index) - shift;
|
|
384
|
+
byId.set(keep.id, {
|
|
385
|
+
index: keep.index,
|
|
386
|
+
start: scale.scaled ? Math.min(Math.max(start, -size), scale.size) : start,
|
|
387
|
+
size
|
|
388
|
+
});
|
|
389
|
+
}
|
|
167
390
|
return {
|
|
168
|
-
rows,
|
|
169
|
-
totalSize: scaled ? size :
|
|
170
|
-
range:
|
|
171
|
-
start:
|
|
172
|
-
end:
|
|
391
|
+
rows: this.#pooled(byId),
|
|
392
|
+
totalSize: scale.scaled ? scale.size : sizes.total,
|
|
393
|
+
range: visible ? {
|
|
394
|
+
start: visible.start,
|
|
395
|
+
end: visible.end
|
|
173
396
|
} : null
|
|
174
397
|
};
|
|
175
398
|
}
|
|
176
|
-
/**
|
|
177
|
-
|
|
178
|
-
|
|
399
|
+
/**
|
|
400
|
+
* Auto mode: records the heights of rendered body rows (`[index, px]`), read
|
|
401
|
+
* once their content has rendered. Heights under the row height count as it.
|
|
402
|
+
* Returns whether any changed; the controller then requests the update that
|
|
403
|
+
* places the rows. Rows above the top row keep the rows in view in place.
|
|
404
|
+
*/
|
|
405
|
+
measureRows(measured) {
|
|
406
|
+
const { auto, rowIdAt, cap } = this.#config;
|
|
407
|
+
if (!auto) return false;
|
|
408
|
+
const sizes = this.#sizes;
|
|
409
|
+
const element = this.#element;
|
|
410
|
+
const top = element ? this.#top(element) : 0;
|
|
411
|
+
const anchor = sizes.indexAt(top);
|
|
412
|
+
const within = top - sizes.startOf(anchor);
|
|
413
|
+
let changed = false;
|
|
414
|
+
for (const [index, px] of measured) {
|
|
415
|
+
if (index < 0 || index >= sizes.count || !(px > 0)) continue;
|
|
416
|
+
this.#heights.set(rowIdAt(index), px);
|
|
417
|
+
if (sizes.set(index, this.#fit(px)) !== 0) changed = true;
|
|
418
|
+
}
|
|
419
|
+
if (!changed) return false;
|
|
420
|
+
const target = sizes.startOf(anchor) + within;
|
|
421
|
+
const scale = this.#scale;
|
|
422
|
+
const wasScaled = scale.excess > 0;
|
|
423
|
+
const logical = sizes.total;
|
|
424
|
+
if (scale.configure(logical, cap(logical)) && element) {
|
|
425
|
+
if (wasScaled && scale.scaled) scale.pivot(element.scrollTop, target, element.scrollHeight - element.clientHeight);
|
|
426
|
+
else if (wasScaled !== scale.scaled) this.#anchor = target;
|
|
427
|
+
else if (Math.abs(target - top) > .01) {
|
|
428
|
+
this.#hold += target - top;
|
|
429
|
+
clearTimeout(this.#holdTimer);
|
|
430
|
+
this.#holdTimer = setTimeout(this.#flushHold, HOLD_MS);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
this.#host.requestUpdate();
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
/** The height of the row at `index` (auto mode: measured, or the row height). */
|
|
437
|
+
sizeOf(index) {
|
|
438
|
+
return this.#config.auto ? this.#sizes.sizeOf(index) : this.#config.rowHeight;
|
|
439
|
+
}
|
|
440
|
+
/** The logical offset of the row at `index` in the body. */
|
|
441
|
+
startOf(index) {
|
|
442
|
+
return this.#config.auto ? this.#sizes.startOf(index) : index * this.#config.rowHeight;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Auto mode: how many rows a page moves from row `from`, the rows that fit in
|
|
446
|
+
* `view` px below it (`down`) or above it. At least one.
|
|
447
|
+
*/
|
|
448
|
+
pageRows(from, down, view) {
|
|
449
|
+
const sizes = this.#sizes;
|
|
450
|
+
const start = sizes.startOf(from);
|
|
451
|
+
if (down) return Math.max(1, sizes.indexAt(start + view + .01) - from);
|
|
452
|
+
let to = sizes.indexAt(start - view);
|
|
453
|
+
if (sizes.startOf(to) < start - view - .01) to += 1;
|
|
454
|
+
return Math.max(1, from - to);
|
|
179
455
|
}
|
|
180
456
|
/** Whether the body is taller than the cap and scrolls scaled. */
|
|
181
457
|
get scaled() {
|
|
182
458
|
return this.#scale.scaled;
|
|
183
459
|
}
|
|
460
|
+
/** The index of the row at the top of the body (0 before the first layout). */
|
|
461
|
+
get topRow() {
|
|
462
|
+
const element = this.#element;
|
|
463
|
+
const { count, rowHeight } = this.#config;
|
|
464
|
+
if (!element || count === 0 || rowHeight <= 0) return 0;
|
|
465
|
+
if (this.#config.auto) return this.#sizes.indexAt(this.#top(element) + .5);
|
|
466
|
+
const row = Math.floor((this.#top(element) + .5) / rowHeight);
|
|
467
|
+
return Math.min(Math.max(row, 0), count - 1);
|
|
468
|
+
}
|
|
184
469
|
/** `'start'` puts the row at the top; `'auto'` scrolls only as far as needed to show it. */
|
|
185
470
|
scrollToIndex(index, align = "start") {
|
|
471
|
+
if (this.#config.auto) {
|
|
472
|
+
const element = this.#element;
|
|
473
|
+
if (!element) return;
|
|
474
|
+
if (align === "auto") this.reveal(index, 0, 0);
|
|
475
|
+
else this.#write(element, this.#sizes.startOf(Math.min(Math.max(index, 0), this.#sizes.count)));
|
|
476
|
+
this.#host.requestUpdate();
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
186
479
|
this.#virtual.getVirtualizer().scrollToIndex(index, { align });
|
|
187
480
|
}
|
|
188
481
|
/** Scrolls by `delta` px of rows (logical px). */
|
|
@@ -205,21 +498,22 @@ var VirtualController = class {
|
|
|
205
498
|
reveal(index, before, after) {
|
|
206
499
|
const element = this.#element;
|
|
207
500
|
if (!element) return false;
|
|
208
|
-
const
|
|
501
|
+
const auto = this.#config.auto !== void 0;
|
|
209
502
|
const offset = this.#top(element);
|
|
210
503
|
const view = element.clientHeight - before - after;
|
|
211
|
-
const start = index
|
|
504
|
+
const start = this.startOf(index);
|
|
505
|
+
const size = this.sizeOf(index);
|
|
212
506
|
let target;
|
|
213
507
|
let round;
|
|
214
|
-
if (start < offset) {
|
|
508
|
+
if (start < offset || auto && size > view) {
|
|
215
509
|
target = start;
|
|
216
510
|
round = Math.floor;
|
|
217
|
-
} else if (start +
|
|
218
|
-
target = start +
|
|
511
|
+
} else if (start + size > offset + view) {
|
|
512
|
+
target = start + size - view;
|
|
219
513
|
round = Math.ceil;
|
|
220
514
|
} else target = offset;
|
|
221
515
|
if (target !== offset) this.#write(element, target, void 0, round);
|
|
222
|
-
return this.#virtual.getVirtualizer().scrollOffset !== this.#top(element);
|
|
516
|
+
return (auto ? this.#rendered?.top : this.#virtual.getVirtualizer().scrollOffset) !== this.#top(element);
|
|
223
517
|
}
|
|
224
518
|
};
|
|
225
519
|
export { VirtualController };
|