2020-09-07 17:48:39 +08:00

324 lines
8.5 KiB
JavaScript

import React from 'react';
import { findDOMNode } from 'react-dom';
import PropTypes from 'prop-types';
import { dom } from '../util';
import VirtualBody from './virtual/body';
import { statics } from './util';
const noop = () => {};
export default function virtual(BaseComponent) {
class VirtualTable extends React.Component {
static VirtualBody = VirtualBody;
static propTypes = {
/**
* 是否开启虚拟滚动
*/
useVirtual: PropTypes.bool,
/**
* 设置行高
*/
rowHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.func]),
maxBodyHeight: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string,
]),
primaryKey: PropTypes.string,
dataSource: PropTypes.array,
/**
* 在内容区域滚动的时候触发的函数
*/
onBodyScroll: PropTypes.func,
...BaseComponent.propTypes,
};
static defaultProps = {
...BaseComponent.defaultProps,
primaryKey: 'id',
rowHeight: noop,
maxBodyHeight: 200,
components: {},
prefix: 'next-',
onBodyScroll: noop,
};
static childContextTypes = {
onVirtualScroll: PropTypes.func,
bodyHeight: PropTypes.number,
innerTop: PropTypes.number,
getBodyNode: PropTypes.func,
getTableInstanceForVirtual: PropTypes.func,
rowSelection: PropTypes.object,
};
state = {
rowHeight: this.props.rowHeight,
scrollToRow: this.props.scrollToRow,
height: this.props.maxBodyHeight,
};
getChildContext() {
return {
onVirtualScroll: this.onScroll,
bodyHeight: this.computeBodyHeight(),
innerTop: this.computeInnerTop(),
getBodyNode: this.getBodyNode,
getTableInstanceForVirtual: this.getTableInstance,
rowSelection: this.rowSelection,
};
}
componentWillMount() {
const { useVirtual, dataSource } = this.props;
this.hasVirtualData =
useVirtual && dataSource && dataSource.length > 0;
}
componentDidMount() {
if (this.hasVirtualData) {
this.lastScrollTop = this.bodyNode.scrollTop;
}
this.adjustScrollTop();
this.adjustSize();
this.reComputeSize();
}
componentWillReceiveProps(nextProps) {
const { useVirtual, dataSource } = nextProps;
this.hasVirtualData =
useVirtual && dataSource && dataSource.length > 0;
if ('maxBodyHeight' in nextProps) {
if (this.state.height !== nextProps.maxBodyHeight) {
this.setState({
height: nextProps.maxBodyHeight,
});
}
}
if ('scrollToRow' in nextProps) {
this.setState({
scrollToRow: nextProps.scrollToRow,
});
}
if (this.state.rowHeight && 'rowHeight' in nextProps) {
const row = this.getRowNode();
const rowClientHeight = row && row.clientHeight;
if (
rowClientHeight &&
rowClientHeight !== this.state.rowHeight
) {
this.setState({
rowHeight: rowClientHeight,
});
}
}
}
componentDidUpdate() {
this.adjustScrollTop();
this.adjustSize();
this.reComputeSize();
}
reComputeSize() {
const { rowHeight } = this.state;
if (typeof rowHeight === 'function' && this.hasVirtualData) {
const row = this.getRowNode();
const rowClientHeight = row && row.clientHeight;
if (rowClientHeight !== this.state.rowHeight) {
this.setState({
rowHeight: rowClientHeight,
});
}
}
}
computeBodyHeight() {
const { rowHeight } = this.state;
const { dataSource } = this.props;
if (typeof rowHeight === 'function') {
return 0;
}
return dataSource.length * rowHeight;
}
computeInnerTop() {
const { rowHeight } = this.state;
if (typeof rowHeight === 'function') {
return 0;
}
return this.start * rowHeight;
}
getVisibleRange(ExpectStart) {
const { height, rowHeight } = this.state;
const len = this.props.dataSource.length;
let end;
let visibleCount = 0;
let start = 0;
if (typeof rowHeight === 'function') {
// try get cell height;
end = 1;
} else {
visibleCount = parseInt(dom.getPixels(height) / rowHeight, 10);
if (typeof ExpectStart === 'number') {
start = ExpectStart < len ? ExpectStart : 0;
}
end = Math.min(+start + 1 + visibleCount + 10, len);
}
this.end = end;
this.visibleCount = visibleCount;
return {
start,
end,
};
}
adjustScrollTop() {
if (this.hasVirtualData) {
this.bodyNode.scrollTop =
(this.lastScrollTop % this.state.rowHeight) +
this.state.rowHeight * this.state.scrollToRow;
}
}
adjustSize() {
if (this.hasVirtualData) {
const body = this.bodyNode;
const virtualScrollNode = body.querySelector('div');
const { clientHeight, clientWidth } = body;
const { tableInc } = this;
const tableNode = findDOMNode(tableInc);
const { prefix } = this.props;
const headerNode = tableNode.querySelector(
`.${prefix}table-header table`,
);
const headerClientWidth = headerNode && headerNode.clientWidth;
if (clientWidth < headerClientWidth) {
dom.setStyle(
virtualScrollNode,
'min-width',
headerClientWidth,
);
const leftNode = this.bodyLeftNode;
const rightNode = this.bodyRightNode;
leftNode &&
dom.setStyle(leftNode, 'max-height', clientHeight);
rightNode &&
dom.setStyle(rightNode, 'max-height', clientHeight);
this.hasScrollbar = true;
} else {
dom.setStyle(virtualScrollNode, 'min-width', 'auto');
this.hasScrollbar = false;
}
}
}
onScroll = () => {
// 避免横向滚动带来的性能问题
const { scrollTop } = this.bodyNode;
if (scrollTop === this.lastScrollTop) {
return;
}
const start = this.computeScrollToRow(scrollTop);
if (!('scrollToRow' in this.props)) {
this.setState({
scrollToRow: start,
});
}
this.props.onBodyScroll(start);
this.lastScrollTop = scrollTop;
};
computeScrollToRow(offset) {
const { rowHeight } = this.state;
const start = parseInt(offset / rowHeight);
this.start = start;
return start;
}
getBodyNode = (node, lockType) => {
lockType = lockType
? lockType.charAt(0).toUpperCase() + lockType.substr(1)
: '';
this[`body${lockType}Node`] = node;
};
getTableInstance = (type, instance) => {
type = type ? type.charAt(0).toUpperCase() + type.substr(1) : '';
this[`table${type}Inc`] = instance;
};
getRowNode() {
try {
// in case of finding an unmounted component due to cached data
// need to clear refs of this.tableInc when dataSource Changed
// use try catch for temporary
return findDOMNode(this.tableInc.getRowRef(0));
} catch (error) {
return null;
}
}
render() {
/* eslint-disable no-unused-vars, prefer-const */
let {
useVirtual,
components,
dataSource,
fixedHeader,
rowHeight,
scrollToRow,
onBodyScroll,
...others
} = this.props;
const entireDataSource = dataSource;
let newDataSource = dataSource;
this.rowSelection = this.props.rowSelection;
if (this.hasVirtualData) {
newDataSource = [];
components = { ...components };
const { start, end } = this.getVisibleRange(
this.state.scrollToRow,
);
dataSource.forEach((current, index, record) => {
if (index >= start && index < end) {
current.__rowIndex = index;
newDataSource.push(current);
}
});
if (!components.Body) {
components.Body = VirtualBody;
}
fixedHeader = true;
}
return (
<BaseComponent
{...others}
dataSource={newDataSource}
entireDataSource={entireDataSource}
components={components}
fixedHeader={fixedHeader}
/>
);
}
}
statics(VirtualTable, BaseComponent);
return VirtualTable;
}