Table.tsx 57 KB

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