import * as React from 'react'; import { get, isArrayLike, runInAction } from 'mobx'; import classNames from 'classnames'; import isFunction from 'lodash/isFunction'; import flatten from 'lodash/flatten'; import debounce from 'lodash/debounce'; import isEqual from 'lodash/isEqual'; import eq from 'lodash/eq'; import defaultTo from 'lodash/defaultTo'; import omit from 'lodash/omit'; import merge from 'lodash/merge'; import isNil from 'lodash/isNil'; import BScroll from '@better-scroll/core'; import bindElementResize, { unbind as unbindElementResize } from 'element-resize-event'; import { getTranslateDOMPositionXY } from 'dom-lib/lib/transition/translateDOMPositionXY'; import { addStyle, getHeight, getOffset, getWidth, on, scrollLeft, scrollTop, WheelHandler } from 'dom-lib'; import { DragDropContext, Draggable, DraggableChildrenFn, DraggableProps, DraggableProvided, DraggableStateSnapshot, DragStart, Droppable, DroppableProps, DroppableProvided, DropResult, ResponderProvided, } from 'react-beautiful-dnd'; import isPromise from 'is-promise'; import ConfigContext, { ConfigContextValue } from 'choerodon-ui/lib/config-provider/ConfigContext'; import { toPx } from 'choerodon-ui/lib/_util/UnitConvertor'; import LocaleReceiver from 'choerodon-ui/lib/locale-provider/LocaleReceiver'; import { PerformanceTable as PerformanceTableLocal } from 'choerodon-ui/lib/locale-provider'; import defaultLocale from 'choerodon-ui/lib/locale-provider/default'; import warning from 'choerodon-ui/lib/_util/warning'; import { RadioChangeEvent } from 'choerodon-ui/lib/radio'; import { CheckboxChangeEvent } from 'choerodon-ui/lib/checkbox'; import { stopPropagation } from '../_util/EventManager'; import ModalProvider from '../modal-provider/ModalProvider'; import Row, { RowProps } from './Row'; import CellGroup from './CellGroup'; import Scrollbar from './Scrollbar'; import SelectionBox from './SelectionBox'; import SelectionCheckboxAll from './SelectionCheckboxAll'; import TableContext from './TableContext'; import { CELL_PADDING_HEIGHT, SCROLLBAR_LARGE_WIDTH, SCROLLBAR_WIDTH } from './constants'; import { TableQueryBarType } from './enum'; import { cancelAnimationTimeout, defaultClassPrefix, findAllParents, findRowKeys, flattenData, getTotalByColumns, getUnhandledProps, isNumberOrTrue, isRTL, mergeCells, prefix, requestAnimationTimeout, resetLeftForCells, shouldShowRowByExpanded, toggleClass, } from './utils'; import isMobile from '../_util/isMobile'; import { transformZoomData } from '../_util/DocumentUtils'; import { RowDataType, SortType, StandardProps } from './common'; import ColumnGroup from './ColumnGroup'; import Column, { ColumnProps } from './Column'; import Cell from './Cell'; import HeaderCell from './HeaderCell'; import Spin from '../spin'; import PerformanceTableQueryBar from './query-bar'; import ProfessionalBar from './query-bar/TableProfessionalBar'; import DynamicFilterBar from './query-bar/TableDynamicFilterBar'; import TableStore from './TableStore'; import Toolbar, { ToolBarProps } from './tool-bar'; import { TableAutoHeightType, TableColumnResizeTriggerType, TableHeightType } from '../table/enum'; import { isDropresult } from '../table/utils'; import { arrayMove } from '../data-set/utils'; import { $l } from '../locale-context'; import DataSet from '../data-set'; import { TransportProps } from '../data-set/Transport'; import { FormProps } from '../form/Form'; export interface TableLocale { emptyMessage?: string; loading?: string; } export interface TableScrollLength { horizontal?: number; vertical?: number; } export { TableQueryBarType, }; export interface TableQueryBarProps { type?: TableQueryBarType; renderer?: (props: TableQueryBarHookProps) => React.ReactNode; dataSet?: DataSet; queryFormProps?: FormProps; defaultExpanded?: Boolean; queryDataSet?: DataSet; queryFields?: React.ReactElement<any>[]; queryFieldsLimit?: number; onQuery?: (props: object) => void; onReset?: () => void; } export interface TableQueryBarHookProps { dataSet: DataSet; queryDataSet?: DataSet; queryFields: React.ReactElement<any>[]; queryFieldsLimit?: number; onQuery?: (props: object) => void; onReset?: () => void; } export interface DynamicFilterBarConfig { searchCode: string; searchText?: string; quickSearch?: boolean; suffixes?: React.ReactElement<any>[]; prefixes?: React.ReactElement<any>[]; tableFilterAdapter?: TransportProps; } export interface PerformanceTableCustomized { columns: object; heightType?: TableHeightType; height?: number; heightDiff?: number; } export type RowSelectionType = 'checkbox' | 'radio'; export type SelectionSelectFn = ( record: object, selected: boolean, selectedRows: Object[], nativeEvent: Event, ) => void; export type TableSelectWay = 'onSelect' | 'onSelectMultiple' | 'onSelectAll' | 'onSelectInvert'; export type SelectionItemSelectFn = (key: string[]) => void; export interface SelectionItem { key: string; text: React.ReactNode; onSelect?: SelectionItemSelectFn; } export interface TableRowSelection { type?: RowSelectionType; selectedRowKeys?: string[] | number[]; onChange?: (selectedRowKeys: string[] | number[], selectedRows: object[]) => void; getCheckboxProps?: (record: object) => Object; onSelect?: SelectionSelectFn; onSelectMultiple?: (selected: boolean, selectedRows: object[], changeRows: object[]) => void; onSelectAll?: (selected: boolean, selectedRows: object[], changeRows: object[]) => void; onSelectInvert?: (selectedRowKeys: string[] | number[]) => void; selections?: SelectionItem[] | boolean; hideDefaultSelections?: boolean; fixed?: boolean | string; columnWidth?: number; selectWay?: TableSelectWay; columnTitle?: string | React.ReactNode; columnIndex?: number; } export interface SelectionCheckboxAllProps { store: any; disabled: boolean; getCheckboxPropsByItem: (item: object, index: number) => { defaultChecked: boolean }; getRecordKey: (record: object, index?: number) => string; data: object[]; prefixCls: string | undefined; onSelect: (key: string, index: number, selectFunc: any) => void; hideDefaultSelections?: boolean; selections?: SelectionItem[] | boolean; } export interface SelectionCheckboxAllState { checked?: boolean; indeterminate?: boolean; } export interface SelectionBoxProps { store: any; type?: RowSelectionType; defaultSelection: string[]; rowIndex: string; name?: string; disabled?: boolean; onChange: (e: RadioChangeEvent | CheckboxChangeEvent) => void; } export interface SelectionBoxState { checked?: boolean; } export interface SelectionInfo { selectWay: TableSelectWay; record?: object; checked?: boolean; changeRowKeys?: React.Key[]; nativeEvent?: Event; } export interface ColumnRenderIcon { column: ColumnProps; dataSet?: DataSet | undefined; snapshot?: DraggableStateSnapshot; } export interface DragRender { droppableProps?: DroppableProps; draggableProps?: DraggableProps; renderClone?: DraggableChildrenFn; renderIcon?: ((rowRenderIcon: ColumnRenderIcon) => React.ReactElement<any>); } export interface TableProps extends StandardProps { /** 左上角的 title */ headerTitle?: React.ReactNode; rowSelection?: TableRowSelection; queryBar?: false | TableQueryBarProps; components?: TableComponents; toolbar?: ToolBarProps, /** 渲染操作栏 */ toolBarRender?: ToolBarProps['toolBarRender'] | false, columns?: ColumnProps[]; autoHeight?: boolean | { type: TableAutoHeightType; diff: number }; affixHeader?: boolean | number; affixHorizontalScrollbar?: boolean | number; bodyRef?: (ref: HTMLElement) => void; bordered?: boolean; className?: string; classPrefix?: string; children?: React.ReactNode; cellBordered?: boolean; defaultSortType?: SortType; disabledScroll?: boolean; defaultExpandAllRows?: boolean; defaultExpandedRowKeys?: string[] | number[]; data: object[]; expandedRowKeys?: string[] | number[]; height?: number; hover?: boolean; headerHeight?: number; locale?: TableLocale; clickScrollLength?: TableScrollLength, loading?: boolean; loadAnimation?: boolean; minHeight?: number; rowHeight?: number | ((rowData: object) => number); rowKey?: string; isTree?: boolean; rowExpandedHeight?: number; rowClassName?: string | ((rowData: object) => string); showHeader?: boolean; showScrollArrow?: boolean; style?: React.CSSProperties; sortColumn?: string; sortType?: SortType; shouldUpdateScroll?: boolean; translate3d?: boolean; rtl?: boolean; width?: number; wordWrap?: boolean; virtualized?: boolean; renderTreeToggle?: ( expandButton: React.ReactNode, rowData?: RowDataType, expanded?: boolean, ) => React.ReactNode; renderRowExpanded?: (rowDate?: object) => React.ReactNode; renderEmpty?: (info: React.ReactNode) => React.ReactNode; renderLoading?: (loading: React.ReactNode) => React.ReactNode; onRowClick?: (rowData: object, event: React.MouseEvent) => void; onRowContextMenu?: (rowData: object, event: React.MouseEvent) => void; onScroll?: (scrollX: number, scrollY: number) => void; onSortColumn?: (dataKey: string, sortType?: SortType) => void; onExpandChange?: (expanded: boolean, rowData: object) => void; onTouchStart?: (event: React.TouchEvent) => void; // for tests onTouchMove?: (event: React.TouchEvent) => void; // for tests onDataUpdated?: (nextData: object[], scrollTo: (coord: { x: number; y: number }) => void) => void; customizedCode?: string; customizable?: boolean; columnDraggable?: boolean; columnTitleEditable?: boolean; columnsDragRender?: DragRender; rowDraggable?: boolean; onDragStart?: (initial: DragStart, provided: ResponderProvided) => void; onDragEnd?: (result: DropResult, provided: ResponderProvided, data: object) => void; onDragEndBefore?: (result: DropResult, provided: ResponderProvided) => boolean; } export type CustomizeComponent = React.Component<any>; export interface TableComponents { table?: CustomizeComponent; header?: { wrapper?: CustomizeComponent; row?: CustomizeComponent; cell?: CustomizeComponent; }; body?: { wrapper?: CustomizeComponent; row?: CustomizeComponent; cell?: CustomizeComponent; }; } interface TableRowProps extends RowProps { key?: string | number; depth?: number; } const SORT_TYPE = { DESC: 'desc', ASC: 'asc', }; type Offset = { top?: number; left?: number; width?: number; height?: number; }; type StartRowSpan = { rowIndex: number; rowSpan: number; height: number; } interface ColumnCellProps extends ColumnProps { parent?: React.ReactElement; } interface TableState { headerOffset?: Offset; tableOffset?: Offset; width: number; columnWidth: number; dataKey: number; shouldFixedColumn: boolean; contentHeight: number; contentWidth: number; tableRowsMaxHeight: number[]; isColumnResizing?: boolean; expandedRowKeys: string[] | number[]; searchText: string; sortType?: SortType; scrollY: number; isScrolling?: boolean; data: object[]; cacheData: object[]; fixedHeader: boolean; fixedHorizontalScrollbar?: boolean; isTree?: boolean; selectedRowKeys: string[] | number[]; selectionDirty?: boolean; [key: string]: any; } const propTypeKeys = [ 'columns', 'autoHeight', 'affixHeader', 'affixHorizontalScrollbar', 'bordered', 'bodyRef', 'className', 'classPrefix', 'children', 'cellBordered', 'clickScrollLength', 'data', 'defaultExpandAllRows', 'defaultExpandedRowKeys', 'defaultSortType', 'disabledScroll', 'expandedRowKeys', 'hover', 'height', 'headerHeight', 'locale', 'loading', 'loadAnimation', 'minHeight', 'rowKey', 'rowHeight', 'renderTreeToggle', 'renderRowExpanded', 'rowExpandedHeight', 'renderEmpty', 'renderLoading', 'rowClassName', 'rtl', 'style', 'sortColumn', 'sortType', 'showHeader', 'showScrollArrow', 'shouldUpdateScroll', 'translate3d', 'wordWrap', 'width', 'virtualized', 'isTree', 'onRowClick', 'onRowContextMenu', 'onScroll', 'onSortColumn', 'onExpandChange', 'onTouchStart', 'onTouchMove', 'onDataUpdated', 'highLightRow', 'queryBar', 'customizedCode', 'customizable', 'columnDraggable', 'columnTitleEditable', 'columnsDragRender', 'rowSelection', 'rowDraggable', 'onDragEndBefore', 'onDragEnd', 'onDragStart', ]; export const CUSTOMIZED_KEY = '__customized-column__'; // TODO:Symbol function getRowSelection(props: TableProps): TableRowSelection { return props.rowSelection || {}; } //计算两根手指之间的距离 function getDistance(start, stop) { return Math.sqrt(Math.pow(Math.abs(start.x - stop.x), 2) + Math.pow(Math.abs(start.y - stop.y), 2)); }; export default class PerformanceTable extends React.Component<TableProps, TableState> { static displayName = 'performance'; static Column = Column; static Row = Row; static Cell = Cell; static CellGroup = CellGroup; static ColumnGroup = ColumnGroup; static HeaderCell = HeaderCell; static ProfessionalBar = ProfessionalBar; static DynamicFilterBar = DynamicFilterBar; static defaultProps = { classPrefix: defaultClassPrefix('performance-table'), data: [], defaultSortType: SORT_TYPE.ASC, height: 200, rowHeight: 33, headerHeight: 33, minHeight: 0, rowExpandedHeight: 100, hover: true, highLightRow: true, showHeader: true, showScrollArrow: false, bordered: true, rowKey: 'key', translate3d: true, shouldUpdateScroll: true, locale: { emptyMessage: 'No data found', loading: 'Loading...', }, clickScrollLength: { horizontal: 100, vertical: 33, }, }; static getDerivedStateFromProps(props: TableProps, state: TableState) { if (props.data !== state.cacheData || props.isTree !== state.isTree) { return { cacheData: props.data, isTree: props.isTree, data: props.isTree ? flattenData(props.data) : props.data, }; } return null; } static get contextType(): typeof ConfigContext { return ConfigContext; } context: ConfigContextValue; translateDOMPositionXY = null; scrollListener: any = null; bscroll: any = null; tableRef: React.RefObject<any>; scrollbarYRef: React.RefObject<any>; scrollbarXRef: React.RefObject<any>; tableBodyRef: React.RefObject<any>; affixHeaderWrapperRef: React.RefObject<any>; mouseAreaRef: React.RefObject<any>; headerWrapperRef: React.RefObject<any>; tableHeaderRef: React.RefObject<any>; wheelWrapperRef: React.RefObject<any>; tableRows: { [key: string]: [HTMLElement, any] } = {}; mounted = false; disableEventsTimeoutId = null; scrollY = 0; scrollX = 0; wheelHandler: any; minScrollY: any; minScrollX: any; mouseArea: any; touchX: any; touchY: any; wheelListener: any; touchStartListener: any; touchMoveListener: any; nextRowZIndex: Array<number> = []; calcStartRowSpan: StartRowSpan = { rowIndex: 0, rowSpan: 0, height: 0 }; _cacheCalcStartRowSpan: Array<StartRowSpan> = []; // 缓存合并行的计算结果 _cacheCells: any = null; _cacheScrollX: number = 0; _cacheRenderCols: any = []; _cacheChildrenSize = 0; _visibleRows: any = []; _lastRowIndex: string | number; tableStore: TableStore = new TableStore(this); scaleStore = { firstDistance: 0, // 手指距离 centerPoint: [0, 0], // 中心点坐标 scale: 1, // 缩放比例 originScale: 1 // 原始缩放比例 } constructor(props: TableProps, context: ConfigContextValue) { super(props, context); const { width, height, data, rowKey, defaultExpandAllRows, renderRowExpanded, defaultExpandedRowKeys, children = [], columns = [], isTree, defaultSortType, } = props; const expandedRowKeys = defaultExpandAllRows ? findRowKeys(data, rowKey!, isFunction(renderRowExpanded)) : defaultExpandedRowKeys || []; let shouldFixedColumn = Array.from(children as Iterable<any>).some( (child: any) => child && child.props && child.props.fixed, ); if (columns && columns.length) { shouldFixedColumn = Array.from(columns as Iterable<any>).some( (child: any) => child && child.fixed, ); } if (isTree && !rowKey) { throw new Error('The `rowKey` is required when set isTree'); } this.state = { isTree, expandedRowKeys, shouldFixedColumn, cacheData: data, data: isTree ? flattenData(data) : data, width: width || 0, height: height || 0, columnWidth: 0, dataKey: 0, contentHeight: 0, contentWidth: 0, tableRowsMaxHeight: [], sortType: defaultSortType, scrollY: 0, isScrolling: false, fixedHeader: false, searchText: '', pivot: undefined, selectedRowKeys: [], dragRowIndex: '', }; this.scrollY = 0; this.scrollX = 0; this._cacheScrollX = 0; this._cacheRenderCols = []; this.wheelHandler = new WheelHandler( this.listenWheel, this.shouldHandleWheelX, this.shouldHandleWheelY, false, ); this._cacheChildrenSize = flatten(children as any[] || columns).length; this.translateDOMPositionXY = getTranslateDOMPositionXY({ enable3DTransform: props.translate3d, }); this.tableRef = React.createRef(); this.scrollbarYRef = React.createRef(); this.scrollbarXRef = React.createRef(); this.tableBodyRef = React.createRef(); this.affixHeaderWrapperRef = React.createRef(); this.mouseAreaRef = React.createRef(); this.headerWrapperRef = React.createRef(); this.wheelWrapperRef = React.createRef(); this.tableHeaderRef = React.createRef(); runInAction(() => this.setSelectionColumn(props)); } setSelectionColumn = (props) => { const { rowSelection, columns = [], children = [] } = props; const index = columns.findIndex(column => column.key === 'rowSelection'); if (rowSelection) { if (index > -1) columns.splice(index, 1); let rowSelectionFixed: any = 'left'; if ('fixed' in rowSelection) { rowSelectionFixed = rowSelection.fixed; } if (columns && columns.length) { const columnsWithRowSelectionProps: ColumnProps = { title: $l('Table', 'select_current_page'), key: 'rowSelection', width: 50, align: 'center', fixed: rowSelectionFixed, }; columns.splice(rowSelection.columnIndex || 0, 0, columnsWithRowSelectionProps); } if (children && (children as any[]).length) { const columnsWithRowSelection = this.renderRowSelection(rowSelectionFixed); if (columnsWithRowSelection) { if (rowSelectionFixed) { (children as any[]).splice((rowSelectionFixed === true || 'left') ? (rowSelection.columnIndex || 0) : (rowSelection.columnIndex || (children as any[]).length), 0, columnsWithRowSelection); this.setState({ shouldFixedColumn: true }); } else { (children as any[]).splice(rowSelection.columnIndex || 0, 0, columnsWithRowSelection); } } } } this.tableStore.originalColumns = columns; this.tableStore.originalChildren = children as any[]; }; listenWheel = (deltaX: number, deltaY: number) => { this.handleWheel(deltaX, deltaY); const xRef = this.scrollbarXRef.current; if (xRef && xRef.onWheelScroll) { xRef.onWheelScroll(deltaX); } const yRef = this.scrollbarYRef.current; if (yRef && yRef.onWheelScroll) { yRef.onWheelScroll(deltaY); } }; componentDidMount() { this.calculateTableWidth(); this.calculateTableContextHeight(); this.calculateRowMaxHeight(); this.setOffsetByAffix(); this.initPosition(); bindElementResize(this.tableRef.current, debounce(this.calculateTableWidth, 400)); const options = { passive: false }; const tableBody = this.tableBodyRef.current; const wheelWrapper = this.wheelWrapperRef.current; if (tableBody) { if (isMobile()) { this.initBScroll(tableBody); // 移动端适配缩放功能 if (wheelWrapper) { this.touchStartListener = on(wheelWrapper, 'touchstart', this.handleWheelTouchStart); this.touchMoveListener = on(wheelWrapper, 'touchmove', this.handleWheelTouchMove); } } this.wheelListener = on(tableBody, 'wheel', this.wheelHandler.onWheel, options); // this.touchStartListener = on(tableBody, 'touchstart', this.handleTouchStart, options); // this.touchMoveListener = on(tableBody, 'touchmove', this.handleTouchMove, options); } const { affixHeader, affixHorizontalScrollbar, bodyRef } = this.props; if (isNumberOrTrue(affixHeader) || isNumberOrTrue(affixHorizontalScrollbar)) { this.scrollListener = on(window, 'scroll', this.handleWindowScroll); } if (bodyRef) { bodyRef(this.wheelWrapperRef.current); } } shouldComponentUpdate(nextProps: TableProps, nextState: TableState) { const _cacheChildrenSize = flatten((nextProps.children as any[] || nextProps.columns) || []).length; /** * 单元格列的信息在初始化后会被缓存,在某些属性被更新以后,需要清除缓存。 */ if (_cacheChildrenSize !== this._cacheChildrenSize) { this._cacheChildrenSize = _cacheChildrenSize; this._cacheCells = null; this.tableStore.updateProps(nextProps, this); } else if ( this.props.children !== nextProps.children || this.props.columns !== nextProps.columns || this.props.sortColumn !== nextProps.sortColumn || this.props.sortType !== nextProps.sortType ) { this._cacheCells = null; this.tableStore.updateProps(nextProps, this); } else if (this.props.data !== nextProps.data && this._cacheCells) { const { rowSelection } = nextProps; if (rowSelection && rowSelection.type !== 'radio') { const flatData = nextState.data.filter((item, index) => { if (rowSelection.getCheckboxProps) { return !this.getCheckboxPropsByItem(item, index).disabled; } return true; }); const checkboxAllDisabled = flatData.every( (item, index) => this.getCheckboxPropsByItem(item, index).disabled, ); const checkboxAllHeaderCell = React.cloneElement( this._cacheCells.headerCells[0], { children: React.cloneElement( this._cacheCells.headerCells[0].props.children, { disabled: checkboxAllDisabled, data: flatData, }, ), }, ); this._cacheCells.headerCells[0] = checkboxAllHeaderCell; } } const flag = this.props.columns !== nextProps.columns || this.props.children !== nextProps.children || this.props.rowSelection !== nextProps.rowSelection; if (flag) { runInAction(() => this.setSelectionColumn(nextProps)); } return !eq(this.props, nextProps) || !isEqual(this.state, nextState); } componentDidUpdate(prevProps: TableProps, prevState: TableState) { const { rowHeight, data, autoHeight, height, virtualized, children, columns } = prevProps; const { props, state } = this; const { data: nextData, autoHeight: nextAutoHeight, onDataUpdated, shouldUpdateScroll, columns: nextColumns, rowSelection, children: nextChildren } = props; if (data !== nextData) { this.calculateRowMaxHeight(); if (onDataUpdated) { onDataUpdated(nextData, this.scrollTo); } const maxHeight = nextData.length * (typeof rowHeight === 'function' ? rowHeight({}) : rowHeight!); // 当开启允许更新滚动条,或者滚动条位置大于表格的最大高度,则初始滚动条位置 if (shouldUpdateScroll || Math.abs(this.scrollY) > maxHeight) { this.scrollTo({ x: 0, y: 0 }); } } else { this.updatePosition(); } if (columns !== nextColumns || children !== nextChildren || this.tableStore.customizable) { let shouldFixedColumn = false; if ((!columns || columns.length === 0) && rowSelection && nextColumns && nextColumns.length) { let rowSelectionFixed: any = 'left'; if ('fixed' in rowSelection) { rowSelectionFixed = rowSelection.fixed; } const columnsWithRowSelectionProps: ColumnProps = { title: $l('Table', 'select_current_page'), key: 'rowSelection', width: 50, align: 'center', fixed: rowSelectionFixed, }; runInAction(() => { this.tableStore.originalColumns = nextColumns.splice(rowSelection.columnIndex || 0, 0, columnsWithRowSelectionProps); }); } if (rowSelection) { runInAction(() => { this.tableStore.selectedRowKeys = rowSelection.selectedRowKeys || []; }); } if (nextChildren) { shouldFixedColumn = Array.from(nextChildren as Iterable<any>).some( (child: any) => child && child.props && child.props.fixed, ); } const { originalColumns } = this.tableStore; if (originalColumns && originalColumns.length) { shouldFixedColumn = Array.from(originalColumns as Iterable<any>).some( (child: any) => child && child.fixed, ); } this.setState({ shouldFixedColumn: shouldFixedColumn }); } if ( // 当 Table 的 data 发生变化,需要重新计算高度 data !== nextData || // 当 Table 内容区的高度发生变化需要重新计算 height !== props.height || autoHeight !== nextAutoHeight || // 当 Table 内容区的高度发生变化需要重新计算 prevState.contentHeight !== state.contentHeight || // 当 expandedRowKeys 发生变化,需要重新计算 Table 高度,如果重算会导致滚动条不显示。 prevState.expandedRowKeys !== state.expandedRowKeys || prevProps.expandedRowKeys !== props.expandedRowKeys ) { this.calculateTableContextHeight(prevProps); } this.calculateTableContentWidth(prevProps); if (virtualized) { this.calculateTableWidth(); } const tableBody = this.tableBodyRef.current; if (!this.wheelListener && tableBody) { const options = { passive: false }; if (isMobile()) { this.initBScroll(tableBody); } this.wheelListener = on(tableBody, 'wheel', this.wheelHandler.onWheel, options); } } componentWillUnmount() { this.wheelHandler = null; const { current } = this.tableRef; if (current) { unbindElementResize(current); } const { wheelListener, touchStartListener, touchMoveListener, scrollListener } = this; if (wheelListener) { wheelListener.off(); } if (touchStartListener) { touchStartListener.off(); } if (touchMoveListener) { touchMoveListener.off(); } if (scrollListener) { scrollListener.off(); } } getExpandedRowKeys() { const { expandedRowKeys } = this.props; return typeof expandedRowKeys === 'undefined' ? this.state.expandedRowKeys : expandedRowKeys; } getSortType() { const { sortType } = this.props; return typeof sortType === 'undefined' ? this.state.sortType : sortType; } getScrollCellGroups() { const { current } = this.tableRef; return current && current.querySelectorAll(`.${this.addPrefix('cell-group-scroll')}`); } getFixedLeftCellGroups() { const { current } = this.tableRef; return current && current.querySelectorAll(`.${this.addPrefix('cell-group-fixed-left')}`); } getFixedRightCellGroups() { const { current } = this.tableRef; return current && current.querySelectorAll(`.${this.addPrefix('cell-group-fixed-right')}`); } isRTL() { return this.props.rtl || isRTL(); } getRowHeight(rowData = {}) { const { rowHeight } = this.props; return typeof rowHeight === 'function' ? rowHeight(rowData) : rowHeight!; } get getScrollBarYWidth(): number { const { contentHeight } = this.state; const { showScrollArrow } = this.props; const height = this.getTableHeight(); if (contentHeight > height && !!this.scrollbarYRef) { return showScrollArrow ? SCROLLBAR_LARGE_WIDTH : SCROLLBAR_WIDTH; } return 0; } /** * 获取表头高度 */ getTableHeaderHeight() { const { headerHeight, showHeader } = this.props; return showHeader ? headerHeight! : 0; } /** * Table 个性化高度变更 */ handleHeightTypeChange() { this.calculateTableContextHeight(); } /** * 获取 Table 需要渲染的高度 */ getTableHeight() { const { contentHeight } = this.state; const { minHeight, height, data, showScrollArrow } = this.props; const headerHeight = this.getTableHeaderHeight(); const { tableStore: { customized: { heightType, height: cusHeight, heightDiff }, tempCustomized, autoHeight, }, } = this; let tableHeight: number = Math.max(height!, minHeight!); if (this.tableStore.customizable) { const tempHeightType = get(tempCustomized, 'heightType'); if (tempHeightType) { if (tempHeightType === TableHeightType.fixed) { tableHeight = get(tempCustomized, 'height'); } if (tempHeightType === TableHeightType.flex) { tableHeight = document.documentElement.clientHeight - (get(tempCustomized, 'heightDiff') || 0); } } if (heightType) { if (heightType === TableHeightType.fixed) { tableHeight = cusHeight as number; } if (heightType === TableHeightType.flex) { tableHeight = document.documentElement.clientHeight - (heightDiff || 0); } } this.setState({ height: tableHeight, }); } if ((data.length === 0 && !autoHeight)) { return tableHeight; } if (autoHeight) { if (typeof autoHeight === 'boolean') { return Math.max(headerHeight + contentHeight, minHeight!) + (showScrollArrow ? SCROLLBAR_LARGE_WIDTH : SCROLLBAR_WIDTH); } else { const tableRef = this.tableRef && this.tableRef.current; if (!tableRef) return tableHeight; const { parentNode } = tableRef; const { offsetHeight } = parentNode const { type, diff } = autoHeight; if (type === TableAutoHeightType.minHeight) { return offsetHeight - diff; } else { return Math.min(offsetHeight, headerHeight + contentHeight + (showScrollArrow ? SCROLLBAR_LARGE_WIDTH : SCROLLBAR_WIDTH) + diff) - diff; } } } return tableHeight; } /** * 处理 column props * @param column */ getColumnProps(column) { return omit(column, ['title', 'dataIndex', 'key', 'render']); } /** * 处理columns json -> reactNode * @param columns */ processTableColumns(columns: any[]) { const visibleColumn = columns.filter(col => !col.hidden); const { components } = this.props; return visibleColumn.map((column) => { const dataKey = column.dataIndex; if (column.type === 'ColumnGroup') { return <ColumnGroup {...this.getColumnProps(column)}>{this.processTableColumns(column.children)}</ColumnGroup>; } if (column.key === 'rowSelection') { return this.renderRowSelection(column.fixed); } return ( // @ts-ignore <Column {...this.getColumnProps(column)} dataKey={dataKey} key={dataKey}> { <HeaderCell> {components && components.header && components.header.cell && React.isValidElement(components.header.cell) ? React.cloneElement(components.header.cell, { ...column }) : (typeof column.title === 'function' ? column.title() : column.title)} </HeaderCell> } {typeof column.render === 'function' ? ( <Cell dataKey={dataKey}> { (rowData, rowIndex) => column.render!({ rowData, rowIndex, dataIndex: dataKey }) } </Cell> ) : ( components && components.body && components.body.cell && React.isValidElement(components.body.cell) ? React.cloneElement(components.body.cell, { ...column, dataKey }) : <Cell dataKey={dataKey} /> )} </Column> ); }); } getFlattenColumn(column: React.ReactElement, cellProps: ColumnCellProps, array: Array<React.ReactElement>) { const { header, children: childColumns, align, fixed, verticalAlign } = column.props; for (let index = 0; index < childColumns.length; index += 1) { const childColumn = childColumns[index]; if (childColumn) { const { verticalAlign: childVerticalAlign, align: childAlign } = childColumn.props; const parentProps = { align: childAlign || align, fixed, verticalAlign: childVerticalAlign || verticalAlign, ...cellProps, }; const groupCellProps: any = { parent: column, ...childColumn.props, ...parentProps, }; if (index === 0) { groupCellProps.groupCount = childColumns.length; groupCellProps.groupHeader = header; } if (childColumn.type && (childColumn.type as typeof ColumnGroup).__PRO_TABLE_COLUMN_GROUP) { const res = this.getFlattenColumn(childColumn, { ...parentProps, parent: column }, array); array.concat(res); } else { array.push(React.cloneElement(childColumn, groupCellProps)); } } } return array; } /** * 获取 columns ReactElement 数组 * - 处理 children 中存在 <Column> 数组的情况 * - 过滤 children 中的空项 */ getTableColumns(): React.ReactNodeArray { const { originalColumns, originalChildren } = this.tableStore; let children = originalChildren; if (originalColumns && originalColumns.length) { children = this.processTableColumns(originalColumns); } if (!Array.isArray(children) && !isArrayLike(children)) { return children as React.ReactNodeArray; } // Fix that the `ColumnGroup` array cannot be rendered in the Table const flattenColumns = flatten(children).map((column: React.ReactElement) => { if (column) { const columnChildren: any = column.props.children; let cellProps: ColumnProps = { dataIndex: columnChildren.length > 1 ? columnChildren[1].props.dataKey : columnChildren[0].props.dataKey, }; cellProps.hidden = column.props.hidden; if (column.type && (column.type as typeof ColumnGroup).__PRO_TABLE_COLUMN_GROUP) { return this.getFlattenColumn(column, cellProps, []); } return React.cloneElement(column, cellProps); } return column; }); // 把 Columns 中的数组,展平为一维数组,计算 lastColumn 与 firstColumn。 const flatColumns = flatten(flattenColumns).filter(col => col && col.props && !col.props.hidden); /** * 将左固定列、右固定列和其他列提取出来 * 排列成正常显示列的顺序 * 提供给后面bodyCell使用 */ const { fixedLeftCells, fixedRightCells, scrollCells } = this.calculateFixedAndScrollColumn(flatColumns); // 重新计算列的时候需清除在虚拟滚动时候的渲染列缓存 this._cacheRenderCols = []; return [...fixedLeftCells, ...scrollCells, ...fixedRightCells]; } getRecordKey = (record: object, index: number) => { const { rowKey } = this.props; const recordKey = record[rowKey!]; warning( recordKey !== undefined, 'Each record in dataSource of table should have a unique `key` prop, ' + 'or set `rowKey` of Table to an unique primary key.', ); return recordKey === undefined ? index : recordKey; }; getCheckboxPropsByItem = (item: object, index: number) => { const rowSelection = getRowSelection(this.props); if (!rowSelection.getCheckboxProps) { return {}; } const key = this.getRecordKey(item, index); // Cache checkboxProps if (!this.tableStore.checkboxPropsCache[key]) { this.tableStore.checkboxPropsCache[key] = rowSelection.getCheckboxProps(item) || {}; const checkboxProps = this.tableStore.checkboxPropsCache[key]; warning( !('checked' in checkboxProps) && !('defaultChecked' in checkboxProps), 'Do not set `checked` or `defaultChecked` in `getCheckboxProps`. Please use `selectedRowKeys` instead.', ); } return this.tableStore.checkboxPropsCache[key]; }; getDefaultSelection() { const rowSelection = getRowSelection(this.props); if (!rowSelection.getCheckboxProps) { return []; } return this.state.data .filter((item: any, rowIndex) => this.getCheckboxPropsByItem(item, rowIndex).defaultChecked) .map((record, rowIndex) => this.getRecordKey(record, rowIndex)); } handleSelect = (record: object, rowIndex: number, e: CheckboxChangeEvent) => { const checked = e.target.checked; const nativeEvent = e.nativeEvent; const defaultSelection = this.tableStore.selectionDirty ? [] : this.getDefaultSelection(); // @ts-ignore let selectedRowKeys = this.tableStore.selectedRowKeys.concat(defaultSelection); const key = this.getRecordKey(record, rowIndex); const { pivot, data } = this.state; const rows = { ...data }; let realIndex = rowIndex; if (this.props.expandedRowRender) { realIndex = rows.findIndex(row => this.getRecordKey(row, rowIndex) === key); } if (nativeEvent.shiftKey && pivot !== undefined && realIndex !== pivot) { const changeRowKeys: string[] = []; const direction = Math.sign(pivot - realIndex); const dist = Math.abs(pivot - realIndex); let step = 0; while (step <= dist) { const i = realIndex + step * direction; step += 1; const row = rows[i]; const rowKey = this.getRecordKey(row, i); const checkboxProps = this.getCheckboxPropsByItem(row, i); if (!checkboxProps.disabled) { if (selectedRowKeys.includes(rowKey)) { if (!checked) { selectedRowKeys = selectedRowKeys.filter((j: string) => rowKey !== j); changeRowKeys.push(rowKey); } } else if (checked) { selectedRowKeys.push(rowKey); changeRowKeys.push(rowKey); } } } this.setState({ pivot: realIndex }); this.tableStore.selectionDirty = true; this.setSelectedRowKeys(selectedRowKeys, { selectWay: 'onSelectMultiple', record, checked, changeRowKeys, nativeEvent, }); } else { if (checked) { selectedRowKeys.push(this.getRecordKey(record, realIndex)); } else { selectedRowKeys = selectedRowKeys.filter((i: string) => key !== i); } this.setState({ pivot: realIndex }); this.setSelectedRowKeys(selectedRowKeys, { selectWay: 'onSelect', record, checked, changeRowKeys: undefined, nativeEvent, }); } }; handleSelectRow = (selectionKey: string, index: number, onSelectFunc: SelectionItemSelectFn) => { const { data } = this.state; const defaultSelection = this.tableStore.selectionDirty ? [] : this.getDefaultSelection(); //@ts-ignore const selectedRowKeys = this.tableStore.selectedRowKeys.concat(defaultSelection); const changeableRowKeys = data .filter((item, i) => !this.getCheckboxPropsByItem(item, i).disabled) .map((item, i) => this.getRecordKey(item, i)); const changeRowKeys: string[] = []; let selectWay: TableSelectWay = 'onSelectAll'; let checked; // handle default selection switch (selectionKey) { case 'all': changeableRowKeys.forEach(key => { if (selectedRowKeys.indexOf(key) < 0) { selectedRowKeys.push(key); changeRowKeys.push(key); } }); selectWay = 'onSelectAll'; checked = true; break; case 'removeAll': changeableRowKeys.forEach(key => { if (selectedRowKeys.indexOf(key) >= 0) { selectedRowKeys.splice(selectedRowKeys.indexOf(key), 1); changeRowKeys.push(key); } }); selectWay = 'onSelectAll'; checked = false; break; case 'invert': changeableRowKeys.forEach(key => { if (selectedRowKeys.indexOf(key) < 0) { selectedRowKeys.push(key); } else { selectedRowKeys.splice(selectedRowKeys.indexOf(key), 1); } changeRowKeys.push(key); selectWay = 'onSelectInvert'; }); break; default: break; } this.tableStore.selectionDirty = true; // when select custom selection, callback selections[n].onSelect const { rowSelection } = this.props; let customSelectionStartIndex = 2; if (rowSelection && rowSelection.hideDefaultSelections) { customSelectionStartIndex = 0; } if (index >= customSelectionStartIndex && typeof onSelectFunc === 'function') { return onSelectFunc(changeableRowKeys); } this.setSelectedRowKeys(selectedRowKeys, { selectWay, checked, changeRowKeys, }); }; handleRadioSelect = (record: object, rowIndex: number, e: RadioChangeEvent) => { const checked = e.target.checked; const nativeEvent = e.nativeEvent; const key = this.getRecordKey(record, rowIndex); const selectedRowKeys = [key]; this.tableStore.selectionDirty = true; this.setSelectedRowKeys(selectedRowKeys, { selectWay: 'onSelect', record, checked, changeRowKeys: undefined, nativeEvent, }); }; renderSelectionBox = (type: RowSelectionType | undefined, rowData: object, rowIndex: number) => { const rowKey = this.getRecordKey(rowData, rowIndex); const props = this.getCheckboxPropsByItem(rowData, rowIndex); const handleChange = (e: RadioChangeEvent | CheckboxChangeEvent) => type === 'radio' ? this.handleRadioSelect(rowData, rowIndex, e) : this.handleSelect(rowData, rowIndex, e); return ( <span onClick={stopPropagation}> <SelectionBox store={this.tableStore} type={type} rowIndex={rowKey} onChange={handleChange} defaultSelection={this.getDefaultSelection()} {...props} /> </span> ); }; renderRowSelection(fixed) { const { rowSelection, classPrefix, rowKey } = this.props; if (rowSelection) { const flatData = this.state.data.filter((item, index) => { if (rowSelection.getCheckboxProps) { return !this.getCheckboxPropsByItem(item, index).disabled; } return true; }); const selectionColumn: any = { key: 'selection-column', fixed, width: rowSelection.columnWidth || 50, title: rowSelection.columnTitle, }; let selectionCheckboxAll: any = null; if (rowSelection.type !== 'radio') { const checkboxAllDisabled = flatData.every( (item, index) => this.getCheckboxPropsByItem(item, index).disabled, ); selectionCheckboxAll = rowSelection.columnTitle || ( <SelectionCheckboxAll store={this.tableStore} data={flatData} getCheckboxPropsByItem={this.getCheckboxPropsByItem} getRecordKey={this.getRecordKey} disabled={checkboxAllDisabled} prefixCls={classPrefix} onSelect={this.handleSelectRow} selections={rowSelection.selections} hideDefaultSelections={rowSelection.hideDefaultSelections} /> ); } return ( <Column key={selectionColumn.key} width={selectionColumn.width} align="center" fixed={fixed} > <HeaderCell> {selectionCheckboxAll} </HeaderCell> <Cell dataKey={rowKey}> {(rowData, rowIndex) => this.renderSelectionBox(rowSelection.type, rowData, rowIndex)} </Cell> </Column> ); } } getCellDescriptor() { if (this._cacheCells) { return this._cacheCells; } let hasCustomTreeCol = false; let left = 0; // Cell left margin const headerCells = []; // Table header cell const bodyCells = []; // Table body cell const { children, columns: columnsJson } = this.props; let columnHack = children; if (columnsJson && columnsJson.length) { columnHack = columnsJson; } if (!columnHack) { this._cacheCells = { headerCells, bodyCells, hasCustomTreeCol, allColumnsWidth: left, }; return this._cacheCells; } const columns = this.getTableColumns(); const { width: tableWidth } = this.state; const { sortColumn, rowHeight, showHeader } = this.props; const { totalFlexGrow, totalWidth } = getTotalByColumns(columns, this.state); const headerHeight = this.getTableHeaderHeight(); React.Children.forEach(columns, (column, index) => { if (React.isValidElement(column)) { const columnChildren = column.props.children; const { width, resizable, flexGrow, minWidth, onResize, treeCol } = column.props; if (treeCol) { hasCustomTreeCol = true; } if (resizable && flexGrow) { console.warn( `Cannot set 'resizable' and 'flexGrow' together in <Column>, column index: ${index}`, ); } if (columnChildren.length !== 2) { throw new Error(`Component <HeaderCell> and <Cell> is required, column index: ${index} `); } let nextWidth = this.state[`${columnChildren[1].props.dataKey}_${index}_width`] || width || 0; if (tableWidth && flexGrow && totalFlexGrow) { nextWidth = Math.max( ((tableWidth - totalWidth) / totalFlexGrow) * flexGrow, minWidth || 60, ); } const cellProps = { ...omit(column.props, ['children']), 'aria-colindex': index + 1, // Use ARIA to improve accessibility left, index, headerHeight, key: index, width: nextWidth, height: rowHeight, firstColumn: index === 0, lastColumn: index === columns.length - 1, onCell: column.props.onCell, }; if (showHeader && headerHeight) { const headerCellProps = { // index 用于拖拽列宽时候(Resizable column),定义的序号 index, dataKey: columnChildren[1].props.dataKey, isHeaderCell: true, minWidth: column.props.minWidth, sortable: column.props.sortable, onSortColumn: this.handleSortColumn, sortType: this.getSortType(), sortColumn, flexGrow, }; if (resizable) { merge(headerCellProps, { onResize, onColumnResizeEnd: this.handleColumnResizeEnd, onColumnResizeStart: this.handleColumnResizeStart, onColumnResizeMove: this.handleColumnResizeMove, onMouseEnterHandler: this.handleShowMouseArea, onMouseLeaveHandler: this.handleHideMouseArea, }); } headerCells.push( // @ts-ignore React.cloneElement(columnChildren[0], { ...cellProps, ...headerCellProps }), ); } // @ts-ignore bodyCells.push(React.cloneElement(columnChildren[1], cellProps)); left += nextWidth; } }); if (this.tableStore.customizable) { const customizationHeaderProps = { 'aria-colindex': 999, // Use ARIA to improve accessibility left: left - 30, headerHeight, key: CUSTOMIZED_KEY, width: 14, height: rowHeight, fixed: 'right', className: this.addPrefix('customization-header'), isHeaderCell: true, }; const { tableStore: { customizedColumnHeader } } = this; // @ts-ignore headerCells.push(<HeaderCell {...customizationHeaderProps}>{customizedColumnHeader()}</HeaderCell>); } return (this._cacheCells = { headerCells, bodyCells, allColumnsWidth: left, hasCustomTreeCol, }); } setOffsetByAffix = () => { const { affixHeader, affixHorizontalScrollbar } = this.props; const { headerWrapperRef, tableRef } = this; const headerNode = headerWrapperRef && headerWrapperRef.current; if (isNumberOrTrue(affixHeader) && headerNode) { this.setState(() => ({ headerOffset: getOffset(headerNode) })); } const tableNode = tableRef && tableRef.current; if (isNumberOrTrue(affixHorizontalScrollbar) && tableNode) { this.setState(() => ({ tableOffset: getOffset(tableNode) })); } }; handleWindowScroll = () => { const { affixHeader, affixHorizontalScrollbar } = this.props; if (isNumberOrTrue(affixHeader)) { this.affixTableHeader(); } if (isNumberOrTrue(affixHorizontalScrollbar)) { this.affixHorizontalScrollbar(); } }; affixHorizontalScrollbar = () => { const scrollY = window.scrollY || window.pageYOffset; const windowHeight = getHeight(window); const height = this.getTableHeight(); const { tableOffset, fixedHorizontalScrollbar } = this.state; const { affixHorizontalScrollbar } = this.props; const headerHeight = this.getTableHeaderHeight(); const bottom = typeof affixHorizontalScrollbar === 'number' ? affixHorizontalScrollbar : 0; const offset = defaultTo(tableOffset && tableOffset.top, 0) + bottom; const fixedScrollbar = // @ts-ignore scrollY + windowHeight < height + offset && // @ts-ignore scrollY + windowHeight - headerHeight > offset; const { scrollbarXRef } = this; if (scrollbarXRef) { const { current } = scrollbarXRef; if (current) { const { barRef } = current; if ( barRef && barRef.current && fixedHorizontalScrollbar !== fixedScrollbar ) { this.setState({ fixedHorizontalScrollbar: fixedScrollbar }); } } } }; affixTableHeader = () => { const { affixHeader } = this.props; const top = typeof affixHeader === 'number' ? affixHeader : 0; const { headerOffset, contentHeight } = this.state; const scrollY = window.scrollY || window.pageYOffset; const fixedHeader = // @ts-ignore scrollY - (headerOffset.top - top) >= 0 && scrollY < headerOffset.top - top + contentHeight; if (this.affixHeaderWrapperRef.current) { toggleClass(this.affixHeaderWrapperRef.current, 'fixed', fixedHeader); } }; handleSortColumn = (dataKey: string) => { let sortType = this.getSortType(); const { onSortColumn, sortColumn } = this.props; if (sortColumn === dataKey) { switch (sortType) { case SORT_TYPE.ASC: sortType = SORT_TYPE.DESC as SortType; break; case SORT_TYPE.DESC: sortType = undefined; break; default: sortType = SORT_TYPE.ASC as SortType; } this.setState({ sortType }); } if (onSortColumn) { onSortColumn(dataKey, sortType); } }; handleShowMouseArea = (width: number, left: number, fixed: boolean | string | undefined): void => { const { tableColumnResizeTrigger } = this.tableStore; if (tableColumnResizeTrigger !== TableColumnResizeTriggerType.hover) return; this.handleColumnResizeMove(width, left, !!fixed); }; handleHideMouseArea = () => { const { tableColumnResizeTrigger } = this.tableStore; if (tableColumnResizeTrigger !== TableColumnResizeTriggerType.hover) return; addStyle(this.mouseAreaRef.current, { display: 'none' }); }; handleColumnResizeEnd = ( columnWidth: number, _cursorDelta: number, dataKey: any, index: number, ) => { this._cacheCells = null; if (this.tableStore.customizable) { this.tableStore.changeCustomizedColumnValue(dataKey, { width: columnWidth, }); } this.setState({ isColumnResizing: false, [`${dataKey}_${index}_width`]: columnWidth }); addStyle(this.mouseAreaRef.current, { display: 'none' }); this.scrollLeft(-this.scrollX); }; handleColumnResizeStart = (width: number, left: number, fixed: boolean) => { this.setState({ isColumnResizing: true }); this.handleColumnResizeMove(width, left, fixed); }; handleColumnResizeMove = (width: number, left: number, fixed: boolean) => { let mouseAreaLeft = width + left; let x = mouseAreaLeft; let dir = 'left'; if (this.isRTL()) { mouseAreaLeft += this.minScrollX + this.getScrollBarYWidth; dir = 'right'; } if (!fixed) { x = mouseAreaLeft + (this.isRTL() ? -this.scrollX : this.scrollX); } addStyle(this.mouseAreaRef.current, { display: 'block', [dir]: `${x}px` }); }; handleTreeToggle = (rowKey: any, _rowIndex: number, rowData: any) => { const expandedRowKeys = this.getExpandedRowKeys(); let open = false; const nextExpandedRowKeys = []; for (let i = 0; i < expandedRowKeys.length; i++) { const key = expandedRowKeys[i]; if (key === rowKey) { open = true; } else { // @ts-ignore nextExpandedRowKeys.push(key); } } if (!open) { // @ts-ignore nextExpandedRowKeys.push(rowKey); } this.setState({ expandedRowKeys: nextExpandedRowKeys }); const { onExpandChange } = this.props; if (onExpandChange) { onExpandChange(!open, rowData); } }; setSelectedRowKeys(selectedRowKeys: string[], selectionInfo: SelectionInfo) { const { selectWay, record, checked, changeRowKeys, nativeEvent } = selectionInfo; const rowSelection = getRowSelection(this.props); if (rowSelection) { runInAction(() => { this.tableStore.selectedRowKeys = selectedRowKeys; }); } const { data } = this.state; if (!rowSelection.onChange && !rowSelection[selectWay]) { return; } const selectedRows = data.filter( (row, i) => selectedRowKeys.indexOf(this.getRecordKey(row, i)) >= 0, ); if (rowSelection.onChange) { rowSelection.onChange(selectedRowKeys, selectedRows); } if (selectWay === 'onSelect' && rowSelection.onSelect) { rowSelection.onSelect(record!, checked!, selectedRows, nativeEvent!); } else if (selectWay === 'onSelectMultiple' && rowSelection.onSelectMultiple) { const changeRows = data.filter( (row, i) => changeRowKeys!.indexOf(this.getRecordKey(row, i)) >= 0, ); rowSelection.onSelectMultiple(checked!, selectedRows, changeRows); } else if (selectWay === 'onSelectAll' && rowSelection.onSelectAll) { const changeRows = data.filter( (row, i) => changeRowKeys!.indexOf(this.getRecordKey(row, i)) >= 0, ); rowSelection.onSelectAll(checked!, selectedRows, changeRows); } else if (selectWay === 'onSelectInvert' && rowSelection.onSelectInvert) { rowSelection.onSelectInvert(selectedRowKeys); } } handleScrollX = (delta: number) => { this.handleWheel(delta, 0); }; handleScrollY = (delta: number) => { this.handleWheel(0, delta); }; handleWheel = (deltaX: number, deltaY: number) => { const { onScroll, virtualized } = this.props; const { contentWidth, width } = this.state; if (!this.tableRef.current) { return; } const nextScrollX = contentWidth <= width ? 0 : this.scrollX - deltaX; const nextScrollY = this.scrollY - deltaY; this.scrollY = Math.min(0, nextScrollY < this.minScrollY ? this.minScrollY : nextScrollY); this.scrollX = Math.min(0, nextScrollX < this.minScrollX ? this.minScrollX : nextScrollX); this.updatePosition(); if (onScroll) { onScroll(this.scrollX, this.scrollY); } if (virtualized) { this.setState({ isScrolling: true, scrollX: this.scrollX, scrollY: this.scrollY, }); if (this.disableEventsTimeoutId) { // @ts-ignore cancelAnimationTimeout(this.disableEventsTimeoutId); } // @ts-ignore this.disableEventsTimeoutId = requestAnimationTimeout(this.debounceScrollEndedCallback, 150); } }; debounceScrollEndedCallback = () => { this.disableEventsTimeoutId = null; this.setState({ isScrolling: false, }); }; // 处理移动端 Touch 事件, Start 的时候初始化 x,y handleTouchStart = (event: React.TouchEvent) => { if (event.touches) { const { pageX, pageY } = event.touches[0]; this.touchX = transformZoomData(pageX); this.touchY = transformZoomData(pageY); } const { onTouchStart } = this.props; if (onTouchStart) { onTouchStart(event); } }; // 处理移动端 Touch 事件, Move 的时候初始化,更新 scroll handleTouchMove = ({ e }) => { const { autoHeight, onTouchMove } = this.props; if (e.touches) { const eTouch = e.touches[0]; const pageX = transformZoomData(eTouch.pageX); const pageY = transformZoomData(eTouch.pageY); const deltaX = this.touchX - pageX; const deltaY = autoHeight ? 0 : this.touchY - pageY; if (!this.shouldHandleWheelY(deltaY) && !this.shouldHandleWheelX(deltaX)) { return; } if (e.preventDefault) { e.preventDefault(); } this.handleWheel(deltaX, deltaY); const xRef = this.scrollbarXRef.current; if (xRef && xRef.onWheelScroll) { xRef.onWheelScroll(deltaX); } const yRef = this.scrollbarYRef.current; if (yRef && yRef.onWheelScroll) { yRef.onWheelScroll(deltaY); } this.touchX = pageX; this.touchY = pageY; } if (onTouchMove) { onTouchMove(e); } }; handleWheelTouchStart = (e) => { const { touches } = e; //判断是否是两指 if (touches.length === 2) { const events1 = touches[0]; const events2 = touches[1]; // 第一根手指的坐标 const one = { x: events1.pageX, y: events1.pageY, }; // 第二根手指的坐标 const two = { x: events2.pageX, y: events2.pageY, }; // 取手指中间坐标 const centerX = (one.x + two.x) / 2; const centerY = (one.y + two.y) / 2; this.scaleStore.centerPoint = [centerX, centerY]; this.scaleStore.firstDistance = getDistance(one, two); this.scaleStore.originScale = this.scaleStore.scale || 1; } } handleWheelTouchMove = (e) => { const { touches } = e; if (touches.length === 2) { e.stopPropagation(); const events1 = touches[0]; const events2 = touches[1]; // 第一根手指的横坐标 const one = { x: events1.pageX, y: events1.pageY, }; // 第二根手指的横坐标 const two = { x: events2.pageX, y: events2.pageY, }; const distance = getDistance(one, two); let zoom = distance / this.scaleStore.firstDistance; let newScale = this.scaleStore.originScale * zoom; // 最大缩放比例限制 if (newScale > 3) { newScale = 3; } else if (newScale < 1) { newScale = 1; } // 记住使用的缩放值 this.scaleStore.scale = newScale; document.documentElement.style.transformOrigin = `${this.scaleStore.centerPoint[0]}px ${this.scaleStore.centerPoint[1]}px`; document.documentElement.style.transform = 'scale(' + newScale + ')'; } } /** * 当用户在 Table 内使用 tab 键,触发了 onScroll 事件,这个时候应该更新滚动条位置 * https://github.com/rsuite/rsuite/issues/234 */ handleBodyScroll = (event: React.UIEvent<HTMLDivElement>) => { if (event.target !== this.tableBodyRef.current) { return; } const left = scrollLeft(event.target); const top = scrollTop(event.target); if (top === 0 && left === 0) { return; } this.listenWheel(left, top); scrollLeft(event.target, 0); scrollTop(event.target, 0); }; handleDragStart = (initial: DragStart, provided: ResponderProvided) => { const { onDragStart } = this.props; const { draggableId } = initial; this.setState({ dragRowIndex: draggableId, }); if (isFunction(onDragStart)) { onDragStart(initial, provided); } }; handleDragEnd = async (resultDrag: DropResult, provided: ResponderProvided) => { const { onDragEnd, onDragEndBefore } = this.props; const { data } = this.state; this.setState({ dragRowIndex: '', }); let resultBefore: DropResult | undefined = resultDrag; if (onDragEndBefore) { const resultStatus = onDragEndBefore(resultDrag, provided); let result = isPromise(resultStatus) ? await resultStatus : resultStatus; if (!result) { return; } if (isDropresult(result)) { resultBefore = result; } } if (resultBefore && resultBefore.destination) { const resData = [...data]; arrayMove(resData, resultBefore.source.index, resultBefore.destination.index); // 使setState变成同步处理 setTimeout(() => { this.setState({ data: resData, }); }); if (onDragEnd) { onDragEnd(resultBefore, provided, resData); } } }; initPosition() { if (this.isRTL()) { setTimeout(() => { const { contentWidth, width } = this.state; this.scrollX = width - contentWidth - this.getScrollBarYWidth; this.updatePosition(); const { scrollbarXRef } = this; if (scrollbarXRef) { const { current } = scrollbarXRef; if (current && current.resetScrollBarPosition) { current.resetScrollBarPosition(-this.scrollX); } } }, 0); } } initBScroll(tableBody) { this.bscroll = new BScroll(tableBody, { disableMouse: false, disableTouch: false, useTransition: false, scrollbar: false, probeType: 3, scrollX: true, click: true, momentumLimitTime: 500, }); const hooks = this.bscroll.scroller.actions.hooks; hooks.on('start', this.handleTouchStart); const thHooks = this.bscroll.scroller.actionsHandler.hooks; thHooks.on('move', this.handleTouchMove); this.bscroll.on('scroll', (pos) => { this.handleScrollY(this.scrollY - pos.y); this.scrollY = pos.y; }); } updatePosition() { /** * 当存在锁定列情况处理 */ if (this.state.shouldFixedColumn) { this.updatePositionByFixedCell(); } else { const wheelStyle = {}; const headerStyle = {}; // @ts-ignore this.translateDOMPositionXY(wheelStyle, this.scrollX, this.scrollY); // @ts-ignore this.translateDOMPositionXY(headerStyle, this.scrollX, 0); const wheelElement = this.wheelWrapperRef && this.wheelWrapperRef.current; const headerElement = this.headerWrapperRef && this.headerWrapperRef.current; const affixHeaderElement = this.affixHeaderWrapperRef && this.affixHeaderWrapperRef.current; wheelElement && addStyle(wheelElement, wheelStyle); headerElement && addStyle(headerElement, headerStyle); if (affixHeaderElement && affixHeaderElement.hasChildNodes && affixHeaderElement.hasChildNodes()) { addStyle(affixHeaderElement.firstChild, headerStyle); } } const header = this.tableHeaderRef && this.tableHeaderRef.current; if (header) { toggleClass( header, this.addPrefix('cell-group-shadow'), this.scrollY < 0, ); } } updatePositionByFixedCell() { const wheelGroupStyle = {}; const wheelStyle = {}; const headerTranslateStyle = {}; const scrollGroups = this.getScrollCellGroups(); const fixedLeftGroups = this.getFixedLeftCellGroups(); const fixedRightGroups = this.getFixedRightCellGroups(); const { contentWidth, width } = this.state; // @ts-ignore this.translateDOMPositionXY(wheelGroupStyle, this.scrollX, 0); // @ts-ignore this.translateDOMPositionXY(wheelStyle, 0, this.scrollY); // @ts-ignore this.translateDOMPositionXY(headerTranslateStyle, 0, 0); const scrollArrayGroups = Array.from(scrollGroups); for (let i = 0; i < scrollArrayGroups.length; i++) { const group = scrollArrayGroups[i]; addStyle(group, wheelGroupStyle); } const header = this.wheelWrapperRef && this.wheelWrapperRef.current; const headerTranslate = this.headerWrapperRef && this.headerWrapperRef.current; if (header) { addStyle(header, wheelStyle); addStyle(headerTranslate, headerTranslateStyle); } const leftShadowClassName = this.addPrefix('cell-group-left-shadow'); const rightShadowClassName = this.addPrefix('cell-group-right-shadow'); const showLeftShadow = this.scrollX < 0; const showRightShadow = width - contentWidth - this.getScrollBarYWidth !== this.scrollX; toggleClass(fixedLeftGroups, leftShadowClassName, showLeftShadow); toggleClass(fixedRightGroups, rightShadowClassName, showRightShadow); } shouldHandleWheelX = (delta: number) => { const { disabledScroll, loading } = this.props; if (delta === 0 || disabledScroll || loading) { return false; } return true; }; shouldHandleWheelY = (delta: number) => { const { disabledScroll, loading } = this.props; if (delta === 0 || disabledScroll || loading) { return false; } return (delta >= 0 && this.scrollY > this.minScrollY) || (delta < 0 && this.scrollY < 0); }; shouldRenderExpandedRow(rowData: object) { const { rowKey, renderRowExpanded, isTree } = this.props; const expandedRowKeys = this.getExpandedRowKeys() || []; return ( isFunction(renderRowExpanded) && !isTree && expandedRowKeys.some(key => key === rowData[rowKey!]) ); } addPrefix = (name: string): string => prefix(this.props.classPrefix!)(name); calculateRowMaxHeight() { const { wordWrap } = this.props; if (wordWrap) { const tableRowsMaxHeight = []; const tableRows = Object.values(this.tableRows); for (let i = 0; i < tableRows.length; i++) { const [row] = tableRows[i]; if (row) { const cells = row.querySelectorAll(`.${this.addPrefix('cell-wrap')}`) || []; const cellArray = Array.from(cells); let maxHeight = 0; for (let j = 0; j < cellArray.length; j++) { const cell = cellArray[j]; const h = getHeight(cell); maxHeight = Math.max(maxHeight, h); } // @ts-ignore tableRowsMaxHeight.push(maxHeight); } } this.setState({ tableRowsMaxHeight }); } } calculateTableWidth = () => { const table = this.tableRef && this.tableRef.current; const { width } = this.state; if (table) { const nextWidth = getWidth(table); if (width !== nextWidth) { this.scrollX = 0; const current = this.scrollbarXRef && this.scrollbarXRef.current; current && current.resetScrollBarPosition(); this._cacheCells = null; } if (nextWidth !== 0) { this.setState({ width: nextWidth }); } } this.setOffsetByAffix(); }; calculateTableContentWidth(prevProps: TableProps) { const table = this.tableRef && this.tableRef.current; const row = table.querySelector(`.${this.addPrefix('row')}:not(.virtualized)`); const contentWidth = row ? getWidth(row) : 0; this.setState({ contentWidth }); // 这里 -SCROLLBAR_WIDTH 是为了让滚动条不挡住内容部分 this.minScrollX = -(contentWidth - this.state.width) - this.getScrollBarYWidth; /** * 1.判断 Table 列数是否发生变化 * 2.判断 Table 内容区域是否宽度有变化 * * 满足 1 和 2 则更新横向滚动条位置 */ if ( flatten(this.props.children as any[]).length !== flatten(prevProps.children as any[]).length && this.state.contentWidth !== contentWidth ) { this.scrollLeft(0); } } calculateTableContextHeight(prevProps?: TableProps) { const table = this.tableRef.current; const rows = table.querySelectorAll(`.${this.addPrefix('row')}`) || []; const { affixHeader } = this.props; const height = this.getTableHeight(); const headerHeight = this.getTableHeaderHeight(); const contentHeight = rows.length ? Array.from(rows) .map((row: HTMLElement) => { return Math.max(getHeight(row), Number(toPx(row.style.height)), this.getRowHeight()) || this.getRowHeight(); }) .reduce((x, y) => x + y) : 0; // 当设置 affixHeader 属性后要减掉两个 header 的高度 const nextContentHeight = contentHeight - (affixHeader ? headerHeight * 2 : headerHeight); if (nextContentHeight !== this.state.contentHeight) { this.setState({ contentHeight: nextContentHeight }); } if ( prevProps && // 当 data 更新,或者表格高度更新,则更新滚动条 (prevProps.height !== height || prevProps.data !== this.props.data) && this.scrollY !== 0 ) { this.scrollTop(Math.abs(this.scrollY)); this.updatePosition(); } this.minScrollY = -(contentHeight - height) - this.getScrollBarYWidth; // 如果内容区域的高度小于表格的高度,则重置 Y 坐标滚动条 if (contentHeight < height) { this.scrollTop(0); } // 如果 scrollTop 的值大于可以滚动的范围 ,则重置 Y 坐标滚动条 // 当 Table 为 virtualized 时, wheel 事件触发每次都会进入该逻辑, 避免在滚动到底部后滚动条重置, +SCROLLBAR_WIDTH if (Math.abs(this.scrollY) + height - headerHeight > nextContentHeight + this.getScrollBarYWidth) { this.scrollTop(this.scrollY); } } getControlledScrollTopValue(value) { if (this.props.autoHeight) { return [0, 0]; } const { contentHeight } = this.state; const headerHeight = this.getTableHeaderHeight(); const height = this.getTableHeight(); // 滚动值的最大范围判断 value = Math.min(value, Math.max(0, contentHeight - (height - headerHeight))); // value 值是表格理论滚动位置的一个值,通过 value 计算出 scrollY 坐标值与滚动条位置的值 return [-value, (value / contentHeight) * (height - headerHeight)]; } getControlledScrollLeftValue(value) { const { contentWidth, width } = this.state; // 滚动值的最大范围判断 value = Math.min(value, Math.max(0, contentWidth - width)); return [-value, (value / contentWidth) * width]; } /** * public method */ scrollTop = (top = 0) => { const [scrollY, handleScrollY] = this.getControlledScrollTopValue(top); this.scrollY = scrollY; const current = this.scrollbarYRef && this.scrollbarYRef.current; current && current.resetScrollBarPosition && current.resetScrollBarPosition(handleScrollY); this.updatePosition(); /** * 当开启 virtualized,调用 scrollTop 后会出现白屏现象, * 原因是直接操作 DOM 的坐标,但是组件没有重新渲染,需要调用 forceUpdate 重新进入 render。 * Fix: rsuite#1044 */ if (this.props.virtualized && this.state.contentHeight > this.getTableHeight()) { this.forceUpdate(); } }; // public method scrollLeft = (left = 0) => { const [scrollX, handleScrollX] = this.getControlledScrollLeftValue(left); this.scrollX = scrollX; const current = this.scrollbarXRef && this.scrollbarXRef.current; current && current.resetScrollBarPosition && current.resetScrollBarPosition(handleScrollX); this.updatePosition(); /** * 这里的条件与 scrollTop 触发强制更新的条件相反,避免重复的渲染。 */ if (this.props.virtualized && this.state.contentHeight <= this.getTableHeight()) { this.forceUpdate(); } }; scrollTo = (coord: { x: number; y: number }) => { const { x, y } = coord || {}; if (typeof x === 'number') { this.scrollLeft(x); } if (typeof y === 'number') { this.scrollTop(y); } }; bindTableRowsRef = (index: number | string, rowData: any, provided?: DraggableProvided) => (ref: HTMLElement) => { if (ref) { this.tableRows[index] = [ref, rowData]; if (provided) { provided.innerRef(ref); } } }; bindRowClick = (rowIndex: number | string, index: number | string, rowData: object) => { return (event: React.MouseEvent) => { this.onRowClick(rowData, event, rowIndex, index); }; }; onRowClick(rowData, event, rowIndex, index) { const { highLightRow, rowKey, rowDraggable, isTree, onRowClick, virtualized } = this.props; const useRowKey = rowDraggable || isTree || virtualized; const rowNum = useRowKey ? rowData[rowKey!] : rowIndex; if (highLightRow && (!useRowKey || useRowKey && !isNil(rowNum))) { const tableRows = Object.values(this.tableRows); if (tableRows[0][0].className.includes(`${this.addPrefix('row-header')}`)) tableRows.shift(); const ref = useRowKey ? this.tableRows[rowNum] && this.tableRows[rowNum][0] : tableRows[index] && tableRows[index][0]; if (this._lastRowIndex !== rowNum && ref) { if (this._lastRowIndex || this._lastRowIndex === 0) { const row = useRowKey ? this.tableRows[this._lastRowIndex] : tableRows[this._lastRowIndex]; if (row && row[0]) { row[0].className = ref.className.replace(` ${this.addPrefix('row-highLight')}`, ''); } } ref.className = `${ref.className} ${this.addPrefix('row-highLight')}`; } this._lastRowIndex = rowNum; } if (onRowClick) { onRowClick(rowData, event); } } bindRowContextMenu = (rowData: object) => { return (event: React.MouseEvent) => { const { onRowContextMenu } = this.props; if (onRowContextMenu) { onRowContextMenu(rowData, event); } }; }; renderRowData( bodyCells: any[], rowData: any, props: TableRowProps, shouldRenderExpandedRow?: boolean, ) { const { renderTreeToggle, rowKey, wordWrap, isTree, components } = this.props; const hasChildren = isTree && rowData.children && Array.isArray(rowData.children); const nextRowKey = typeof rowData[rowKey!] !== 'undefined' ? rowData[rowKey!] : props.key; const rowProps: TableRowProps = { ...props, // Fixed Row missing custom rowKey key: nextRowKey, 'aria-rowindex': (props.key as number) + 2, rowRef: this.bindTableRowsRef(props.key!, rowData), onClick: this.bindRowClick(props.rowIndex, props.key!, rowData), onContextMenu: this.bindRowContextMenu(rowData), }; const expandedRowKeys = this.getExpandedRowKeys() || []; const expanded = expandedRowKeys.some(key => key === rowData[rowKey!]); const cells = []; for (let i = 0; i < bodyCells.length; i++) { const cell = bodyCells[i]; cells.push( // @ts-ignore React.cloneElement<any>(cell, { hasChildren, rowData, wordWrap, renderTreeToggle, height: props.height, rowIndex: props.rowIndex, depth: props.depth, onTreeToggle: this.handleTreeToggle, rowKey: nextRowKey, expanded, }), ); } return components && components.body && components.body.row && React.isValidElement(components.body.row) ? (React.cloneElement(components.body.row, { rowProps, cells, shouldRenderExpandedRow, rowData })) : this.renderRow(rowProps, cells, shouldRenderExpandedRow, rowData); } calculateFixedAndScrollColumn(cells: any[]) { const fixedLeftCells: any[] = []; const fixedRightCells: any[] = []; const scrollCells: any[] = []; let fixedLeftCellGroupWidth: number = 0; let fixedRightCellGroupWidth: number = 0; for (let i = 0; i < cells.length; i++) { const cell = cells[i]; const { fixed, width } = cell.props; let isFixedStart = fixed === 'left' || fixed === true; let isFixedEnd = fixed === 'right'; if (this.isRTL()) { isFixedStart = fixed === 'right'; isFixedEnd = fixed === 'left' || fixed === true; } if (isFixedStart) { // @ts-ignore fixedLeftCells.push(cell); fixedLeftCellGroupWidth += width; } else if (isFixedEnd) { // @ts-ignore fixedRightCells.push(cell); if (cell.key !== CUSTOMIZED_KEY) { fixedRightCellGroupWidth += width; } } else { // @ts-ignore scrollCells.push(cell); } } return { fixedLeftCells, fixedRightCells, scrollCells, fixedLeftCellGroupWidth, fixedRightCellGroupWidth }; } renderRow(props: TableRowProps, cells: any[], shouldRenderExpandedRow?: boolean, rowData?: any) { const { rowClassName, highLightRow, virtualized, rowDraggable } = this.props; const { shouldFixedColumn, width, contentWidth, dragRowIndex } = this.state; const { depth, rowIndex, isHeaderRow, ...restRowProps } = props; const rowKey = rowData && this.getRecordKey(rowData, rowIndex); if (typeof rowClassName === 'function') { restRowProps.className = rowClassName(rowData); } else { restRowProps.className = rowClassName; } if (rowKey === this._lastRowIndex && virtualized && highLightRow && !isHeaderRow) { restRowProps.className = `${rowClassName} ${this.addPrefix('row-highLight')}`; } const rowStyles: React.CSSProperties = {}; let rowRight = 0; if (this.isRTL() && contentWidth > width) { rowRight = width - contentWidth; rowStyles.right = rowRight; } let currnetRowIndex: string = ''; // 修复合并行的最后一行没有borderBottom 和 合并行后的单元格被遮挡的问题 for (let i = 0; i < cells.length; i++) { const cellUnit = cells[i]; const { onCell, dataIndex, fixed } = cellUnit.props; if (onCell && typeof onCell === 'function') { const cellExternalProps = onCell({ rowData, dataIndex, rowIndex, }) || {}; const { rowSpan = 1 } = cellExternalProps; if (rowSpan > 1 && !currnetRowIndex) { currnetRowIndex = `${rowIndex}`; rowStyles.zIndex = fixed ? cells.length - i : 0; } } } // 优化拖拽行被 rowSpan 合并行覆盖的问题 if (dragRowIndex === `${rowIndex}`) { rowStyles.zIndex = 1; } // IF there are fixed columns, add a fixed group if (shouldFixedColumn && contentWidth > width) { // if (rowData && uniq(this.tableStore.rowZIndex!.slice()).includes(rowIndex)) { // rowStyles.zIndex = 1; // } const { fixedLeftCells = [], fixedRightCells = [], scrollCells = [], fixedLeftCellGroupWidth = 0, fixedRightCellGroupWidth = 0, } = this.calculateFixedAndScrollColumn(cells); if (rowDraggable && !isHeaderRow) { return ( <Draggable draggableId={String(rowKey)} index={rowIndex} key={rowKey} > {( provided: DraggableProvided, snapshot: DraggableStateSnapshot, ) => ( <Row {...restRowProps} data-depth={depth} style={rowStyles} rowDraggable={rowDraggable} isHeaderRow={isHeaderRow} provided={provided} snapshot={snapshot} rowRef={this.bindTableRowsRef(props.key!, rowData, provided)} > {fixedLeftCellGroupWidth ? ( <CellGroup provided={provided} snapshot={snapshot} rowDraggable={rowDraggable} fixed="left" height={props.isHeaderRow ? props.headerHeight : props.height} width={fixedLeftCellGroupWidth} // @ts-ignore style={this.isRTL() ? { right: width - fixedLeftCellGroupWidth - rowRight } : null} > {mergeCells(resetLeftForCells(fixedLeftCells))} </CellGroup> ) : null} <CellGroup provided={provided} snapshot={snapshot} rowDraggable={rowDraggable} > {mergeCells(scrollCells)} </CellGroup> {fixedRightCellGroupWidth || fixedRightCells.length ? ( <CellGroup provided={provided} snapshot={snapshot} rowDraggable={rowDraggable} fixed="right" style={ this.isRTL() ? { right: 0 - rowRight } : { left: width - fixedRightCellGroupWidth - this.getScrollBarYWidth } } height={props.isHeaderRow ? props.headerHeight : props.height} width={fixedRightCellGroupWidth + this.getScrollBarYWidth} > {mergeCells(resetLeftForCells(fixedRightCells, this.getScrollBarYWidth))} </CellGroup> ) : null} {shouldRenderExpandedRow && this.renderRowExpanded(rowData)} </Row> )} </Draggable> ); } return ( <Row {...restRowProps} isHeaderRow={isHeaderRow} data-depth={depth} style={rowStyles} rowRef={this.bindTableRowsRef(props.key!, rowData)}> {fixedLeftCellGroupWidth ? ( <CellGroup fixed="left" height={props.isHeaderRow ? props.headerHeight : props.height} width={fixedLeftCellGroupWidth} // @ts-ignore style={this.isRTL() ? { right: width - fixedLeftCellGroupWidth - rowRight } : null} > {mergeCells(resetLeftForCells(fixedLeftCells))} </CellGroup> ) : null} <CellGroup>{mergeCells(scrollCells, fixedLeftCells.length)}</CellGroup> {fixedRightCellGroupWidth || fixedRightCells.length ? ( <CellGroup fixed="right" style={ this.isRTL() ? { right: 0 - rowRight } : { left: width - fixedRightCellGroupWidth - this.getScrollBarYWidth } } height={props.isHeaderRow ? props.headerHeight : props.height} width={fixedRightCellGroupWidth + this.getScrollBarYWidth} > {mergeCells(resetLeftForCells(fixedRightCells, this.getScrollBarYWidth))} </CellGroup> ) : null} {shouldRenderExpandedRow && this.renderRowExpanded(rowData)} </Row> ); } if (rowDraggable && !isHeaderRow) { return ( <Draggable draggableId={String(rowKey)} index={rowIndex} key={rowKey} > {( provided: DraggableProvided, snapshot: DraggableStateSnapshot, ) => ( <Row {...restRowProps} data-depth={depth} style={rowStyles} rowDraggable={rowDraggable} provided={provided} snapshot={snapshot} isHeaderRow={isHeaderRow} rowRef={this.bindTableRowsRef(props.key!, rowData, provided)} // {...(rowDragRender && rowDragRender.draggableProps)} todo > <CellGroup provided={provided} snapshot={snapshot} rowDraggable={rowDraggable} > {mergeCells(cells)} </CellGroup> {shouldRenderExpandedRow && this.renderRowExpanded(rowData)} </Row> )} </Draggable> ); } return ( <Row {...restRowProps} isHeaderRow={isHeaderRow} data-depth={depth} style={rowStyles} rowRef={this.bindTableRowsRef(props.key!, rowData)}> <CellGroup>{mergeCells(cells)}</CellGroup> {shouldRenderExpandedRow && this.renderRowExpanded(rowData)} </Row> ); } renderRowExpanded(rowData?: object) { const { renderRowExpanded, rowExpandedHeight } = this.props; const styles = { height: rowExpandedHeight }; if (isFunction(renderRowExpanded)) { return ( <div className={this.addPrefix('row-expanded')} style={styles}> {renderRowExpanded(rowData)} </div> ); } return null; } renderMouseArea() { const headerHeight = this.getTableHeaderHeight(); const styles = { height: this.getTableHeight() }; const spanStyles = { height: headerHeight - 1 }; return ( <div ref={this.mouseAreaRef} className={this.addPrefix('mouse-area')} style={styles}> <span style={spanStyles} /> </div> ); } renderTableHeader(headerCells: any[], rowWidth: number) { const { affixHeader, components } = this.props; const { width: tableWidth } = this.state; const top = typeof affixHeader === 'number' ? affixHeader : 0; const headerHeight = this.getTableHeaderHeight(); const rowProps: TableRowProps = { 'aria-rowindex': 1, rowRef: this.tableHeaderRef, width: rowWidth, height: this.getRowHeight(), headerHeight, isHeaderRow: true, top: 0, }; const fixedStyle: React.CSSProperties = { position: 'fixed', overflow: 'hidden', height: this.getTableHeaderHeight(), width: tableWidth, top, }; // Affix header const header = ( <div className={classNames(this.addPrefix('affix-header'))} style={fixedStyle} ref={this.affixHeaderWrapperRef} > {this.renderRow(rowProps, headerCells)} </div> ); // cusomized header const cusomizedHeader = components && components.header && components.header.row && React.isValidElement(components.header.row) ? (React.cloneElement(components.header.row, { rowProps, headerCells })) : this.renderRow(rowProps, headerCells); return ( <React.Fragment> {(affixHeader === 0 || affixHeader) && header} {components && components.header && components.header.wrapper && React.isValidElement(components.header.wrapper) ? (React.cloneElement(components.header.wrapper, { role: "rowgroup", className: this.addPrefix('header-row-wrapper'), ref: this.headerWrapperRef }, cusomizedHeader, )) : ( <div role="rowgroup" className={this.addPrefix('header-row-wrapper')} ref={this.headerWrapperRef} > {cusomizedHeader} </div> ) } </React.Fragment> ); } renderTableBody(bodyCells: any[], rowWidth: number) { const { rowExpandedHeight, renderRowExpanded, isTree, rowKey, wordWrap, virtualized, rowHeight, rowDraggable, components, } = this.props; const headerHeight = this.getTableHeaderHeight(); const { tableRowsMaxHeight, isScrolling, data, contentWidth, shouldFixedColumn, width } = this.state; const height = this.getTableHeight(); const bodyHeight = height - headerHeight; const minLeft = Math.abs(this.scrollX); const bodyStyles = { top: headerHeight, height: bodyHeight, }; let contentHeight = 0; let topHideHeight = 0; let bottomHideHeight = 0; this._visibleRows = []; this.nextRowZIndex = []; this._cacheCalcStartRowSpan = []; if (data) { let top = 0; // Row position let renderCols: any[] = bodyCells; // Render Col const minTop = Math.abs(this.scrollY); // @ts-ignore const maxTop = minTop + height + rowExpandedHeight!; const isCustomRowHeight = isFunction(rowHeight); const isUncertainHeight = !!(renderRowExpanded || isCustomRowHeight || isTree); /** * 如果开启了虚拟滚动 则计算列显示 * 判断是否有缓存,如果minLeft 没有变化,就取缓存的值,有变化就重新计算 */ if (virtualized && contentWidth > width && (this._cacheScrollX !== minLeft || !this._cacheRenderCols.length)) { // 计算渲染列数量 let colIndex: number = 0; // 列索引 let displayColWidth: number = 0; // 显示列的宽度 let renderLeftFixedCol: any[] = []; // 需要渲染的固定左边列 let renderRightFixedCol: any[] = []; // 需要渲染的固定右边列 let showNum: number = 0; // 显示列数 let divideLeftFixedCol: number = 0; let divideRightFixedCol: number = 0; // 找到左固定列 if (shouldFixedColumn) { for (let i = 0; i < bodyCells.length; i++) { const bc = bodyCells[i]; const { fixed } = bc.props; if ((fixed && fixed !== 'right') || fixed === 'left') { renderLeftFixedCol.push(bc); } else { break; } } // 找到右固定列 for (let i = bodyCells.length - 1; i >= 0; i--) { const bc = bodyCells[i]; const { fixed } = bc.props; if (fixed === 'right') { renderRightFixedCol.push(bc); } else { break; } } renderRightFixedCol = renderRightFixedCol.reverse(); // 计算需要减去左右固定列的宽度和 divideLeftFixedCol = renderLeftFixedCol.reduce((val, item) => val + item.props.width, 0); divideRightFixedCol = renderRightFixedCol.reduce((val, item) => val + item.props.width, 0); } // 遍历显示列的总宽度与x滚动条关系 for (let i = 0; i < bodyCells.length; i++) { const elem: any = bodyCells[i]; displayColWidth += elem.props.width; if ((displayColWidth - divideLeftFixedCol) > minLeft) { colIndex = i; break; } } // 计算显示列开始下标 const colStartIndex: number = colIndex > renderLeftFixedCol.length ? colIndex - 1 : colIndex; // // 判断当前容器宽度能容纳列数 let currentDisplayColWidth: number = 0; // 总宽度减去左右固定列的宽度 const divideWidth = width - divideLeftFixedCol - divideRightFixedCol; // 遍历列宽度 与 容器宽度 得出该容器下显示的列数 for (let i = colIndex + 1 + renderRightFixedCol.length; i < bodyCells.length; i++) { const elem: any = bodyCells[i]; currentDisplayColWidth += elem.props.width; if (currentDisplayColWidth > divideWidth) { showNum = i - colIndex; break; } } // 计算列显示的结束下标 const colEndIndex: number = (showNum ? (colIndex + showNum) + 1 : bodyCells.length) - renderRightFixedCol.length; // // 最后slice 需要渲染的部分列 if (renderLeftFixedCol.length + renderRightFixedCol.length === bodyCells.length) { renderCols = [...renderLeftFixedCol, ...renderRightFixedCol]; } else { renderCols = [...renderLeftFixedCol, ...bodyCells.slice(colStartIndex, colEndIndex), ...renderRightFixedCol]; } this._cacheScrollX = minLeft; this._cacheRenderCols = renderCols; } else { renderCols = this._cacheRenderCols.length ? this._cacheRenderCols : bodyCells; } /** 如果开启了 virtualized 同时 Table 中的的行高是可变的, 则需要循环遍历 data, 获取每一行的高度。 */ if ((isUncertainHeight && virtualized) || !virtualized) { // temp test let keyIndex = 0; for (let index = 0; index < data.length; index++) { const rowData = data[index]; const maxHeight = tableRowsMaxHeight[index]; const shouldRenderExpandedRow = this.shouldRenderExpandedRow(rowData); let nextRowHeight = 0; let depth = 0; if (typeof rowHeight === 'function') { nextRowHeight = rowHeight(rowData); } else { nextRowHeight = maxHeight ? Math.max(maxHeight + CELL_PADDING_HEIGHT, rowHeight!) : rowHeight!; if (shouldRenderExpandedRow) { // @ts-ignore nextRowHeight += rowExpandedHeight!; } } if (isTree) { const parents = findAllParents(rowData, rowKey); const expandedRowKeys = this.getExpandedRowKeys(); depth = parents.length; // 如果是 Tree Table, 判断当前的行是否展开/折叠,如果是折叠则不显示该行。 // @ts-ignore if (!shouldShowRowByExpanded(expandedRowKeys, parents)) { continue; } } contentHeight += nextRowHeight; const rowProps = { key: keyIndex, rowIndex: index, top, width: rowWidth, depth, height: nextRowHeight, }; top += nextRowHeight; if (virtualized && !wordWrap) { if (top + nextRowHeight < minTop) { topHideHeight += nextRowHeight; continue; } else if (top > maxTop) { bottomHideHeight += nextRowHeight; continue; } } this._visibleRows.push( // @ts-ignore this.renderRowData(renderCols, rowData, rowProps, shouldRenderExpandedRow), ); keyIndex++; } } else { /** 如果 Table 的行高是固定的,则直接通过行高与行数进行计算, 减少遍历所有 data 带来的性能消耗 */ const nextRowHeight = this.getRowHeight(); const startIndex = Math.max(Math.floor(minTop / nextRowHeight), 0); const endIndex = Math.min(startIndex + Math.ceil(bodyHeight / nextRowHeight), data.length); contentHeight = data.length * nextRowHeight; topHideHeight = startIndex * nextRowHeight; bottomHideHeight = (data.length - endIndex) * nextRowHeight; /** * 判断行合并的情况,并遍历出合并行,在滚动的时候根据滚动高度判断是否显示 */ const hasSpanRow: any = []; let keyIndex: number = 0; let rowSpanStartIndex: number; if (this.calcStartRowSpan.rowIndex > startIndex) { // 从缓存记录往上找 const lastHistory: StartRowSpan | undefined = this._cacheCalcStartRowSpan.pop(); this.calcStartRowSpan = lastHistory || { rowIndex: 0, rowSpan: 0, height: 0 }; } if (this.calcStartRowSpan.height >= minTop) { rowSpanStartIndex = this.calcStartRowSpan.rowIndex; } else { rowSpanStartIndex = (this.calcStartRowSpan.rowIndex + this.calcStartRowSpan.rowSpan); } const hasOnCellCol = renderCols.filter(x => x.props.onCell); for (let i = rowSpanStartIndex; i < endIndex; i++) { const rowData = data[i]; for (let j = 0; j < hasOnCellCol.length; j++) { const col = hasOnCellCol[j]; const onCellInfo = col.props.onCell({ rowData, dataIndex: col.props.dataIndex, rowIndex: i }); const cellRowSpan = onCellInfo.rowSpan; // 12 const calcHeight = (cellRowSpan + i) * nextRowHeight; if (cellRowSpan > 1 && minTop < calcHeight) { const rowProps = { key: keyIndex, rowIndex: i, top: i * nextRowHeight, width: rowWidth, height: nextRowHeight, }; if (calcHeight > this.calcStartRowSpan.height && rowSpanStartIndex >= i) { const tempCalc = { rowIndex: i, rowSpan: cellRowSpan, height: calcHeight }; this.calcStartRowSpan = tempCalc; this._cacheCalcStartRowSpan.push(tempCalc); } const isExits = hasSpanRow.find(x => x.rowIndex === i); if (!isExits) { hasSpanRow.push({ rowIndex: i, render: this.renderRowData(renderCols, rowData, rowProps, false) }); } keyIndex++; } } } for (let index = startIndex; index < endIndex; index++) { const rowData = data[index]; const rowProps = { key: keyIndex, rowIndex: index, top: index * nextRowHeight, width: rowWidth, height: nextRowHeight, }; if (!hasSpanRow.length) { this._visibleRows.push(this.renderRowData(renderCols, rowData, rowProps, false)); keyIndex++; } else { const findHasSpanRow = hasSpanRow.find(x => x.rowIndex === index); if (!findHasSpanRow) { hasSpanRow.push({ rowIndex: index, render: this.renderRowData(renderCols, rowData, rowProps, false) }); keyIndex++; } } } if (hasSpanRow.length) { const sortRow = hasSpanRow.sort((a, b) => a.rowIndex - b.rowIndex); this._visibleRows = sortRow.map(x => x.render); } } } const wheelStyles: React.CSSProperties = { position: 'absolute', height: contentHeight, minHeight: height, pointerEvents: isScrolling ? 'none' : undefined, }; const topRowStyles = { height: topHideHeight }; const bottomRowStyles = { height: bottomHideHeight }; const body = ( <LocaleReceiver componentName="PerformanceTable" defaultLocale={defaultLocale.PerformanceTable}> {(locale: PerformanceTableLocal) => { const rowWrapperChildren = ( <> <div style={wheelStyles} className={this.addPrefix('body-wheel-area')} ref={this.wheelWrapperRef} > {topHideHeight ? <Row style={topRowStyles} className="virtualized" /> : null} {this._visibleRows} {bottomHideHeight ? <Row style={bottomRowStyles} className="virtualized" /> : null} </div> {this.renderInfo(locale)} {this.renderScrollbar()} {this.renderLoading()} </> ) return components && components.body && components.body.wrapper && React.isValidElement(components.body.wrapper) ? React.cloneElement(components.body.wrapper, { refs: this.tableBodyRef, role: "rowgroup", className: this.addPrefix('body-row-wrapper'), style: bodyStyles, onScroll: this.handleBodyScroll, }, components.body.wrapper.props.children || rowWrapperChildren) : ( <div ref={this.tableBodyRef} role="rowgroup" className={this.addPrefix('body-row-wrapper')} style={bodyStyles} onScroll={this.handleBodyScroll} > {rowWrapperChildren} </div> ); }} </LocaleReceiver> ); return rowDraggable ? ( <DragDropContext onDragStart={this.handleDragStart} onDragEnd={this.handleDragEnd}> <Droppable droppableId="table" key="table" > {(droppableProvided: DroppableProvided) => ( <div ref={droppableProvided.innerRef} {...droppableProvided.droppableProps} > {body} {droppableProvided.placeholder} </div> )} </Droppable> </DragDropContext> ) : body; } renderInfo(locale: PerformanceTableLocal) { const { renderEmpty, loading } = this.props; if (this._visibleRows.length || loading) { return null; } const emptyMessage = <div className={this.addPrefix('body-info')}>{locale.emptyMessage}</div>; return renderEmpty ? renderEmpty(emptyMessage) : <div className={this.addPrefix('body-info')}>{this.tableStore.getConfig('renderEmpty')('Table')}</div>; } renderScrollbar() { const { disabledScroll, affixHorizontalScrollbar, id, showScrollArrow, clickScrollLength } = this.props; const { contentWidth, contentHeight, width, fixedHorizontalScrollbar } = this.state; const bottom = typeof affixHorizontalScrollbar === 'number' ? affixHorizontalScrollbar : 0; const headerHeight = this.getTableHeaderHeight(); const height = this.getTableHeight(); if (disabledScroll) { return null; } const scrollBarOffset = (contentWidth <= this.state.width) || contentHeight <= (height - headerHeight) ? 40 : 60; // 减去border的左右边框像素 const decScrollBarOffset = showScrollArrow ? scrollBarOffset : 2; return ( <div> <Scrollbar tableId={id} showScrollArrow={showScrollArrow} clickScrollLength={clickScrollLength!} className={classNames({ fixed: fixedHorizontalScrollbar })} style={{ width, bottom: fixedHorizontalScrollbar ? bottom : undefined }} length={this.state.width - decScrollBarOffset} onScroll={this.handleScrollX} scrollLength={contentWidth - decScrollBarOffset} ref={this.scrollbarXRef} scrollBarOffset={decScrollBarOffset} /> <Scrollbar vertical tableId={id} showScrollArrow={showScrollArrow} clickScrollLength={clickScrollLength!} length={height - headerHeight - decScrollBarOffset} scrollLength={contentHeight - decScrollBarOffset} onScroll={this.handleScrollY} ref={this.scrollbarYRef} scrollBarOffset={decScrollBarOffset} /> </div> ); } /** * show loading */ renderLoading() { const { loading, loadAnimation, renderLoading } = this.props; if (!loadAnimation && !loading) { return null; } const loadingElement = ( <div className={this.addPrefix('loader-wrapper')}> <div className={this.addPrefix('loader')}> <Spin /> </div> </div> ); return renderLoading ? renderLoading(loadingElement) : loadingElement; } renderTableToolbar() { const { toolbar, toolBarRender, } = this.props; if (toolBarRender === false || !toolbar) return null; const { header, buttons, settings } = toolbar; /** 内置的工具栏 */ return ( <Toolbar header={header} hideToolbar={ settings === false && !header && !toolBarRender && !toolbar } buttons={buttons} settings={settings} toolBarRender={toolBarRender} /> ); } render() { const { children, columns, className, data, style, isTree = false, hover, bordered, cellBordered, wordWrap, classPrefix, loading, showHeader, queryBar, components, ...rest } = this.props; const { isColumnResizing, width } = this.state; const { headerCells, bodyCells, allColumnsWidth, hasCustomTreeCol } = this.getCellDescriptor(); const rowWidth = allColumnsWidth > width ? allColumnsWidth : width; const clesses = classNames(classPrefix, className, { [this.addPrefix('word-wrap')]: wordWrap, [this.addPrefix('treetable')]: isTree, [this.addPrefix('bordered')]: bordered, [this.addPrefix('cell-bordered')]: cellBordered, [this.addPrefix('column-resizing')]: isColumnResizing, [this.addPrefix('hover')]: hover, [this.addPrefix('loading')]: loading, }); const height = this.getTableHeight(); const styles = { width: rest.width || 'auto', height: height, lineHeight: `${toPx(this.getRowHeight())}px`, ...style, }; runInAction(() => { this.tableStore.totalHeight = height; }); const unhandled = getUnhandledProps(propTypeKeys, rest); const { tableStore, translateDOMPositionXY } = this; const gridChildren = ( <> {showHeader && this.renderTableHeader(headerCells, rowWidth)} {columns && columns.length ? this.renderTableBody(bodyCells, rowWidth) : children && this.renderTableBody(bodyCells, rowWidth)} {showHeader && this.renderMouseArea()} </> ) const gridProps = { role: isTree ? 'treegrid' : 'grid', // The aria-rowcount is specified on the element with the table. // Its value is an integer equal to the total number of rows available, including header rows. "aria-rowcount": data.length + 1, "aria-colcount": this._cacheChildrenSize, ...unhandled, className: clesses, style: styles, } return ( <TableContext.Provider value={{ translateDOMPositionXY, rtl: this.isRTL(), isTree, hasCustomTreeCol, scrollX: this.scrollX, tableWidth: width, tableStore, }} > <ModalProvider> {queryBar === false ? null : <PerformanceTableQueryBar />} {this.renderTableToolbar()} { components && components.table && React.isValidElement(components.table) ? React.cloneElement( components.table, { ...gridProps, refs: this.tableRef }, components.table.props.children || gridChildren) : ( <div {...gridProps} ref={this.tableRef} > {gridChildren} </div> ) } </ModalProvider> </TableContext.Provider> ); } }