forked from vitest-dev/vitest
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(useVirtualList): add useVirtualList function (vitest-dev#710)
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
- Loading branch information
1 parent
7c4fefc
commit bde7f1a
Showing
8 changed files
with
286 additions
and
0 deletions.
There are no files selected for viewing
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
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
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { defineComponent, h } from 'vue-demi' | ||
import { useVirtualList, UseVirtualListOptions } from '@vueuse/core' | ||
|
||
export interface UseVirtualListProps { | ||
/** | ||
* data of scrollable list | ||
* | ||
* @default [] | ||
*/ | ||
list: Array<any> | ||
/** | ||
* useVirtualList's options | ||
* | ||
* @default {} | ||
*/ | ||
options: UseVirtualListOptions | ||
/** | ||
* virtualList's height | ||
* | ||
* @default 300px | ||
*/ | ||
height: string | ||
} | ||
|
||
export const UseVirtualList = defineComponent<UseVirtualListProps>({ | ||
name: 'UseVirtualList', | ||
props: [ | ||
'list', | ||
'options', | ||
'height', | ||
] as unknown as undefined, | ||
setup(props, { slots }) { | ||
const { list, containerProps, wrapperProps } = useVirtualList( | ||
props.list, | ||
props.options, | ||
) | ||
containerProps.style.height = props.height || '300px' | ||
return () => h('div', | ||
{ ...containerProps }, | ||
[ | ||
h('div', | ||
{ ...wrapperProps.value }, | ||
list.value.map((item: any) => h('div', | ||
{ style: { overFlow: 'hidden', height: item.height } }, | ||
slots.default ? slots.default(item) : 'Please set content!', | ||
)), | ||
), | ||
], | ||
) | ||
}, | ||
}) |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
<script setup lang="ts"> | ||
import { ref, Ref } from 'vue-demi' | ||
import { useVirtualList } from '.' | ||
const index: Ref = ref(0) | ||
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList( | ||
Array.from(Array(99999).keys()), | ||
{ | ||
itemHeight: i => (i % 2 === 0 ? 42 + 8 : 84 + 8), | ||
overscan: 10, | ||
}, | ||
) | ||
const handleScrollTo = () => { | ||
scrollTo(index.value) | ||
} | ||
</script> | ||
|
||
<template> | ||
<div> | ||
<div class="mb-4 flex gap-2"> | ||
<input v-model="index" placeholder="Index" type="number" /> | ||
<button type="button" @click="handleScrollTo"> | ||
Go | ||
</button> | ||
</div> | ||
<div v-bind="containerProps" class="h-300px overflow-auto p-2 bg-gray-500/5 rounded"> | ||
<div v-bind="wrapperProps"> | ||
<div | ||
v-for="ele in list" | ||
:key="ele.index" | ||
class="border border-$c-divider mb-2" | ||
:style="{ | ||
height: ele.index % 2 === 0 ? '42px' : '84px', | ||
display: 'flex', | ||
justifyContent: 'center', | ||
alignItems: 'center', | ||
}" | ||
> | ||
Row {{ ele.data }} | ||
<span opacity="70" m="l-1">({{ ele.index % 2 === 0 ? 'small' : 'large' }})</span> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</template> |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
--- | ||
category: Component | ||
--- | ||
|
||
# useVirtualList | ||
|
||
Composable virtual list. It allows you to display a large list of items while only rendering visible ones on the screen. | ||
|
||
## Usage | ||
|
||
```typescript | ||
import { useVirtualList } from '@vueuse/core' | ||
|
||
const { list, containerProps, wrapperProps } = useVirtualList( | ||
Array.from(Array(99999).keys()), | ||
{ | ||
itemHeight: 22, | ||
}, | ||
) | ||
``` | ||
|
||
```html | ||
<template> | ||
<div v-bind="containerProps" style="height: 300px"> | ||
<div v-bind="wrapperProps"> | ||
<div v-for="item in list" :key="item.index"> | ||
Row: {{ item.data }} | ||
</div> | ||
</div> | ||
</div> | ||
</template> | ||
``` | ||
|
||
## Component | ||
|
||
```html | ||
<UseVirtualList :list="list" :options="options" height="300px"> | ||
<template #="props"> | ||
<!-- you can get current item of list here --> | ||
Row {{ props.data }} | ||
</template> | ||
</UseVirtualList> | ||
``` |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import { watch, Ref, ref, computed } from 'vue-demi' | ||
import { useElementSize } from '../useElementSize' | ||
|
||
export interface UseVirtualListOptions { | ||
/** | ||
* item height, accept a pixel value or a function that returns the height | ||
* | ||
* @default 0 | ||
*/ | ||
itemHeight: number | ((index: number) => number) | ||
/** | ||
* the extra buffer items outside of the view area | ||
* | ||
* @default 5 | ||
*/ | ||
overscan?: number | ||
} | ||
|
||
export function useVirtualList <T = any>(list: T[], options: UseVirtualListOptions) { | ||
const containerRef: Ref = ref<HTMLElement | null>() | ||
const size = useElementSize(containerRef) | ||
const currentList: Ref = ref([]) | ||
|
||
const state: Ref = ref({ start: 0, end: 10 }) | ||
const { itemHeight, overscan = 5 } = options | ||
|
||
if (!itemHeight) | ||
console.warn('please enter a valid itemHeight') | ||
|
||
const getViewCapacity = (containerHeight: number) => { | ||
if (typeof itemHeight === 'number') | ||
return Math.ceil(containerHeight / itemHeight) | ||
|
||
const { start = 0 } = state.value | ||
let sum = 0 | ||
let capacity = 0 | ||
for (let i = start; i < list.length; i++) { | ||
const height = (itemHeight as (index: number) => number)(i) | ||
sum += height | ||
if (sum >= containerHeight) { | ||
capacity = i | ||
break | ||
} | ||
} | ||
return capacity - start | ||
} | ||
|
||
const getOffset = (scrollTop: number) => { | ||
if (typeof itemHeight === 'number') | ||
return Math.floor(scrollTop / itemHeight) + 1 | ||
|
||
let sum = 0 | ||
let offset = 0 | ||
for (let i = 0; i < list.length; i++) { | ||
const height = (itemHeight as (index: number) => number)(i) | ||
sum += height | ||
if (sum >= scrollTop) { | ||
offset = i | ||
break | ||
} | ||
} | ||
return offset + 1 | ||
} | ||
|
||
const calculateRange = () => { | ||
const element = containerRef.value | ||
if (element) { | ||
const offset = getOffset(element.scrollTop) | ||
const viewCapacity = getViewCapacity(element.clientHeight) | ||
|
||
const from = offset - overscan | ||
const to = offset + viewCapacity + overscan | ||
state.value = { | ||
start: from < 0 ? 0 : from, | ||
end: to > list.length ? list.length : to, | ||
} | ||
currentList.value = list.slice(state.value.start, state.value.end).map((ele, index) => ({ | ||
data: ele, | ||
index: index + state.value.start, | ||
})) | ||
} | ||
} | ||
|
||
watch([size.width, size.height], () => { | ||
calculateRange() | ||
}) | ||
|
||
const totalHeight = computed(() => { | ||
if (typeof itemHeight === 'number') | ||
return list.length * itemHeight | ||
|
||
return list.reduce((sum, _, index) => sum + itemHeight(index), 0) | ||
}) | ||
|
||
const getDistanceTop = (index: number) => { | ||
if (typeof itemHeight === 'number') { | ||
const height = index * itemHeight | ||
return height | ||
} | ||
const height = list.slice(0, index).reduce((sum, _, i) => sum + itemHeight(i), 0) | ||
return height | ||
} | ||
|
||
const scrollTo = (index: number) => { | ||
if (containerRef.value) { | ||
containerRef.value.scrollTop = getDistanceTop(index) | ||
calculateRange() | ||
} | ||
} | ||
|
||
const offsetTop = computed(() => getDistanceTop(state.value.start)) | ||
const wrapperProps = computed(() => { | ||
return { | ||
style: { | ||
width: '100%', | ||
height: `${totalHeight.value - offsetTop.value}px`, | ||
marginTop: `${offsetTop.value}px`, | ||
}, | ||
} | ||
}) | ||
|
||
const containerStyle: Partial<CSSStyleDeclaration> = { overflowY: 'auto' } | ||
|
||
return { | ||
list: currentList, | ||
scrollTo, | ||
containerProps: { | ||
ref: containerRef, | ||
onScroll: () => { | ||
calculateRange() | ||
}, | ||
style: containerStyle, | ||
}, | ||
wrapperProps, | ||
} | ||
} |
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