UNPKG

react-infinite

Version:

A browser-ready efficient scrolling container based on UITableView

769 lines (649 loc) 26.3 kB
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Infinite = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ (function (global){ var React = global.React || require('react'), _isArray = require('lodash.isarray'), _isFinite = require('lodash.isfinite'), ConstantInfiniteComputer = require('./computers/constant_infinite_computer.js'), ArrayInfiniteComputer = require('./computers/array_infinite_computer.js'); var Infinite = React.createClass({displayName: "Infinite", propTypes: { handleScroll: React.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: React.PropTypes.number, // 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: React.PropTypes.number, // 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: React.PropTypes.oneOfType([ React.PropTypes.number, React.PropTypes.arrayOf(React.PropTypes.number) ]).isRequired, // This is the total height of the visible window. containerHeight: React.PropTypes.number.isRequired, infiniteLoadBeginBottomOffset: React.PropTypes.number, onInfiniteLoad: React.PropTypes.func, loadingSpinnerDelegate: React.PropTypes.node, isInfiniteLoading: React.PropTypes.bool, timeScrollStateLastsForAfterUserScrolls: React.PropTypes.number, className: React.PropTypes.string }, getDefaultProps:function() { return { handleScroll: function() {}, loadingSpinnerDelegate: React.createElement("div", null), onInfiniteLoad: function() {}, isInfiniteLoading: false, timeScrollStateLastsForAfterUserScrolls: 150 }; }, // automatic adjust to scroll direction // give spinner a ReactCSSTransitionGroup getInitialState:function() { var computer = this.createInfiniteComputer(this.props.elementHeight, this.props.children); var preloadBatchSize = this.props.preloadBatchSize ? this.props.preloadBatchSize : this.props.containerHeight / 2, preloadAdditionalHeight = this.props.preloadAdditionalHeight ? this.props.preloadAdditionalHeight : this.props.containerHeight; return { infiniteComputer: computer, numberOfChildren: this.props.children.length, scrollableHeight: undefined, displayIndexStart: 0, displayIndexEnd: computer.getDisplayIndexEnd( preloadBatchSize + preloadAdditionalHeight ), isInfiniteLoading: false, currentScrollTop: undefined, previousScrollTop: undefined, preloadBatchSize: preloadBatchSize, preloadAdditionalHeight: preloadAdditionalHeight, scrollTimeout: undefined, isScrolling: false }; }, createInfiniteComputer:function(data, children) { var computer; var numberOfChildren = React.Children.count(children); if (_isFinite(data)) { computer = new ConstantInfiniteComputer(data, numberOfChildren); } else if (_isArray(data)) { computer = new ArrayInfiniteComputer(data, numberOfChildren); } else { throw new Error("You must provide either a number or an array of numbers as the elementHeight prop."); } return computer; }, componentWillReceiveProps:function(nextProps) { var that = this, newStateObject = {}; // TODO: more efficient elementHeight change detection newStateObject.infiniteComputer = this.createInfiniteComputer( nextProps.elementHeight, nextProps.children ); if (nextProps.isInfiniteLoading !== undefined) { newStateObject.isInfiniteLoading = nextProps.isInfiniteLoading; } var nextPBS = nextProps.preloadBatchSize; newStateObject.preloadBatchSize = nextPBS ? nextPBS : nextProps.containerHeight / 2; var nextPAH = nextProps.preloadAdditionalHeight; newStateObject.preloadAdditionalHeight = nextPAH ? nextPAH : nextProps.containerHeight; this.setState(newStateObject, function() { that.setStateFromScrollTop(that.getScrollTop()); }); }, componentDidUpdate:function(prevProps, prevState) { if (React.Children.count(this.props.children) !== prevProps.children.length) { this.setStateFromScrollTop(this.getScrollTop()); } }, componentWillMount:function() { if (_isArray(this.props.elementHeight)) { if (this.props.children.length !== this.props.elementHeight.length) { throw new Error("There must be as many values provided in the elementHeight prop as there are children.") } } }, componentDidMount:function() { var that = this; this.setState({ scrollableHeight: this.refs.scrollable.getDOMNode().clientHeight, currentScrollTop: this.getScrollTop(), previousScrollTop: this.getScrollTop() }); }, getScrollTop:function() { return this.refs.scrollable.getDOMNode().scrollTop; }, isScrollingDown:function() { return this.state.previousScrollTop < this.state.currentScrollTop; }, // Given the scrollTop of the container, computes the state the // component should be in. The goal is to abstract all of this // from any actual representation in the DOM. // The window is the block with any preloadAdditionalHeight // added to it. setStateFromScrollTop:function(scrollTop) { var blockNumber = Math.floor(scrollTop / this.state.preloadBatchSize), blockStart = this.state.preloadBatchSize * blockNumber, blockEnd = blockStart + this.state.preloadBatchSize, windowTop = Math.max(0, blockStart - this.state.preloadAdditionalHeight), windowBottom = Math.min(this.state.infiniteComputer.getTotalScrollableHeight(), blockEnd + this.state.preloadAdditionalHeight) this.setState({ displayIndexStart: this.state.infiniteComputer.getDisplayIndexStart(windowTop), displayIndexEnd: this.state.infiniteComputer.getDisplayIndexEnd(windowBottom), currentScrollTop: scrollTop, previousScrollTop: this.state.currentScrollTop }); }, infiniteHandleScroll:function(e) { this.props.handleScroll(this.refs.scrollable.getDOMNode()); this.handleScroll(e.target.scrollTop); }, manageScrollTimeouts:function() { // Maintains a series of timeouts to set this.state.isScrolling // to be true when the element is scrolling. if (this.state.scrollTimeout) { clearTimeout(this.state.scrollTimeout); } var that = this, scrollTimeout = setTimeout(function() { that.setState({ isScrolling: false, scrollTimeout: undefined }) }, this.props.timeScrollStateLastsForAfterUserScrolls); this.setState({ isScrolling: true, scrollTimeout: scrollTimeout }); }, handleScroll:function(scrollTop) { var that = this; this.manageScrollTimeouts(); this.setStateFromScrollTop(scrollTop); var infiniteScrollBottomLimit = scrollTop > (this.state.infiniteComputer.getTotalScrollableHeight() - this.props.containerHeight - this.props.infiniteLoadBeginBottomOffset); if (infiniteScrollBottomLimit && !this.state.isInfiniteLoading) { this.setState({ isInfiniteLoading: true }); this.props.onInfiniteLoad(); } }, // Helpers for React styles. buildScrollableStyle:function() { return { height: this.props.containerHeight, overflowX: 'hidden', overflowY: 'scroll' }; }, buildHeightStyle:function(height) { return { width: '100%', height: Math.ceil(height) + 'px' }; }, render:function() { var that = this; var displayables = this.props.children.slice(this.state.displayIndexStart, this.state.displayIndexEnd); 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); // topSpacer and bottomSpacer take up the amount of space that the // rendered elements would have taken up otherwise return React.createElement("div", {className: this.props.className ? this.props.className : '', ref: "scrollable", style: this.buildScrollableStyle(), onScroll: this.infiniteHandleScroll}, React.createElement("div", {ref: "smoothScrollingWrapper", style: infiniteScrollStyles}, React.createElement("div", {ref: "topSpacer", style: this.buildHeightStyle(topSpacerHeight)}), displayables, React.createElement("div", {ref: "bottomSpacer", style: this.buildHeightStyle(bottomSpacerHeight)}), React.createElement("div", {ref: "loadingSpinner"}, this.state.isInfiniteLoading ? this.props.loadingSpinnerDelegate : null ) ) ); } }); module.exports = Infinite; global.Infinite = Infinite; }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{"./computers/array_infinite_computer.js":5,"./computers/constant_infinite_computer.js":6,"lodash.isarray":2,"lodash.isfinite":3,"react":undefined}],2:[function(require,module,exports){ /** * lodash 3.0.0 (Custom Build) <https://lodash.com/> * Build: `lodash modern modularize exports="npm" -o ./` * Copyright 2012-2015 The Dojo Foundation <http://dojofoundation.org/> * Based on Underscore.js 1.7.0 <http://underscorejs.org/LICENSE> * Copyright 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors * Available under MIT license <https://lodash.com/license> */ /** `Object#toString` result references. */ var arrayTag = '[object Array]', funcTag = '[object Function]'; /** Used to detect host constructors (Safari > 5). */ var reHostCtor = /^\[object .+?Constructor\]$/; /** * Used to match `RegExp` special characters. * See this [article on `RegExp` characters](http://www.regular-expressions.info/characters.html#special) * for more details. */ var reRegExpChars = /[.*+?^${}()|[\]\/\\]/g, reHasRegExpChars = RegExp(reRegExpChars.source); /** * Converts `value` to a string if it is not one. An empty string is returned * for `null` or `undefined` values. * * @private * @param {*} value The value to process. * @returns {string} Returns the string. */ function baseToString(value) { if (typeof value == 'string') { return value; } return value == null ? '' : (value + ''); } /** * Checks if `value` is object-like. * * @private * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is object-like, else `false`. */ function isObjectLike(value) { return (value && typeof value == 'object') || false; } /** Used for native method references. */ var objectProto = Object.prototype; /** Used to resolve the decompiled source of functions. */ var fnToString = Function.prototype.toString; /** * Used to resolve the `toStringTag` of values. * See the [ES spec](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-object.prototype.tostring) * for more details. */ var objToString = objectProto.toString; /** Used to detect if a method is native. */ var reNative = RegExp('^' + escapeRegExp(objToString) .replace(/toString|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$' ); /* Native method references for those with the same name as other `lodash` methods. */ var nativeIsArray = isNative(nativeIsArray = Array.isArray) && nativeIsArray; /** * Used as the maximum length of an array-like value. * See the [ES spec](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength) * for more details. */ var MAX_SAFE_INTEGER = Math.pow(2, 53) - 1; /** * Checks if `value` is a valid array-like length. * * @private * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a valid length, else `false`. */ function isLength(value) { return typeof value == 'number' && value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER; } /** * Checks if `value` is classified as an `Array` object. * * @static * @memberOf _ * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`. * @example * * _.isArray([1, 2, 3]); * // => true * * (function() { return _.isArray(arguments); })(); * // => false */ var isArray = nativeIsArray || function(value) { return (isObjectLike(value) && isLength(value.length) && objToString.call(value) == arrayTag) || false; }; /** * Checks if `value` is a native function. * * @static * @memberOf _ * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a native function, else `false`. * @example * * _.isNative(Array.prototype.push); * // => true * * _.isNative(_); * // => false */ function isNative(value) { if (value == null) { return false; } if (objToString.call(value) == funcTag) { return reNative.test(fnToString.call(value)); } return (isObjectLike(value) && reHostCtor.test(value)) || false; } /** * Escapes the `RegExp` special characters "\", "^", "$", ".", "|", "?", "*", * "+", "(", ")", "[", "]", "{" and "}" in `string`. * * @static * @memberOf _ * @category String * @param {string} [string=''] The string to escape. * @returns {string} Returns the escaped string. * @example * * _.escapeRegExp('[lodash](https://lodash.com/)'); * // => '\[lodash\]\(https://lodash\.com/\)' */ function escapeRegExp(string) { string = baseToString(string); return (string && reHasRegExpChars.test(string)) ? string.replace(reRegExpChars, '\\$&') : string; } module.exports = isArray; },{}],3:[function(require,module,exports){ (function (global){ /** * lodash 3.0.0 (Custom Build) <https://lodash.com/> * Build: `lodash modern modularize exports="npm" -o ./` * Copyright 2012-2015 The Dojo Foundation <http://dojofoundation.org/> * Based on Underscore.js 1.7.0 <http://underscorejs.org/LICENSE> * Copyright 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors * Available under MIT license <https://lodash.com/license> */ var isNative = require('lodash.isnative'); /* Native method references for those with the same name as other `lodash` methods. */ var nativeIsFinite = global.isFinite, nativeNumIsFinite = isNative(nativeNumIsFinite = Number.isFinite) && nativeNumIsFinite; /** * Checks if `value` is a finite primitive number. * * **Note:** This method is based on ES `Number.isFinite`. See the * [ES spec](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-number.isfinite) * for more details. * * @static * @memberOf _ * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a finite number, else `false`. * @example * * _.isFinite(10); * // => true * * _.isFinite('10'); * // => false * * _.isFinite(true); * // => false * * _.isFinite(Object(10)); * // => false * * _.isFinite(Infinity); * // => false */ var isFinite = nativeNumIsFinite || function(value) { return typeof value == 'number' && nativeIsFinite(value); }; module.exports = isFinite; }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{"lodash.isnative":4}],4:[function(require,module,exports){ /** * lodash 3.0.0 (Custom Build) <https://lodash.com/> * Build: `lodash modern modularize exports="npm" -o ./` * Copyright 2012-2015 The Dojo Foundation <http://dojofoundation.org/> * Based on Underscore.js 1.7.0 <http://underscorejs.org/LICENSE> * Copyright 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors * Available under MIT license <https://lodash.com/license> */ /** `Object#toString` result references. */ var funcTag = '[object Function]'; /** Used to detect host constructors (Safari > 5). */ var reHostCtor = /^\[object .+?Constructor\]$/; /** * Used to match `RegExp` special characters. * See this [article on `RegExp` characters](http://www.regular-expressions.info/characters.html#special) * for more details. */ var reRegExpChars = /[.*+?^${}()|[\]\/\\]/g, reHasRegExpChars = RegExp(reRegExpChars.source); /** * Converts `value` to a string if it is not one. An empty string is returned * for `null` or `undefined` values. * * @private * @param {*} value The value to process. * @returns {string} Returns the string. */ function baseToString(value) { if (typeof value == 'string') { return value; } return value == null ? '' : (value + ''); } /** * Checks if `value` is object-like. * * @private * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is object-like, else `false`. */ function isObjectLike(value) { return (value && typeof value == 'object') || false; } /** Used for native method references. */ var objectProto = Object.prototype; /** Used to resolve the decompiled source of functions. */ var fnToString = Function.prototype.toString; /** * Used to resolve the `toStringTag` of values. * See the [ES spec](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-object.prototype.tostring) * for more details. */ var objToString = objectProto.toString; /** Used to detect if a method is native. */ var reNative = RegExp('^' + escapeRegExp(objToString) .replace(/toString|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$' ); /** * Checks if `value` is a native function. * * @static * @memberOf _ * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a native function, else `false`. * @example * * _.isNative(Array.prototype.push); * // => true * * _.isNative(_); * // => false */ function isNative(value) { if (value == null) { return false; } if (objToString.call(value) == funcTag) { return reNative.test(fnToString.call(value)); } return (isObjectLike(value) && reHostCtor.test(value)) || false; } /** * Escapes the `RegExp` special characters "\", "^", "$", ".", "|", "?", "*", * "+", "(", ")", "[", "]", "{" and "}" in `string`. * * @static * @memberOf _ * @category String * @param {string} [string=''] The string to escape. * @returns {string} Returns the escaped string. * @example * * _.escapeRegExp('[lodash](https://lodash.com/)'); * // => '\[lodash\]\(https://lodash\.com/\)' */ function escapeRegExp(string) { string = baseToString(string); return (string && reHasRegExpChars.test(string)) ? string.replace(reRegExpChars, '\\$&') : string; } module.exports = isNative; },{}],5:[function(require,module,exports){ var InfiniteComputer = require('./infinite_computer.js'), bs = require('../utils/binary_index_search.js'); for(var InfiniteComputer____Key in InfiniteComputer){if(InfiniteComputer.hasOwnProperty(InfiniteComputer____Key)){ArrayInfiniteComputer[InfiniteComputer____Key]=InfiniteComputer[InfiniteComputer____Key];}}var ____SuperProtoOfInfiniteComputer=InfiniteComputer===null?null:InfiniteComputer.prototype;ArrayInfiniteComputer.prototype=Object.create(____SuperProtoOfInfiniteComputer);ArrayInfiniteComputer.prototype.constructor=ArrayInfiniteComputer;ArrayInfiniteComputer.__superConstructor__=InfiniteComputer; function ArrayInfiniteComputer(heightData, numberOfChildren) {"use strict"; InfiniteComputer.call(this,heightData, numberOfChildren); this.prefixHeightData = this.heightData.reduce(function(acc, next) { if (acc.length === 0) { return [next]; } else { acc.push(acc[acc.length - 1] + next); return acc; } }, []); } ArrayInfiniteComputer.prototype.getTotalScrollableHeight=function() {"use strict"; var length = this.prefixHeightData.length; return length === 0 ? 0 : this.prefixHeightData[length - 1]; }; ArrayInfiniteComputer.prototype.getDisplayIndexStart=function(windowTop) {"use strict"; return bs.binaryIndexSearch(this.prefixHeightData, windowTop, bs.opts.CLOSEST_HIGHER); }; ArrayInfiniteComputer.prototype.getDisplayIndexEnd=function(windowBottom) {"use strict"; return bs.binaryIndexSearch(this.prefixHeightData, windowBottom, bs.opts.CLOSEST_HIGHER) + 1; }; ArrayInfiniteComputer.prototype.getTopSpacerHeight=function(displayIndexStart) {"use strict"; var previous = displayIndexStart - 1; return previous < 0 ? 0 : this.prefixHeightData[previous]; }; ArrayInfiniteComputer.prototype.getBottomSpacerHeight=function(displayIndexEnd) {"use strict"; var previous = displayIndexEnd - 1; if (displayIndexEnd === 0) { return this.getTotalScrollableHeight(); } else if (displayIndexEnd >= this.prefixHeightData.length) { return 0; } else { return this.getTotalScrollableHeight() - this.prefixHeightData[previous]; } }; module.exports = ArrayInfiniteComputer; },{"../utils/binary_index_search.js":8,"./infinite_computer.js":7}],6:[function(require,module,exports){ var InfiniteComputer = require('./infinite_computer.js'); for(var InfiniteComputer____Key in InfiniteComputer){if(InfiniteComputer.hasOwnProperty(InfiniteComputer____Key)){ConstantInfiniteComputer[InfiniteComputer____Key]=InfiniteComputer[InfiniteComputer____Key];}}var ____SuperProtoOfInfiniteComputer=InfiniteComputer===null?null:InfiniteComputer.prototype;ConstantInfiniteComputer.prototype=Object.create(____SuperProtoOfInfiniteComputer);ConstantInfiniteComputer.prototype.constructor=ConstantInfiniteComputer;ConstantInfiniteComputer.__superConstructor__=InfiniteComputer;function ConstantInfiniteComputer(){"use strict";if(InfiniteComputer!==null){InfiniteComputer.apply(this,arguments);}} ConstantInfiniteComputer.prototype.getTotalScrollableHeight=function() {"use strict"; return this.heightData * this.numberOfChildren; }; ConstantInfiniteComputer.prototype.getDisplayIndexStart=function(windowTop) {"use strict"; return Math.floor(windowTop / this.heightData); }; ConstantInfiniteComputer.prototype.getDisplayIndexEnd=function(windowBottom) {"use strict"; return Math.ceil(windowBottom / this.heightData); }; ConstantInfiniteComputer.prototype.getTopSpacerHeight=function(displayIndexStart) {"use strict"; return displayIndexStart * this.heightData; }; ConstantInfiniteComputer.prototype.getBottomSpacerHeight=function(displayIndexEnd) {"use strict"; return Math.max(0, (this.numberOfChildren - displayIndexEnd) * this.heightData); }; module.exports = ConstantInfiniteComputer; },{"./infinite_computer.js":7}],7:[function(require,module,exports){ // An infinite computer must be able to do the following things: // 1. getTotalScrollableHeight() // 2. getDisplayIndexStart() // 3. getDisplayIndexEnd() function InfiniteComputer(heightData, numberOfChildren) {"use strict"; this.heightData = heightData; this.numberOfChildren = numberOfChildren; } InfiniteComputer.prototype.getTotalScrollableHeight=function() {"use strict"; throw new Error("getTotalScrollableHeight not implemented."); }; InfiniteComputer.prototype.getDisplayIndexStart=function(windowTop) {"use strict"; throw new Error("getDisplayIndexStart not implemented."); }; InfiniteComputer.prototype.getDisplayIndexEnd=function(windowBottom) {"use strict"; throw new Error("getDisplayIndexEnd not implemented."); }; // These are helper methods, and can be calculated from // the above details. InfiniteComputer.prototype.getTopSpacerHeight=function(displayIndexStart) {"use strict"; throw new Error("getTopSpacerHeight not implemented."); }; InfiniteComputer.prototype.getBottomSpacerHeight=function(displayIndexEnd) {"use strict"; throw new Error("getBottomSpacerHeight not implemented."); }; module.exports = InfiniteComputer; },{}],8:[function(require,module,exports){ var opts = { CLOSEST_LOWER: 1, CLOSEST_HIGHER: 2 } var binaryIndexSearch = function(array, item, opt) { var index; var high = array.length - 1, low = 0, middle, middleItem; while(low <= high) { middle = low + Math.floor((high - low) / 2); middleItem = array[middle]; if (middleItem === item) { return middle; } else if (middleItem < item) { low = middle + 1; } else if (middleItem > item) { high = middle - 1; } } if (opt === opts.CLOSEST_LOWER && low > 0) { index = low - 1; } else if (opt === opts.CLOSEST_HIGHER && high < array.length - 1) { index = high + 1; } return index; }; module.exports = { binaryIndexSearch: binaryIndexSearch, opts: opts } },{}]},{},[1])(1) });