import {
batch,
createEffect,
createSignal,
For,
JSX,
MergeProps,
mergeProps,
on,
onMount,
} from "solid-js";
type DefaultProps<T, K extends keyof T> = MergeProps<[Required<Pick<T, K>>, T]>;
function useDefaultProps<T, K extends keyof T>(
props: T,
defaults: Required<Pick<T, K>>
): DefaultProps<T, K> {
return mergeProps(defaults, props);
}
type OnParameters<T1, T2> = Parameters<typeof on<T1, T2>>;
let useEffectWatch = <T1, T2>(
a: OnParameters<T1, T2>[0],
b: OnParameters<T1, T2>[1],
c?: OnParameters<T1, T2>[2]
) => {
createEffect(on(a, b, c));
};
let useEffectWatchDefer = <T1, T2>(
a: OnParameters<T1, T2>[0],
b: OnParameters<T1, T2>[1]
) => {
createEffect(on(a, b, { defer: true }));
};
export function VirtualList<T>(inProps: {
data: T[];
rowHeight: number;
renderRow: (row: T) => JSX.Element;
overscanCount?: number;
style?: JSX.CSSProperties;
class?: string;
}) {
let props = useDefaultProps(inProps, {
overscanCount: 5,
});
let rootElement: HTMLDivElement;
const [height, setHeight] = createSignal<number>(0);
const [rowCount, setRowCount] = createSignal<number>(0);
const [offset, setOffset] = createSignal<number>(0);
const [start, setStart] = createSignal<number>(0);
const [end, setEnd] = createSignal<number>(0);
const [selection, setSelection] = createSignal<T[]>([]);
const resize = () => {
if (height() !== rootElement.offsetHeight) {
setHeight(rootElement.offsetHeight);
setRowCount(getRowCount());
}
};
const getStart = () => {
let ret = Number((offset() / props.rowHeight).toFixed(0));
ret = Math.max(0, ret - (ret % props.overscanCount));
return ret;
};
const getRowCount = () => {
let ret = Number((height() / props.rowHeight).toFixed(0)) + props.overscanCount;
return ret;
};
const getEnd = () => {
let ret = start() + rowCount() + 1;
return ret;
};
const tick = () => {
batch(() => {
setOffset(rootElement.scrollTop);
setStart(getStart());
setEnd(getEnd());
setSelection(props.data?.slice(start(), end()));
});
};
const handleScroll = () => {
if (offset() != rootElement.scrollTop) {
tick();
}
};
onMount(() => {
resize();
tick();
rootElement?.addEventListener?.("resize", resize);
});
useEffectWatchDefer(
() => props.data,
() => {
tick();
}
);
return (
<div
ref={(r) => (rootElement = r)}
style={{ overflow: "auto", ...props.style }}
class={props.class}
onScroll={handleScroll}
>
<div
style={{
position: "relative",
overflow: "hidden",
width: "100%",
"min-height": "100%",
height: `${(props.data?.length ?? 0) * props.rowHeight}px`,
}}
>
<div
style={{
position: "absolute",
top: `${start() * props.rowHeight}px`,
left: 0,
height: "100%",
width: "100%",
overflow: "visible",
}}
>
<For each={selection()}>
{(row) => {
return props.renderRow(row);
}}
</For>
</div>
</div>
</div>
);
}
使用
<VirtualList
style={{height:'100%'}}
data={dataList()!}
rowHeight={30}
renderRow={(row) => {
return (
<div style={{ height: "30px" }} >...</div>
)
}}
/>