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.
158 lines
3.8 KiB
158 lines
3.8 KiB
/** |
|
* 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 } = range.value; |
|
const rows: VirtualRow[] = []; |
|
|
|
// Safety check: ensure indices are valid |
|
if (startIndex < 0 || endIndex < startIndex) { |
|
return rows; |
|
} |
|
|
|
for (let i = startIndex; i < endIndex; i++) { |
|
const offsetY = offsets.value[i]; |
|
const height = rowHeights.value[i]; |
|
if (offsetY === undefined || height === undefined) { |
|
continue; // Skip invalid entries |
|
} |
|
rows.push({ |
|
index: i, |
|
offsetY, |
|
height, |
|
}); |
|
} |
|
|
|
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, |
|
}; |
|
}
|
|
|