Last active
June 8, 2023 10:12
-
-
Save NateScarlet/519ec66700e07b73bbf5398fca9ef90c to your computer and use it in GitHub Desktop.
vue composable for implement virtual scroll on x or/and y axis
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import type { StyleValue } from 'vue'; | |
import { computed, reactive, ref } from 'vue'; | |
export default function useVirtualScroll({ | |
x, | |
y, | |
}: { | |
x?: { | |
totalCount: () => number; | |
itemWidth: () => number; | |
maxWidth?: () => number; | |
}; | |
y?: { | |
totalCount: () => number; | |
itemHeight: () => number; | |
maxHeight?: () => number; | |
}; | |
}) { | |
const container = ref<HTMLElement>(); | |
const state = reactive({ | |
scrollTop: 0, | |
scrollLeft: 0, | |
}); | |
const topPlaceholder = ref<HTMLElement>(); | |
const visibleItemCountY = () => | |
y | |
? Math.ceil( | |
(y.maxHeight?.() ?? document.body.clientHeight) / y.itemHeight() | |
) | |
: 0; | |
const offsetTop = () => | |
(topPlaceholder.value?.clientTop ?? 0) - (container.value?.clientTop ?? 0); | |
const topIndex = () => | |
y | |
? Math.max( | |
0, | |
Math.floor((state.scrollTop - offsetTop()) / y.itemHeight()) | |
) | |
: 0; | |
const leftPlaceholder = ref<HTMLElement>(); | |
const visibleItemCountX = () => | |
x | |
? Math.ceil((x.maxWidth?.() ?? document.body.clientWidth) / x.itemWidth()) | |
: 0; | |
const offsetLeft = () => | |
(leftPlaceholder.value?.clientLeft ?? 0) - | |
(container.value?.clientLeft ?? 0); | |
const leftIndex = () => | |
x | |
? Math.max( | |
0, | |
Math.floor((state.scrollLeft - offsetLeft()) / x.itemWidth()) | |
) | |
: 0; | |
function yData<T>(value: (index: number) => T) { | |
if (!y) { | |
return []; | |
} | |
const s = [] as number[]; | |
for ( | |
let index = topIndex(); | |
index < y.totalCount() && s.length < visibleItemCountY(); | |
index += 1 | |
) { | |
s.push(index); | |
} | |
return s.map((index) => { | |
return { | |
key: index, | |
index, | |
value: value(index), | |
itemAttrs: { | |
style: { | |
height: `${y.itemHeight()}px`, | |
overflowY: 'hidden', | |
} as StyleValue, | |
}, | |
}; | |
}); | |
} | |
function xData<T>(value: (index: number) => T) { | |
if (!x) { | |
return []; | |
} | |
const s = [] as number[]; | |
for ( | |
let index = leftIndex(); | |
index < x.totalCount() && s.length < visibleItemCountX(); | |
index += 1 | |
) { | |
s.push(index); | |
} | |
return s.map((index) => { | |
return { | |
key: index, | |
index, | |
value: value(index), | |
itemAttrs: { | |
style: { | |
width: `${x.itemWidth()}px`, | |
overflowX: 'hidden', | |
} as StyleValue, | |
}, | |
}; | |
}); | |
} | |
const containerAttrs = computed(() => { | |
return { | |
style: { | |
...(x ? { overflowX: 'auto' } : {}), | |
...(y ? { overflowY: 'auto' } : {}), | |
} as StyleValue, | |
onScrollPassive: (e: UIEvent) => { | |
const el = e.target as HTMLElement; | |
state.scrollTop = el.scrollTop; | |
state.scrollLeft = el.scrollLeft; | |
container.value = el; | |
}, | |
}; | |
}); | |
const topPlaceholderHeight = () => | |
y ? topIndex() * y.itemHeight() - offsetTop() : 0; | |
const topPlaceholderAttrs = computed(() => { | |
const height = topPlaceholderHeight(); | |
return { | |
ref: topPlaceholder, | |
style: | |
height > 0 | |
? { | |
height: `${height}px`, | |
} | |
: { display: 'none' }, | |
}; | |
}); | |
const bottomPlaceholderHeight = () => | |
y | |
? y.itemHeight() * (y.totalCount() - visibleItemCountY() - topIndex()) | |
: 0; | |
const bottomPlaceholderAttrs = computed(() => { | |
const height = bottomPlaceholderHeight(); | |
return { | |
style: | |
height > 0 | |
? { | |
height: `${height}px`, | |
} | |
: { display: 'none' }, | |
}; | |
}); | |
const leftPlaceholderWidth = () => | |
x ? leftIndex() * x.itemWidth() - offsetLeft() : 0; | |
const leftPlaceholderAttrs = computed(() => { | |
const width = leftPlaceholderWidth(); | |
return { | |
ref: leftPlaceholder, | |
style: | |
width > 0 | |
? { | |
width: `${width}px`, | |
} | |
: { display: 'none' }, | |
}; | |
}); | |
const rightPlaceholderWidth = () => | |
x | |
? x.itemWidth() * (x.totalCount() - visibleItemCountX() - leftIndex()) | |
: 0; | |
const rightPlaceholderAttrs = computed(() => { | |
const width = rightPlaceholderWidth(); | |
return { | |
style: | |
width > 0 | |
? { | |
width: `${width}px`, | |
} | |
: { display: 'none' }, | |
}; | |
}); | |
return reactive({ | |
x: xData, | |
y: yData, | |
containerAttrs, | |
topPlaceholder, | |
leftPlaceholder, | |
topPlaceholderAttrs, | |
rightPlaceholderAttrs, | |
bottomPlaceholderAttrs, | |
leftPlaceholderAttrs, | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment