UNPKG

element-plus

Version:

A Component Library for Vue 3

622 lines (557 loc) 18.2 kB
import { computed, defineComponent, getCurrentInstance, ref, nextTick, onMounted, onUpdated, resolveDynamicComponent, h, unref, } from 'vue' import { hasOwn } from '@vue/shared' import { isNumber, isString } from '@element-plus/utils/util' import isServer from '@element-plus/utils/isServer' import getScrollBarWidth from '@element-plus/utils/scrollbar-width' import Scrollbar from '../components/scrollbar' import { useGridWheel } from '../hooks/use-grid-wheel' import { useCache } from '../hooks/use-cache' import { virtualizedGridProps } from '../props' import { getScrollDir, getRTLOffsetType, isRTL } from '../utils' import { 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 { CSSProperties, VNode, VNodeChild } from 'vue' import type { StyleValue } from '@element-plus/utils/types' import type { GridConstructorProps, Alignment, ScrollbarExpose } from '../types' import type { VirtualizedGridProps } from '../props' const createGrid = ({ name, clearCache, getColumnPosition, getColumnStartIndexForOffset, getColumnStopIndexForStartIndex, getEstimatedTotalHeight, getEstimatedTotalWidth, getColumnOffset, getRowOffset, getRowPosition, getRowStartIndexForOffset, getRowStopIndexForStartIndex, initCache, validateProps, }: GridConstructorProps<VirtualizedGridProps>) => { return defineComponent({ name: name ?? 'ElVirtualList', props: virtualizedGridProps, emits: [ITEM_RENDER_EVT, SCROLL_EVT], setup(props, { emit, expose, slots }) { 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>() const hScrollbar = ref<ScrollbarExpose>() const vScrollbar = ref<ScrollbarExpose>() // 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, }) const getItemStyleCache = useCache() // computed const parsedHeight = computed(() => parseInt(`${props.height}`, 10)) const parsedWidth = computed(() => parseInt(`${props.width}`, 10)) const columnsToRender = computed(() => { const { totalColumn, totalRow, columnCache } = props const { isScrolling, xAxisScrollDir, scrollLeft } = unref(states) if (totalColumn === 0 || totalRow === 0) { return [0, 0, 0, 0] } const startIndex = getColumnStartIndexForOffset( props, scrollLeft, unref(cache) ) const stopIndex = getColumnStopIndexForStartIndex( props, startIndex, scrollLeft, unref(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 } = unref(states) if (totalColumn === 0 || totalRow === 0) { return [0, 0, 0, 0] } const startIndex = getRowStartIndexForOffset( props, scrollTop, unref(cache) ) const stopIndex = getRowStopIndexForStartIndex( props, startIndex, scrollTop, unref(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, unref(cache)) ) const estimatedTotalWidth = computed(() => getEstimatedTotalWidth(props, unref(cache)) ) const windowStyle = computed<StyleValue>(() => [ { position: 'relative', overflow: 'hidden', 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 = `${unref(estimatedTotalWidth)}px` const height = `${unref(estimatedTotalHeight)}px` return { height, pointerEvents: unref(states).isScrolling ? 'none' : undefined, width, } }) // methods const emitEvents = () => { const { totalColumn, totalRow } = props if (totalColumn! > 0 && totalRow! > 0) { const [ columnCacheStart, columnCacheEnd, columnVisibleStart, columnVisibleEnd, ] = unref(columnsToRender) const [rowCacheStart, rowCacheEnd, rowVisibleStart, rowVisibleEnd] = unref(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, } = unref(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 = unref(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() } const onVerticalScroll = (distance: number, totalSteps: number) => { const height = unref(parsedHeight) const offset = ((estimatedTotalHeight.value - height) / totalSteps) * distance scrollTo({ scrollTop: Math.min(estimatedTotalHeight.value - height, offset), }) } const onHorizontalScroll = (distance: number, totalSteps: number) => { const width = unref(parsedWidth) const offset = ((estimatedTotalWidth.value - width) / totalSteps) * distance scrollTo({ scrollLeft: Math.min(estimatedTotalWidth.value - width, offset), }) } const { onWheel } = useGridWheel( { atXStartEdge: computed(() => states.value.scrollLeft <= 0), atXEndEdge: computed( () => states.value.scrollLeft >= estimatedTotalWidth.value ), atYStartEdge: computed(() => states.value.scrollTop <= 0), atYEndEdge: computed( () => states.value.scrollTop >= estimatedTotalHeight.value ), }, (x: number, y: number) => { hScrollbar.value?.onMouseUp?.() hScrollbar.value?.onMouseUp?.() const width = unref(parsedWidth) const height = unref(parsedHeight) scrollTo({ scrollLeft: Math.min( states.value.scrollLeft + x, estimatedTotalWidth.value - width ), scrollTop: Math.min( states.value.scrollTop + y, estimatedTotalHeight.value - height ), }) } ) const scrollTo = ({ scrollLeft = states.value.scrollLeft, scrollTop = states.value.scrollTop, }) => { scrollLeft = Math.max(scrollLeft, 0) scrollTop = Math.max(scrollTop, 0) const _states = unref(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 = 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 = unref(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.value( 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, unref(cache)) const _cache = unref(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.value(-1, null, null) }) } // life cycles onMounted(() => { // for SSR if (isServer) return const { initScrollLeft, initScrollTop } = props const windowElement = unref(windowRef) if (windowElement) { if (isNumber(initScrollLeft)) { windowElement.scrollLeft = initScrollLeft } if (isNumber(initScrollTop)) { windowElement.scrollTop = initScrollTop } } emitEvents() }) onUpdated(() => { const { direction } = props const { scrollLeft, scrollTop, updateRequested } = unref(states) const windowElement = unref(windowRef) if (updateRequested && windowElement) { 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) } }) expose({ windowRef, innerRef, getItemStyleCache, scrollTo, scrollToItem, states, }) // rendering part const renderScrollbars = () => { const { totalColumn, totalRow } = props const width = unref(parsedWidth) const height = unref(parsedHeight) const estimatedWidth = unref(estimatedTotalWidth) const estimatedHeight = unref(estimatedTotalHeight) const { scrollLeft, scrollTop } = unref(states) const horizontalScrollbar = h(Scrollbar, { ref: hScrollbar, clientSize: width, layout: 'horizontal', onScroll: onHorizontalScroll, ratio: (width * 100) / estimatedWidth, scrollFrom: scrollLeft / (estimatedWidth - width), total: totalRow, visible: true, }) const verticalScrollbar = h(Scrollbar, { ref: vScrollbar, clientSize: height, layout: 'vertical', onScroll: onVerticalScroll, ratio: (height * 100) / estimatedHeight, scrollFrom: scrollTop / (estimatedHeight - height), total: totalColumn, visible: true, }) return { horizontalScrollbar, verticalScrollbar, } } const renderItems = () => { const [columnStart, columnEnd] = unref(columnsToRender) const [rowStart, rowEnd] = unref(rowsToRender) const { data, totalColumn, totalRow, useIsScrolling } = props 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?.({ columnIndex: column, data, key: column, isScrolling: useIsScrolling ? unref(states).isScrolling : undefined, style: getItemStyle(row, column), rowIndex: row, }) ) } } } return children } const renderInner = () => { const Inner = resolveDynamicComponent(props.innerElement) as VNode const children = renderItems() return [ h( Inner, { style: unref(innerStyle), ref: innerRef, }, !isString(Inner) ? { default: () => children, } : children ), ] } const renderWindow = () => { const Container = resolveDynamicComponent( props.containerElement ) as VNode const { horizontalScrollbar, verticalScrollbar } = renderScrollbars() const Inner = renderInner() return h( 'div', { key: 0, class: 'el-vg__wrapper', }, [ h( Container, { class: props.className, style: unref(windowStyle), onScroll, onWheel, ref: windowRef, }, !isString(Container) ? { default: () => Inner } : Inner ), horizontalScrollbar, verticalScrollbar, ] ) } return renderWindow }, }) } export default createGrid