Table.tsx 56 KB

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