Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | 1x 1x 1x 1x 11x 11x 11x 11x 11x 25x 25x 25x 13x 13x 12x 12x 12x 12x 12x 11x 11x 11x 11x 24x 24x 24x 13x 13x 13x 13x 11x 11x 11x 11x 11x 24x 1x 18x 18x 18x 1x 1x 24x 149x 149x 149x 10x 11x 11x 11x 11x 2x 2x 2x 11x 4x 3x 3x 4x 11x 10x 10x 10x 11x 11x 11x 11x 11x 4x 4x 4x 11x 11x 11x 11x | /* eslint-disable @typescript-eslint/no-explicit-any */
import { onCleanup } from '../lifecycle/lifecycle';
import { createMemo } from '../primitives/memo';
import { createSignal } from '../primitives/signal';
import type { Memo, SignalGetter } from '../types';
export interface VirtualizerOptions<T = any> {
items: SignalGetter<T[]>;
itemHeight: number;
overscan?: number;
workBuffer?: Uint32Array; // optional zero-allocation hot path
}
export interface VirtualItem<T = any> {
index: number;
data: T;
offsetTop: number;
}
export interface VisibleState {
startIndex: number;
endIndex: number;
scrollOffset: number;
}
export interface Virtualizer<T = any> {
// The subset of items that should be rendered to the DOM
visibleItems: Memo<VirtualItem<T>[]>;
// The total height of the entire list, for the scroll container
totalHeight: Memo<number>;
// Unified visible window state
visibleState: Memo<VisibleState>;
// A function to be called with the scroll container element
setContainer: (el: HTMLElement) => void;
}
/**
* Creates a headless virtual scroller with optimal performance for Aided.
* @template T - Type of list items
*/
export function createVirtualizer<T = any>(options: VirtualizerOptions<T>): Virtualizer<T> {
const { items, itemHeight, overscan = 5 } = options;
const [scrollTop, setScrollTop] = createSignal(0);
const [containerHeight, setContainerHeight] = createSignal(0);
const totalHeight = createMemo(() => items().length * itemHeight);
const visibleState = createMemo<VisibleState>(() => {
const listLen = items().length;
const height = containerHeight();
if (height <= 0 || listLen === 0 || itemHeight <= 0) {
return { startIndex: 0, endIndex: -1, scrollOffset: 0 };
}
const scroll = scrollTop();
const startIndex = Math.max(0, Math.floor(scroll / itemHeight) - overscan);
const endIndex = Math.min(listLen - 1, Math.ceil((scroll + height) / itemHeight) + overscan);
const scrollOffset = startIndex * itemHeight;
return { startIndex, endIndex, scrollOffset };
});
// Reuse array to minimize allocations
let reused: VirtualItem<T>[] = [];
let lastCount = 0;
const visibleItems = createMemo<VirtualItem<T>[]>(() => {
const list = items();
const vs = visibleState();
if (vs.endIndex < vs.startIndex) {
if (reused.length) reused = [];
lastCount = 0;
return reused;
}
const count = vs.endIndex - vs.startIndex + 1;
if (count !== lastCount) {
reused = new Array(count);
lastCount = count;
}
// Small arrays path: direct fill
if (list.length < 64) {
for (let i = 0; i < count; i++) {
const idx = vs.startIndex + i;
reused[i] = { index: idx, data: list[idx], offsetTop: idx * itemHeight } as VirtualItem<T>;
}
return reused;
}
// Larger arrays path
for (let i = 0; i < count; i++) {
const idx = vs.startIndex + i;
reused[i] = { index: idx, data: list[idx], offsetTop: idx * itemHeight } as VirtualItem<T>;
}
return reused;
});
const setContainer = (el: HTMLElement) => {
let rafId: number | null = null;
const flushScroll = () => {
rafId = null;
setScrollTop(el.scrollTop);
};
const onScroll = () => {
if (rafId == null) {
rafId = requestAnimationFrame(flushScroll);
}
};
// Use ResizeObserver to handle container size changes
const resizeObserver = new ResizeObserver(entries => {
if (entries[0]) {
setContainerHeight(entries[0].contentRect.height);
}
});
el.addEventListener('scroll', onScroll, { passive: true });
resizeObserver.observe(el);
setContainerHeight(el.clientHeight); // initial
onCleanup(() => {
if (rafId != null) cancelAnimationFrame(rafId);
el.removeEventListener('scroll', onScroll as EventListener);
resizeObserver.disconnect();
});
};
return { visibleItems, totalHeight, visibleState, setContainer };
}
|