feat(components): [virtual-list] renderer (#7167)

- Add column-resizer for resizing column
- Add sort and column resizer to row header renderer
This commit is contained in:
JeremyWuuuuu 2022-04-14 22:13:16 +08:00 committed by GitHub
parent bc4f82d7b5
commit 971d58d9f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 255 additions and 23 deletions

View File

@ -1,16 +1,14 @@
import { buildProps, definePropType } from '@element-plus/utils'
import { column } from './common'
import type { ExtractPropTypes } from 'vue'
import type { Column } from './types'
export const tableV2CellProps = buildProps({
class: String,
cellData: {
type: definePropType<any>([String, Boolean, Number, Object]),
},
column: {
type: definePropType<Column<any>>(Object),
},
column,
columnIndex: Number,
rowData: {
type: definePropType<any>(Object),

View File

@ -0,0 +1,31 @@
import { buildProps, definePropType } from '@element-plus/utils'
import { column, styleType } from './common'
import type { ExtractPropTypes } from 'vue'
import type { AnyColumn } from './common'
type ResizeTimingFn = (column: AnyColumn) => void
type ResizeHandler = (column: AnyColumn, width: number) => void
export const tableV2ColumnResizerProps = buildProps({
column: { ...column, required: true },
onResize: {
type: definePropType<ResizeHandler>(Function),
},
onResizeStart: {
type: definePropType<ResizeTimingFn>(Function),
},
onResizeStop: {
type: definePropType<ResizeTimingFn>(Function),
},
minWidth: {
type: Number,
default: 30,
},
style: styleType,
} as const)
export type TableV2ColumnResizerProps = ExtractPropTypes<
typeof tableV2ColumnResizerProps
>

View File

@ -3,7 +3,7 @@ import { definePropType, mutable } from '@element-plus/utils'
import type { CSSProperties } from 'vue'
import type { Column, KeyType } from './types'
type AnyColumn = Column<any>
export type AnyColumn = Column<any>
/**
* @Note even though we can use `string[] | string` as the type but for

View File

@ -0,0 +1,21 @@
import { defineComponent } from 'vue'
import ElIcon from '@element-plus/components/icon'
import { SortDown, SortUp } from '@element-plus/icons-vue'
import { SortOrder } from './constants'
export type SortIconProps = {
sortOrder: SortOrder
class?: string
}
const SortIcon = defineComponent((props: SortIconProps) => {
const { sortOrder } = props
return (
<ElIcon size={14} class={props.class}>
{sortOrder === SortOrder.ASC ? SortUp : SortDown}
</ElIcon>
)
})
export default SortIcon

View File

@ -0,0 +1,160 @@
import {
computed,
defineComponent,
onBeforeUnmount,
ref,
shallowRef,
unref,
watch,
withModifiers,
} from 'vue'
import { NOOP } from '@vue/shared'
import { useNamespace } from '@element-plus/hooks'
import { tableV2ColumnResizerProps } from './column-resizer'
import type { TableV2ColumnResizerProps } from './column-resizer'
const COMPONENT_NAME = 'ColumnResizer'
const ColumnResizer = defineComponent({
name: COMPONENT_NAME,
props: tableV2ColumnResizerProps,
setup(props) {
const { resizerRef, onClick, onMousedown, onTouchStart, onDragEnd } =
useColumnResizer(props)
return () => (
<div
ref={resizerRef}
onClick={onClick}
onMousedown={onMousedown}
onMouseup={onDragEnd}
onTouchstart={onTouchStart}
onTouchend={onDragEnd}
style={props.style}
/>
)
},
})
function useColumnResizer(props: TableV2ColumnResizerProps) {
const ns = useNamespace('')
const isDragging = shallowRef(false)
const resizerRef = ref<HTMLElement>()
const width = shallowRef(0)
const lastX = shallowRef<null | number>(null)
const ownerDocument = computed(() => unref(resizerRef)?.ownerDocument)
const onDragStart = withModifiers(() => {
isDragging.value = true
const column = props.column!
width.value = column.width
props.onResizeStart?.(column)
}, ['left'])
const addEvents = (
drag: 'mousemove' | 'touchmove',
end: 'mouseup' | 'touchend'
) => {
const _ownerDocument = unref(ownerDocument)!
if (!_ownerDocument) return
_ownerDocument.addEventListener(drag, onDrag)
_ownerDocument.addEventListener(end, onDragEnd)
}
const detachEvents = () => {
const _ownerDocument = unref(ownerDocument)!
if (!_ownerDocument) return
_ownerDocument.removeEventListener('mousemove', onDrag)
_ownerDocument.removeEventListener('touchmove', onDrag)
_ownerDocument.removeEventListener('mouseup', onDragEnd)
_ownerDocument.removeEventListener('touchend', onDragEnd)
}
const onDragEnd = () => {
if (!unref(isDragging)) return
const { column, onResizeStop } = props
isDragging.value = false
onResizeStop?.(column)
detachEvents()
}
const onClick = withModifiers(NOOP, ['stop'])
const onMousedown = (e: MouseEvent) => {
onDragStart(e)
addEvents('mousemove', 'mouseup')
}
const onTouchStart = (e: TouchEvent) => {
onDragStart(e)
addEvents('touchmove', 'touchend')
}
function onDrag(e: MouseEvent | TouchEvent) {
const { type, preventDefault } = e
let { clientX } = e as MouseEvent
const { targetTouches } = e as TouchEvent
if (type === 'touchmove') {
preventDefault()
if (targetTouches?.[0]) clientX = targetTouches[0].clientX
}
const { offsetParent } = unref(resizerRef)!
const offsetParentRect = offsetParent!.getBoundingClientRect()
const x = clientX + offsetParent!.scrollLeft - offsetParentRect.left
const _lastX = unref(lastX)
if (_lastX === null) {
lastX.value = x
return
}
const { column, minWidth: propMinWidth } = props
const { width: columnWidth, maxWidth, minWidth = propMinWidth } = column!
const movedX = x - _lastX
if (!movedX) return
width.value = columnWidth + movedX
lastX.value = x
let newWidth = unref(width)
if (maxWidth && newWidth > maxWidth) {
newWidth = maxWidth
} else if (newWidth < minWidth) {
newWidth = minWidth
}
if (newWidth === unref(width)) return
props.onResize?.(column, newWidth)
}
watch(isDragging, (val) => {
const _ownerDocument = unref(ownerDocument)
if (!_ownerDocument) return
if (val) {
_ownerDocument.body.classList.add(`${unref(ns.namespace)}-dragging`)
} else {
_ownerDocument.body.classList.remove(`${unref(ns.namespace)}-dragging`)
}
})
onBeforeUnmount(() => {
detachEvents()
})
return {
resizerRef,
onClick,
onMousedown,
onTouchStart,
onDragEnd,
}
}
export default ColumnResizer
export { useColumnResizer }

View File

@ -5,7 +5,7 @@ import { isFunction, isObject } from '@element-plus/utils'
import { useTable } from './use-table'
import { tryCall } from './utils'
import { TableV2InjectionKey } from './tokens'
import { Alignment } from './constants'
import { Alignment, SortOrder, oppositeOrderMap } from './constants'
import { placeholderSign } from './private'
import { tableV2Props } from './table'
// components
@ -14,7 +14,9 @@ import TableRow from './table-row'
import TableHeaderRow from './table-header-row'
import TableCell from './table-cell'
import TableHeaderCell from './table-header-cell'
import ColumnResizer from './table-column-resizer'
import ExpandIcon from './expand-icon'
import SortIcon from './sort-icon'
import type { VNode } from 'vue'
import type { TableGridRowSlotParams } from './table-grid'
@ -49,6 +51,9 @@ const TableV2 = defineComponent({
vScrollbarSize,
onColumnSorted,
onColumnResized,
onColumnResizeStart,
onColumnResizeEnd,
onRowHovered,
onRowExpanded,
onRowsRendered,
@ -302,7 +307,7 @@ const TableV2 = defineComponent({
return
}
const { headerCellRenderer, headerClass } = column
const { headerCellRenderer, headerClass, sortable, resizable } = column
/**
* render Cell children
@ -319,7 +324,7 @@ const TableV2 = defineComponent({
/**
* Render cell container and sort indicator
*/
// const { sortBy, sortState, headerCellProps } = props
const { sortBy, sortState, headerCellProps } = props
const element = 'header-cell'
const cellKls = [
@ -327,29 +332,43 @@ const TableV2 = defineComponent({
...tryCall(headerClass, renderHeaderCellProps, ''),
column.align === Alignment.CENTER && ns.em(element, 'align-center'),
column.align === Alignment.RIGHT && ns.em(element, 'align-right'),
column.sortable && ns.is('sortable'),
sortable && ns.is('sortable'),
column.key === unref(resizingKey) && ns.is('resizing'),
]
// let sorting: boolean, sortOrder: SortOrder
// if (sortState) {
// const order = sortState[column.key]
// sorting = Boolean(oppositeOrderMap[order])
// sortOrder = sorting ? order : SortOrder.ASC
// } else {
// sorting = column.key === sortBy.key
// sortOrder = sorting ? sortBy.order : SortOrder.ASC
// }
let sorting: boolean, sortOrder: SortOrder
if (sortState) {
const order = sortState[column.key]
sorting = Boolean(oppositeOrderMap[order])
sortOrder = sorting ? order : SortOrder.ASC
} else {
sorting = column.key === sortBy.key
sortOrder = sorting ? sortBy.order : SortOrder.ASC
}
const cellProps = {
...tryCall(props.headerCellProps, renderHeaderCellProps),
...tryCall(headerCellProps, renderHeaderCellProps),
onClick: column.sortable ? onColumnSorted : undefined,
class: cellKls,
style: unref(columnsStyles)[column.key],
dataKey: column.key,
}
return <div {...cellProps}>{Cell}</div>
return (
<div {...cellProps}>
{Cell}
{sortable && <SortIcon sortOrder={sortOrder} />}
{resizable && (
<ColumnResizer
class={ns.e('column-resizer')}
column={column}
onResize={onColumnResized}
onResizeStart={onColumnResizeStart}
onResizeStop={onColumnResizeEnd}
/>
)}
</div>
)
}
provide(TableV2InjectionKey, {

View File

@ -178,7 +178,7 @@ export const tableV2Props = buildProps({
},
onRowExpand: tableV2RowProps.onRowExpand,
onScroll: tableV2GridProps.onScroll,
onRowRendered: tableV2GridProps.onRowRendered,
onRowRendered: tableV2GridProps.onRowsRendered,
rowEventHandlers: tableV2RowProps.rowEventHandlers,
} as const)

View File

@ -204,7 +204,10 @@ function useTable(props: TableV2Props) {
props.onExpandedRowsChange?.(_expandedRowKeys)
}
function onColumnResized(key: KeyType, width: number) {
function onColumnResized(
{ key }: TableV2Props['columns'][number],
width: number
) {
resizingWidth.value = width
const column = getColumn(key)!
@ -212,7 +215,7 @@ function useTable(props: TableV2Props) {
props.onColumnResize?.(column, width)
}
function onColumnResizeStart(key: KeyType) {
function onColumnResizeStart({ key }: TableV2Props['columns'][number]) {
resizingKey.value = key
}