TableCell.tsx 14 KB

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