react + antd + react-resizable 扩展 Table 组件,可伸缩列头和固定 Table 高度

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

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值