antd-table-infinity
Version:
An infinite scroll component based on antd table that supports virtual scrolling & high-performance form http://keruyun.com/
435 lines (365 loc) • 16.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.InfinityTable = exports.default = void 0;
var _react = _interopRequireWildcard(require("react"));
var _reactDom = _interopRequireDefault(require("react-dom"));
var _propTypes = require("prop-types");
var _antd = require("antd");
var _lodash = _interopRequireDefault(require("lodash.throttle"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } }
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
const noop = () => {};
const computeState = (_ref, _ref2) => {
let pageSize = _ref.pageSize;
let direction = _ref2.direction,
scrollTop = _ref2.scrollTop,
scrollHeight = _ref2.scrollHeight,
tableHeight = _ref2.tableHeight,
visibleHeight = _ref2.visibleHeight,
size = _ref2.size;
if (scrollHeight === 0) {
return {
startIndex: 0,
underHeight: 0,
upperHeight: 0
};
}
const rowHeight = tableHeight / pageSize;
const visibleRowCount = Math.round(visibleHeight / rowHeight);
let startIndex = Math.round(scrollTop / scrollHeight * size);
if (direction === 'up') {
startIndex -= pageSize - visibleRowCount;
startIndex = startIndex < 0 ? 0 : startIndex;
} else {
startIndex = startIndex + pageSize > size ? size - pageSize : startIndex;
}
const underHeight = Math.round((size - (startIndex + pageSize)) * rowHeight);
startIndex = startIndex > 0 ? startIndex : 0;
const upperHeight = Math.round(startIndex * rowHeight);
return {
visibleRowCount,
startIndex,
underHeight,
upperHeight
};
};
class InfinityTable extends _react.PureComponent {
constructor() {
super(...arguments);
_defineProperty(this, "state", {
size: 0,
// 缓存的数据集大小
scrollTop: 0,
// 滚动条位置
scrollHeight: 0,
// 滚动总高度
tableHeight: 0,
// antd 表格高度(DOM上的数据集高度)
visibleHeight: 0,
// 可视区域高度
visibleRowCount: 0,
// 可视区域行数
startIndex: 0,
// DOM上的数据集起始索引
underHeight: 0,
// 下撑高
upperHeight: 0,
// 上撑高
isPropsChange: false // 是否是props改变
});
_defineProperty(this, "refUpperPlaceholder", null);
_defineProperty(this, "refUnderPlaceholder", null);
_defineProperty(this, "refScroll", null);
_defineProperty(this, "refTable", null);
_defineProperty(this, "refFooter", null);
_defineProperty(this, "ioTargetState", {
refUnderPlaceholder: null,
refUpperPlaceholder: null,
refTable: null
});
_defineProperty(this, "updateTable", () => {
const tableHeight = this.refTable.clientHeight;
const _this$refScroll = this.refScroll,
scrollHeight = _this$refScroll.scrollHeight,
visibleHeight = _this$refScroll.clientHeight;
const isPropsChange = this.state.isPropsChange;
if (!isPropsChange) {
// fix bug: 当直接传入的dataSource数据量很大,无法滚动的问题。常见于不是按每页大小递增的情况,如1000条已有数据不需loading直接虚拟滚动显示的时候
// 如果 isPropsChange === true 则下面的情况是合理的,不能退出state计算,要求重新计算
if ( // fix bug: 可能存在DOM更新时空白的情况,此种无效状态,需要过虑掉, (tableHeight 是很大的值,而this.state.tableHeight只是一个初始小值)
this.state.tableHeight && Math.abs(tableHeight - this.state.tableHeight) > 200 || // 容许正常误差
this.state.scrollHeight && Math.abs(scrollHeight - this.state.scrollHeight) > 200 || this.state.visibleHeight && visibleHeight !== this.state.visibleHeight) {
if (this.props.debug) {
console.log({
visibleHeight,
'this.state.visibleHeight': this.state.visibleHeight
});
console.log({
tableHeight,
'this.state.tableHeight': this.state.tableHeight
});
console.log({
scrollHeight,
'this.state.scrollHeight': this.state.scrollHeight
});
}
return;
}
}
this.setState(_objectSpread({}, computeState(this.props, Object.assign(this.state, {
direction: this.refScroll.scrollTop - this.state.scrollTop < 0 ? 'up' : 'down',
scrollTop: this.refScroll.scrollTop,
visibleHeight,
scrollHeight,
tableHeight
})), {
isPropsChange: false
}) // () => console.log('computeState done', this.state),
);
});
}
static PlaceHolder(_ref3) {
let height = _ref3.height,
domNode = _ref3.domNode,
loading = _ref3.loading,
loadingIndicator = _ref3.loadingIndicator;
return domNode && _reactDom.default.createPortal(_react.default.createElement("div", {
style: {
height: `${height}px`
}
}, loading && loadingIndicator, ' '), domNode);
}
static getDerivedStateFromProps(_ref4, prevState) {
let pageSize = _ref4.pageSize,
length = _ref4.dataSource.length,
loading = _ref4.loading;
const tableHeight = prevState.tableHeight,
prevStateScrollHeight = prevState.scrollHeight,
visibleRowCount = prevState.visibleRowCount,
size = prevState.size;
if (pageSize < visibleRowCount) {
console.warn(`pagesize(${pageSize}) less than visible row count(${visibleRowCount}), maybe you set error!`);
}
const rowHeight = tableHeight / pageSize;
const increase = length - size;
let scrollHeight = Math.round(prevStateScrollHeight + increase * rowHeight);
if (!loading) {
scrollHeight = length * rowHeight;
}
if (pageSize < increase) {
console.warn(`increase(${increase}) greater than pageSize(${pageSize}) that will cause the scroll bar shake, maybe you set error! `);
}
return _objectSpread({}, computeState({
pageSize
}, Object.assign(prevState, {
size: length,
scrollHeight
})), {
isPropsChange: true
});
}
componentDidMount() {
/* eslint-disable */
this.refScroll = _reactDom.default.findDOMNode(this).getElementsByClassName('ant-table-body')[0];
this.refTable = this.refScroll.getElementsByTagName('tbody')[0];
/* eslint-enabled */
this.createUnderPlaceholder();
this.createUpperPlaceholder();
this.setStateWithThrottle = (0, _lodash.default)(this.updateTable, 200);
this.props.onScroll && this.refScroll.addEventListener('scroll', this.props.onScroll);
if (this.refScroll) {
this.io = new IntersectionObserver(changes => {
this.refScroll.removeEventListener('scroll', this.setStateWithThrottle);
if (this.state.scrollTop && Math.abs(this.refScroll.scrollTop - this.state.scrollTop) < 20) {
// fix bug: 如果滚动步长小于20象素不做处理
// console.log('scroll step less than 20px', this.refScroll.scrollTop - this.state.scrollTop);
return;
}
this.ioTargetState = changes.reduce((result, change) => {
const ret = _objectSpread({}, result);
switch (change.target) {
case this.refUnderPlaceholder:
ret.refUnderPlaceholder = change;
break;
case this.refUpperPlaceholder:
ret.refUpperPlaceholder = change;
break;
case this.refTable:
ret.refTable = change;
break;
default:
console.warn('Miss match dom', change);
}
return ret;
}, this.ioTargetState);
let mutation = 'cache scrolling';
const _this$ioTargetState = this.ioTargetState,
refUnderPlaceholder = _this$ioTargetState.refUnderPlaceholder,
refUpperPlaceholder = _this$ioTargetState.refUpperPlaceholder,
refTable = _this$ioTargetState.refTable;
const _this$state = this.state,
startIndex = _this$state.startIndex,
visibleRowCount = _this$state.visibleRowCount,
size = _this$state.size;
const _this$props = this.props,
loading = _this$props.loading,
pageSize = _this$props.pageSize;
if (refUnderPlaceholder.intersectionRatio > 0) {
if (refTable.intersectionRatio > 0 && startIndex + pageSize + visibleRowCount >= size && !loading) {
// 已滚动到最后,加载数据
mutation = 'end';
if (this.props.debug) {
console.log(mutation);
}
return this.setState({
scrollTop: this.refScroll.scrollTop,
scrollHeight: this.refScroll.scrollHeight,
tableHeight: this.refTable.clientHeight
}, this.props.onFetch); // mutation = 'fastEnd'; // 滚动到最后,但数据已经加载完
} else if (refTable.intersectionRatio > 0) {
mutation = 'down'; // 滚动到显示的数据未尾
} else if (refTable.intersectionRatio === 0) {
mutation = 'fastDown'; // 滚动到没有任何数据显示的区域
}
} else if (refUpperPlaceholder.intersectionRatio > 0) {
if (refUpperPlaceholder.intersectionRatio === 1) {
mutation = 'top'; // 滚动到顶了
} else if (refTable.intersectionRatio > 0) {
mutation = 'up'; // 滚动到显示的数据头部
} else if (refTable.intersectionRatio === 0) {
mutation = 'fastUp'; // 滚动到没有任何数据显示的区域
}
} else if (refTable.intersectionRatio === 0) {
if (this.props.debug) {
console.log('Bug: IntersectionObserver miss, because which waiting for Idle trigger');
} // fix bug: 重新触发,获取正确值
this.toggleObserver(false);
this.toggleObserver();
return;
}
if (this.props.debug) {
console.log(mutation);
}
if (mutation.includes('fast')) {
this.refScroll.addEventListener('scroll', this.setStateWithThrottle);
}
this.setStateWithThrottle();
}, {
root: this.refScroll
});
this.toggleObserver();
}
}
componentWillUnmount() {
this.props.onScroll && this.refScroll.removeEventListener('scroll', this.props.onScroll);
this.refScroll.removeEventListener('scroll', this.setStateWithThrottle);
this.io.disconnect();
}
// 创建底部填充块
createUnderPlaceholder() {
const refUnderPlaceholder = document.createElement('div');
refUnderPlaceholder.setAttribute('id', 'refUnderPlaceholder');
this.refScroll.appendChild(refUnderPlaceholder);
this.refUnderPlaceholder = refUnderPlaceholder;
} // 创建顶部填充块
createUpperPlaceholder() {
const refUpperPlaceholder = document.createElement('div');
refUpperPlaceholder.setAttribute('id', 'refUpperPlaceholder');
this.refScroll.insertBefore(refUpperPlaceholder, this.refScroll.firstChild);
this.refUpperPlaceholder = refUpperPlaceholder;
}
toggleObserver() {
let condition = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
if (condition) {
this.io.observe(this.refUpperPlaceholder);
this.io.observe(this.refUnderPlaceholder);
this.io.observe(this.refTable);
} else {
this.io.unobserve(this.refUpperPlaceholder);
this.io.unobserve(this.refUnderPlaceholder);
this.io.unobserve(this.refTable);
}
}
render() {
const _this$props2 = this.props,
dataSource = _this$props2.dataSource,
pageSize = _this$props2.pageSize,
loadingIndicator = _this$props2.loadingIndicator,
forwardedRef = _this$props2.forwardedRef,
loading = _this$props2.loading,
columns = _this$props2.columns,
rest = _objectWithoutProperties(_this$props2, ["dataSource", "pageSize", "loadingIndicator", "forwardedRef", "loading", "columns"]);
const _this$state2 = this.state,
startIndex = _this$state2.startIndex,
upperHeight = _this$state2.upperHeight,
underHeight = _this$state2.underHeight;
return _react.default.createElement(_react.Fragment, null, _react.default.createElement(InfinityTable.PlaceHolder, {
height: upperHeight,
domNode: this.refUpperPlaceholder
}), _react.default.createElement(InfinityTable.PlaceHolder, {
height: underHeight,
domNode: this.refUnderPlaceholder,
loading: loading,
loadingIndicator: loadingIndicator
}), _react.default.createElement(_antd.Table, _extends({
rowKey: record => record.key
}, rest, {
ref: forwardedRef,
columns: columns,
dataSource: dataSource.slice(startIndex, startIndex + pageSize),
pagination: false
})));
}
}
exports.InfinityTable = InfinityTable;
InfinityTable.defaultProps = {
// loading 效果, A visual react component for Loading status
loadingIndicator: _react.default.createElement("div", {
style: {
textAlign: 'center',
paddingTop: 20,
paddingBottom: 20,
border: '1px solid #e8e8e8'
}
}, _react.default.createElement(_antd.Spin, {
tip: "Loading..."
})),
onScroll: null,
// 滚动事件
onFetch: noop,
// 滚动到低部触发Fetch方法
sumData: null,
// 合计行
debug: false,
// display console log for debug
loading: false,
// 是否loading状态
pageSize: 30 // 真实DOM大小,Reality DOM row count
};
InfinityTable.propTypes = {
// loading 效果
loadingIndicator: _propTypes.element,
onScroll: _propTypes.func,
// 滚动事件
onFetch: _propTypes.func,
// 滚动到低部触发Fetch方法
sumData: _propTypes.array,
// 合计行
dataSource: _propTypes.array.isRequired,
columns: _propTypes.array.isRequired,
forwardedRef: _propTypes.object,
debug: _propTypes.bool,
pageSize: _propTypes.number,
loading: _propTypes.bool
};
var _default = _react.default.forwardRef((props, ref) => _react.default.createElement(InfinityTable, _extends({}, props, {
forwardedRef: ref
})));
exports.default = _default;