react-virtualized
Version:
React components for efficiently rendering large, scrollable lists and tabular data
289 lines (242 loc) • 10.4 kB
JavaScript
/**
* Helper method that determines when to recalculate row or column metadata.
*
* @param cellsCount Number of rows or columns in the current axis
* @param cellsSize Width or height of cells for the current axis
* @param computeMetadataCallback Method to invoke if cell metadata should be recalculated
* @param computeMetadataCallbackProps Parameters to pass to :computeMetadataCallback
* @param computeMetadataOnNextUpdate Flag specifying that metadata should be recalculated
* @param nextCellsCount Newly updated number of rows or columns in the current axis
* @param nextCellsSize Newly updated width or height of cells for the current axis
* @param nextScrollToIndex Newly updated scroll-to-index
* @param scrollToIndex Scroll-to-index
* @param updateScrollOffsetForScrollToIndex Callback to invoke if the scroll position should be recalculated
*/
export function computeCellMetadataAndUpdateScrollOffsetHelper(_ref) {
var cellsCount = _ref.cellsCount;
var cellSize = _ref.cellSize;
var computeMetadataCallback = _ref.computeMetadataCallback;
var computeMetadataCallbackProps = _ref.computeMetadataCallbackProps;
var computeMetadataOnNextUpdate = _ref.computeMetadataOnNextUpdate;
var nextCellsCount = _ref.nextCellsCount;
var nextCellSize = _ref.nextCellSize;
var nextScrollToIndex = _ref.nextScrollToIndex;
var scrollToIndex = _ref.scrollToIndex;
var updateScrollOffsetForScrollToIndex = _ref.updateScrollOffsetForScrollToIndex;
// Don't compare cell sizes if they are functions because inline functions would cause infinite loops.
// In that event users should use the manual recompute methods to inform of changes.
if (computeMetadataOnNextUpdate || cellsCount !== nextCellsCount || (typeof cellSize === 'number' || typeof nextCellSize === 'number') && cellSize !== nextCellSize) {
computeMetadataCallback(computeMetadataCallbackProps);
// Updated cell metadata may have hidden the previous scrolled-to item.
// In this case we should also update the scrollTop to ensure it stays visible.
if (scrollToIndex >= 0 && scrollToIndex === nextScrollToIndex) {
updateScrollOffsetForScrollToIndex();
}
}
}
/**
* Helper utility that updates the specified callback whenever any of the specified indices have changed.
*/
export function createCallbackMemoizer() {
var requireAllKeys = arguments.length <= 0 || arguments[0] === undefined ? true : arguments[0];
var cachedIndices = {};
return function (_ref2) {
var callback = _ref2.callback;
var indices = _ref2.indices;
var keys = Object.keys(indices);
var allInitialized = !requireAllKeys || keys.every(function (key) {
return indices[key] >= 0;
});
var indexChanged = keys.some(function (key) {
return cachedIndices[key] !== indices[key];
});
cachedIndices = indices;
if (allInitialized && indexChanged) {
callback(indices);
}
};
}
/**
* Binary search function inspired by react-infinite.
*/
export function findNearestCell(_ref3) {
var cellMetadata = _ref3.cellMetadata;
var mode = _ref3.mode;
var offset = _ref3.offset;
var high = cellMetadata.length - 1;
var low = 0;
var middle = undefined;
var currentOffset = undefined;
// TODO Add better guards here against NaN offset
while (low <= high) {
middle = low + Math.floor((high - low) / 2);
currentOffset = cellMetadata[middle].offset;
if (currentOffset === offset) {
return middle;
} else if (currentOffset < offset) {
low = middle + 1;
} else if (currentOffset > offset) {
high = middle - 1;
}
}
if (mode === findNearestCell.EQUAL_OR_LOWER && low > 0) {
return low - 1;
} else if (mode === findNearestCell.EQUAL_OR_HIGHER && high < cellMetadata.length - 1) {
return high + 1;
}
}
findNearestCell.EQUAL_OR_LOWER = 1;
findNearestCell.EQUAL_OR_HIGHER = 2;
export function getOverscanIndices(_ref4) {
var cellsCount = _ref4.cellsCount;
var overscanCellsCount = _ref4.overscanCellsCount;
var startIndex = _ref4.startIndex;
var stopIndex = _ref4.stopIndex;
return {
overscanStartIndex: Math.max(0, startIndex - overscanCellsCount),
overscanStopIndex: Math.min(cellsCount - 1, stopIndex + overscanCellsCount)
};
}
/**
* 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 cellMetadata Metadata initially computed by initCellMetadata()
* @param containerSize Total size (width or height) of the container
* @param currentOffset Container's current (x or y) offset
* @param targetIndex Index of target cell
* @return Offset to use to ensure the specified cell is visible
*/
export function getUpdatedOffsetForIndex(_ref5) {
var cellMetadata = _ref5.cellMetadata;
var containerSize = _ref5.containerSize;
var currentOffset = _ref5.currentOffset;
var targetIndex = _ref5.targetIndex;
if (cellMetadata.length === 0) {
return 0;
}
targetIndex = Math.max(0, Math.min(cellMetadata.length - 1, targetIndex));
var datum = cellMetadata[targetIndex];
var maxOffset = datum.offset;
var minOffset = maxOffset - containerSize + datum.size;
var newOffset = Math.max(minOffset, Math.min(maxOffset, currentOffset));
return newOffset;
}
/**
* Determines the range of cells to display for a given offset in order to fill the specified container.
*
* @param cellsCount Total number of cells.
* @param cellMetadata Metadata initially computed by initCellMetadata()
* @param containerSize Total size (width or height) of the container
* @param currentOffset Container's current (x or y) offset
* @return An object containing :start and :stop attributes, each specifying a cell index
*/
export function getVisibleCellIndices(_ref6) {
var cellsCount = _ref6.cellsCount;
var cellMetadata = _ref6.cellMetadata;
var containerSize = _ref6.containerSize;
var currentOffset = _ref6.currentOffset;
if (cellsCount === 0) {
return {};
}
// TODO Add better guards here against NaN offset
var lastDatum = cellMetadata[cellMetadata.length - 1];
var totalCellSize = lastDatum.offset + lastDatum.size;
// Ensure offset is within reasonable bounds
currentOffset = Math.max(0, Math.min(totalCellSize - containerSize, currentOffset));
var maxOffset = Math.min(totalCellSize, currentOffset + containerSize);
var start = findNearestCell({
cellMetadata: cellMetadata,
mode: findNearestCell.EQUAL_OR_LOWER,
offset: currentOffset
});
var datum = cellMetadata[start];
currentOffset = datum.offset + datum.size;
var stop = start;
while (currentOffset < maxOffset && stop < cellsCount - 1) {
stop++;
currentOffset += cellMetadata[stop].size;
}
return {
start: start,
stop: stop
};
}
/**
* Initializes metadata for an axis and its cells.
* This data is used to determine which cells are visible given a container size and scroll position.
*
* @param cellsCount Total number of cells.
* @param size Either a fixed size or a function that returns the size for a given given an index.
* @return Object mapping cell index to cell metadata (size, offset)
*/
export function initCellMetadata(_ref7) {
var cellsCount = _ref7.cellsCount;
var size = _ref7.size;
var sizeGetter = size instanceof Function ? size : function (index) {
return size;
};
var cellMetadata = [];
var offset = 0;
for (var i = 0; i < cellsCount; i++) {
var _size = sizeGetter(i);
if (_size == null || isNaN(_size)) {
throw Error('Invalid size returned for cell ' + i + ' of value ' + _size);
}
cellMetadata[i] = {
size: _size,
offset: offset
};
offset += _size;
}
return cellMetadata;
}
/**
* Helper function that determines when to update scroll offsets to ensure that a scroll-to-index remains visible.
*
* @param cellMetadata Metadata initially computed by initCellMetadata()
* @param cellsCount Number of rows or columns in the current axis
* @param cellsSize Width or height of cells for the current axis
* @param previousCellsCount Previous number of rows or columns
* @param previousCellsSize Previous width or height of cells
* @param previousScrollToIndex Previous scroll-to-index
* @param previousSize Previous width or height of the virtualized container
* @param scrollOffset Current scrollLeft or scrollTop
* @param scrollToIndex Scroll-to-index
* @param size Width or height of the virtualized container
* @param updateScrollIndexCallback Callback to invoke with an optional scroll-to-index override
*/
export function updateScrollIndexHelper(_ref8) {
var cellMetadata = _ref8.cellMetadata;
var cellsCount = _ref8.cellsCount;
var cellSize = _ref8.cellSize;
var previousCellsCount = _ref8.previousCellsCount;
var previousCellSize = _ref8.previousCellSize;
var previousScrollToIndex = _ref8.previousScrollToIndex;
var previousSize = _ref8.previousSize;
var scrollOffset = _ref8.scrollOffset;
var scrollToIndex = _ref8.scrollToIndex;
var size = _ref8.size;
var updateScrollIndexCallback = _ref8.updateScrollIndexCallback;
var hasScrollToIndex = scrollToIndex >= 0 && scrollToIndex < cellsCount;
var sizeHasChanged = size !== previousSize || !previousCellSize || typeof cellSize === 'number' && cellSize !== previousCellSize;
// If we have a new scroll target OR if height/row-height has changed,
// We should ensure that the scroll target is visible.
if (hasScrollToIndex && (sizeHasChanged || scrollToIndex !== previousScrollToIndex)) {
updateScrollIndexCallback();
// If we don't have a selected item but list size or number of children have decreased,
// Make sure we aren't scrolled too far past the current content.
} else if (!hasScrollToIndex && (size < previousSize || cellsCount < previousCellsCount)) {
var calculatedScrollOffset = getUpdatedOffsetForIndex({
cellMetadata: cellMetadata,
containerSize: size,
currentOffset: scrollOffset,
targetIndex: cellsCount - 1
});
// Only adjust the scroll position if we've scrolled below the last set of rows.
if (calculatedScrollOffset < scrollOffset) {
updateScrollIndexCallback(cellsCount - 1);
}
}
}