feat: add resize table column
parent
17a1ca5edf
commit
1272457517
|
@ -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(() => {
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -273,6 +273,7 @@ export default defineComponent<CellProps>({
|
|||
<Component {...componentProps}>
|
||||
{appendNode}
|
||||
{childNode}
|
||||
{slots.dragHandle?.()}
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue