diff --git a/components/table/Table.tsx b/components/table/Table.tsx index 75db806cc..4129004fe 100644 --- a/components/table/Table.tsx +++ b/components/table/Table.tsx @@ -43,7 +43,7 @@ import { useLocaleReceiver } from '../locale-provider/LocaleReceiver'; import classNames from '../_util/classNames'; import omit from '../_util/omit'; import { initDefaultProps } from '../_util/props-util'; -import { useProvideSlots } from './context'; +import { useProvideSlots, useProvideTableContext } from './context'; import type { ContextSlots } from './context'; import useColumns from './hooks/useColumns'; import { convertChildrenToColumns } from './util'; @@ -195,7 +195,10 @@ export const tableProps = () => { >, default: undefined, }, - + onResizeColumn: { + type: Function as PropType<(w: number, col: ColumnsType) => void>, + default: undefined, + }, rowSelection: { type: Object as PropType, default: undefined }, getPopupContainer: { type: Function as PropType, default: undefined }, scroll: { @@ -243,7 +246,7 @@ const InteralTable = defineComponent< 'customFilterIcon', 'customFilterDropdown', ], - setup(props, { attrs, slots, expose }) { + setup(props, { attrs, slots, expose, emit }) { devWarning( !(typeof props.rowKey === 'function' && props.rowKey.length > 1), 'Table', @@ -251,7 +254,11 @@ const InteralTable = defineComponent< ); useProvideSlots(computed(() => props.contextSlots)); - + useProvideTableContext({ + onResizeColumn: (w, col) => { + emit('resizeColumn', w, col); + }, + }); const screens = useBreakpoint(); const mergedColumns = computed(() => { diff --git a/components/table/context.ts b/components/table/context.ts index 6747ad42d..eb873d0b4 100644 --- a/components/table/context.ts +++ b/components/table/context.ts @@ -1,5 +1,6 @@ import type { ComputedRef, InjectionKey } from 'vue'; import { computed, inject, provide } from 'vue'; +import type { ColumnType } from './interface'; export type ContextSlots = { emptyText?: (...args: any[]) => any; @@ -15,14 +16,28 @@ export type ContextSlots = { [key: string]: ((...args: any[]) => any) | undefined; }; -export type ContextProps = ComputedRef; +type SlotsContextProps = ComputedRef; -export const ContextKey: InjectionKey = Symbol('ContextProps'); +const SlotsContextKey: InjectionKey = Symbol('SlotsContextProps'); -export const useProvideSlots = (props: ContextProps) => { - provide(ContextKey, props); +export const useProvideSlots = (props: SlotsContextProps) => { + provide(SlotsContextKey, props); }; export const useInjectSlots = () => { - return inject(ContextKey, computed(() => ({})) as ContextProps); + return inject(SlotsContextKey, computed(() => ({})) as SlotsContextProps); +}; + +type ContextProps = { + onResizeColumn: (w: number, column: ColumnType) => void; +}; + +const ContextKey: InjectionKey = Symbol('ContextProps'); + +export const useProvideTableContext = (props: ContextProps) => { + provide(ContextKey, props); +}; + +export const useInjectTableContext = () => { + return inject(ContextKey, { onResizeColumn: () => {} } as ContextProps); }; diff --git a/components/table/demo/index.vue b/components/table/demo/index.vue index e4aa9ba15..57bdfb1a4 100644 --- a/components/table/demo/index.vue +++ b/components/table/demo/index.vue @@ -22,6 +22,7 @@ + @@ -57,6 +58,7 @@ import Stripe from './stripe.vue'; import MultipleSorter from './multiple-sorter.vue'; import Summary from './summary.vue'; import Sticky from './sticky.vue'; +import ResizableColumn from './resizable-column.vue'; import CN from '../index.zh-CN.md'; import US from '../index.en-US.md'; import { defineComponent } from '@vue/runtime-core'; @@ -91,6 +93,7 @@ export default defineComponent({ MultipleSorter, Summary, Sticky, + ResizableColumn, }, }); diff --git a/components/table/demo/resizable-column.vue b/components/table/demo/resizable-column.vue new file mode 100644 index 000000000..4ca99c3f5 --- /dev/null +++ b/components/table/demo/resizable-column.vue @@ -0,0 +1,138 @@ + +--- +order: 0 +title: + en-US: Resizable column + zh-CN: 可伸缩列 +--- + +## zh-CN + +设置 resizable 开启拖动列 + +鼠标 hover 到 Name、 Age 分割线上体验一下吧 + +## en-US + +set resizable for drag column + + + + diff --git a/components/table/style/index.less b/components/table/style/index.less index 42d8f0f9f..acc14d232 100644 --- a/components/table/style/index.less +++ b/components/table/style/index.less @@ -2,6 +2,7 @@ @import '../../style/mixins/index'; @import './size'; @import './bordered'; +@import './resize.less'; @table-prefix-cls: ~'@{ant-prefix}-table'; @dropdown-prefix-cls: ~'@{ant-prefix}-dropdown'; diff --git a/components/table/style/resize.less b/components/table/style/resize.less new file mode 100644 index 000000000..f2da887b2 --- /dev/null +++ b/components/table/style/resize.less @@ -0,0 +1,28 @@ +.@{table-prefix-cls}-resize-handle { + position: absolute; + top: 0; + height: 100% !important; + bottom: 0; + left: auto !important; + right: -8px; + cursor: col-resize; + touch-action: none; + user-select: auto; + width: 16px; + z-index: 1; + &-line { + display: block; + width: 3px; + margin-left: 7px; + height: 100% !important; + background-color: @primary-color; + opacity: 0; + } + &:hover &-line { + opacity: 1; + } +} + +.dragging .@{table-prefix-cls}-resize-handle-line { + opacity: 1; +} diff --git a/components/vc-table/Cell/index.tsx b/components/vc-table/Cell/index.tsx index 65f981834..0350590ac 100644 --- a/components/vc-table/Cell/index.tsx +++ b/components/vc-table/Cell/index.tsx @@ -273,6 +273,7 @@ export default defineComponent({ {appendNode} {childNode} + {slots.dragHandle?.()} ); }; diff --git a/components/vc-table/Header/DragHandle.tsx b/components/vc-table/Header/DragHandle.tsx new file mode 100644 index 000000000..c8ddc7eaf --- /dev/null +++ b/components/vc-table/Header/DragHandle.tsx @@ -0,0 +1,180 @@ +import addEventListenerWrap from '../../vc-util/Dom/addEventListener'; +import type { EventHandler } from '../../_util/EventInterface'; +import raf from '../../_util/raf'; +import { + defineComponent, + onUnmounted, + nextTick, + watch, + computed, + ref, + watchEffect, + getCurrentInstance, + onMounted, +} from 'vue'; +import type { PropType } from 'vue'; +import devWarning from '../../vc-util/devWarning'; +import type { ColumnType } from '../interface'; +import { useInjectTableContext } from '../../table/context'; +import supportsPassive from '../../_util/supportsPassive'; + +const events = { + mouse: { + start: 'mousedown', + move: 'mousemove', + stop: 'mouseup', + }, + touch: { + start: 'touchstart', + move: 'touchmove', + stop: 'touchend', + }, +}; +type HandleEvent = MouseEvent & TouchEvent; + +const defaultMinWidth = 50; +export default defineComponent({ + name: 'DragHandle', + props: { + prefixCls: String, + width: { + type: Number, + required: true, + }, + minWidth: { + type: Number, + default: defaultMinWidth, + }, + maxWidth: { + type: Number, + default: Infinity, + }, + column: { + type: Object as PropType>, + default: undefined as ColumnType, + }, + }, + setup(props) { + let startX = 0; + let moveEvent = { remove: () => {} }; + let stopEvent = { remove: () => {} }; + const removeEvents = () => { + moveEvent.remove(); + stopEvent.remove(); + }; + onUnmounted(() => { + removeEvents(); + }); + watchEffect(() => { + devWarning(!isNaN(props.width), 'Table', 'width must be a number when use resizable'); + }); + + const { onResizeColumn } = useInjectTableContext(); + const minWidth = computed(() => { + return typeof props.minWidth === 'number' && !isNaN(props.minWidth) + ? props.minWidth + : defaultMinWidth; + }); + const maxWidth = computed(() => { + return typeof props.maxWidth === 'number' && !isNaN(props.maxWidth) + ? props.maxWidth + : Infinity; + }); + const instance = getCurrentInstance(); + // eslint-disable-next-line vue/no-setup-props-destructure + let baseWidth = props.width; + onMounted(() => { + nextTick(() => { + baseWidth = instance.vnode.el?.parentNode?.getBoundingClientRect().width; + }); + }); + const dragging = ref(false); + let rafId: number; + const updateWidth = (e: HandleEvent) => { + let pageX = 0; + if (e.touches) { + if (e.touches.length) { + // touchmove + pageX = e.touches[0].pageX; + } else { + // touchend + pageX = e.changedTouches[0].pageX; + } + } else { + pageX = e.pageX; + } + const tmpDeltaX = startX - pageX; + let w = Math.max(baseWidth - tmpDeltaX, minWidth.value); + w = Math.min(w, maxWidth.value); + raf.cancel(rafId); + rafId = raf(() => { + onResizeColumn(w, props.column.__originColumn__); + }); + }; + const handleMove = (e: HandleEvent) => { + updateWidth(e); + }; + const handleStop = (e: HandleEvent) => { + dragging.value = false; + updateWidth(e); + nextTick(() => { + baseWidth = instance.vnode.el?.parentNode?.getBoundingClientRect().width; + }); + removeEvents(); + }; + const handleStart = (e: HandleEvent, eventsFor: any) => { + dragging.value = true; + removeEvents(); + + if (e instanceof MouseEvent && e.which !== 1) { + return; + } + if (e.stopPropagation) e.stopPropagation(); + startX = e.touches ? e.touches[0].pageX : e.pageX; + moveEvent = addEventListenerWrap(document.documentElement, eventsFor.move, handleMove); + stopEvent = addEventListenerWrap(document.documentElement, eventsFor.stop, handleStop); + }; + const handleDown: EventHandler = (e: HandleEvent) => { + e.stopPropagation(); + e.preventDefault(); + handleStart(e, events.mouse); + }; + const handleTouchDown: EventHandler = (e: HandleEvent) => { + e.stopPropagation(); + e.preventDefault(); + handleStart(e, events.touch); + }; + + const handleClick: EventHandler = (e: HandleEvent) => { + e.stopPropagation(); + e.preventDefault(); + }; + + watch( + () => props.width, + () => { + if (!dragging.value) { + baseWidth = props.width; + } + }, + { immediate: true }, + ); + + return () => { + const { prefixCls } = props; + const touchEvents = { + [supportsPassive ? 'onTouchstartPassive' : 'onTouchstart']: e => handleTouchDown(e), + }; + return ( +
+
+
+ ); + }; + }, +}); diff --git a/components/vc-table/Header/HeaderRow.tsx b/components/vc-table/Header/HeaderRow.tsx index 785203528..5c5784b52 100644 --- a/components/vc-table/Header/HeaderRow.tsx +++ b/components/vc-table/Header/HeaderRow.tsx @@ -11,6 +11,7 @@ import type { } from '../interface'; import { getCellFixedInfo } from '../utils/fixUtil'; import { getColumnsKey } from '../utils/valueUtil'; +import DragHandleVue from './DragHandle'; export interface RowProps { cells: readonly CellType[]; @@ -73,7 +74,7 @@ export default defineComponent({ if (column && column.customHeaderCell) { additionalProps = cell.column.customHeaderCell(column); } - + const col: ColumnType = column; return ( ({ additionalProps={additionalProps} rowType="header" column={column} - v-slots={{ default: () => column.title }} + v-slots={{ + default: () => column.title, + dragHandle: () => + col.resizable ? ( + + ) : null, + }} /> ); })} diff --git a/components/vc-table/interface.ts b/components/vc-table/interface.ts index 642c95006..ecb2dd6b7 100644 --- a/components/vc-table/interface.ts +++ b/components/vc-table/interface.ts @@ -110,6 +110,9 @@ export interface ColumnType extends ColumnSharedType { }) => any | RenderedCell; rowSpan?: number; width?: number | string; + minWidth?: number; + maxWidth?: number; + resizable?: boolean; customCell?: GetComponentProps; /** @deprecated Please use `onCell` instead */ onCellClick?: (record: RecordType, e: MouseEvent) => void;