vue-sticky-directive
Version:
A powerful vue directive make element sticky.
351 lines (320 loc) • 10.8 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.VueStickyDirective = factory());
}(this, (function () { 'use strict';
var classCallCheck = function (instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
};
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 namespace = '@@vue-sticky-directive';
var events = ['resize', 'scroll', 'touchstart', 'touchmove', 'touchend', 'pageshow', 'load'];
var batchStyle = function batchStyle(el) {
var style = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var className = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
for (var k in style) {
el.style[k] = style[k];
}
for (var _k in className) {
if (className[_k] && !el.classList.contains(_k)) {
el.classList.add(_k);
} else if (!className[_k] && el.classList.contains(_k)) {
el.classList.remove(_k);
}
}
};
var Sticky$1 = function () {
function Sticky(el, vm) {
classCallCheck(this, Sticky);
this.el = el;
this.vm = vm;
this.unsubscribers = [];
this.isPending = false;
this.state = {
isTopSticky: null,
isBottomSticky: null,
height: null,
width: null,
xOffset: null
};
this.lastState = {
top: null,
bottom: null,
sticked: false
};
var offset = this.getAttribute('sticky-offset') || {};
var side = this.getAttribute('sticky-side') || 'top';
var zIndex = this.getAttribute('sticky-z-index') || '10';
var onStick = this.getAttribute('on-stick') || null;
this.options = {
topOffset: Number(offset.top) || 0,
bottomOffset: Number(offset.bottom) || 0,
shouldTopSticky: side === 'top' || side === 'both',
shouldBottomSticky: side === 'bottom' || side === 'both',
zIndex: zIndex,
onStick: onStick
};
}
createClass(Sticky, [{
key: 'doBind',
value: function doBind() {
var _this = this;
if (this.unsubscribers.length > 0) {
return;
}
var el = this.el,
vm = this.vm;
vm.$nextTick(function () {
_this.placeholderEl = document.createElement('div');
_this.containerEl = _this.getContainerEl();
el.parentNode.insertBefore(_this.placeholderEl, el);
events.forEach(function (event) {
var fn = _this.update.bind(_this);
_this.unsubscribers.push(function () {
return window.removeEventListener(event, fn);
});
_this.unsubscribers.push(function () {
return _this.containerEl.removeEventListener(event, fn);
});
window.addEventListener(event, fn, { passive: true });
_this.containerEl.addEventListener(event, fn, { passive: true });
});
});
}
}, {
key: 'doUnbind',
value: function doUnbind() {
this.unsubscribers.forEach(function (fn) {
return fn();
});
this.unsubscribers = [];
this.resetElement();
}
}, {
key: 'update',
value: function update() {
var _this2 = this;
if (!this.isPending) {
requestAnimationFrame(function () {
_this2.isPending = false;
_this2.recomputeState();
_this2.updateElements();
});
this.isPending = true;
}
}
}, {
key: 'isTopSticky',
value: function isTopSticky() {
if (!this.options.shouldTopSticky) return false;
var fromTop = this.state.placeholderElRect.top;
var fromBottom = this.state.containerElRect.bottom;
var topBreakpoint = this.options.topOffset;
var bottomBreakpoint = this.options.bottomOffset;
return fromTop <= topBreakpoint && fromBottom >= bottomBreakpoint;
}
}, {
key: 'isBottomSticky',
value: function isBottomSticky() {
if (!this.options.shouldBottomSticky) return false;
var fromBottom = window.innerHeight - this.state.placeholderElRect.top - this.state.height;
var fromTop = window.innerHeight - this.state.containerElRect.top;
var topBreakpoint = this.options.topOffset;
var bottomBreakpoint = this.options.bottomOffset;
return fromBottom <= bottomBreakpoint && fromTop >= topBreakpoint;
}
}, {
key: 'recomputeState',
value: function recomputeState() {
this.state = Object.assign({}, this.state, {
height: this.getHeight(),
width: this.getWidth(),
xOffset: this.getXOffset(),
placeholderElRect: this.getPlaceholderElRect(),
containerElRect: this.getContainerElRect()
});
this.state.isTopSticky = this.isTopSticky();
this.state.isBottomSticky = this.isBottomSticky();
}
}, {
key: 'fireEvents',
value: function fireEvents() {
if (typeof this.options.onStick === 'function' && (this.lastState.top !== this.state.isTopSticky || this.lastState.bottom !== this.state.isBottomSticky || this.lastState.sticked !== (this.state.isTopSticky || this.state.isBottomSticky))) {
this.lastState = {
top: this.state.isTopSticky,
bottom: this.state.isBottomSticky,
sticked: this.state.isBottomSticky || this.state.isTopSticky
};
this.options.onStick(this.lastState);
}
}
}, {
key: 'updateElements',
value: function updateElements() {
var placeholderStyle = { paddingTop: 0 };
var elStyle = {
position: 'static',
top: 'auto',
bottom: 'auto',
left: 'auto',
width: 'auto',
zIndex: this.options.zIndex
};
var placeholderClassName = { 'vue-sticky-placeholder': true };
var elClassName = {
'vue-sticky-el': true,
'top-sticky': false,
'bottom-sticky': false
};
if (this.state.isTopSticky) {
elStyle.position = 'fixed';
elStyle.top = this.options.topOffset + 'px';
elStyle.left = this.state.xOffset + 'px';
elStyle.width = this.state.width + 'px';
var bottomLimit = this.state.containerElRect.bottom - this.state.height - this.options.bottomOffset - this.options.topOffset;
if (bottomLimit < 0) {
elStyle.top = bottomLimit + this.options.topOffset + 'px';
}
placeholderStyle.paddingTop = this.state.height + 'px';
elClassName['top-sticky'] = true;
} else if (this.state.isBottomSticky) {
elStyle.position = 'fixed';
elStyle.bottom = this.options.bottomOffset + 'px';
elStyle.left = this.state.xOffset + 'px';
elStyle.width = this.state.width + 'px';
var topLimit = window.innerHeight - this.state.containerElRect.top - this.state.height - this.options.bottomOffset - this.options.topOffset;
if (topLimit < 0) {
elStyle.bottom = topLimit + this.options.bottomOffset + 'px';
}
placeholderStyle.paddingTop = this.state.height + 'px';
elClassName['bottom-sticky'] = true;
} else {
placeholderStyle.paddingTop = 0;
}
batchStyle(this.el, elStyle, elClassName);
batchStyle(this.placeholderEl, placeholderStyle, placeholderClassName);
this.fireEvents();
}
}, {
key: 'resetElement',
value: function resetElement() {
var _this3 = this;
['position', 'top', 'bottom', 'left', 'width', 'zIndex'].forEach(function (attr) {
_this3.el.style.removeProperty(attr);
});
this.el.classList.remove('bottom-sticky', 'top-sticky');
var parentNode = this.placeholderEl.parentNode;
if (parentNode) {
parentNode.removeChild(this.placeholderEl);
}
}
}, {
key: 'getContainerEl',
value: function getContainerEl() {
var node = this.el.parentNode;
while (node && node.tagName !== 'HTML' && node.tagName !== 'BODY' && node.nodeType === 1) {
if (node.hasAttribute('sticky-container')) {
return node;
}
node = node.parentNode;
}
return this.el.parentNode;
}
}, {
key: 'getXOffset',
value: function getXOffset() {
return this.placeholderEl.getBoundingClientRect().left;
}
}, {
key: 'getWidth',
value: function getWidth() {
return this.placeholderEl.getBoundingClientRect().width;
}
}, {
key: 'getHeight',
value: function getHeight() {
return this.el.getBoundingClientRect().height;
}
}, {
key: 'getPlaceholderElRect',
value: function getPlaceholderElRect() {
return this.placeholderEl.getBoundingClientRect();
}
}, {
key: 'getContainerElRect',
value: function getContainerElRect() {
return this.containerEl.getBoundingClientRect();
}
}, {
key: 'getAttribute',
value: function getAttribute(name) {
var expr = this.el.getAttribute(name);
var result = undefined;
if (expr) {
if (this.vm[expr]) {
result = this.vm[expr];
} else {
try {
result = eval('(' + expr + ')');
} catch (error) {
result = expr;
}
}
}
return result;
}
}]);
return Sticky;
}();
var Sticky$2 = {
inserted: function inserted(el, bind, vnode) {
if (typeof bind.value === 'undefined' || bind.value) {
el[namespace] = new Sticky$1(el, vnode.context);
el[namespace].doBind();
}
},
unbind: function unbind(el, bind, vnode) {
if (el[namespace]) {
el[namespace].doUnbind();
el[namespace] = undefined;
}
},
componentUpdated: function componentUpdated(el, bind, vnode) {
if (typeof bind.value === 'undefined' || bind.value) {
if (!el[namespace]) {
el[namespace] = new Sticky$1(el, vnode.context);
}
el[namespace].doBind();
} else {
if (el[namespace]) {
el[namespace].doUnbind();
}
}
}
};
var install = function install(Vue) {
Vue.directive('Sticky', Sticky$2);
};
if (window.Vue) {
Vue.use(install);
}
Sticky$2.install = install;
return Sticky$2;
})));