UNPKG

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
"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;