TableCell.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. /* eslint-disable prefer-destructuring */
  2. /* eslint-disable eqeqeq */
  3. import React, { createRef, Fragment, ReactNode } from 'react';
  4. import classnames from 'classnames';
  5. import PropTypes from 'prop-types';
  6. import { get, noop, set, omit, isEqual, merge } from 'lodash';
  7. import { cssClasses, numbers } from '@douyinfe/semi-foundation/table/constants';
  8. import TableCellFoundation, { TableCellAdapter } from '@douyinfe/semi-foundation/table/cellFoundation';
  9. import { isSelectionColumn, isExpandedColumn } from '@douyinfe/semi-foundation/table/utils';
  10. import BaseComponent, { BaseProps } from '../_base/baseComponent';
  11. import Context from './table-context';
  12. import { amendTableWidth } from './utils';
  13. import { Align, ColumnProps } from './interface';
  14. export interface TableCellProps extends BaseProps {
  15. record?: Record<string, any>;
  16. prefixCls?: string;
  17. index?: number; // index of dataSource
  18. fixedLeft?: boolean | number;
  19. lastFixedLeft?: boolean;
  20. fixedRight?: boolean | number;
  21. firstFixedRight?: boolean;
  22. indent?: number; // The level of the tree structure
  23. indentSize?: number; // Tree structure indent size
  24. column?: ColumnProps; // The column of the current cell
  25. /**
  26. * Does the first column include expandIcon
  27. * When hideExpandedColumn is true or isSection is true
  28. * expandIcon is a custom icon or true
  29. */
  30. expandIcon?: ReactNode | boolean;
  31. renderExpandIcon?: (record: Record<string, any>) => ReactNode;
  32. hideExpandedColumn?: boolean;
  33. component?: any;
  34. onClick?: (record: Record<string, any>, e: React.MouseEvent) => void; // callback of click cell event
  35. onDidUpdate?: (ref: React.MutableRefObject<any>) => void;
  36. isSection?: boolean; // Whether it is in group row
  37. width?: string | number; // cell width
  38. height?: string | number; // cell height
  39. selected?: boolean; // Whether the current row is selected
  40. expanded?: boolean; // Whether the current line is expanded
  41. disabled?: boolean;
  42. colIndex?: number;
  43. }
  44. function isInvalidRenderCellText(text: any) {
  45. return text && !React.isValidElement(text) && Object.prototype.toString.call(text) === '[object Object]';
  46. }
  47. export default class TableCell extends BaseComponent<TableCellProps, Record<string, any>> {
  48. static contextType = Context;
  49. static defaultProps = {
  50. indent: 0,
  51. indentSize: numbers.DEFAULT_INDENT_WIDTH,
  52. onClick: noop,
  53. prefixCls: cssClasses.PREFIX,
  54. component: 'td',
  55. onDidUpdate: noop,
  56. column: {},
  57. };
  58. static propTypes = {
  59. record: PropTypes.object,
  60. prefixCls: PropTypes.string,
  61. index: PropTypes.number,
  62. fixedLeft: PropTypes.oneOfType([PropTypes.bool, PropTypes.number]),
  63. lastFixedLeft: PropTypes.bool,
  64. fixedRight: PropTypes.oneOfType([PropTypes.bool, PropTypes.number]),
  65. firstFixedRight: PropTypes.bool,
  66. indent: PropTypes.number,
  67. indentSize: PropTypes.number,
  68. column: PropTypes.object,
  69. expandIcon: PropTypes.any,
  70. renderExpandIcon: PropTypes.func,
  71. hideExpandedColumn: PropTypes.bool,
  72. component: PropTypes.any,
  73. onClick: PropTypes.func,
  74. onDidUpdate: PropTypes.func,
  75. isSection: PropTypes.bool,
  76. width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  77. height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  78. selected: PropTypes.bool,
  79. expanded: PropTypes.bool,
  80. colIndex: PropTypes.number,
  81. };
  82. get adapter(): TableCellAdapter {
  83. return {
  84. ...super.adapter,
  85. notifyClick: (...args) => {
  86. const { onClick } = this.props;
  87. if (typeof onClick === 'function') {
  88. onClick(...args);
  89. }
  90. },
  91. };
  92. }
  93. ref: React.MutableRefObject<any>;
  94. constructor(props: TableCellProps) {
  95. super(props);
  96. this.ref = createRef();
  97. this.foundation = new TableCellFoundation(this.adapter);
  98. }
  99. /**
  100. * Control whether to execute the render function of the cell
  101. * 1. Scenes that return true
  102. * - The cell contains the selection state, you need to calculate whether its selection state has changed during selection
  103. * - The cell contains the folding state, it needs to be calculated when the folding state has changed
  104. * 2. Scenarios that return false
  105. * - Cells without table operation operation status, only need to judge that their props have changed
  106. * At this time, the update of the table cell is controlled by the user. At this time, its update will not affect other cells
  107. *
  108. * 控制是否执行cell的render函数
  109. * 1. 返回true的场景
  110. * - cell内包含选择状态,需要在选择时计算它的选择态是否发生变化
  111. * - cell内包含折叠状态,需要在折叠时计算它的折叠态是否发生了变化
  112. * 2. 返回false的场景
  113. * - 没有table操作操作状态的cell,只需判断自己的props发生了变化
  114. * 此时table cell的更新由用户自己控制,此时它的更新不会影响其他cell
  115. *
  116. * @param {*} nextProps
  117. * @returns
  118. */
  119. shouldComponentUpdate(nextProps: TableCellProps) {
  120. const props = this.props;
  121. const { column, expandIcon } = props;
  122. const cellInSelectionColumn = isSelectionColumn(column);
  123. // The expand button may be in a separate column or in the first data column
  124. const columnHasExpandIcon = isExpandedColumn(column) || expandIcon;
  125. if ((cellInSelectionColumn || columnHasExpandIcon) && !isEqual(nextProps, this.props)) {
  126. return true;
  127. } else {
  128. const omitProps = ['selected', 'expanded', 'expandIcon', 'disabled'];
  129. const propsOmitSelected = omit(props, omitProps);
  130. const nextPropsOmitSelected = omit(nextProps, omitProps);
  131. if (!isEqual(nextPropsOmitSelected, propsOmitSelected)) {
  132. return true;
  133. }
  134. }
  135. return false;
  136. }
  137. componentDidUpdate() {
  138. this.props.onDidUpdate(this.ref);
  139. }
  140. setRef = (ref: React.MutableRefObject<any>) => (this.ref = ref);
  141. handleClick = (e: React.MouseEvent) => {
  142. this.foundation.handleClick(e);
  143. const customCellProps = this.adapter.getCache('customCellProps');
  144. if (customCellProps && typeof customCellProps.onClick === 'function') {
  145. customCellProps.onClick(e);
  146. }
  147. };
  148. getTdProps() {
  149. const {
  150. record,
  151. index,
  152. column = {},
  153. fixedLeft,
  154. fixedRight,
  155. width,
  156. height,
  157. } = this.props;
  158. let tdProps: { style?: Partial<React.CSSProperties> } = {};
  159. let customCellProps = {};
  160. const fixedLeftFlag = fixedLeft || typeof fixedLeft === 'number';
  161. const fixedRightFlag = fixedRight || typeof fixedRight === 'number';
  162. if (fixedLeftFlag) {
  163. set(tdProps, 'style.left', typeof fixedLeft === 'number' ? fixedLeft : 0);
  164. } else if (fixedRightFlag) {
  165. set(tdProps, 'style.right', typeof fixedRight === 'number' ? fixedRight : 0);
  166. }
  167. if (width != null) {
  168. set(tdProps, 'style.width', width);
  169. }
  170. if (height != null) {
  171. set(tdProps, 'style.height', height);
  172. }
  173. if (column.onCell) {
  174. customCellProps = (column as any).onCell(record, index);
  175. this.adapter.setCache('customCellProps', { ...customCellProps });
  176. tdProps = { ...tdProps, ...omit(customCellProps, ['style', 'className', 'onClick']) };
  177. const customCellStyle = get(customCellProps, 'style') || {};
  178. tdProps.style = { ...tdProps.style, ...customCellStyle };
  179. }
  180. if (column.align) {
  181. tdProps.style = { ...tdProps.style, textAlign: column.align as Align };
  182. }
  183. return { tdProps, customCellProps };
  184. }
  185. /**
  186. * We should return undefined if no dataIndex is specified, but in order to
  187. * be compatible with object-path's behavior, we return the record object instead.
  188. */
  189. renderText(tdProps: { style?: React.CSSProperties; colSpan?: number; rowSpan?: number }) {
  190. const {
  191. record,
  192. indentSize,
  193. prefixCls,
  194. indent,
  195. index,
  196. expandIcon,
  197. renderExpandIcon,
  198. column = {},
  199. } = this.props;
  200. const { dataIndex, render, useFullRender } = column;
  201. let text: any,
  202. colSpan: number,
  203. rowSpan: number;
  204. if (typeof dataIndex === 'number') {
  205. text = get(record, dataIndex);
  206. } else if (!dataIndex || dataIndex.length === 0) {
  207. text = record;
  208. } else {
  209. text = get(record, dataIndex);
  210. }
  211. const indentText = (indent && indentSize) ? (
  212. <span
  213. style={{ paddingLeft: `${indentSize * indent}px` }}
  214. className={`${prefixCls}-row-indent indent-level-${indent}`}
  215. />
  216. ) : null;
  217. // column.render
  218. const realExpandIcon = typeof renderExpandIcon === 'function' ? renderExpandIcon(record) : expandIcon;
  219. if (render) {
  220. const renderOptions = {
  221. expandIcon: realExpandIcon,
  222. };
  223. // column.useFullRender
  224. if (useFullRender) {
  225. const { renderSelection } = this.context;
  226. const realSelection = typeof renderSelection === 'function' ? renderSelection(record) : null;
  227. Object.assign(renderOptions, {
  228. selection: realSelection,
  229. indentText,
  230. });
  231. }
  232. text = render(text, record, index, renderOptions);
  233. if (isInvalidRenderCellText(text)) {
  234. // eslint-disable-next-line no-param-reassign
  235. tdProps = text.props ? merge(tdProps, text.props) : tdProps;
  236. colSpan = tdProps.colSpan;
  237. rowSpan = tdProps.rowSpan;
  238. text = text.children;
  239. }
  240. }
  241. return { text, indentText, rowSpan, colSpan, realExpandIcon, tdProps };
  242. }
  243. renderInner(text: ReactNode, indentText: ReactNode, realExpandIcon: ReactNode) {
  244. const {
  245. prefixCls,
  246. isSection,
  247. expandIcon,
  248. column = {},
  249. } = this.props;
  250. const { tableWidth, anyColumnFixed } = this.context;
  251. const { useFullRender } = column;
  252. let inner = null;
  253. if (useFullRender) {
  254. inner = text;
  255. } else {
  256. inner = [
  257. <Fragment key={'indentText'}>{indentText}</Fragment>,
  258. <Fragment key={'expandIcon'}>{expandIcon ? realExpandIcon : null}</Fragment>,
  259. <Fragment key={'text'}>{text}</Fragment>,
  260. ];
  261. }
  262. if (isSection) {
  263. inner = (
  264. <div
  265. className={classnames(`${prefixCls}-section-inner`)}
  266. style={{ width: anyColumnFixed ? amendTableWidth(tableWidth) : undefined }}
  267. >
  268. {inner}
  269. </div>
  270. );
  271. }
  272. return inner;
  273. }
  274. render() {
  275. const {
  276. prefixCls,
  277. column = {},
  278. component: BodyCell,
  279. fixedLeft,
  280. fixedRight,
  281. lastFixedLeft,
  282. firstFixedRight,
  283. colIndex
  284. } = this.props;
  285. const { className } = column;
  286. const fixedLeftFlag = fixedLeft || typeof fixedLeft === 'number';
  287. const fixedRightFlag = fixedRight || typeof fixedRight === 'number';
  288. const { tdProps, customCellProps } = this.getTdProps();
  289. const renderTextResult = this.renderText(tdProps);
  290. let { text } = renderTextResult;
  291. const { indentText, rowSpan, colSpan, realExpandIcon, tdProps: newTdProps } = renderTextResult;
  292. if (rowSpan === 0 || colSpan === 0) {
  293. return null;
  294. }
  295. if (isInvalidRenderCellText(text)) {
  296. text = null;
  297. }
  298. const inner = this.renderInner(text, indentText, realExpandIcon);
  299. const columnCls = classnames(
  300. className,
  301. `${prefixCls}-row-cell`,
  302. get(customCellProps, 'className'),
  303. {
  304. [`${prefixCls}-cell-fixed-left`]: fixedLeftFlag,
  305. [`${prefixCls}-cell-fixed-left-last`]: lastFixedLeft,
  306. [`${prefixCls}-cell-fixed-right`]: fixedRightFlag,
  307. [`${prefixCls}-cell-fixed-right-first`]: firstFixedRight,
  308. }
  309. );
  310. return (
  311. <BodyCell
  312. role="gridcell"
  313. aria-colindex={colIndex + 1}
  314. className={columnCls}
  315. onClick={this.handleClick}
  316. {...newTdProps}
  317. ref={this.setRef}
  318. >
  319. {inner}
  320. </BodyCell>
  321. );
  322. }
  323. }