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
	
	 tangjinzhou
						tangjinzhou