InternalTable.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. "use client";
  2. import * as React from 'react';
  3. import classNames from 'classnames';
  4. import { INTERNAL_HOOKS } from 'rc-table';
  5. import { convertChildrenToColumns } from "rc-table/es/hooks/useColumns";
  6. import omit from "rc-util/es/omit";
  7. import useProxyImperativeHandle from '../_util/hooks/useProxyImperativeHandle';
  8. import scrollTo from '../_util/scrollTo';
  9. import { devUseWarning } from '../_util/warning';
  10. import ConfigProvider from '../config-provider';
  11. import { ConfigContext } from '../config-provider/context';
  12. import DefaultRenderEmpty from '../config-provider/defaultRenderEmpty';
  13. import useCSSVarCls from '../config-provider/hooks/useCSSVarCls';
  14. import useSize from '../config-provider/hooks/useSize';
  15. import useBreakpoint from '../grid/hooks/useBreakpoint';
  16. import defaultLocale from '../locale/en_US';
  17. import Pagination from '../pagination';
  18. import Spin from '../spin';
  19. import { useToken } from '../theme/internal';
  20. import renderExpandIcon from './ExpandIcon';
  21. import useContainerWidth from './hooks/useContainerWidth';
  22. import useFilter, { getFilterData } from './hooks/useFilter';
  23. import useLazyKVMap from './hooks/useLazyKVMap';
  24. import usePagination, { DEFAULT_PAGE_SIZE, getPaginationParam } from './hooks/usePagination';
  25. import useSelection from './hooks/useSelection';
  26. import useSorter, { getSortData } from './hooks/useSorter';
  27. import useTitleColumns from './hooks/useTitleColumns';
  28. import RcTable from './RcTable';
  29. import RcVirtualTable from './RcTable/VirtualTable';
  30. import useStyle from './style';
  31. const EMPTY_LIST = [];
  32. const InternalTable = (props, ref) => {
  33. var _a, _b;
  34. const {
  35. prefixCls: customizePrefixCls,
  36. className,
  37. rootClassName,
  38. style,
  39. size: customizeSize,
  40. bordered,
  41. dropdownPrefixCls: customizeDropdownPrefixCls,
  42. dataSource,
  43. pagination,
  44. rowSelection,
  45. rowKey = 'key',
  46. rowClassName,
  47. columns,
  48. children,
  49. childrenColumnName: legacyChildrenColumnName,
  50. onChange,
  51. getPopupContainer,
  52. loading,
  53. expandIcon,
  54. expandable,
  55. expandedRowRender,
  56. expandIconColumnIndex,
  57. indentSize,
  58. scroll,
  59. sortDirections,
  60. locale,
  61. showSorterTooltip = {
  62. target: 'full-header'
  63. },
  64. virtual
  65. } = props;
  66. const warning = devUseWarning('Table');
  67. if (process.env.NODE_ENV !== 'production') {
  68. process.env.NODE_ENV !== "production" ? warning(!(typeof rowKey === 'function' && rowKey.length > 1), 'usage', '`index` parameter of `rowKey` function is deprecated. There is no guarantee that it will work as expected.') : void 0;
  69. }
  70. const baseColumns = React.useMemo(() => columns || convertChildrenToColumns(children), [columns, children]);
  71. const needResponsive = React.useMemo(() => baseColumns.some(col => col.responsive), [baseColumns]);
  72. const screens = useBreakpoint(needResponsive);
  73. const mergedColumns = React.useMemo(() => {
  74. const matched = new Set(Object.keys(screens).filter(m => screens[m]));
  75. return baseColumns.filter(c => !c.responsive || c.responsive.some(r => matched.has(r)));
  76. }, [baseColumns, screens]);
  77. const tableProps = omit(props, ['className', 'style', 'columns']);
  78. const {
  79. locale: contextLocale = defaultLocale,
  80. direction,
  81. table,
  82. renderEmpty,
  83. getPrefixCls,
  84. getPopupContainer: getContextPopupContainer
  85. } = React.useContext(ConfigContext);
  86. const mergedSize = useSize(customizeSize);
  87. const tableLocale = Object.assign(Object.assign({}, contextLocale.Table), locale);
  88. const rawData = dataSource || EMPTY_LIST;
  89. const prefixCls = getPrefixCls('table', customizePrefixCls);
  90. const dropdownPrefixCls = getPrefixCls('dropdown', customizeDropdownPrefixCls);
  91. const [, token] = useToken();
  92. const rootCls = useCSSVarCls(prefixCls);
  93. const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls, rootCls);
  94. const mergedExpandable = Object.assign(Object.assign({
  95. childrenColumnName: legacyChildrenColumnName,
  96. expandIconColumnIndex
  97. }, expandable), {
  98. expandIcon: (_a = expandable === null || expandable === void 0 ? void 0 : expandable.expandIcon) !== null && _a !== void 0 ? _a : (_b = table === null || table === void 0 ? void 0 : table.expandable) === null || _b === void 0 ? void 0 : _b.expandIcon
  99. });
  100. const {
  101. childrenColumnName = 'children'
  102. } = mergedExpandable;
  103. const expandType = React.useMemo(() => {
  104. if (rawData.some(item => item === null || item === void 0 ? void 0 : item[childrenColumnName])) {
  105. return 'nest';
  106. }
  107. if (expandedRowRender || (expandable === null || expandable === void 0 ? void 0 : expandable.expandedRowRender)) {
  108. return 'row';
  109. }
  110. return null;
  111. }, [rawData]);
  112. const internalRefs = {
  113. body: React.useRef(null)
  114. };
  115. // ============================ Width =============================
  116. const getContainerWidth = useContainerWidth(prefixCls);
  117. // ============================= Refs =============================
  118. const rootRef = React.useRef(null);
  119. const tblRef = React.useRef(null);
  120. useProxyImperativeHandle(ref, () => Object.assign(Object.assign({}, tblRef.current), {
  121. nativeElement: rootRef.current
  122. }));
  123. // ============================ RowKey ============================
  124. const getRowKey = React.useMemo(() => {
  125. if (typeof rowKey === 'function') {
  126. return rowKey;
  127. }
  128. return record => record === null || record === void 0 ? void 0 : record[rowKey];
  129. }, [rowKey]);
  130. const [getRecordByKey] = useLazyKVMap(rawData, childrenColumnName, getRowKey);
  131. // ============================ Events =============================
  132. const changeEventInfo = {};
  133. const triggerOnChange = (info, action, reset = false) => {
  134. var _a, _b, _c, _d;
  135. const changeInfo = Object.assign(Object.assign({}, changeEventInfo), info);
  136. if (reset) {
  137. (_a = changeEventInfo.resetPagination) === null || _a === void 0 ? void 0 : _a.call(changeEventInfo);
  138. // Reset event param
  139. if ((_b = changeInfo.pagination) === null || _b === void 0 ? void 0 : _b.current) {
  140. changeInfo.pagination.current = 1;
  141. }
  142. // Trigger pagination events
  143. if (pagination) {
  144. (_c = pagination.onChange) === null || _c === void 0 ? void 0 : _c.call(pagination, 1, (_d = changeInfo.pagination) === null || _d === void 0 ? void 0 : _d.pageSize);
  145. }
  146. }
  147. if (scroll && scroll.scrollToFirstRowOnChange !== false && internalRefs.body.current) {
  148. scrollTo(0, {
  149. getContainer: () => internalRefs.body.current
  150. });
  151. }
  152. onChange === null || onChange === void 0 ? void 0 : onChange(changeInfo.pagination, changeInfo.filters, changeInfo.sorter, {
  153. currentDataSource: getFilterData(getSortData(rawData, changeInfo.sorterStates, childrenColumnName), changeInfo.filterStates, childrenColumnName),
  154. action
  155. });
  156. };
  157. /**
  158. * Controlled state in `columns` is not a good idea that makes too many code (1000+ line?) to read
  159. * state out and then put it back to title render. Move these code into `hooks` but still too
  160. * complex. We should provides Table props like `sorter` & `filter` to handle control in next big
  161. * version.
  162. */
  163. // ============================ Sorter =============================
  164. const onSorterChange = (sorter, sorterStates) => {
  165. triggerOnChange({
  166. sorter,
  167. sorterStates
  168. }, 'sort', false);
  169. };
  170. const [transformSorterColumns, sortStates, sorterTitleProps, getSorters] = useSorter({
  171. prefixCls,
  172. mergedColumns,
  173. onSorterChange,
  174. sortDirections: sortDirections || ['ascend', 'descend'],
  175. tableLocale,
  176. showSorterTooltip
  177. });
  178. const sortedData = React.useMemo(() => getSortData(rawData, sortStates, childrenColumnName), [rawData, sortStates]);
  179. changeEventInfo.sorter = getSorters();
  180. changeEventInfo.sorterStates = sortStates;
  181. // ============================ Filter ============================
  182. const onFilterChange = (filters, filterStates) => {
  183. triggerOnChange({
  184. filters,
  185. filterStates
  186. }, 'filter', true);
  187. };
  188. const [transformFilterColumns, filterStates, filters] = useFilter({
  189. prefixCls,
  190. locale: tableLocale,
  191. dropdownPrefixCls,
  192. mergedColumns,
  193. onFilterChange,
  194. getPopupContainer: getPopupContainer || getContextPopupContainer,
  195. rootClassName: classNames(rootClassName, rootCls)
  196. });
  197. const mergedData = getFilterData(sortedData, filterStates, childrenColumnName);
  198. changeEventInfo.filters = filters;
  199. changeEventInfo.filterStates = filterStates;
  200. // ============================ Column ============================
  201. const columnTitleProps = React.useMemo(() => {
  202. const mergedFilters = {};
  203. Object.keys(filters).forEach(filterKey => {
  204. if (filters[filterKey] !== null) {
  205. mergedFilters[filterKey] = filters[filterKey];
  206. }
  207. });
  208. return Object.assign(Object.assign({}, sorterTitleProps), {
  209. filters: mergedFilters
  210. });
  211. }, [sorterTitleProps, filters]);
  212. const [transformTitleColumns] = useTitleColumns(columnTitleProps);
  213. // ========================== Pagination ==========================
  214. const onPaginationChange = (current, pageSize) => {
  215. triggerOnChange({
  216. pagination: Object.assign(Object.assign({}, changeEventInfo.pagination), {
  217. current,
  218. pageSize
  219. })
  220. }, 'paginate');
  221. };
  222. const [mergedPagination, resetPagination] = usePagination(mergedData.length, onPaginationChange, pagination);
  223. changeEventInfo.pagination = pagination === false ? {} : getPaginationParam(mergedPagination, pagination);
  224. changeEventInfo.resetPagination = resetPagination;
  225. // ============================= Data =============================
  226. const pageData = React.useMemo(() => {
  227. if (pagination === false || !mergedPagination.pageSize) {
  228. return mergedData;
  229. }
  230. const {
  231. current = 1,
  232. total,
  233. pageSize = DEFAULT_PAGE_SIZE
  234. } = mergedPagination;
  235. process.env.NODE_ENV !== "production" ? warning(current > 0, 'usage', '`current` should be positive number.') : void 0;
  236. // Dynamic table data
  237. if (mergedData.length < total) {
  238. if (mergedData.length > pageSize) {
  239. process.env.NODE_ENV !== "production" ? warning(false, 'usage', '`dataSource` length is less than `pagination.total` but large than `pagination.pageSize`. Please make sure your config correct data with async mode.') : void 0;
  240. return mergedData.slice((current - 1) * pageSize, current * pageSize);
  241. }
  242. return mergedData;
  243. }
  244. return mergedData.slice((current - 1) * pageSize, current * pageSize);
  245. }, [!!pagination, mergedData, mergedPagination === null || mergedPagination === void 0 ? void 0 : mergedPagination.current, mergedPagination === null || mergedPagination === void 0 ? void 0 : mergedPagination.pageSize, mergedPagination === null || mergedPagination === void 0 ? void 0 : mergedPagination.total]);
  246. // ========================== Selections ==========================
  247. const [transformSelectionColumns, selectedKeySet] = useSelection({
  248. prefixCls,
  249. data: mergedData,
  250. pageData,
  251. getRowKey,
  252. getRecordByKey,
  253. expandType,
  254. childrenColumnName,
  255. locale: tableLocale,
  256. getPopupContainer: getPopupContainer || getContextPopupContainer
  257. }, rowSelection);
  258. const internalRowClassName = (record, index, indent) => {
  259. let mergedRowClassName;
  260. if (typeof rowClassName === 'function') {
  261. mergedRowClassName = classNames(rowClassName(record, index, indent));
  262. } else {
  263. mergedRowClassName = classNames(rowClassName);
  264. }
  265. return classNames({
  266. [`${prefixCls}-row-selected`]: selectedKeySet.has(getRowKey(record, index))
  267. }, mergedRowClassName);
  268. };
  269. // ========================== Expandable ==========================
  270. // Pass origin render status into `rc-table`, this can be removed when refactor with `rc-table`
  271. mergedExpandable.__PARENT_RENDER_ICON__ = mergedExpandable.expandIcon;
  272. // Customize expandable icon
  273. mergedExpandable.expandIcon = mergedExpandable.expandIcon || expandIcon || renderExpandIcon(tableLocale);
  274. // Adjust expand icon index, no overwrite expandIconColumnIndex if set.
  275. if (expandType === 'nest' && mergedExpandable.expandIconColumnIndex === undefined) {
  276. mergedExpandable.expandIconColumnIndex = rowSelection ? 1 : 0;
  277. } else if (mergedExpandable.expandIconColumnIndex > 0 && rowSelection) {
  278. mergedExpandable.expandIconColumnIndex -= 1;
  279. }
  280. // Indent size
  281. if (typeof mergedExpandable.indentSize !== 'number') {
  282. mergedExpandable.indentSize = typeof indentSize === 'number' ? indentSize : 15;
  283. }
  284. // ============================ Render ============================
  285. const transformColumns = React.useCallback(innerColumns => transformTitleColumns(transformSelectionColumns(transformFilterColumns(transformSorterColumns(innerColumns)))), [transformSorterColumns, transformFilterColumns, transformSelectionColumns]);
  286. let topPaginationNode;
  287. let bottomPaginationNode;
  288. if (pagination !== false && (mergedPagination === null || mergedPagination === void 0 ? void 0 : mergedPagination.total)) {
  289. let paginationSize;
  290. if (mergedPagination.size) {
  291. paginationSize = mergedPagination.size;
  292. } else {
  293. paginationSize = mergedSize === 'small' || mergedSize === 'middle' ? 'small' : undefined;
  294. }
  295. const renderPagination = position => (/*#__PURE__*/React.createElement(Pagination, Object.assign({}, mergedPagination, {
  296. className: classNames(`${prefixCls}-pagination ${prefixCls}-pagination-${position}`, mergedPagination.className),
  297. size: paginationSize
  298. })));
  299. const defaultPosition = direction === 'rtl' ? 'left' : 'right';
  300. const {
  301. position
  302. } = mergedPagination;
  303. if (position !== null && Array.isArray(position)) {
  304. const topPos = position.find(p => p.includes('top'));
  305. const bottomPos = position.find(p => p.includes('bottom'));
  306. const isDisable = position.every(p => `${p}` === 'none');
  307. if (!topPos && !bottomPos && !isDisable) {
  308. bottomPaginationNode = renderPagination(defaultPosition);
  309. }
  310. if (topPos) {
  311. topPaginationNode = renderPagination(topPos.toLowerCase().replace('top', ''));
  312. }
  313. if (bottomPos) {
  314. bottomPaginationNode = renderPagination(bottomPos.toLowerCase().replace('bottom', ''));
  315. }
  316. } else {
  317. bottomPaginationNode = renderPagination(defaultPosition);
  318. }
  319. }
  320. // >>>>>>>>> Spinning
  321. let spinProps;
  322. if (typeof loading === 'boolean') {
  323. spinProps = {
  324. spinning: loading
  325. };
  326. } else if (typeof loading === 'object') {
  327. spinProps = Object.assign({
  328. spinning: true
  329. }, loading);
  330. }
  331. const wrapperClassNames = classNames(cssVarCls, rootCls, `${prefixCls}-wrapper`, table === null || table === void 0 ? void 0 : table.className, {
  332. [`${prefixCls}-wrapper-rtl`]: direction === 'rtl'
  333. }, className, rootClassName, hashId);
  334. const mergedStyle = Object.assign(Object.assign({}, table === null || table === void 0 ? void 0 : table.style), style);
  335. // ========== empty ==========
  336. const mergedEmptyNode = React.useMemo(() => {
  337. // When dataSource is null/undefined (detected by reference equality with EMPTY_LIST),
  338. // and the table is in a loading state, we only show the loading spinner without the empty placeholder.
  339. // For empty arrays (datasource={[]}), both loading and empty states would normally be shown.
  340. // discussion https://github.com/ant-design/ant-design/issues/54601#issuecomment-3158091383
  341. if ((spinProps === null || spinProps === void 0 ? void 0 : spinProps.spinning) && rawData === EMPTY_LIST) {
  342. return null;
  343. }
  344. if (typeof (locale === null || locale === void 0 ? void 0 : locale.emptyText) !== 'undefined') {
  345. return locale.emptyText;
  346. }
  347. return (renderEmpty === null || renderEmpty === void 0 ? void 0 : renderEmpty('Table')) || /*#__PURE__*/React.createElement(DefaultRenderEmpty, {
  348. componentName: "Table"
  349. });
  350. }, [spinProps === null || spinProps === void 0 ? void 0 : spinProps.spinning, rawData, locale === null || locale === void 0 ? void 0 : locale.emptyText, renderEmpty]);
  351. // ========================== Render ==========================
  352. const TableComponent = virtual ? RcVirtualTable : RcTable;
  353. // >>> Virtual Table props. We set height here since it will affect height collection
  354. const virtualProps = {};
  355. const listItemHeight = React.useMemo(() => {
  356. const {
  357. fontSize,
  358. lineHeight,
  359. lineWidth,
  360. padding,
  361. paddingXS,
  362. paddingSM
  363. } = token;
  364. const fontHeight = Math.floor(fontSize * lineHeight);
  365. switch (mergedSize) {
  366. case 'middle':
  367. return paddingSM * 2 + fontHeight + lineWidth;
  368. case 'small':
  369. return paddingXS * 2 + fontHeight + lineWidth;
  370. default:
  371. return padding * 2 + fontHeight + lineWidth;
  372. }
  373. }, [token, mergedSize]);
  374. if (virtual) {
  375. virtualProps.listItemHeight = listItemHeight;
  376. }
  377. return wrapCSSVar(/*#__PURE__*/React.createElement("div", {
  378. ref: rootRef,
  379. className: wrapperClassNames,
  380. style: mergedStyle
  381. }, /*#__PURE__*/React.createElement(Spin, Object.assign({
  382. spinning: false
  383. }, spinProps), topPaginationNode, /*#__PURE__*/React.createElement(TableComponent, Object.assign({}, virtualProps, tableProps, {
  384. ref: tblRef,
  385. columns: mergedColumns,
  386. direction: direction,
  387. expandable: mergedExpandable,
  388. prefixCls: prefixCls,
  389. className: classNames({
  390. [`${prefixCls}-middle`]: mergedSize === 'middle',
  391. [`${prefixCls}-small`]: mergedSize === 'small',
  392. [`${prefixCls}-bordered`]: bordered,
  393. [`${prefixCls}-empty`]: rawData.length === 0
  394. }, cssVarCls, rootCls, hashId),
  395. data: pageData,
  396. rowKey: getRowKey,
  397. rowClassName: internalRowClassName,
  398. emptyText: mergedEmptyNode,
  399. // Internal
  400. internalHooks: INTERNAL_HOOKS,
  401. internalRefs: internalRefs,
  402. transformColumns: transformColumns,
  403. getContainerWidth: getContainerWidth,
  404. measureRowRender: measureRow => (/*#__PURE__*/React.createElement(ConfigProvider, {
  405. getPopupContainer: node => node
  406. }, measureRow))
  407. })), bottomPaginationNode)));
  408. };
  409. export default /*#__PURE__*/React.forwardRef(InternalTable);