argus-table 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cdn/argus-table.js +26 -26
- package/dist/cdn/argus-table.js.map +1 -1
- package/dist/controllers/virtual-controller.js +3 -1
- package/dist/controllers/virtual-controller.js.map +1 -1
- package/dist/core/src/virtual/steady-window.js +30 -0
- package/dist/core/src/virtual/steady-window.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { RowPool } from "../core/src/virtual/row-pool.js";
|
|
2
2
|
import { ScrollScale } from "../core/src/virtual/scroll-scale.js";
|
|
3
|
+
import { maxVisibleRows, steadyWindow } from "../core/src/virtual/steady-window.js";
|
|
3
4
|
import { VirtualizerController } from "@tanstack/lit-virtual";
|
|
4
5
|
// src/controllers/virtual-controller.ts
|
|
5
6
|
/**
|
|
@@ -56,7 +57,8 @@ var VirtualController = class {
|
|
|
56
57
|
};
|
|
57
58
|
},
|
|
58
59
|
estimateSize: () => this.#config.rowHeight,
|
|
59
|
-
overscan: this.#config.overscan
|
|
60
|
+
overscan: this.#config.overscan,
|
|
61
|
+
rangeExtractor: (range) => steadyWindow(range, maxVisibleRows(this.#element?.clientHeight ?? 0, this.#config.rowHeight))
|
|
60
62
|
});
|
|
61
63
|
host.addController(this);
|
|
62
64
|
}
|
|
@@ -1 +1 @@
|
|
|
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":";;;;;;;;;;;;;;;;;;AAkDA,IAAa,oBAAb,MAA6D;CAC3D;CACA,QAAiB,IAAI,QAAQ;CAC7B,SAAkB,IAAI,YAAY;CAClC;CACA;CACA;CACA;CACA,UAAyB;EACvB,OAAO;EACP,WAAW;EACX,UAAU;EACV,SAAS;EACT,KAAK;CACP;;CAEA;CAEA,YAAY,MAA8B,kBAA4C;EACpF,KAAK,QAAQ;EACb,KAAK,oBAAoB;EACzB,KAAK,WAAW,IAAI,sBAA4C,MAAM;GACpE,OAAO;GACP,wBAAwB,KAAK,UAAU;GAEvC,qBAAqB,WAAW,OAAO;IACrC,MAAM,UAAU,KAAK;IACrB,IAAI,CAAC,SAAS,OAAO,KAAA;IACrB,MAAM,eAAe;KACnB,GAAG;MAAE,OAAO,QAAQ;MAAa,QAAQ,QAAQ;KAAa,CAAC;IACjE;IACA,OAAO;IACP,MAAM,WAAW,IAAI,eAAe,MAAM;IAC1C,SAAS,QAAQ,OAAO;IACxB,aAAa;KACX,SAAS,WAAW;IACtB;GACF;GACA,oBAAoB,KAAK,QAAQ;GACjC,UAAU,KAAK,QAAQ;EACzB,CAAC;EACD,KAAK,cAAc,IAAI;CACzB;;CAGA,YAAgC;EAC9B,MAAM,UAAU,KAAK,kBAAkB;EACvC,IAAI,CAAC,SAAS,OAAO;EACrB,IAAI,KAAK,aAAa,WAAW,CAAC,KAAK,UAAU;GAC/C,KAAK,WAAW;GAChB,KAAK,WAAW,IAAI,MAAM,SAAS,EACjC,MAAM,QAAQ,QAAQ;IACpB,IAAI,QAAQ,aAAa,OAAO,KAAK,KAAK,MAAM;IAChD,IAAI,QAAQ,gBAAgB,OAAO,OAAO,eAAe,KAAK,OAAO;IACrE,IAAI,QAAQ,YACV,QAAQ,YAA6B;KACnC,IAAI,QAAQ,QAAQ,KAAA,GAAW,OAAO,SAAS,OAAO;UACjD,KAAK,OAAO,QAAQ,QAAQ,KAAK,QAAQ,QAAQ;IACxD;IAEF,MAAM,QAAiB,QAAQ,IAAI,QAAQ,GAAG;IAE9C,OAAO,OAAO,UAAU,aACnB,MAA0C,KAAK,MAAM,IACtD;GACN,EACF,CAAC;EACH;EACA,OAAO,KAAK;CACd;;CAGA,KAAK,SAA8B;EACjC,MAAM,MAAM,QAAQ;EACpB,MAAM,QAAQ,KAAK;EACnB,OAAO,MAAM,SAAS,MAAM,UAAU,KAAK,QAAQ,eAAe,QAAQ,YAAY,IAAI;CAC5F;;;;;CAMA,OACE,SACA,SACA,UACA,OACM;EACN,MAAM,QAAQ,KAAK;EACnB,IAAI,CAAC,MAAM,QAAQ;GACjB,QAAQ,SAAS;IAAE,KAAK;IAAS;GAAS,CAAC;GAC3C;EACF;EACA,MAAM,QAAQ,QAAQ,eAAe,QAAQ;EAC7C,MAAM,SAAS,KAAK,IAAI,KAAK,IAAI,SAAS,CAAC,GAAG,QAAQ,MAAM,MAAM;EAClE,MAAM,WAAW,MAAM,WAAW,QAAQ,KAAK;EAC/C,QAAQ,SAAS;GAAE,KAAK,QAAQ,MAAM,QAAQ,IAAI;GAAU;EAAS,CAAC;EACtE,IAAI,aAAa,UAAU,MAAM,KAAK,QAAQ,QAAQ,SAAS;CACjE;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;EAEA,MAAM,YAAY,KAAK,OAAO,SAAS;EACvC,MAAM,SAAS,KAAK,SAAS,eAAe,CAAC,CAAC;EAC9C,IAAI,KAAK,OAAO,UAAU,KAAK,QAAQ,KAAK,WAAW,KAAK,GAAG,GAIxD;QAAA,aAAa,KAAK,OAAO,WAAW,WAAW,QAAQ,SAAS,GACnE,KAAK,UAAU;EAAA;CAGrB;CAEA,cAAoB;EAClB,MAAM,SAAS,KAAK;EACpB,IAAI,WAAW,KAAA,GAAW;EAC1B,KAAK,UAAU,KAAA;EACf,IAAI,KAAK,UAAU,KAAK,OAAO,KAAK,UAAU,MAAM;EAGpD,KAAK,MAAM,cAAc;CAC3B;;;;CAKA,gBAA+B;EAC7B,MAAM,IAAI,KAAK,SAAS,eAAe;EACvC,MAAM,QAAQ,EAAE,gBAAgB;EAIhC,MAAM,SAAS,KAAK,OAAO;EAC3B,MAAM,OAAO,KAAK,OAAO;EACzB,MAAM,QAAQ,UAAU,EAAE,gBAAgB,MAAM,KAAK,UAAU,aAAa,KAAK;EACjF,MAAM,uBAAO,IAAI,IAA8C;EAC/D,KAAK,MAAM,QAAQ,OACjB,KAAK,IAAI,KAAK,QAAQ,QAAQ,KAAK,KAAK,GAAG;GAAE,OAAO,KAAK;GAAO,OAAO,KAAK,QAAQ;EAAM,CAAC;EAE7F,MAAM,EAAE,MAAM,cAAc,KAAK;EACjC,KAAK,MAAM,IAAI,MAAM,EAAE;EACvB,IAAI,QAAQ,CAAC,KAAK,IAAI,KAAK,EAAE,GAAG;GAI9B,MAAM,QAAQ,KAAK,QAAQ,YAAY;GACvC,KAAK,IAAI,KAAK,IAAI;IAChB,OAAO,KAAK;IACZ,OAAO,SAAS,KAAK,IAAI,KAAK,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,IAAI;GAChE,CAAC;EACH;EACA,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,SAAS,OAAO,EAAE,aAAa;GAC1C,OAAO,QAAQ;IAAE,OAAO,MAAM;IAAY,KAAK,MAAM;GAAS,IAAI;EACpE;CACF;;CAGA,IAAI,WAAmB;EACrB,OAAO,KAAK,MAAM;CACpB;;CAGA,IAAI,SAAkB;EACpB,OAAO,KAAK,OAAO;CACrB;;CAGA,cAAc,OAAe,QAA0B,SAAe;EACpE,KAAK,SAAS,eAAe,CAAC,CAAC,cAAc,OAAO,EAAE,MAAM,CAAC;CAC/D;;CAGA,SAAS,OAAqB;EAC5B,MAAM,UAAU,KAAK;EACrB,IAAI,SAAS,KAAK,OAAO,SAAS,KAAK,KAAK,OAAO,IAAI,KAAK;CAC9D;;;;;;;;CASA,OAAO,OAAe,QAAgB,OAAwB;EAC5D,MAAM,UAAU,KAAK;EACrB,IAAI,CAAC,SAAS,OAAO;EACrB,MAAM,EAAE,cAAc,KAAK;EAC3B,MAAM,SAAS,KAAK,KAAK,OAAO;EAChC,MAAM,OAAO,QAAQ,eAAe,SAAS;EAC7C,MAAM,QAAQ,QAAQ;EACtB,IAAI;EACJ,IAAI;EACJ,IAAI,QAAQ,QAAQ;GAClB,SAAS;GACT,QAAQ,KAAK;EACf,OAAO,IAAI,QAAQ,YAAY,SAAS,MAAM;GAC5C,SAAS,QAAQ,YAAY;GAC7B,QAAQ,KAAK;EACf,OACE,SAAS;EAEX,IAAI,WAAW,QAAQ,KAAK,OAAO,SAAS,QAAQ,KAAA,GAAW,KAAK;EAGpE,OAAO,KAAK,SAAS,eAAe,CAAC,CAAC,iBAAiB,KAAK,KAAK,OAAO;CAC1E;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, maxVisibleRows, steadyWindow } 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 // One window size from the first render: the pool never grows while scrolling\n // (in WebKit, new rows' slots would cut a wheel scroll short; see steadyWindow).\n rangeExtractor: (range) =>\n steadyWindow(\n range,\n maxVisibleRows(this.#element?.clientHeight ?? 0, this.#config.rowHeight),\n ),\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,30 @@
|
|
|
1
|
+
// ../core/src/virtual/steady-window.ts
|
|
2
|
+
/**
|
|
3
|
+
* The most rows of `rowHeight` px a view `viewHeight` px tall can show at once,
|
|
4
|
+
* partly: one more than fit, when the view starts inside a row.
|
|
5
|
+
*/
|
|
6
|
+
function maxVisibleRows(viewHeight, rowHeight) {
|
|
7
|
+
return viewHeight > 0 && rowHeight > 0 ? Math.ceil(viewHeight / rowHeight) + 1 : 0;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The row indexes to render: the visible rows plus `overscan` on each side,
|
|
11
|
+
* always the same number of rows (`maxVisible` plus twice the overscan, or all
|
|
12
|
+
* rows when there are fewer). At either end the overscan that has no room goes to
|
|
13
|
+
* the other side.
|
|
14
|
+
*
|
|
15
|
+
* The usual window is cut off at the ends instead: at the top it holds only the
|
|
16
|
+
* overscan below, and the row pool grows by a row, its cells and their slots,
|
|
17
|
+
* with every row scrolled until the overscan above is full. WebKit cancels a
|
|
18
|
+
* wheel scroll's animation when slot assignment changes inside the scroller, so
|
|
19
|
+
* from the top each wheel step moved a few px instead of a full step. A window of
|
|
20
|
+
* one size fills the pool on the first render, and scrolling only reuses it.
|
|
21
|
+
*/
|
|
22
|
+
function steadyWindow({ startIndex, endIndex, overscan, count }, maxVisible) {
|
|
23
|
+
const visible = Math.max(maxVisible, endIndex - startIndex + 1);
|
|
24
|
+
const size = Math.min(count, visible + 2 * overscan);
|
|
25
|
+
const first = Math.max(0, Math.min(startIndex - overscan, count - size));
|
|
26
|
+
return Array.from({ length: size }, (_, i) => first + i);
|
|
27
|
+
}
|
|
28
|
+
export { maxVisibleRows, steadyWindow };
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=steady-window.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"steady-window.js","names":[],"sources":["../../../../../core/src/virtual/steady-window.ts"],"sourcesContent":["/** The visible rows as the virtualizer reports them: indexes inclusive. */\nexport interface VisibleRange {\n startIndex: number;\n endIndex: number;\n overscan: number;\n count: number;\n}\n\n/**\n * The most rows of `rowHeight` px a view `viewHeight` px tall can show at once,\n * partly: one more than fit, when the view starts inside a row.\n */\nexport function maxVisibleRows(viewHeight: number, rowHeight: number): number {\n return viewHeight > 0 && rowHeight > 0 ? Math.ceil(viewHeight / rowHeight) + 1 : 0;\n}\n\n/**\n * The row indexes to render: the visible rows plus `overscan` on each side,\n * always the same number of rows (`maxVisible` plus twice the overscan, or all\n * rows when there are fewer). At either end the overscan that has no room goes to\n * the other side.\n *\n * The usual window is cut off at the ends instead: at the top it holds only the\n * overscan below, and the row pool grows by a row, its cells and their slots,\n * with every row scrolled until the overscan above is full. WebKit cancels a\n * wheel scroll's animation when slot assignment changes inside the scroller, so\n * from the top each wheel step moved a few px instead of a full step. A window of\n * one size fills the pool on the first render, and scrolling only reuses it.\n */\nexport function steadyWindow(\n { startIndex, endIndex, overscan, count }: VisibleRange,\n maxVisible: number,\n): number[] {\n const visible = Math.max(maxVisible, endIndex - startIndex + 1);\n const size = Math.min(count, visible + 2 * overscan);\n const first = Math.max(0, Math.min(startIndex - overscan, count - size));\n return Array.from({ length: size }, (_, i) => first + i);\n}\n"],"mappings":";;;;;AAYA,SAAgB,eAAe,YAAoB,WAA2B;CAC5E,OAAO,aAAa,KAAK,YAAY,IAAI,KAAK,KAAK,aAAa,SAAS,IAAI,IAAI;AACnF;;;;;;;;;;;;;;AAeA,SAAgB,aACd,EAAE,YAAY,UAAU,UAAU,SAClC,YACU;CACV,MAAM,UAAU,KAAK,IAAI,YAAY,WAAW,aAAa,CAAC;CAC9D,MAAM,OAAO,KAAK,IAAI,OAAO,UAAU,IAAI,QAAQ;CACnD,MAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,aAAa,UAAU,QAAQ,IAAI,CAAC;CACvE,OAAO,MAAM,KAAK,EAAE,QAAQ,KAAK,IAAI,GAAG,MAAM,QAAQ,CAAC;AACzD"}
|