2022-04-27 20:52:37 +00:00
|
|
|
<template>
|
2022-04-29 10:43:44 +00:00
|
|
|
<div ref="scroller" class="virtual-scroller" @scroll.passive="onScroll">
|
2022-04-27 20:52:37 +00:00
|
|
|
<div :style="{ height: `${totalHeight}px` }">
|
|
|
|
<div :style="{ transform: `translateY(${offsetY}px)`}">
|
|
|
|
<template v-for="item in renderedItems">
|
|
|
|
<slot :item="item"></slot>
|
|
|
|
</template>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
2022-05-12 17:03:06 +00:00
|
|
|
import { computed, onBeforeUnmount, onMounted, ref, toRefs } from 'vue'
|
2022-04-27 20:52:37 +00:00
|
|
|
|
|
|
|
const props = defineProps<{ items: any[], itemHeight: number }>()
|
|
|
|
const { items, itemHeight } = toRefs(props)
|
|
|
|
|
|
|
|
const scroller = ref<HTMLElement>()
|
|
|
|
const scrollerHeight = ref(0)
|
|
|
|
const renderAhead = 5
|
|
|
|
const scrollTop = ref(0)
|
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
const emit = defineEmits(['scrolled-to-end'])
|
|
|
|
|
2022-04-27 20:52:37 +00:00
|
|
|
const totalHeight = computed(() => items.value.length * itemHeight.value)
|
2022-04-28 14:17:22 +00:00
|
|
|
const startPosition = computed(() => Math.max(0, Math.floor(scrollTop.value / itemHeight.value) - renderAhead))
|
2022-04-27 20:52:37 +00:00
|
|
|
const offsetY = computed(() => startPosition.value * itemHeight.value)
|
|
|
|
|
|
|
|
const renderedItems = computed(() => {
|
|
|
|
let count = Math.ceil(scrollerHeight.value / itemHeight.value) + 2 * renderAhead
|
|
|
|
count = Math.min(items.value.length - startPosition.value, count)
|
|
|
|
return items.value.slice(startPosition.value, startPosition.value + count)
|
|
|
|
})
|
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
const onScroll = e => requestAnimationFrame(() => {
|
|
|
|
scrollTop.value = (e.target as HTMLElement).scrollTop
|
|
|
|
|
|
|
|
if (!scroller.value) return
|
|
|
|
|
|
|
|
if (scroller.value.scrollTop + scroller.value.clientHeight + itemHeight.value >= scroller.value.scrollHeight) {
|
|
|
|
emit('scrolled-to-end')
|
|
|
|
}
|
|
|
|
})
|
2022-04-27 20:52:37 +00:00
|
|
|
|
|
|
|
const observer = new ResizeObserver(entries => entries.forEach(el => scrollerHeight.value = el.contentRect.height))
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
observer.observe(scroller.value!)
|
|
|
|
scrollerHeight.value = scroller.value!.offsetHeight
|
|
|
|
})
|
2022-04-29 10:43:44 +00:00
|
|
|
|
2022-04-30 10:36:09 +00:00
|
|
|
onBeforeUnmount(() => observer.unobserve(scroller.value!))
|
2022-04-27 20:52:37 +00:00
|
|
|
</script>
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
.virtual-scroller {
|
|
|
|
will-change: transform;
|
2022-07-05 18:01:33 +00:00
|
|
|
overflow: scroll;
|
|
|
|
|
|
|
|
@supports (scrollbar-gutter: stable) {
|
|
|
|
overflow: auto;
|
|
|
|
scrollbar-gutter: stable;
|
|
|
|
}
|
2022-04-27 20:52:37 +00:00
|
|
|
|
|
|
|
> div {
|
|
|
|
overflow: hidden;
|
|
|
|
will-change: transform;
|
|
|
|
|
|
|
|
> div {
|
|
|
|
will-change: transform;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</style>
|