feat: use home-grown virtual scroller

This commit is contained in:
Phan An 2022-04-27 22:52:37 +02:00
parent 98384b56c6
commit 9cf7a09cde
No known key found for this signature in database
GPG key ID: A81E4477F0BB6FDC
5 changed files with 78 additions and 39 deletions

View file

@ -32,7 +32,6 @@
"slugify": "^1.0.2", "slugify": "^1.0.2",
"vue": "^3.2.32", "vue": "^3.2.32",
"vue-global-events": "^2.1.1", "vue-global-events": "^2.1.1",
"vue-virtual-scroller": "^2.0.0-alpha.1",
"vuequery": "~2.1.1", "vuequery": "~2.1.1",
"youtube-player": "^3.0.4" "youtube-player": "^3.0.4"
}, },

View file

@ -46,15 +46,9 @@
<span class="play"></span> <span class="play"></span>
</div> </div>
<RecycleScroller <VirtualScroller v-slot="{ item }" :item-height="35" :items="songProxies">
class="scroller" <SongListItem :item="item" :columns="mergedConfig.columns" :key="item.song.id"/>
:items="songProxies" </VirtualScroller>
:item-size="35"
key-field="id"
v-slot="{ item }"
>
<SongListItem :item="item" :columns="mergedConfig.columns"/>
</RecycleScroller>
</div> </div>
</template> </template>
@ -66,6 +60,7 @@ export default {
<script lang="ts" setup> <script lang="ts" setup>
import isMobile from 'ismobilejs' import isMobile from 'ismobilejs'
import { import {
computed, computed,
defineAsyncComponent, defineAsyncComponent,
@ -76,8 +71,7 @@ import {
toRefs, toRefs,
watch watch
} from 'vue' } from 'vue'
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
import { $, eventBus, orderBy, startDragging, arrayify } from '@/utils' import { $, eventBus, orderBy, startDragging, arrayify } from '@/utils'
import { queueStore } from '@/stores' import { queueStore } from '@/stores'
@ -96,6 +90,7 @@ interface SongRow {
} }
} }
const VirtualScroller = defineAsyncComponent(() => import('@/components/ui/VirtualScroller.vue'))
const SongListItem = defineAsyncComponent(() => import('@/components/song/SongListItem.vue')) const SongListItem = defineAsyncComponent(() => import('@/components/song/SongListItem.vue'))
const props = withDefaults( const props = withDefaults(
@ -105,6 +100,11 @@ const props = withDefaults(
const { items, type, config } = toRefs(props) const { items, type, config } = toRefs(props)
onMounted(() => {
generateSongProxies()
render()
})
const lastSelectedRow = ref<SongRow>() const lastSelectedRow = ref<SongRow>()
const sortFields = ref<SortField[]>([]) const sortFields = ref<SortField[]>([])
const sortOrder = ref<SortOrder>('None') const sortOrder = ref<SortOrder>('None')
@ -132,7 +132,6 @@ const generateSongProxies = () => {
const selectedSongIds = selectedSongs.value.map(song => song.id) const selectedSongIds = selectedSongs.value.map(song => song.id)
return items.value.map(song => ({ return items.value.map(song => ({
id: song.id,
song, song,
selected: selectedSongIds.includes(song.id) selected: selectedSongIds.includes(song.id)
})) }))
@ -306,7 +305,7 @@ const openContextMenu = async (rowVm: SongRow, event: MouseEvent) => {
const getAllSongsWithSort = () => songProxies.value.map(proxy => proxy.song) const getAllSongsWithSort = () => songProxies.value.map(proxy => proxy.song)
onMounted(() => items.value && render()) // onMounted(() => items.value && render())
defineExpose({ defineExpose({
rowClicked, rowClicked,
@ -324,6 +323,8 @@ defineExpose({
.song-list-wrap { .song-list-wrap {
position: relative; position: relative;
padding: 0 !important; padding: 0 !important;
display: flex;
flex-direction: column;
.song-list-header { .song-list-header {
background: var(--color-bg-secondary); background: var(--color-bg-secondary);

View file

@ -0,0 +1,64 @@
<template>
<div ref="scroller" class="virtual-scroller" @scroll="onScroll">
<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>
import { computed, onMounted, ref, toRefs } from 'vue'
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)
const totalHeight = computed(() => items.value.length * itemHeight.value)
const startPosition = computed(() => {
const position = Math.floor(scrollTop.value / itemHeight.value) - renderAhead
return Math.max(0, position)
})
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)
})
const onScroll = e => requestAnimationFrame(() => scrollTop.value = (e.target as HTMLElement).scrollTop)
const observer = new ResizeObserver(entries => entries.forEach(el => scrollerHeight.value = el.contentRect.height))
onMounted(() => {
observer.observe(scroller.value!)
scrollerHeight.value = scroller.value!.offsetHeight
})
</script>
<style lang="scss" scoped>
.virtual-scroller {
overflow: auto;
will-change: transform;
> div {
overflow: hidden;
will-change: transform;
> div {
will-change: transform;
}
}
}
</style>

View file

@ -336,7 +336,6 @@ declare module 'koel/types/ui' {
} }
interface SongProxy { interface SongProxy {
id: string,
song: Song song: Song
selected: boolean selected: boolean
} }

View file

@ -6555,11 +6555,6 @@ minimist@^1.2.5:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
mitt@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mitt/-/mitt-2.1.0.tgz#f740577c23176c6205b121b2973514eade1b2230"
integrity sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==
mkdirp@^0.5.5, mkdirp@~0.5.1: mkdirp@^0.5.5, mkdirp@~0.5.1:
version "0.5.6" version "0.5.6"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
@ -9607,16 +9602,6 @@ vue-loader@^16.2.0:
hash-sum "^2.0.0" hash-sum "^2.0.0"
loader-utils "^2.0.0" loader-utils "^2.0.0"
vue-observe-visibility@^2.0.0-alpha.1:
version "2.0.0-alpha.1"
resolved "https://registry.yarnpkg.com/vue-observe-visibility/-/vue-observe-visibility-2.0.0-alpha.1.tgz#1e4eda7b12562161d58984b7e0dea676d83bdb13"
integrity sha512-flFbp/gs9pZniXR6fans8smv1kDScJ8RS7rEpMjhVabiKeq7Qz3D9+eGsypncjfIyyU84saU88XZ0zjbD6Gq/g==
vue-resize@^2.0.0-alpha.1:
version "2.0.0-alpha.1"
resolved "https://registry.yarnpkg.com/vue-resize/-/vue-resize-2.0.0-alpha.1.tgz#43eeb79e74febe932b9b20c5c57e0ebc14e2df3a"
integrity sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==
vue-style-loader@^4.1.3: vue-style-loader@^4.1.3:
version "4.1.3" version "4.1.3"
resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.3.tgz#6d55863a51fa757ab24e89d9371465072aa7bc35" resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.3.tgz#6d55863a51fa757ab24e89d9371465072aa7bc35"
@ -9640,15 +9625,6 @@ vue-test-helpers@^2.0.0:
dependencies: dependencies:
np "^2.18.3" np "^2.18.3"
vue-virtual-scroller@^2.0.0-alpha.1:
version "2.0.0-alpha.1"
resolved "https://registry.yarnpkg.com/vue-virtual-scroller/-/vue-virtual-scroller-2.0.0-alpha.1.tgz#5b5410105b8e60ca57bbd5f2faf5ad1d8108d046"
integrity sha512-Mn5w3Qe06t7c3Imm2RHD43RACab1CCWplpdgzq+/FWJcpQtcGKd5vDep8i+nIwFtzFLsWAqEK0RzM7KrfAcBng==
dependencies:
mitt "^2.1.0"
vue-observe-visibility "^2.0.0-alpha.1"
vue-resize "^2.0.0-alpha.1"
vue@^3.2.32: vue@^3.2.32:
version "3.2.32" version "3.2.32"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.32.tgz#a09840e237384c673f421ff7280c4469714f2ac0" resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.32.tgz#a09840e237384c673f421ff7280c4469714f2ac0"