UNPKG

react-virtualized

Version:

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

463 lines (398 loc) 17.6 kB
import _extends from 'babel-runtime/helpers/extends'; import _defineProperty from 'babel-runtime/helpers/defineProperty'; import _Object$getPrototypeOf from 'babel-runtime/core-js/object/get-prototype-of'; import _classCallCheck from 'babel-runtime/helpers/classCallCheck'; import _createClass from 'babel-runtime/helpers/createClass'; import _possibleConstructorReturn from 'babel-runtime/helpers/possibleConstructorReturn'; import _inherits from 'babel-runtime/helpers/inherits'; import clsx from 'clsx'; import * as React from 'react'; import { polyfill } from 'react-lifecycles-compat'; import PositionCache from './PositionCache'; import { requestAnimationTimeout, cancelAnimationTimeout } from '../utils/requestAnimationTimeout'; var emptyObject = {}; /** * Specifies the number of miliseconds during which to disable pointer events while a scroll is in progress. * This improves performance and makes scrolling smoother. */ export var DEFAULT_SCROLLING_RESET_TIME_INTERVAL = 150; /** * This component efficiently displays arbitrarily positioned cells using windowing techniques. * Cell position is determined by an injected `cellPositioner` property. * Windowing is vertical; this component does not support horizontal scrolling. * * Rendering occurs in two phases: * 1) First pass uses estimated cell sizes (provided by the cache) to determine how many cells to measure in a batch. * Batch size is chosen using a fast, naive layout algorithm that stacks images in order until the viewport has been filled. * After measurement is complete (componentDidMount or componentDidUpdate) this component evaluates positioned cells * in order to determine if another measurement pass is required (eg if actual cell sizes were less than estimated sizes). * All measurements are permanently cached (keyed by `keyMapper`) for performance purposes. * 2) Second pass uses the external `cellPositioner` to layout cells. * At this time the positioner has access to cached size measurements for all cells. * The positions it returns are cached by Masonry for fast access later. * Phase one is repeated if the user scrolls beyond the current layout's bounds. * If the layout is invalidated due to eg a resize, cached positions can be cleared using `recomputeCellPositions()`. * * Animation constraints: * Simple animations are supported (eg translate/slide into place on initial reveal). * More complex animations are not (eg flying from one position to another on resize). * * Layout constraints: * This component supports multi-column layout. * The height of each item may vary. * The width of each item must not exceed the width of the column it is "in". * The left position of all items within a column must align. * (Items may not span multiple columns.) */ var Masonry = function (_React$PureComponent) { _inherits(Masonry, _React$PureComponent); function Masonry() { var _ref; var _temp, _this, _ret; _classCallCheck(this, Masonry); for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref = Masonry.__proto__ || _Object$getPrototypeOf(Masonry)).call.apply(_ref, [this].concat(args))), _this), _this.state = { isScrolling: false, scrollTop: 0 }, _this._invalidateOnUpdateStartIndex = null, _this._invalidateOnUpdateStopIndex = null, _this._positionCache = new PositionCache(), _this._startIndex = null, _this._startIndexMemoized = null, _this._stopIndex = null, _this._stopIndexMemoized = null, _this._debounceResetIsScrollingCallback = function () { _this.setState({ isScrolling: false }); }, _this._setScrollingContainerRef = function (ref) { _this._scrollingContainer = ref; }, _this._onScroll = function (event) { var height = _this.props.height; var eventScrollTop = event.currentTarget.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 scrollTop = Math.min(Math.max(0, _this._getEstimatedTotalHeight() - height), eventScrollTop); // On iOS, we can arrive at negative offsets by swiping past the start or end. // Avoid re-rendering in this case as it can cause problems; see #532 for more. if (eventScrollTop !== scrollTop) { return; } // Prevent pointer events from interrupting a smooth scroll _this._debounceResetIsScrolling(); // 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.scrollTop !== scrollTop) { _this.setState({ isScrolling: true, scrollTop: scrollTop }); } }, _temp), _possibleConstructorReturn(_this, _ret); } _createClass(Masonry, [{ key: 'clearCellPositions', value: function clearCellPositions() { this._positionCache = new PositionCache(); this.forceUpdate(); } // HACK This method signature was intended for Grid }, { key: 'invalidateCellSizeAfterRender', value: function invalidateCellSizeAfterRender(_ref2) { var index = _ref2.rowIndex; if (this._invalidateOnUpdateStartIndex === null) { this._invalidateOnUpdateStartIndex = index; this._invalidateOnUpdateStopIndex = index; } else { this._invalidateOnUpdateStartIndex = Math.min(this._invalidateOnUpdateStartIndex, index); this._invalidateOnUpdateStopIndex = Math.max(this._invalidateOnUpdateStopIndex, index); } } }, { key: 'recomputeCellPositions', value: function recomputeCellPositions() { var stopIndex = this._positionCache.count - 1; this._positionCache = new PositionCache(); this._populatePositionCache(0, stopIndex); this.forceUpdate(); } }, { key: 'componentDidMount', value: function componentDidMount() { this._checkInvalidateOnUpdate(); this._invokeOnScrollCallback(); this._invokeOnCellsRenderedCallback(); } }, { key: 'componentDidUpdate', value: function componentDidUpdate(prevProps, prevState) { this._checkInvalidateOnUpdate(); this._invokeOnScrollCallback(); this._invokeOnCellsRenderedCallback(); if (this.props.scrollTop !== prevProps.scrollTop) { this._debounceResetIsScrolling(); } } }, { key: 'componentWillUnmount', value: function componentWillUnmount() { if (this._debounceResetIsScrollingId) { cancelAnimationTimeout(this._debounceResetIsScrollingId); } } }, { key: 'render', value: function render() { var _this2 = this; var _props = this.props, autoHeight = _props.autoHeight, cellCount = _props.cellCount, cellMeasurerCache = _props.cellMeasurerCache, cellRenderer = _props.cellRenderer, className = _props.className, height = _props.height, id = _props.id, keyMapper = _props.keyMapper, overscanByPixels = _props.overscanByPixels, role = _props.role, style = _props.style, tabIndex = _props.tabIndex, width = _props.width, rowDirection = _props.rowDirection; var _state = this.state, isScrolling = _state.isScrolling, scrollTop = _state.scrollTop; var children = []; var estimateTotalHeight = this._getEstimatedTotalHeight(); var shortestColumnSize = this._positionCache.shortestColumnSize; var measuredCellCount = this._positionCache.count; var startIndex = 0; var stopIndex = void 0; this._positionCache.range(Math.max(0, scrollTop - overscanByPixels), height + overscanByPixels * 2, function (index, left, top) { var _style; if (typeof stopIndex === 'undefined') { startIndex = index; stopIndex = index; } else { startIndex = Math.min(startIndex, index); stopIndex = Math.max(stopIndex, index); } children.push(cellRenderer({ index: index, isScrolling: isScrolling, key: keyMapper(index), parent: _this2, style: (_style = { height: cellMeasurerCache.getHeight(index) }, _defineProperty(_style, rowDirection === 'ltr' ? 'left' : 'right', left), _defineProperty(_style, 'position', 'absolute'), _defineProperty(_style, 'top', top), _defineProperty(_style, 'width', cellMeasurerCache.getWidth(index)), _style) })); }); // We need to measure additional cells for this layout if (shortestColumnSize < scrollTop + height + overscanByPixels && measuredCellCount < cellCount) { var batchSize = Math.min(cellCount - measuredCellCount, Math.ceil((scrollTop + height + overscanByPixels - shortestColumnSize) / cellMeasurerCache.defaultHeight * width / cellMeasurerCache.defaultWidth)); for (var _index = measuredCellCount; _index < measuredCellCount + batchSize; _index++) { stopIndex = _index; children.push(cellRenderer({ index: _index, isScrolling: isScrolling, key: keyMapper(_index), parent: this, style: { width: cellMeasurerCache.getWidth(_index) } })); } } this._startIndex = startIndex; this._stopIndex = stopIndex; return React.createElement( 'div', { ref: this._setScrollingContainerRef, 'aria-label': this.props['aria-label'], className: clsx('ReactVirtualized__Masonry', className), id: id, onScroll: this._onScroll, role: role, style: _extends({ boxSizing: 'border-box', direction: 'ltr', height: autoHeight ? 'auto' : height, overflowX: 'hidden', overflowY: estimateTotalHeight < height ? 'hidden' : 'auto', position: 'relative', width: width, WebkitOverflowScrolling: 'touch', willChange: 'transform' }, style), tabIndex: tabIndex }, React.createElement( 'div', { className: 'ReactVirtualized__Masonry__innerScrollContainer', style: { width: '100%', height: estimateTotalHeight, maxWidth: '100%', maxHeight: estimateTotalHeight, overflow: 'hidden', pointerEvents: isScrolling ? 'none' : '', position: 'relative' } }, children ) ); } }, { key: '_checkInvalidateOnUpdate', value: function _checkInvalidateOnUpdate() { if (typeof this._invalidateOnUpdateStartIndex === 'number') { var _startIndex = this._invalidateOnUpdateStartIndex; var _stopIndex = this._invalidateOnUpdateStopIndex; this._invalidateOnUpdateStartIndex = null; this._invalidateOnUpdateStopIndex = null; // Query external layout logic for position of newly-measured cells this._populatePositionCache(_startIndex, _stopIndex); this.forceUpdate(); } } }, { key: '_debounceResetIsScrolling', value: function _debounceResetIsScrolling() { var scrollingResetTimeInterval = this.props.scrollingResetTimeInterval; if (this._debounceResetIsScrollingId) { cancelAnimationTimeout(this._debounceResetIsScrollingId); } this._debounceResetIsScrollingId = requestAnimationTimeout(this._debounceResetIsScrollingCallback, scrollingResetTimeInterval); } }, { key: '_getEstimatedTotalHeight', value: function _getEstimatedTotalHeight() { var _props2 = this.props, cellCount = _props2.cellCount, cellMeasurerCache = _props2.cellMeasurerCache, width = _props2.width; var estimatedColumnCount = Math.max(1, Math.floor(width / cellMeasurerCache.defaultWidth)); return this._positionCache.estimateTotalHeight(cellCount, estimatedColumnCount, cellMeasurerCache.defaultHeight); } }, { key: '_invokeOnScrollCallback', value: function _invokeOnScrollCallback() { var _props3 = this.props, height = _props3.height, onScroll = _props3.onScroll; var scrollTop = this.state.scrollTop; if (this._onScrollMemoized !== scrollTop) { onScroll({ clientHeight: height, scrollHeight: this._getEstimatedTotalHeight(), scrollTop: scrollTop }); this._onScrollMemoized = scrollTop; } } }, { key: '_invokeOnCellsRenderedCallback', value: function _invokeOnCellsRenderedCallback() { if (this._startIndexMemoized !== this._startIndex || this._stopIndexMemoized !== this._stopIndex) { var _onCellsRendered = this.props.onCellsRendered; _onCellsRendered({ startIndex: this._startIndex, stopIndex: this._stopIndex }); this._startIndexMemoized = this._startIndex; this._stopIndexMemoized = this._stopIndex; } } }, { key: '_populatePositionCache', value: function _populatePositionCache(startIndex, stopIndex) { var _props4 = this.props, cellMeasurerCache = _props4.cellMeasurerCache, cellPositioner = _props4.cellPositioner; for (var _index2 = startIndex; _index2 <= stopIndex; _index2++) { var _cellPositioner = cellPositioner(_index2), _left = _cellPositioner.left, _top = _cellPositioner.top; this._positionCache.setPosition(_index2, _left, _top, cellMeasurerCache.getHeight(_index2)); } } }], [{ key: 'getDerivedStateFromProps', value: function getDerivedStateFromProps(nextProps, prevState) { if (nextProps.scrollTop !== undefined && prevState.scrollTop !== nextProps.scrollTop) { return { isScrolling: true, scrollTop: nextProps.scrollTop }; } return null; } }]); return Masonry; }(React.PureComponent); Masonry.defaultProps = { autoHeight: false, keyMapper: identity, onCellsRendered: noop, onScroll: noop, overscanByPixels: 20, role: 'grid', scrollingResetTimeInterval: DEFAULT_SCROLLING_RESET_TIME_INTERVAL, style: emptyObject, tabIndex: 0, rowDirection: 'ltr' }; Masonry.propTypes = process.env.NODE_ENV === 'production' ? null : { autoHeight: PropTypes.bool.isRequired, cellCount: PropTypes.number.isRequired, cellMeasurerCache: function cellMeasurerCache() { return (typeof CellMeasurerCache === 'function' ? PropTypes.instanceOf(CellMeasurerCache).isRequired : PropTypes.any.isRequired).apply(this, arguments); }, cellPositioner: function cellPositioner() { return (typeof Positioner === 'function' ? PropTypes.instanceOf(Positioner).isRequired : PropTypes.any.isRequired).apply(this, arguments); }, cellRenderer: function cellRenderer() { return (typeof CellRenderer === 'function' ? PropTypes.instanceOf(CellRenderer).isRequired : PropTypes.any.isRequired).apply(this, arguments); }, className: PropTypes.string, height: PropTypes.number.isRequired, id: PropTypes.string, keyMapper: function keyMapper() { return (typeof KeyMapper === 'function' ? PropTypes.instanceOf(KeyMapper).isRequired : PropTypes.any.isRequired).apply(this, arguments); }, onCellsRendered: function onCellsRendered() { return (typeof OnCellsRenderedCallback === 'function' ? PropTypes.instanceOf(OnCellsRenderedCallback) : PropTypes.any).apply(this, arguments); }, onScroll: function onScroll() { return (typeof OnScrollCallback === 'function' ? PropTypes.instanceOf(OnScrollCallback) : PropTypes.any).apply(this, arguments); }, overscanByPixels: PropTypes.number.isRequired, role: PropTypes.string.isRequired, scrollingResetTimeInterval: PropTypes.number.isRequired, style: function style(props, propName, componentName) { if (!Object.prototype.hasOwnProperty.call(props, propName)) { throw new Error('Prop `' + propName + '` has type \'any\' or \'mixed\', but was not provided to `' + componentName + '`. Pass undefined or any other value.'); } }, tabIndex: PropTypes.number.isRequired, width: PropTypes.number.isRequired, rowDirection: PropTypes.string.isRequired }; function identity(value) { return value; } function noop() {} var bpfrpt_proptype_CellMeasurerCache = process.env.NODE_ENV === 'production' ? null : { defaultHeight: PropTypes.number.isRequired, defaultWidth: PropTypes.number.isRequired, getHeight: PropTypes.func.isRequired, getWidth: PropTypes.func.isRequired }; polyfill(Masonry); export default Masonry; var bpfrpt_proptype_Positioner = process.env.NODE_ENV === 'production' ? null : PropTypes.func; import { bpfrpt_proptype_AnimationTimeoutId } from '../utils/requestAnimationTimeout'; import PropTypes from 'prop-types'; export { bpfrpt_proptype_CellMeasurerCache }; export { bpfrpt_proptype_Positioner };