@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
JavaScript
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
}));
});