UNPKG

react-virtualized

Version:

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

297 lines (244 loc) 10.1 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; }; }(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } /** * Just-in-time calculates and caches size and position information for a collection of cells. */ var CellSizeAndPositionManager = function () { function CellSizeAndPositionManager(_ref) { var cellCount = _ref.cellCount; var cellSizeGetter = _ref.cellSizeGetter; var estimatedCellSize = _ref.estimatedCellSize; _classCallCheck(this, CellSizeAndPositionManager); this._cellSizeGetter = cellSizeGetter; this._cellCount = cellCount; this._estimatedCellSize = estimatedCellSize; // Cache of size and position data for cells, mapped by cell index. // Note that invalid values may exist in this map so only rely on cells up to this._lastMeasuredIndex this._cellSizeAndPositionData = {}; // Measurements for cells up to this index can be trusted; cells afterward should be estimated. this._lastMeasuredIndex = -1; } _createClass(CellSizeAndPositionManager, [{ key: 'configure', value: function configure(_ref2) { var cellCount = _ref2.cellCount; var estimatedCellSize = _ref2.estimatedCellSize; this._cellCount = cellCount; this._estimatedCellSize = estimatedCellSize; } }, { key: 'getCellCount', value: function getCellCount() { return this._cellCount; } }, { key: 'getEstimatedCellSize', value: function getEstimatedCellSize() { return this._estimatedCellSize; } }, { key: 'getLastMeasuredIndex', value: function getLastMeasuredIndex() { return this._lastMeasuredIndex; } /** * This method returns the size and position for the cell at the specified index. * It just-in-time calculates (or used cached values) for cells leading up to the index. */ }, { key: 'getSizeAndPositionOfCell', value: function getSizeAndPositionOfCell(index) { if (index < 0 || index >= this._cellCount) { throw Error('Requested index ' + index + ' is outside of range 0..' + this._cellCount); } if (index > this._lastMeasuredIndex) { var lastMeasuredCellSizeAndPosition = this.getSizeAndPositionOfLastMeasuredCell(); var _offset = lastMeasuredCellSizeAndPosition.offset + lastMeasuredCellSizeAndPosition.size; for (var i = this._lastMeasuredIndex + 1; i <= index; i++) { var _size = this._cellSizeGetter({ index: i }); if (_size == null || isNaN(_size)) { throw Error('Invalid size returned for cell ' + i + ' of value ' + _size); } this._cellSizeAndPositionData[i] = { offset: _offset, size: _size }; _offset += _size; } this._lastMeasuredIndex = index; } return this._cellSizeAndPositionData[index]; } }, { key: 'getSizeAndPositionOfLastMeasuredCell', value: function getSizeAndPositionOfLastMeasuredCell() { return this._lastMeasuredIndex >= 0 ? this._cellSizeAndPositionData[this._lastMeasuredIndex] : { offset: 0, size: 0 }; } /** * Total size of all cells being measured. * This value will be completedly estimated initially. * As cells as measured the estimate will be updated. */ }, { key: 'getTotalSize', value: function getTotalSize() { var lastMeasuredCellSizeAndPosition = this.getSizeAndPositionOfLastMeasuredCell(); return lastMeasuredCellSizeAndPosition.offset + lastMeasuredCellSizeAndPosition.size + (this._cellCount - this._lastMeasuredIndex - 1) * this._estimatedCellSize; } /** * Determines a new offset that ensures a certain cell is visible, given the current offset. * If the cell is already visible then the current offset will be returned. * If the current offset is too great or small, it will be adjusted just enough to ensure the specified index is visible. * * @param align Desired alignment within container; one of "auto" (default), "start", or "end" * @param containerSize Size (width or height) of the container viewport * @param currentOffset Container's current (x or y) offset * @param totalSize Total size (width or height) of all cells * @return Offset to use to ensure the specified cell is visible */ }, { key: 'getUpdatedOffsetForIndex', value: function getUpdatedOffsetForIndex(_ref3) { var _ref3$align = _ref3.align; var align = _ref3$align === undefined ? 'auto' : _ref3$align; var containerSize = _ref3.containerSize; var currentOffset = _ref3.currentOffset; var targetIndex = _ref3.targetIndex; var datum = this.getSizeAndPositionOfCell(targetIndex); var maxOffset = datum.offset; var minOffset = maxOffset - containerSize + datum.size; var idealOffset = void 0; switch (align) { case 'start': idealOffset = maxOffset; break; case 'end': idealOffset = minOffset; break; case 'center': idealOffset = maxOffset - (containerSize - datum.size) / 2; break; default: idealOffset = Math.max(minOffset, Math.min(maxOffset, currentOffset)); break; } var totalSize = this.getTotalSize(); return Math.max(0, Math.min(totalSize - containerSize, idealOffset)); } }, { key: 'getVisibleCellRange', value: function getVisibleCellRange(_ref4) { var containerSize = _ref4.containerSize; var offset = _ref4.offset; var totalSize = this.getTotalSize(); if (totalSize === 0) { return {}; } var maxOffset = offset + containerSize; var start = this._findNearestCell(offset); var datum = this.getSizeAndPositionOfCell(start); offset = datum.offset + datum.size; var stop = start; while (offset < maxOffset && stop < this._cellCount - 1) { stop++; offset += this.getSizeAndPositionOfCell(stop).size; } return { start: start, stop: stop }; } /** * Clear all cached values for cells after the specified index. * This method should be called for any cell that has changed its size. * It will not immediately perform any calculations; they'll be performed the next time getSizeAndPositionOfCell() is called. */ }, { key: 'resetCell', value: function resetCell(index) { this._lastMeasuredIndex = Math.min(this._lastMeasuredIndex, index - 1); } }, { key: '_binarySearch', value: function _binarySearch(_ref5) { var high = _ref5.high; var low = _ref5.low; var offset = _ref5.offset; var middle = void 0; var currentOffset = void 0; while (low <= high) { middle = low + Math.floor((high - low) / 2); currentOffset = this.getSizeAndPositionOfCell(middle).offset; if (currentOffset === offset) { return middle; } else if (currentOffset < offset) { low = middle + 1; } else if (currentOffset > offset) { high = middle - 1; } } if (low > 0) { return low - 1; } } }, { key: '_exponentialSearch', value: function _exponentialSearch(_ref6) { var index = _ref6.index; var offset = _ref6.offset; var interval = 1; while (index < this._cellCount && this.getSizeAndPositionOfCell(index).offset < offset) { index += interval; interval *= 2; } return this._binarySearch({ high: Math.min(index, this._cellCount - 1), low: Math.floor(index / 2), offset: offset }); } /** * Searches for the cell (index) nearest the specified offset. * * If no exact match is found the next lowest cell index will be returned. * This allows partially visible cells (with offsets just before/above the fold) to be visible. */ }, { key: '_findNearestCell', value: function _findNearestCell(offset) { if (isNaN(offset)) { throw Error('Invalid offset ' + offset + ' specified'); } // Our search algorithms find the nearest match at or below the specified offset. // So make sure the offset is at least 0 or no match will be found. offset = Math.max(0, offset); var lastMeasuredCellSizeAndPosition = this.getSizeAndPositionOfLastMeasuredCell(); var lastMeasuredIndex = Math.max(0, this._lastMeasuredIndex); if (lastMeasuredCellSizeAndPosition.offset >= offset) { // If we've already measured cells within this range just use a binary search as it's faster. return this._binarySearch({ high: lastMeasuredIndex, low: 0, offset: offset }); } else { // If we haven't yet measured this high, fallback to an exponential search with an inner binary search. // The exponential search avoids pre-computing sizes for the full set of cells as a binary search would. // The overall complexity for this approach is O(log n). return this._exponentialSearch({ index: lastMeasuredIndex, offset: offset }); } } }]); return CellSizeAndPositionManager; }(); exports.default = CellSizeAndPositionManager;