Files
frontend-svelte/src/shared/lib/helpers/createVirtualizer/createVirtualizer.svelte.ts

117 lines
3.2 KiB
TypeScript
Raw Normal View History

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),
};
}
export type Virtualizer = ReturnType<typeof createVirtualizer>;