UNPKG

react-infinite

Version:

A browser-ready efficient scrolling container based on UITableView

445 lines (366 loc) 16.1 kB
'use strict'; 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; }; }(); 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; } var React = global.React || require('react'); var PropTypes = global.PropTypes || require('prop-types'); var window = require('./utils/window'); require('./utils/establish-polyfills'); var scaleEnum = require('./utils/scaleEnum'); var infiniteHelpers = require('./utils/infiniteHelpers'); var _isFinite = require('lodash.isfinite'); var checkProps = require('./utils/checkProps'); var Infinite = function (_React$Component) { _inherits(Infinite, _React$Component); _createClass(Infinite, null, [{ key: 'containerHeightScaleFactor', value: function containerHeightScaleFactor(factor) { if (!_isFinite(factor)) { throw new Error('The scale factor must be a number.'); } return { type: scaleEnum.CONTAINER_HEIGHT_SCALE_FACTOR, amount: factor }; } }]); function Infinite(props) { _classCallCheck(this, Infinite); var _this = _possibleConstructorReturn(this, (Infinite.__proto__ || Object.getPrototypeOf(Infinite)).call(this, props)); _initialiseProps.call(_this); var nextInternalState = _this.recomputeInternalStateFromProps(props); _this.computedProps = nextInternalState.computedProps; _this.utils = nextInternalState.utils; _this.shouldAttachToBottom = props.displayBottomUpwards; var state = nextInternalState.newState; state.scrollTimeout = undefined; state.isScrolling = false; _this.state = state; return _this; } // Properties currently used but which may be // refactored away in the future. // Refs _createClass(Infinite, [{ key: 'componentWillReceiveProps', value: function componentWillReceiveProps(nextProps) { var nextInternalState = this.recomputeInternalStateFromProps(nextProps); this.computedProps = nextInternalState.computedProps; this.utils = nextInternalState.utils; this.setState(nextInternalState.newState); } }, { key: 'componentWillUpdate', value: function componentWillUpdate() { if (this.props.displayBottomUpwards) { this.preservedScrollState = this.utils.getScrollTop() - this.loadingSpinnerHeight; } } }, { key: 'componentDidUpdate', value: function componentDidUpdate(prevProps, prevState) { this.loadingSpinnerHeight = this.utils.getLoadingSpinnerHeight(); if (!prevProps.useWindowAsScrollContainer && this.props.useWindowAsScrollContainer) { this.utils.subscribeToScrollListener(); } if (this.props.displayBottomUpwards) { var lowestScrollTop = this.getLowestPossibleScrollTop(); if (this.shouldAttachToBottom && this.utils.getScrollTop() < lowestScrollTop) { this.utils.setScrollTop(lowestScrollTop); } else if (prevProps.isInfiniteLoading && !this.props.isInfiniteLoading) { this.utils.setScrollTop(this.state.infiniteComputer.getTotalScrollableHeight() - prevState.infiniteComputer.getTotalScrollableHeight() + this.preservedScrollState); } } var hasLoadedMoreChildren = this.state.numberOfChildren !== prevState.numberOfChildren; if (hasLoadedMoreChildren) { var newApertureState = infiniteHelpers.recomputeApertureStateFromOptionsAndScrollTop(this.state, this.utils.getScrollTop()); this.setState(newApertureState); } var isMissingVisibleRows = hasLoadedMoreChildren && !this.hasAllVisibleItems() && !this.state.isInfiniteLoading; if (isMissingVisibleRows) { this.onInfiniteLoad(); } } }, { key: 'componentDidMount', value: function componentDidMount() { this.utils.subscribeToScrollListener(); if (!this.hasAllVisibleItems()) { this.onInfiniteLoad(); } if (this.props.displayBottomUpwards) { var lowestScrollTop = this.getLowestPossibleScrollTop(); if (this.shouldAttachToBottom && this.utils.getScrollTop() < lowestScrollTop) { this.utils.setScrollTop(lowestScrollTop); } } } }, { key: 'componentWillUnmount', value: function componentWillUnmount() { this.utils.unsubscribeFromScrollListener(); } }, { key: 'render', value: function render() { var _this2 = this; var displayables; if (this.state.numberOfChildren > 1) { displayables = this.computedProps.children.slice(this.state.displayIndexStart, this.state.displayIndexEnd + 1); } else { displayables = this.computedProps.children; } var infiniteScrollStyles = {}; if (this.state.isScrolling) { infiniteScrollStyles.pointerEvents = 'none'; } var topSpacerHeight = this.state.infiniteComputer.getTopSpacerHeight(this.state.displayIndexStart), bottomSpacerHeight = this.state.infiniteComputer.getBottomSpacerHeight(this.state.displayIndexEnd); // This asymmetry is due to a reluctance to use CSS to control // the bottom alignment if (this.computedProps.displayBottomUpwards) { var heightDifference = this.computedProps.containerHeight - this.state.infiniteComputer.getTotalScrollableHeight(); if (heightDifference > 0) { topSpacerHeight = heightDifference - this.loadingSpinnerHeight; } } var loadingSpinner = this.computedProps.infiniteLoadBeginEdgeOffset === undefined ? null : React.createElement( 'div', { ref: function ref(c) { _this2.loadingSpinner = c; } }, this.state.isInfiniteLoading ? this.computedProps.loadingSpinnerDelegate : null ); // topSpacer and bottomSpacer take up the amount of space that the // rendered elements would have taken up otherwise return React.createElement( 'div', { className: this.computedProps.className, ref: function ref(c) { _this2.scrollable = c; }, style: this.utils.buildScrollableStyle(), onScroll: this.utils.nodeScrollListener }, React.createElement( 'div', { ref: function ref(c) { _this2.smoothScrollingWrapper = c; }, style: infiniteScrollStyles }, React.createElement('div', { ref: function ref(c) { _this2.topSpacer = c; }, style: infiniteHelpers.buildHeightStyle(topSpacerHeight) }), this.computedProps.displayBottomUpwards && loadingSpinner, displayables, !this.computedProps.displayBottomUpwards && loadingSpinner, React.createElement('div', { ref: function ref(c) { _this2.bottomSpacer = c; }, style: infiniteHelpers.buildHeightStyle(bottomSpacerHeight) }) ) ); } }]); return Infinite; }(React.Component); Infinite.propTypes = { children: PropTypes.any, handleScroll: PropTypes.func, // preloadBatchSize causes updates only to // happen each preloadBatchSize pixels of scrolling. // Set a larger number to cause fewer updates to the // element list. preloadBatchSize: PropTypes.oneOfType([PropTypes.number, PropTypes.shape({ type: PropTypes.oneOf(['containerHeightScaleFactor']).isRequired, amount: PropTypes.number.isRequired })]), // preloadAdditionalHeight determines how much of the // list above and below the container is preloaded even // when it is not currently visible to the user. In the // regular scroll implementation, preloadAdditionalHeight // is equal to the entire height of the list. preloadAdditionalHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.shape({ type: PropTypes.oneOf(['containerHeightScaleFactor']).isRequired, amount: PropTypes.number.isRequired })]), // page to screen ratio // The provided elementHeight can be either // 1. a constant: all elements are the same height // 2. an array containing the height of each element elementHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.arrayOf(PropTypes.number)]).isRequired, // This is the total height of the visible window. One // of containerHeight: PropTypes.number, useWindowAsScrollContainer: PropTypes.bool, displayBottomUpwards: PropTypes.bool.isRequired, infiniteLoadBeginEdgeOffset: PropTypes.number, onInfiniteLoad: PropTypes.func, loadingSpinnerDelegate: PropTypes.node, isInfiniteLoading: PropTypes.bool, timeScrollStateLastsForAfterUserScrolls: PropTypes.number, className: PropTypes.string, styles: PropTypes.shape({ scrollableStyle: PropTypes.object }).isRequired }; Infinite.defaultProps = { handleScroll: function handleScroll() {}, useWindowAsScrollContainer: false, onInfiniteLoad: function onInfiniteLoad() {}, loadingSpinnerDelegate: React.createElement('div', null), displayBottomUpwards: false, isInfiniteLoading: false, timeScrollStateLastsForAfterUserScrolls: 150, className: '', styles: {} }; var _initialiseProps = function _initialiseProps() { var _this3 = this; this.shouldAttachToBottom = false; this.preservedScrollState = 0; this.loadingSpinnerHeight = 0; this.generateComputedUtilityFunctions = function (props) { var utilities = {}; utilities.getLoadingSpinnerHeight = function () { var loadingSpinnerHeight = 0; if (_this3.loadingSpinner) { loadingSpinnerHeight = _this3.loadingSpinner.offsetHeight || 0; } return loadingSpinnerHeight; }; if (props.useWindowAsScrollContainer) { utilities.subscribeToScrollListener = function () { window.addEventListener('scroll', _this3.infiniteHandleScroll); }; utilities.unsubscribeFromScrollListener = function () { window.removeEventListener('scroll', _this3.infiniteHandleScroll); }; utilities.nodeScrollListener = function () {}; utilities.getScrollTop = function () { return window.pageYOffset; }; utilities.setScrollTop = function (top) { window.scroll(window.pageXOffset, top); }; utilities.scrollShouldBeIgnored = function () { return false; }; utilities.buildScrollableStyle = function () { return {}; }; } else { utilities.subscribeToScrollListener = function () {}; utilities.unsubscribeFromScrollListener = function () {}; utilities.nodeScrollListener = _this3.infiniteHandleScroll; utilities.getScrollTop = function () { return _this3.scrollable ? _this3.scrollable.scrollTop : 0; }; utilities.setScrollTop = function (top) { if (_this3.scrollable) { _this3.scrollable.scrollTop = top; } }; utilities.scrollShouldBeIgnored = function (event) { return event.target !== _this3.scrollable; }; utilities.buildScrollableStyle = function () { return Object.assign({}, { height: _this3.computedProps.containerHeight, overflowX: 'hidden', overflowY: 'scroll', WebkitOverflowScrolling: 'touch' }, _this3.computedProps.styles.scrollableStyle || {}); }; } return utilities; }; this.recomputeInternalStateFromProps = function (props) { checkProps(props); var computedProps = infiniteHelpers.generateComputedProps(props); var utils = _this3.generateComputedUtilityFunctions(props); var newState = {}; newState.numberOfChildren = React.Children.count(computedProps.children); newState.infiniteComputer = infiniteHelpers.createInfiniteComputer(computedProps.elementHeight, computedProps.children); if (computedProps.isInfiniteLoading !== undefined) { newState.isInfiniteLoading = computedProps.isInfiniteLoading; } newState.preloadBatchSize = computedProps.preloadBatchSize; newState.preloadAdditionalHeight = computedProps.preloadAdditionalHeight; newState = Object.assign(newState, infiniteHelpers.recomputeApertureStateFromOptionsAndScrollTop(newState, utils.getScrollTop())); return { computedProps: computedProps, utils: utils, newState: newState }; }; this.infiniteHandleScroll = function (e) { if (_this3.utils.scrollShouldBeIgnored(e)) { return; } _this3.computedProps.handleScroll(_this3.scrollable); _this3.handleScroll(_this3.utils.getScrollTop()); }; this.manageScrollTimeouts = function () { // Maintains a series of timeouts to set this.state.isScrolling // to be true when the element is scrolling. if (_this3.state.scrollTimeout) { clearTimeout(_this3.state.scrollTimeout); } var that = _this3, scrollTimeout = setTimeout(function () { that.setState({ isScrolling: false, scrollTimeout: undefined }); }, _this3.computedProps.timeScrollStateLastsForAfterUserScrolls); _this3.setState({ isScrolling: true, scrollTimeout: scrollTimeout }); }; this.getLowestPossibleScrollTop = function () { return _this3.state.infiniteComputer.getTotalScrollableHeight() - _this3.computedProps.containerHeight; }; this.hasAllVisibleItems = function () { return !(_isFinite(_this3.computedProps.infiniteLoadBeginEdgeOffset) && _this3.state.infiniteComputer.getTotalScrollableHeight() < _this3.computedProps.containerHeight); }; this.passedEdgeForInfiniteScroll = function (scrollTop) { var edgeOffset = _this3.computedProps.infiniteLoadBeginEdgeOffset; if (typeof edgeOffset !== 'number') { return false; } if (_this3.computedProps.displayBottomUpwards) { return !_this3.shouldAttachToBottom && scrollTop < edgeOffset; } else { return scrollTop > _this3.state.infiniteComputer.getTotalScrollableHeight() - _this3.computedProps.containerHeight - edgeOffset; } }; this.onInfiniteLoad = function () { _this3.setState({ isInfiniteLoading: true }); _this3.computedProps.onInfiniteLoad(); }; this.handleScroll = function (scrollTop) { _this3.shouldAttachToBottom = _this3.computedProps.displayBottomUpwards && scrollTop >= _this3.getLowestPossibleScrollTop(); _this3.manageScrollTimeouts(); var newApertureState = infiniteHelpers.recomputeApertureStateFromOptionsAndScrollTop(_this3.state, scrollTop); if (_this3.passedEdgeForInfiniteScroll(scrollTop) && !_this3.state.isInfiniteLoading) { _this3.setState(Object.assign({}, newApertureState)); _this3.onInfiniteLoad(); } else { _this3.setState(newApertureState); } }; }; module.exports = Infinite; global.Infinite = Infinite;