Table.tsx 58 KB

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