UNPKG

hyperlist

Version:

A performant virtual scrolling list library

499 lines (397 loc) 17 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.HyperList = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(_dereq_,module,exports){ 'use strict'; // Default configuration. Object.defineProperty(exports, "__esModule", { value: true }); 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 _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var defaultConfig = { width: '100%', height: '100%' // Check for valid number. };var isNumber = function isNumber(input) { return Number(input) === Number(input); }; // Add a class to an element. var addClass = 'classList' in document.documentElement ? function (element, className) { element.classList.add(className); } : function (element, className) { var oldClass = element.getAttribute('class') || ''; element.setAttribute('class', oldClass + ' ' + className); }; /** * Creates a HyperList instance that virtually scrolls very large amounts of * data effortlessly. */ var HyperList = function () { _createClass(HyperList, null, [{ key: 'create', value: function create(element, userProvidedConfig) { return new HyperList(element, userProvidedConfig); } /** * Merge given css style on an element * @param {DOMElement} element * @param {Object} style */ }, { key: 'mergeStyle', value: function mergeStyle(element, style) { for (var i in style) { if (element.style[i] !== style[i]) { element.style[i] = style[i]; } } } }, { key: 'getMaxBrowserHeight', value: function getMaxBrowserHeight() { // Create two elements, the wrapper is `1px` tall and is transparent and // positioned at the top of the page. Inside that is an element that gets // set to 1 billion pixels. Then reads the max height the browser can // calculate. var wrapper = document.createElement('div'); var fixture = document.createElement('div'); // As said above, these values get set to put the fixture elements into the // right visual state. HyperList.mergeStyle(wrapper, { position: 'absolute', height: '1px', opacity: 0 }); HyperList.mergeStyle(fixture, { height: '1e7px' }); // Add the fixture into the wrapper element. wrapper.appendChild(fixture); // Apply to the page, the values won't kick in unless this is attached. document.body.appendChild(wrapper); // Get the maximum element height in pixels. var maxElementHeight = fixture.offsetHeight; // Remove the element immediately after reading the value. document.body.removeChild(wrapper); return maxElementHeight; } }]); function HyperList(element, userProvidedConfig) { var _this = this; _classCallCheck(this, HyperList); this._config = {}; this._lastRepaint = null; this._maxElementHeight = HyperList.getMaxBrowserHeight(); this.refresh(element, userProvidedConfig); var config = this._config; // Create internal render loop. var render = function render() { var scrollTop = _this._getScrollPosition(); var lastRepaint = _this._lastRepaint; _this._renderAnimationFrame = window.requestAnimationFrame(render); if (scrollTop === lastRepaint) { return; } var diff = lastRepaint ? scrollTop - lastRepaint : 0; if (!lastRepaint || diff < 0 || diff > _this._averageHeight) { var rendered = _this._renderChunk(); _this._lastRepaint = scrollTop; if (rendered !== false && typeof config.afterRender === 'function') { config.afterRender(); } } }; render(); } _createClass(HyperList, [{ key: 'destroy', value: function destroy() { window.cancelAnimationFrame(this._renderAnimationFrame); } }, { key: 'refresh', value: function refresh(element, userProvidedConfig) { var _scrollerStyle; Object.assign(this._config, defaultConfig, userProvidedConfig); if (!element || element.nodeType !== 1) { throw new Error('HyperList requires a valid DOM Node container'); } this._element = element; var config = this._config; var scroller = this._scroller || config.scroller || document.createElement(config.scrollerTagName || 'tr'); // Default configuration option `useFragment` to `true`. if (typeof config.useFragment !== 'boolean') { this._config.useFragment = true; } if (!config.generate) { throw new Error('Missing required `generate` function'); } if (!isNumber(config.total)) { throw new Error('Invalid required `total` value, expected number'); } if (!Array.isArray(config.itemHeight) && !isNumber(config.itemHeight)) { throw new Error('\n Invalid required `itemHeight` value, expected number or array\n '.trim()); } else if (isNumber(config.itemHeight)) { this._itemHeights = Array(config.total).fill(config.itemHeight); } else { this._itemHeights = config.itemHeight; } // Width and height should be coerced to string representations. Either in // `%` or `px`. Object.keys(defaultConfig).filter(function (prop) { return prop in config; }).forEach(function (prop) { var value = config[prop]; var isValueNumber = isNumber(value); if (value && typeof value !== 'string' && typeof value !== 'number') { var msg = 'Invalid optional `' + prop + '`, expected string or number'; throw new Error(msg); } else if (isValueNumber) { config[prop] = value + 'px'; } }); var isHoriz = Boolean(config.horizontal); var value = config[isHoriz ? 'width' : 'height']; if (value) { var isValueNumber = isNumber(value); var isValuePercent = isValueNumber ? false : value.slice(-1) === '%'; // Compute the containerHeight as number var numberValue = isValueNumber ? value : parseInt(value.replace(/px|%/, ''), 10); var innerSize = window[isHoriz ? 'innerWidth' : 'innerHeight']; if (isValuePercent) { this._containerSize = innerSize * numberValue / 100; } else { this._containerSize = isNumber(value) ? value : numberValue; } } var scrollContainer = config.scrollContainer; var scrollerHeight = config.itemHeight * config.total; var maxElementHeight = this._maxElementHeight; if (scrollerHeight > maxElementHeight) { console.warn(['HyperList: The maximum element height', maxElementHeight + 'px has', 'been exceeded; please reduce your item height.'].join(' ')); } // Decorate the container element with styles that will match // the user supplied configuration. var elementStyle = { width: '' + config.width, height: scrollContainer ? scrollerHeight + 'px' : '' + config.height, overflow: scrollContainer ? 'none' : 'auto', position: 'relative' }; HyperList.mergeStyle(element, elementStyle); if (scrollContainer) { HyperList.mergeStyle(config.scrollContainer, { overflow: 'auto' }); } var scrollerStyle = (_scrollerStyle = { opacity: '0', position: 'absolute' }, _defineProperty(_scrollerStyle, isHoriz ? 'height' : 'width', '1px'), _defineProperty(_scrollerStyle, isHoriz ? 'width' : 'height', scrollerHeight + 'px'), _scrollerStyle); HyperList.mergeStyle(scroller, scrollerStyle); // Only append the scroller element once. if (!this._scroller) { element.appendChild(scroller); } var padding = this._computeScrollPadding(); this._scrollPaddingBottom = padding.bottom; this._scrollPaddingTop = padding.top; // Set the scroller instance. this._scroller = scroller; this._scrollHeight = this._computeScrollHeight(); // Reuse the item positions if refreshed, otherwise set to empty array. this._itemPositions = this._itemPositions || Array(config.total).fill(0); // Each index in the array should represent the position in the DOM. this._computePositions(0); // Render after refreshing. Force render if we're calling refresh manually. this._renderChunk(this._lastRepaint !== null); if (typeof config.afterRender === 'function') { config.afterRender(); } } }, { key: '_getRow', value: function _getRow(i) { var config = this._config; var item = config.generate(i); var height = item.height; if (height !== undefined && isNumber(height)) { item = item.element; // The height isn't the same as predicted, compute positions again if (height !== this._itemHeights[i]) { this._itemHeights[i] = height; this._computePositions(i); this._scrollHeight = this._computeScrollHeight(i); } } else { height = this._itemHeights[i]; } if (!item || item.nodeType !== 1) { throw new Error('Generator did not return a DOM Node for index: ' + i); } addClass(item, config.rowClassName || 'vrow'); var top = this._itemPositions[i] + this._scrollPaddingTop; HyperList.mergeStyle(item, _defineProperty({ position: 'absolute' }, config.horizontal ? 'left' : 'top', top + 'px')); return item; } }, { key: '_getScrollPosition', value: function _getScrollPosition() { var config = this._config; if (typeof config.overrideScrollPosition === 'function') { return config.overrideScrollPosition(); } return this._element[config.horizontal ? 'scrollLeft' : 'scrollTop']; } }, { key: '_renderChunk', value: function _renderChunk(force) { var config = this._config; var element = this._element; var scrollTop = this._getScrollPosition(); var total = config.total; var from = config.reverse ? this._getReverseFrom(scrollTop) : this._getFrom(scrollTop) - 1; if (from < 0 || from - this._screenItemsLen < 0) { from = 0; } if (!force && this._lastFrom === from) { return false; } this._lastFrom = from; var to = from + this._cachedItemsLen; if (to > total || to + this._cachedItemsLen > total) { to = total; } // Append all the new rows in a document fragment that we will later append // to the parent node var fragment = config.useFragment ? document.createDocumentFragment() : [] // Sometimes you'll pass fake elements to this tool and Fragments require // real elements. // The element that forces the container to scroll. ;var scroller = this._scroller; // Keep the scroller in the list of children. fragment[config.useFragment ? 'appendChild' : 'push'](scroller); for (var i = from; i < to; i++) { var row = this._getRow(i); fragment[config.useFragment ? 'appendChild' : 'push'](row); } if (config.applyPatch) { return config.applyPatch(element, fragment); } element.innerHTML = ''; element.appendChild(fragment); } }, { key: '_computePositions', value: function _computePositions() { var from = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1; var config = this._config; var total = config.total; var reverse = config.reverse; if (from < 1 && !reverse) { from = 1; } for (var i = from; i < total; i++) { if (reverse) { if (i === 0) { this._itemPositions[0] = this._scrollHeight - this._itemHeights[0]; } else { this._itemPositions[i] = this._itemPositions[i - 1] - this._itemHeights[i]; } } else { this._itemPositions[i] = this._itemHeights[i - 1] + this._itemPositions[i - 1]; } } } }, { key: '_computeScrollHeight', value: function _computeScrollHeight() { var _HyperList$mergeStyle2, _this2 = this; var config = this._config; var isHoriz = Boolean(config.horizontal); var total = config.total; var scrollHeight = this._itemHeights.reduce(function (a, b) { return a + b; }, 0) + this._scrollPaddingBottom + this._scrollPaddingTop; HyperList.mergeStyle(this._scroller, (_HyperList$mergeStyle2 = { opacity: 0, position: 'absolute', top: '0px' }, _defineProperty(_HyperList$mergeStyle2, isHoriz ? 'height' : 'width', '1px'), _defineProperty(_HyperList$mergeStyle2, isHoriz ? 'width' : 'height', scrollHeight + 'px'), _HyperList$mergeStyle2)); // Calculate the height median var sortedItemHeights = this._itemHeights.slice(0).sort(function (a, b) { return a - b; }); var middle = Math.floor(total / 2); var averageHeight = total % 2 === 0 ? (sortedItemHeights[middle] + sortedItemHeights[middle - 1]) / 2 : sortedItemHeights[middle]; var clientProp = isHoriz ? 'clientWidth' : 'clientHeight'; var element = config.scrollContainer ? config.scrollContainer : this._element; var containerHeight = element[clientProp] ? element[clientProp] : this._containerSize; this._screenItemsLen = Math.ceil(containerHeight / averageHeight); this._containerSize = containerHeight; // Cache 3 times the number of items that fit in the container viewport. this._cachedItemsLen = Math.max(this._cachedItemsLen || 0, this._screenItemsLen * 3); this._averageHeight = averageHeight; if (config.reverse) { window.requestAnimationFrame(function () { if (isHoriz) { _this2._element.scrollLeft = scrollHeight; } else { _this2._element.scrollTop = scrollHeight; } }); } return scrollHeight; } }, { key: '_computeScrollPadding', value: function _computeScrollPadding() { var config = this._config; var isHoriz = Boolean(config.horizontal); var isReverse = config.reverse; var styles = window.getComputedStyle(this._element); var padding = function padding(location) { var cssValue = styles.getPropertyValue('padding-' + location); return parseInt(cssValue, 10) || 0; }; if (isHoriz && isReverse) { return { bottom: padding('left'), top: padding('right') }; } else if (isHoriz) { return { bottom: padding('right'), top: padding('left') }; } else if (isReverse) { return { bottom: padding('top'), top: padding('bottom') }; } else { return { bottom: padding('bottom'), top: padding('top') }; } } }, { key: '_getFrom', value: function _getFrom(scrollTop) { var i = 0; while (this._itemPositions[i] < scrollTop) { i++; } return i; } }, { key: '_getReverseFrom', value: function _getReverseFrom(scrollTop) { var i = this._config.total - 1; while (i > 0 && this._itemPositions[i] < scrollTop + this._containerSize) { i--; } return i; } }]); return HyperList; }(); exports.default = HyperList; module.exports = exports['default']; },{}]},{},[1])(1) });