'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.DEFAULT_SCROLLING_RESET_TIME_INTERVAL = undefined; var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 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; }; }(); var _react = require('react'); var _react2 = _interopRequireDefault(_react); var _propTypes = require('prop-types'); var _propTypes2 = _interopRequireDefault(_propTypes); var _classnames = require('classnames'); var _classnames2 = _interopRequireDefault(_classnames); var _calculateSizeAndPositionDataAndUpdateScrollOffset = require('./utils/calculateSizeAndPositionDataAndUpdateScrollOffset'); var _calculateSizeAndPositionDataAndUpdateScrollOffset2 = _interopRequireDefault(_calculateSizeAndPositionDataAndUpdateScrollOffset); var _ScalingCellSizeAndPositionManager = require('./utils/ScalingCellSizeAndPositionManager'); var _ScalingCellSizeAndPositionManager2 = _interopRequireDefault(_ScalingCellSizeAndPositionManager); var _createCallbackMemoizer = require('../utils/createCallbackMemoizer'); var _createCallbackMemoizer2 = _interopRequireDefault(_createCallbackMemoizer); var _defaultOverscanIndicesGetter = require('./defaultOverscanIndicesGetter'); var _defaultOverscanIndicesGetter2 = _interopRequireDefault(_defaultOverscanIndicesGetter); var _updateScrollIndexHelper = require('./utils/updateScrollIndexHelper'); var _updateScrollIndexHelper2 = _interopRequireDefault(_updateScrollIndexHelper); var _defaultCellRangeRenderer = require('./defaultCellRangeRenderer'); var _defaultCellRangeRenderer2 = _interopRequireDefault(_defaultCellRangeRenderer); var _scrollbarSize = require('dom-helpers/util/scrollbarSize'); var _scrollbarSize2 = _interopRequireDefault(_scrollbarSize); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /** * Specifies the number of miliseconds during which to disable pointer events while a scroll is in progress. * This improves performance and makes scrolling smoother. */ var DEFAULT_SCROLLING_RESET_TIME_INTERVAL = exports.DEFAULT_SCROLLING_RESET_TIME_INTERVAL = 150; /** * Controls whether the Grid updates the DOM element's scrollLeft/scrollTop based on the current state or just observes it. * This prevents Grid from interrupting mouse-wheel animations (see issue #2). */ var SCROLL_POSITION_CHANGE_REASONS = { OBSERVED: 'observed', REQUESTED: 'requested' }; /** * Renders tabular data with virtualization along the vertical and horizontal axes. * Row heights and column widths must be known ahead of time and specified as properties. */ var Grid = function (_PureComponent) { _inherits(Grid, _PureComponent); function Grid(props, context) { _classCallCheck(this, Grid); var _this = _possibleConstructorReturn(this, (Grid.__proto__ || Object.getPrototypeOf(Grid)).call(this, props, context)); _this.state = { isScrolling: false, scrollDirectionHorizontal: _defaultOverscanIndicesGetter.SCROLL_DIRECTION_FORWARD, scrollDirectionVertical: _defaultOverscanIndicesGetter.SCROLL_DIRECTION_FORWARD, scrollLeft: 0, scrollTop: 0 }; // Invokes onSectionRendered callback only when start/stop row or column indices change _this._onGridRenderedMemoizer = (0, _createCallbackMemoizer2.default)(); _this._onScrollMemoizer = (0, _createCallbackMemoizer2.default)(false); // Bind functions to instance so they don't lose context when passed around _this._debounceScrollEndedCallback = _this._debounceScrollEndedCallback.bind(_this); _this._invokeOnGridRenderedHelper = _this._invokeOnGridRenderedHelper.bind(_this); _this._onScroll = _this._onScroll.bind(_this); _this._setScrollingContainerRef = _this._setScrollingContainerRef.bind(_this); _this._columnWidthGetter = _this._wrapSizeGetter(props.columnWidth); _this._rowHeightGetter = _this._wrapSizeGetter(props.rowHeight); _this._deferredInvalidateColumnIndex = null; _this._deferredInvalidateRowIndex = null; _this._recomputeScrollLeftFlag = false; _this._recomputeScrollTopFlag = false; var deferredMeasurementCache = props.deferredMeasurementCache; var deferredMode = typeof deferredMeasurementCache !== 'undefined'; _this._columnSizeAndPositionManager = new _ScalingCellSizeAndPositionManager2.default({ batchAllCells: deferredMode && !deferredMeasurementCache.hasFixedHeight(), cellCount: props.columnCount, cellSizeGetter: function cellSizeGetter(params) { return _this._columnWidthGetter(params); }, estimatedCellSize: _this._getEstimatedColumnSize(props) }); _this._rowSizeAndPositionManager = new _ScalingCellSizeAndPositionManager2.default({ batchAllCells: deferredMode && !deferredMeasurementCache.hasFixedWidth(), cellCount: props.rowCount, cellSizeGetter: function cellSizeGetter(params) { return _this._rowHeightGetter(params); }, estimatedCellSize: _this._getEstimatedRowSize(props) }); // See defaultCellRangeRenderer() for more information on the usage of these caches _this._cellCache = {}; _this._styleCache = {}; return _this; } /** * Gets offsets for a given cell and alignment. */ _createClass(Grid, [{ key: 'getOffsetForCell', value: function getOffsetForCell() { var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, _ref$alignment = _ref.alignment, alignment = _ref$alignment === undefined ? this.props.scrollToAlignment : _ref$alignment, _ref$columnIndex = _ref.columnIndex, columnIndex = _ref$columnIndex === undefined ? this.props.scrollToColumn : _ref$columnIndex, _ref$rowIndex = _ref.rowIndex, rowIndex = _ref$rowIndex === undefined ? this.props.scrollToRow : _ref$rowIndex; var offsetProps = _extends({}, this.props, { scrollToAlignment: alignment, scrollToColumn: columnIndex, scrollToRow: rowIndex }); return { scrollLeft: this._getCalculatedScrollLeft(offsetProps), scrollTop: this._getCalculatedScrollTop(offsetProps) }; } /** * Invalidate Grid size and recompute visible cells. * This is a deferred wrapper for recomputeGridSize(). * It sets a flag to be evaluated on cDM/cDU to avoid unnecessary renders. * This method is intended for advanced use-cases like CellMeasurer. */ // @TODO (bvaughn) Add automated test coverage for this. }, { key: 'invalidateCellSizeAfterRender', value: function invalidateCellSizeAfterRender(_ref2) { var columnIndex = _ref2.columnIndex, rowIndex = _ref2.rowIndex; this._deferredInvalidateColumnIndex = typeof this._deferredInvalidateColumnIndex === 'number' ? Math.min(this._deferredInvalidateColumnIndex, columnIndex) : columnIndex; this._deferredInvalidateRowIndex = typeof this._deferredInvalidateRowIndex === 'number' ? Math.min(this._deferredInvalidateRowIndex, rowIndex) : rowIndex; } /** * Pre-measure all columns and rows in a Grid. * Typically cells are only measured as needed and estimated sizes are used for cells that have not yet been measured. * This method ensures that the next call to getTotalSize() returns an exact size (as opposed to just an estimated one). */ }, { key: 'measureAllCells', value: function measureAllCells() { var _props = this.props, columnCount = _props.columnCount, rowCount = _props.rowCount; this._columnSizeAndPositionManager.getSizeAndPositionOfCell(columnCount - 1); this._rowSizeAndPositionManager.getSizeAndPositionOfCell(rowCount - 1); } /** * Forced recompute of row heights and column widths. * This function should be called if dynamic column or row sizes have changed but nothing else has. * Since Grid only receives :columnCount and :rowCount it has no way of detecting when the underlying data changes. */ }, { key: 'recomputeGridSize', value: function recomputeGridSize() { var _ref3 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, _ref3$columnIndex = _ref3.columnIndex, columnIndex = _ref3$columnIndex === undefined ? 0 : _ref3$columnIndex, _ref3$rowIndex = _ref3.rowIndex, rowIndex = _ref3$rowIndex === undefined ? 0 : _ref3$rowIndex; var _props2 = this.props, scrollToColumn = _props2.scrollToColumn, scrollToRow = _props2.scrollToRow; this._columnSizeAndPositionManager.resetCell(columnIndex); this._rowSizeAndPositionManager.resetCell(rowIndex); // Cell sizes may be determined by a function property. // In this case the cDU handler can't know if they changed. // Store this flag to let the next cDU pass know it needs to recompute the scroll offset. this._recomputeScrollLeftFlag = scrollToColumn >= 0 && columnIndex <= scrollToColumn; this._recomputeScrollTopFlag = scrollToRow >= 0 && rowIndex <= scrollToRow; // Clear cell cache in case we are scrolling; // Invalid row heights likely mean invalid cached content as well. this._cellCache = {}; this._styleCache = {}; this.forceUpdate(); } /** * Ensure column and row are visible. */ }, { key: 'scrollToCell', value: function scrollToCell(_ref4) { var columnIndex = _ref4.columnIndex, rowIndex = _ref4.rowIndex; var columnCount = this.props.columnCount; var props = this.props; // Don't adjust scroll offset for single-column grids (eg List, Table). // This can cause a funky scroll offset because of the vertical scrollbar width. if (columnCount > 1) { this._updateScrollLeftForScrollToColumn(_extends({}, props, { scrollToColumn: columnIndex })); } this._updateScrollTopForScrollToRow(_extends({}, props, { scrollToRow: rowIndex })); } /** * Scroll to the specified offset(s). * Useful for animating position changes. */ }, { key: 'scrollToPosition', value: function scrollToPosition() { var _ref5 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, scrollLeft = _ref5.scrollLeft, scrollTop = _ref5.scrollTop; this._setScrollPosition({ scrollLeft: scrollLeft, scrollTop: scrollTop }); } }, { key: 'componentDidMount', value: function componentDidMount() { var _props3 = this.props, getScrollbarSize = _props3.getScrollbarSize, scrollLeft = _props3.scrollLeft, scrollToColumn = _props3.scrollToColumn, scrollTop = _props3.scrollTop, scrollToRow = _props3.scrollToRow; // If cell sizes have been invalidated (eg we are using CellMeasurer) then reset cached positions. // We must do this at the start of the method as we may calculate and update scroll position below. this._handleInvalidatedGridSize(); // If this component was first rendered server-side, scrollbar size will be undefined. // In that event we need to remeasure. if (!this._scrollbarSizeMeasured) { this._scrollbarSize = getScrollbarSize(); this._scrollbarSizeMeasured = true; this.setState({}); } if (scrollLeft >= 0 || scrollTop >= 0) { this._setScrollPosition({ scrollLeft: scrollLeft, scrollTop: scrollTop }); } if (scrollToColumn >= 0 || scrollToRow >= 0) { this._updateScrollLeftForScrollToColumn(); this._updateScrollTopForScrollToRow(); } // Update onRowsRendered callback this._invokeOnGridRenderedHelper(); // Initialize onScroll callback this._invokeOnScrollMemoizer({ scrollLeft: scrollLeft || 0, scrollTop: scrollTop || 0, totalColumnsWidth: this._columnSizeAndPositionManager.getTotalSize(), totalRowsHeight: this._rowSizeAndPositionManager.getTotalSize() }); } /** * @private * This method updates scrollLeft/scrollTop in state for the following conditions: * 1) New scroll-to-cell props have been set */ }, { key: 'componentDidUpdate', value: function componentDidUpdate(prevProps, prevState) { var _this2 = this; var _props4 = this.props, autoHeight = _props4.autoHeight, autoWidth = _props4.autoWidth, columnCount = _props4.columnCount, height = _props4.height, rowCount = _props4.rowCount, scrollToAlignment = _props4.scrollToAlignment, scrollToColumn = _props4.scrollToColumn, scrollToRow = _props4.scrollToRow, width = _props4.width; var _state = this.state, scrollLeft = _state.scrollLeft, scrollPositionChangeReason = _state.scrollPositionChangeReason, scrollTop = _state.scrollTop; // If cell sizes have been invalidated (eg we are using CellMeasurer) then reset cached positions. // We must do this at the start of the method as we may calculate and update scroll position below. this._handleInvalidatedGridSize(); // Handle edge case where column or row count has only just increased over 0. // In this case we may have to restore a previously-specified scroll offset. // For more info see bvaughn/react-virtualized/issues/218 var columnOrRowCountJustIncreasedFromZero = columnCount > 0 && prevProps.columnCount === 0 || rowCount > 0 && prevProps.rowCount === 0; // Make sure requested changes to :scrollLeft or :scrollTop get applied. // Assigning to scrollLeft/scrollTop tells the browser to interrupt any running scroll animations, // And to discard any pending async changes to the scroll position that may have happened in the meantime (e.g. on a separate scrolling thread). // So we only set these when we require an adjustment of the scroll position. // See issue #2 for more information. if (scrollPositionChangeReason === SCROLL_POSITION_CHANGE_REASONS.REQUESTED) { // @TRICKY :autoHeight and :autoWidth properties instructs Grid to leave :scrollTop and :scrollLeft management to an external HOC (eg WindowScroller). // In this case we should avoid checking scrollingContainer.scrollTop and scrollingContainer.scrollLeft since it forces layout/flow. if (!autoWidth && scrollLeft >= 0 && (scrollLeft !== prevState.scrollLeft && scrollLeft !== this._scrollingContainer.scrollLeft || columnOrRowCountJustIncreasedFromZero)) { this._scrollingContainer.scrollLeft = scrollLeft; } if (!autoHeight && scrollTop >= 0 && (scrollTop !== prevState.scrollTop && scrollTop !== this._scrollingContainer.scrollTop || columnOrRowCountJustIncreasedFromZero)) { this._scrollingContainer.scrollTop = scrollTop; } } // Update scroll offsets if the current :scrollToColumn or :scrollToRow values requires it // @TODO Do we also need this check or can the one in componentWillUpdate() suffice? if (this._recomputeScrollLeftFlag) { this._recomputeScrollLeftFlag = false; this._updateScrollLeftForScrollToColumn(this.props); } else { (0, _updateScrollIndexHelper2.default)({ cellSizeAndPositionManager: this._columnSizeAndPositionManager, previousCellsCount: prevProps.columnCount, previousCellSize: prevProps.columnWidth, previousScrollToAlignment: prevProps.scrollToAlignment, previousScrollToIndex: prevProps.scrollToColumn, previousSize: prevProps.width, scrollOffset: scrollLeft, scrollToAlignment: scrollToAlignment, scrollToIndex: scrollToColumn, size: width, updateScrollIndexCallback: function updateScrollIndexCallback(scrollToColumn) { return _this2._updateScrollLeftForScrollToColumn(_this2.props); } }); } if (this._recomputeScrollTopFlag) { this._recomputeScrollTopFlag = false; this._updateScrollTopForScrollToRow(this.props); } else { (0, _updateScrollIndexHelper2.default)({ cellSizeAndPositionManager: this._rowSizeAndPositionManager, previousCellsCount: prevProps.rowCount, previousCellSize: prevProps.rowHeight, previousScrollToAlignment: prevProps.scrollToAlignment, previousScrollToIndex: prevProps.scrollToRow, previousSize: prevProps.height, scrollOffset: scrollTop, scrollToAlignment: scrollToAlignment, scrollToIndex: scrollToRow, size: height, updateScrollIndexCallback: function updateScrollIndexCallback(scrollToRow) { return _this2._updateScrollTopForScrollToRow(_this2.props); } }); } // Update onRowsRendered callback if start/stop indices have changed this._invokeOnGridRenderedHelper(); // Changes to :scrollLeft or :scrollTop should also notify :onScroll listeners if (scrollLeft !== prevState.scrollLeft || scrollTop !== prevState.scrollTop) { var totalRowsHeight = this._rowSizeAndPositionManager.getTotalSize(); var totalColumnsWidth = this._columnSizeAndPositionManager.getTotalSize(); this._invokeOnScrollMemoizer({ scrollLeft: scrollLeft, scrollTop: scrollTop, totalColumnsWidth: totalColumnsWidth, totalRowsHeight: totalRowsHeight }); } } }, { key: 'componentWillMount', value: function componentWillMount() { var getScrollbarSize = this.props.getScrollbarSize; // If this component is being rendered server-side, getScrollbarSize() will return undefined. // We handle this case in componentDidMount() this._scrollbarSize = getScrollbarSize(); if (this._scrollbarSize === undefined) { this._scrollbarSizeMeasured = false; this._scrollbarSize = 0; } else { this._scrollbarSizeMeasured = true; } this._calculateChildrenToRender(); } }, { key: 'componentWillUnmount', value: function componentWillUnmount() { if (this._disablePointerEventsTimeoutId) { clearTimeout(this._disablePointerEventsTimeoutId); } } /** * @private * This method updates scrollLeft/scrollTop in state for the following conditions: * 1) Empty content (0 rows or columns) * 2) New scroll props overriding the current state * 3) Cells-count or cells-size has changed, making previous scroll offsets invalid */ }, { key: 'componentWillReceiveProps', value: function componentWillReceiveProps(nextProps) { var _this3 = this; var _state2 = this.state, scrollLeft = _state2.scrollLeft, scrollTop = _state2.scrollTop; if (nextProps.columnCount === 0 && scrollLeft !== 0 || nextProps.rowCount === 0 && scrollTop !== 0) { this._setScrollPosition({ scrollLeft: 0, scrollTop: 0 }); } else if (nextProps.scrollLeft !== this.props.scrollLeft || nextProps.scrollTop !== this.props.scrollTop) { var newState = {}; if (nextProps.scrollLeft != null) { newState.scrollLeft = nextProps.scrollLeft; } if (nextProps.scrollTop != null) { newState.scrollTop = nextProps.scrollTop; } this._setScrollPosition(newState); } if (nextProps.columnWidth !== this.props.columnWidth || nextProps.rowHeight !== this.props.rowHeight) { this._styleCache = {}; } this._columnWidthGetter = this._wrapSizeGetter(nextProps.columnWidth); this._rowHeightGetter = this._wrapSizeGetter(nextProps.rowHeight); this._columnSizeAndPositionManager.configure({ cellCount: nextProps.columnCount, estimatedCellSize: this._getEstimatedColumnSize(nextProps) }); this._rowSizeAndPositionManager.configure({ cellCount: nextProps.rowCount, estimatedCellSize: this._getEstimatedRowSize(nextProps) }); var _props5 = this.props, columnCount = _props5.columnCount, rowCount = _props5.rowCount; // Special case when either cols or rows were 0 // This would prevent any cells from rendering // So we need to reset row scroll if cols changed from 0 (and vice versa) if (columnCount === 0 || rowCount === 0) { columnCount = 0; rowCount = 0; } // If scrolling is controlled outside this component, clear cache when scrolling stops if (nextProps.autoHeight && nextProps.isScrolling === false && this.props.isScrolling === true) { this._resetStyleCache(); } // Update scroll offsets if the size or number of cells have changed, invalidating the previous value (0, _calculateSizeAndPositionDataAndUpdateScrollOffset2.default)({ cellCount: columnCount, cellSize: this.props.columnWidth, computeMetadataCallback: function computeMetadataCallback() { return _this3._columnSizeAndPositionManager.resetCell(0); }, computeMetadataCallbackProps: nextProps, nextCellsCount: nextProps.columnCount, nextCellSize: nextProps.columnWidth, nextScrollToIndex: nextProps.scrollToColumn, scrollToIndex: this.props.scrollToColumn, updateScrollOffsetForScrollToIndex: function updateScrollOffsetForScrollToIndex() { return _this3._updateScrollLeftForScrollToColumn(nextProps, _this3.state); } }); (0, _calculateSizeAndPositionDataAndUpdateScrollOffset2.default)({ cellCount: rowCount, cellSize: this.props.rowHeight, computeMetadataCallback: function computeMetadataCallback() { return _this3._rowSizeAndPositionManager.resetCell(0); }, computeMetadataCallbackProps: nextProps, nextCellsCount: nextProps.rowCount, nextCellSize: nextProps.rowHeight, nextScrollToIndex: nextProps.scrollToRow, scrollToIndex: this.props.scrollToRow, updateScrollOffsetForScrollToIndex: function updateScrollOffsetForScrollToIndex() { return _this3._updateScrollTopForScrollToRow(nextProps, _this3.state); } }); } }, { key: 'componentWillUpdate', value: function componentWillUpdate(nextProps, nextState) { this._calculateChildrenToRender(nextProps, nextState); } }, { key: 'render', value: function render() { var _props6 = this.props, autoContainerWidth = _props6.autoContainerWidth, autoHeight = _props6.autoHeight, autoWidth = _props6.autoWidth, className = _props6.className, containerStyle = _props6.containerStyle, height = _props6.height, id = _props6.id, noContentRenderer = _props6.noContentRenderer, role = _props6.role, style = _props6.style, tabIndex = _props6.tabIndex, width = _props6.width; var isScrolling = this._isScrolling(); var gridStyle = { boxSizing: 'border-box', direction: 'ltr', height: autoHeight ? 'auto' : height, position: 'relative', width: autoWidth ? 'auto' : width, WebkitOverflowScrolling: 'touch', willChange: 'transform' }; var totalColumnsWidth = this._columnSizeAndPositionManager.getTotalSize(); var totalRowsHeight = this._rowSizeAndPositionManager.getTotalSize(); // Force browser to hide scrollbars when we know they aren't necessary. // Otherwise once scrollbars appear they may not disappear again. // For more info see issue #116 var verticalScrollBarSize = totalRowsHeight > height ? this._scrollbarSize : 0; var horizontalScrollBarSize = totalColumnsWidth > width ? this._scrollbarSize : 0; // Also explicitly init styles to 'auto' if scrollbars are required. // This works around an obscure edge case where external CSS styles have not yet been loaded, // But an initial scroll index of offset is set as an external prop. // Without this style, Grid would render the correct range of cells but would NOT update its internal offset. // This was originally reported via clauderic/react-infinite-calendar/issues/23 gridStyle.overflowX = totalColumnsWidth + verticalScrollBarSize <= width ? 'hidden' : 'auto'; gridStyle.overflowY = totalRowsHeight + horizontalScrollBarSize <= height ? 'hidden' : 'auto'; var childrenToDisplay = this._childrenToDisplay; var showNoContentRenderer = childrenToDisplay.length === 0 && height > 0 && width > 0; return _react2.default.createElement( 'div', { ref: this._setScrollingContainerRef, 'aria-label': this.props['aria-label'], className: (0, _classnames2.default)('ReactVirtualized__Grid', className), id: id, onScroll: this._onScroll, role: role, style: _extends({}, gridStyle, style), tabIndex: tabIndex }, childrenToDisplay.length > 0 && _react2.default.createElement( 'div', { className: 'ReactVirtualized__Grid__innerScrollContainer', style: _extends({ width: autoContainerWidth ? 'auto' : totalColumnsWidth, height: totalRowsHeight, maxWidth: totalColumnsWidth, maxHeight: totalRowsHeight, overflow: 'hidden', pointerEvents: isScrolling ? 'none' : '', position: 'relative' }, containerStyle) }, childrenToDisplay ), showNoContentRenderer && noContentRenderer() ); } /* ---------------------------- Helper methods ---------------------------- */ }, { key: '_calculateChildrenToRender', value: function _calculateChildrenToRender() { var props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.props; var state = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.state; var cellRenderer = props.cellRenderer, cellRangeRenderer = props.cellRangeRenderer, columnCount = props.columnCount, deferredMeasurementCache = props.deferredMeasurementCache, height = props.height, overscanColumnCount = props.overscanColumnCount, overscanIndicesGetter = props.overscanIndicesGetter, overscanRowCount = props.overscanRowCount, rowCount = props.rowCount, width = props.width; var scrollDirectionHorizontal = state.scrollDirectionHorizontal, scrollDirectionVertical = state.scrollDirectionVertical, scrollLeft = state.scrollLeft, scrollTop = state.scrollTop; var isScrolling = this._isScrolling(props, state); this._childrenToDisplay = []; // Render only enough columns and rows to cover the visible area of the grid. if (height > 0 && width > 0) { var visibleColumnIndices = this._columnSizeAndPositionManager.getVisibleCellRange({ containerSize: width, offset: scrollLeft }); var visibleRowIndices = this._rowSizeAndPositionManager.getVisibleCellRange({ containerSize: height, offset: scrollTop }); var horizontalOffsetAdjustment = this._columnSizeAndPositionManager.getOffsetAdjustment({ containerSize: width, offset: scrollLeft }); var verticalOffsetAdjustment = this._rowSizeAndPositionManager.getOffsetAdjustment({ containerSize: height, offset: scrollTop }); // Store for _invokeOnGridRenderedHelper() this._renderedColumnStartIndex = visibleColumnIndices.start; this._renderedColumnStopIndex = visibleColumnIndices.stop; this._renderedRowStartIndex = visibleRowIndices.start; this._renderedRowStopIndex = visibleRowIndices.stop; var overscanColumnIndices = overscanIndicesGetter({ direction: 'horizontal', cellCount: columnCount, overscanCellsCount: overscanColumnCount, scrollDirection: scrollDirectionHorizontal, startIndex: this._renderedColumnStartIndex, stopIndex: this._renderedColumnStopIndex }); var overscanRowIndices = overscanIndicesGetter({ direction: 'vertical', cellCount: rowCount, overscanCellsCount: overscanRowCount, scrollDirection: scrollDirectionVertical, startIndex: this._renderedRowStartIndex, stopIndex: this._renderedRowStopIndex }); // Store for _invokeOnGridRenderedHelper() this._columnStartIndex = overscanColumnIndices.overscanStartIndex; this._columnStopIndex = overscanColumnIndices.overscanStopIndex; this._rowStartIndex = overscanRowIndices.overscanStartIndex; this._rowStopIndex = overscanRowIndices.overscanStopIndex; this._childrenToDisplay = cellRangeRenderer({ cellCache: this._cellCache, cellRenderer: cellRenderer, columnSizeAndPositionManager: this._columnSizeAndPositionManager, columnStartIndex: this._columnStartIndex, columnStopIndex: this._columnStopIndex, deferredMeasurementCache: deferredMeasurementCache, horizontalOffsetAdjustment: horizontalOffsetAdjustment, isScrolling: isScrolling, parent: this, rowSizeAndPositionManager: this._rowSizeAndPositionManager, rowStartIndex: this._rowStartIndex, rowStopIndex: this._rowStopIndex, scrollLeft: scrollLeft, scrollTop: scrollTop, styleCache: this._styleCache, verticalOffsetAdjustment: verticalOffsetAdjustment, visibleColumnIndices: visibleColumnIndices, visibleRowIndices: visibleRowIndices }); } } /** * Sets an :isScrolling flag for a small window of time. * This flag is used to disable pointer events on the scrollable portion of the Grid. * This prevents jerky/stuttery mouse-wheel scrolling. */ }, { key: '_debounceScrollEnded', value: function _debounceScrollEnded() { var scrollingResetTimeInterval = this.props.scrollingResetTimeInterval; if (this._disablePointerEventsTimeoutId) { clearTimeout(this._disablePointerEventsTimeoutId); } this._disablePointerEventsTimeoutId = setTimeout(this._debounceScrollEndedCallback, scrollingResetTimeInterval); } }, { key: '_debounceScrollEndedCallback', value: function _debounceScrollEndedCallback() { this._disablePointerEventsTimeoutId = null; this._resetStyleCache(); } }, { key: '_getEstimatedColumnSize', value: function _getEstimatedColumnSize(props) { return typeof props.columnWidth === 'number' ? props.columnWidth : props.estimatedColumnSize; } }, { key: '_getEstimatedRowSize', value: function _getEstimatedRowSize(props) { return typeof props.rowHeight === 'number' ? props.rowHeight : props.estimatedRowSize; } /** * Check for batched CellMeasurer size invalidations. * This will occur the first time one or more previously unmeasured cells are rendered. */ }, { key: '_handleInvalidatedGridSize', value: function _handleInvalidatedGridSize() { if (typeof this._deferredInvalidateColumnIndex === 'number') { var columnIndex = this._deferredInvalidateColumnIndex; var rowIndex = this._deferredInvalidateRowIndex; this._deferredInvalidateColumnIndex = null; this._deferredInvalidateRowIndex = null; this.recomputeGridSize({ columnIndex: columnIndex, rowIndex: rowIndex }); } } }, { key: '_invokeOnGridRenderedHelper', value: function _invokeOnGridRenderedHelper() { var onSectionRendered = this.props.onSectionRendered; this._onGridRenderedMemoizer({ callback: onSectionRendered, indices: { columnOverscanStartIndex: this._columnStartIndex, columnOverscanStopIndex: this._columnStopIndex, columnStartIndex: this._renderedColumnStartIndex, columnStopIndex: this._renderedColumnStopIndex, rowOverscanStartIndex: this._rowStartIndex, rowOverscanStopIndex: this._rowStopIndex, rowStartIndex: this._renderedRowStartIndex, rowStopIndex: this._renderedRowStopIndex } }); } }, { key: '_invokeOnScrollMemoizer', value: function _invokeOnScrollMemoizer(_ref6) { var _this4 = this; var scrollLeft = _ref6.scrollLeft, scrollTop = _ref6.scrollTop, totalColumnsWidth = _ref6.totalColumnsWidth, totalRowsHeight = _ref6.totalRowsHeight; this._onScrollMemoizer({ callback: function callback(_ref7) { var scrollLeft = _ref7.scrollLeft, scrollTop = _ref7.scrollTop; var _props7 = _this4.props, height = _props7.height, onScroll = _props7.onScroll, width = _props7.width; onScroll({ clientHeight: height, clientWidth: width, scrollHeight: totalRowsHeight, scrollLeft: scrollLeft, scrollTop: scrollTop, scrollWidth: totalColumnsWidth }); }, indices: { scrollLeft: scrollLeft, scrollTop: scrollTop } }); } }, { key: '_isScrolling', value: function _isScrolling() { var props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.props; var state = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.state; // If isScrolling is defined in props, use it to override the value in state // This is a performance optimization for WindowScroller + Grid return Object.hasOwnProperty.call(props, 'isScrolling') ? props.isScrolling : state.isScrolling; } }, { key: '_setScrollingContainerRef', value: function _setScrollingContainerRef(ref) { this._scrollingContainer = ref; } }, { key: '_setScrollPosition', value: function _setScrollPosition(_ref8) { var scrollLeft = _ref8.scrollLeft, scrollTop = _ref8.scrollTop; var newState = { scrollPositionChangeReason: SCROLL_POSITION_CHANGE_REASONS.REQUESTED }; if (scrollLeft >= 0) { newState.scrollDirectionHorizontal = scrollLeft > this.state.scrollLeft ? _defaultOverscanIndicesGetter.SCROLL_DIRECTION_FORWARD : _defaultOverscanIndicesGetter.SCROLL_DIRECTION_BACKWARD; newState.scrollLeft = scrollLeft; } if (scrollTop >= 0) { newState.scrollDirectionVertical = scrollTop > this.state.scrollTop ? _defaultOverscanIndicesGetter.SCROLL_DIRECTION_FORWARD : _defaultOverscanIndicesGetter.SCROLL_DIRECTION_BACKWARD; newState.scrollTop = scrollTop; } if (scrollLeft >= 0 && scrollLeft !== this.state.scrollLeft || scrollTop >= 0 && scrollTop !== this.state.scrollTop) { this.setState(newState); } } }, { key: '_wrapPropertyGetter', value: function _wrapPropertyGetter(value) { return typeof value === 'function' ? value : function () { return value; }; } }, { key: '_wrapSizeGetter', value: function _wrapSizeGetter(size) { return this._wrapPropertyGetter(size); } }, { key: '_getCalculatedScrollLeft', value: function _getCalculatedScrollLeft() { var props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.props; var state = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.state; var columnCount = props.columnCount, height = props.height, scrollToAlignment = props.scrollToAlignment, scrollToColumn = props.scrollToColumn, width = props.width; var scrollLeft = state.scrollLeft; if (scrollToColumn >= 0 && columnCount > 0) { var targetIndex = Math.max(0, Math.min(columnCount - 1, scrollToColumn)); var totalRowsHeight = this._rowSizeAndPositionManager.getTotalSize(); var scrollBarSize = totalRowsHeight > height ? this._scrollbarSize : 0; return this._columnSizeAndPositionManager.getUpdatedOffsetForIndex({ align: scrollToAlignment, containerSize: width - scrollBarSize, currentOffset: scrollLeft, targetIndex: targetIndex }); } } }, { key: '_updateScrollLeftForScrollToColumn', value: function _updateScrollLeftForScrollToColumn() { var props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.props; var state = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.state; var scrollLeft = state.scrollLeft; var calculatedScrollLeft = this._getCalculatedScrollLeft(props, state); if (calculatedScrollLeft >= 0 && scrollLeft !== calculatedScrollLeft) { this._setScrollPosition({ scrollLeft: calculatedScrollLeft }); } } }, { key: '_getCalculatedScrollTop', value: function _getCalculatedScrollTop() { var props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.props; var state = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.state; var height = props.height, rowCount = props.rowCount, scrollToAlignment = props.scrollToAlignment, scrollToRow = props.scrollToRow, width = props.width; var scrollTop = state.scrollTop; if (scrollToRow >= 0 && rowCount > 0) { var targetIndex = Math.max(0, Math.min(rowCount - 1, scrollToRow)); var totalColumnsWidth = this._columnSizeAndPositionManager.getTotalSize(); var scrollBarSize = totalColumnsWidth > width ? this._scrollbarSize : 0; return this._rowSizeAndPositionManager.getUpdatedOffsetForIndex({ align: scrollToAlignment, containerSize: height - scrollBarSize, currentOffset: scrollTop, targetIndex: targetIndex }); } } }, { key: '_resetStyleCache', value: function _resetStyleCache() { var styleCache = this._styleCache; // Reset cell and style caches once scrolling stops. // This makes Grid simpler to use (since cells commonly change). // And it keeps the caches from growing too large. // Performance is most sensitive when a user is scrolling. this._cellCache = {}; this._styleCache = {}; // Copy over the visible cell styles so avoid unnecessary re-render. for (var rowIndex = this._rowStartIndex; rowIndex <= this._rowStopIndex; rowIndex++) { for (var columnIndex = this._columnStartIndex; columnIndex <= this._columnStopIndex; columnIndex++) { var key = rowIndex + '-' + columnIndex; this._styleCache[key] = styleCache[key]; } } this.setState({ isScrolling: false }); } }, { key: '_updateScrollTopForScrollToRow', value: function _updateScrollTopForScrollToRow() { var props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.props; var state = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.state; var scrollTop = state.scrollTop; var calculatedScrollTop = this._getCalculatedScrollTop(props, state); if (calculatedScrollTop >= 0 && scrollTop !== calculatedScrollTop) { this._setScrollPosition({ scrollTop: calculatedScrollTop }); } } }, { key: '_onScroll', value: function _onScroll(event) { // In certain edge-cases React dispatches an onScroll event with an invalid target.scrollLeft / target.scrollTop. // This invalid event can be detected by comparing event.target to this component's scrollable DOM element. // See issue #404 for more information. if (event.target !== this._scrollingContainer) { return; } // On iOS, we can arrive at negative offsets by swiping past the start. // To prevent flicker here, we make playing in the negative offset zone cause nothing to happen. if (event.target.scrollTop < 0) { return; } // Prevent pointer events from interrupting a smooth scroll this._debounceScrollEnded(); var _props8 = this.props, autoHeight = _props8.autoHeight, autoWidth = _props8.autoWidth, height = _props8.height, width = _props8.width; var _event$target = event.target, eventScrollLeft = _event$target.scrollLeft, eventScrollTop = _event$target.scrollTop; // When this component is shrunk drastically, React dispatches a series of back-to-back scroll events, // Gradually converging on a scrollTop that is within the bounds of the new, smaller height. // This causes a series of rapid renders that is slow for long lists. // We can avoid that by doing some simple bounds checking to ensure that scroll offsets never exceed their bounds. var scrollbarSize = this._scrollbarSize; var totalRowsHeight = this._rowSizeAndPositionManager.getTotalSize(); var totalColumnsWidth = this._columnSizeAndPositionManager.getTotalSize(); var scrollLeft = Math.min(Math.max(0, totalColumnsWidth - width + scrollbarSize), eventScrollLeft); var scrollTop = Math.min(Math.max(0, totalRowsHeight - height + scrollbarSize), eventScrollTop); // Certain devices (like Apple touchpad) rapid-fire duplicate events. // Don't force a re-render if this is the case. // The mouse may move faster then the animation frame does. // Use requestAnimationFrame to avoid over-updating. if (this.state.scrollLeft !== scrollLeft || this.state.scrollTop !== scrollTop) { // Track scrolling direction so we can more efficiently overscan rows to reduce empty space around the edges while scrolling. // Don't change direction for an axis unless scroll offset has changed. var scrollDirectionHorizontal = scrollLeft !== this.state.scrollLeft ? scrollLeft > this.state.scrollLeft ? _defaultOverscanIndicesGetter.SCROLL_DIRECTION_FORWARD : _defaultOverscanIndicesGetter.SCROLL_DIRECTION_BACKWARD : this.state.scrollDirectionHorizontal; var scrollDirectionVertical = scrollTop !== this.state.scrollTop ? scrollTop > this.state.scrollTop ? _defaultOverscanIndicesGetter.SCROLL_DIRECTION_FORWARD : _defaultOverscanIndicesGetter.SCROLL_DIRECTION_BACKWARD : this.state.scrollDirectionVertical; var newState = { isScrolling: true, scrollDirectionHorizontal: scrollDirectionHorizontal, scrollDirectionVertical: scrollDirectionVertical, scrollPositionChangeReason: SCROLL_POSITION_CHANGE_REASONS.OBSERVED }; if (!autoHeight) { newState.scrollTop = scrollTop; } if (!autoWidth) { newState.scrollLeft = scrollLeft; } this.setState(newState); } this._invokeOnScrollMemoizer({ scrollLeft: scrollLeft, scrollTop: scrollTop, totalColumnsWidth: totalColumnsWidth, totalRowsHeight: totalRowsHeight }); } }]); return Grid; }(_react.PureComponent); Grid.defaultProps = { 'aria-label': 'grid', cellRangeRenderer: _defaultCellRangeRenderer2.default, estimatedColumnSize: 100, estimatedRowSize: 30, getScrollbarSize: _scrollbarSize2.default, noContentRenderer: function noContentRenderer() { return null; }, onScroll: function onScroll() { return null; }, onSectionRendered: function onSectionRendered() { return null; }, overscanColumnCount: 0, overscanIndicesGetter: _defaultOverscanIndicesGetter2.default, overscanRowCount: 10, role: 'grid', scrollingResetTimeInterval: DEFAULT_SCROLLING_RESET_TIME_INTERVAL, scrollToAlignment: 'auto', scrollToColumn: -1, scrollToRow: -1, style: {}, tabIndex: 0 }; exports.default = Grid; process.env.NODE_ENV !== "production" ? Grid.propTypes = { 'aria-label': _propTypes2.default.string, /** * Set the width of the inner scrollable container to 'auto'. * This is useful for single-column Grids to ensure that the column doesn't extend below a vertical scrollbar. */ autoContainerWidth: _propTypes2.default.bool, /** * Removes fixed height from the scrollingContainer so that the total height of rows can stretch the window. * Intended for use with WindowScroller */ autoHeight: _propTypes2.default.bool, /** * Removes fixed width from the scrollingContainer so that the total width of rows can stretch the window. * Intended for use with WindowScroller */ autoWidth: _propTypes2.default.bool, /** * Responsible for rendering a cell given an row and column index. * Should implement the following interface: ({ columnIndex: number, rowIndex: number }): PropTypes.node */ cellRenderer: _propTypes2.default.func.isRequired, /** * Responsible for rendering a group of cells given their index ranges. * Should implement the following interface: ({ * cellCache: Map, * cellRenderer: Function, * columnSizeAndPositionManager: CellSizeAndPositionManager, * columnStartIndex: number, * columnStopIndex: number, * isScrolling: boolean, * rowSizeAndPositionManager: CellSizeAndPositionManager, * rowStartIndex: number, * rowStopIndex: number, * scrollLeft: number, * scrollTop: number * }): Array */ cellRangeRenderer: _propTypes2.default.func.isRequired, /** * Optional custom CSS class name to attach to root Grid element. */ className: _propTypes2.default.string, /** * Number of columns in grid. */ columnCount: _propTypes2.default.number.isRequired, /** * Either a fixed column width (number) or a function that returns the width of a column given its index. * Should implement the following interface: (index: number): number */ columnWidth: _propTypes2.default.oneOfType([_propTypes2.default.number, _propTypes2.default.func]).isRequired, /** Optional inline style applied to inner cell-container */ containerStyle: _propTypes2.default.object, /** * If CellMeasurer is used to measure this Grid's children, this should be a pointer to its CellMeasurerCache. * A shared CellMeasurerCache reference enables Grid and CellMeasurer to share measurement data. */ deferredMeasurementCache: _propTypes2.default.object, /** * Used to estimate the total width of a Grid before all of its columns have actually been measured. * The estimated total width is adjusted as columns are rendered. */ estimatedColumnSize: _propTypes2.default.number.isRequired, /** * Used to estimate the total height of a Grid before all of its rows have actually been measured. * The estimated total height is adjusted as rows are rendered. */ estimatedRowSize: _propTypes2.default.number.isRequired, /** * Exposed for testing purposes only. */ getScrollbarSize: _propTypes2.default.func.isRequired, /** * Height of Grid; this property determines the number of visible (vs virtualized) rows. */ height: _propTypes2.default.number.isRequired, /** * Optional custom id to attach to root Grid element. */ id: _propTypes2.default.string, /** * Override internal is-scrolling state tracking. * This property is primarily intended for use with the WindowScroller component. */ isScrolling: _propTypes2.default.bool, /** * Optional renderer to be used in place of rows when either :rowCount or :columnCount is 0. */ noContentRenderer: _propTypes2.default.func.isRequired, /** * Callback invoked whenever the scroll offset changes within the inner scrollable region. * This callback can be used to sync scrolling between lists, tables, or grids. * ({ clientHeight, clientWidth, scrollHeight, scrollLeft, scrollTop, scrollWidth }): void */ onScroll: _propTypes2.default.func.isRequired, /** * Callback invoked with information about the section of the Grid that was just rendered. * ({ columnStartIndex, columnStopIndex, rowStartIndex, rowStopIndex }): void */ onSectionRendered: _propTypes2.default.func.isRequired, /** * Number of columns to render before/after the visible section of the grid. * These columns can help for smoother scrolling on touch devices or browsers that send scroll events infrequently. */ overscanColumnCount: _propTypes2.default.number.isRequired, /** * Calculates the number of cells to overscan before and after a specified range. * This function ensures that overscanning doesn't exceed the available cells. * Should implement the following interface: ({ * cellCount: number, * overscanCellsCount: number, * scrollDirection: number, * startIndex: number, * stopIndex: number * }): {overscanStartIndex: number, overscanStopIndex: number} */ overscanIndicesGetter: _propTypes2.default.func.isRequired, /** * Number of rows to render above/below the visible section of the grid. * These rows can help for smoother scrolling on touch devices or browsers that send scroll events infrequently. */ overscanRowCount: _propTypes2.default.number.isRequired, /** * ARIA role for the grid element. */ role: _propTypes2.default.string, /** * Either a fixed row height (number) or a function that returns the height of a row given its index. * Should implement the following interface: ({ index: number }): number */ rowHeight: _propTypes2.default.oneOfType([_propTypes2.default.number, _propTypes2.default.func]).isRequired, /** * Number of rows in grid. */ rowCount: _propTypes2.default.number.isRequired, /** Wait this amount of time after the last scroll event before resetting Grid `pointer-events`. */ scrollingResetTimeInterval: _propTypes2.default.number, /** Horizontal offset. */ scrollLeft: _propTypes2.default.number, /** * Controls scroll-to-cell behavior of the Grid. * The default ("auto") scrolls the least amount possible to ensure that the specified cell is fully visible. * Use "start" to align cells to the top/left of the Grid and "end" to align bottom/right. */ scrollToAlignment: _propTypes2.default.oneOf(['auto', 'end', 'start', 'center']).isRequired, /** * Column index to ensure visible (by forcefully scrolling if necessary) */ scrollToColumn: _propTypes2.default.number.isRequired, /** Vertical offset. */ scrollTop: _propTypes2.default.number, /** * Row index to ensure visible (by forcefully scrolling if necessary) */ scrollToRow: _propTypes2.default.number.isRequired, /** Optional inline style */ style: _propTypes2.default.object, /** Tab index for focus */ tabIndex: _propTypes2.default.number, /** * Width of Grid; this property determines the number of visible (vs virtualized) columns. */ width: _propTypes2.default.number.isRequired } : void 0;