UNPKG

react-lazyload

Version:

Lazyload your components, images or anything where performance matters.

443 lines (366 loc) 16.8 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.forceVisible = exports.forceCheck = exports.lazyload = undefined; 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 _event = require('./utils/event'); var _scrollParent = require('./utils/scrollParent'); var _scrollParent2 = _interopRequireDefault(_scrollParent); var _debounce = require('./utils/debounce'); var _debounce2 = _interopRequireDefault(_debounce); var _throttle = require('./utils/throttle'); var _throttle2 = _interopRequireDefault(_throttle); 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; } /** * react-lazyload */ var defaultBoundingClientRect = { top: 0, right: 0, bottom: 0, left: 0, width: 0, height: 0 }; var LISTEN_FLAG = 'data-lazyload-listened'; var listeners = []; var pending = []; // try to handle passive events var passiveEventSupported = false; try { var opts = Object.defineProperty({}, 'passive', { get: function get() { passiveEventSupported = true; } }); window.addEventListener('test', null, opts); } catch (e) {} // if they are supported, setup the optional params // IMPORTANT: FALSE doubles as the default CAPTURE value! var passiveEvent = passiveEventSupported ? { capture: false, passive: true } : false; /** * Check if `component` is visible in overflow container `parent` * @param {node} component React component * @param {node} parent component's scroll parent * @return {bool} */ var checkOverflowVisible = function checkOverflowVisible(component, parent) { var node = component.ref; var parentTop = void 0; var parentLeft = void 0; var parentHeight = void 0; var parentWidth = void 0; try { var _parent$getBoundingCl = parent.getBoundingClientRect(); parentTop = _parent$getBoundingCl.top; parentLeft = _parent$getBoundingCl.left; parentHeight = _parent$getBoundingCl.height; parentWidth = _parent$getBoundingCl.width; } catch (e) { parentTop = defaultBoundingClientRect.top; parentLeft = defaultBoundingClientRect.left; parentHeight = defaultBoundingClientRect.height; parentWidth = defaultBoundingClientRect.width; } var windowInnerHeight = window.innerHeight || document.documentElement.clientHeight; var windowInnerWidth = window.innerWidth || document.documentElement.clientWidth; // calculate top and height of the intersection of the element's scrollParent and viewport var intersectionTop = Math.max(parentTop, 0); // intersection's top relative to viewport var intersectionLeft = Math.max(parentLeft, 0); // intersection's left relative to viewport var intersectionHeight = Math.min(windowInnerHeight, parentTop + parentHeight) - intersectionTop; // height var intersectionWidth = Math.min(windowInnerWidth, parentLeft + parentWidth) - intersectionLeft; // width // check whether the element is visible in the intersection var top = void 0; var left = void 0; var height = void 0; var width = void 0; try { var _node$getBoundingClie = node.getBoundingClientRect(); top = _node$getBoundingClie.top; left = _node$getBoundingClie.left; height = _node$getBoundingClie.height; width = _node$getBoundingClie.width; } catch (e) { top = defaultBoundingClientRect.top; left = defaultBoundingClientRect.left; height = defaultBoundingClientRect.height; width = defaultBoundingClientRect.width; } var offsetTop = top - intersectionTop; // element's top relative to intersection var offsetLeft = left - intersectionLeft; // element's left relative to intersection var offsets = Array.isArray(component.props.offset) ? component.props.offset : [component.props.offset, component.props.offset]; // Be compatible with previous API return offsetTop - offsets[0] <= intersectionHeight && offsetTop + height + offsets[1] >= 0 && offsetLeft - offsets[0] <= intersectionWidth && offsetLeft + width + offsets[1] >= 0; }; /** * Check if `component` is visible in document * @param {node} component React component * @return {bool} */ var checkNormalVisible = function checkNormalVisible(component) { var node = component.ref; // If this element is hidden by css rules somehow, it's definitely invisible if (!(node.offsetWidth || node.offsetHeight || node.getClientRects().length)) return false; var top = void 0; var elementHeight = void 0; try { var _node$getBoundingClie2 = node.getBoundingClientRect(); top = _node$getBoundingClie2.top; elementHeight = _node$getBoundingClie2.height; } catch (e) { top = defaultBoundingClientRect.top; elementHeight = defaultBoundingClientRect.height; } var windowInnerHeight = window.innerHeight || document.documentElement.clientHeight; var offsets = Array.isArray(component.props.offset) ? component.props.offset : [component.props.offset, component.props.offset]; // Be compatible with previous API return top - offsets[0] <= windowInnerHeight && top + elementHeight + offsets[1] >= 0; }; /** * Detect if element is visible in viewport, if so, set `visible` state to true. * If `once` prop is provided true, remove component as listener after checkVisible * * @param {React} component React component that respond to scroll and resize */ var checkVisible = function checkVisible(component) { var node = component.ref; if (!(node instanceof HTMLElement)) { return; } var parent = (0, _scrollParent2.default)(node); var isOverflow = component.props.overflow && parent !== node.ownerDocument && parent !== document && parent !== document.documentElement; var visible = isOverflow ? checkOverflowVisible(component, parent) : checkNormalVisible(component); if (visible) { // Avoid extra render if previously is visible if (!component.visible) { if (component.props.once) { pending.push(component); } component.visible = true; component.forceUpdate(); } } else if (!(component.props.once && component.visible)) { component.visible = false; if (component.props.unmountIfInvisible) { component.forceUpdate(); } } }; var purgePending = function purgePending() { pending.forEach(function (component) { var index = listeners.indexOf(component); if (index !== -1) { listeners.splice(index, 1); } }); pending = []; }; var lazyLoadHandler = function lazyLoadHandler() { for (var i = 0; i < listeners.length; ++i) { var listener = listeners[i]; checkVisible(listener); } // Remove `once` component in listeners purgePending(); }; /** * Forces the component to display regardless of whether the element is visible in the viewport. */ var forceVisible = function forceVisible() { for (var i = 0; i < listeners.length; ++i) { var listener = listeners[i]; listener.visible = true; listener.forceUpdate(); } // Remove `once` component in listeners purgePending(); }; // Depending on component's props var delayType = void 0; var finalLazyLoadHandler = null; var isString = function isString(string) { return typeof string === 'string'; }; var LazyLoad = function (_Component) { _inherits(LazyLoad, _Component); function LazyLoad(props) { _classCallCheck(this, LazyLoad); var _this = _possibleConstructorReturn(this, (LazyLoad.__proto__ || Object.getPrototypeOf(LazyLoad)).call(this, props)); _this.visible = false; _this.setRef = _this.setRef.bind(_this); return _this; } _createClass(LazyLoad, [{ key: 'componentDidMount', value: function componentDidMount() { // It's unlikely to change delay type on the fly, this is mainly // designed for tests var scrollport = window; var scrollContainer = this.props.scrollContainer; if (scrollContainer) { if (isString(scrollContainer)) { scrollport = scrollport.document.querySelector(scrollContainer); } } var needResetFinalLazyLoadHandler = this.props.debounce !== undefined && delayType === 'throttle' || delayType === 'debounce' && this.props.debounce === undefined; if (needResetFinalLazyLoadHandler) { (0, _event.off)(scrollport, 'scroll', finalLazyLoadHandler, passiveEvent); (0, _event.off)(window, 'resize', finalLazyLoadHandler, passiveEvent); finalLazyLoadHandler = null; } if (!finalLazyLoadHandler) { if (this.props.debounce !== undefined) { finalLazyLoadHandler = (0, _debounce2.default)(lazyLoadHandler, typeof this.props.debounce === 'number' ? this.props.debounce : 300); delayType = 'debounce'; } else if (this.props.throttle !== undefined) { finalLazyLoadHandler = (0, _throttle2.default)(lazyLoadHandler, typeof this.props.throttle === 'number' ? this.props.throttle : 300); delayType = 'throttle'; } else { finalLazyLoadHandler = lazyLoadHandler; } } if (this.props.overflow) { var parent = (0, _scrollParent2.default)(this.ref); if (parent && typeof parent.getAttribute === 'function') { var listenerCount = 1 + +parent.getAttribute(LISTEN_FLAG); if (listenerCount === 1) { parent.addEventListener('scroll', finalLazyLoadHandler, passiveEvent); } parent.setAttribute(LISTEN_FLAG, listenerCount); } } else if (listeners.length === 0 || needResetFinalLazyLoadHandler) { var _props = this.props, scroll = _props.scroll, resize = _props.resize; if (scroll) { (0, _event.on)(scrollport, 'scroll', finalLazyLoadHandler, passiveEvent); } if (resize) { (0, _event.on)(window, 'resize', finalLazyLoadHandler, passiveEvent); } } listeners.push(this); checkVisible(this); } }, { key: 'shouldComponentUpdate', value: function shouldComponentUpdate() { return this.visible; } }, { key: 'componentWillUnmount', value: function componentWillUnmount() { if (this.props.overflow) { var parent = (0, _scrollParent2.default)(this.ref); if (parent && typeof parent.getAttribute === 'function') { var listenerCount = +parent.getAttribute(LISTEN_FLAG) - 1; if (listenerCount === 0) { parent.removeEventListener('scroll', finalLazyLoadHandler, passiveEvent); parent.removeAttribute(LISTEN_FLAG); } else { parent.setAttribute(LISTEN_FLAG, listenerCount); } } } var index = listeners.indexOf(this); if (index !== -1) { listeners.splice(index, 1); } if (listeners.length === 0 && typeof window !== 'undefined') { (0, _event.off)(window, 'resize', finalLazyLoadHandler, passiveEvent); (0, _event.off)(window, 'scroll', finalLazyLoadHandler, passiveEvent); } } }, { key: 'setRef', value: function setRef(element) { if (element) { this.ref = element; } } }, { key: 'render', value: function render() { var _props2 = this.props, height = _props2.height, children = _props2.children, placeholder = _props2.placeholder, className = _props2.className, classNamePrefix = _props2.classNamePrefix, style = _props2.style; return _react2.default.createElement( 'div', { className: classNamePrefix + '-wrapper ' + className, ref: this.setRef, style: style }, this.visible ? children : placeholder ? placeholder : _react2.default.createElement('div', { style: { height: height }, className: classNamePrefix + '-placeholder' }) ); } }]); return LazyLoad; }(_react.Component); LazyLoad.propTypes = { className: _propTypes2.default.string, classNamePrefix: _propTypes2.default.string, once: _propTypes2.default.bool, height: _propTypes2.default.oneOfType([_propTypes2.default.number, _propTypes2.default.string]), offset: _propTypes2.default.oneOfType([_propTypes2.default.number, _propTypes2.default.arrayOf(_propTypes2.default.number)]), overflow: _propTypes2.default.bool, resize: _propTypes2.default.bool, scroll: _propTypes2.default.bool, children: _propTypes2.default.node, throttle: _propTypes2.default.oneOfType([_propTypes2.default.number, _propTypes2.default.bool]), debounce: _propTypes2.default.oneOfType([_propTypes2.default.number, _propTypes2.default.bool]), placeholder: _propTypes2.default.node, scrollContainer: _propTypes2.default.oneOfType([_propTypes2.default.string, _propTypes2.default.object]), unmountIfInvisible: _propTypes2.default.bool, style: _propTypes2.default.object }; LazyLoad.defaultProps = { className: '', classNamePrefix: 'lazyload', once: false, offset: 0, overflow: false, resize: false, scroll: true, unmountIfInvisible: false }; var getDisplayName = function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || 'Component'; }; var decorator = function decorator() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; return function lazyload(WrappedComponent) { return function (_Component2) { _inherits(LazyLoadDecorated, _Component2); function LazyLoadDecorated() { _classCallCheck(this, LazyLoadDecorated); var _this2 = _possibleConstructorReturn(this, (LazyLoadDecorated.__proto__ || Object.getPrototypeOf(LazyLoadDecorated)).call(this)); _this2.displayName = 'LazyLoad' + getDisplayName(WrappedComponent); return _this2; } _createClass(LazyLoadDecorated, [{ key: 'render', value: function render() { return _react2.default.createElement( LazyLoad, options, _react2.default.createElement(WrappedComponent, this.props) ); } }]); return LazyLoadDecorated; }(_react.Component); }; }; exports.lazyload = decorator; exports.default = LazyLoad; exports.forceCheck = lazyLoadHandler; exports.forceVisible = forceVisible;