react-lazyload
Version:
Lazyload your components, images or anything where performance matters.
443 lines (366 loc) • 16.8 kB
JavaScript
;
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;