react + antd + react-resizable 扩展 Table 组件,可拖拽列头和固定 Table 高度。支持复合表头的拖拽。
注释不多,代码仅供参考,请高手多多指教。
import { Table, TableColumnsType, TableColumnType, TableColumnGroupType, TableProps } from 'antd';
import { AnyObject } from 'antd/es/table/Table';
import { assign, cloneDeep } from 'lodash';
import { Dispatch, SetStateAction, useState } from 'react';
import { Resizable, ResizableProps } from 'react-resizable';
type TablePlusProps<T> = TableProps<T> & {
fixedHeight?: boolean;
width?: number;
}
const paginationSize: Record<string|symbol, number> = {
large: 32 + 32,
middle: 24 + 32,
small: 24 + 32
};
const ResizeableTitle = (props: ResizableProps) => {
const { onResize, width, ...restProps } = props;
if (!width) {
return <th {...restProps} />;
}
return (
<>
<Resizable width={width} height={0} onResize={onResize} draggableOpts={{ enableUserSelectHack: true }}
handle={
<span className="react-resizable-handle react-resizable-handle-se"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
/>
}
onResizeStart={(e) => {
const target = e.target as HTMLSpanElement;
target.style.width = '100%';
const mouseUp = () => {
target.style.width = '';
document.removeEventListener('mouseup',mouseUp);
};
document.addEventListener('mouseup',mouseUp);
}}
>
<th {...restProps} />
</Resizable>
</>
);
};
const findCellsResizeable = <T extends AnyObject> (key: string, columns: TableColumnsType<T>) => {
let column = columns.find((col) => (Reflect.get(col, 'key') || Reflect.get(col, 'dataIndex')) === key);
if(column) {
return column;
} else {
columns.filter((col) => Reflect.get(col, 'children') !== undefined)
.forEach(it => {
column = findCellsResizeable(key, Reflect.get(it, 'children'));
return column === undefined;
});
}
return column;
}
const handleCellsResizeable = <T extends AnyObject> (columns: TableColumnsType<T>, setColumns: Dispatch<SetStateAction<TableColumnsType<T>>>) => {
const resizeableCells: TableColumnsType<T> = [];
columns.forEach((col) => {
const children = Reflect.get(col, 'children');
if(children) {
handleCellsResizeable(children, setColumns).forEach(it => resizeableCells.push(it));
} else {
assign(col, {
onHeaderCell: (column: { width?: number | string }) => ({
width: column.width,
onResize: handleResize(col, setColumns),
}),
})
if(Reflect.get(col, 'autoWidth') === undefined) {
assign(col, {autoWidth: col.width === undefined});
}
resizeableCells.push(col);
}
});
return resizeableCells;
}
const handleResize = <T extends AnyObject> (col: TableColumnGroupType<T>|TableColumnType<T>, setColumns: Dispatch<SetStateAction<TableColumnsType<T>>>) =>
(e: Event, {size}: {size: {width: number}}) => {
e.stopPropagation();
e.preventDefault();
const key = Reflect.get(col, 'key') || Reflect.get(col, 'dataIndex');
setColumns((columns) => {
const nextColumns = cloneDeep(columns);
const column = findCellsResizeable(key, nextColumns);
column!.width = size.width;
return nextColumns;
});
}
function initTablePlus<T extends AnyObject> (this: unknown, table: HTMLDivElement) {
const vm = this as {
id?:string;
fixedHeight?: boolean;
pagination?: unknown;
size?:string;
autoWidthCells: TableColumnsType<T>;
fixedCellsWidth: number;
width: number;
};
const tableContainer = table.querySelector('.ant-table-container') as HTMLDivElement;
const tableHeader = table.querySelector('.ant-table-header') as HTMLDivElement;
const tableBodySelection = table.querySelector('th.ant-table-selection-column') as HTMLTableRowElement;
const tableBody = table.querySelector('.ant-table-body') as HTMLDivElement;
let cellScrollbarWidth = 0;
if(tableBody && tableHeader) {
const tableBodyPlaceholder = tableBody.querySelector('tr.ant-table-placeholder') as HTMLTableRowElement;
if(vm.fixedHeight) {
const tableBodyMaxHeigth = tableBody.style.maxHeight;
tableContainer.style.minHeight = tableContainer.style.maxHeight = `calc(${tableHeader.offsetHeight}px + ${tableBodyMaxHeigth})`;
tableBody.style.minHeight = tableBody.style.maxHeight;
table.style.marginBottom = '';
if(tableBodyPlaceholder) {
const cellScrollbarHeight = tableBody.offsetHeight - tableBody.clientHeight;
tableBodyPlaceholder.style.height =`calc(${tableBody.style.minHeight} - ${cellScrollbarHeight}px)`;
if(vm.pagination) {
table.style.marginBottom = `${paginationSize[vm.size||'large']}px`;
}
}
}
cellScrollbarWidth = tableBody.offsetWidth - tableBody.clientWidth;
}
if(0 !== vm.autoWidthCells.length) {
const autoCellsWidth = (vm.width - vm.fixedCellsWidth - cellScrollbarWidth -
(tableBodySelection ? tableBodySelection.offsetWidth : 0)) / vm.autoWidthCells.length;
vm.autoWidthCells.forEach(col => col.width = autoCellsWidth);
}
}
const ViewModal = <T extends AnyObject> (props: TablePlusProps<T>) => {
const [columns, setColumns] = useState<TableColumnsType<T>>([...props.columns||[]]);
const [width, setWidth] = useState<number|undefined>(props.width);
const [resizeableCells] = useState<TableColumnsType<T>>(handleCellsResizeable(columns, setColumns));
return {...props, columns, width, setWidth,
autoWidthCells:resizeableCells.filter(it => Reflect.get(it, 'autoWidth') === true),
fixedCellsWidth: (() => {
const fixedCells = resizeableCells.filter(it => Reflect.get(it, 'autoWidth') === false)
.map(it => parseInt(`${it.width?it.width:0}`));
return fixedCells.length !== 0 ? fixedCells.reduce((p, n) => p + n) : 0;
})(),
initTablePlus
};
}
export const TablePlus = <T extends AnyObject> (props: TablePlusProps<T>) => {
const {columns, style, width, ...tableProps} = props;
const vm = ViewModal(props);
return (
<div className="antd-table-plus" style={{width: width||style?.width}} ref={(target) => {
if(target && !vm.width) {
vm.setWidth(target.clientWidth);
}
}}>
<Table {...tableProps} style={style} columns={vm.columns} components={{header: {cell: ResizeableTitle}}}
ref={(target) => {
if(props.id) {
vm.initTablePlus(document.getElementById(props.id) as HTMLDivElement);
} else {
target && vm.initTablePlus(target.querySelector('.ant-table') as HTMLDivElement)
}
}}
/>
</div>
);
};