Table.tsx 63 KB

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