sticky-state
Version:
StickyState is a high performant module making native position:sticky statefull and polyfill the missing sticky browser feature
641 lines (532 loc) • 21.4 kB
JavaScript
'use strict';
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; }; }();
var _objectAssign = require('object-assign');
var _objectAssign2 = _interopRequireDefault(_objectAssign);
var _classstring = require('classstring');
var _classstring2 = _interopRequireDefault(_classstring);
var _eventdispatcher = require('eventdispatcher');
var _eventdispatcher2 = _interopRequireDefault(_eventdispatcher);
var _scrollfeatures = require('scrollfeatures');
var _scrollfeatures2 = _interopRequireDefault(_scrollfeatures);
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; }
var defaults = {
disabled: false,
className: 'sticky',
stateClassName: 'is-sticky',
fixedClass: 'sticky-fixed',
wrapperClass: 'sticky-wrap',
wrapFixedSticky: true,
absoluteClass: 'is-absolute',
scrollClass: {
down: null,
up: null,
none: null,
persist: false,
active: false
}
};
var initialState = {
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 StickyState = function (_EventDispatcher) {
_inherits(StickyState, _EventDispatcher);
function StickyState(element, options) {
_classCallCheck(this, StickyState);
var elements;
if (element instanceof window.NodeList) {
elements = [].slice.call(element, 1);
element = element[0];
}
var _this = _possibleConstructorReturn(this, _EventDispatcher.call(this, { target: element }));
_this.el = null;
_this.firstRender = true;
_this.scroll = null;
_this.wrapper = null;
_this.options = null;
_this.el = element;
if (options && options.scrollClass) {
options.scrollClass = (0, _objectAssign2.default)({}, defaults.scrollClass, options.scrollClass, { active: true });
}
_this.options = (0, _objectAssign2.default)({}, defaults, options);
_this.setState((0, _objectAssign2.default)({}, initialState, { disabled: _this.options.disabled }), true);
_this.scrollTarget = _scrollfeatures2.default.getScrollParent(_this.el);
_this.hasOwnScrollTarget = _this.scrollTarget !== window;
if (_this.hasOwnScrollTarget) {
_this.updateFixedOffset = _this.updateFixedOffset.bind(_this);
}
_this.render = _this.render.bind(_this);
_this.addSrollHandler();
_this.addResizeHandler();
_this.render();
if (elements && elements.length) {
var _ret;
var collection = StickyState.apply(elements, options);
collection.push(_this);
return _ret = collection, _possibleConstructorReturn(_this, _ret);
}
return _this;
}
StickyState.apply = function apply(elements, options) {
if (elements && elements.length) {
var arr = new StickyStateCollection();
for (var i = 0; i < elements.length; i++) {
arr.push(new StickyState(elements[i], options));
}
return arr;
}
return new StickyState(elements, options);
};
StickyState.prototype.setState = function setState(newState, silent) {
this.lastState = this.state || newState;
this.state = (0, _objectAssign2.default)({}, this.state, newState);
if (silent !== true) {
this.render();
this.trigger(this.state.sticky ? 'sticky:on' : 'sticky:off');
}
};
StickyState.prototype.disable = function disable(value) {
this.setState({ disabled: value }, value);
};
StickyState.prototype.getBoundingClientRect = function getBoundingClientRect() {
return this.el.getBoundingClientRect();
};
StickyState.prototype.getBounds = 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.el);
}
var style = initialStyle;
var child = this.wrapper || this.el;
var rect;
var restrict;
var offsetY = 0;
var offsetX = 0;
if (!Can.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
};
};
StickyState.prototype.updateBounds = function updateBounds(silent, noCache) {
silent = silent === true;
noCache = noCache === true;
this.setState(this.getBounds(noCache), silent);
};
StickyState.prototype.updateFixedOffset = function updateFixedOffset() {
this.lastState.fixedOffset = this.state.fixedOffset;
if (this.state.sticky) {
this.state.fixedOffset = this.scrollTarget.getBoundingClientRect().top + 'px;';
} else {
this.state.fixedOffset = '';
}
if (this.lastState.fixedOffset !== this.state.fixedOffset) {
this.render();
}
};
StickyState.prototype.addSrollHandler = 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.options.scrollClass.active) {
this.onScrollDirection = this.onScrollDirection.bind(this);
this.scroll.on('scroll:up', this.onScrollDirection);
this.scroll.on('scroll:down', this.onScrollDirection);
if (!this.options.scrollClass.persist) {
this.scroll.on('scroll:stop', this.onScrollDirection);
}
}
if (hasScrollTarget && this.scroll.scrollY > 0) {
this.scroll.trigger('scroll:progress');
}
}
};
StickyState.prototype.removeSrollHandler = 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.options.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;
}
};
StickyState.prototype.addResizeHandler = 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);
}
};
StickyState.prototype.removeResizeHandler = function removeResizeHandler() {
if (this.onResize) {
window.removeEventListener('sticky:update', this.onResize);
window.removeEventListener('resize', this.onResize);
window.removeEventListener('orientationchange', this.onResize);
this.onResize = null;
}
};
StickyState.prototype.destroy = function destroy() {
_EventDispatcher.prototype.destroy.call(this);
this.removeSrollHandler();
this.removeResizeHandler();
this.render = null;
this.el = null;
this.state = null;
this.wrapper = null;
};
StickyState.prototype.getScrollClasses = 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;
};
StickyState.prototype.onScrollDirection = function onScrollDirection(e) {
if (this.state.sticky || e.type === _scrollfeatures2.default.events.SCROLL_STOP) {
this.el.className = (0, _classstring2.default)(this.el.className, this.getScrollClasses());
}
};
StickyState.prototype.onScroll = function onScroll(e) {
this.updateStickyState(false);
if (this.hasOwnScrollTarget && !Can.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);
}
}
};
StickyState.prototype.update = function update() {
this.scroll.updateScrollPosition();
this.updateBounds(true, true);
this.updateStickyState(false);
};
StickyState.prototype.getStickyState = 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 };
};
StickyState.prototype.updateStickyState = function updateStickyState(silent) {
var values = this.getStickyState();
if (values.sticky !== this.state.sticky || values.absolute !== this.state.absolute) {
silent = silent === true;
values = (0, _objectAssign2.default)(values, this.getBounds());
this.setState(values, silent);
}
};
StickyState.prototype.render = function render() {
if (this.state.disabled) {
return;
}
var classNameObj = {};
if (this.firstRender) {
this.firstRender = false;
if (!Can.sticky) {
this.wrapper = document.createElement('div');
this.wrapper.className = this.options.wrapperClass;
var parent = this.el.parentNode;
if (parent) {
parent.insertBefore(this.wrapper, this.el);
}
if (this.options.wrapFixedSticky) {
this.wrapper.appendChild(this.el);
}
classNameObj[this.options.fixedClass] = true;
}
this.updateBounds(true, true);
this.updateStickyState(true);
}
if (!Can.sticky) {
var elementStyle = '';
var height = this.state.disabled || this.state.bounds.height === null || !this.state.sticky && !this.state.absolute ? 'auto;' : this.state.bounds.height + 'px;';
var wrapperStyle = 'height:' + height;
wrapperStyle += height === 'auto;' ? '' : (this.state.style['margin-top'] ? 'margin-top:' + this.state.style['margin-top'] + 'px;' : '0;') + (this.state.style['margin-bottom'] ? 'margin-bottom' + this.state.style['margin-bottom'] + 'px;' : '0;');
if (this.state.absolute !== this.lastState.absolute) {
wrapperStyle += this.state.absolute ? 'position:relative;' : '';
classNameObj[this.options.absoluteClass] = this.state.absolute;
elementStyle += this.state.absolute ? this.state.style.top !== null ? 'margin-top:' + (this.state.restrict.height - (this.state.bounds.height + this.state.style.top) + (this.state.restrict.top - this.state.bounds.top)) + 'px;' : 'margin-top:0;' + (this.state.style.bottom !== null ? 'margin-bottom:' + (this.state.restrict.height - (this.state.bounds.height + this.state.style.bottom) + (this.state.restrict.bottom - this.state.bounds.bottom)) + 'px;' : 'margin-bottom:0;') : 'margin-bottom:0;margin-top:0;';
}
if ((this.state.style.top !== null || this.state.style.bottom !== null) && this.hasOwnScrollTarget && !this.state.absolute && this.lastState.fixedOffset !== this.state.fixedOffset) {
elementStyle += 'margin-top:' + (this.state.fixedOffset ? this.state.fixedOffset : '0;');
}
if (this.state.wrapperStyle !== wrapperStyle) {
this.state.wrapperStyle = wrapperStyle;
this.wrapper.style.cssText += wrapperStyle;
}
if (this.state.elementStyle !== elementStyle) {
this.state.elementStyle = elementStyle;
this.el.style.cssText += elementStyle;
}
}
classNameObj[this.options.stateClassName] = this.state.sticky;
classNameObj = this.getScrollClasses(classNameObj);
var className = (0, _classstring2.default)(this.el.className, classNameObj);
if (this.el.className !== className) {
this.el.className = className;
}
return this.el;
};
_createClass(StickyState, null, [{
key: 'native',
get: function get() {
return Can.sticky;
}
}]);
return StickyState;
}(_eventdispatcher2.default);
var _canSticky = null;
var Can = function () {
function Can() {
_classCallCheck(this, Can);
}
_createClass(Can, null, [{
key: 'sticky',
get: function get() {
if (_canSticky !== null) {
return _canSticky;
}
if (typeof window !== 'undefined') {
if (window.Modernizr && window.Modernizr.hasOwnProperty('csspositionsticky')) {
return _canSticky = window.Modernizr.csspositionsticky;
}
var documentFragment = document.documentElement;
var testEl = document.createElement('div');
documentFragment.appendChild(testEl);
var prefixedSticky = ['sticky', '-webkit-sticky'];
_canSticky = false;
for (var i = 0; i < prefixedSticky.length; i++) {
testEl.style.position = prefixedSticky[i];
_canSticky = !!window.getComputedStyle(testEl).position.match('sticky');
if (_canSticky) {
break;
}
}
documentFragment.removeChild(testEl);
}
return _canSticky;
}
}]);
return Can;
}();
var StickyStateCollection = function (_EventDispatcher2) {
_inherits(StickyStateCollection, _EventDispatcher2);
function StickyStateCollection() {
_classCallCheck(this, StickyStateCollection);
var _this2 = _possibleConstructorReturn(this, _EventDispatcher2.call(this));
_this2.items = [];
return _this2;
}
StickyStateCollection.prototype.push = function push(item) {
this.items.push(item);
};
StickyStateCollection.prototype.update = function update() {
var i = -1;
while (++i < this.items.length) {
this.items[i].update();
}
};
StickyStateCollection.prototype.addListener = function addListener(event, listener) {
var i = -1;
while (++i < this.items.length) {
this.items[i].addListener(event, listener);
}
return this;
};
StickyStateCollection.prototype.removeListener = function removeListener(event, listener) {
var i = -1;
while (++i < this.items.length) {
this.items[i].removeListener(event, listener);
}
return this;
};
return StickyStateCollection;
}(_eventdispatcher2.default);
exports.default = StickyState;
module.exports = exports['default'];