feat(createVirtualizer): refine virtualizer logic, add useWindowScroll flag to use window scroll
This commit is contained in:
@@ -4,19 +4,37 @@
|
||||
* Used to render visible items with absolute positioning based on computed offsets.
|
||||
*/
|
||||
export interface VirtualItem {
|
||||
/** Index of the item in the data array */
|
||||
/**
|
||||
* Index of the item in the data array
|
||||
*/
|
||||
index: number;
|
||||
/** Offset from the top of the list in pixels */
|
||||
/**
|
||||
* Offset from the top of the list in pixels
|
||||
*/
|
||||
start: number;
|
||||
/** Height/size of the item in pixels */
|
||||
/**
|
||||
* Height/size of the item in pixels
|
||||
*/
|
||||
size: number;
|
||||
/** End position in pixels (start + size) */
|
||||
/**
|
||||
* End position in pixels (start + size)
|
||||
*/
|
||||
end: number;
|
||||
/** Unique key for the item (for Svelte's {#each} keying) */
|
||||
/**
|
||||
* Unique key for the item (for Svelte's {#each} keying)
|
||||
*/
|
||||
key: string | number;
|
||||
/** Whether the item is currently visible in the viewport */
|
||||
isVisible: boolean;
|
||||
/** Proximity of the item to the center of the viewport */
|
||||
/**
|
||||
* Whether the item is currently fully visible in the viewport
|
||||
*/
|
||||
isFullyVisible: boolean;
|
||||
/**
|
||||
* Whether the item is currently partially visible in the viewport
|
||||
*/
|
||||
isPartiallyVisible: boolean;
|
||||
/**
|
||||
* Proximity of the item to the center of the viewport
|
||||
*/
|
||||
proximity: number;
|
||||
}
|
||||
|
||||
@@ -45,6 +63,11 @@ export interface VirtualizerOptions {
|
||||
* Can be useful for handling sticky headers or other UI elements.
|
||||
*/
|
||||
scrollMargin?: number;
|
||||
/**
|
||||
* Whether to use the window as the scroll container.
|
||||
* @default false
|
||||
*/
|
||||
useWindowScroll?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,6 +115,7 @@ export function createVirtualizer<T>(
|
||||
let containerHeight = $state(0);
|
||||
let measuredSizes = $state<Record<number, number>>({});
|
||||
let elementRef: HTMLElement | null = null;
|
||||
let elementOffsetTop = 0;
|
||||
|
||||
// By wrapping the getter in $derived, we track everything inside it
|
||||
const options = $derived(optionsGetter());
|
||||
@@ -157,9 +181,8 @@ export function createVirtualizer<T>(
|
||||
const itemEnd = itemStart + itemSize;
|
||||
|
||||
// Visibility check: Does the item overlap the viewport?
|
||||
// const isVisible = itemStart < viewportEnd && itemEnd > scrollOffset;
|
||||
// Fully visible
|
||||
const isVisible = itemStart >= scrollOffset && itemEnd <= viewportEnd;
|
||||
const isPartiallyVisible = itemStart < viewportEnd && itemEnd > scrollOffset;
|
||||
const isFullyVisible = itemStart >= scrollOffset && itemEnd <= viewportEnd;
|
||||
|
||||
// Proximity calculation: 1.0 at center, 0.0 at edges
|
||||
const itemCenter = itemStart + (itemSize / 2);
|
||||
@@ -173,7 +196,8 @@ export function createVirtualizer<T>(
|
||||
size: itemSize,
|
||||
end: itemEnd,
|
||||
key: options.getItemKey?.(i) ?? i,
|
||||
isVisible,
|
||||
isPartiallyVisible,
|
||||
isFullyVisible,
|
||||
proximity,
|
||||
});
|
||||
}
|
||||
@@ -192,6 +216,53 @@ export function createVirtualizer<T>(
|
||||
*/
|
||||
function container(node: HTMLElement) {
|
||||
elementRef = node;
|
||||
const { useWindowScroll } = optionsGetter();
|
||||
|
||||
if (useWindowScroll) {
|
||||
// Calculate initial offset ONCE
|
||||
const getElementOffset = () => {
|
||||
const rect = node.getBoundingClientRect();
|
||||
return rect.top + window.scrollY;
|
||||
};
|
||||
|
||||
let cachedOffsetTop = getElementOffset();
|
||||
containerHeight = window.innerHeight;
|
||||
|
||||
const handleScroll = () => {
|
||||
// Use cached offset for scroll calculations
|
||||
scrollOffset = Math.max(0, window.scrollY - cachedOffsetTop);
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
const oldHeight = containerHeight;
|
||||
containerHeight = window.innerHeight;
|
||||
|
||||
// Recalculate offset on resize (layout may have shifted)
|
||||
const newOffsetTop = getElementOffset();
|
||||
if (Math.abs(newOffsetTop - cachedOffsetTop) > 0.5) {
|
||||
cachedOffsetTop = newOffsetTop;
|
||||
handleScroll(); // Recalculate scroll position
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// Initial calculation
|
||||
handleScroll();
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
window.removeEventListener('resize', handleResize);
|
||||
if (frameId !== null) {
|
||||
cancelAnimationFrame(frameId);
|
||||
frameId = null;
|
||||
}
|
||||
elementRef = null;
|
||||
},
|
||||
};
|
||||
} else {
|
||||
containerHeight = node.offsetHeight;
|
||||
|
||||
const handleScroll = () => {
|
||||
@@ -213,6 +284,7 @@ export function createVirtualizer<T>(
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let measurementBuffer: Record<number, number> = {};
|
||||
let frameId: number | null = null;
|
||||
@@ -275,12 +347,23 @@ export function createVirtualizer<T>(
|
||||
const itemStart = offsets[index];
|
||||
const itemSize = measuredSizes[index] ?? options.estimateSize(index);
|
||||
let target = itemStart;
|
||||
const { useWindowScroll } = optionsGetter();
|
||||
|
||||
if (useWindowScroll) {
|
||||
if (align === 'center') target = itemStart - window.innerHeight / 2 + itemSize / 2;
|
||||
if (align === 'end') target = itemStart - window.innerHeight + itemSize;
|
||||
|
||||
// Add container offset to target to get absolute document position
|
||||
const absoluteTarget = target + elementOffsetTop;
|
||||
|
||||
window.scrollTo({ top: absoluteTarget, behavior: 'smooth' });
|
||||
} else {
|
||||
if (align === 'center') target = itemStart - containerHeight / 2 + itemSize / 2;
|
||||
if (align === 'end') target = itemStart - containerHeight + itemSize;
|
||||
|
||||
elementRef.scrollTo({ top: target, behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
/** Computed array of visible items to render (reactive) */
|
||||
|
||||
Reference in New Issue
Block a user