@indielayer/ui
Version:
Indielayer UI Components with Tailwind CSS build for Vue 3
150 lines (115 loc) • 3.71 kB
text/typescript
import { getCurrentInstance, onBeforeUnmount, ref, watch, type Ref } from 'vue'
import { assert } from '../../utils/assert'
import type { DynamicRowHeight } from './types'
export const DATA_ATTRIBUTE_LIST_INDEX = 'data-virtual-index'
export function useDynamicRowHeight({
defaultRowHeight,
key,
}: {
defaultRowHeight: number;
key?: Ref<string | number | undefined> | string | number;
}): DynamicRowHeight {
const map = ref<Map<number, number>>(new Map())
// Revision counter to trigger reactivity when map changes
const revision = ref(0)
// Handle reactive key changes
if (key !== undefined && typeof key === 'object' && 'value' in key) {
watch(key as Ref<string | number | undefined>, () => {
map.value = new Map()
revision.value++
})
}
const getAverageRowHeight = () => {
// Access revision to track dependency
revision.value
let totalHeight = 0
map.value.forEach((height) => {
totalHeight += height
})
if (totalHeight === 0) {
return defaultRowHeight
}
return totalHeight / map.value.size
}
const getRowHeight = (index: number) => {
// Access revision to track dependency
revision.value
const measuredHeight = map.value.get(index)
if (measuredHeight !== undefined) {
return measuredHeight
}
// Don't cache default height here - let ResizeObserver do the measuring
// This prevents stale default heights from blocking updates
return defaultRowHeight
}
const setRowHeight = (index: number, size: number) => {
if (map.value.get(index) === size) {
return
}
// Create a new Map to trigger reactivity
const newMap = new Map(map.value)
newMap.set(index, size)
map.value = newMap
revision.value++
}
const resizeObserverCallback = (entries: ResizeObserverEntry[]) => {
if (entries.length === 0) {
return
}
// Batch updates to avoid triggering multiple recalculations
let hasChanges = false
const updates: Array<{ index: number; height: number; }> = []
entries.forEach((entry) => {
const { borderBoxSize, target } = entry
const attribute = target.getAttribute(DATA_ATTRIBUTE_LIST_INDEX)
assert(
attribute !== null,
`Invalid ${DATA_ATTRIBUTE_LIST_INDEX} attribute value`,
)
const index = parseInt(attribute)
const { blockSize: height } = borderBoxSize[0]
if (!height) {
// Ignore heights that have not yet been measured (e.g. <img> elements that have not yet loaded)
return
}
// Check if height actually changed before updating
const currentHeight = map.value.get(index)
if (currentHeight !== height) {
updates.push({ index, height })
hasChanges = true
}
})
// Apply all updates at once
if (hasChanges) {
const newMap = new Map(map.value)
updates.forEach(({ index, height }) => {
newMap.set(index, height)
})
map.value = newMap
revision.value++
}
}
const resizeObserver = new ResizeObserver(resizeObserverCallback)
function cleanup() {
resizeObserver.disconnect()
}
if (getCurrentInstance()) {
onBeforeUnmount(cleanup)
}
const observeRowElements = (elements: Element[] | NodeListOf<Element>) => {
const elementsArray = Array.isArray(elements)
? elements
: Array.from(elements)
elementsArray.forEach((element) => resizeObserver.observe(element))
return () => {
elementsArray.forEach((element) => resizeObserver.unobserve(element))
}
}
return {
getAverageRowHeight,
getRowHeight,
setRowHeight,
observeRowElements,
cleanup,
}
}