element-plus
Version:
A Component Library for Vue3.0
502 lines (437 loc) • 14.2 kB
text/typescript
import {
computed,
defineComponent,
getCurrentInstance,
ref,
nextTick,
onMounted,
onUpdated,
resolveDynamicComponent,
h,
} from 'vue'
import { hasOwn } from '@vue/shared'
import memo from 'lodash/memoize'
import { isNumber, isString, $ } from '@element-plus/utils/util'
import isServer from '@element-plus/utils/isServer'
import getScrollBarWidth from '@element-plus/utils/scrollbar-width'
import { getScrollDir, getRTLOffsetType, isRTL } from '../utils'
import {
DefaultGridProps,
AUTO_ALIGNMENT,
BACKWARD,
FORWARD,
RTL,
ITEM_RENDER_EVT,
SCROLL_EVT,
RTL_OFFSET_NAG,
RTL_OFFSET_POS_DESC,
RTL_OFFSET_POS_ASC,
} from '../defaults'
import type { VNode, CSSProperties } from 'vue'
import type { GridConstructorProps, Alignment } from '../types'
const createGrid = ({
name,
clearCache,
getColumnPosition,
getColumnStartIndexForOffset,
getColumnStopIndexForStartIndex,
getEstimatedTotalHeight,
getEstimatedTotalWidth,
getColumnOffset,
getRowOffset,
getRowPosition,
getRowStartIndexForOffset,
getRowStopIndexForStartIndex,
initCache,
validateProps,
}: GridConstructorProps<typeof DefaultGridProps>) => {
return defineComponent({
name: name ?? 'ElVirtualList',
props: DefaultGridProps,
emits: [ITEM_RENDER_EVT, SCROLL_EVT],
setup(props, { emit, expose }) {
validateProps(props)
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)
// innerRef is the actual container element which contains all the elements
const innerRef = ref(null)
const states = ref({
isScrolling: false,
scrollLeft: isNumber(props.initScrollLeft) ? props.initScrollLeft : 0,
scrollTop: isNumber(props.initScrollTop) ? props.initScrollTop : 0,
updateRequested: false,
xAxisScrollDir: FORWARD,
yAxisScrollDir: FORWARD,
})
// computed
const columnsToRender = computed(() => {
const { totalColumn, totalRow, columnCache } = props
const { isScrolling, xAxisScrollDir, scrollLeft } = $(states)
if (totalColumn === 0 || totalRow === 0) {
return [0, 0, 0, 0]
}
const startIndex = getColumnStartIndexForOffset(
props,
scrollLeft,
$(cache),
)
const stopIndex = getColumnStopIndexForStartIndex(
props,
startIndex,
scrollLeft,
$(cache),
)
const cacheBackward =
!isScrolling || xAxisScrollDir === BACKWARD
? Math.max(1, columnCache)
: 1
const cacheForward =
!isScrolling || xAxisScrollDir === FORWARD
? Math.max(1, columnCache)
: 1
return [
Math.max(0, startIndex - cacheBackward),
Math.max(0, Math.min(totalColumn - 1, stopIndex + cacheForward)),
startIndex,
stopIndex,
]
})
const rowsToRender = computed(() => {
const { totalColumn, totalRow, rowCache } = props
const { isScrolling, yAxisScrollDir, scrollTop } = $(states)
if (totalColumn === 0 || totalRow === 0) {
return [0, 0, 0, 0]
}
const startIndex = getRowStartIndexForOffset(
props,
scrollTop,
$(cache),
)
const stopIndex = getRowStopIndexForStartIndex(
props,
startIndex,
scrollTop,
$(cache),
)
const cacheBackward =
!isScrolling || yAxisScrollDir === BACKWARD
? Math.max(1, rowCache)
: 1
const cacheForward =
!isScrolling || yAxisScrollDir === FORWARD
? Math.max(1, rowCache)
: 1
return [
Math.max(0, startIndex - cacheBackward),
Math.max(0, Math.min(totalRow - 1, stopIndex + cacheForward)),
startIndex,
stopIndex,
]
})
const estimatedTotalHeight = computed(() => getEstimatedTotalHeight(props, $(cache)))
const estimatedTotalWidth = computed(() => getEstimatedTotalWidth(props, $(cache)))
const windowStyle = computed(() => ([
{
position: 'relative',
overflow: 'auto',
WebkitOverflowScrolling: 'touch',
willChange: 'transform',
}, {
direction: props.direction,
height: isNumber(props.height) ? `${props.height}px` : props.height,
width: isNumber(props.width) ? `${props.width}px` : props.width,
...props.style,
},
]))
const innerStyle = computed(() => {
const width = `${$(estimatedTotalWidth)}px`
const height = `${$(estimatedTotalHeight)}px`
return {
height,
pointerEvents: $(states).isScrolling ? 'none' : undefined,
width,
}
})
// methods
const emitEvents = () => {
const { totalColumn, totalRow } = props
if (totalColumn > 0 && totalRow > 0) {
const [columnCacheStart, columnCacheEnd, columnVisibleStart, columnVisibleEnd] = $(columnsToRender)
const [rowCacheStart, rowCacheEnd, rowVisibleStart, rowVisibleEnd] = $(rowsToRender)
// emit the render item event with
// [xAxisInvisibleStart, xAxisInvisibleEnd, xAxisVisibleStart, xAxisVisibleEnd]
// [yAxisInvisibleStart, yAxisInvisibleEnd, yAxisVisibleStart, yAxisVisibleEnd]
emit(
ITEM_RENDER_EVT,
columnCacheStart,
columnCacheEnd,
rowCacheStart,
rowCacheEnd,
columnVisibleStart,
columnVisibleEnd,
rowVisibleStart,
rowVisibleEnd,
)
}
const { scrollLeft, scrollTop, updateRequested, xAxisScrollDir, yAxisScrollDir } = $(states)
emit(
SCROLL_EVT,
xAxisScrollDir,
scrollLeft,
yAxisScrollDir,
scrollTop,
updateRequested,
)
}
const onScroll = (e: Event) => {
const {
clientHeight,
clientWidth,
scrollHeight,
scrollLeft,
scrollTop,
scrollWidth,
} = e.currentTarget as HTMLElement
const _states = $(states)
if (_states.scrollTop === scrollTop && _states.scrollLeft === scrollLeft) {
return
}
let _scrollLeft = scrollLeft
if (isRTL(props.direction)) {
switch (getRTLOffsetType()) {
case RTL_OFFSET_NAG:
_scrollLeft = -scrollLeft
break
case RTL_OFFSET_POS_DESC:
_scrollLeft = scrollWidth - clientWidth - scrollLeft
break
}
}
states.value = {
..._states,
isScrolling: true,
scrollLeft: _scrollLeft,
scrollTop: Math.max(0, Math.min(scrollTop, scrollHeight - clientHeight)),
updateRequested: false,
xAxisScrollDir: getScrollDir(_states.scrollLeft, _scrollLeft),
yAxisScrollDir: getScrollDir(_states.scrollTop, scrollTop),
}
nextTick(resetIsScrolling)
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)
if (scrollTop === _states.scrollTop && scrollLeft === _states.scrollLeft) {
return
}
states.value = {
..._states,
xAxisScrollDir: getScrollDir(_states.scrollLeft, scrollLeft),
yAxisScrollDir: getScrollDir(_states.scrollTop, scrollTop),
scrollLeft,
scrollTop,
updateRequested: true,
}
nextTick(resetIsScrolling)
}
const scrollToItem = (
rowIndex = 0,
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 scrollBarWidth = getScrollBarWidth()
const _cache = $(cache)
const estimatedHeight = getEstimatedTotalHeight(props, _cache)
const estimatedWidth = getEstimatedTotalWidth(props, _cache)
scrollTo({
scrollLeft: getColumnOffset(
props,
columnIdx,
alignment,
_states.scrollLeft,
_cache,
estimatedWidth > props.width ? scrollBarWidth : 0,
),
scrollTop: getRowOffset(
props,
rowIndex,
alignment,
_states.scrollTop,
_cache,
estimatedHeight > props.height ? scrollBarWidth : 0,
),
})
}
const getItemStyle = (rowIndex: number, columnIndex: number): CSSProperties => {
const { columnWidth, direction, rowHeight } = props
const itemStyleCache = getItemStyleCache(
clearCache && columnWidth,
clearCache && rowHeight,
clearCache && direction,
)
// since there was no need to introduce an nested array into cache object
// we use row,column to construct the key for indexing the map.
const key = `${rowIndex},${columnIndex}`
if (hasOwn(itemStyleCache, key)) {
return itemStyleCache[key]
} else {
const [, left] = getColumnPosition(props, columnIndex, $(cache))
const _cache = $(cache)
const rtl = isRTL(direction)
const [height, top] = getRowPosition(props, rowIndex, _cache)
const [width] = getColumnPosition(props, columnIndex, _cache)
itemStyleCache[key] = {
position: 'absolute',
left: rtl ? undefined : `${left}px`,
right: rtl ? `${left}px` : undefined,
top: `${top}px`,
height: `${height}px`,
width: `${width}px`,
}
return itemStyleCache[key]
}
}
// TODO: debounce setting is scrolling.
const resetIsScrolling = () => {
// timer = null
states.value.isScrolling = false
nextTick(() => {
getItemStyleCache(-1, null, null)
})
}
// life cycles
onMounted(() => {
// for SSR
if (isServer) return
const { initScrollLeft, initScrollTop } = props
const windowElement = $(windowRef)
if (windowElement !== null) {
if (isNumber(initScrollLeft)) {
windowElement.scrollLeft = initScrollLeft
}
if (isNumber(initScrollTop)) {
windowElement.scrollTop = initScrollTop
}
}
emitEvents()
})
onUpdated(() => {
const { direction } = props
const { scrollLeft, scrollTop, updateRequested } = $(states)
if (updateRequested && $(windowRef) !== null) {
const windowElement = $(windowRef)
if (direction === RTL) {
switch (getRTLOffsetType()) {
case RTL_OFFSET_NAG: {
windowElement.scrollLeft = -scrollLeft
break
}
case RTL_OFFSET_POS_ASC: {
windowElement.scrollLeft = scrollLeft
break
}
default: {
const { clientWidth, scrollWidth } = windowElement
windowElement.scrollLeft = scrollWidth - clientWidth - scrollLeft
break
}
}
} else {
windowElement.scrollLeft = Math.max(0, scrollLeft)
}
windowElement.scrollTop = Math.max(0, scrollTop)
}
})
const api = {
windowStyle,
windowRef,
columnsToRender,
innerRef,
innerStyle,
states,
rowsToRender,
getItemStyle,
onScroll,
scrollTo,
scrollToItem,
}
expose({
windowRef,
innerRef,
getItemStyleCache,
scrollTo,
scrollToItem,
states,
})
return api
},
render(ctx: any) {
const {
$slots,
className,
containerElement,
columnsToRender,
data,
getItemStyle,
innerElement,
innerStyle,
rowsToRender,
onScroll,
states,
useIsScrolling,
windowStyle,
totalColumn,
totalRow,
} = ctx
const [columnStart, columnEnd] = columnsToRender
const [rowStart, rowEnd] = rowsToRender
const Container = resolveDynamicComponent(containerElement)
const Inner = resolveDynamicComponent(innerElement)
const children = []
if (totalRow > 0 && totalColumn > 0) {
for (let row = rowStart; row <= rowEnd; row ++) {
for (let column = columnStart; column <= columnEnd; column ++) {
children.push(
$slots.default?.({
columnIndex: column,
data,
key: column,
isScrolling: useIsScrolling ? states.isScrolling : undefined,
style: getItemStyle(row, column),
rowIndex: row,
}),
)
}
}
}
const InnerNode = [h(Inner as VNode, {
style: innerStyle,
ref: 'innerRef',
}, !isString(Inner) ? {
default: () => children,
} : children)]
return h(Container as VNode, {
class: className,
style: windowStyle,
onScroll,
ref: 'windowRef',
}, !isString(Container) ? { default: () => InnerNode } : InnerNode)
},
})
}
export default createGrid