react-virtualized
Version:
React components for efficiently rendering large, scrollable lists and tabular data
296 lines (288 loc) • 11.8 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
/*:: import type {Alignment, CellSizeGetter, VisibleCellRange} from '../types';*/
/*:: type CellSizeAndPositionManagerParams = {
cellCount: number,
cellSizeGetter: CellSizeGetter,
estimatedCellSize: number,
};*/
/*:: type ConfigureParams = {
cellCount: number,
estimatedCellSize: number,
cellSizeGetter: CellSizeGetter,
};*/
/*:: type GetUpdatedOffsetForIndex = {
align: Alignment,
containerSize: number,
currentOffset: number,
targetIndex: number,
};*/
/*:: type GetVisibleCellRangeParams = {
containerSize: number,
offset: number,
};*/
/*:: type SizeAndPositionData = {
offset: number,
size: number,
};*/
/**
* Just-in-time calculates and caches size and position information for a collection of cells.
*/
var CellSizeAndPositionManager = exports["default"] = /*#__PURE__*/function () {
function CellSizeAndPositionManager(_ref /*:: */) {
var cellCount = _ref /*:: */.cellCount,
cellSizeGetter = _ref /*:: */.cellSizeGetter,
estimatedCellSize = _ref /*:: */.estimatedCellSize;
(0, _classCallCheck2["default"])(this, CellSizeAndPositionManager);
// 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
(0, _defineProperty2["default"])(this, "_cellSizeAndPositionData", {});
// Measurements for cells up to this index can be trusted; cells afterward should be estimated.
(0, _defineProperty2["default"])(this, "_lastMeasuredIndex", -1);
// Used in deferred mode to track which cells have been queued for measurement.
(0, _defineProperty2["default"])(this, "_lastBatchedIndex", -1);
(0, _defineProperty2["default"])(this, "_cellCount", void 0);
(0, _defineProperty2["default"])(this, "_cellSizeGetter", void 0);
(0, _defineProperty2["default"])(this, "_estimatedCellSize", void 0);
this._cellSizeGetter = cellSizeGetter;
this._cellCount = cellCount;
this._estimatedCellSize = estimatedCellSize;
}
return (0, _createClass2["default"])(CellSizeAndPositionManager, [{
key: "areOffsetsAdjusted",
value: function areOffsetsAdjusted() {
return false;
}
}, {
key: "configure",
value: function configure(_ref2 /*:: */) {
var cellCount = _ref2 /*:: */.cellCount,
estimatedCellSize = _ref2 /*:: */.estimatedCellSize,
cellSizeGetter = _ref2 /*:: */.cellSizeGetter;
this._cellCount = cellCount;
this._estimatedCellSize = estimatedCellSize;
this._cellSizeGetter = cellSizeGetter;
}
}, {
key: "getCellCount",
value: function getCellCount() /*: number*/{
return this._cellCount;
}
}, {
key: "getEstimatedCellSize",
value: function getEstimatedCellSize() /*: number*/{
return this._estimatedCellSize;
}
}, {
key: "getLastMeasuredIndex",
value: function getLastMeasuredIndex() /*: number*/{
return this._lastMeasuredIndex;
}
}, {
key: "getOffsetAdjustment",
value: function getOffsetAdjustment() {
return 0;
}
/**
* 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 /*: number*/) /*: SizeAndPositionData*/{
if (index < 0 || index >= this._cellCount) {
throw Error("Requested index ".concat(index, " is outside of range 0..").concat(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
});
// undefined or NaN probably means a logic error in the size getter.
// null means we're using CellMeasurer and haven't yet measured a given index.
if (size === undefined || isNaN(size)) {
throw Error("Invalid size returned for cell ".concat(i, " of value ").concat(size));
} else if (size === null) {
this._cellSizeAndPositionData[i] = {
offset: offset,
size: 0
};
this._lastBatchedIndex = index;
} else {
this._cellSizeAndPositionData[i] = {
offset: offset,
size: size
};
offset += size;
this._lastMeasuredIndex = index;
}
}
}
return this._cellSizeAndPositionData[index];
}
}, {
key: "getSizeAndPositionOfLastMeasuredCell",
value: function getSizeAndPositionOfLastMeasuredCell() /*: SizeAndPositionData*/{
return this._lastMeasuredIndex >= 0 ? this._cellSizeAndPositionData[this._lastMeasuredIndex] : {
offset: 0,
size: 0
};
}
/**
* Total size of all cells being measured.
* This value will be completely estimated initially.
* As cells are measured, the estimate will be updated.
*/
}, {
key: "getTotalSize",
value: function getTotalSize() /*: number*/{
var lastMeasuredCellSizeAndPosition = this.getSizeAndPositionOfLastMeasuredCell();
var totalSizeOfMeasuredCells = lastMeasuredCellSizeAndPosition.offset + lastMeasuredCellSizeAndPosition.size;
var numUnmeasuredCells = this._cellCount - this._lastMeasuredIndex - 1;
var totalSizeOfUnmeasuredCells = numUnmeasuredCells * this._estimatedCellSize;
return totalSizeOfMeasuredCells + totalSizeOfUnmeasuredCells;
}
/**
* 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 /*:: */) /*: number*/{
var _ref3$align = _ref3 /*:: */.align,
align = _ref3$align === void 0 ? 'auto' : _ref3$align,
containerSize = _ref3 /*:: */.containerSize,
currentOffset = _ref3 /*:: */.currentOffset,
targetIndex = _ref3 /*:: */.targetIndex;
if (containerSize <= 0) {
return 0;
}
var datum = this.getSizeAndPositionOfCell(targetIndex);
var maxOffset = datum.offset;
var minOffset = maxOffset - containerSize + datum.size;
var idealOffset;
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(params /*: GetVisibleCellRangeParams*/) /*: VisibleCellRange*/{
var containerSize = params.containerSize,
offset = params.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 /*: number*/) /*: void*/{
this._lastMeasuredIndex = Math.min(this._lastMeasuredIndex, index - 1);
}
}, {
key: "_binarySearch",
value: function _binarySearch(high /*: number*/, low /*: number*/, offset /*: number*/) /*: number*/{
while (low <= high) {
var middle = low + Math.floor((high - low) / 2);
var 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;
} else {
return 0;
}
}
}, {
key: "_exponentialSearch",
value: function _exponentialSearch(index /*: number*/, offset /*: number*/) /*: number*/{
var interval = 1;
while (index < this._cellCount && this.getSizeAndPositionOfCell(index).offset < offset) {
index += interval;
interval *= 2;
}
return this._binarySearch(Math.min(index, this._cellCount - 1), Math.floor(index / 2), 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 /*: number*/) /*: number*/{
if (isNaN(offset)) {
throw Error("Invalid offset ".concat(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(lastMeasuredIndex, 0, 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(lastMeasuredIndex, offset);
}
}
}]);
}();