mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-12-02 19:28:14 +08:00
feat(components): [el-virtualized-grid] fulfillment (#3612)
* feat(components): [el-virtualized-grid] fulfillment - Add scrollabr and scroll handler to v-grid * Address comments and code perfection * Update render function * fix linter * address comments
This commit is contained in:
parent
9a5a1d5ea6
commit
50a1b6f892
@ -1,4 +1,4 @@
|
|||||||
import { nextTick } from 'vue'
|
import { nextTick, unref } from 'vue'
|
||||||
import makeMount from '@element-plus/test-utils/make-mount'
|
import makeMount from '@element-plus/test-utils/make-mount'
|
||||||
import makeScroll from '@element-plus/test-utils/make-scroll'
|
import makeScroll from '@element-plus/test-utils/make-scroll'
|
||||||
import setupMock from '../setup-mock'
|
import setupMock from '../setup-mock'
|
||||||
@ -96,12 +96,12 @@ describe('<fixed-size-grid />', () => {
|
|||||||
expect(wrapper.findAll(ITEM_SELECTOR)).toHaveLength(36)
|
expect(wrapper.findAll(ITEM_SELECTOR)).toHaveLength(36)
|
||||||
const gridRef = wrapper.vm.$refs.gridRef as GridRef
|
const gridRef = wrapper.vm.$refs.gridRef as GridRef
|
||||||
|
|
||||||
makeScroll(gridRef.windowRef, 'scrollTop', 100)
|
makeScroll(unref(gridRef.windowRef), 'scrollTop', 100)
|
||||||
await nextTick()
|
await nextTick()
|
||||||
// 8 x 5 grid
|
// 8 x 5 grid
|
||||||
expect(wrapper.findAll(ITEM_SELECTOR)).toHaveLength(40)
|
expect(wrapper.findAll(ITEM_SELECTOR)).toHaveLength(40)
|
||||||
|
|
||||||
makeScroll(gridRef.windowRef, 'scrollLeft', 100)
|
makeScroll(unref(gridRef.windowRef), 'scrollLeft', 100)
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(wrapper.findAll(ITEM_SELECTOR)).toHaveLength(64)
|
expect(wrapper.findAll(ITEM_SELECTOR)).toHaveLength(64)
|
||||||
})
|
})
|
||||||
@ -112,11 +112,11 @@ describe('<fixed-size-grid />', () => {
|
|||||||
expect(wrapper.findAll(ITEM_SELECTOR)).toHaveLength(36)
|
expect(wrapper.findAll(ITEM_SELECTOR)).toHaveLength(36)
|
||||||
|
|
||||||
const gridRef = wrapper.vm.$refs.gridRef as GridRef
|
const gridRef = wrapper.vm.$refs.gridRef as GridRef
|
||||||
makeScroll(gridRef.windowRef, 'scrollTop', 0)
|
makeScroll(unref(gridRef.windowRef), 'scrollTop', 0)
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(wrapper.findAll(ITEM_SELECTOR)).toHaveLength(36)
|
expect(wrapper.findAll(ITEM_SELECTOR)).toHaveLength(36)
|
||||||
|
|
||||||
makeScroll(gridRef.windowRef, 'scrollLeft', 0)
|
makeScroll(unref(gridRef.windowRef), 'scrollLeft', 0)
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(wrapper.findAll(ITEM_SELECTOR)).toHaveLength(36)
|
expect(wrapper.findAll(ITEM_SELECTOR)).toHaveLength(36)
|
||||||
})
|
})
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { nextTick } from 'vue'
|
import { nextTick, unref } from 'vue'
|
||||||
import makeMount from '@element-plus/test-utils/make-mount'
|
import makeMount from '@element-plus/test-utils/make-mount'
|
||||||
import makeScroll from '@element-plus/test-utils/make-scroll'
|
import makeScroll from '@element-plus/test-utils/make-scroll'
|
||||||
import setupMock from '../setup-mock'
|
import setupMock from '../setup-mock'
|
||||||
@ -71,8 +71,8 @@ describe('<fixed-size-grid />', () => {
|
|||||||
expect(wrapper.findAll(ITEM_SELECTOR)).toHaveLength(24)
|
expect(wrapper.findAll(ITEM_SELECTOR)).toHaveLength(24)
|
||||||
|
|
||||||
const gridRef = wrapper.vm.$refs.gridRef as GridRef
|
const gridRef = wrapper.vm.$refs.gridRef as GridRef
|
||||||
expect(gridRef.innerRef.style.height).toBe('2500px')
|
expect(unref(gridRef.innerRef).style.height).toBe('2500px')
|
||||||
expect(gridRef.innerRef.style.width).toBe('5000px')
|
expect(unref(gridRef.innerRef).style.width).toBe('5000px')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render zero row zero column', async () => {
|
it('should render zero row zero column', async () => {
|
||||||
@ -96,11 +96,11 @@ describe('<fixed-size-grid />', () => {
|
|||||||
|
|
||||||
const gridRef = wrapper.vm.$refs.gridRef as GridRef
|
const gridRef = wrapper.vm.$refs.gridRef as GridRef
|
||||||
|
|
||||||
makeScroll(gridRef.windowRef, 'scrollTop', 100)
|
makeScroll(unref(gridRef.windowRef), 'scrollTop', 100)
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(wrapper.findAll(ITEM_SELECTOR)).toHaveLength(21)
|
expect(wrapper.findAll(ITEM_SELECTOR)).toHaveLength(21)
|
||||||
|
|
||||||
makeScroll(gridRef.windowRef, 'scrollLeft', 100)
|
makeScroll(unref(gridRef.windowRef), 'scrollLeft', 100)
|
||||||
await nextTick()
|
await nextTick()
|
||||||
// 5 (backward cache 1 + visible 2 + forward cache 2)
|
// 5 (backward cache 1 + visible 2 + forward cache 2)
|
||||||
// * 7 (backward cache 1 + visible 4 + forward cache 2)
|
// * 7 (backward cache 1 + visible 4 + forward cache 2)
|
||||||
@ -114,11 +114,11 @@ describe('<fixed-size-grid />', () => {
|
|||||||
expect(wrapper.findAll(ITEM_SELECTOR)).toHaveLength(24)
|
expect(wrapper.findAll(ITEM_SELECTOR)).toHaveLength(24)
|
||||||
|
|
||||||
const gridRef = wrapper.vm.$refs.gridRef as GridRef
|
const gridRef = wrapper.vm.$refs.gridRef as GridRef
|
||||||
makeScroll(gridRef.windowRef, 'scrollTop', 0)
|
makeScroll(unref(gridRef.windowRef), 'scrollTop', 0)
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(wrapper.findAll(ITEM_SELECTOR)).toHaveLength(24)
|
expect(wrapper.findAll(ITEM_SELECTOR)).toHaveLength(24)
|
||||||
|
|
||||||
makeScroll(gridRef.windowRef, 'scrollLeft', 0)
|
makeScroll(unref(gridRef.windowRef), 'scrollLeft', 0)
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(wrapper.findAll(ITEM_SELECTOR)).toHaveLength(24)
|
expect(wrapper.findAll(ITEM_SELECTOR)).toHaveLength(24)
|
||||||
})
|
})
|
||||||
|
@ -6,7 +6,7 @@ import { ScrollbarDirKey } from '../src/defaults'
|
|||||||
describe('virtual scrollbar', () => {
|
describe('virtual scrollbar', () => {
|
||||||
async function testInlineStyle(layout = 'vertical') {
|
async function testInlineStyle(layout = 'vertical') {
|
||||||
const wrapper = mount({
|
const wrapper = mount({
|
||||||
template: `<scrollbar visible layout="${layout}"></scrollbar>`,
|
template: `<scrollbar visible layout="${layout}" :total="100" :ratio="25" :client-size="100" :scroll-from="20"></scrollbar>`,
|
||||||
components: {
|
components: {
|
||||||
Scrollbar,
|
Scrollbar,
|
||||||
},
|
},
|
||||||
|
@ -16,7 +16,9 @@ import { isNumber, isString } from '@element-plus/utils/util'
|
|||||||
import isServer from '@element-plus/utils/isServer'
|
import isServer from '@element-plus/utils/isServer'
|
||||||
import getScrollBarWidth from '@element-plus/utils/scrollbar-width'
|
import getScrollBarWidth from '@element-plus/utils/scrollbar-width'
|
||||||
|
|
||||||
import { useCache } from '../hooks/useCache'
|
import Scrollbar from '../components/scrollbar'
|
||||||
|
import { useGridWheel } from '../hooks/use-grid-wheel'
|
||||||
|
import { useCache } from '../hooks/use-cache'
|
||||||
import { virtualizedGridProps } from '../props'
|
import { virtualizedGridProps } from '../props'
|
||||||
import { getScrollDir, getRTLOffsetType, isRTL } from '../utils'
|
import { getScrollDir, getRTLOffsetType, isRTL } from '../utils'
|
||||||
import {
|
import {
|
||||||
@ -31,8 +33,9 @@ import {
|
|||||||
RTL_OFFSET_POS_ASC,
|
RTL_OFFSET_POS_ASC,
|
||||||
} from '../defaults'
|
} from '../defaults'
|
||||||
|
|
||||||
import type { CSSProperties, Slot, VNode, VNodeChild } from 'vue'
|
import type { CSSProperties, VNode, VNodeChild } from 'vue'
|
||||||
import type { GridConstructorProps, Alignment } from '../types'
|
import type { StyleValue } from '@element-plus/utils/types'
|
||||||
|
import type { GridConstructorProps, Alignment, ScrollbarExpose } from '../types'
|
||||||
import type { VirtualizedGridProps } from '../props'
|
import type { VirtualizedGridProps } from '../props'
|
||||||
|
|
||||||
const createGrid = ({
|
const createGrid = ({
|
||||||
@ -56,7 +59,7 @@ const createGrid = ({
|
|||||||
name: name ?? 'ElVirtualList',
|
name: name ?? 'ElVirtualList',
|
||||||
props: virtualizedGridProps,
|
props: virtualizedGridProps,
|
||||||
emits: [ITEM_RENDER_EVT, SCROLL_EVT],
|
emits: [ITEM_RENDER_EVT, SCROLL_EVT],
|
||||||
setup(props, { emit, expose }) {
|
setup(props, { emit, expose, slots }) {
|
||||||
validateProps(props)
|
validateProps(props)
|
||||||
const instance = getCurrentInstance()!
|
const instance = getCurrentInstance()!
|
||||||
const cache = ref(initCache(props, instance))
|
const cache = ref(initCache(props, instance))
|
||||||
@ -65,6 +68,8 @@ const createGrid = ({
|
|||||||
// or user defined component type, depends on the type passed
|
// or user defined component type, depends on the type passed
|
||||||
// by user
|
// by user
|
||||||
const windowRef = ref<HTMLElement>()
|
const windowRef = ref<HTMLElement>()
|
||||||
|
const hScrollbar = ref<ScrollbarExpose>()
|
||||||
|
const vScrollbar = ref<ScrollbarExpose>()
|
||||||
// innerRef is the actual container element which contains all the elements
|
// innerRef is the actual container element which contains all the elements
|
||||||
const innerRef = ref(null)
|
const innerRef = ref(null)
|
||||||
const states = ref({
|
const states = ref({
|
||||||
@ -79,6 +84,8 @@ const createGrid = ({
|
|||||||
const getItemStyleCache = useCache()
|
const getItemStyleCache = useCache()
|
||||||
|
|
||||||
// computed
|
// computed
|
||||||
|
const parsedHeight = computed(() => parseInt(`${props.height}`, 10))
|
||||||
|
const parsedWidth = computed(() => parseInt(`${props.width}`, 10))
|
||||||
const columnsToRender = computed(() => {
|
const columnsToRender = computed(() => {
|
||||||
const { totalColumn, totalRow, columnCache } = props
|
const { totalColumn, totalRow, columnCache } = props
|
||||||
const { isScrolling, xAxisScrollDir, scrollLeft } = unref(states)
|
const { isScrolling, xAxisScrollDir, scrollLeft } = unref(states)
|
||||||
@ -158,10 +165,10 @@ const createGrid = ({
|
|||||||
getEstimatedTotalWidth(props, unref(cache))
|
getEstimatedTotalWidth(props, unref(cache))
|
||||||
)
|
)
|
||||||
|
|
||||||
const windowStyle = computed(() => [
|
const windowStyle = computed<StyleValue>(() => [
|
||||||
{
|
{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
overflow: 'auto',
|
overflow: 'hidden',
|
||||||
WebkitOverflowScrolling: 'touch',
|
WebkitOverflowScrolling: 'touch',
|
||||||
willChange: 'transform',
|
willChange: 'transform',
|
||||||
},
|
},
|
||||||
@ -170,7 +177,7 @@ const createGrid = ({
|
|||||||
height: isNumber(props.height) ? `${props.height}px` : props.height,
|
height: isNumber(props.height) ? `${props.height}px` : props.height,
|
||||||
width: isNumber(props.width) ? `${props.width}px` : props.width,
|
width: isNumber(props.width) ? `${props.width}px` : props.width,
|
||||||
},
|
},
|
||||||
props.style,
|
props.style ?? {},
|
||||||
])
|
])
|
||||||
|
|
||||||
const innerStyle = computed(() => {
|
const innerStyle = computed(() => {
|
||||||
@ -279,7 +286,57 @@ const createGrid = ({
|
|||||||
emitEvents()
|
emitEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollTo = ({ scrollLeft, scrollTop }) => {
|
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)
|
scrollLeft = Math.max(scrollLeft, 0)
|
||||||
scrollTop = Math.max(scrollTop, 0)
|
scrollTop = Math.max(scrollTop, 0)
|
||||||
const _states = unref(states)
|
const _states = unref(states)
|
||||||
@ -434,20 +491,6 @@ const createGrid = ({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const api = {
|
|
||||||
windowStyle,
|
|
||||||
windowRef,
|
|
||||||
columnsToRender,
|
|
||||||
innerRef,
|
|
||||||
innerStyle,
|
|
||||||
states,
|
|
||||||
rowsToRender,
|
|
||||||
getItemStyle,
|
|
||||||
onScroll,
|
|
||||||
scrollTo,
|
|
||||||
scrollToItem,
|
|
||||||
}
|
|
||||||
|
|
||||||
expose({
|
expose({
|
||||||
windowRef,
|
windowRef,
|
||||||
innerRef,
|
innerRef,
|
||||||
@ -457,77 +500,121 @@ const createGrid = ({
|
|||||||
states,
|
states,
|
||||||
})
|
})
|
||||||
|
|
||||||
return api
|
// rendering part
|
||||||
},
|
|
||||||
|
|
||||||
render(ctx: any) {
|
const renderScrollbars = () => {
|
||||||
const {
|
const { totalColumn, totalRow } = props
|
||||||
$slots,
|
|
||||||
className,
|
|
||||||
containerElement,
|
|
||||||
columnsToRender,
|
|
||||||
data,
|
|
||||||
getItemStyle,
|
|
||||||
innerElement,
|
|
||||||
innerStyle,
|
|
||||||
rowsToRender,
|
|
||||||
onScroll,
|
|
||||||
states,
|
|
||||||
useIsScrolling,
|
|
||||||
windowStyle,
|
|
||||||
totalColumn,
|
|
||||||
totalRow,
|
|
||||||
} = ctx
|
|
||||||
|
|
||||||
const [columnStart, columnEnd] = columnsToRender
|
const width = unref(parsedWidth)
|
||||||
const [rowStart, rowEnd] = rowsToRender
|
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 Container = resolveDynamicComponent(containerElement)
|
const verticalScrollbar = h(Scrollbar, {
|
||||||
const Inner = resolveDynamicComponent(innerElement)
|
ref: vScrollbar,
|
||||||
|
clientSize: height,
|
||||||
|
layout: 'vertical',
|
||||||
|
onScroll: onVerticalScroll,
|
||||||
|
ratio: (height * 100) / estimatedHeight,
|
||||||
|
scrollFrom: scrollTop / (estimatedHeight - height),
|
||||||
|
total: totalColumn,
|
||||||
|
visible: true,
|
||||||
|
})
|
||||||
|
|
||||||
const children: VNodeChild[] = []
|
return {
|
||||||
if (totalRow > 0 && totalColumn > 0) {
|
horizontalScrollbar,
|
||||||
for (let row = rowStart; row <= rowEnd; row++) {
|
verticalScrollbar,
|
||||||
for (let column = columnStart; column <= columnEnd; column++) {
|
|
||||||
children.push(
|
|
||||||
($slots.default as Slot)?.({
|
|
||||||
columnIndex: column,
|
|
||||||
data,
|
|
||||||
key: column,
|
|
||||||
isScrolling: useIsScrolling ? states.isScrolling : undefined,
|
|
||||||
style: getItemStyle(row, column),
|
|
||||||
rowIndex: row,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const InnerNode = [
|
const renderItems = () => {
|
||||||
h(
|
const [columnStart, columnEnd] = unref(columnsToRender)
|
||||||
Inner as VNode,
|
const [rowStart, rowEnd] = unref(rowsToRender)
|
||||||
{
|
const { data, totalColumn, totalRow, useIsScrolling } = props
|
||||||
style: innerStyle,
|
const children: VNodeChild[] = []
|
||||||
ref: 'innerRef',
|
if (totalRow > 0 && totalColumn > 0) {
|
||||||
},
|
for (let row = rowStart; row <= rowEnd; row++) {
|
||||||
!isString(Inner)
|
for (let column = columnStart; column <= columnEnd; column++) {
|
||||||
? {
|
children.push(
|
||||||
default: () => children,
|
slots.default?.({
|
||||||
}
|
columnIndex: column,
|
||||||
: children
|
data,
|
||||||
),
|
key: column,
|
||||||
]
|
isScrolling: useIsScrolling
|
||||||
|
? unref(states).isScrolling
|
||||||
|
: undefined,
|
||||||
|
style: getItemStyle(row, column),
|
||||||
|
rowIndex: row,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return children
|
||||||
|
}
|
||||||
|
|
||||||
return h(
|
const renderInner = () => {
|
||||||
Container as VNode,
|
const Inner = resolveDynamicComponent(props.innerElement) as VNode
|
||||||
{
|
const children = renderItems()
|
||||||
class: className,
|
return [
|
||||||
style: windowStyle,
|
h(
|
||||||
onScroll,
|
Inner,
|
||||||
ref: 'windowRef',
|
{
|
||||||
},
|
style: unref(innerStyle),
|
||||||
!isString(Container) ? { default: () => InnerNode } : InnerNode
|
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
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
@ -15,8 +15,8 @@ import { hasOwn } from '@vue/shared'
|
|||||||
import { isNumber, isString } from '@element-plus/utils/util'
|
import { isNumber, isString } from '@element-plus/utils/util'
|
||||||
import isServer from '@element-plus/utils/isServer'
|
import isServer from '@element-plus/utils/isServer'
|
||||||
|
|
||||||
import { useCache } from '../hooks/useCache'
|
import { useCache } from '../hooks/use-cache'
|
||||||
import useWheel from '../hooks/useWheel'
|
import useWheel from '../hooks/use-wheel'
|
||||||
import Scrollbar from '../components/scrollbar'
|
import Scrollbar from '../components/scrollbar'
|
||||||
import { getScrollDir, isHorizontal, getRTLOffsetType } from '../utils'
|
import { getScrollDir, isHorizontal, getRTLOffsetType } from '../utils'
|
||||||
import { virtualizedListProps } from '../props'
|
import { virtualizedListProps } from '../props'
|
||||||
@ -64,7 +64,7 @@ const createList = ({
|
|||||||
// by user
|
// by user
|
||||||
const windowRef = ref<HTMLElement>()
|
const windowRef = ref<HTMLElement>()
|
||||||
const innerRef = ref<HTMLElement>()
|
const innerRef = ref<HTMLElement>()
|
||||||
const scrollbarRef = ref(null)
|
const scrollbarRef = ref()
|
||||||
|
|
||||||
const states = ref({
|
const states = ref({
|
||||||
isScrolling: false,
|
isScrolling: false,
|
@ -1,6 +1,6 @@
|
|||||||
import { isFunction } from '@vue/shared'
|
import { isFunction } from '@vue/shared'
|
||||||
import { throwError } from '@element-plus/utils/error'
|
import { throwError } from '@element-plus/utils/error'
|
||||||
import createGrid from '../builders/buildGrid'
|
import createGrid from '../builders/build-grid'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AUTO_ALIGNMENT,
|
AUTO_ALIGNMENT,
|
||||||
@ -10,15 +10,14 @@ import {
|
|||||||
SMART_ALIGNMENT,
|
SMART_ALIGNMENT,
|
||||||
START_ALIGNMENT,
|
START_ALIGNMENT,
|
||||||
} from '../defaults'
|
} from '../defaults'
|
||||||
import type { DefaultGridProps } from '../defaults'
|
import type { VirtualizedGridProps } from '../props'
|
||||||
|
|
||||||
import type { ExtractPropTypes } from 'vue'
|
|
||||||
import type { Alignment, GridCache, ListItem, ItemSize } from '../types'
|
import type { Alignment, GridCache, ListItem, ItemSize } from '../types'
|
||||||
|
|
||||||
const { max, min, floor } = Math
|
const { max, min, floor } = Math
|
||||||
const SCOPE = 'ElDynamicSizeGrid'
|
const SCOPE = 'ElDynamicSizeGrid'
|
||||||
|
|
||||||
type Props = ExtractPropTypes<typeof DefaultGridProps>
|
type Props = VirtualizedGridProps
|
||||||
type CacheItemType = 'column' | 'row'
|
type CacheItemType = 'column' | 'row'
|
||||||
|
|
||||||
// generates props access key via type
|
// generates props access key via type
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { throwError } from '@element-plus/utils/error'
|
import { throwError } from '@element-plus/utils/error'
|
||||||
|
|
||||||
import createList from '../builders/buildList'
|
import createList from '../builders/build-list'
|
||||||
|
|
||||||
import { isHorizontal } from '../utils'
|
import { isHorizontal } from '../utils'
|
||||||
import {
|
import {
|
||||||
@ -228,10 +228,10 @@ const DynamicSizeList = createList({
|
|||||||
|
|
||||||
cache.clearCacheAfterIndex = (index: number, forceUpdate = true) => {
|
cache.clearCacheAfterIndex = (index: number, forceUpdate = true) => {
|
||||||
cache.lastVisitedIndex = Math.min(cache.lastVisitedIndex, index - 1)
|
cache.lastVisitedIndex = Math.min(cache.lastVisitedIndex, index - 1)
|
||||||
instance.exposed.getItemStyleCache(-1)
|
instance.exposed?.getItemStyleCache(-1)
|
||||||
|
|
||||||
if (forceUpdate) {
|
if (forceUpdate) {
|
||||||
instance.proxy.$forceUpdate()
|
instance.proxy?.$forceUpdate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { isNumber } from '@element-plus/utils/util'
|
import { isNumber } from '@element-plus/utils/util'
|
||||||
import { throwError } from '@element-plus/utils/error'
|
import { throwError } from '@element-plus/utils/error'
|
||||||
import createGrid from '../builders/buildGrid'
|
import createGrid from '../builders/build-grid'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AUTO_ALIGNMENT,
|
AUTO_ALIGNMENT,
|
||||||
@ -198,8 +198,11 @@ const FixedSizeGrid = createGrid({
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
initCache: () => undefined,
|
* Fixed size grid does not need this cache
|
||||||
|
* Using any to bypass it, TODO: Using type inference to fix this.
|
||||||
|
*/
|
||||||
|
initCache: () => undefined as any,
|
||||||
|
|
||||||
clearCache: true,
|
clearCache: true,
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { isString } from '@element-plus/utils/util'
|
import { isString } from '@element-plus/utils/util'
|
||||||
import { throwError } from '@element-plus/utils/error'
|
import { throwError } from '@element-plus/utils/error'
|
||||||
import buildList from '../builders/buildList'
|
import buildList from '../builders/build-list'
|
||||||
import { isHorizontal } from '../utils'
|
import { isHorizontal } from '../utils'
|
||||||
import {
|
import {
|
||||||
SMART_ALIGNMENT,
|
SMART_ALIGNMENT,
|
||||||
@ -9,11 +9,10 @@ import {
|
|||||||
END_ALIGNMENT,
|
END_ALIGNMENT,
|
||||||
CENTERED_ALIGNMENT,
|
CENTERED_ALIGNMENT,
|
||||||
} from '../defaults'
|
} from '../defaults'
|
||||||
import type { DefaultListProps } from '../defaults'
|
|
||||||
|
|
||||||
import type { ExtractPropTypes } from 'vue'
|
import type { VirtualizedListProps } from '../props'
|
||||||
|
|
||||||
type IProps = ExtractPropTypes<typeof DefaultListProps>
|
type Props = VirtualizedListProps
|
||||||
|
|
||||||
const FixedSizeList = buildList({
|
const FixedSizeList = buildList({
|
||||||
name: 'ElFixedSizeList',
|
name: 'ElFixedSizeList',
|
||||||
@ -92,7 +91,7 @@ const FixedSizeList = buildList({
|
|||||||
Math.max(0, Math.min(total - 1, Math.floor(offset / (itemSize as number)))),
|
Math.max(0, Math.min(total - 1, Math.floor(offset / (itemSize as number)))),
|
||||||
|
|
||||||
getStopIndexForStartIndex: (
|
getStopIndexForStartIndex: (
|
||||||
{ height, total, itemSize, layout, width }: IProps,
|
{ height, total, itemSize, layout, width }: Props,
|
||||||
startIndex: number,
|
startIndex: number,
|
||||||
scrollOffset: number
|
scrollOffset: number
|
||||||
) => {
|
) => {
|
||||||
@ -112,8 +111,12 @@ const FixedSizeList = buildList({
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixed size list does not need this cache
|
||||||
|
* Using any to bypass it, TODO: Using type inference to fix this.
|
||||||
|
*/
|
||||||
initCache() {
|
initCache() {
|
||||||
return undefined
|
return undefined as any
|
||||||
},
|
},
|
||||||
|
|
||||||
clearCache: true,
|
clearCache: true,
|
||||||
|
61
packages/components/virtual-list/src/hooks/use-grid-wheel.ts
Normal file
61
packages/components/virtual-list/src/hooks/use-grid-wheel.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { rAF, cAF } from '@element-plus/utils/raf'
|
||||||
|
import { isFF } from '../utils'
|
||||||
|
|
||||||
|
import type { ComputedRef } from 'vue'
|
||||||
|
|
||||||
|
interface GridWheelState {
|
||||||
|
atXStartEdge: ComputedRef<boolean>
|
||||||
|
atXEndEdge: ComputedRef<boolean>
|
||||||
|
atYStartEdge: ComputedRef<boolean>
|
||||||
|
atYEndEdge: ComputedRef<boolean>
|
||||||
|
}
|
||||||
|
|
||||||
|
type GridWheelHandler = (x: number, y: number) => void
|
||||||
|
|
||||||
|
export const useGridWheel = (
|
||||||
|
{ atXEndEdge, atXStartEdge, atYEndEdge, atYStartEdge }: GridWheelState,
|
||||||
|
onWheelDelta: GridWheelHandler
|
||||||
|
) => {
|
||||||
|
let frameHandle: number | null = null
|
||||||
|
let xOffset = 0
|
||||||
|
let yOffset = 0
|
||||||
|
|
||||||
|
const hasReachedEdge = (x: number, y: number) => {
|
||||||
|
const xEdgeReached =
|
||||||
|
(x < 0 && atXStartEdge.value) || (x > 0 && atXEndEdge.value)
|
||||||
|
const yEdgeReached =
|
||||||
|
(y < 0 && atYStartEdge.value) || (y > 0 && atYEndEdge.value)
|
||||||
|
return xEdgeReached && yEdgeReached
|
||||||
|
}
|
||||||
|
|
||||||
|
const onWheel = (e: WheelEvent) => {
|
||||||
|
cAF(frameHandle!)
|
||||||
|
|
||||||
|
const x = e.deltaX
|
||||||
|
const y = e.deltaY
|
||||||
|
|
||||||
|
if (
|
||||||
|
hasReachedEdge(xOffset, yOffset) &&
|
||||||
|
hasReachedEdge(xOffset + x, yOffset + y)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
xOffset += x
|
||||||
|
yOffset += y
|
||||||
|
|
||||||
|
if (!isFF) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
frameHandle = rAF(() => {
|
||||||
|
onWheelDelta(xOffset, yOffset)
|
||||||
|
xOffset = 0
|
||||||
|
yOffset = 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasReachedEdge,
|
||||||
|
onWheel,
|
||||||
|
}
|
||||||
|
}
|
@ -10,19 +10,19 @@ const LayoutKeys = {
|
|||||||
[VERTICAL]: 'deltaY',
|
[VERTICAL]: 'deltaY',
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IWheelState {
|
interface ListWheelState {
|
||||||
atStartEdge: ComputedRef<boolean> // exclusive to reachEnd
|
atStartEdge: ComputedRef<boolean> // exclusive to reachEnd
|
||||||
atEndEdge: ComputedRef<boolean>
|
atEndEdge: ComputedRef<boolean>
|
||||||
layout: ComputedRef<LayoutDirection>
|
layout: ComputedRef<LayoutDirection>
|
||||||
}
|
}
|
||||||
|
|
||||||
type IWheelHandler = (offset: number) => void
|
type ListWheelHandler = (offset: number) => void
|
||||||
|
|
||||||
const useWheel = (
|
const useWheel = (
|
||||||
{ atEndEdge, atStartEdge, layout }: IWheelState,
|
{ atEndEdge, atStartEdge, layout }: ListWheelState,
|
||||||
onWheelDelta: IWheelHandler
|
onWheelDelta: ListWheelHandler
|
||||||
) => {
|
) => {
|
||||||
let frameHandle: number | null = null
|
let frameHandle: number
|
||||||
let offset = 0
|
let offset = 0
|
||||||
|
|
||||||
// let scrollLock = false
|
// let scrollLock = false
|
@ -1,4 +1,4 @@
|
|||||||
import { isNumber } from '@element-plus/utils/util'
|
import { buildProp } from '@element-plus/utils/props'
|
||||||
import { LTR, RTL, VERTICAL } from './defaults'
|
import { LTR, RTL, VERTICAL } from './defaults'
|
||||||
|
|
||||||
import type { ExtractPropTypes, PropType } from 'vue'
|
import type { ExtractPropTypes, PropType } from 'vue'
|
||||||
@ -32,10 +32,10 @@ const initScrollOffset = {
|
|||||||
default: 0,
|
default: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
const total = {
|
const total = buildProp({
|
||||||
type: Number as PropType<number>,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
}
|
})
|
||||||
|
|
||||||
const layout = {
|
const layout = {
|
||||||
type: String as PropType<LayoutDirection>,
|
type: String as PropType<LayoutDirection>,
|
||||||
@ -63,11 +63,10 @@ export const virtualizedProps = {
|
|||||||
*/
|
*/
|
||||||
direction,
|
direction,
|
||||||
|
|
||||||
height: {
|
height: buildProp({
|
||||||
type: [String, Number] as PropType<string | number>,
|
type: [String, Number],
|
||||||
required: true,
|
required: true,
|
||||||
validator: isNumber,
|
}),
|
||||||
},
|
|
||||||
|
|
||||||
innerElement: {
|
innerElement: {
|
||||||
type: [String, Object],
|
type: [String, Object],
|
||||||
@ -83,11 +82,10 @@ export const virtualizedProps = {
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
width: {
|
width: buildProp({
|
||||||
type: [Number, String] as PropType<string | number>,
|
type: [Number, String],
|
||||||
required: true,
|
required: true,
|
||||||
validator: isNumber,
|
}),
|
||||||
},
|
|
||||||
perfMode: {
|
perfMode: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
|
@ -1,8 +1,4 @@
|
|||||||
import type {
|
import type { CSSProperties, ComponentInternalInstance, Ref } from 'vue'
|
||||||
CSSProperties,
|
|
||||||
ComponentInternalInstance,
|
|
||||||
ExtractPropTypes,
|
|
||||||
} from 'vue'
|
|
||||||
|
|
||||||
export type Instance = ComponentInternalInstance
|
export type Instance = ComponentInternalInstance
|
||||||
|
|
||||||
@ -46,7 +42,7 @@ export type GridCache = {
|
|||||||
export type ScrollDir = 'forwards' | 'backwards'
|
export type ScrollDir = 'forwards' | 'backwards'
|
||||||
|
|
||||||
export type ListItemSizer<T, P extends InitListCacheFunc<T>> = (
|
export type ListItemSizer<T, P extends InitListCacheFunc<T>> = (
|
||||||
props: ExtractPropTypes<T>,
|
props: T,
|
||||||
index: number,
|
index: number,
|
||||||
cache: ReturnType<P>
|
cache: ReturnType<P>
|
||||||
) => number
|
) => number
|
||||||
@ -54,10 +50,10 @@ export type ListItemSizer<T, P extends InitListCacheFunc<T>> = (
|
|||||||
export type GetEstimatedTotalSize<
|
export type GetEstimatedTotalSize<
|
||||||
T,
|
T,
|
||||||
P extends InitCacheFunc<T, GridCache | ListCache>
|
P extends InitCacheFunc<T, GridCache | ListCache>
|
||||||
> = (props: ExtractPropTypes<T>, cache: ReturnType<P>) => number
|
> = (props: T, cache: ReturnType<P>) => number
|
||||||
|
|
||||||
export type GetOffset<T, P extends InitListCacheFunc<T>> = (
|
export type GetOffset<T, P extends InitListCacheFunc<T>> = (
|
||||||
props: ExtractPropTypes<T>,
|
props: T,
|
||||||
idx: number,
|
idx: number,
|
||||||
alignment: Alignment,
|
alignment: Alignment,
|
||||||
offset: number,
|
offset: number,
|
||||||
@ -67,24 +63,21 @@ export type GetOffset<T, P extends InitListCacheFunc<T>> = (
|
|||||||
export type GetStartIndexForOffset<
|
export type GetStartIndexForOffset<
|
||||||
T,
|
T,
|
||||||
P extends InitCacheFunc<T, GridCache | ListCache>
|
P extends InitCacheFunc<T, GridCache | ListCache>
|
||||||
> = (props: ExtractPropTypes<T>, offset: number, cache: ReturnType<P>) => number
|
> = (props: T, offset: number, cache: ReturnType<P>) => number
|
||||||
|
|
||||||
export type GetStopIndexForStartIndex<
|
export type GetStopIndexForStartIndex<
|
||||||
T,
|
T,
|
||||||
P extends InitCacheFunc<T, GridCache | ListCache>
|
P extends InitCacheFunc<T, GridCache | ListCache>
|
||||||
> = (
|
> = (
|
||||||
props: ExtractPropTypes<T>,
|
props: T,
|
||||||
startIndex: number,
|
startIndex: number,
|
||||||
scrollOffset: number,
|
scrollOffset: number,
|
||||||
cache: ReturnType<P>
|
cache: ReturnType<P>
|
||||||
) => number
|
) => number
|
||||||
|
|
||||||
export type PropValidator<T> = (props: ExtractPropTypes<T>) => void
|
export type PropValidator<T> = (props: T) => void
|
||||||
|
|
||||||
export type InitCacheFunc<T, P> = (
|
export type InitCacheFunc<T, P> = (props: T, cache: Instance) => P
|
||||||
props: ExtractPropTypes<T>,
|
|
||||||
cache: Instance
|
|
||||||
) => P
|
|
||||||
export type InitListCacheFunc<T> = InitCacheFunc<T, ListCache>
|
export type InitListCacheFunc<T> = InitCacheFunc<T, ListCache>
|
||||||
export type InitGridCacheFunc<T> = InitCacheFunc<T, GridCache>
|
export type InitGridCacheFunc<T> = InitCacheFunc<T, GridCache>
|
||||||
|
|
||||||
@ -110,8 +103,8 @@ export type ExposesStates = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type SharedExposes = {
|
export type SharedExposes = {
|
||||||
windowRef: HTMLElement
|
windowRef: Ref<HTMLElement>
|
||||||
innerRef: HTMLElement
|
innerRef: Ref<HTMLElement>
|
||||||
getItemStyleCache: (_: any, __: any, ___: any) => CSSProperties
|
getItemStyleCache: (_: any, __: any, ___: any) => CSSProperties
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,8 +132,12 @@ export type GridExposes = {
|
|||||||
) => void
|
) => void
|
||||||
} & SharedExposes
|
} & SharedExposes
|
||||||
|
|
||||||
|
export type ScrollbarExpose = {
|
||||||
|
onMouseUp: () => void
|
||||||
|
}
|
||||||
|
|
||||||
export type GetGridOffset<T, P extends InitGridCacheFunc<T>> = (
|
export type GetGridOffset<T, P extends InitGridCacheFunc<T>> = (
|
||||||
props: ExtractPropTypes<T>,
|
props: T,
|
||||||
index: number,
|
index: number,
|
||||||
alignment: Alignment,
|
alignment: Alignment,
|
||||||
offset: number,
|
offset: number,
|
||||||
@ -149,7 +146,7 @@ export type GetGridOffset<T, P extends InitGridCacheFunc<T>> = (
|
|||||||
) => number
|
) => number
|
||||||
|
|
||||||
export type GetPosition<T, P extends InitGridCacheFunc<T>> = (
|
export type GetPosition<T, P extends InitGridCacheFunc<T>> = (
|
||||||
props: ExtractPropTypes<T>,
|
props: T,
|
||||||
index: number,
|
index: number,
|
||||||
cache: ReturnType<P>
|
cache: ReturnType<P>
|
||||||
) => [number, number]
|
) => [number, number]
|
||||||
|
@ -6,3 +6,9 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@include b(vg) {
|
||||||
|
@include e(wrapper) {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user