react-virtualized
Version:
React components for efficiently rendering large, scrollable lists and tabular data
274 lines (268 loc) • 12.2 kB
JavaScript
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _typeof = require("@babel/runtime/helpers/typeof");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
exports.forceUpdateReactVirtualizedComponent = forceUpdateReactVirtualizedComponent;
exports.isRangeVisible = isRangeVisible;
exports.scanForUnloadedRanges = scanForUnloadedRanges;
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn"));
var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf"));
var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var React = _interopRequireWildcard(require("react"));
var _propTypes = _interopRequireDefault(require("prop-types"));
var _createCallbackMemoizer = _interopRequireDefault(require("../utils/createCallbackMemoizer"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != _typeof(e) && "function" != typeof e) return { "default": e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n["default"] = e, t && t.set(e, n), n; }
function _callSuper(t, o, e) { return o = (0, _getPrototypeOf2["default"])(o), (0, _possibleConstructorReturn2["default"])(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], (0, _getPrototypeOf2["default"])(t).constructor) : o.apply(t, e)); }
function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); }
/**
* 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 = exports["default"] = /*#__PURE__*/function (_React$PureComponent) {
function InfiniteLoader(props, context) {
var _this;
(0, _classCallCheck2["default"])(this, InfiniteLoader);
_this = _callSuper(this, InfiniteLoader, [props, context]);
_this._loadMoreRowsMemoizer = (0, _createCallbackMemoizer["default"])();
_this._onRowsRendered = _this._onRowsRendered.bind(_this);
_this._registerChild = _this._registerChild.bind(_this);
return _this;
}
(0, _inherits2["default"])(InfiniteLoader, _React$PureComponent);
return (0, _createClass2["default"])(InfiniteLoader, [{
key: "resetLoadMoreRowsCache",
value: function resetLoadMoreRowsCache(autoReload) {
this._loadMoreRowsMemoizer = (0, _createCallbackMemoizer["default"])();
if (autoReload) {
this._doStuff(this._lastRenderedStartIndex, this._lastRenderedStopIndex);
}
}
}, {
key: "render",
value: function render() {
var children = this.props.children;
return children({
onRowsRendered: this._onRowsRendered,
registerChild: this._registerChild
});
}
}, {
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, _this2._lastRenderedStartIndex);
}
}
});
}
});
}
}, {
key: "_onRowsRendered",
value: function _onRowsRendered(_ref) {
var startIndex = _ref.startIndex,
stopIndex = _ref.stopIndex;
this._lastRenderedStartIndex = startIndex;
this._lastRenderedStopIndex = stopIndex;
this._doStuff(startIndex, stopIndex);
}
}, {
key: "_doStuff",
value: function _doStuff(startIndex, stopIndex) {
var _ref2,
_this3 = this;
var _this$props = this.props,
isRowLoaded = _this$props.isRowLoaded,
minimumBatchSize = _this$props.minimumBatchSize,
rowCount = _this$props.rowCount,
threshold = _this$props.threshold;
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 = (_ref2 = []).concat.apply(_ref2, (0, _toConsumableArray2["default"])(unloadedRanges.map(function (_ref3) {
var startIndex = _ref3.startIndex,
stopIndex = _ref3.stopIndex;
return [startIndex, stopIndex];
})));
this._loadMoreRowsMemoizer({
callback: function callback() {
_this3._loadUnloadedRanges(unloadedRanges);
},
indices: {
squashedUnloadedRanges: squashedUnloadedRanges
}
});
}
}, {
key: "_registerChild",
value: function _registerChild(registeredChild) {
this._registeredChild = registeredChild;
}
}]);
}(React.PureComponent);
/**
* Determines if the specified start/stop range is visible based on the most recently rendered range.
*/
(0, _defineProperty2["default"])(InfiniteLoader, "defaultProps", {
minimumBatchSize: 10,
rowCount: 0,
threshold: 15
});
InfiniteLoader.propTypes = process.env.NODE_ENV !== "production" ? {
/**
* Function responsible 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: _propTypes["default"].func.isRequired,
/**
* Function responsible for tracking the loaded state of each row.
* It should implement the following signature: ({ index: number }): boolean
*/
isRowLoaded: _propTypes["default"].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: _propTypes["default"].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: _propTypes["default"].number.isRequired,
/**
* Number of rows in list; can be arbitrary high number if actual number is unknown.
*/
rowCount: _propTypes["default"].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: _propTypes["default"].number.isRequired
} : {};
function isRangeVisible(_ref4) {
var lastRenderedStartIndex = _ref4.lastRenderedStartIndex,
lastRenderedStopIndex = _ref4.lastRenderedStopIndex,
startIndex = _ref4.startIndex,
stopIndex = _ref4.stopIndex;
return !(startIndex > lastRenderedStopIndex || stopIndex < lastRenderedStartIndex);
}
/**
* Returns all of the ranges within a larger range that contain unloaded rows.
*/
function scanForUnloadedRanges(_ref5) {
var isRowLoaded = _ref5.isRowLoaded,
minimumBatchSize = _ref5.minimumBatchSize,
rowCount = _ref5.rowCount,
startIndex = _ref5.startIndex,
stopIndex = _ref5.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.
*
* Additionally, while a Grid is scrolling the cells can be cached,
* So it's important to invalidate that cache by recalculating sizes
* before forcing a rerender.
*/
function forceUpdateReactVirtualizedComponent(component) {
var currentIndex = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
var recomputeSize = typeof component.recomputeGridSize === 'function' ? component.recomputeGridSize : component.recomputeRowHeights;
if (recomputeSize) {
recomputeSize.call(component, currentIndex);
} else {
component.forceUpdate();
}
}
;