mirror of
https://github.com/element-plus/element-plus.git
synced 2025-01-24 11:05:17 +08:00
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:
parent
3ace6f2fcf
commit
2a0ebbc0b9
@ -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'
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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 = (
|
||||
|
@ -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
|
||||
[]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -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',
|
||||
|
19
packages/components/virtual-list/src/hooks/useCache.ts
Normal file
19
packages/components/virtual-list/src/hooks/useCache.ts
Normal 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)
|
||||
})
|
||||
}
|
160
packages/components/virtual-list/src/props.ts
Normal file
160
packages/components/virtual-list/src/props.ts
Normal 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
|
||||
>
|
Loading…
Reference in New Issue
Block a user