Table.tsx 61 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495
  1. /* eslint-disable no-nested-ternary */
  2. /* eslint-disable prefer-const */
  3. /* eslint-disable prefer-destructuring */
  4. /* eslint-disable no-shadow */
  5. /* eslint-disable no-param-reassign */
  6. /* eslint-disable max-len */
  7. /* eslint-disable react/no-did-update-set-state */
  8. /* eslint-disable eqeqeq */
  9. /* eslint-disable max-lines-per-function */
  10. import React, { ReactNode, createRef, isValidElement } from 'react';
  11. import PropTypes from 'prop-types';
  12. import classnames from 'classnames';
  13. import {
  14. get,
  15. noop,
  16. includes,
  17. find,
  18. findIndex,
  19. some,
  20. debounce,
  21. flattenDeep,
  22. each,
  23. omit,
  24. isNull,
  25. difference,
  26. isFunction,
  27. isObject,
  28. isPlainObject
  29. } from 'lodash';
  30. import {
  31. mergeQueries,
  32. equalWith,
  33. isAnyFixedRight,
  34. assignColumnKeys,
  35. flattenColumns,
  36. getAllDisabledRowKeys,
  37. shouldShowEllipsisTitle
  38. } from '@douyinfe/semi-foundation/table/utils';
  39. import Store from '@douyinfe/semi-foundation/utils/Store';
  40. import TableFoundation, { TableAdapter, BasePageData, BaseRowKeyType, BaseHeadWidth } from '@douyinfe/semi-foundation/table/foundation';
  41. import { TableSelectionCellEvent } from '@douyinfe/semi-foundation/table/tableSelectionCellFoundation';
  42. import { strings, cssClasses, numbers } from '@douyinfe/semi-foundation/table/constants';
  43. import '@douyinfe/semi-foundation/table/table.scss';
  44. import Spin from '../spin';
  45. import BaseComponent from '../_base/baseComponent';
  46. import LocaleConsumer from '../locale/localeConsumer';
  47. import ColumnShape from './ColumnShape';
  48. import getColumns from './getColumns';
  49. import TableContext, { TableContextProps } from './table-context';
  50. import TableContextProvider from './TableContextProvider';
  51. import ColumnSelection from './ColumnSelection';
  52. import TablePagination from './TablePagination';
  53. import ColumnFilter, { OnSelectData } from './ColumnFilter';
  54. import ColumnSorter from './ColumnSorter';
  55. import ExpandedIcon from './CustomExpandIcon';
  56. import HeadTable, { HeadTableProps } from './HeadTable';
  57. import BodyTable, { BodyProps } from './Body';
  58. import { logger, cloneDeep, mergeComponents, mergeColumns } from './utils';
  59. import {
  60. ColumnProps,
  61. TablePaginationProps,
  62. BodyScrollEvent,
  63. BodyScrollPosition,
  64. ExpandIcon,
  65. ColumnTitleProps,
  66. Pagination,
  67. RenderPagination,
  68. TableLocale,
  69. TableProps,
  70. TableComponents,
  71. RowSelectionProps,
  72. Data
  73. } from './interface';
  74. import { ArrayElement } from '../_base/base';
  75. import ConfigContext from '../configProvider/context';
  76. export type NormalTableProps<RecordType extends Record<string, any> = Data> = Omit<TableProps<RecordType>, 'resizable'>;
  77. export interface NormalTableState<RecordType extends Record<string, any> = Data> {
  78. cachedColumns?: ColumnProps<RecordType>[];
  79. cachedChildren?: React.ReactNode;
  80. flattenColumns?: ColumnProps<RecordType>[];
  81. components?: TableComponents;
  82. queries?: ColumnProps<RecordType>[];
  83. dataSource?: RecordType[];
  84. flattenData?: RecordType[];
  85. expandedRowKeys?: (string | number)[];
  86. rowSelection?: TableStateRowSelection<RecordType>;
  87. pagination?: Pagination;
  88. groups?: Map<string, RecordType[]>;
  89. allRowKeys?: (string | number)[];
  90. disabledRowKeys?: (string | number)[];
  91. disabledRowKeysSet?: Set<string | number>;
  92. headWidths?: Array<Array<BaseHeadWidth>>;
  93. bodyHasScrollBar?: boolean;
  94. prePropRowSelection?: TableStateRowSelection<RecordType>;
  95. tableWidth?: number;
  96. prePagination?: Pagination;
  97. /**
  98. * Disabled row keys in sorted and filtered data
  99. */
  100. allDisabledRowKeys?: BaseRowKeyType[];
  101. /**
  102. * Disabled row keys set in sorted and filtered data
  103. */
  104. allDisabledRowKeysSet?: Set<BaseRowKeyType>
  105. }
  106. export type TableStateRowSelection<RecordType extends Record<string, any> = Data> = (RowSelectionProps<RecordType> & { selectedRowKeysSet?: Set<(string | number)> }) | boolean;
  107. export interface RenderTableProps<RecordType> extends HeadTableProps, BodyProps {
  108. filteredColumns: ColumnProps<RecordType>[];
  109. useFixedHeader: boolean;
  110. bodyRef: React.MutableRefObject<HTMLDivElement> | ((instance: any) => void);
  111. rowSelection: TableStateRowSelection<RecordType>;
  112. bodyHasScrollBar: boolean
  113. }
  114. class Table<RecordType extends Record<string, any>> extends BaseComponent<NormalTableProps<RecordType>, NormalTableState<RecordType>> {
  115. static contextType = TableContext;
  116. static propTypes = {
  117. className: PropTypes.string,
  118. style: PropTypes.object,
  119. prefixCls: PropTypes.string,
  120. components: PropTypes.any,
  121. bordered: PropTypes.bool,
  122. loading: PropTypes.bool,
  123. size: PropTypes.oneOf(strings.SIZES),
  124. tableLayout: PropTypes.oneOf(strings.LAYOUTS),
  125. columns: PropTypes.arrayOf(PropTypes.shape(ColumnShape)),
  126. hideExpandedColumn: PropTypes.bool,
  127. id: PropTypes.string,
  128. expandIcon: PropTypes.oneOfType([PropTypes.bool, PropTypes.func, PropTypes.node]),
  129. expandCellFixed: PropTypes.oneOf(strings.FIXED_SET),
  130. title: PropTypes.oneOfType([PropTypes.string, PropTypes.node, PropTypes.func]),
  131. onHeaderRow: PropTypes.func,
  132. showHeader: PropTypes.bool,
  133. indentSize: PropTypes.number,
  134. rowKey: PropTypes.oneOfType([PropTypes.func, PropTypes.string, PropTypes.number]),
  135. onRow: PropTypes.func,
  136. onExpandedRowsChange: PropTypes.func,
  137. onExpand: PropTypes.func,
  138. rowExpandable: PropTypes.func,
  139. expandedRowRender: PropTypes.func,
  140. expandedRowKeys: PropTypes.array,
  141. defaultExpandAllRows: PropTypes.bool,
  142. expandAllRows: PropTypes.bool,
  143. defaultExpandAllGroupRows: PropTypes.bool,
  144. expandAllGroupRows: PropTypes.bool,
  145. defaultExpandedRowKeys: PropTypes.array,
  146. pagination: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
  147. renderPagination: PropTypes.func,
  148. footer: PropTypes.oneOfType([PropTypes.func, PropTypes.string, PropTypes.node]),
  149. empty: PropTypes.node,
  150. dataSource: PropTypes.array,
  151. childrenRecordName: PropTypes.string, // children data property name
  152. rowSelection: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
  153. onChange: PropTypes.func,
  154. scroll: PropTypes.shape({
  155. x: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.bool]),
  156. y: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  157. }),
  158. groupBy: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.func]),
  159. renderGroupSection: PropTypes.oneOfType([PropTypes.func]),
  160. onGroupedRow: PropTypes.func,
  161. clickGroupedRowToExpand: PropTypes.bool,
  162. virtualized: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
  163. dropdownPrefixCls: PropTypes.string, // TODO: future api
  164. expandRowByClick: PropTypes.bool, // TODO: future api
  165. getVirtualizedListRef: PropTypes.func, // TODO: future api
  166. };
  167. static defaultProps = {
  168. // rowExpandable: stubTrue,
  169. tableLayout: '',
  170. dataSource: [] as [],
  171. prefixCls: cssClasses.PREFIX,
  172. rowSelection: null as null,
  173. className: '',
  174. childrenRecordName: 'children',
  175. size: 'default',
  176. loading: false,
  177. bordered: false,
  178. expandCellFixed: false,
  179. hideExpandedColumn: true,
  180. showHeader: true,
  181. indentSize: numbers.DEFAULT_INDENT_WIDTH,
  182. onChange: noop,
  183. pagination: true,
  184. rowKey: 'key',
  185. defaultExpandedRowKeys: [] as [],
  186. defaultExpandAllRows: false,
  187. defaultExpandAllGroupRows: false,
  188. expandAllRows: false,
  189. expandAllGroupRows: false,
  190. onFilterDropdownVisibleChange: noop,
  191. onExpand: noop,
  192. onExpandedRowsChange: noop,
  193. expandRowByClick: false,
  194. };
  195. get adapter(): TableAdapter<RecordType> {
  196. return {
  197. ...super.adapter,
  198. resetScrollY: () => {
  199. if (this.bodyWrapRef.current) {
  200. this.bodyWrapRef.current.scrollTop = 0;
  201. }
  202. },
  203. setSelectedRowKeys: selectedRowKeys => {
  204. this.setState({
  205. rowSelection: {
  206. ...this.state.rowSelection as Record<string, any>,
  207. selectedRowKeys: [...selectedRowKeys],
  208. selectedRowKeysSet: new Set(selectedRowKeys),
  209. }
  210. });
  211. },
  212. setDisabledRowKeys: disabledRowKeys => {
  213. this.setState({ disabledRowKeys, disabledRowKeysSet: new Set(disabledRowKeys) });
  214. },
  215. setCurrentPage: currentPage => {
  216. const { pagination } = this.state;
  217. if (typeof pagination === 'object') {
  218. this.setState({ pagination: { ...pagination, currentPage } });
  219. } else {
  220. this.setState({ pagination: { currentPage } });
  221. }
  222. },
  223. setPagination: pagination => this.setState({ pagination }),
  224. setGroups: groups => this.setState({ groups }),
  225. setDataSource: dataSource => this.setState({ dataSource }),
  226. setExpandedRowKeys: expandedRowKeys => this.setState({ expandedRowKeys: [...expandedRowKeys] }),
  227. setQuery: (query = {}) => {
  228. let queries = [...this.state.queries];
  229. queries = mergeQueries(query, queries);
  230. this.setState({ queries });
  231. },
  232. // Update queries when filtering or sorting
  233. setQueries: (queries: ColumnProps<RecordType>[]) => this.setState({ queries }),
  234. setFlattenData: flattenData => this.setState({ flattenData }),
  235. setAllRowKeys: allRowKeys => this.setState({ allRowKeys }),
  236. setHoveredRowKey: hoveredRowKey => {
  237. this.store.setState({ hoveredRowKey });
  238. },
  239. setCachedFilteredSortedDataSource: filteredSortedDataSource => {
  240. this.cachedFilteredSortedDataSource = filteredSortedDataSource;
  241. },
  242. setCachedFilteredSortedRowKeys: filteredSortedRowKeys => {
  243. this.cachedFilteredSortedRowKeys = filteredSortedRowKeys;
  244. this.cachedFilteredSortedRowKeysSet = new Set(filteredSortedRowKeys);
  245. },
  246. setAllDisabledRowKeys: allDisabledRowKeys => {
  247. const allDisabledRowKeysSet = new Set(allDisabledRowKeys);
  248. this.setState({ allDisabledRowKeys, allDisabledRowKeysSet });
  249. },
  250. getCurrentPage: () => get(this.state, 'pagination.currentPage', 1),
  251. getCurrentPageSize: () => get(this.state, 'pagination.pageSize', numbers.DEFAULT_PAGE_SIZE),
  252. getCachedFilteredSortedDataSource: () => this.cachedFilteredSortedDataSource,
  253. getCachedFilteredSortedRowKeys: () => this.cachedFilteredSortedRowKeys,
  254. getCachedFilteredSortedRowKeysSet: () => this.cachedFilteredSortedRowKeysSet,
  255. getAllDisabledRowKeys: () => this.state.allDisabledRowKeys,
  256. getAllDisabledRowKeysSet: () => this.state.allDisabledRowKeysSet,
  257. notifyFilterDropdownVisibleChange: (visible, dataIndex) =>
  258. this._invokeColumnFn(dataIndex, 'onFilterDropdownVisibleChange', visible),
  259. notifyChange: (...args) => this.props.onChange(...args),
  260. notifyExpand: (...args) => this.props.onExpand(...args),
  261. notifyExpandedRowsChange: (...args) => this.props.onExpandedRowsChange(...args),
  262. notifySelect: (...args) => this._invokeRowSelection('onSelect', ...args),
  263. notifySelectAll: (...args) => this._invokeRowSelection('onSelectAll', ...args),
  264. notifySelectInvert: (...args) => this._invokeRowSelection('onSelectInvert', ...args),
  265. notifySelectionChange: (...args) => this._invokeRowSelection('onChange', ...args),
  266. isAnyColumnFixed: (columns: ColumnProps<RecordType>[]) =>
  267. some(this.getColumns(columns || this.props.columns, this.props.children), column => Boolean(column.fixed)),
  268. useFixedHeader: () => {
  269. const { scroll, sticky } = this.props;
  270. if (get(scroll, 'y')) {
  271. return true;
  272. }
  273. if (sticky) {
  274. return true;
  275. }
  276. return false;
  277. },
  278. getTableLayout: () => {
  279. let isFixed = false;
  280. const { flattenColumns } = this.state;
  281. if (Array.isArray(flattenColumns)) {
  282. isFixed = flattenColumns.some(column => (Boolean(column.ellipsis) || Boolean(column.fixed)));
  283. }
  284. return isFixed ? 'fixed' : 'auto';
  285. },
  286. setHeadWidths: (headWidths: Array<BaseHeadWidth>, index = 0) => {
  287. if (!equalWith(this.state.headWidths[index], headWidths)) {
  288. // The map call depends on the last state
  289. this.setState(state => {
  290. const newHeadWidths: Array<Array<BaseHeadWidth>> = [...state.headWidths];
  291. newHeadWidths[index] = [...headWidths];
  292. return { headWidths: newHeadWidths };
  293. });
  294. }
  295. },
  296. getHeadWidths: (index = 0) => {
  297. if (this.state.headWidths.length && typeof index === 'number') {
  298. const configs = this.state.headWidths[index] || [];
  299. return configs.map(item => item.width);
  300. }
  301. return [];
  302. },
  303. // This method is called by row rendering function
  304. getCellWidths: (flattenedColumns: ColumnProps<RecordType>[], flattenedWidths: BaseHeadWidth[] = null, ignoreScrollBarKey = false): number[] => {
  305. if (Array.isArray(flattenedColumns) && flattenedColumns.length) {
  306. flattenedWidths =
  307. flattenedWidths == null && this.state.headWidths.length ?
  308. flattenDeep(this.state.headWidths) :
  309. [];
  310. if (
  311. Array.isArray(flattenedWidths) &&
  312. flattenedWidths.length
  313. ) {
  314. return flattenedColumns.reduce((result, column) => {
  315. const found =
  316. column.key === strings.DEFAULT_KEY_COLUMN_SCROLLBAR && ignoreScrollBarKey ?
  317. null :
  318. find(
  319. flattenedWidths,
  320. item => item && item.key != null && item.key === column.key
  321. );
  322. if (found) {
  323. result.push(found.width);
  324. }
  325. return result;
  326. }, [] as number[]);
  327. }
  328. }
  329. return [];
  330. },
  331. mergedRowExpandable: record => {
  332. const { expandedRowRender, childrenRecordName, rowExpandable } = this.props;
  333. const children = get(record, childrenRecordName);
  334. const hasExpandedRowRender = typeof expandedRowRender === 'function';
  335. const hasRowExpandable = typeof rowExpandable === 'function';
  336. const hasChildren = Array.isArray(children) && children.length;
  337. const strictExpandableResult = hasRowExpandable && rowExpandable(record);
  338. const looseExpandableResult = !hasRowExpandable || strictExpandableResult;
  339. return (
  340. ((hasExpandedRowRender || hasChildren) && looseExpandableResult) ||
  341. (!(hasExpandedRowRender || hasChildren) && strictExpandableResult)
  342. );
  343. },
  344. isAnyColumnUseFullRender: (columns: ColumnProps<RecordType>[]) =>
  345. some(columns, column => Boolean(column.useFullRender)),
  346. getNormalizeColumns: () => this.normalizeColumns,
  347. getHandleColumns: () => this.handleColumns,
  348. getMergePagination: () => this.mergePagination,
  349. setBodyHasScrollbar: bodyHasScrollBar => {
  350. if (bodyHasScrollBar !== this.state.bodyHasScrollBar) {
  351. this.setState({ bodyHasScrollBar });
  352. }
  353. },
  354. stopPropagation(e: TableSelectionCellEvent) {
  355. // The event definition here is not very accurate for now, it belongs to a broad structure definition
  356. if (e && typeof e === 'object') {
  357. if (typeof e.stopPropagation === 'function') {
  358. e.stopPropagation();
  359. }
  360. if (e.nativeEvent && typeof e.nativeEvent.stopPropagation === 'function') {
  361. e.nativeEvent.stopPropagation();
  362. } else if (typeof e.stopImmediatePropagation === 'function') {
  363. e.stopImmediatePropagation();
  364. }
  365. }
  366. }
  367. };
  368. }
  369. bodyWrapRef: React.MutableRefObject<HTMLDivElement>;
  370. rootWrapRef: React.MutableRefObject<HTMLDivElement>;
  371. wrapRef: React.MutableRefObject<HTMLDivElement>;
  372. headerWrapRef: React.MutableRefObject<HTMLDivElement>;
  373. debouncedWindowResize: () => void;
  374. cachedFilteredSortedDataSource: RecordType[];
  375. cachedFilteredSortedRowKeys: BaseRowKeyType[];
  376. cachedFilteredSortedRowKeysSet: Set<string | number>;
  377. store: Store;
  378. lastScrollTop!: number;
  379. lastScrollLeft!: number;
  380. scrollPosition!: BodyScrollPosition;
  381. position!: BodyScrollPosition;
  382. foundation: TableFoundation<RecordType>;
  383. context: TableContextProps;
  384. constructor(props: NormalTableProps<RecordType>, context: TableContextProps) {
  385. super(props);
  386. this.foundation = new TableFoundation<RecordType>(this.adapter);
  387. // columns cannot be deepClone, otherwise the comparison will be false
  388. const columns = this.getColumns(props.columns, props.children);
  389. const cachedflattenColumns = flattenColumns(columns);
  390. const queries = TableFoundation.initColumnsFilteredValueAndSorterOrder(cloneDeep(cachedflattenColumns));
  391. const filteredSortedDataSource = this.foundation.getFilteredSortedDataSource(this.props.dataSource, queries);
  392. const newPagination = isPlainObject(this.props.pagination) ? this.props.pagination : {} as any;
  393. const pageData: BasePageData<RecordType> = this.foundation.getCurrentPageData(filteredSortedDataSource, newPagination, queries);
  394. this.state = {
  395. /**
  396. * Cached props
  397. */
  398. cachedColumns: columns, // update cachedColumns after columns or children change
  399. cachedChildren: props.children,
  400. flattenColumns: cachedflattenColumns,
  401. components: mergeComponents(props.components, props.virtualized), // cached components
  402. /**
  403. * State calculated based on prop
  404. */
  405. queries, // flatten columns, update when sorting or filtering
  406. dataSource: pageData.dataSource, // data after paging
  407. flattenData: [],
  408. expandedRowKeys: [...(props.expandedRowKeys || []), ...(props.defaultExpandedRowKeys || [])], // cached expandedRowKeys
  409. rowSelection: props.rowSelection ? isObject(props.rowSelection) ? { ...props.rowSelection } : {} : null,
  410. pagination: pageData.pagination,
  411. /**
  412. * Internal state
  413. */
  414. groups: null,
  415. allRowKeys: [], // row keys after paging
  416. disabledRowKeys: [], // disabled row keys after paging
  417. disabledRowKeysSet: new Set(),
  418. allDisabledRowKeys: [],
  419. allDisabledRowKeysSet: new Set(),
  420. headWidths: [], // header cell width
  421. bodyHasScrollBar: false,
  422. prePropRowSelection: undefined,
  423. prePagination: undefined
  424. };
  425. this.rootWrapRef = createRef();
  426. this.wrapRef = createRef(); // table's outside wrap
  427. this.bodyWrapRef = createRef();
  428. this.headerWrapRef = createRef();
  429. this.store = new Store({
  430. hoveredRowKey: null,
  431. });
  432. this.debouncedWindowResize = debounce(this.handleWindowResize, 150);
  433. this.cachedFilteredSortedDataSource = [];
  434. this.cachedFilteredSortedRowKeys = [];
  435. this.cachedFilteredSortedRowKeysSet = new Set();
  436. }
  437. static getDerivedStateFromProps(props: NormalTableProps, state: NormalTableState) {
  438. const willUpdateStates: Partial<NormalTableState> = {};
  439. const { rowSelection, dataSource, childrenRecordName, rowKey, pagination } = props;
  440. props.columns && props.children && logger.warn('columns should not given by object and children at the same time');
  441. if (props.columns && props.columns !== state.cachedColumns) {
  442. const newFlattenColumns = flattenColumns(props.columns);
  443. willUpdateStates.flattenColumns = newFlattenColumns;
  444. willUpdateStates.queries = mergeColumns(state.queries, newFlattenColumns, null, false);
  445. willUpdateStates.cachedColumns = props.columns;
  446. willUpdateStates.cachedChildren = null;
  447. } else if (props.children && props.children !== state.cachedChildren) {
  448. const newNestedColumns = getColumns(props.children);
  449. const newFlattenColumns = flattenColumns(newNestedColumns);
  450. const columns = mergeColumns(state.queries, newFlattenColumns, null, false);
  451. willUpdateStates.flattenColumns = newFlattenColumns;
  452. willUpdateStates.queries = [...columns];
  453. willUpdateStates.cachedColumns = [...newNestedColumns];
  454. willUpdateStates.cachedChildren = props.children;
  455. }
  456. // Update controlled selection column
  457. if (rowSelection !== state.prePropRowSelection) {
  458. let newSelectionStates: TableStateRowSelection = {};
  459. if (isObject(state.rowSelection)) {
  460. newSelectionStates = { ...newSelectionStates, ...state.rowSelection };
  461. }
  462. if (isObject(rowSelection)) {
  463. newSelectionStates = { ...newSelectionStates, ...rowSelection };
  464. }
  465. const selectedRowKeys = get(rowSelection, 'selectedRowKeys');
  466. const getCheckboxProps = get(rowSelection, 'getCheckboxProps');
  467. if (selectedRowKeys && Array.isArray(selectedRowKeys)) {
  468. newSelectionStates.selectedRowKeysSet = new Set(selectedRowKeys);
  469. }
  470. // The return value of getCheckboxProps affects the disabled rows
  471. if (isFunction(getCheckboxProps)) {
  472. const disabledRowKeys = getAllDisabledRowKeys({ dataSource, getCheckboxProps, childrenRecordName, rowKey });
  473. const disabledRowKeysSet = new Set(disabledRowKeys);
  474. willUpdateStates.disabledRowKeys = disabledRowKeys;
  475. willUpdateStates.disabledRowKeysSet = disabledRowKeysSet;
  476. willUpdateStates.allDisabledRowKeys = disabledRowKeys;
  477. willUpdateStates.allDisabledRowKeysSet = disabledRowKeysSet;
  478. }
  479. willUpdateStates.rowSelection = newSelectionStates;
  480. willUpdateStates.prePropRowSelection = rowSelection;
  481. }
  482. if (pagination !== state.prePagination) {
  483. let newPagination: Pagination = {};
  484. if (isObject(state.pagination)) {
  485. newPagination = { ...newPagination, ...state.pagination };
  486. }
  487. if (isObject(pagination)) {
  488. newPagination = { ...newPagination, ...pagination };
  489. }
  490. willUpdateStates.pagination = newPagination;
  491. willUpdateStates.prePagination = pagination;
  492. }
  493. return willUpdateStates;
  494. }
  495. componentDidMount() {
  496. super.componentDidMount();
  497. this.setScrollPosition('left');
  498. if (this.adapter.isAnyColumnFixed() || (this.props.showHeader && this.adapter.useFixedHeader())) {
  499. this.handleWindowResize();
  500. window.addEventListener('resize', this.debouncedWindowResize);
  501. }
  502. }
  503. // TODO: Extract the setState operation to the adapter or getDerivedStateFromProps function
  504. componentDidUpdate(prevProps: NormalTableProps<RecordType>, prevState: NormalTableState<RecordType>) {
  505. const {
  506. dataSource,
  507. expandedRowKeys,
  508. expandAllRows,
  509. expandAllGroupRows,
  510. virtualized,
  511. components,
  512. pagination: propsPagination
  513. } = this.props;
  514. const {
  515. pagination: statePagination,
  516. queries: stateQueries,
  517. cachedColumns: stateCachedColumns,
  518. cachedChildren: stateCachedChildren,
  519. groups: stateGroups,
  520. } = this.state;
  521. /**
  522. * State related to paging
  523. *
  524. * @param dataSource
  525. * @param groups
  526. * @param pagination
  527. * @param disabledRowKeys
  528. * @param allRowKeys
  529. * @param queries
  530. */
  531. const states: Partial<NormalTableState<RecordType>> = {};
  532. this._warnIfNoKey();
  533. /**
  534. * The state that needs to be updated after props changes
  535. */
  536. // Update controlled expand column
  537. if (Array.isArray(expandedRowKeys) && expandedRowKeys !== prevProps.expandedRowKeys) {
  538. this.setState({ expandedRowKeys });
  539. }
  540. // Update components
  541. if (components !== prevProps.components || virtualized !== prevProps.virtualized) {
  542. this.setState({ components: mergeComponents(components, virtualized) });
  543. }
  544. // Update the default expanded column
  545. if (
  546. expandAllRows !== prevProps.expandAllRows ||
  547. expandAllGroupRows !== prevProps.expandAllGroupRows
  548. ) {
  549. this.foundation.initExpandedRowKeys({ groups: stateGroups });
  550. }
  551. /**
  552. * After dataSource is updated || (cachedColumns || cachedChildren updated)
  553. * 1. Cache filtered sorted data and a collection of data rows, stored in this
  554. * 2. Update pager and group, stored in state
  555. */
  556. if (dataSource !== prevProps.dataSource || stateCachedColumns !== prevState.cachedColumns || stateCachedChildren !== prevState.cachedChildren) {
  557. // TODO: foundation.getFilteredSortedDataSource has side effects and will be modified to the dataSource reference
  558. // Temporarily use _dataSource=[...dataSource] for processing
  559. const _dataSource = [...dataSource];
  560. const filteredSortedDataSource = this.foundation.getFilteredSortedDataSource(_dataSource, stateQueries);
  561. const allDataDisabledRowKeys = this.foundation.getAllDisabledRowKeys(filteredSortedDataSource);
  562. this.foundation.setCachedFilteredSortedDataSource(filteredSortedDataSource);
  563. this.foundation.setAllDisabledRowKeys(allDataDisabledRowKeys);
  564. states.dataSource = filteredSortedDataSource;
  565. if (this.props.groupBy) {
  566. states.groups = null;
  567. }
  568. }
  569. // when dataSource has change, should reset currentPage
  570. if (dataSource !== prevProps.dataSource) {
  571. states.pagination = isObject(statePagination) ? {
  572. ...statePagination,
  573. currentPage: isObject(propsPagination) && propsPagination.currentPage ? propsPagination.currentPage : 1,
  574. } : statePagination;
  575. }
  576. if (Object.keys(states).length) {
  577. const {
  578. // eslint-disable-next-line @typescript-eslint/no-shadow
  579. pagination: mergedStatePagination = null,
  580. queries: stateQueries = null,
  581. dataSource: stateDataSource = null,
  582. } = states;
  583. const handledProps: Partial<NormalTableState<RecordType>> = this.foundation.getCurrentPageData(stateDataSource, mergedStatePagination as TablePaginationProps, stateQueries);
  584. // After the pager is updated, reset allRowKeys of the current page
  585. this.adapter.setAllRowKeys(handledProps.allRowKeys);
  586. this.adapter.setDisabledRowKeys(handledProps.disabledRowKeys);
  587. if ('dataSource' in states) {
  588. if (this.props.defaultExpandAllRows && handledProps.groups && handledProps.groups.size ||
  589. this.props.expandAllRows ||
  590. this.props.expandAllGroupRows
  591. ) {
  592. this.foundation.initExpandedRowKeys(handledProps);
  593. }
  594. states.pagination = handledProps.pagination;
  595. }
  596. // Centrally update paging related state
  597. const statesKeys: any[] = Object.keys(states);
  598. for (const k of statesKeys) {
  599. this.setState({ [k]: handledProps[k] });
  600. }
  601. }
  602. if (this.adapter.isAnyColumnFixed() || (this.props.showHeader && this.adapter.useFixedHeader())) {
  603. if (!this.debouncedWindowResize) {
  604. window.addEventListener('resize', this.debouncedWindowResize);
  605. }
  606. }
  607. }
  608. componentWillUnmount() {
  609. super.componentWillUnmount();
  610. if (this.debouncedWindowResize) {
  611. window.removeEventListener('resize', this.debouncedWindowResize);
  612. (this.debouncedWindowResize as any).cancel();
  613. this.debouncedWindowResize = null;
  614. }
  615. }
  616. // TODO: notify when data don't have key
  617. _warnIfNoKey = () => {
  618. if (
  619. (this.props.rowSelection || this.props.expandedRowRender) &&
  620. some(this.props.dataSource, record => this.foundation.getRecordKey(record) == null)
  621. ) {
  622. logger.error(
  623. 'You must specify a key for each element in the dataSource or use "rowKey" to specify an attribute name as the primary key!'
  624. );
  625. }
  626. };
  627. _invokeRowSelection = (funcName: string, ...args: any[]) => {
  628. const func = get(this.state, ['rowSelection', funcName]);
  629. if (typeof func === 'function') {
  630. func(...args);
  631. }
  632. };
  633. _invokeColumnFn = (key: string, funcName: string, ...args: any[]) => {
  634. if (key && funcName) {
  635. const column = this.foundation.getQuery(key);
  636. const func = get(column, funcName, null);
  637. if (typeof func === 'function') {
  638. func(...args);
  639. }
  640. }
  641. };
  642. _cacheHeaderRef = (node: HTMLDivElement) => {
  643. this.headerWrapRef.current = node;
  644. };
  645. getCurrentPageData = () => {
  646. const pageData = this.foundation.getCurrentPageData();
  647. const retObj = ['dataSource', 'groups'].reduce((result, key) => {
  648. if (pageData[key]) {
  649. result[key] = pageData[key];
  650. }
  651. return result;
  652. }, {});
  653. return cloneDeep(retObj);
  654. };
  655. getColumns = (columns: ColumnProps<RecordType>[], children: ReactNode) => (!Array.isArray(columns) || !columns || !columns.length ? getColumns(children) : columns);
  656. // @ts-ignore
  657. getCellWidths = (...args: any[]) => this.foundation.getCellWidths(...args);
  658. // @ts-ignore
  659. setHeadWidths = (...args: any[]) => this.foundation.setHeadWidths(...args);
  660. // @ts-ignore
  661. getHeadWidths = (...args: any[]) => this.foundation.getHeadWidths(...args);
  662. // @ts-ignore
  663. mergedRowExpandable = (...args: any[]) => this.foundation.mergedRowExpandable(...args);
  664. // @ts-ignore
  665. setBodyHasScrollbar = (...args: any[]) => this.foundation.setBodyHasScrollbar(...args);
  666. handleWheel = (event: React.WheelEvent<HTMLDivElement>) => {
  667. const { scroll = {} } = this.props;
  668. if (window.navigator.userAgent.match(/Trident\/7\./) && scroll.y) {
  669. event.preventDefault();
  670. const wd = event.deltaY;
  671. const { target } = event;
  672. // const { bodyTable, fixedColumnsBodyLeft, fixedColumnsBodyRight } = this;
  673. const bodyTable = this.bodyWrapRef.current;
  674. let scrollTop = 0;
  675. if (this.lastScrollTop) {
  676. scrollTop = this.lastScrollTop + wd;
  677. } else {
  678. scrollTop = wd;
  679. }
  680. if (bodyTable && target !== bodyTable) {
  681. bodyTable.scrollTop = scrollTop;
  682. }
  683. }
  684. };
  685. handleBodyScrollLeft = (e: BodyScrollEvent) => {
  686. if (e.currentTarget !== e.target) {
  687. return;
  688. }
  689. const { target } = e;
  690. // const { headTable, bodyTable } = this;
  691. const headTable = this.headerWrapRef.current;
  692. const bodyTable = this.bodyWrapRef.current;
  693. if (target.scrollLeft !== this.lastScrollLeft) {
  694. if (target === bodyTable && headTable) {
  695. headTable.scrollLeft = target.scrollLeft;
  696. } else if (target === headTable && bodyTable) {
  697. bodyTable.scrollLeft = target.scrollLeft;
  698. }
  699. this.setScrollPositionClassName();
  700. }
  701. // Remember last scrollLeft for scroll direction detecting.
  702. this.lastScrollLeft = target.scrollLeft;
  703. };
  704. handleWindowResize = () => {
  705. this.syncTableWidth();
  706. this.setScrollPositionClassName();
  707. };
  708. handleBodyScrollTop = (e: BodyScrollEvent) => {
  709. const { target } = e;
  710. if (e.currentTarget !== target) {
  711. return;
  712. }
  713. const { scroll = {} } = this.props;
  714. // const { headTable, bodyTable, fixedColumnsBodyLeft, fixedColumnsBodyRight } = this;
  715. const headTable = this.headerWrapRef.current;
  716. const bodyTable = this.bodyWrapRef.current;
  717. if (target.scrollTop !== this.lastScrollTop && scroll.y && target !== headTable) {
  718. const { scrollTop } = target;
  719. if (bodyTable && target !== bodyTable) {
  720. bodyTable.scrollTop = scrollTop;
  721. }
  722. }
  723. // Remember last scrollTop for scroll direction detecting.
  724. this.lastScrollTop = target.scrollTop;
  725. };
  726. handleBodyScroll = (e: BodyScrollEvent) => {
  727. this.handleBodyScrollLeft(e);
  728. this.handleBodyScrollTop(e);
  729. };
  730. setScrollPosition = (position: BodyScrollPosition) => {
  731. const { prefixCls } = this.props;
  732. const positionAll = [
  733. `${prefixCls}-scroll-position-both`,
  734. `${prefixCls}-scroll-position-middle`,
  735. `${prefixCls}-scroll-position-left`,
  736. `${prefixCls}-scroll-position-right`,
  737. ];
  738. this.scrollPosition = position;
  739. const tableNode = this.wrapRef.current;
  740. if (tableNode && tableNode.nodeType) {
  741. if (position === 'both') {
  742. const acceptPosition = [`${prefixCls}-scroll-position-left`, `${prefixCls}-scroll-position-right`];
  743. tableNode.classList.remove(...difference(positionAll, acceptPosition));
  744. tableNode.classList.add(...acceptPosition);
  745. } else {
  746. const acceptPosition = [`${prefixCls}-scroll-position-${position}`];
  747. tableNode.classList.remove(...difference(positionAll, acceptPosition));
  748. tableNode.classList.add(...acceptPosition);
  749. }
  750. }
  751. };
  752. setScrollPositionClassName = () => {
  753. const node = this.bodyWrapRef.current;
  754. if (node && node.children && node.children.length) {
  755. const scrollToLeft = node.scrollLeft === 0;
  756. // why use Math.abs? @see https://bugzilla.mozilla.org/show_bug.cgi?id=1447743
  757. const scrollToRight =
  758. Math.abs(node.scrollLeft) + 1 >=
  759. node.children[0].getBoundingClientRect().width - node.getBoundingClientRect().width;
  760. if (scrollToLeft && scrollToRight) {
  761. this.setScrollPosition('both');
  762. } else if (scrollToLeft) {
  763. this.setScrollPosition('left');
  764. } else if (scrollToRight) {
  765. this.setScrollPosition('right');
  766. } else if (this.scrollPosition !== 'middle') {
  767. this.setScrollPosition('middle');
  768. }
  769. }
  770. };
  771. syncTableWidth = () => {
  772. if (this.rootWrapRef && this.rootWrapRef.current) {
  773. this.setState({ tableWidth: this.rootWrapRef.current.getBoundingClientRect().width });
  774. }
  775. };
  776. renderSelection = (record = {} as any, inHeader = false): React.ReactNode => {
  777. const { rowSelection, allDisabledRowKeysSet } = this.state;
  778. if (rowSelection && typeof rowSelection === 'object') {
  779. const { selectedRowKeys = [], selectedRowKeysSet = new Set(), getCheckboxProps, disabled } = rowSelection;
  780. if (inHeader) {
  781. const columnKey = get(rowSelection, 'key', strings.DEFAULT_KEY_COLUMN_SELECTION);
  782. const allRowKeys = this.cachedFilteredSortedRowKeys;
  783. const allRowKeysSet = this.cachedFilteredSortedRowKeysSet;
  784. const allIsSelected = this.foundation.allIsSelected(selectedRowKeysSet, allDisabledRowKeysSet, allRowKeys);
  785. const hasRowSelected = this.foundation.hasRowSelected(selectedRowKeys, allRowKeysSet);
  786. return (
  787. <ColumnSelection
  788. aria-label={`${allIsSelected ? 'Deselect' : 'Select'} all rows`}
  789. disabled={disabled}
  790. key={columnKey}
  791. selected={allIsSelected}
  792. indeterminate={hasRowSelected && !allIsSelected}
  793. onChange={(selected, e) => {
  794. this.toggleSelectAllRow(selected, e);
  795. }}
  796. />
  797. );
  798. } else {
  799. const key = this.foundation.getRecordKey(record);
  800. const selected = selectedRowKeysSet.has(key);
  801. const checkboxPropsFn = () => (typeof getCheckboxProps === 'function' ? getCheckboxProps(record) : {});
  802. return (
  803. <ColumnSelection
  804. aria-label={`${selected ? 'Deselect' : 'Select'} this row`}
  805. getCheckboxProps={checkboxPropsFn}
  806. selected={selected}
  807. onChange={(status, e) => this.toggleSelectRow(status, key, e)}
  808. />
  809. );
  810. }
  811. }
  812. return null;
  813. };
  814. renderRowSelectionCallback = (text: string, record: RecordType = {} as RecordType) => this.renderSelection(record);
  815. renderTitleSelectionCallback = () => this.renderSelection(null, true);
  816. normalizeSelectionColumn = (props: { rowSelection?: TableStateRowSelection<RecordType>; prefixCls?: string } = {}) => {
  817. const { rowSelection, prefixCls } = props;
  818. let column: ColumnProps = {};
  819. if (rowSelection) {
  820. const needOmitSelectionKey = ['selectedRowKeys', 'selectedRowKeysSet'];
  821. column = { key: strings.DEFAULT_KEY_COLUMN_SELECTION };
  822. if (isObject(rowSelection)) {
  823. column = { ...column, ...omit(rowSelection, needOmitSelectionKey) };
  824. }
  825. column.className = classnames(column.className, `${prefixCls}-column-selection`);
  826. column.title = this.renderTitleSelectionCallback;
  827. column.render = this.renderRowSelectionCallback;
  828. }
  829. return column;
  830. };
  831. // If there is a scroll bar, manually construct a column and insert it into the header
  832. normalizeScrollbarColumn = (props: { scrollbarWidth?: number } = {}): { key: 'column-scrollbar'; width: number; fixed: 'right' } => {
  833. const { scrollbarWidth = 0 } = props;
  834. return {
  835. key: strings.DEFAULT_KEY_COLUMN_SCROLLBAR as 'column-scrollbar',
  836. width: scrollbarWidth,
  837. fixed: 'right',
  838. };
  839. };
  840. /**
  841. * render expand icon
  842. * @param {Object} record
  843. * @param {Boolean} isNested
  844. * @param {String} groupKey
  845. * @returns {ReactNode}
  846. */
  847. renderExpandIcon = (record = {}, isNested = false, groupKey: string | number = null) => {
  848. const { expandedRowKeys } = this.state;
  849. const { expandIcon } = this.props;
  850. const key =
  851. typeof groupKey === 'string' || typeof groupKey === 'number' ?
  852. groupKey :
  853. this.foundation.getRecordKey(record as RecordType);
  854. return (
  855. <ExpandedIcon
  856. key={key}
  857. componentType={isNested ? 'tree' : 'expand'}
  858. expanded={includes(expandedRowKeys, key)}
  859. expandIcon={expandIcon}
  860. onClick={(expanded, e) => this.handleRowExpanded(expanded, key, e)}
  861. />
  862. );
  863. };
  864. // @ts-ignore
  865. handleRowExpanded = (...args: any[]) => this.foundation.handleRowExpanded(...args);
  866. normalizeExpandColumn = (props: { prefixCls?: string; expandCellFixed?: ArrayElement<typeof strings.FIXED_SET>; expandIcon?: ExpandIcon } = {}) => {
  867. let column: ColumnProps = null;
  868. const { prefixCls, expandCellFixed, expandIcon } = props;
  869. column = { fixed: expandCellFixed, key: strings.DEFAULT_KEY_COLUMN_EXPAND };
  870. column.className = classnames(column.className, `${prefixCls}-column-expand`);
  871. column.render =
  872. expandIcon !== false ?
  873. (text = '', record, index) =>
  874. (this.adapter.mergedRowExpandable(record) ? this.renderExpandIcon(record) : null) :
  875. () => null;
  876. return column;
  877. };
  878. /**
  879. * Add sorting, filtering, and rendering functions to columns, and add column event handling
  880. * Title support function, passing parameters as {filter: node, sorter: node, selection: node}
  881. * @param {*} column
  882. */
  883. addFnsInColumn = (column: ColumnProps = {}) => {
  884. const { prefixCls } = this.props;
  885. if (column && (column.sorter || column.filters || column.useFullRender)) {
  886. let hasSorterOrFilter = false;
  887. const { dataIndex, title: rawTitle, useFullRender } = column;
  888. const curQuery = this.foundation.getQuery(dataIndex);
  889. const titleMap: ColumnTitleProps = {};
  890. const titleArr = [];
  891. // useFullRender adds select buttons to each column
  892. if (useFullRender) {
  893. titleMap.selection = this.renderSelection(null, true);
  894. }
  895. const stateSortOrder = get(curQuery, 'sortOrder');
  896. const defaultSortOrder = get(curQuery, 'defaultSortOrder', false);
  897. const sortOrder = this.foundation.isSortOrderValid(stateSortOrder) ? stateSortOrder : defaultSortOrder;
  898. const showEllipsisTitle = shouldShowEllipsisTitle(column.ellipsis);
  899. const TitleNode = typeof rawTitle !== 'function' && (
  900. <span
  901. className={`${prefixCls}-row-head-title`}
  902. key={strings.DEFAULT_KEY_COLUMN_TITLE}
  903. title={showEllipsisTitle && typeof rawTitle === 'string' ? rawTitle : undefined}
  904. >
  905. {rawTitle as React.ReactNode}
  906. </span>
  907. );
  908. if (typeof column.sorter === 'function' || column.sorter === true) {
  909. // In order to increase the click hot area of ​​sorting, when sorting is required & useFullRender is false,
  910. // both the title and sorting areas are used as the click hot area for sorting。
  911. const sorter = (
  912. <ColumnSorter
  913. key={strings.DEFAULT_KEY_COLUMN_SORTER}
  914. sortOrder={sortOrder}
  915. onClick={e => this.foundation.handleSort(column, e)}
  916. title={TitleNode}
  917. />
  918. );
  919. useFullRender && (titleMap.sorter = sorter);
  920. hasSorterOrFilter = true;
  921. titleArr.push(sorter);
  922. } else {
  923. titleArr.push(TitleNode);
  924. }
  925. const stateFilteredValue = get(curQuery, 'filteredValue');
  926. const defaultFilteredValue = get(curQuery, 'defaultFilteredValue');
  927. const filteredValue = stateFilteredValue ? stateFilteredValue : defaultFilteredValue;
  928. if ((Array.isArray(column.filters) && column.filters.length) || isValidElement(column.filterDropdown)) {
  929. const filter = (
  930. <ColumnFilter
  931. key={strings.DEFAULT_KEY_COLUMN_FILTER}
  932. {...curQuery}
  933. filteredValue={filteredValue}
  934. onFilterDropdownVisibleChange={(visible: boolean) =>
  935. this.foundation.toggleShowFilter(dataIndex, visible)
  936. }
  937. onSelect={(data: OnSelectData) => this.foundation.handleFilterSelect(dataIndex, data)}
  938. />
  939. );
  940. useFullRender && (titleMap.filter = filter);
  941. hasSorterOrFilter = true;
  942. titleArr.push(filter);
  943. }
  944. const newTitle =
  945. typeof rawTitle === 'function' ? (
  946. () => rawTitle(titleMap)
  947. ) : hasSorterOrFilter ? (
  948. <div className={`${prefixCls}-operate-wrapper`}>{titleArr}</div>
  949. ) : (
  950. titleArr
  951. );
  952. column = { ...column, title: newTitle };
  953. }
  954. return column;
  955. };
  956. toggleSelectRow = (selected: boolean, realKey: string | number, e: TableSelectionCellEvent) => {
  957. this.foundation.handleSelectRow(realKey, selected, e);
  958. };
  959. toggleSelectAllRow = (selected: boolean, e: TableSelectionCellEvent) => {
  960. this.foundation.handleSelectAllRow(selected, e);
  961. };
  962. /**
  963. * render pagination
  964. * @param {object} pagination
  965. * @param {object} propRenderPagination
  966. */
  967. renderPagination = (pagination: TablePaginationProps, propRenderPagination: RenderPagination) => {
  968. if (!pagination) {
  969. return null;
  970. }
  971. // use memoized pagination
  972. const mergedPagination = this.foundation.memoizedPagination(pagination);
  973. return (
  974. <LocaleConsumer componentName="Table">
  975. {(locale: TableLocale) => {
  976. const info = this.foundation.formatPaginationInfo(mergedPagination, locale.pageText);
  977. return <TablePagination info={info} pagination={mergedPagination} renderPagination={propRenderPagination} />;
  978. }}
  979. </LocaleConsumer>
  980. );
  981. };
  982. renderTitle = (props: { title?: ReactNode | ((dataSource?: RecordType[]) => ReactNode); prefixCls?: string; dataSource?: any[] } = {}) => {
  983. let { title } = props;
  984. const { prefixCls, dataSource } = props;
  985. if (typeof title === 'function') {
  986. title = title(dataSource);
  987. }
  988. return isValidElement(title) || typeof title === 'string' ? (
  989. <div className={`${prefixCls}-title`} x-semi-prop="title">{title}</div>
  990. ) : null;
  991. };
  992. renderEmpty = (props: { prefixCls?: string; empty?: ReactNode; dataSource?: RecordType[] } = {}) => {
  993. const { prefixCls, empty, dataSource } = props;
  994. const wrapCls = `${prefixCls}-placeholder`;
  995. const isEmpty = this.foundation.isEmpty(dataSource);
  996. if (!isEmpty) {
  997. return null;
  998. }
  999. return (
  1000. <LocaleConsumer componentName="Table" key={'emptyText'}>
  1001. {(locale: TableLocale, localeCode: string) => (
  1002. <div className={wrapCls}>
  1003. <div className={`${prefixCls}-empty`} x-semi-prop="empty">
  1004. {empty || locale.emptyText}
  1005. </div>
  1006. </div>
  1007. )}
  1008. </LocaleConsumer>
  1009. );
  1010. };
  1011. renderFooter = (props: { footer?: ReactNode | ((dataSource?: RecordType[]) => ReactNode); prefixCls?: string; dataSource?: RecordType[] } = {}) => {
  1012. let { footer } = props;
  1013. const { prefixCls, dataSource } = props;
  1014. if (typeof footer === 'function') {
  1015. footer = footer(dataSource);
  1016. }
  1017. return isValidElement(footer) || typeof footer === 'string' ? (
  1018. <div className={`${prefixCls}-footer`} key="footer" x-semi-prop="footer">
  1019. {footer}
  1020. </div>
  1021. ) : null;
  1022. };
  1023. renderMainTable = (props: any) => {
  1024. const useFixedHeader = this.adapter.useFixedHeader();
  1025. const emptySlot = this.renderEmpty(props);
  1026. const table = [
  1027. this.renderTable({
  1028. ...props,
  1029. fixed: false,
  1030. useFixedHeader,
  1031. headerRef: this._cacheHeaderRef,
  1032. bodyRef: this.bodyWrapRef,
  1033. includeHeader: !useFixedHeader,
  1034. emptySlot
  1035. }),
  1036. this.renderFooter(props),
  1037. ];
  1038. return table;
  1039. };
  1040. renderTable = (props: RenderTableProps<RecordType>) => {
  1041. const {
  1042. columns,
  1043. filteredColumns,
  1044. fixed,
  1045. useFixedHeader,
  1046. scroll,
  1047. prefixCls,
  1048. anyColumnFixed,
  1049. includeHeader,
  1050. showHeader,
  1051. components,
  1052. headerRef,
  1053. bodyRef,
  1054. onHeaderRow,
  1055. rowSelection,
  1056. dataSource,
  1057. bodyHasScrollBar,
  1058. disabledRowKeysSet,
  1059. sticky,
  1060. } = props;
  1061. const selectedRowKeysSet = get(rowSelection, 'selectedRowKeysSet', new Set());
  1062. const tableLayout = this.adapter.getTableLayout();
  1063. const headTable =
  1064. fixed || useFixedHeader ? (
  1065. <HeadTable
  1066. key="head"
  1067. tableLayout={tableLayout}
  1068. ref={headerRef}
  1069. columns={filteredColumns}
  1070. prefixCls={prefixCls}
  1071. fixed={fixed}
  1072. handleBodyScroll={this.handleBodyScrollLeft}
  1073. components={components}
  1074. scroll={scroll}
  1075. showHeader={showHeader}
  1076. selectedRowKeysSet={selectedRowKeysSet}
  1077. onHeaderRow={onHeaderRow}
  1078. dataSource={dataSource}
  1079. bodyHasScrollBar={bodyHasScrollBar}
  1080. sticky={sticky}
  1081. />
  1082. ) : null;
  1083. const bodyTable = (
  1084. <BodyTable
  1085. {...omit(props, ['rowSelection', 'headWidths']) as any}
  1086. key="body"
  1087. ref={bodyRef}
  1088. columns={filteredColumns}
  1089. fixed={fixed}
  1090. prefixCls={prefixCls}
  1091. handleWheel={this.handleWheel}
  1092. handleBodyScroll={this.handleBodyScroll}
  1093. anyColumnFixed={anyColumnFixed}
  1094. tableLayout={tableLayout}
  1095. includeHeader={includeHeader}
  1096. showHeader={showHeader}
  1097. scroll={scroll}
  1098. components={components}
  1099. store={this.store}
  1100. selectedRowKeysSet={selectedRowKeysSet}
  1101. disabledRowKeysSet={disabledRowKeysSet}
  1102. />
  1103. );
  1104. return [headTable, bodyTable];
  1105. };
  1106. /**
  1107. * When columns change, call this function to get the latest withFnsColumns
  1108. * In addition to changes in columns, these props changes must be recalculated
  1109. * - hideExpandedColumn
  1110. * -rowSelection changes from trusy to falsy or rowSelection.hidden changes
  1111. * -isAnyFixedRight(columns) || get(scroll,'y') changes
  1112. *
  1113. * columns变化时,调用此函数获取最新的withFnsColumns
  1114. * 除了 columns 变化,这些 props 变化也要重新计算
  1115. * - hideExpandedColumn
  1116. * - rowSelection 从 trusy 变为 falsy 或 rowSelection.hidden 发生变化
  1117. * - isAnyFixedRight(columns) || get(scroll, 'y') 发生变化
  1118. *
  1119. * @param {Array} queries
  1120. * @param {Array} cachedColumns
  1121. * @returns columns after adding extended functions
  1122. */
  1123. handleColumns = (queries: ColumnProps<RecordType>[], cachedColumns: ColumnProps<RecordType>[]) => {
  1124. const { hideExpandedColumn, scroll, prefixCls, expandCellFixed, expandIcon, rowSelection } = this.props;
  1125. const childrenColumnName = 'children';
  1126. let columns: ColumnProps<RecordType>[] = cloneDeep(cachedColumns);
  1127. // eslint-disable-next-line @typescript-eslint/no-shadow
  1128. const addFns = (columns: ColumnProps<RecordType>[] = []) => {
  1129. if (Array.isArray(columns) && columns.length) {
  1130. each(columns, (column, index, originColumns) => {
  1131. const newColumn = this.addFnsInColumn(column);
  1132. const children = column[childrenColumnName];
  1133. if (Array.isArray(children) && children.length) {
  1134. const newChildren = [...children];
  1135. addFns(newChildren);
  1136. newColumn[childrenColumnName] = newChildren;
  1137. }
  1138. originColumns[index] = newColumn;
  1139. });
  1140. }
  1141. };
  1142. addFns(columns);
  1143. // hideExpandedColumn=false render expand column separately
  1144. if (!hideExpandedColumn) {
  1145. const column = this.normalizeExpandColumn({ prefixCls, expandCellFixed, expandIcon });
  1146. const destIndex = findIndex(columns, item => item.key === strings.DEFAULT_KEY_COLUMN_EXPAND);
  1147. if (column) {
  1148. if (destIndex > -1) {
  1149. columns[destIndex] = { ...column, ...columns[destIndex] };
  1150. } else if (column.fixed === 'right') {
  1151. columns = [...columns, column];
  1152. } else {
  1153. columns = [column, ...columns];
  1154. }
  1155. }
  1156. }
  1157. // selection column
  1158. if (rowSelection && !get(rowSelection, 'hidden')) {
  1159. const destIndex = findIndex(columns, item => item.key === strings.DEFAULT_KEY_COLUMN_SELECTION);
  1160. const column = this.normalizeSelectionColumn({ rowSelection, prefixCls });
  1161. if (destIndex > -1) {
  1162. columns[destIndex] = { ...column, ...columns[destIndex] };
  1163. } else if (column.fixed === 'right') {
  1164. columns = [...columns, column];
  1165. } else {
  1166. columns = [column, ...columns];
  1167. }
  1168. }
  1169. assignColumnKeys(columns);
  1170. return columns;
  1171. };
  1172. /**
  1173. * Convert children to columns object
  1174. * @param {Array} columns
  1175. * @param {ReactNode} children
  1176. * @returns {Array}
  1177. */
  1178. normalizeColumns = (columns: ColumnProps<RecordType>[], children: ReactNode) => {
  1179. const normalColumns = cloneDeep(this.getColumns(columns, children));
  1180. return normalColumns;
  1181. };
  1182. /**
  1183. * Combine pagination and table paging processing functions
  1184. */
  1185. mergePagination = (pagination: TablePaginationProps) => {
  1186. const newPagination = { onChange: this.foundation.setPage, ...pagination };
  1187. return newPagination;
  1188. };
  1189. render() {
  1190. let {
  1191. scroll,
  1192. prefixCls,
  1193. className,
  1194. style: wrapStyle = {},
  1195. bordered,
  1196. id,
  1197. pagination: propPagination,
  1198. virtualized,
  1199. size,
  1200. renderPagination: propRenderPagination,
  1201. getVirtualizedListRef,
  1202. loading,
  1203. hideExpandedColumn,
  1204. rowSelection: propRowSelection,
  1205. ...rest
  1206. } = this.props;
  1207. let {
  1208. rowSelection,
  1209. expandedRowKeys,
  1210. headWidths,
  1211. tableWidth,
  1212. pagination,
  1213. dataSource,
  1214. queries,
  1215. cachedColumns,
  1216. bodyHasScrollBar,
  1217. } = this.state;
  1218. wrapStyle = { ...wrapStyle };
  1219. let columns: ColumnProps<RecordType>[];
  1220. /**
  1221. * As state.queries will change, the columns should be refreshed as a whole at this time
  1222. * The scene of changes in queries
  1223. * 1. Filter
  1224. * 2. Pagination
  1225. *
  1226. * useFullRender needs to be passed to the user selection ReactNode, so columns need to be recalculated every time the selectedRowKeys changes
  1227. * 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
  1228. *
  1229. * 由于state.queries会发生变化,此时columns应该整体刷新
  1230. * queries变化的场景
  1231. * 1. 筛选
  1232. * 2. 分页
  1233. * useFullRender需要传给用户selection ReactNode,因此需要每次selectedRowKeys变化时重新计算columns
  1234. * TODO: 未来可以将传给用户的selection改为函数类型,让用户执行函数获取selection title的实时状态
  1235. */
  1236. if (!this.adapter.isAnyColumnUseFullRender(queries)) {
  1237. const rowSelectionUpdate: boolean = propRowSelection && !get(propRowSelection, 'hidden');
  1238. columns = this.foundation.memoizedWithFnsColumns(
  1239. queries,
  1240. cachedColumns,
  1241. rowSelectionUpdate,
  1242. hideExpandedColumn,
  1243. // Update the columns after the body scrollbar changes to ensure that the head and body are aligned
  1244. bodyHasScrollBar
  1245. );
  1246. } else {
  1247. columns = this.handleColumns(queries, cachedColumns);
  1248. }
  1249. const filteredColumns: ColumnProps<RecordType>[] = this.foundation.memoizedFilterColumns(columns);
  1250. const flattenFnsColumns: ColumnProps<RecordType>[] = this.foundation.memoizedFlattenFnsColumns(columns);
  1251. const anyColumnFixed = this.adapter.isAnyColumnFixed(columns);
  1252. /**
  1253. * - If it is the first page break, you need to calculate the current page
  1254. * - If it is manual paging, call foundation to modify the state
  1255. *
  1256. * TODO: After merging issue 1007, you can place it in the constructor to complete
  1257. * The reason is that #1007 exposes the parameters required by getCurrentPageData in the constructor
  1258. */
  1259. // if (isNull(dataSource)) {
  1260. // const pageData: BasePageData<RecordType> = this.foundation.getCurrentPageData(this.props.dataSource);
  1261. // dataSource = pageData.dataSource;
  1262. // pagination = pageData.pagination;
  1263. // }
  1264. const props = {
  1265. ...rest,
  1266. ...this.state,
  1267. // props not in rest
  1268. virtualized,
  1269. scroll,
  1270. prefixCls,
  1271. size,
  1272. hideExpandedColumn,
  1273. // renamed state
  1274. columns,
  1275. // calculated value
  1276. anyColumnFixed,
  1277. rowExpandable: this.mergedRowExpandable,
  1278. pagination,
  1279. dataSource,
  1280. rowSelection,
  1281. expandedRowKeys,
  1282. renderExpandIcon: this.renderExpandIcon,
  1283. filteredColumns,
  1284. };
  1285. const x = get(scroll, 'x');
  1286. const y = get(scroll, 'y');
  1287. if (virtualized) {
  1288. if (typeof wrapStyle.width !== 'number') {
  1289. wrapStyle.width = x;
  1290. }
  1291. }
  1292. const wrapCls = classnames({
  1293. [`${prefixCls}-${strings.SIZE_SMALL}`]: size === strings.SIZE_SMALL,
  1294. [`${prefixCls}-${strings.SIZE_MIDDLE}`]: size === strings.SIZE_MIDDLE,
  1295. [`${prefixCls}-virtualized`]: Boolean(virtualized),
  1296. [`${prefixCls}-bordered`]: bordered,
  1297. [`${prefixCls}-fixed-header`]: Boolean(y),
  1298. [`${prefixCls}-scroll-position-left`]: ['both', 'left'].includes(this.position),
  1299. [`${prefixCls}-scroll-position-right`]: ['both', 'right'].includes(this.position),
  1300. });
  1301. // pagination
  1302. const tablePagination = pagination && propPagination ? this.renderPagination(pagination as TablePaginationProps, propRenderPagination) : null;
  1303. const paginationPosition = get(propPagination, 'position', 'bottom');
  1304. const tableContextValue: TableContextProps = {
  1305. ...this.context,
  1306. headWidths,
  1307. tableWidth,
  1308. anyColumnFixed,
  1309. flattenedColumns: flattenFnsColumns,
  1310. renderExpandIcon: this.renderExpandIcon,
  1311. renderSelection: this.renderSelection,
  1312. setHeadWidths: this.setHeadWidths,
  1313. getHeadWidths: this.getHeadWidths,
  1314. getCellWidths: this.getCellWidths,
  1315. handleRowExpanded: this.handleRowExpanded,
  1316. getVirtualizedListRef,
  1317. setBodyHasScrollbar: this.setBodyHasScrollbar,
  1318. };
  1319. return (
  1320. <div
  1321. ref={this.rootWrapRef}
  1322. className={classnames(className, `${prefixCls}-wrapper`, `${prefixCls}-wrapper-${props.direction}`)}
  1323. data-column-fixed={anyColumnFixed}
  1324. style={wrapStyle}
  1325. id={id}
  1326. >
  1327. <TableContextProvider {...tableContextValue} direction={props.direction}>
  1328. <Spin spinning={loading} size="large">
  1329. <div ref={this.wrapRef} className={wrapCls}>
  1330. <React.Fragment key={'pagination-top'}>
  1331. {['top', 'both'].includes(paginationPosition) ? tablePagination : null}
  1332. </React.Fragment>
  1333. {this.renderTitle({
  1334. title: (props as any).title,
  1335. dataSource: props.dataSource,
  1336. prefixCls: props.prefixCls,
  1337. })}
  1338. <div className={`${prefixCls}-container`}>{this.renderMainTable({ ...props })}</div>
  1339. <React.Fragment key={'pagination-bottom'}>
  1340. {['bottom', 'both'].includes(paginationPosition) ? tablePagination : null}
  1341. </React.Fragment>
  1342. </div>
  1343. </Spin>
  1344. </TableContextProvider>
  1345. </div>
  1346. );
  1347. }
  1348. }
  1349. export default Table;