import { createVirtualizer as coreCreateVirtualizer, observeElementRect, } from '@tanstack/svelte-virtual'; import type { VirtualItem as CoreVirtualItem } from '@tanstack/virtual-core'; import { get } from 'svelte/store'; export interface VirtualItem { index: number; start: number; size: number; end: number; key: string | number; } export interface VirtualizerOptions { /** Total number of items in the data array */ count: number; /** Function to estimate the size of an item at a given index */ estimateSize: (index: number) => number; /** Number of extra items to render outside viewport (default: 5) */ overscan?: number; /** Function to get the key of an item at a given index (defaults to index) */ getItemKey?: (index: number) => string | number; /** Optional margin in pixels for scroll calculations */ scrollMargin?: number; } /** * Creates a reactive virtualizer using Svelte 5 runes and TanStack's core library. * * @example * ```ts * const virtualizer = createVirtualizer(() => ({ * count: items.length, * estimateSize: () => 80, * overscan: 5, * })); * * // In template: * //
* // {#each virtualizer.items as item} * //
* // {items[item.index]} * //
* // {/each} * //
* ``` */ export function createVirtualizer( optionsGetter: () => VirtualizerOptions, ) { let element = $state(null); const internalStore = coreCreateVirtualizer({ get count() { return optionsGetter().count; }, get estimateSize() { return optionsGetter().estimateSize; }, get overscan() { return optionsGetter().overscan ?? 5; }, get scrollMargin() { return optionsGetter().scrollMargin; }, get getItemKey() { return optionsGetter().getItemKey ?? (i => i); }, getScrollElement: () => element, observeElementRect: observeElementRect, }); const state = $derived(get(internalStore)); const virtualItems = $derived( state.getVirtualItems().map((item: CoreVirtualItem): VirtualItem => ({ index: item.index, start: item.start, size: item.size, end: item.end, key: typeof item.key === 'bigint' ? Number(item.key) : item.key, })), ); return { get items() { return virtualItems; }, get totalSize() { return state.getTotalSize(); }, get scrollOffset() { return state.scrollOffset ?? 0; }, get scrollElement() { return element; }, set scrollElement(el) { element = el; }, scrollToIndex: (idx: number, opt?: { align?: 'start' | 'center' | 'end' | 'auto' }) => state.scrollToIndex(idx, opt), scrollToOffset: (off: number) => state.scrollToOffset(off), measureElement: (el: HTMLElement) => state.measureElement(el), }; } export type Virtualizer = ReturnType;