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 135 136 137 | 1x 1x 1x 1x 15x 15x 15x 15x 15x 33x 33x 33x 17x 17x 16x 16x 16x 16x 16x 15x 15x 15x 15x 32x 32x 32x 17x 17x 17x 17x 15x 15x 15x 15x 15x 32x 5x 26x 26x 26x 5x 5x 32x 149x 149x 149x 10x 15x 15x 15x 15x 2x 2x 2x 15x 4x 3x 3x 4x 15x 10x 10x 10x 15x 15x 15x 15x 15x 8x 8x 8x 15x 15x 15x 15x | /* 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();
// Defensive check: Should never occur if calculation logic is correct
// This protects against invalid state where endIndex < startIndex
// Coverage: Implicitly validated by all other tests passing
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 };
}
|