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.
|
* Used to render visible items with absolute positioning based on computed offsets.
|
||||||
*/
|
*/
|
||||||
export interface VirtualItem {
|
export interface VirtualItem {
|
||||||
/** Index of the item in the data array */
|
/**
|
||||||
|
* Index of the item in the data array
|
||||||
|
*/
|
||||||
index: number;
|
index: number;
|
||||||
/** Offset from the top of the list in pixels */
|
/**
|
||||||
|
* Offset from the top of the list in pixels
|
||||||
|
*/
|
||||||
start: number;
|
start: number;
|
||||||
/** Height/size of the item in pixels */
|
/**
|
||||||
|
* Height/size of the item in pixels
|
||||||
|
*/
|
||||||
size: number;
|
size: number;
|
||||||
/** End position in pixels (start + size) */
|
/**
|
||||||
|
* End position in pixels (start + size)
|
||||||
|
*/
|
||||||
end: number;
|
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;
|
key: string | number;
|
||||||
/** Whether the item is currently visible in the viewport */
|
/**
|
||||||
isVisible: boolean;
|
* Whether the item is currently fully visible in the viewport
|
||||||
/** Proximity of the item to the center of 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;
|
proximity: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +63,11 @@ export interface VirtualizerOptions {
|
|||||||
* Can be useful for handling sticky headers or other UI elements.
|
* Can be useful for handling sticky headers or other UI elements.
|
||||||
*/
|
*/
|
||||||
scrollMargin?: number;
|
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 containerHeight = $state(0);
|
||||||
let measuredSizes = $state<Record<number, number>>({});
|
let measuredSizes = $state<Record<number, number>>({});
|
||||||
let elementRef: HTMLElement | null = null;
|
let elementRef: HTMLElement | null = null;
|
||||||
|
let elementOffsetTop = 0;
|
||||||
|
|
||||||
// By wrapping the getter in $derived, we track everything inside it
|
// By wrapping the getter in $derived, we track everything inside it
|
||||||
const options = $derived(optionsGetter());
|
const options = $derived(optionsGetter());
|
||||||
@@ -157,9 +181,8 @@ export function createVirtualizer<T>(
|
|||||||
const itemEnd = itemStart + itemSize;
|
const itemEnd = itemStart + itemSize;
|
||||||
|
|
||||||
// Visibility check: Does the item overlap the viewport?
|
// Visibility check: Does the item overlap the viewport?
|
||||||
// const isVisible = itemStart < viewportEnd && itemEnd > scrollOffset;
|
const isPartiallyVisible = itemStart < viewportEnd && itemEnd > scrollOffset;
|
||||||
// Fully visible
|
const isFullyVisible = itemStart >= scrollOffset && itemEnd <= viewportEnd;
|
||||||
const isVisible = itemStart >= scrollOffset && itemEnd <= viewportEnd;
|
|
||||||
|
|
||||||
// Proximity calculation: 1.0 at center, 0.0 at edges
|
// Proximity calculation: 1.0 at center, 0.0 at edges
|
||||||
const itemCenter = itemStart + (itemSize / 2);
|
const itemCenter = itemStart + (itemSize / 2);
|
||||||
@@ -173,7 +196,8 @@ export function createVirtualizer<T>(
|
|||||||
size: itemSize,
|
size: itemSize,
|
||||||
end: itemEnd,
|
end: itemEnd,
|
||||||
key: options.getItemKey?.(i) ?? i,
|
key: options.getItemKey?.(i) ?? i,
|
||||||
isVisible,
|
isPartiallyVisible,
|
||||||
|
isFullyVisible,
|
||||||
proximity,
|
proximity,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -192,6 +216,53 @@ export function createVirtualizer<T>(
|
|||||||
*/
|
*/
|
||||||
function container(node: HTMLElement) {
|
function container(node: HTMLElement) {
|
||||||
elementRef = node;
|
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;
|
containerHeight = node.offsetHeight;
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
@@ -213,6 +284,7 @@ export function createVirtualizer<T>(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let measurementBuffer: Record<number, number> = {};
|
let measurementBuffer: Record<number, number> = {};
|
||||||
let frameId: number | null = null;
|
let frameId: number | null = null;
|
||||||
@@ -275,12 +347,23 @@ export function createVirtualizer<T>(
|
|||||||
const itemStart = offsets[index];
|
const itemStart = offsets[index];
|
||||||
const itemSize = measuredSizes[index] ?? options.estimateSize(index);
|
const itemSize = measuredSizes[index] ?? options.estimateSize(index);
|
||||||
let target = itemStart;
|
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 === 'center') target = itemStart - containerHeight / 2 + itemSize / 2;
|
||||||
if (align === 'end') target = itemStart - containerHeight + itemSize;
|
if (align === 'end') target = itemStart - containerHeight + itemSize;
|
||||||
|
|
||||||
elementRef.scrollTo({ top: target, behavior: 'smooth' });
|
elementRef.scrollTo({ top: target, behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
/** Computed array of visible items to render (reactive) */
|
/** Computed array of visible items to render (reactive) */
|
||||||
|
|||||||
Reference in New Issue
Block a user