import * as React from 'react' ;
import classNames from 'classnames' ;
import omit from 'omit.js' ;
import RcTable , { Summary } from 'rc-table' ;
import { TableProps as RcTableProps , INTERNAL_HOOKS } from 'rc-table/lib/Table' ;
import { convertChildrenToColumns } from 'rc-table/lib/hooks/useColumns' ;
import Spin , { SpinProps } from '../spin' ;
import Pagination from '../pagination' ;
import { ConfigContext } from '../config-provider/context' ;
import usePagination , { DEFAULT_PAGE_SIZE , getPaginationParam } from './hooks/usePagination' ;
import useLazyKVMap from './hooks/useLazyKVMap' ;
import { Breakpoint } from '../_util/responsiveObserve' ;
import {
TableRowSelection ,
GetRowKey ,
ColumnType ,
ColumnsType ,
TableCurrentDataSource ,
SorterResult ,
Key ,
GetPopupContainer ,
ExpandableConfig ,
ExpandType ,
TablePaginationConfig ,
SortOrder ,
TableLocale ,
} from './interface' ;
import useSelection , { SELECTION_ALL , SELECTION_INVERT } from './hooks/useSelection' ;
import useSorter , { getSortData , SortState } from './hooks/useSorter' ;
import useFilter , { getFilterData , FilterState } from './hooks/useFilter' ;
import useTitleColumns from './hooks/useTitleColumns' ;
import renderExpandIcon from './ExpandIcon' ;
import scrollTo from '../_util/scrollTo' ;
import defaultLocale from '../locale/en_US' ;
import SizeContext , { SizeType } from '../config-provider/SizeContext' ;
import Column from './Column' ;
import ColumnGroup from './ColumnGroup' ;
import devWarning from '../_util/devWarning' ;
import useBreakpoint from '../grid/hooks/useBreakpoint' ;
export { ColumnsType , TablePaginationConfig } ;
const EMPTY_LIST : any [ ] = [ ] ;
interface ChangeEventInfo < RecordType > {
pagination : {
current? : number ;
pageSize? : number ;
total? : number ;
} ;
filters : Record < string , Key [ ] | null > ;
sorter : SorterResult < RecordType > | SorterResult < RecordType > [ ] ;
filterStates : FilterState < RecordType > [ ] ;
sorterStates : SortState < RecordType > [ ] ;
resetPagination : Function ;
}
export interface TableProps < RecordType >
extends Omit <
RcTableProps < RecordType > ,
| 'transformColumns'
| 'internalHooks'
| 'internalRefs'
| 'data'
| 'columns'
| 'scroll'
| 'emptyText'
> {
dropdownPrefixCls? : string ;
dataSource? : RcTableProps < RecordType > [ 'data' ] ;
columns? : ColumnsType < RecordType > ;
pagination? : false | TablePaginationConfig ;
loading? : boolean | SpinProps ;
size? : SizeType ;
bordered? : boolean ;
locale? : TableLocale ;
onChange ? : (
pagination : TablePaginationConfig ,
filters : Record < string , Key [ ] | null > ,
sorter : SorterResult < RecordType > | SorterResult < RecordType > [ ] ,
extra : TableCurrentDataSource < RecordType > ,
) = > void ;
rowSelection? : TableRowSelection < RecordType > ;
getPopupContainer? : GetPopupContainer ;
scroll? : RcTableProps < RecordType > [ 'scroll' ] & {
scrollToFirstRowOnChange? : boolean ;
} ;
sortDirections? : SortOrder [ ] ;
showSorterTooltip? : boolean ;
}
function Table < RecordType extends object = any > ( props : TableProps < RecordType > ) {
const {
prefixCls : customizePrefixCls ,
className ,
style ,
size : customizeSize ,
bordered ,
dropdownPrefixCls : customizeDropdownPrefixCls ,
dataSource ,
pagination ,
rowSelection ,
rowKey ,
rowClassName ,
columns ,
children ,
childrenColumnName : legacyChildrenColumnName ,
onChange ,
getPopupContainer ,
loading ,
expandIcon ,
expandable ,
expandedRowRender ,
expandIconColumnIndex ,
indentSize ,
scroll ,
sortDirections ,
locale ,
showSorterTooltip = true ,
} = props ;
const screens = useBreakpoint ( ) ;
const mergedColumns = React . useMemo ( ( ) = > {
const matched = new Set ( Object . keys ( screens ) . filter ( ( m : Breakpoint ) = > screens [ m ] ) ) ;
return ( columns || convertChildrenToColumns ( children ) ) . filter (
( c : ColumnType < RecordType > ) = >
! c . responsive || c . responsive . some ( ( r : Breakpoint ) = > matched . has ( r ) ) ,
) ;
} , [ children , columns , screens ] ) ;
const tableProps = omit ( props , [ 'className' , 'style' , 'columns' ] ) as TableProps < RecordType > ;
const size = React . useContext ( SizeContext ) ;
const { locale : contextLocale = defaultLocale , renderEmpty , direction } = React . useContext (
ConfigContext ,
) ;
const mergedSize = customizeSize || size ;
const tableLocale = { . . . contextLocale . Table , . . . locale } as TableLocale ;
const rawData : RecordType [ ] = dataSource || EMPTY_LIST ;
const { getPrefixCls } = React . useContext ( ConfigContext ) ;
const prefixCls = getPrefixCls ( 'table' , customizePrefixCls ) ;
const dropdownPrefixCls = getPrefixCls ( 'dropdown' , customizeDropdownPrefixCls ) ;
const mergedExpandable : ExpandableConfig < RecordType > = {
childrenColumnName : legacyChildrenColumnName ,
expandIconColumnIndex ,
. . . expandable ,
} ;
const { childrenColumnName = 'children' } = mergedExpandable ;
const expandType : ExpandType = React . useMemo < ExpandType > ( ( ) = > {
if ( rawData . some ( item = > ( item as any ) [ childrenColumnName ] ) ) {
return 'nest' ;
}
if ( expandedRowRender || ( expandable && expandable . expandedRowRender ) ) {
return 'row' ;
}
return null ;
} , [ rawData ] ) ;
const internalRefs = {
body : React.useRef < HTMLDivElement > ( ) ,
} ;
// ============================ RowKey ============================
const getRowKey = React . useMemo < GetRowKey < RecordType > > ( ( ) = > {
if ( typeof rowKey === 'function' ) {
return rowKey ;
}
return ( record : RecordType ) = > ( record as any ) [ rowKey as string ] ;
} , [ rowKey ] ) ;
const [ getRecordByKey ] = useLazyKVMap ( rawData , childrenColumnName , getRowKey ) ;
// ============================ Events =============================
const changeEventInfo : Partial < ChangeEventInfo < RecordType > > = { } ;
const triggerOnChange = ( info : Partial < ChangeEventInfo < RecordType > > , reset : boolean = false ) = > {
const changeInfo = {
. . . changeEventInfo ,
. . . info ,
} ;
if ( reset ) {
changeEventInfo . resetPagination ! ( ) ;
// Reset event param
if ( changeInfo . pagination ! . current ) {
changeInfo . pagination ! . current = 1 ;
}
// Trigger pagination events
if ( pagination && pagination . onChange ) {
pagination . onChange ( 1 , changeInfo . pagination ! . pageSize ) ;
}
}
if ( scroll && scroll . scrollToFirstRowOnChange !== false && internalRefs . body . current ) {
scrollTo ( 0 , {
getContainer : ( ) = > internalRefs . body . current ! ,
} ) ;
}
if ( onChange ) {
onChange ( changeInfo . pagination ! , changeInfo . filters ! , changeInfo . sorter ! , {
currentDataSource : getFilterData (
getSortData ( rawData , changeInfo . sorterStates ! , childrenColumnName ) ,
changeInfo . filterStates ! ,
) ,
} ) ;
}
} ;
/ * *
* Controlled state in ` columns ` is not a good idea that makes too many code ( 1000 + line ? )
* to read state out and then put it back to title render .
* Move these code into ` hooks ` but still too complex .
* We should provides Table props like ` sorter ` & ` filter ` to handle control in next big version .
* /
// ============================ Sorter =============================
const onSorterChange = (
sorter : SorterResult < RecordType > | SorterResult < RecordType > [ ] ,
sorterStates : SortState < RecordType > [ ] ,
) = > {
triggerOnChange (
{
sorter ,
sorterStates ,
} ,
false ,
) ;
} ;
const [ transformSorterColumns , sortStates , sorterTitleProps , getSorters ] = useSorter < RecordType > ( {
prefixCls ,
mergedColumns ,
onSorterChange ,
sortDirections : sortDirections || [ 'ascend' , 'descend' ] ,
tableLocale ,
showSorterTooltip ,
} ) ;
const sortedData = React . useMemo ( ( ) = > getSortData ( rawData , sortStates , childrenColumnName ) , [
rawData ,
sortStates ,
] ) ;
changeEventInfo . sorter = getSorters ( ) ;
changeEventInfo . sorterStates = sortStates ;
// ============================ Filter ============================
const onFilterChange = (
filters : Record < string , Key [ ] > ,
filterStates : FilterState < RecordType > [ ] ,
) = > {
triggerOnChange (
{
filters ,
filterStates ,
} ,
true ,
) ;
} ;
const [ transformFilterColumns , filterStates , getFilters ] = useFilter < RecordType > ( {
prefixCls ,
locale : tableLocale ,
dropdownPrefixCls ,
mergedColumns ,
onFilterChange ,
getPopupContainer ,
} ) ;
const mergedData = getFilterData ( sortedData , filterStates ) ;
changeEventInfo . filters = getFilters ( ) ;
changeEventInfo . filterStates = filterStates ;
// ============================ Column ============================
const columnTitleProps = React . useMemo (
( ) = > ( {
. . . sorterTitleProps ,
} ) ,
[ sorterTitleProps ] ,
) ;
const [ transformTitleColumns ] = useTitleColumns ( columnTitleProps ) ;
// ========================== Pagination ==========================
const onPaginationChange = ( current : number , pageSize : number ) = > {
triggerOnChange ( {
pagination : { . . . changeEventInfo . pagination , current , pageSize } ,
} ) ;
} ;
const [ mergedPagination , resetPagination ] = usePagination (
mergedData . length ,
pagination ,
onPaginationChange ,
) ;
changeEventInfo . pagination =
pagination === false ? { } : getPaginationParam ( pagination , mergedPagination ) ;
changeEventInfo . resetPagination = resetPagination ;
// ============================= Data =============================
const pageData = React . useMemo < RecordType [ ] > ( ( ) = > {
if ( pagination === false || ! mergedPagination . pageSize ) {
return mergedData ;
}
const { current = 1 , total , pageSize = DEFAULT_PAGE_SIZE } = mergedPagination ;
// Dynamic table data
if ( mergedData . length < total ! ) {
if ( mergedData . length > pageSize ) {
devWarning (
false ,
'Table' ,
'`dataSource` length is less than `pagination.total` but large than `pagination.pageSize`. Please make sure your config correct data with async mode.' ,
) ;
return mergedData . slice ( ( current - 1 ) * pageSize , current * pageSize ) ;
}
return mergedData ;
}
return mergedData . slice ( ( current - 1 ) * pageSize , current * pageSize ) ;
} , [
! ! pagination ,
mergedData ,
mergedPagination && mergedPagination . current ,
mergedPagination && mergedPagination . pageSize ,
mergedPagination && mergedPagination . total ,
] ) ;
// ========================== Selections ==========================
const [ transformSelectionColumns , selectedKeySet ] = useSelection < RecordType > ( rowSelection , {
prefixCls ,
data : mergedData ,
pageData ,
getRowKey ,
getRecordByKey ,
expandType ,
childrenColumnName ,
locale : tableLocale ,
expandIconColumnIndex : mergedExpandable.expandIconColumnIndex ,
getPopupContainer ,