feat(components): [el-virtualized-list] enable virtual list perf mode (#3547)

* feat(components): [el-virtualized-list] enable virtual list perf mode

- Add perfMode API for both list and grid
- Code refactor

* fix linter issue
This commit is contained in:
jeremywu 2021-09-22 18:29:54 +08:00 committed by GitHub
parent 3ace6f2fcf
commit 2a0ebbc0b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 331 additions and 246 deletions

View File

@ -3,3 +3,4 @@ export { default as DynamicSizeList } from './src/components/dynamic-size-list'
export { default as FixedSizeGrid } from './src/components/fixed-size-grid'
export { default as DynamicSizeGrid } from './src/components/dynamic-size-grid'
export * from './src/types'
export * from './src/props'

View File

@ -8,17 +8,18 @@ import {
onUpdated,
resolveDynamicComponent,
h,
unref,
} from 'vue'
import { hasOwn } from '@vue/shared'
import memo from 'memoize-one'
import { isNumber, isString, $ } from '@element-plus/utils/util'
import { isNumber, isString } from '@element-plus/utils/util'
import isServer from '@element-plus/utils/isServer'
import getScrollBarWidth from '@element-plus/utils/scrollbar-width'
import { useCache } from '../hooks/useCache'
import { virtualizedGridProps } from '../props'
import { getScrollDir, getRTLOffsetType, isRTL } from '../utils'
import {
DefaultGridProps,
AUTO_ALIGNMENT,
BACKWARD,
FORWARD,
@ -30,8 +31,9 @@ import {
RTL_OFFSET_POS_ASC,
} from '../defaults'
import type { VNode, CSSProperties } from 'vue'
import type { CSSProperties, Slot, VNode, VNodeChild } from 'vue'
import type { GridConstructorProps, Alignment } from '../types'
import type { VirtualizedGridProps } from '../props'
const createGrid = ({
name,
@ -49,20 +51,20 @@ const createGrid = ({
initCache,
validateProps,
}: GridConstructorProps<typeof DefaultGridProps>) => {
}: GridConstructorProps<VirtualizedGridProps>) => {
return defineComponent({
name: name ?? 'ElVirtualList',
props: DefaultGridProps,
props: virtualizedGridProps,
emits: [ITEM_RENDER_EVT, SCROLL_EVT],
setup(props, { emit, expose }) {
validateProps(props)
const instance = getCurrentInstance()
const instance = getCurrentInstance()!
const cache = ref(initCache(props, instance))
// refs
// here windowRef and innerRef can be type of HTMLElement
// or user defined component type, depends on the type passed
// by user
const windowRef = ref<HTMLElement>(null)
const windowRef = ref<HTMLElement>()
// innerRef is the actual container element which contains all the elements
const innerRef = ref(null)
const states = ref({
@ -74,10 +76,12 @@ const createGrid = ({
yAxisScrollDir: FORWARD,
})
const getItemStyleCache = useCache()
// computed
const columnsToRender = computed(() => {
const { totalColumn, totalRow, columnCache } = props
const { isScrolling, xAxisScrollDir, scrollLeft } = $(states)
const { isScrolling, xAxisScrollDir, scrollLeft } = unref(states)
if (totalColumn === 0 || totalRow === 0) {
return [0, 0, 0, 0]
@ -86,13 +90,13 @@ const createGrid = ({
const startIndex = getColumnStartIndexForOffset(
props,
scrollLeft,
$(cache)
unref(cache)
)
const stopIndex = getColumnStopIndexForStartIndex(
props,
startIndex,
scrollLeft,
$(cache)
unref(cache)
)
const cacheBackward =
@ -106,7 +110,7 @@ const createGrid = ({
return [
Math.max(0, startIndex - cacheBackward),
Math.max(0, Math.min(totalColumn - 1, stopIndex + cacheForward)),
Math.max(0, Math.min(totalColumn! - 1, stopIndex + cacheForward)),
startIndex,
stopIndex,
]
@ -114,18 +118,22 @@ const createGrid = ({
const rowsToRender = computed(() => {
const { totalColumn, totalRow, rowCache } = props
const { isScrolling, yAxisScrollDir, scrollTop } = $(states)
const { isScrolling, yAxisScrollDir, scrollTop } = unref(states)
if (totalColumn === 0 || totalRow === 0) {
return [0, 0, 0, 0]
}
const startIndex = getRowStartIndexForOffset(props, scrollTop, $(cache))
const startIndex = getRowStartIndexForOffset(
props,
scrollTop,
unref(cache)
)
const stopIndex = getRowStopIndexForStartIndex(
props,
startIndex,
scrollTop,
$(cache)
unref(cache)
)
const cacheBackward =
@ -137,17 +145,17 @@ const createGrid = ({
return [
Math.max(0, startIndex - cacheBackward),
Math.max(0, Math.min(totalRow - 1, stopIndex + cacheForward)),
Math.max(0, Math.min(totalRow! - 1, stopIndex + cacheForward)),
startIndex,
stopIndex,
]
})
const estimatedTotalHeight = computed(() =>
getEstimatedTotalHeight(props, $(cache))
getEstimatedTotalHeight(props, unref(cache))
)
const estimatedTotalWidth = computed(() =>
getEstimatedTotalWidth(props, $(cache))
getEstimatedTotalWidth(props, unref(cache))
)
const windowStyle = computed(() => [
@ -161,17 +169,17 @@ const createGrid = ({
direction: props.direction,
height: isNumber(props.height) ? `${props.height}px` : props.height,
width: isNumber(props.width) ? `${props.width}px` : props.width,
...props.style,
},
props.style,
])
const innerStyle = computed(() => {
const width = `${$(estimatedTotalWidth)}px`
const height = `${$(estimatedTotalHeight)}px`
const width = `${unref(estimatedTotalWidth)}px`
const height = `${unref(estimatedTotalHeight)}px`
return {
height,
pointerEvents: $(states).isScrolling ? 'none' : undefined,
pointerEvents: unref(states).isScrolling ? 'none' : undefined,
width,
}
})
@ -180,15 +188,15 @@ const createGrid = ({
const emitEvents = () => {
const { totalColumn, totalRow } = props
if (totalColumn > 0 && totalRow > 0) {
if (totalColumn! > 0 && totalRow! > 0) {
const [
columnCacheStart,
columnCacheEnd,
columnVisibleStart,
columnVisibleEnd,
] = $(columnsToRender)
] = unref(columnsToRender)
const [rowCacheStart, rowCacheEnd, rowVisibleStart, rowVisibleEnd] =
$(rowsToRender)
unref(rowsToRender)
// emit the render item event with
// [xAxisInvisibleStart, xAxisInvisibleEnd, xAxisVisibleStart, xAxisVisibleEnd]
// [yAxisInvisibleStart, yAxisInvisibleEnd, yAxisVisibleStart, yAxisVisibleEnd]
@ -211,7 +219,7 @@ const createGrid = ({
updateRequested,
xAxisScrollDir,
yAxisScrollDir,
} = $(states)
} = unref(states)
emit(
SCROLL_EVT,
xAxisScrollDir,
@ -232,7 +240,7 @@ const createGrid = ({
scrollWidth,
} = e.currentTarget as HTMLElement
const _states = $(states)
const _states = unref(states)
if (
_states.scrollTop === scrollTop &&
_states.scrollLeft === scrollLeft
@ -271,13 +279,10 @@ const createGrid = ({
emitEvents()
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const getItemStyleCache = memo((_: any, __: any, ___: any) => ({}))
const scrollTo = ({ scrollLeft, scrollTop }) => {
scrollLeft = Math.max(scrollLeft, 0)
scrollTop = Math.max(scrollTop, 0)
const _states = $(states)
const _states = unref(states)
if (
scrollTop === _states.scrollTop &&
scrollLeft === _states.scrollLeft
@ -302,12 +307,12 @@ const createGrid = ({
columnIdx = 0,
alignment: Alignment = AUTO_ALIGNMENT
) => {
const _states = $(states)
columnIdx = Math.max(0, Math.min(columnIdx, props.totalColumn - 1))
rowIndex = Math.max(0, Math.min(rowIndex, props.totalRow - 1))
const _states = unref(states)
columnIdx = Math.max(0, Math.min(columnIdx, props.totalColumn! - 1))
rowIndex = Math.max(0, Math.min(rowIndex, props.totalRow! - 1))
const scrollBarWidth = getScrollBarWidth()
const _cache = $(cache)
const _cache = unref(cache)
const estimatedHeight = getEstimatedTotalHeight(props, _cache)
const estimatedWidth = getEstimatedTotalWidth(props, _cache)
@ -318,7 +323,7 @@ const createGrid = ({
alignment,
_states.scrollLeft,
_cache,
estimatedWidth > props.width ? scrollBarWidth : 0
estimatedWidth > props.width! ? scrollBarWidth : 0
),
scrollTop: getRowOffset(
props,
@ -326,7 +331,7 @@ const createGrid = ({
alignment,
_states.scrollTop,
_cache,
estimatedHeight > props.height ? scrollBarWidth : 0
estimatedHeight > props.height! ? scrollBarWidth : 0
),
})
}
@ -337,7 +342,7 @@ const createGrid = ({
): CSSProperties => {
const { columnWidth, direction, rowHeight } = props
const itemStyleCache = getItemStyleCache(
const itemStyleCache = getItemStyleCache.value(
clearCache && columnWidth,
clearCache && rowHeight,
clearCache && direction
@ -349,8 +354,8 @@ const createGrid = ({
if (hasOwn(itemStyleCache, key)) {
return itemStyleCache[key]
} else {
const [, left] = getColumnPosition(props, columnIndex, $(cache))
const _cache = $(cache)
const [, left] = getColumnPosition(props, columnIndex, unref(cache))
const _cache = unref(cache)
const rtl = isRTL(direction)
const [height, top] = getRowPosition(props, rowIndex, _cache)
@ -376,7 +381,7 @@ const createGrid = ({
states.value.isScrolling = false
nextTick(() => {
getItemStyleCache(-1, null, null)
getItemStyleCache.value(-1, null, null)
})
}
@ -385,8 +390,8 @@ const createGrid = ({
// for SSR
if (isServer) return
const { initScrollLeft, initScrollTop } = props
const windowElement = $(windowRef)
if (windowElement !== null) {
const windowElement = unref(windowRef)
if (windowElement) {
if (isNumber(initScrollLeft)) {
windowElement.scrollLeft = initScrollLeft
}
@ -399,11 +404,11 @@ const createGrid = ({
onUpdated(() => {
const { direction } = props
const { scrollLeft, scrollTop, updateRequested } = $(states)
const { scrollLeft, scrollTop, updateRequested } = unref(states)
if (updateRequested && $(windowRef) !== null) {
const windowElement = $(windowRef)
const windowElement = unref(windowRef)
if (updateRequested && windowElement) {
if (direction === RTL) {
switch (getRTLOffsetType()) {
case RTL_OFFSET_NAG: {
@ -480,12 +485,12 @@ const createGrid = ({
const Container = resolveDynamicComponent(containerElement)
const Inner = resolveDynamicComponent(innerElement)
const children = []
const children: VNodeChild[] = []
if (totalRow > 0 && totalColumn > 0) {
for (let row = rowStart; row <= rowEnd; row++) {
for (let column = columnStart; column <= columnEnd; column++) {
children.push(
$slots.default?.({
($slots.default as Slot)?.({
columnIndex: column,
data,
key: column,

View File

@ -8,18 +8,19 @@ import {
onUpdated,
resolveDynamicComponent,
h,
unref,
} from 'vue'
import { hasOwn } from '@vue/shared'
import memo from 'memoize-one'
import { isNumber, isString, $ } from '@element-plus/utils/util'
import { isNumber, isString } from '@element-plus/utils/util'
import isServer from '@element-plus/utils/isServer'
import { useCache } from '../hooks/useCache'
import useWheel from '../hooks/useWheel'
import Scrollbar from '../components/scrollbar'
import { getScrollDir, isHorizontal, getRTLOffsetType } from '../utils'
import { virtualizedListProps } from '../props'
import {
DefaultListProps,
AUTO_ALIGNMENT,
BACKWARD,
FORWARD,
@ -31,8 +32,9 @@ import {
RTL_OFFSET_POS_DESC,
} from '../defaults'
import type { VNode, CSSProperties } from 'vue'
import type { VNode, CSSProperties, Slot, VNodeChild } from 'vue'
import type { ListConstructorProps, Alignment } from '../types'
import type { VirtualizedListProps } from '../props'
const createList = ({
name,
@ -45,21 +47,23 @@ const createList = ({
initCache,
clearCache,
validateProps,
}: ListConstructorProps<typeof DefaultListProps>) => {
}: ListConstructorProps<VirtualizedListProps>) => {
return defineComponent({
name: name ?? 'ElVirtualList',
props: DefaultListProps,
props: virtualizedListProps,
emits: [ITEM_RENDER_EVT, SCROLL_EVT],
setup(props, { emit, expose }) {
validateProps(props)
const instance = getCurrentInstance()
const instance = getCurrentInstance()!
const dynamicSizeCache = ref(initCache(props, instance))
const getItemStyleCache = useCache()
// refs
// here windowRef and innerRef can be type of HTMLElement
// or user defined component type, depends on the type passed
// by user
const windowRef = ref<HTMLElement>(null)
const innerRef = ref<HTMLElement>(null)
const windowRef = ref<HTMLElement>()
const innerRef = ref<HTMLElement>()
const scrollbarRef = ref(null)
const states = ref({
@ -75,7 +79,7 @@ const createList = ({
// computed
const itemsToRender = computed(() => {
const { total, cache } = props
const { isScrolling, scrollDir, scrollOffset } = $(states)
const { isScrolling, scrollDir, scrollOffset } = unref(states)
if (total === 0) {
return [0, 0, 0, 0]
@ -84,13 +88,13 @@ const createList = ({
const startIndex = getStartIndexForOffset(
props,
scrollOffset,
$(dynamicSizeCache)
unref(dynamicSizeCache)
)
const stopIndex = getStopIndexForStartIndex(
props,
startIndex,
scrollOffset,
$(dynamicSizeCache)
unref(dynamicSizeCache)
)
const cacheBackward =
@ -100,14 +104,14 @@ const createList = ({
return [
Math.max(0, startIndex - cacheBackward),
Math.max(0, Math.min(total - 1, stopIndex + cacheForward)),
Math.max(0, Math.min(total! - 1, stopIndex + cacheForward)),
startIndex,
stopIndex,
]
})
const estimatedTotalSize = computed(() =>
getEstimatedTotalSize(props, $(dynamicSizeCache))
getEstimatedTotalSize(props, unref(dynamicSizeCache))
)
const _isHorizontal = computed(() => isHorizontal(props.layout))
@ -123,16 +127,16 @@ const createList = ({
direction: props.direction,
height: isNumber(props.height) ? `${props.height}px` : props.height,
width: isNumber(props.width) ? `${props.width}px` : props.width,
...props.style,
},
props.style,
])
const innerStyle = computed(() => {
const size = $(estimatedTotalSize)
const horizontal = $(_isHorizontal)
const size = unref(estimatedTotalSize)
const horizontal = unref(_isHorizontal)
return {
height: horizontal ? '100%' : `${size}px`,
pointerEvents: $(states).isScrolling ? 'none' : undefined,
pointerEvents: unref(states).isScrolling ? 'none' : undefined,
width: horizontal ? `${size}px` : '100%',
}
})
@ -151,7 +155,11 @@ const createList = ({
layout: computed(() => props.layout),
},
(offset) => {
scrollbarRef.value.onMouseUp?.()
;(
scrollbarRef.value as any as {
onMouseUp: () => void
}
).onMouseUp?.()
scrollTo(
Math.min(
states.value.scrollOffset + offset,
@ -164,20 +172,20 @@ const createList = ({
const emitEvents = () => {
const { total } = props
if (total > 0) {
if (total! > 0) {
const [cacheStart, cacheEnd, visibleStart, visibleEnd] =
$(itemsToRender)
unref(itemsToRender)
emit(ITEM_RENDER_EVT, cacheStart, cacheEnd, visibleStart, visibleEnd)
}
const { scrollDir, scrollOffset, updateRequested } = $(states)
const { scrollDir, scrollOffset, updateRequested } = unref(states)
emit(SCROLL_EVT, scrollDir, scrollOffset, updateRequested)
}
const scrollVertically = (e: Event) => {
const { clientHeight, scrollHeight, scrollTop } =
e.currentTarget as HTMLElement
const _states = $(states)
const _states = unref(states)
if (_states.scrollOffset === scrollTop) {
return
}
@ -201,7 +209,7 @@ const createList = ({
const scrollHorizontally = (e: Event) => {
const { clientWidth, scrollLeft, scrollWidth } =
e.currentTarget as HTMLElement
const _states = $(states)
const _states = unref(states)
if (_states.scrollOffset === scrollLeft) {
return
@ -245,7 +253,7 @@ const createList = ({
}
const onScroll = (e: Event) => {
$(_isHorizontal) ? scrollHorizontally(e) : scrollVertically(e)
unref(_isHorizontal) ? scrollHorizontally(e) : scrollVertically(e)
emitEvents()
}
@ -262,20 +270,17 @@ const createList = ({
)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const getItemStyleCache = memo((_: any, __: any, ___: any) => ({}))
const scrollTo = (offset: number) => {
offset = Math.max(offset, 0)
if (offset === $(states).scrollOffset) {
if (offset === unref(states).scrollOffset) {
return
}
states.value = {
...$(states),
...unref(states),
scrollOffset: offset,
scrollDir: getScrollDir($(states).scrollOffset, offset),
scrollDir: getScrollDir(unref(states).scrollOffset, offset),
updateRequested: true,
}
@ -286,18 +291,24 @@ const createList = ({
idx: number,
alignment: Alignment = AUTO_ALIGNMENT
) => {
const { scrollOffset } = $(states)
const { scrollOffset } = unref(states)
idx = Math.max(0, Math.min(idx, props.total - 1))
idx = Math.max(0, Math.min(idx, props.total! - 1))
scrollTo(
getOffset(props, idx, alignment, scrollOffset, $(dynamicSizeCache))
getOffset(
props,
idx,
alignment,
scrollOffset,
unref(dynamicSizeCache)
)
)
}
const getItemStyle = (idx: number) => {
const { direction, itemSize, layout } = props
const itemStyleCache = getItemStyleCache(
const itemStyleCache = getItemStyleCache.value(
clearCache && itemSize,
clearCache && layout,
clearCache && direction
@ -307,9 +318,9 @@ const createList = ({
if (hasOwn(itemStyleCache, String(idx))) {
style = itemStyleCache[idx]
} else {
const offset = getItemOffset(props, idx, $(dynamicSizeCache))
const size = getItemSize(props, idx, $(dynamicSizeCache))
const horizontal = $(_isHorizontal)
const offset = getItemOffset(props, idx, unref(dynamicSizeCache))
const size = getItemSize(props, idx, unref(dynamicSizeCache))
const horizontal = unref(_isHorizontal)
const isRtl = direction === RTL
const offsetHorizontal = horizontal ? offset : 0
@ -334,7 +345,7 @@ const createList = ({
states.value.isScrolling = false
nextTick(() => {
getItemStyleCache(-1, null, null)
getItemStyleCache.value(-1, null, null)
})
}
@ -349,9 +360,9 @@ const createList = ({
onMounted(() => {
if (isServer) return
const { initScrollOffset } = props
const windowElement = $(windowRef)
if (isNumber(initScrollOffset) && windowElement !== null) {
if ($(_isHorizontal)) {
const windowElement = unref(windowRef)
if (isNumber(initScrollOffset) && windowElement) {
if (unref(_isHorizontal)) {
windowElement.scrollLeft = initScrollOffset
} else {
windowElement.scrollTop = initScrollOffset
@ -363,11 +374,10 @@ const createList = ({
onUpdated(() => {
const { direction, layout } = props
const { scrollOffset, updateRequested } = $(states)
if (updateRequested && $(windowRef) !== null) {
const windowElement = $(windowRef)
const { scrollOffset, updateRequested } = unref(states)
const windowElement = unref(windowRef)
if (updateRequested && windowElement) {
if (layout === HORIZONTAL) {
if (direction === RTL) {
// TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements.
@ -456,12 +466,12 @@ const createList = ({
const Container = resolveDynamicComponent(containerElement)
const Inner = resolveDynamicComponent(innerElement)
const children = []
const children = [] as VNodeChild[]
if (total > 0) {
for (let i = start; i <= end; i++) {
children.push(
$slots.default?.({
($slots.default as Slot)?.({
data,
key: i,
index: i,

View File

@ -11,12 +11,11 @@ import {
SMART_ALIGNMENT,
START_ALIGNMENT,
} from '../defaults'
import type { DefaultListProps } from '../defaults'
import type { VirtualizedListProps } from '../props'
import type { ListCache, ListItem, ItemSize } from '../types'
import type { ExtractPropTypes } from 'vue'
type Props = ExtractPropTypes<typeof DefaultListProps>
type Props = VirtualizedListProps
const SCOPE = 'ElDynamicSizeList'
const getItemFromCache = (

View File

@ -8,38 +8,33 @@ import {
watch,
h,
withModifiers,
unref,
} from 'vue'
import { BAR_MAP } from '@element-plus/components/scrollbar'
import { on, off } from '@element-plus/utils/dom'
import { rAF, cAF } from '@element-plus/utils/raf'
import isServer from '@element-plus/utils/isServer'
import {
DefaultScrollBarProps,
SCROLLBAR_MIN_SIZE,
HORIZONTAL,
ScrollbarDirKey,
} from '../defaults'
import { SCROLLBAR_MIN_SIZE, HORIZONTAL, ScrollbarDirKey } from '../defaults'
import { virtualizedScrollbarProps } from '../props'
import { renderThumbStyle } from '../utils'
import type { CSSProperties } from 'vue'
type SyntheticMouseEvent = TouchEvent | MouseEvent
const ScrollBar = defineComponent({
name: 'ElVirtualScrollBar',
props: DefaultScrollBarProps,
props: virtualizedScrollbarProps,
emits: ['scroll', 'start-move', 'stop-move'],
setup(props, { emit }) {
const GAP = 4 // top 2 + bottom 2 | left 2 + right 2
// DOM refs
const trackRef = ref(null)
const thumbRef = ref(null)
const trackRef = ref<HTMLElement>()
const thumbRef = ref<HTMLElement>()
// local variables
let frameHandle: null | number = null
let onselectstartStore = null
let onselectstartStore: null | typeof document.onselectstart = null
// data
const state = reactive({
@ -49,10 +44,9 @@ const ScrollBar = defineComponent({
const bar = computed(() => BAR_MAP[props.layout])
const trackSize = computed(() => props.clientSize - GAP)
const trackSize = computed(() => props.clientSize! - GAP)
const trackStyle = computed<CSSProperties>(() => ({
display: props.visible ? null : 'none',
position: 'absolute',
width: HORIZONTAL === props.layout ? `${trackSize.value}px` : '6px',
height: HORIZONTAL === props.layout ? '6px' : `${trackSize.value}px`,
@ -60,21 +54,24 @@ const ScrollBar = defineComponent({
right: '2px',
bottom: '2px',
borderRadius: '4px',
...(props.visible ? {} : { display: 'none' }),
}))
const thumbSize = computed(() => {
if (props.ratio >= 100) {
const ratio = props.ratio!
const clientSize = props.clientSize!
if (ratio >= 100) {
return Number.POSITIVE_INFINITY
}
if (props.ratio >= 50) {
return (props.ratio * props.clientSize) / 100
if (ratio >= 50) {
return (ratio * clientSize) / 100
}
const SCROLLBAR_MAX_SIZE = props.clientSize / 3
const SCROLLBAR_MAX_SIZE = clientSize / 3
return Math.floor(
Math.min(
Math.max(props.ratio * props.clientSize, SCROLLBAR_MIN_SIZE),
Math.max(ratio * clientSize, SCROLLBAR_MIN_SIZE),
SCROLLBAR_MAX_SIZE
)
)
@ -104,14 +101,16 @@ const ScrollBar = defineComponent({
})
const totalSteps = computed(() =>
Math.floor(props.clientSize - thumbSize.value - GAP)
Math.floor(props.clientSize! - thumbSize.value - GAP)
)
const attachEvents = () => {
on(window, 'mousemove', onMouseMove)
on(window, 'mouseup', onMouseUp)
const thumbEl = thumbRef.value
const thumbEl = unref(thumbRef)
if (!thumbEl) return
onselectstartStore = document.onselectstart
document.onselectstart = () => false
@ -127,20 +126,25 @@ const ScrollBar = defineComponent({
document.onselectstart = onselectstartStore
onselectstartStore = null
const thumbEl = thumbRef.value
const thumbEl = unref(thumbRef)
if (!thumbEl) return
off(thumbEl, 'touchmove', onMouseMove)
off(thumbEl, 'touchend', onMouseUp)
}
const onThumbMouseDown = (e: SyntheticMouseEvent) => {
const onThumbMouseDown = (e: Event) => {
e.stopImmediatePropagation()
if (e.ctrlKey || [1, 2].includes((e as MouseEvent).button)) {
if (
(e as KeyboardEvent).ctrlKey ||
[1, 2].includes((e as MouseEvent).button)
) {
return
}
state.isDragging = true
state[bar.value.axis] =
e.currentTarget[bar.value.offset] -
e.currentTarget![bar.value.offset] -
(e[bar.value.client] -
(e.currentTarget as HTMLElement).getBoundingClientRect()[
bar.value.direction
@ -157,14 +161,15 @@ const ScrollBar = defineComponent({
detachEvents()
}
const onMouseMove = (e: SyntheticMouseEvent) => {
const onMouseMove = (e: Event) => {
const { isDragging } = state
if (!isDragging) return
if (!thumbRef.value || !trackRef.value) return
const prevPage = state[bar.value.axis]
if (!prevPage) return
cAF(frameHandle)
cAF(frameHandle!)
// using the current track's offset top/left - the current pointer's clientY/clientX
// to get the relative position of the pointer to the track.
const offset =
@ -212,7 +217,7 @@ const ScrollBar = defineComponent({
(e.target as HTMLElement).getBoundingClientRect()[bar.value.direction] -
e[bar.value.client]
)
const thumbHalf = thumbRef.value[bar.value.offset] / 2
const thumbHalf = thumbRef.value![bar.value.offset] / 2
const distance = offset - thumbHalf
state.traveled = Math.max(0, Math.min(distance, totalSteps.value))
@ -235,19 +240,19 @@ const ScrollBar = defineComponent({
* formula 2:
* traveled = (v * clientSize) / (clientSize / totalSteps) --> (v * clientSize) * (totalSteps / clientSize) --> v * totalSteps
*/
state.traveled = Math.ceil(v * totalSteps.value)
state.traveled = Math.ceil(v! * totalSteps.value)
}
)
onMounted(() => {
if (isServer) return
on(trackRef.value, 'touchstart', onScrollbarTouchStart)
on(thumbRef.value, 'touchstart', onThumbMouseDown)
on(trackRef.value!, 'touchstart', onScrollbarTouchStart)
on(thumbRef.value!, 'touchstart', onThumbMouseDown)
})
onBeforeUnmount(() => {
off(trackRef.value, 'touchstart', onScrollbarTouchStart)
off(trackRef.value!, 'touchstart', onScrollbarTouchStart)
detachEvents()
})
@ -269,7 +274,7 @@ const ScrollBar = defineComponent({
style: thumbStyle.value,
onMousedown: onThumbMouseDown,
},
null
[]
)
)
}

View File

@ -1,9 +1,3 @@
import { isNumber } from '@element-plus/utils/util'
import type { PropType } from 'vue'
import type { Direction, LayoutDirection, ItemSize } from './types'
import type { StyleValue } from '@element-plus/utils/types'
export const DEFAULT_DYNAMIC_LIST_ITEM_SIZE = 50
export const ITEM_RENDER_EVT = 'item-rendered'
@ -28,114 +22,6 @@ export const RTL_OFFSET_NAG = 'negative'
export const RTL_OFFSET_POS_ASC = 'positive-ascending'
export const RTL_OFFSET_POS_DESC = 'positive-descending'
export const DefaultListProps = {
cache: {
type: Number as PropType<number>,
default: 2,
},
className: {
type: String as PropType<string>,
default: '',
},
containerElement: {
type: [String, Object],
default: 'div',
},
data: {
type: [Array] as PropType<any[]>,
default: () => [],
},
// even though we can use Enums here but due to the issue for some
// intelligence plugin distinguishes string enums to strings
// we had some code that uses enums but the plugins were quite
// about that and reporting issues because of it.
direction: {
type: String as PropType<Direction>,
default: 'ltr',
validator: (val: Direction) => {
return val === LTR || val === RTL
},
},
estimatedItemSize: {
type: [Number] as PropType<number>,
},
height: {
type: [String, Number] as PropType<string | number>,
required: true,
},
layout: {
type: String as PropType<LayoutDirection>,
default: VERTICAL,
},
initScrollOffset: {
type: Number,
default: 0,
},
innerElement: {
type: [String, Object],
default: 'div',
},
total: {
type: Number as PropType<number>,
required: true,
},
itemSize: {
type: [Number, Function] as PropType<number | ItemSize>,
required: true,
},
style: {
type: [Object, String, Array] as PropType<StyleValue>,
default: () => ({}),
},
useIsScrolling: {
type: Boolean,
default: false,
},
width: {
type: [Number, String] as PropType<string | number>,
required: true,
},
}
export const DefaultGridProps = {
className: DefaultListProps.className,
columnCache: DefaultListProps.cache,
columnWidth: DefaultListProps.itemSize,
containerElement: DefaultListProps.containerElement,
data: DefaultListProps.data,
direction: DefaultListProps.direction,
estimatedColumnWidth: DefaultListProps.estimatedItemSize,
estimatedRowHeight: DefaultListProps.estimatedItemSize,
height: {
...DefaultListProps.height,
validator: (val: number) => isNumber(val),
},
initScrollLeft: DefaultListProps.initScrollOffset,
initScrollTop: DefaultListProps.initScrollOffset,
innerElement: DefaultListProps.innerElement,
rowCache: DefaultListProps.cache,
rowHeight: DefaultListProps.itemSize,
style: DefaultListProps.style,
useIsScrolling: DefaultListProps.useIsScrolling,
width: {
...DefaultListProps.width,
validator: (val: number) => {
return isNumber(val)
},
},
totalColumn: DefaultListProps.total,
totalRow: DefaultListProps.total,
}
export const DefaultScrollBarProps = {
layout: DefaultListProps.layout,
total: Number,
ratio: Number,
clientSize: Number,
scrollFrom: Number,
visible: Boolean,
}
export const PageKey = {
[HORIZONTAL]: 'pageX',
[VERTICAL]: 'pageY',

View File

@ -0,0 +1,19 @@
import { computed, getCurrentInstance } from 'vue'
import memo from 'lodash/memoize'
import memoOne from 'memoize-one'
import type { VirtualizedProps } from '../props'
export const useCache = () => {
const vm = getCurrentInstance()!
const props = vm.proxy!.$props as VirtualizedProps
return computed(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _getItemStyleCache = (_: any, __: any, ___: any) => ({})
return props.perfMode
? memo(_getItemStyleCache)
: memoOne(_getItemStyleCache)
})
}

View File

@ -0,0 +1,160 @@
import { isNumber } from '@element-plus/utils/util'
import { LTR, RTL, VERTICAL } from './defaults'
import type { ExtractPropTypes, PropType } from 'vue'
import type { StyleValue } from '@element-plus/utils/types'
import type { Direction, LayoutDirection, ItemSize } from './types'
const itemSize = {
type: [Number, Function] as PropType<number | ItemSize>,
required: true,
}
const estimatedItemSize = {
type: [Number] as PropType<number>,
}
const cache = {
type: Number as PropType<number>,
default: 2,
}
const direction = {
type: String as PropType<Direction>,
default: 'ltr',
validator: (val: Direction) => {
return val === LTR || val === RTL
},
}
const initScrollOffset = {
type: Number,
default: 0,
}
const total = {
type: Number as PropType<number>,
required: true,
}
const layout = {
type: String as PropType<LayoutDirection>,
default: VERTICAL,
}
export const virtualizedProps = {
className: {
type: String as PropType<string>,
default: '',
},
containerElement: {
type: [String, Object],
default: 'div',
},
data: {
type: [Array] as PropType<any[]>,
default: () => [],
},
/**
* @description controls the horizontal direction.
*/
direction,
height: {
type: [String, Number] as PropType<string | number>,
required: true,
validator: isNumber,
},
innerElement: {
type: [String, Object],
default: 'div',
},
style: {
type: [Object, String, Array] as PropType<StyleValue>,
},
useIsScrolling: {
type: Boolean,
default: false,
},
width: {
type: [Number, String] as PropType<string | number>,
required: true,
validator: isNumber,
},
perfMode: {
type: Boolean,
default: true,
},
}
export const virtualizedListProps = {
/**
* @description describes how many items should be pre rendered to the head
* and the tail of the window
*/
cache,
estimatedItemSize,
/**
* @description controls the list's orientation
*/
layout,
initScrollOffset,
/**
* @description describes the total number of the list.
*/
total,
itemSize,
...virtualizedProps,
}
export const virtualizedGridProps = {
columnCache: cache,
columnWidth: itemSize,
estimatedColumnWidth: estimatedItemSize,
estimatedRowHeight: estimatedItemSize,
initScrollLeft: initScrollOffset,
initScrollTop: initScrollOffset,
rowCache: cache,
rowHeight: itemSize,
totalColumn: total,
totalRow: total,
...virtualizedProps,
}
export const virtualizedScrollbarProps = {
layout,
total,
ratio: {
type: Number,
required: true,
},
clientSize: {
type: Number,
required: true,
},
scrollFrom: {
type: Number,
required: true,
},
visible: Boolean,
}
export type VirtualizedProps = ExtractPropTypes<typeof virtualizedProps>
export type VirtualizedListProps = ExtractPropTypes<typeof virtualizedListProps>
export type VirtualizedGridProps = ExtractPropTypes<typeof virtualizedGridProps>
export type VirtualizedScrollbarProps = ExtractPropTypes<
typeof virtualizedScrollbarProps
>