UNPKG

react-virtualized

Version:

React components for efficiently rendering large, scrollable lists and tabular data

286 lines (233 loc) 10.8 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); exports.isRangeVisible = isRangeVisible; exports.scanForUnloadedRanges = scanForUnloadedRanges; exports.forceUpdateReactVirtualizedComponent = forceUpdateReactVirtualizedComponent; var _react = require('react'); var _reactAddonsShallowCompare = require('react-addons-shallow-compare'); var _reactAddonsShallowCompare2 = _interopRequireDefault(_reactAddonsShallowCompare); var _createCallbackMemoizer = require('../utils/createCallbackMemoizer'); var _createCallbackMemoizer2 = _interopRequireDefault(_createCallbackMemoizer); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /** * Higher-order component that manages lazy-loading for "infinite" data. * This component decorates a virtual component and just-in-time prefetches rows as a user scrolls. * It is intended as a convenience component; fork it if you'd like finer-grained control over data-loading. */ var InfiniteLoader = function (_Component) { _inherits(InfiniteLoader, _Component); function InfiniteLoader(props, context) { _classCallCheck(this, InfiniteLoader); var _this = _possibleConstructorReturn(this, (InfiniteLoader.__proto__ || Object.getPrototypeOf(InfiniteLoader)).call(this, props, context)); _this._loadMoreRowsMemoizer = (0, _createCallbackMemoizer2.default)(); _this._onRowsRendered = _this._onRowsRendered.bind(_this); _this._registerChild = _this._registerChild.bind(_this); return _this; } _createClass(InfiniteLoader, [{ key: 'render', value: function render() { var children = this.props.children; return children({ onRowsRendered: this._onRowsRendered, registerChild: this._registerChild }); } }, { key: 'shouldComponentUpdate', value: function shouldComponentUpdate(nextProps, nextState) { return (0, _reactAddonsShallowCompare2.default)(this, nextProps, nextState); } }, { key: '_loadUnloadedRanges', value: function _loadUnloadedRanges(unloadedRanges) { var _this2 = this; var loadMoreRows = this.props.loadMoreRows; unloadedRanges.forEach(function (unloadedRange) { var promise = loadMoreRows(unloadedRange); if (promise) { promise.then(function () { // Refresh the visible rows if any of them have just been loaded. // Otherwise they will remain in their unloaded visual state. if (isRangeVisible({ lastRenderedStartIndex: _this2._lastRenderedStartIndex, lastRenderedStopIndex: _this2._lastRenderedStopIndex, startIndex: unloadedRange.startIndex, stopIndex: unloadedRange.stopIndex })) { if (_this2._registeredChild) { forceUpdateReactVirtualizedComponent(_this2._registeredChild); } } }); } }); } }, { key: '_onRowsRendered', value: function _onRowsRendered(_ref) { var _this3 = this; var startIndex = _ref.startIndex; var stopIndex = _ref.stopIndex; var _props = this.props; var isRowLoaded = _props.isRowLoaded; var minimumBatchSize = _props.minimumBatchSize; var rowCount = _props.rowCount; var threshold = _props.threshold; this._lastRenderedStartIndex = startIndex; this._lastRenderedStopIndex = stopIndex; var unloadedRanges = scanForUnloadedRanges({ isRowLoaded: isRowLoaded, minimumBatchSize: minimumBatchSize, rowCount: rowCount, startIndex: Math.max(0, startIndex - threshold), stopIndex: Math.min(rowCount - 1, stopIndex + threshold) }); // For memoize comparison var squashedUnloadedRanges = unloadedRanges.reduce(function (reduced, unloadedRange) { return reduced.concat([unloadedRange.startIndex, unloadedRange.stopIndex]); }, []); this._loadMoreRowsMemoizer({ callback: function callback() { _this3._loadUnloadedRanges(unloadedRanges); }, indices: { squashedUnloadedRanges: squashedUnloadedRanges } }); } }, { key: '_registerChild', value: function _registerChild(registeredChild) { this._registeredChild = registeredChild; } }]); return InfiniteLoader; }(_react.Component); /** * Determines if the specified start/stop range is visible based on the most recently rendered range. */ InfiniteLoader.propTypes = { /** * Function respondible for rendering a virtualized component. * This function should implement the following signature: * ({ onRowsRendered, registerChild }) => PropTypes.element * * The specified :onRowsRendered function should be passed through to the child's :onRowsRendered property. * The :registerChild callback should be set as the virtualized component's :ref. */ children: _react.PropTypes.func.isRequired, /** * Function responsible for tracking the loaded state of each row. * It should implement the following signature: ({ index: number }): boolean */ isRowLoaded: _react.PropTypes.func.isRequired, /** * Callback to be invoked when more rows must be loaded. * It should implement the following signature: ({ startIndex, stopIndex }): Promise * The returned Promise should be resolved once row data has finished loading. * It will be used to determine when to refresh the list with the newly-loaded data. * This callback may be called multiple times in reaction to a single scroll event. */ loadMoreRows: _react.PropTypes.func.isRequired, /** * Minimum number of rows to be loaded at a time. * This property can be used to batch requests to reduce HTTP requests. */ minimumBatchSize: _react.PropTypes.number.isRequired, /** * Number of rows in list; can be arbitrary high number if actual number is unknown. */ rowCount: _react.PropTypes.number.isRequired, /** * Threshold at which to pre-fetch data. * A threshold X means that data will start loading when a user scrolls within X rows. * This value defaults to 15. */ threshold: _react.PropTypes.number.isRequired }; InfiniteLoader.defaultProps = { minimumBatchSize: 10, rowCount: 0, threshold: 15 }; exports.default = InfiniteLoader; function isRangeVisible(_ref2) { var lastRenderedStartIndex = _ref2.lastRenderedStartIndex; var lastRenderedStopIndex = _ref2.lastRenderedStopIndex; var startIndex = _ref2.startIndex; var stopIndex = _ref2.stopIndex; return !(startIndex > lastRenderedStopIndex || stopIndex < lastRenderedStartIndex); } /** * Returns all of the ranges within a larger range that contain unloaded rows. */ function scanForUnloadedRanges(_ref3) { var isRowLoaded = _ref3.isRowLoaded; var minimumBatchSize = _ref3.minimumBatchSize; var rowCount = _ref3.rowCount; var startIndex = _ref3.startIndex; var stopIndex = _ref3.stopIndex; var unloadedRanges = []; var rangeStartIndex = null; var rangeStopIndex = null; for (var index = startIndex; index <= stopIndex; index++) { var loaded = isRowLoaded({ index: index }); if (!loaded) { rangeStopIndex = index; if (rangeStartIndex === null) { rangeStartIndex = index; } } else if (rangeStopIndex !== null) { unloadedRanges.push({ startIndex: rangeStartIndex, stopIndex: rangeStopIndex }); rangeStartIndex = rangeStopIndex = null; } } // If :rangeStopIndex is not null it means we haven't ran out of unloaded rows. // Scan forward to try filling our :minimumBatchSize. if (rangeStopIndex !== null) { var potentialStopIndex = Math.min(Math.max(rangeStopIndex, rangeStartIndex + minimumBatchSize - 1), rowCount - 1); for (var _index = rangeStopIndex + 1; _index <= potentialStopIndex; _index++) { if (!isRowLoaded({ index: _index })) { rangeStopIndex = _index; } else { break; } } unloadedRanges.push({ startIndex: rangeStartIndex, stopIndex: rangeStopIndex }); } // Check to see if our first range ended prematurely. // In this case we should scan backwards to try filling our :minimumBatchSize. if (unloadedRanges.length) { var firstUnloadedRange = unloadedRanges[0]; while (firstUnloadedRange.stopIndex - firstUnloadedRange.startIndex + 1 < minimumBatchSize && firstUnloadedRange.startIndex > 0) { var _index2 = firstUnloadedRange.startIndex - 1; if (!isRowLoaded({ index: _index2 })) { firstUnloadedRange.startIndex = _index2; } else { break; } } } return unloadedRanges; } /** * Since RV components use shallowCompare we need to force a render (even though props haven't changed). * However InfiniteLoader may wrap a Grid or it may wrap a Table or List. * In the first case the built-in React forceUpdate() method is sufficient to force a re-render, * But in the latter cases we need to use the RV-specific forceUpdateGrid() method. * Else the inner Grid will not be re-rendered and visuals may be stale. */ function forceUpdateReactVirtualizedComponent(component) { typeof component.forceUpdateGrid === 'function' ? component.forceUpdateGrid() : component.forceUpdate(); }