UNPKG

@douyinfe/semi-ui

Version:

A modern, comprehensive, flexible design system and UI library. Connect DesignOps & DevOps. Quickly build beautiful React apps. Maintained by Douyin-fe team.

743 lines 25.8 kB
import _isFunction from "lodash/isFunction"; import _isNull from "lodash/isNull"; import _pick from "lodash/pick"; import _isEqual from "lodash/isEqual"; import _each from "lodash/each"; import _isMap from "lodash/isMap"; import _size from "lodash/size"; import _get from "lodash/get"; var __rest = this && this.__rest || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { VariableSizeList as List } from 'react-window'; import { arrayAdd, getRecordKey, isExpanded, isSelected, isDisabled, getRecord, genExpandedRowKey, getDefaultVirtualizedRowConfig, isTreeTable } from '@douyinfe/semi-foundation/lib/es/table/utils'; import BodyFoundation from '@douyinfe/semi-foundation/lib/es/table/bodyFoundation'; import { strings } from '@douyinfe/semi-foundation/lib/es/table/constants'; import BaseComponent from '../../_base/baseComponent'; import { logger } from '../utils'; import ColGroup from '../ColGroup'; import BaseRow, { baseRowPropTypes } from './BaseRow'; import ExpandedRow from './ExpandedRow'; import SectionRow, { sectionRowPropTypes } from './SectionRow'; import TableHeader from '../TableHeader'; import TableContext from '../table-context'; class Body extends BaseComponent { constructor(props, context) { var _this; super(props); _this = this; this.forwardRef = node => { const { forwardedRef } = this.props; this.ref.current = node; this.foundation.observeBodyResize(node); if (typeof forwardedRef === 'function') { forwardedRef(node); } else if (forwardedRef && typeof forwardedRef === 'object') { forwardedRef.current = node; } }; this.setListRef = listInstance => { this.listRef.current = listInstance; const { getVirtualizedListRef } = this.context; if (getVirtualizedListRef) { if (this.props.virtualized) { getVirtualizedListRef(this.listRef); } else { console.warn('getVirtualizedListRef only works with virtualized. ' + 'See https://semi.design/en-US/show/table for more information.'); } } }; this.itemSize = index => { const { virtualized, size: tableSize } = this.props; const { virtualizedData } = this.state; const virtualizedItem = _get(virtualizedData, index); const defaultConfig = getDefaultVirtualizedRowConfig(tableSize, virtualizedItem.sectionRow); const itemSize = _get(virtualized, 'itemSize', defaultConfig.height); let realSize = itemSize; if (typeof itemSize === 'function') { realSize = itemSize(index, { expandedRow: _get(virtualizedItem, 'expandedRow', false), sectionRow: _get(virtualizedItem, 'sectionRow', false) }); } if (realSize < defaultConfig.minHeight) { logger.warn(`The computed real \`itemSize\` cannot be less than ${defaultConfig.minHeight}`); } return realSize; }; this.itemKey = (index, data) => _get(data, [index, 'key'], index); this.handleRowClick = (rowKey, e, expand) => { const { handleRowExpanded } = this.context; handleRowExpanded(!expand, rowKey, e); }; this.handleVirtualizedScroll = function () { let props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; const onScroll = _get(_this.props.virtualized, 'onScroll'); if (typeof onScroll === 'function') { onScroll(props); } }; /** * @param {MouseEvent<HTMLDivElement>} e */ this.handleVirtualizedBodyScroll = e => { const { handleBodyScroll } = this.props; const newScrollLeft = _get(e, 'nativeEvent.target.scrollLeft'); const newScrollTop = _get(e, 'nativeEvent.target.scrollTop'); if (newScrollTop === this.state.cache.virtualizedScrollTop) { this.handleVirtualizedScroll({ horizontalScrolling: true }); } this.state.cache.virtualizedScrollLeft = newScrollLeft; this.state.cache.virtualizedScrollTop = newScrollTop; if (typeof handleBodyScroll === 'function') { handleBodyScroll(e); } }; this.getVirtualizedRowWidth = () => { const { getCellWidths } = this.context; const { columns } = this.props; const cellWidths = getCellWidths(columns); const rowWidth = arrayAdd(cellWidths, 0, _size(columns)); return rowWidth; }; this.renderVirtualizedRow = options => { const { index, style } = options; const { virtualizedData, cachedExpandBtnShouldInRow } = this.state; const { flattenedColumns } = this.context; const virtualizedItem = _get(virtualizedData, [index], {}); const { key, parentKeys, expandedRow, sectionRow } = virtualizedItem, rest = __rest(virtualizedItem, ["key", "parentKeys", "expandedRow", "sectionRow"]); const rowWidth = this.getVirtualizedRowWidth(); const expandBtnShouldInRow = cachedExpandBtnShouldInRow; const props = Object.assign(Object.assign(Object.assign(Object.assign({}, this.props), { style: Object.assign(Object.assign({}, style), { width: rowWidth }) }), rest), { columns: flattenedColumns, index, expandBtnShouldInRow }); return sectionRow ? this.renderSectionRow(props) : expandedRow ? this.renderExpandedRow(props) : this.renderBaseRow(props); }; // virtualized List innerElementType this.renderTbody = /*#__PURE__*/React.forwardRef(function () { let props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; let ref = arguments.length > 1 ? arguments[1] : undefined; return /*#__PURE__*/React.createElement("div", Object.assign({}, props, { onScroll: function () { if (props.onScroll) { props.onScroll(...arguments); } }, // eslint-disable-next-line react/no-this-in-sfc,react/destructuring-assignment className: classnames(props.className, `${_this.props.prefixCls}-tbody`), style: Object.assign({}, props.style), ref: ref })); }); // virtualized List outerElementType this.renderOuter = /*#__PURE__*/React.forwardRef((props, ref) => { const { children } = props, rest = __rest(props, ["children"]); const { handleWheel, prefixCls, emptySlot, dataSource } = this.props; const tableWidth = this.getVirtualizedRowWidth(); const tableCls = classnames(`${prefixCls}`, `${prefixCls}-fixed`); return /*#__PURE__*/React.createElement("div", Object.assign({}, rest, { ref: ref, onWheel: function () { if (handleWheel) { handleWheel(...arguments); } if (rest.onWheel) { rest.onWheel(...arguments); } }, onScroll: function () { _this.handleVirtualizedBodyScroll(...arguments); if (rest.onScroll) { rest.onScroll(...arguments); } } }), /*#__PURE__*/React.createElement("div", { style: { width: tableWidth }, className: tableCls }, children), _size(dataSource) === 0 && emptySlot); }); this.onItemsRendered = props => { if (this.state.cache.virtualizedScrollLeft && this.ref.current) { this.ref.current.scrollLeft = this.state.cache.virtualizedScrollLeft; } }; this.renderVirtualizedBody = direction => { const { scroll, prefixCls, virtualized, columns } = this.props; const { virtualizedData } = this.state; const { getCellWidths } = this.context; const cellWidths = getCellWidths(columns); if (!_size(cellWidths)) { return null; } const rawY = _get(scroll, 'y'); const yIsNumber = typeof rawY === 'number'; const y = yIsNumber ? rawY : 600; if (!yIsNumber) { logger.warn('You have to specific "scroll.y" which must be a number for table virtualization!'); } const listStyle = { width: '100%', height: (virtualizedData === null || virtualizedData === void 0 ? void 0 : virtualizedData.length) ? y : null, overflowX: 'auto', overflowY: 'auto' }; const wrapCls = classnames(`${prefixCls}-body`); return /*#__PURE__*/React.createElement(List, Object.assign({}, typeof virtualized === 'object' ? virtualized : {}, { initialScrollOffset: this.state.cache.virtualizedScrollTop, onScroll: this.handleVirtualizedScroll, onItemsRendered: this.onItemsRendered, ref: this.setListRef, className: wrapCls, outerRef: this.forwardRef, height: (virtualizedData === null || virtualizedData === void 0 ? void 0 : virtualizedData.length) ? y : 0, width: listStyle.width, itemData: virtualizedData, itemSize: this.itemSize, itemCount: virtualizedData.length, itemKey: this.itemKey, innerElementType: this.renderTbody, outerElementType: this.renderOuter, style: Object.assign(Object.assign({}, listStyle), { direction }), direction: direction }), this.renderVirtualizedRow); }; /** * render group title * @param {*} props */ this.renderSectionRow = function () { let props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { groupKey: undefined }; const { dataSource, rowKey, group, groupKey, index } = props; const sectionRowPickKeys = Object.keys(sectionRowPropTypes); const sectionRowProps = _pick(props, sectionRowPickKeys); const { handleRowExpanded } = _this.context; return /*#__PURE__*/React.createElement(SectionRow, Object.assign({}, sectionRowProps, { record: { groupKey, records: [...group].map(recordKey => getRecord(dataSource, recordKey, rowKey)) }, index: index, onExpand: handleRowExpanded, data: dataSource, key: groupKey || index })); }; this.renderExpandedRow = function () { let props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { renderExpandIcon: () => null }; const { style, components, renderExpandIcon, expandedRowRender, record, columns, expanded, index, rowKey, virtualized, displayNone } = props; let key = getRecordKey(record, rowKey); if (key == null) { key = index; } const { flattenedColumns, getCellWidths } = _this.context; // we use memoized cellWidths to avoid re-render expanded row (fix #686) if (flattenedColumns !== _this.flattenedColumns) { _this.flattenedColumns = flattenedColumns; _this.cellWidths = getCellWidths(flattenedColumns); } return /*#__PURE__*/React.createElement(ExpandedRow, { style: style, components: components, renderExpandIcon: renderExpandIcon, expandedRowRender: expandedRowRender, record: record, columns: columns, expanded: expanded, index: index, virtualized: virtualized, key: genExpandedRowKey(key), cellWidths: _this.cellWidths, displayNone: displayNone }); }; /** * render grouped rows * @returns {ReactNode[]} renderedRows */ this.renderGroupedRows = () => { const { groups, dataSource: data, rowKey, expandedRowKeys, keepDOM } = this.props; const { flattenedColumns } = this.context; const groupsInData = new Map(); const renderedRows = []; if (groups != null && Array.isArray(data) && data.length) { data.forEach(record => { const recordKey = getRecordKey(record, rowKey); groups.forEach((group, key) => { if (group.has(recordKey)) { if (!groupsInData.has(key)) { groupsInData.set(key, new Set([])); } groupsInData.get(key).add(recordKey); return false; } return undefined; }); }); } let index = -1; groupsInData.forEach((group, groupKey) => { // Calculate the expanded state of the group const expanded = isExpanded(expandedRowKeys, groupKey); // Render the title of the group renderedRows.push(this.renderSectionRow(Object.assign(Object.assign({}, this.props), { columns: flattenedColumns, index: ++index, group, groupKey, expanded }))); // Render the grouped content when the group is expanded if (expanded || keepDOM) { const dataInGroup = []; group.forEach(recordKey => { const record = getRecord(data, recordKey, rowKey); if (record != null) { dataInGroup.push(record); } }); /** * Render the contents of the group row */ renderedRows.push(this.renderBodyRows(dataInGroup, undefined, [], !expanded)); } }); return renderedRows; }; this.renderBody = direction => { const { scroll, prefixCls, columns, components, fixed, handleWheel, headerRef, handleBodyScroll, anyColumnFixed, showHeader, emptySlot, includeHeader, dataSource, onScroll, groups, expandedRowRender, tableLayout } = this.props; const x = _get(scroll, 'x'); const y = _get(scroll, 'y'); const bodyStyle = {}; const tableStyle = {}; const Table = _get(components, 'body.outer', 'table'); const BodyWrapper = _get(components, 'body.wrapper') || 'tbody'; if (y) { bodyStyle.maxHeight = y; } if (x) { tableStyle.width = x; } if (anyColumnFixed && _size(dataSource)) { // Auto is better than scroll. For example, when there is only scrollY, the scroll axis is not displayed horizontally. bodyStyle.overflow = 'auto'; // Fix weird webkit render bug bodyStyle.WebkitTransform = 'translate3d (0, 0, 0)'; } const colgroup = /*#__PURE__*/React.createElement(ColGroup, { components: _get(components, 'body'), columns: columns, prefixCls: prefixCls }); // const tableBody = this.renderBody(); const wrapCls = `${prefixCls}-body`; const baseTable = /*#__PURE__*/React.createElement("div", { key: "bodyTable", className: wrapCls, style: bodyStyle, ref: this.forwardRef, onWheel: handleWheel, onScroll: handleBodyScroll }, /*#__PURE__*/React.createElement(Table, { role: _isMap(groups) || _isFunction(expandedRowRender) || isTreeTable({ dataSource }) ? 'treegrid' : 'grid', "aria-rowcount": dataSource && dataSource.length, "aria-colcount": columns && columns.length, style: tableStyle, className: classnames(prefixCls, { [`${prefixCls}-fixed`]: tableLayout === 'fixed' }) }, colgroup, includeHeader && showHeader ? (/*#__PURE__*/React.createElement(TableHeader, Object.assign({}, this.props, { ref: headerRef, components: components, columns: columns }))) : null, /*#__PURE__*/React.createElement(BodyWrapper, { className: `${prefixCls}-tbody`, onScroll: onScroll }, _isMap(groups) ? this.renderGroupedRows() : this.renderBodyRows(dataSource))), emptySlot); if (fixed && columns.length) { return /*#__PURE__*/React.createElement("div", { key: "bodyTable", className: `${prefixCls}-body-outer` }, baseTable); } return baseTable; }; this.ref = /*#__PURE__*/React.createRef(); this.state = { virtualizedData: [], cache: { virtualizedScrollTop: null, virtualizedScrollLeft: null }, cachedExpandBtnShouldInRow: null, cachedExpandRelatedProps: [] }; this.listRef = /*#__PURE__*/React.createRef(); const { flattenedColumns, getCellWidths } = context; this.foundation = new BodyFoundation(this.adapter); this.flattenedColumns = flattenedColumns; this.cellWidths = getCellWidths(flattenedColumns); this.observer = null; } get adapter() { return Object.assign(Object.assign({}, super.adapter), { setVirtualizedData: (virtualizedData, cb) => this.setState({ virtualizedData }, cb), setCachedExpandBtnShouldInRow: cachedExpandBtnShouldInRow => this.setState({ cachedExpandBtnShouldInRow }), setCachedExpandRelatedProps: cachedExpandRelatedProps => this.setState({ cachedExpandRelatedProps }), observeBodyResize: bodyWrapDOM => { const { setBodyHasScrollbar } = this.context; // Callback when the size of the body dom content changes, notifying Table.jsx whether the bodyHasScrollBar exists const resizeCallback = () => { const update = () => { const { offsetWidth, clientWidth } = bodyWrapDOM; const bodyHasScrollBar = clientWidth < offsetWidth; setBodyHasScrollbar(bodyHasScrollBar); }; const requestAnimationFrame = window.requestAnimationFrame || window.setTimeout; requestAnimationFrame(update); }; // Monitor body dom resize if (bodyWrapDOM) { if (_get(window, 'ResizeObserver')) { if (this.observer) { this.observer.unobserve(bodyWrapDOM); this.observer = null; } this.observer = new ResizeObserver(resizeCallback); this.observer.observe(bodyWrapDOM); } else { logger.warn('The current browser does not support ResizeObserver,' + 'and the table may be misaligned after plugging and unplugging the mouse and keyboard.' + 'You can try to refresh it.'); } } }, unobserveBodyResize: () => { const bodyWrapDOM = this.ref.current; if (this.observer) { this.observer.unobserve(bodyWrapDOM); this.observer = null; } } }); } componentDidUpdate(prevProps, prevState) { const { virtualized, dataSource, expandedRowKeys, columns, scroll } = this.props; if (virtualized) { if (prevProps.dataSource !== dataSource || prevProps.expandedRowKeys !== expandedRowKeys || prevProps.columns !== columns) { this.foundation.initVirtualizedData(); } } const expandRelatedProps = strings.EXPAND_RELATED_PROPS; const newExpandRelatedProps = expandRelatedProps.map(key => _get(this.props, key, undefined)); if (!_isEqual(newExpandRelatedProps, prevState.cachedExpandRelatedProps)) { this.foundation.initExpandBtnShouldInRow(newExpandRelatedProps); } const scrollY = _get(scroll, 'y'); const bodyWrapDOM = this.ref.current; if (scrollY && scrollY !== _get(prevProps, 'scroll.y')) { this.foundation.observeBodyResize(bodyWrapDOM); } } /** * render base row * @param {*} props * @returns */ renderBaseRow() { let props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; const { rowKey, columns, expandedRowKeys, rowExpandable, record, index, level, expandBtnShouldInRow, // effect the display of the indent span selectedRowKeysSet, disabledRowKeysSet, expandRowByClick } = props; const baseRowPickKeys = Object.keys(baseRowPropTypes); const baseRowProps = _pick(props, baseRowPickKeys); let key = getRecordKey(record, rowKey); if (key == null) { key = index; } const expanded = isExpanded(expandedRowKeys, key); const expandable = rowExpandable && rowExpandable(record); const expandableProps = { level: undefined, expanded }; if (expandable || expandBtnShouldInRow) { expandableProps.level = level; expandableProps.expandableRow = expandable; if (expandRowByClick) { expandableProps.onRowClick = this.handleRowClick; } } const selectionProps = { selected: isSelected(selectedRowKeysSet, key), disabled: isDisabled(disabledRowKeysSet, key) }; const { getCellWidths } = this.context; const cellWidths = getCellWidths(columns, null, true); return /*#__PURE__*/React.createElement(BaseRow, Object.assign({}, baseRowProps, expandableProps, selectionProps, { key: key, rowKey: key, cellWidths: cellWidths })); } renderBodyRows() { let data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; let level = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; let renderedRows = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : []; let displayNone = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false; const { rowKey, expandedRowRender, expandedRowKeys, childrenRecordName, rowExpandable, keepDOM } = this.props; const hasExpandedRowRender = typeof expandedRowRender === 'function'; const expandBtnShouldInRow = this.state.cachedExpandBtnShouldInRow; const { flattenedColumns } = this.context; _each(data, (record, index) => { let key = getRecordKey(record, rowKey); if (key == null) { key = index; } const recordChildren = _get(record, childrenRecordName); const recordHasChildren = Boolean(Array.isArray(recordChildren) && recordChildren.length); renderedRows.push(this.renderBaseRow(Object.assign(Object.assign({}, this.props), { columns: flattenedColumns, expandBtnShouldInRow, displayNone, record, key, level, index }))); // render expand row const expanded = isExpanded(expandedRowKeys, key); const shouldRenderExpandedRows = expanded || keepDOM; if (hasExpandedRowRender && rowExpandable && rowExpandable(record) && shouldRenderExpandedRows) { const currentExpandRow = this.renderExpandedRow(Object.assign(Object.assign({}, this.props), { columns: flattenedColumns, level, index, record, expanded, displayNone: displayNone || !expanded })); /** * If expandedRowRender returns falsy, this expanded row will not be rendered * Render an empty div before v1.19.7 */ if (!_isNull(currentExpandRow)) { renderedRows.push(currentExpandRow); } } // render tree data if (recordHasChildren && shouldRenderExpandedRows) { const nestedRows = this.renderBodyRows(recordChildren, level + 1, [], displayNone || !expanded); renderedRows.push(...nestedRows); } }); return renderedRows; } render() { const { virtualized } = this.props; const { direction } = this.context; return virtualized ? this.renderVirtualizedBody(direction) : this.renderBody(direction); } } Body.contextType = TableContext; Body.propTypes = { anyColumnFixed: PropTypes.bool, childrenRecordName: PropTypes.string, columns: PropTypes.array, components: PropTypes.object, dataSource: PropTypes.array, disabledRowKeysSet: PropTypes.instanceOf(Set).isRequired, emptySlot: PropTypes.node, expandRowByClick: PropTypes.bool, expandedRowKeys: PropTypes.array, expandedRowRender: PropTypes.func, fixed: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), forwardedRef: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), groups: PropTypes.instanceOf(Map), handleBodyScroll: PropTypes.func, handleWheel: PropTypes.func, headerRef: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), includeHeader: PropTypes.bool, onScroll: PropTypes.func, prefixCls: PropTypes.string, renderExpandIcon: PropTypes.func, rowExpandable: PropTypes.func, rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.bool, PropTypes.func]), scroll: PropTypes.object, selectedRowKeysSet: PropTypes.instanceOf(Set).isRequired, showHeader: PropTypes.bool, size: PropTypes.string, store: PropTypes.object, virtualized: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]) }; export default /*#__PURE__*/React.forwardRef(function TableBody(props, ref) { return /*#__PURE__*/React.createElement(Body, Object.assign({}, props, { forwardedRef: ref })); });