UNPKG

react-virtualized

Version:

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

289 lines (242 loc) 10.4 kB
/** * 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); } } }