2026-01-06 21:38:18 +03:00
|
|
|
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:
|
|
|
|
|
* // <div bind:this={virtualizer.scrollElement}>
|
|
|
|
|
* // {#each virtualizer.items as item}
|
|
|
|
|
* // <div style="transform: translateY({item.start}px)">
|
|
|
|
|
* // {items[item.index]}
|
|
|
|
|
* // </div>
|
|
|
|
|
* // {/each}
|
|
|
|
|
* // </div>
|
|
|
|
|
* ```
|
|
|
|
|
*/
|
|
|
|
|
export function createVirtualizer(
|
|
|
|
|
optionsGetter: () => VirtualizerOptions,
|
|
|
|
|
) {
|
|
|
|
|
let element = $state<HTMLElement | null>(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),
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-01-07 14:27:25 +03:00
|
|
|
|
|
|
|
|
export type Virtualizer = ReturnType<typeof createVirtualizer>;
|