forked from mengyxu/noob-components
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
149 lines
3.5 KiB
149 lines
3.5 KiB
|
3 months ago
|
/**
|
||
|
|
* useVirtualRows
|
||
|
|
*
|
||
|
|
* Custom virtualizer using prefix-sum offsets + binary search.
|
||
|
|
* Inspired by pretext-table approach - lightweight, works perfectly
|
||
|
|
* with pre-computed row heights from pretext.
|
||
|
|
*/
|
||
|
|
import { ref, computed, watch, type Ref } from "vue";
|
||
|
|
|
||
|
|
const DEFAULT_OVERSCAN = 5;
|
||
|
|
|
||
|
|
export interface VirtualRow {
|
||
|
|
index: number;
|
||
|
|
offsetY: number;
|
||
|
|
height: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface VirtualRange {
|
||
|
|
startIndex: number;
|
||
|
|
endIndex: number;
|
||
|
|
offsetY: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface UseVirtualRowsOptions {
|
||
|
|
overscan?: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Build a prefix-sum array from row heights for O(1) offset lookups.
|
||
|
|
*/
|
||
|
|
export function buildOffsets(heights: number[]): number[] {
|
||
|
|
const offsets = new Array(heights.length + 1);
|
||
|
|
offsets[0] = 0;
|
||
|
|
for (let i = 0; i < heights.length; i++) {
|
||
|
|
offsets[i + 1] = offsets[i] + heights[i];
|
||
|
|
}
|
||
|
|
return offsets;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Binary search: find the first row index where bottom edge >= scrollTop.
|
||
|
|
* Returns index in range [0, heights.length - 1].
|
||
|
|
*/
|
||
|
|
function findStartIndex(offsets: number[], scrollTop: number): number {
|
||
|
|
let lo = 0;
|
||
|
|
let hi = offsets.length - 2; // last valid row index
|
||
|
|
|
||
|
|
while (lo < hi) {
|
||
|
|
const mid = (lo + hi) >>> 1;
|
||
|
|
if (offsets[mid + 1] <= scrollTop) {
|
||
|
|
lo = mid + 1;
|
||
|
|
} else {
|
||
|
|
hi = mid;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return lo;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function useVirtualRows(
|
||
|
|
rowHeights: Ref<number[]>,
|
||
|
|
viewportHeight: Ref<number>,
|
||
|
|
options?: UseVirtualRowsOptions
|
||
|
|
) {
|
||
|
|
const overscan = options?.overscan ?? DEFAULT_OVERSCAN;
|
||
|
|
const scrollTop = ref(0);
|
||
|
|
|
||
|
|
// Build prefix-sum offsets whenever rowHeights changes
|
||
|
|
const offsets = computed(() => buildOffsets(rowHeights.value));
|
||
|
|
|
||
|
|
// Total scrollable height
|
||
|
|
const totalHeight = computed(() => {
|
||
|
|
const last = offsets.value[offsets.value.length - 1];
|
||
|
|
return last ?? 0;
|
||
|
|
});
|
||
|
|
|
||
|
|
// Compute visible range
|
||
|
|
const range = computed<VirtualRange>(() => {
|
||
|
|
if (rowHeights.value.length === 0) {
|
||
|
|
return { startIndex: 0, endIndex: 0, offsetY: 0 };
|
||
|
|
}
|
||
|
|
|
||
|
|
const st = scrollTop.value;
|
||
|
|
const vp = viewportHeight.value;
|
||
|
|
|
||
|
|
const rawStart = findStartIndex(offsets.value, st);
|
||
|
|
const startIndex = Math.max(0, rawStart - overscan);
|
||
|
|
|
||
|
|
// Find end index: first row whose top is past scrollTop + viewportHeight
|
||
|
|
let endIndex = rawStart;
|
||
|
|
while (
|
||
|
|
endIndex < rowHeights.value.length &&
|
||
|
|
offsets.value[endIndex] < st + vp
|
||
|
|
) {
|
||
|
|
endIndex++;
|
||
|
|
}
|
||
|
|
endIndex = Math.min(rowHeights.value.length, endIndex + overscan);
|
||
|
|
|
||
|
|
return {
|
||
|
|
startIndex,
|
||
|
|
endIndex,
|
||
|
|
offsetY: offsets.value[startIndex],
|
||
|
|
};
|
||
|
|
});
|
||
|
|
|
||
|
|
// Get visible rows with their positions
|
||
|
|
const visibleRows = computed<VirtualRow[]>(() => {
|
||
|
|
const { startIndex, endIndex, offsetY } = range.value;
|
||
|
|
const rows: VirtualRow[] = [];
|
||
|
|
|
||
|
|
for (let i = startIndex; i < endIndex; i++) {
|
||
|
|
rows.push({
|
||
|
|
index: i,
|
||
|
|
offsetY: offsets.value[i],
|
||
|
|
height: rowHeights.value[i],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
return rows;
|
||
|
|
});
|
||
|
|
|
||
|
|
// Scroll handler to be attached to the scroll container
|
||
|
|
const onScroll = (newScrollTop: number) => {
|
||
|
|
scrollTop.value = newScrollTop;
|
||
|
|
};
|
||
|
|
|
||
|
|
// Scroll to a specific row index
|
||
|
|
const scrollToIndex = (index: number) => {
|
||
|
|
if (index < 0 || index >= offsets.value.length) return;
|
||
|
|
scrollTop.value = offsets.value[index];
|
||
|
|
};
|
||
|
|
|
||
|
|
// Scroll to a specific position
|
||
|
|
const scrollTo = (position: number) => {
|
||
|
|
scrollTop.value = Math.max(0, position);
|
||
|
|
};
|
||
|
|
|
||
|
|
return {
|
||
|
|
scrollTop,
|
||
|
|
totalHeight,
|
||
|
|
range,
|
||
|
|
visibleRows,
|
||
|
|
onScroll,
|
||
|
|
scrollToIndex,
|
||
|
|
scrollTo,
|
||
|
|
offsets,
|
||
|
|
};
|
||
|
|
}
|