react-sticky-state
Version:
React StickyState Component makes native position:sticky statefull and polyfills the missing sticky browser feature
708 lines (621 loc) • 24.5 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
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 _classnames = require('classnames');
var _classnames2 = _interopRequireDefault(_classnames);
var _scrollfeatures = require('scrollfeatures');
var _scrollfeatures2 = _interopRequireDefault(_scrollfeatures);
var _objectAssign = require('object-assign');
var _objectAssign2 = _interopRequireDefault(_objectAssign);
var _featureDetect = require('./featureDetect');
var _featureDetect2 = _interopRequireDefault(_featureDetect);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
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 _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }
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 log = function log() {};
var initialState = {
initialized: false,
sticky: false,
absolute: false,
fixedOffset: '',
offsetHeight: 0,
bounds: {
top: null,
left: null,
right: null,
bottom: null,
height: null,
width: null
},
restrict: {
top: null,
left: null,
right: null,
bottom: null,
height: null,
width: null
},
wrapperStyle: null,
elementStyle: null,
initialStyle: null,
style: {
top: null,
bottom: null,
left: null,
right: null,
'margin-top': 0,
'margin-bottom': 0,
'margin-left': 0,
'margin-right': 0
},
disabled: false
};
var getAbsolutBoundingRect = function getAbsolutBoundingRect(el, fixedHeight) {
var rect = el.getBoundingClientRect();
var top = rect.top + _scrollfeatures2.default.windowY;
var height = fixedHeight || rect.height;
return {
top: top,
bottom: top + height,
height: height,
width: rect.width,
left: rect.left,
right: rect.right
};
};
var addBounds = function addBounds(rect1, rect2) {
var rect = (0, _objectAssign2.default)({}, rect1);
rect.top -= rect2.top;
rect.left -= rect2.left;
rect.right = rect.left + rect1.width;
rect.bottom = rect.top + rect1.height;
return rect;
};
var getPositionStyle = function getPositionStyle(el) {
var result = {};
var style = window.getComputedStyle(el, null);
for (var key in initialState.style) {
var value = parseInt(style.getPropertyValue(key));
value = isNaN(value) ? null : value;
result[key] = value;
}
return result;
};
var getPreviousElementSibling = function getPreviousElementSibling(el) {
var prev = el.previousElementSibling;
if (prev && prev.tagName.toLocaleLowerCase() === 'script') {
prev = getPreviousElementSibling(prev);
}
return prev;
};
var ReactStickyState = function (_Component) {
_inherits(ReactStickyState, _Component);
function ReactStickyState(props, context) {
_classCallCheck(this, ReactStickyState);
var _this = _possibleConstructorReturn(this, (ReactStickyState.__proto__ || Object.getPrototypeOf(ReactStickyState)).call(this, props, context));
_this._updatingBounds = false;
_this._shouldComponentUpdate = true;
_this._updatingState = false;
_this.state = (0, _objectAssign2.default)({}, initialState, { disabled: props.disabled });
if (props.debug === true) {
log = console.log.bind(console);
}
return _this;
}
_createClass(ReactStickyState, [{
key: 'getBoundingClientRect',
value: function getBoundingClientRect() {
return this.refs.el.getBoundingClientRect();
}
}, {
key: 'getBounds',
value: function getBounds(noCache) {
var clientRect = this.getBoundingClientRect();
var offsetHeight = _scrollfeatures2.default.documentHeight;
noCache = noCache === true;
if (noCache !== true && this.state.bounds.height !== null) {
if (this.state.offsetHeight === offsetHeight && clientRect.height === this.state.bounds.height) {
return {
offsetHeight: offsetHeight,
style: this.state.style,
bounds: this.state.bounds,
restrict: this.state.restrict
};
}
}
// var style = noCache ? this.state.style : getPositionStyle(this.el);
var initialStyle = this.state.initialStyle;
if (!initialStyle) {
initialStyle = getPositionStyle(this.refs.el);
}
var style = initialStyle;
var child = this.refs.wrapper || this.refs.el;
var rect;
var restrict;
var offsetY = 0;
// var offsetX = 0;
if (!_featureDetect2.default.sticky) {
rect = getAbsolutBoundingRect(child, clientRect.height);
if (this.hasOwnScrollTarget) {
var parentRect = getAbsolutBoundingRect(this.scrollTarget);
offsetY = this.scroll.y;
rect = addBounds(rect, parentRect);
restrict = parentRect;
restrict.top = 0;
restrict.height = this.scroll.scrollHeight || restrict.height;
restrict.bottom = restrict.height;
}
} else {
var elem = getPreviousElementSibling(child);
offsetY = 0;
if (elem) {
offsetY = parseInt(window.getComputedStyle(elem)['margin-bottom']);
offsetY = offsetY || 0;
rect = getAbsolutBoundingRect(elem);
if (this.hasOwnScrollTarget) {
rect = addBounds(rect, getAbsolutBoundingRect(this.scrollTarget));
offsetY += this.scroll.y;
}
rect.top = rect.bottom + offsetY;
} else {
elem = child.parentNode;
offsetY = parseInt(window.getComputedStyle(elem)['padding-top']);
offsetY = offsetY || 0;
rect = getAbsolutBoundingRect(elem);
if (this.hasOwnScrollTarget) {
rect = addBounds(rect, getAbsolutBoundingRect(this.scrollTarget));
offsetY += this.scroll.y;
}
rect.top = rect.top + offsetY;
}
if (this.hasOwnScrollTarget) {
restrict = getAbsolutBoundingRect(this.scrollTarget);
restrict.top = 0;
restrict.height = this.scroll.scrollHeight || restrict.height;
restrict.bottom = restrict.height;
}
rect.height = child.clientHeight;
rect.width = child.clientWidth;
rect.bottom = rect.top + rect.height;
}
restrict = restrict || getAbsolutBoundingRect(child.parentNode);
return {
offsetHeight: offsetHeight,
style: style,
bounds: rect,
initialStyle: initialStyle,
restrict: restrict
};
}
}, {
key: 'updateBounds',
value: function updateBounds(silent, noCache, cb) {
var _this2 = this;
noCache = noCache === true;
this._shouldComponentUpdate = silent !== true;
this.setState(this.getBounds(noCache), function () {
_this2._shouldComponentUpdate = true;
if (cb) {
cb();
}
});
}
// updateFixedOffset() {
// if (this.hasOwnScrollTarget && !Can.sticky) {
// if (this.state.sticky) {
// this.setState({ fixedOffset: this.scrollTarget.getBoundingClientRect().top + 'px' });
// if (!this.hasWindowScrollListener) {
// this.hasWindowScrollListener = true;
// ScrollFeatures.getInstance(window).on('scroll:progress', this.updateFixedOffset);
// }
// } else {
// this.setState({ fixedOffset: '' });
// if (this.hasWindowScrollListener) {
// this.hasWindowScrollListener = false;
// ScrollFeatures.getInstance(window).off('scroll:progress', this.updateFixedOffset);
// }
// }
// }
// }
}, {
key: 'updateFixedOffset',
value: function updateFixedOffset() {
var fixedOffset = this.state.fixedOffset;
if (this.state.sticky) {
this.setState({ fixedOffset: this.scrollTarget.getBoundingClientRect().top + 'px;' });
} else {
this.setState({ fixedOffset: '' });
}
// if (fixedOffset !== this.state.fixedOffset) {
// this.render();
// }
}
}, {
key: 'addSrollHandler',
value: function addSrollHandler() {
if (!this.scroll) {
var hasScrollTarget = _scrollfeatures2.default.hasInstance(this.scrollTarget);
this.scroll = _scrollfeatures2.default.getInstance(this.scrollTarget);
this.onScroll = this.onScroll.bind(this);
this.scroll.on('scroll:start', this.onScroll);
this.scroll.on('scroll:progress', this.onScroll);
this.scroll.on('scroll:stop', this.onScroll);
if (this.props.scrollClass.active) {
this.onScrollDirection = this.onScrollDirection.bind(this);
this.scroll.on('scroll:up', this.onScrollDirection);
this.scroll.on('scroll:down', this.onScrollDirection);
if (!this.props.scrollClass.persist) {
this.scroll.on('scroll:stop', this.onScrollDirection);
}
}
if (hasScrollTarget && this.scroll.scrollY > 0) {
this.scroll.trigger('scroll:progress');
}
}
}
}, {
key: 'removeSrollHandler',
value: function removeSrollHandler() {
if (this.scroll) {
this.scroll.off('scroll:start', this.onScroll);
this.scroll.off('scroll:progress', this.onScroll);
this.scroll.off('scroll:stop', this.onScroll);
if (this.props.scrollClass.active) {
this.scroll.off('scroll:up', this.onScrollDirection);
this.scroll.off('scroll:down', this.onScrollDirection);
this.scroll.off('scroll:stop', this.onScrollDirection);
}
if (!this.scroll.hasListeners()) {
this.scroll.destroy();
}
this.onScroll = null;
this.onScrollDirection = null;
this.scroll = null;
}
}
}, {
key: 'addResizeHandler',
value: function addResizeHandler() {
if (!this.onResize) {
this.onResize = this.update.bind(this);
window.addEventListener('sticky:update', this.onResize, false);
window.addEventListener('resize', this.onResize, false);
window.addEventListener('orientationchange', this.onResize, false);
}
}
}, {
key: 'removeResizeHandler',
value: function removeResizeHandler() {
if (this.onResize) {
window.removeEventListener('sticky:update', this.onResize);
window.removeEventListener('resize', this.onResize);
window.removeEventListener('orientationchange', this.onResize);
this.onResize = null;
}
}
}, {
key: 'destroy',
value: function destroy() {
this._updatingBounds = false;
this._shouldComponentUpdate = false;
this._updatingState = false;
this.removeSrollHandler();
this.removeResizeHandler();
this.scrollTarget = null;
}
}, {
key: 'getScrollClasses',
value: function getScrollClasses(obj) {
if (this.options.scrollClass.active) {
obj = obj || {};
var direction = this.scroll.y <= 0 || this.scroll.y + this.scroll.clientHeight >= this.scroll.scrollHeight ? 0 : this.scroll.directionY;
obj[this.options.scrollClass.up] = direction < 0;
obj[this.options.scrollClass.down] = direction > 0;
}
return obj;
}
}, {
key: 'getScrollClass',
value: function getScrollClass() {
if (this.props.scrollClass.up || this.props.scrollClass.down) {
var direction = this.scroll.y <= 0 || this.scroll.y + this.scroll.clientHeight >= this.scroll.scrollHeight ? 0 : this.scroll.directionY;
var scrollClass = direction < 0 ? this.props.scrollClass.up : this.props.scrollClass.down;
scrollClass = direction === 0 ? null : scrollClass;
return scrollClass;
}
return null;
}
}, {
key: 'onScrollDirection',
value: function onScrollDirection(e) {
if (this.state.sticky || e && e.type === _scrollfeatures2.default.events.SCROLL_STOP) {
this.setState({
scrollClass: this.getScrollClass()
});
}
}
}, {
key: 'onScroll',
value: function onScroll(e) {
this.updateStickyState(false);
if (this.hasOwnScrollTarget && !_featureDetect2.default.sticky) {
this.updateFixedOffset();
if (this.state.sticky && !this.hasWindowScrollListener) {
this.hasWindowScrollListener = true;
_scrollfeatures2.default.getInstance(window).on('scroll:progress', this.updateFixedOffset);
} else if (!this.state.sticky && this.hasWindowScrollListener) {
this.hasWindowScrollListener = false;
_scrollfeatures2.default.getInstance(window).off('scroll:progress', this.updateFixedOffset);
}
}
}
}, {
key: 'update',
value: function update() {
var _this3 = this;
// this.scroll.updateScrollPosition();
this.updateBounds(true, true, function () {
_this3.updateStickyState(false);
});
}
// update(force = false) {
// if (!this._updatingBounds) {
// this._updatingBounds = true;
// this.scroll.updateScrollPosition();
// this.updateBounds(true, true, () => {
// this.updateBounds(force, true, () => {
// this.scroll.updateScrollPosition();
// var updateSticky = this.updateStickyState(false, () => {
// if (force && !updateSticky) {
// this.forceUpdate();
// }
// });
// this._updatingBounds = false;
// });
// });
// }
// }
}, {
key: 'getStickyState',
value: function getStickyState() {
if (this.state.disabled) {
return { sticky: false, absolute: false };
}
var scrollY = this.scroll.y;
// var scrollX = this.scroll.x;
var top = this.state.style.top;
var bottom = this.state.style.bottom;
// var left = this.state.style.left;
// var right = this.state.style.right;
var sticky = this.state.sticky;
var absolute = this.state.absolute;
if (top !== null) {
var offsetBottom = this.state.restrict.bottom - this.state.bounds.height - top;
top = this.state.bounds.top - top;
if (this.state.sticky === false && (scrollY >= top && scrollY <= offsetBottom || top <= 0 && scrollY < top)) {
sticky = true;
absolute = false;
} else if (this.state.sticky && (top > 0 && scrollY < top || scrollY > offsetBottom)) {
sticky = false;
absolute = scrollY > offsetBottom;
}
} else if (bottom !== null) {
scrollY += window.innerHeight;
var offsetTop = this.state.restrict.top + this.state.bounds.height - bottom;
bottom = this.state.bounds.bottom + bottom;
if (this.state.sticky === false && scrollY <= bottom && scrollY >= offsetTop) {
sticky = true;
absolute = false;
} else if (this.state.sticky && (scrollY > bottom || scrollY < offsetTop)) {
sticky = false;
absolute = scrollY <= offsetTop;
}
}
return { sticky: sticky, absolute: absolute };
}
}, {
key: 'updateStickyState',
value: function updateStickyState(silent) {
var _this4 = this;
var values = this.getStickyState();
if (values.sticky !== this.state.sticky || values.absolute !== this.state.absolute) {
this._shouldComponentUpdate = silent !== true;
values = (0, _objectAssign2.default)(values, this.getBounds(false));
this._updatingState = true;
this.setState(values, function () {
_this4._shouldComponentUpdate = true;
_this4._updatingState = false;
});
}
}
// updateStickyState(bounds = true, cb) {
// if (this._updatingState) {
// return;
// }
// var values = this.getStickyState();
// if (values.sticky !== this.state.sticky || values.absolute !== this.state.absolute) {
// this._updatingState = true;
// if (bounds) {
// values = assign(values, this.getBounds(), { scrollClass: this.getScrollClass() });
// }
// this.setState(values, () => {
// this._updatingState = false;
// if (typeof cb === 'function') {
// cb();
// }
// });
// return true;
// } else if (typeof cb === 'function') {
// cb();
// }
// return false;
// }
}, {
key: 'initialize',
value: function initialize() {
var _this5 = this;
if (!this.state.initialized && !this.state.disabled) {
this.setState({
initialized: true
}, function () {
var child = _this5.refs.wrapper || _this5.refs.el;
_this5.scrollTarget = _scrollfeatures2.default.getScrollParent(child);
_this5.hasOwnScrollTarget = _this5.scrollTarget !== window;
if (_this5.hasOwnScrollTarget) {
_this5.updateFixedOffset = _this5.updateFixedOffset.bind(_this5);
}
_this5.addSrollHandler();
_this5.addResizeHandler();
_this5.update();
});
}
}
}, {
key: 'shouldComponentUpdate',
value: function shouldComponentUpdate(newProps, newState) {
return this._shouldComponentUpdate;
}
}, {
key: 'componentWillReceiveProps',
value: function componentWillReceiveProps(nextProps) {
var _this6 = this;
var intialize = !this.state.initialized && nextProps.initialize;
if (nextProps.disabled !== this.state.disabled) {
this.setState({
disabled: nextProps.disabled
}, function () {
if (intialize) {
_this6.initialize();
}
});
}
}
}, {
key: 'componentDidMount',
value: function componentDidMount() {
if (!this.state.initialized && this.props.initialize) {
this.initialize();
}
}
}, {
key: 'componentWillUnmount',
value: function componentWillUnmount() {
this.destroy();
}
}, {
key: 'render',
value: function render() {
var _classNames;
if (!this.state.initialized) {
return this.props.children;
}
var element = _react2.default.Children.only(this.props.children);
var _props = this.props,
wrapperClass = _props.wrapperClass,
stickyClass = _props.stickyClass,
fixedClass = _props.fixedClass,
stateClass = _props.stateClass,
disabledClass = _props.disabledClass,
absoluteClass = _props.absoluteClass,
disabled = _props.disabled,
debug = _props.debug,
tagName = _props.tagName,
props = _objectWithoutProperties(_props, ['wrapperClass', 'stickyClass', 'fixedClass', 'stateClass', 'disabledClass', 'absoluteClass', 'disabled', 'debug', 'tagName']);
var style;
var refName = 'el';
var className = (0, _classnames2.default)((_classNames = {}, _defineProperty(_classNames, stickyClass, !this.state.disabled), _defineProperty(_classNames, disabledClass, this.state.disabled), _classNames), _defineProperty({}, fixedClass, !_featureDetect2.default.sticky), _defineProperty({}, stateClass, this.state.sticky && !this.state.disabled), _defineProperty({}, absoluteClass, this.state.absolute), this.state.scrollClass);
if (!_featureDetect2.default.sticky) {
if (this.state.absolute) {
style = {
marginTop: this.state.style.top !== null ? this.state.restrict.height - (this.state.bounds.height + this.state.style.top) + (this.state.restrict.top - this.state.bounds.top) + 'px' : '',
marginBottom: this.state.style.bottom !== null ? this.state.restrict.height - (this.state.bounds.height + this.state.style.bottom) + (this.state.restrict.bottom - this.state.bounds.bottom) + 'px' : ''
};
} else if (this.hasOwnScrollTarget && this.state.fixedOffset !== '') {
style = {
marginTop: this.state.fixedOffset
};
}
}
if (element) {
element = _react2.default.cloneElement(element, { ref: refName, style: style, className: (0, _classnames2.default)(element.props.className, className) });
} else {
var Comp = this.props.tagName;
element = _react2.default.createElement(
Comp,
_extends({ ref: refName, style: style, className: className }, props),
this.props.children
);
}
if (_featureDetect2.default.sticky) {
return element;
}
var height = this.state.disabled || this.state.bounds.height === null /*|| (!this.state.sticky && !this.state.absolute)*/ ? 'auto' : this.state.bounds.height + 'px';
var marginTop = height === 'auto' ? '' : this.state.style['margin-top'] ? this.state.style['margin-top'] + 'px' : '';
var marginBottom = height === 'auto' ? '' : this.state.style['margin-bottom'] ? this.state.style['margin-bottom'] + 'px' : '';
style = {
height: height,
marginTop: marginTop,
marginBottom: marginBottom
};
if (this.state.absolute) {
style.position = 'relative';
}
return _react2.default.createElement(
'div',
{ ref: 'wrapper', className: wrapperClass, style: style },
element
);
}
}]);
return ReactStickyState;
}(_react.Component);
ReactStickyState.propTypes = {
initialize: _react.PropTypes.bool,
wrapperClass: _react.PropTypes.string,
stickyClass: _react.PropTypes.string,
fixedClass: _react.PropTypes.string,
stateClass: _react.PropTypes.string,
disabledClass: _react.PropTypes.string,
absoluteClass: _react.PropTypes.string,
disabled: _react.PropTypes.bool,
debug: _react.PropTypes.bool,
wrapFixedSticky: _react.PropTypes.bool,
tagName: _react.PropTypes.string,
scrollClass: _react.PropTypes.shape({
down: _react.PropTypes.string,
up: _react.PropTypes.string,
none: _react.PropTypes.string,
persist: _react.PropTypes.bool,
active: _react.PropTypes.bool
})
};
ReactStickyState.defaultProps = {
initialize: true,
wrapperClass: 'sticky-wrap',
stickyClass: 'sticky',
fixedClass: 'sticky-fixed',
stateClass: 'is-sticky',
disabledClass: 'sticky-disabled',
absoluteClass: 'is-absolute',
wrapFixedSticky: true,
debug: false,
disabled: false,
tagName: 'div',
scrollClass: {
down: null,
up: null,
none: null,
persist: false,
active: false
}
};
exports.default = ReactStickyState;
module.exports = exports['default'];