/* eslint-disable no-nested-ternary */ /* eslint-disable prefer-const */ /* eslint-disable prefer-destructuring */ /* eslint-disable no-shadow */ /* eslint-disable no-param-reassign */ /* eslint-disable max-len */ /* eslint-disable react/no-did-update-set-state */ /* eslint-disable eqeqeq */ /* eslint-disable max-lines-per-function */ import React, { ReactNode, createRef, isValidElement } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { get, noop, includes, find, findIndex, some, debounce, flattenDeep, each, omit, isNull, difference, isFunction, isObject } from 'lodash'; import { mergeQueries, equalWith, mergeColumns, isAnyFixedRight, assignColumnKeys, flattenColumns, getAllDisabledRowKeys } from '@douyinfe/semi-foundation/table/utils'; import Store from '@douyinfe/semi-foundation/utils/Store'; import TableFoundation, { TableAdapter, BasePageData, BaseRowKeyType, BaseHeadWidth } from '@douyinfe/semi-foundation/table/foundation'; import { TableSelectionCellEvent } from '@douyinfe/semi-foundation/table/tableSelectionCellFoundation'; import { strings, cssClasses, numbers } from '@douyinfe/semi-foundation/table/constants'; import '@douyinfe/semi-foundation/table/table.scss'; import Spin from '../spin'; import BaseComponent from '../_base/baseComponent'; import LocaleConsumer from '../locale/localeConsumer'; import ColumnShape from './ColumnShape'; import getColumns from './getColumns'; import TableContext, { TableContextProps } from './table-context'; import TableContextProvider from './TableContextProvider'; import ColumnSelection from './ColumnSelection'; import TablePagination from './TablePagination'; import ColumnFilter, { OnSelectData } from './ColumnFilter'; import ColumnSorter from './ColumnSorter'; import ExpandedIcon from './CustomExpandIcon'; import HeadTable, { HeadTableProps } from './HeadTable'; import BodyTable, { BodyProps } from './Body'; import { measureScrollbar, logger, cloneDeep, mergeComponents } from './utils'; import { ColumnProps, TablePaginationProps, BodyScrollEvent, BodyScrollPosition, ExpandIcon, ColumnTitleProps, Pagination, RenderPagination, TableLocale, TableProps, TableComponents, RowSelectionProps, Data } from './interface'; import { ArrayElement } from '../_base/base'; export type NormalTableProps = Data> = Omit, 'resizable'>; export interface NormalTableState = Data> { cachedColumns?: ColumnProps[]; cachedChildren?: React.ReactNode; flattenColumns?: ColumnProps[]; components?: TableComponents; queries?: ColumnProps[]; dataSource?: RecordType[]; flattenData?: RecordType[]; expandedRowKeys?: (string | number)[]; rowSelection?: TableStateRowSelection; pagination?: Pagination; groups?: Map; allRowKeys?: (string | number)[]; disabledRowKeys?: (string | number)[]; disabledRowKeysSet?: Set; headWidths?: Array>; bodyHasScrollBar?: boolean; prePropRowSelection?: TableStateRowSelection; tableWidth?: number; prePagination?: Pagination; } export type TableStateRowSelection = Data> = (RowSelectionProps & { selectedRowKeysSet?: Set<(string | number)> }) | boolean; export interface RenderTableProps extends HeadTableProps, BodyProps { filteredColumns: ColumnProps[]; useFixedHeader: boolean; bodyRef: React.MutableRefObject | ((instance: any) => void); rowSelection: TableStateRowSelection; bodyHasScrollBar: boolean; } class Table> extends BaseComponent, NormalTableState> { static contextType = TableContext; static propTypes = { className: PropTypes.string, style: PropTypes.object, prefixCls: PropTypes.string, components: PropTypes.any, bordered: PropTypes.bool, loading: PropTypes.bool, size: PropTypes.oneOf(strings.SIZES), tableLayout: PropTypes.oneOf(strings.LAYOUTS), columns: PropTypes.arrayOf(PropTypes.shape(ColumnShape)), hideExpandedColumn: PropTypes.bool, id: PropTypes.string, expandIcon: PropTypes.oneOfType([PropTypes.bool, PropTypes.func, PropTypes.node]), expandCellFixed: PropTypes.oneOf(strings.FIXED_SET), title: PropTypes.oneOfType([PropTypes.string, PropTypes.node, PropTypes.func]), onHeaderRow: PropTypes.func, showHeader: PropTypes.bool, indentSize: PropTypes.number, rowKey: PropTypes.oneOfType([PropTypes.func, PropTypes.string, PropTypes.number]), onRow: PropTypes.func, onExpandedRowsChange: PropTypes.func, onExpand: PropTypes.func, rowExpandable: PropTypes.func, expandedRowRender: PropTypes.func, expandedRowKeys: PropTypes.array, defaultExpandAllRows: PropTypes.bool, expandAllRows: PropTypes.bool, defaultExpandAllGroupRows: PropTypes.bool, expandAllGroupRows: PropTypes.bool, defaultExpandedRowKeys: PropTypes.array, pagination: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]), renderPagination: PropTypes.func, footer: PropTypes.oneOfType([PropTypes.func, PropTypes.string, PropTypes.node]), empty: PropTypes.node, dataSource: PropTypes.array, childrenRecordName: PropTypes.string, // children data property name rowSelection: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]), onChange: PropTypes.func, scroll: PropTypes.shape({ x: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.bool]), y: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), }), groupBy: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.func]), renderGroupSection: PropTypes.oneOfType([PropTypes.func]), onGroupedRow: PropTypes.func, clickGroupedRowToExpand: PropTypes.bool, virtualized: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]), dropdownPrefixCls: PropTypes.string, // TODO: future api expandRowByClick: PropTypes.bool, // TODO: future api getVirtualizedListRef: PropTypes.func, // TODO: future api }; static defaultProps = { // rowExpandable: stubTrue, tableLayout: '', dataSource: [] as [], prefixCls: cssClasses.PREFIX, rowSelection: null as null, className: '', childrenRecordName: 'children', size: 'default', loading: false, bordered: false, expandCellFixed: false, hideExpandedColumn: true, showHeader: true, indentSize: numbers.DEFAULT_INDENT_WIDTH, onChange: noop, pagination: true, rowKey: 'key', defaultExpandedRowKeys: [] as [], defaultExpandAllRows: false, defaultExpandAllGroupRows: false, expandAllRows: false, expandAllGroupRows: false, onFilterDropdownVisibleChange: noop, onExpand: noop, onExpandedRowsChange: noop, expandRowByClick: false, }; get adapter(): TableAdapter { return { ...super.adapter, resetScrollY: () => { if (this.bodyWrapRef.current) { this.bodyWrapRef.current.scrollTop = 0; } }, setSelectedRowKeys: selectedRowKeys => { this.setState({ rowSelection: { ...this.state.rowSelection as Record, selectedRowKeys: [...selectedRowKeys], selectedRowKeysSet: new Set(selectedRowKeys), } }); }, setDisabledRowKeys: disabledRowKeys => { this.setState({ disabledRowKeys, disabledRowKeysSet: new Set(disabledRowKeys) }); }, setCurrentPage: currentPage => { const { pagination } = this.state; if (typeof pagination === 'object') { this.setState({ pagination: { ...pagination, currentPage } }); } else { this.setState({ pagination: { currentPage } }); } }, setPagination: pagination => this.setState({ pagination }), setGroups: groups => this.setState({ groups }), setDataSource: dataSource => this.setState({ dataSource }), setExpandedRowKeys: expandedRowKeys => this.setState({ expandedRowKeys: [...expandedRowKeys] }), setQuery: (query = {}) => { let queries = [...this.state.queries]; queries = mergeQueries(query, queries); this.setState({ queries }); }, // Update queries when filtering or sorting setQueries: (queries: ColumnProps[]) => this.setState({ queries }), setFlattenData: flattenData => this.setState({ flattenData }), setAllRowKeys: allRowKeys => this.setState({ allRowKeys }), setHoveredRowKey: hoveredRowKey => { this.store.setState({ hoveredRowKey }); }, setCachedFilteredSortedDataSource: filteredSortedDataSource => { this.cachedFilteredSortedDataSource = filteredSortedDataSource; }, setCachedFilteredSortedRowKeys: filteredSortedRowKeys => { this.cachedFilteredSortedRowKeys = filteredSortedRowKeys; this.cachedFilteredSortedRowKeysSet = new Set(filteredSortedRowKeys); }, getCurrentPage: () => get(this.state, 'pagination.currentPage', 1), getCurrentPageSize: () => get(this.state, 'pagination.pageSize', numbers.DEFAULT_PAGE_SIZE), getCachedFilteredSortedDataSource: () => this.cachedFilteredSortedDataSource, getCachedFilteredSortedRowKeys: () => this.cachedFilteredSortedRowKeys, getCachedFilteredSortedRowKeysSet: () => this.cachedFilteredSortedRowKeysSet, notifyFilterDropdownVisibleChange: (visible, dataIndex) => this._invokeColumnFn(dataIndex, 'onFilterDropdownVisibleChange', visible), notifyChange: (...args) => this.props.onChange(...args), notifyExpand: (...args) => this.props.onExpand(...args), notifyExpandedRowsChange: (...args) => this.props.onExpandedRowsChange(...args), notifySelect: (...args) => this._invokeRowSelection('onSelect', ...args), notifySelectAll: (...args) => this._invokeRowSelection('onSelectAll', ...args), notifySelectInvert: (...args) => this._invokeRowSelection('onSelectInvert', ...args), notifySelectionChange: (...args) => this._invokeRowSelection('onChange', ...args), isAnyColumnFixed: (columns: ColumnProps[]) => some(this.getColumns(columns || this.props.columns, this.props.children), column => Boolean(column.fixed)), useFixedHeader: () => { const { scroll } = this.props; if (get(scroll, 'y')) { return true; } return false; }, setHeadWidths: (headWidths: Array, index = 0) => { if (!equalWith(this.state.headWidths[index], headWidths)) { // The map call depends on the last state this.setState(state => { const newHeadWidths: Array> = [...state.headWidths]; newHeadWidths[index] = [...headWidths]; return { headWidths: newHeadWidths }; }); } }, getHeadWidths: (index = 0) => { if (this.state.headWidths.length && typeof index === 'number') { const configs = this.state.headWidths[index] || []; return configs.map(item => item.width); } return []; }, // This method is called by row rendering function getCellWidths: (flattenedColumns: ColumnProps[], flattenedWidths: BaseHeadWidth[] = null, ignoreScrollBarKey = false): number[] => { if (Array.isArray(flattenedColumns) && flattenedColumns.length) { flattenedWidths = flattenedWidths == null && this.state.headWidths.length ? flattenDeep(this.state.headWidths) : []; if ( Array.isArray(flattenedWidths) && flattenedWidths.length ) { return flattenedColumns.reduce((result, column) => { const found = column.key === strings.DEFAULT_KEY_COLUMN_SCROLLBAR && ignoreScrollBarKey ? null : find( flattenedWidths, item => item && item.key != null && item.key === column.key ); if (found) { result.push(found.width); } return result; }, [] as number[]); } } return []; }, mergedRowExpandable: record => { const { expandedRowRender, childrenRecordName, rowExpandable } = this.props; const children = get(record, childrenRecordName); const hasExpandedRowRender = typeof expandedRowRender === 'function'; const hasRowExpandable = typeof rowExpandable === 'function'; const hasChildren = Array.isArray(children) && children.length; const strictExpandableResult = hasRowExpandable && rowExpandable(record); const looseExpandableResult = !hasRowExpandable || strictExpandableResult; return ( ((hasExpandedRowRender || hasChildren) && looseExpandableResult) || (!(hasExpandedRowRender || hasChildren) && strictExpandableResult) ); }, isAnyColumnUseFullRender: (columns: ColumnProps[]) => some(columns, column => Boolean(column.useFullRender)), getNormalizeColumns: () => this.normalizeColumns, getHandleColumns: () => this.handleColumns, getMergePagination: () => this.mergePagination, setBodyHasScrollbar: bodyHasScrollBar => { if (bodyHasScrollBar !== this.state.bodyHasScrollBar) { this.setState({ bodyHasScrollBar }); } } }; } bodyWrapRef: React.MutableRefObject; rootWrapRef: React.MutableRefObject; wrapRef: React.MutableRefObject; headerWrapRef: React.MutableRefObject; debouncedWindowResize: () => void; cachedFilteredSortedDataSource: RecordType[]; cachedFilteredSortedRowKeys: BaseRowKeyType[]; cachedFilteredSortedRowKeysSet: Set; store: Store; lastScrollTop!: number; lastScrollLeft!: number; scrollPosition!: BodyScrollPosition; position!: BodyScrollPosition; foundation: TableFoundation; constructor(props: NormalTableProps, context: TableContextProps) { super(props); this.foundation = new TableFoundation(this.adapter); // columns cannot be deepClone, otherwise the comparison will be false const columns = this.getColumns(props.columns, props.children); const cachedflattenColumns = flattenColumns(columns); this.state = { /** * Cached props */ cachedColumns: columns, // update cachedColumns after columns or children change cachedChildren: props.children, flattenColumns: cachedflattenColumns, components: mergeComponents(props.components, props.virtualized), // cached components /** * State calculated based on prop */ queries: cloneDeep(cachedflattenColumns), // flatten columns, update when sorting or filtering dataSource: [], // data after paging flattenData: [], expandedRowKeys: [...(props.expandedRowKeys || []), ...(props.defaultExpandedRowKeys || [])], // cached expandedRowKeys rowSelection: props.rowSelection ? isObject(props.rowSelection) ? { ...props.rowSelection } : {} : null, pagination: props.pagination && typeof props.pagination === 'object' ? { ...props.pagination } : props.pagination || false, /** * Internal state */ groups: null, allRowKeys: [], // row keys after paging disabledRowKeys: [], // disabled row keys after paging disabledRowKeysSet: new Set(), headWidths: [], // header cell width bodyHasScrollBar: false, prePropRowSelection: undefined, prePagination: undefined }; this.rootWrapRef = createRef(); this.wrapRef = createRef(); // table's outside wrap this.bodyWrapRef = createRef(); this.headerWrapRef = createRef(); this.store = new Store({ hoveredRowKey: null, }); this.setScrollPosition('left'); this.debouncedWindowResize = debounce(this.handleWindowResize, 150); this.cachedFilteredSortedDataSource = []; this.cachedFilteredSortedRowKeys = []; this.cachedFilteredSortedRowKeysSet = new Set(); } static getDerivedStateFromProps(props: NormalTableProps, state: NormalTableState) { const willUpdateStates: Partial = {}; const { rowSelection, dataSource, childrenRecordName, rowKey, pagination } = props; props.columns && props.children && logger.warn('columns should not given by object and children at the same time'); if (props.columns && props.columns !== state.cachedColumns) { const newFlattenColumns = flattenColumns(props.columns); willUpdateStates.flattenColumns = newFlattenColumns; willUpdateStates.queries = mergeColumns(state.queries, newFlattenColumns, null, false); willUpdateStates.cachedColumns = props.columns; willUpdateStates.cachedChildren = null; } else if (props.children && props.children !== state.cachedChildren) { const newFlattenColumns = flattenColumns(getColumns(props.children)); const columns = mergeColumns(state.queries, newFlattenColumns, null, false); willUpdateStates.flattenColumns = newFlattenColumns; willUpdateStates.queries = [...columns]; willUpdateStates.cachedColumns = [...columns]; willUpdateStates.cachedChildren = props.children; } // Update controlled selection column if (rowSelection !== state.prePropRowSelection) { let newSelectionStates: TableStateRowSelection = {}; if (isObject(state.rowSelection)) { newSelectionStates = { ...newSelectionStates, ...state.rowSelection }; } if (isObject(rowSelection)) { newSelectionStates = { ...newSelectionStates, ...rowSelection }; } const selectedRowKeys = get(rowSelection, 'selectedRowKeys'); const getCheckboxProps = get(rowSelection, 'getCheckboxProps'); if (selectedRowKeys && Array.isArray(selectedRowKeys)) { newSelectionStates.selectedRowKeysSet = new Set(selectedRowKeys); } // The return value of getCheckboxProps affects the disabled rows if (isFunction(getCheckboxProps)) { const disabledRowKeys = getAllDisabledRowKeys({ dataSource, getCheckboxProps, childrenRecordName, rowKey }); willUpdateStates.disabledRowKeys = disabledRowKeys; willUpdateStates.disabledRowKeysSet = new Set(disabledRowKeys); } willUpdateStates.rowSelection = newSelectionStates; willUpdateStates.prePropRowSelection = rowSelection; } if (pagination !== state.prePagination) { let newPagination: Pagination = {}; if (isObject(state.pagination)) { newPagination = { ...newPagination, ...state.pagination }; } if (isObject(pagination)) { newPagination = { ...newPagination, ...pagination }; } willUpdateStates.pagination = newPagination; willUpdateStates.prePagination = pagination; } return willUpdateStates; } componentDidMount() { super.componentDidMount(); if (this.adapter.isAnyColumnFixed() || (this.props.showHeader && this.adapter.useFixedHeader())) { this.handleWindowResize(); window.addEventListener('resize', this.debouncedWindowResize); } } // TODO: Extract the setState operation to the adapter or getDerivedStateFromProps function componentDidUpdate(prevProps: NormalTableProps, prevState: NormalTableState) { const { dataSource, expandedRowKeys, expandAllRows, expandAllGroupRows, virtualized, components, pagination: propsPagination } = this.props; const { pagination: statePagination, queries: stateQueries, cachedColumns: stateCachedColumns, cachedChildren: stateCachedChildren, groups: stateGroups, } = this.state; /** * State related to paging * * @param dataSource * @param groups * @param pagination * @param disabledRowKeys * @param allRowKeys * @param queries */ const states: Partial> = {}; this._warnIfNoKey(); /** * The state that needs to be updated after props changes */ // Update controlled expand column if (Array.isArray(expandedRowKeys) && expandedRowKeys !== prevProps.expandedRowKeys) { this.setState({ expandedRowKeys }); } // Update components if (components !== prevProps.components || virtualized !== prevProps.virtualized) { this.setState({ components: mergeComponents(components, virtualized) }); } // Update the default expanded column if ( expandAllRows !== prevProps.expandAllRows || expandAllGroupRows !== prevProps.expandAllGroupRows ) { this.foundation.initExpandedRowKeys({ groups: stateGroups }); } /** * After dataSource is updated || (cachedColumns || cachedChildren updated) * 1. Cache filtered sorted data and a collection of data rows, stored in this * 2. Update pager and group, stored in state */ if (dataSource !== prevProps.dataSource || stateCachedColumns !== prevState.cachedColumns || stateCachedChildren !== prevState.cachedChildren) { // TODO: foundation.getFilteredSortedDataSource has side effects and will be modified to the dataSource reference // Temporarily use _dataSource=[...dataSource] for processing const _dataSource = [...dataSource]; const filteredSortedDataSource = this.foundation.getFilteredSortedDataSource(_dataSource, stateQueries); this.foundation.setCachedFilteredSortedDataSource(filteredSortedDataSource); states.dataSource = filteredSortedDataSource; if (this.props.groupBy) { states.groups = null; } } // when dataSource has change, should reset currentPage if (dataSource !== prevProps.dataSource) { states.pagination = isObject(statePagination) ? { ...statePagination, currentPage: isObject(propsPagination) && propsPagination.currentPage ? propsPagination.currentPage : 1, } : statePagination; } if (Object.keys(states).length) { const { // eslint-disable-next-line @typescript-eslint/no-shadow pagination: mergedStatePagination = null, queries: stateQueries = null, dataSource: stateDataSource = null, } = states; const handledProps: Partial> = this.foundation.getCurrentPageData(stateDataSource, mergedStatePagination as TablePaginationProps, stateQueries); // After the pager is updated, reset allRowKeys of the current page this.adapter.setAllRowKeys(handledProps.allRowKeys); this.adapter.setDisabledRowKeys(handledProps.disabledRowKeys); if ('dataSource' in states) { if (this.props.defaultExpandAllRows && handledProps.groups && handledProps.groups.size || this.props.expandAllRows || this.props.expandAllGroupRows ) { this.foundation.initExpandedRowKeys(handledProps); } } // Centrally update paging related state const statesKeys: any[] = Object.keys(states); for (const k of statesKeys) { this.setState({ [k]: handledProps[k] }); } } if (this.adapter.isAnyColumnFixed() || (this.props.showHeader && this.adapter.useFixedHeader())) { if (!this.debouncedWindowResize) { window.addEventListener('resize', this.debouncedWindowResize); } } } componentWillUnmount() { super.componentWillUnmount(); if (this.debouncedWindowResize) { window.removeEventListener('resize', this.debouncedWindowResize); (this.debouncedWindowResize as any).cancel(); this.debouncedWindowResize = null; } } // TODO: notify when data don't have key _warnIfNoKey = () => { if ( (this.props.rowSelection || this.props.expandedRowRender) && some(this.props.dataSource, record => this.foundation.getRecordKey(record) == null) ) { logger.error( 'You must specify a key for each element in the dataSource or use "rowKey" to specify an attribute name as the primary key!' ); } }; _invokeRowSelection = (funcName: string, ...args: any[]) => { const func = get(this.state, ['rowSelection', funcName]); if (typeof func === 'function') { func(...args); } }; _invokeColumnFn = (key: string, funcName: string, ...args: any[]) => { if (key && funcName) { const column = this.foundation.getQuery(key); const func = get(column, funcName, null); if (typeof func === 'function') { func(...args); } } }; _cacheHeaderRef = (node: HTMLDivElement) => { this.headerWrapRef.current = node; }; getCurrentPageData = () => { const pageData = this.foundation.getCurrentPageData(); const retObj = ['dataSource', 'groups'].reduce((result, key) => { if (pageData[key]) { result[key] = pageData[key]; } return result; }, {}); return cloneDeep(retObj); }; getColumns = (columns: ColumnProps[], children: ReactNode) => (!Array.isArray(columns) || !columns || !columns.length ? getColumns(children) : columns); // @ts-ignore getCellWidths = (...args: any[]) => this.foundation.getCellWidths(...args); // @ts-ignore setHeadWidths = (...args: any[]) => this.foundation.setHeadWidths(...args); // @ts-ignore getHeadWidths = (...args: any[]) => this.foundation.getHeadWidths(...args); // @ts-ignore mergedRowExpandable = (...args: any[]) => this.foundation.mergedRowExpandable(...args); // @ts-ignore setBodyHasScrollbar = (...args: any[]) => this.foundation.setBodyHasScrollbar(...args); handleWheel = (event: React.WheelEvent) => { const { scroll = {} } = this.props; if (window.navigator.userAgent.match(/Trident\/7\./) && scroll.y) { event.preventDefault(); const wd = event.deltaY; const { target } = event; // const { bodyTable, fixedColumnsBodyLeft, fixedColumnsBodyRight } = this; const bodyTable = this.bodyWrapRef.current; let scrollTop = 0; if (this.lastScrollTop) { scrollTop = this.lastScrollTop + wd; } else { scrollTop = wd; } if (bodyTable && target !== bodyTable) { bodyTable.scrollTop = scrollTop; } } }; handleBodyScrollLeft = (e: BodyScrollEvent) => { if (e.currentTarget !== e.target) { return; } const { target } = e; // const { headTable, bodyTable } = this; const headTable = this.headerWrapRef.current; const bodyTable = this.bodyWrapRef.current; if (target.scrollLeft !== this.lastScrollLeft) { if (target === bodyTable && headTable) { headTable.scrollLeft = target.scrollLeft; } else if (target === headTable && bodyTable) { bodyTable.scrollLeft = target.scrollLeft; } this.setScrollPositionClassName(); } // Remember last scrollLeft for scroll direction detecting. this.lastScrollLeft = target.scrollLeft; }; handleWindowResize = () => { this.syncTableWidth(); this.setScrollPositionClassName(); }; handleBodyScrollTop = (e: BodyScrollEvent) => { const { target } = e; if (e.currentTarget !== target) { return; } const { scroll = {} } = this.props; // const { headTable, bodyTable, fixedColumnsBodyLeft, fixedColumnsBodyRight } = this; const headTable = this.headerWrapRef.current; const bodyTable = this.bodyWrapRef.current; if (target.scrollTop !== this.lastScrollTop && scroll.y && target !== headTable) { const { scrollTop } = target; if (bodyTable && target !== bodyTable) { bodyTable.scrollTop = scrollTop; } } // Remember last scrollTop for scroll direction detecting. this.lastScrollTop = target.scrollTop; }; handleBodyScroll = (e: BodyScrollEvent) => { this.handleBodyScrollLeft(e); this.handleBodyScrollTop(e); }; setScrollPosition = (position: BodyScrollPosition) => { const { prefixCls } = this.props; const positionAll = [ `${prefixCls}-scroll-position-both`, `${prefixCls}-scroll-position-middle`, `${prefixCls}-scroll-position-left`, `${prefixCls}-scroll-position-right`, ]; this.scrollPosition = position; const tableNode = this.wrapRef.current; if (tableNode && tableNode.nodeType) { if (position === 'both') { const acceptPosition = [`${prefixCls}-scroll-position-left`, `${prefixCls}-scroll-position-right`]; tableNode.classList.remove(...difference(positionAll, acceptPosition)); tableNode.classList.add(...acceptPosition); } else { const acceptPosition = [`${prefixCls}-scroll-position-${position}`]; tableNode.classList.remove(...difference(positionAll, acceptPosition)); tableNode.classList.add(...acceptPosition); } } }; setScrollPositionClassName = () => { const node = this.bodyWrapRef.current; if (node && node.children && node.children.length) { const scrollToLeft = node.scrollLeft === 0; const scrollToRight = node.scrollLeft + 1 >= node.children[0].getBoundingClientRect().width - node.getBoundingClientRect().width; if (scrollToLeft && scrollToRight) { this.setScrollPosition('both'); } else if (scrollToLeft) { this.setScrollPosition('left'); } else if (scrollToRight) { this.setScrollPosition('right'); } else if (this.scrollPosition !== 'middle') { this.setScrollPosition('middle'); } } }; syncTableWidth = () => { if (this.rootWrapRef && this.rootWrapRef.current) { this.setState({ tableWidth: this.rootWrapRef.current.getBoundingClientRect().width }); } }; renderSelection = (record: RecordType = {} as RecordType, inHeader = false): React.ReactNode => { const { rowSelection, disabledRowKeysSet } = this.state; if (rowSelection && typeof rowSelection === 'object') { const { selectedRowKeys = [], selectedRowKeysSet = new Set(), getCheckboxProps, disabled } = rowSelection; if (inHeader) { const columnKey = get(rowSelection, 'key', strings.DEFAULT_KEY_COLUMN_SELECTION); const allRowKeys = this.cachedFilteredSortedRowKeys; const allRowKeysSet = this.cachedFilteredSortedRowKeysSet; const allIsSelected = this.foundation.allIsSelected(selectedRowKeysSet, disabledRowKeysSet, allRowKeys); const hasRowSelected = this.foundation.hasRowSelected(selectedRowKeys, allRowKeysSet); return ( { this.toggleSelectAllRow(status, e); }} /> ); } else { const key = this.foundation.getRecordKey(record); const selected = selectedRowKeysSet.has(key); const checkboxPropsFn = () => (typeof getCheckboxProps === 'function' ? getCheckboxProps(record) : {}); return ( this.toggleSelectRow(status, key, e)} /> ); } } return null; }; renderRowSelectionCallback = (text: string, record: RecordType = {} as RecordType) => this.renderSelection(record); renderTitleSelectionCallback = () => this.renderSelection(null, true); normalizeSelectionColumn = (props: { rowSelection?: TableStateRowSelection; prefixCls?: string } = {}) => { const { rowSelection, prefixCls } = props; let column: ColumnProps = {}; if (rowSelection) { const needOmitSelectionKey = ['selectedRowKeys', 'selectedRowKeysSet']; column = { key: strings.DEFAULT_KEY_COLUMN_SELECTION }; if (isObject(rowSelection)) { column = { ...column, ...omit(rowSelection, needOmitSelectionKey) }; } column.className = classnames(column.className, `${prefixCls}-column-selection`); column.title = this.renderTitleSelectionCallback; column.render = this.renderRowSelectionCallback; } return column; }; // If there is a scroll bar, manually construct a column and insert it into the header normalizeScrollbarColumn = (props: { scrollbarWidth?: number } = {}): { key: 'column-scrollbar'; width: number; fixed: 'right' } => { const { scrollbarWidth = 0 } = props; return { key: strings.DEFAULT_KEY_COLUMN_SCROLLBAR as 'column-scrollbar', width: scrollbarWidth, fixed: 'right', }; }; /** * render expand icon * @param {Object} record * @param {Boolean} isNested * @param {String} groupKey * @returns {ReactNode} */ renderExpandIcon = (record = {}, isNested = false, groupKey: string | number = null) => { const { expandedRowKeys } = this.state; const { expandIcon } = this.props; const key = typeof groupKey === 'string' || typeof groupKey === 'number' ? groupKey : this.foundation.getRecordKey(record as RecordType); return ( this.handleRowExpanded(expanded, key, e)} /> ); }; // @ts-ignore handleRowExpanded = (...args: any[]) => this.foundation.handleRowExpanded(...args); normalizeExpandColumn = (props: { prefixCls?: string; expandCellFixed?: ArrayElement; expandIcon?: ExpandIcon } = {}) => { let column: ColumnProps = null; const { prefixCls, expandCellFixed, expandIcon } = props; column = { fixed: expandCellFixed, key: strings.DEFAULT_KEY_COLUMN_EXPAND }; column.className = classnames(column.className, `${prefixCls}-column-expand`); column.render = expandIcon !== false ? (text = '', record, index) => (this.adapter.mergedRowExpandable(record) ? this.renderExpandIcon(record) : null) : () => null; return column; }; /** * Add sorting, filtering, and rendering functions to columns, and add column event handling * Title support function, passing parameters as {filter: node, sorter: node, selection: node} * @param {*} column */ addFnsInColumn = (column: ColumnProps = {}) => { if (column && (column.sorter || column.filters || column.useFullRender)) { const { dataIndex, title: rawTitle, useFullRender } = column; const curQuery = this.foundation.getQuery(dataIndex); const titleMap: ColumnTitleProps = {}; const titleArr = []; // useFullRender adds select buttons to each column if (useFullRender) { titleMap.selection = this.renderSelection(null, true); } const stateSortOrder = get(curQuery, 'sortOrder'); const defaultSortOrder = get(curQuery, 'defaultSortOrder', false); const sortOrder = this.foundation.isSortOrderValid(stateSortOrder) ? stateSortOrder : defaultSortOrder; if (typeof column.sorter === 'function' || column.sorter === true) { const sorter = ( this.foundation.handleSort(column, e)} /> ); useFullRender && (titleMap.sorter = sorter); titleArr.push(sorter); } if ((Array.isArray(column.filters) && column.filters.length) || isValidElement(column.filterDropdown)) { const filter = ( this.foundation.toggleShowFilter(dataIndex, visible)} onSelect={(data: OnSelectData) => this.foundation.handleFilterSelect(dataIndex, data)} /> ); useFullRender && (titleMap.filter = filter); titleArr.push(filter); } const newTitle = typeof rawTitle === 'function' ? () => rawTitle(titleMap) : titleArr.unshift( {rawTitle} ) && titleArr; column = { ...column, title: newTitle }; } return column; }; toggleSelectRow = (selected: boolean, realKey: string | number, e: TableSelectionCellEvent) => { this.foundation.handleSelectRow(realKey, selected, e); }; toggleSelectAllRow = (status: boolean, e: TableSelectionCellEvent) => { this.foundation.handleSelectAllRow(status, e); }; /** * render pagination * @param {object} pagination * @param {object} propRenderPagination */ renderPagination = (pagination: TablePaginationProps, propRenderPagination: RenderPagination) => { if (!pagination) { return null; } // use memoized pagination const mergedPagination = this.foundation.memoizedPagination(pagination); return ( {(locale: TableLocale) => { const info = this.foundation.formatPaginationInfo(mergedPagination, locale.pageText); return ; }} ); }; renderTitle = (props: { title?: ReactNode; prefixCls?: string; dataSource?: any[] } = {}) => { let { title } = props; const { prefixCls, dataSource } = props; if (typeof title === 'function') { title = title(dataSource); } return isValidElement(title) || typeof title === 'string' ? (
{title}
) : null; }; renderEmpty = (props: { prefixCls?: string; empty?: ReactNode; dataSource?: RecordType[] } = {}) => { const { prefixCls, empty, dataSource } = props; const wrapCls = `${prefixCls}-placeholder`; const isEmpty = this.foundation.isEmpty(dataSource); if (!isEmpty) { return null; } return ( {(locale: TableLocale, localeCode: string) => (
{empty || locale.emptyText}
)}
); }; renderFooter = (props: { footer?: ReactNode; prefixCls?: string; dataSource?: RecordType[] } = {}) => { let { footer } = props; const { prefixCls, dataSource } = props; if (typeof footer === 'function') { footer = footer(dataSource); } return isValidElement(footer) || typeof footer === 'string' ? (
{footer}
) : null; }; renderMainTable = (props: any) => { const useFixedHeader = this.adapter.useFixedHeader(); const emptySlot = this.renderEmpty(props); const table = [ this.renderTable({ ...props, fixed: false, useFixedHeader, headerRef: this._cacheHeaderRef, bodyRef: this.bodyWrapRef, includeHeader: !useFixedHeader, }), emptySlot, this.renderFooter(props), ]; return table; }; renderTable = (props: RenderTableProps) => { const { columns, filteredColumns, fixed, useFixedHeader, scroll, prefixCls, anyColumnFixed, includeHeader, showHeader, components, headerRef, bodyRef, rowSelection, dataSource, bodyHasScrollBar, disabledRowKeysSet, } = props; const selectedRowKeysSet = get(rowSelection, 'selectedRowKeysSet', new Set()); const headTable = fixed || useFixedHeader ? ( ) : null; const bodyTable = ( ); return [headTable, bodyTable]; }; /** * When columns change, call this function to get the latest withFnsColumns * In addition to changes in columns, these props changes must be recalculated * - hideExpandedColumn * -rowSelection changes from trusy to falsy or rowSelection.hidden changes * -isAnyFixedRight(columns) || get(scroll,'y') changes * * columns变化时,调用此函数获取最新的withFnsColumns * 除了 columns 变化,这些 props 变化也要重新计算 * - hideExpandedColumn * - rowSelection 从 trusy 变为 falsy 或 rowSelection.hidden 发生变化 * - isAnyFixedRight(columns) || get(scroll, 'y') 发生变化 * * @param {Array} queries * @param {Array} cachedColumns * @returns columns after adding extended functions */ handleColumns = (queries: ColumnProps[], cachedColumns: ColumnProps[]) => { const { hideExpandedColumn, scroll, prefixCls, expandCellFixed, expandIcon, rowSelection } = this.props; const childrenColumnName = 'children'; let columns: ColumnProps[] = cloneDeep(cachedColumns); // eslint-disable-next-line @typescript-eslint/no-shadow const addFns = (columns: ColumnProps[] = []) => { if (Array.isArray(columns) && columns.length) { each(columns, (column, index, originColumns) => { const newColumn = this.addFnsInColumn(column); const children = column[childrenColumnName]; if (Array.isArray(children) && children.length) { const newChildren = [...children]; addFns(newChildren); newColumn[childrenColumnName] = newChildren; } originColumns[index] = newColumn; }); } }; addFns(columns); // hideExpandedColumn=false render expand column separately if (!hideExpandedColumn) { const column = this.normalizeExpandColumn({ prefixCls, expandCellFixed, expandIcon }); const destIndex = findIndex(columns, item => item.key === strings.DEFAULT_KEY_COLUMN_EXPAND); if (column) { if (destIndex > -1) { columns[destIndex] = { ...column, ...columns[destIndex] }; } else if (column.fixed === 'right') { columns = [...columns, column]; } else { columns = [column, ...columns]; } } } // selection column if (rowSelection && !get(rowSelection, 'hidden')) { const destIndex = findIndex(columns, item => item.key === strings.DEFAULT_KEY_COLUMN_SELECTION); const column = this.normalizeSelectionColumn({ rowSelection, prefixCls }); if (destIndex > -1) { columns[destIndex] = { ...column, ...columns[destIndex] }; } else if (column.fixed === 'right') { columns = [...columns, column]; } else { columns = [column, ...columns]; } } assignColumnKeys(columns); return columns; }; /** * Convert children to columns object * @param {Array} columns * @param {ReactNode} children * @returns {Array} */ normalizeColumns = (columns: ColumnProps[], children: ReactNode) => { const normalColumns = cloneDeep(this.getColumns(columns, children)); return normalColumns; }; /** * Combine pagination and table paging processing functions */ mergePagination = (pagination: TablePaginationProps) => { const newPagination = { onChange: this.foundation.setPage, ...pagination }; return newPagination; }; render() { let { scroll, prefixCls, className, style: wrapStyle = {}, bordered, id, pagination: propPagination, virtualized, size, renderPagination: propRenderPagination, getVirtualizedListRef, loading, hideExpandedColumn, rowSelection: propRowSelection, ...rest } = this.props; let { rowSelection, expandedRowKeys, headWidths, tableWidth, pagination, dataSource, queries, cachedColumns, bodyHasScrollBar, } = this.state; wrapStyle = { ...wrapStyle }; let columns: ColumnProps[]; /** * As state.queries will change, the columns should be refreshed as a whole at this time * The scene of changes in queries * 1. Filter * 2. Pagination * * useFullRender needs to be passed to the user selection ReactNode, so columns need to be recalculated every time the selectedRowKeys changes * TODO: In the future, the selection passed to the user can be changed to the function type, allowing the user to execute the function to obtain the real-time status of the selection title * * 由于state.queries会发生变化,此时columns应该整体刷新 * queries变化的场景 * 1. 筛选 * 2. 分页 * useFullRender需要传给用户selection ReactNode,因此需要每次selectedRowKeys变化时重新计算columns * TODO: 未来可以将传给用户的selection改为函数类型,让用户执行函数获取selection title的实时状态 */ if (!this.adapter.isAnyColumnUseFullRender(queries)) { const rowSelectionUpdate: boolean = propRowSelection && !get(propRowSelection, 'hidden'); columns = this.foundation.memoizedWithFnsColumns( queries, cachedColumns, rowSelectionUpdate, hideExpandedColumn, // Update the columns after the body scrollbar changes to ensure that the head and body are aligned bodyHasScrollBar ); } else { columns = this.handleColumns(queries, cachedColumns); } const filteredColumns: ColumnProps[] = this.foundation.memoizedFilterColumns(columns); const flattenFnsColumns: ColumnProps[] = this.foundation.memoizedFlattenFnsColumns(columns); const anyColumnFixed = this.adapter.isAnyColumnFixed(columns); /** * - If it is the first page break, you need to calculate the current page * - If it is manual paging, call foundation to modify the state * * TODO: After merging issue 1007, you can place it in the constructor to complete * The reason is that #1007 exposes the parameters required by getCurrentPageData in the constructor */ if (isNull(dataSource)) { const pageData: BasePageData = this.foundation.getCurrentPageData(this.props.dataSource); dataSource = pageData.dataSource; pagination = pageData.pagination; } const props = { ...rest, ...this.state, // props not in rest virtualized, scroll, prefixCls, size, hideExpandedColumn, // renamed state columns, // calculated value anyColumnFixed, rowExpandable: this.mergedRowExpandable, pagination, dataSource, rowSelection, expandedRowKeys, renderExpandIcon: this.renderExpandIcon, filteredColumns, }; const x = get(scroll, 'x'); const y = get(scroll, 'y'); if (virtualized) { if (typeof wrapStyle.width !== 'number') { wrapStyle.width = x; } } const wrapCls = classnames({ [`${prefixCls}-${strings.SIZE_SMALL}`]: size === strings.SIZE_SMALL, [`${prefixCls}-${strings.SIZE_MIDDLE}`]: size === strings.SIZE_MIDDLE, [`${prefixCls}-virtualized`]: Boolean(virtualized), [`${prefixCls}-bordered`]: bordered, [`${prefixCls}-fixed-header`]: Boolean(y), [`${prefixCls}-scroll-position-left`]: ['both', 'left'].includes(this.position), [`${prefixCls}-scroll-position-right`]: ['both', 'right'].includes(this.position), }); // pagination const tablePagination = pagination && propPagination ? this.renderPagination(pagination as TablePaginationProps, propRenderPagination) : null; const paginationPosition = get(propPagination, 'position', 'bottom'); const tableContextValue: TableContextProps = { ...this.context, headWidths, tableWidth, anyColumnFixed, flattenedColumns: flattenFnsColumns, renderExpandIcon: this.renderExpandIcon, renderSelection: this.renderSelection, setHeadWidths: this.setHeadWidths, getHeadWidths: this.getHeadWidths, getCellWidths: this.getCellWidths, handleRowExpanded: this.handleRowExpanded, getVirtualizedListRef, setBodyHasScrollbar: this.setBodyHasScrollbar, }; return (
{['top', 'both'].includes(paginationPosition) ? tablePagination : null} {this.renderTitle({ title: (props as any).title, dataSource: props.dataSource, prefixCls: props.prefixCls })}
{this.renderMainTable({ ...props })}
{['bottom', 'both'].includes(paginationPosition) ? tablePagination : null}
); } } export default Table;