feat: add resize table column

pull/4825/head
tangjinzhou 2021-10-27 14:58:55 +08:00
parent 17a1ca5edf
commit 1272457517
10 changed files with 400 additions and 11 deletions

View File

@ -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<TableRowSelection>, default: undefined },
getPopupContainer: { type: Function as PropType<GetPopupContainer>, 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(() => {

View File

@ -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<ContextSlots>;
type SlotsContextProps = ComputedRef<ContextSlots>;
export const ContextKey: InjectionKey<ContextProps> = Symbol('ContextProps');
const SlotsContextKey: InjectionKey<SlotsContextProps> = 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<any>) => void;
};
const ContextKey: InjectionKey<ContextProps> = Symbol('ContextProps');
export const useProvideTableContext = (props: ContextProps) => {
provide(ContextKey, props);
};
export const useInjectTableContext = () => {
return inject(ContextKey, { onResizeColumn: () => {} } as ContextProps);
};

View File

@ -22,6 +22,7 @@
<RowSelectionCustom />
<RowSelection />
<Sticky />
<ResizableColumn />
<Size />
<Stripe />
<Summary />
@ -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,
},
});
</script>

View File

@ -0,0 +1,138 @@
<docs>
---
order: 0
title:
en-US: Resizable column
zh-CN: 可伸缩列
---
## zh-CN
设置 resizable 开启拖动列
鼠标 hover Name Age 分割线上体验一下吧
## en-US
set resizable for drag column
</docs>
<template>
<a-table :columns="columns" :data-source="data" @resizeColumn="handleResizeColumn">
<template #headerCell="{ column }">
<template v-if="column.key === 'name'">
<span>
<smile-outlined />
Name
</span>
</template>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
<a>
{{ record.name }}
</a>
</template>
<template v-else-if="column.key === 'tags'">
<span>
<a-tag
v-for="tag in record.tags"
:key="tag"
:color="tag === 'loser' ? 'volcano' : tag.length > 5 ? 'geekblue' : 'green'"
>
{{ tag.toUpperCase() }}
</a-tag>
</span>
</template>
<template v-else-if="column.key === 'action'">
<span>
<a>Invite {{ record.name }}</a>
<a-divider type="vertical" />
<a>Delete</a>
<a-divider type="vertical" />
<a class="ant-dropdown-link">
More actions
<down-outlined />
</a>
</span>
</template>
</template>
</a-table>
</template>
<script lang="ts">
import { SmileOutlined, DownOutlined } from '@ant-design/icons-vue';
import { defineComponent, ref } from 'vue';
const data = [
{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
tags: ['nice', 'developer'],
},
{
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
tags: ['loser'],
},
{
key: '3',
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
tags: ['cool', 'teacher'],
},
];
export default defineComponent({
components: {
SmileOutlined,
DownOutlined,
},
setup() {
const columns = ref([
{
name: 'Name',
dataIndex: 'name',
key: 'name',
resizable: true,
width: 150,
},
{
title: 'Age',
dataIndex: 'age',
key: 'age',
resizable: true,
width: 100,
minWidth: 100,
maxWidth: 200,
},
{
title: 'Address',
dataIndex: 'address',
key: 'address',
},
{
title: 'Tags',
key: 'tags',
dataIndex: 'tags',
},
{
title: 'Action',
key: 'action',
},
]);
return {
data,
columns,
handleResizeColumn: (w, col) => {
col.width = w;
},
};
},
});
</script>

View File

@ -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';

View File

@ -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;
}

View File

@ -273,6 +273,7 @@ export default defineComponent<CellProps>({
<Component {...componentProps}>
{appendNode}
{childNode}
{slots.dragHandle?.()}
</Component>
);
};

View File

@ -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<ColumnType<any>>,
default: undefined as ColumnType<any>,
},
},
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 (
<div
class={`${prefixCls}-resize-handle ${dragging.value ? 'dragging' : ''}`}
onMousedown={handleDown}
{...touchEvents}
onClick={handleClick}
>
<div class={`${prefixCls}-resize-handle-line`}></div>
</div>
);
};
},
});

View File

@ -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<RecordType = DefaultRecordType> {
cells: readonly CellType<RecordType>[];
@ -73,7 +74,7 @@ export default defineComponent<RowProps>({
if (column && column.customHeaderCell) {
additionalProps = cell.column.customHeaderCell(column);
}
const col: ColumnType<any> = column;
return (
<Cell
{...cell}
@ -87,7 +88,19 @@ export default defineComponent<RowProps>({
additionalProps={additionalProps}
rowType="header"
column={column}
v-slots={{ default: () => column.title }}
v-slots={{
default: () => column.title,
dragHandle: () =>
col.resizable ? (
<DragHandleVue
prefixCls={prefixCls}
width={col.width as number}
minWidth={col.minWidth}
maxWidth={col.maxWidth}
column={col}
/>
) : null,
}}
/>
);
})}

View File

@ -110,6 +110,9 @@ export interface ColumnType<RecordType> extends ColumnSharedType<RecordType> {
}) => any | RenderedCell<RecordType>;
rowSpan?: number;
width?: number | string;
minWidth?: number;
maxWidth?: number;
resizable?: boolean;
customCell?: GetComponentProps<RecordType>;
/** @deprecated Please use `onCell` instead */
onCellClick?: (record: RecordType, e: MouseEvent) => void;