@midiu/sticky
Version:
A lightweight vanilla javascript for creating sticky elements pinned to the page or to a container element
240 lines (208 loc) • 7.41 kB
JavaScript
/**
* Sticky is a library for sticky elements written in vanilla javascript.
* With this library you can easily set sticky elements on your website
*
* @author Vinh Trinh <vinhtrinh.live@gmail.com>
*/
export default class Sticky {
/**
* @param {Element} target
* @param {Object} options
*
* @param {Element} options.containment
* @param {Element|Number} options.min
* @param {Element|Number} options.max
* @param {Element|Number} options.inverseMin
*
* @param {Function.<Sticky>} options.onInit
* @param {Function.<Sticky>} options.onDestroy
* @param {Function.<Sticky>} options.onComputeOffset
* @param {Function.<Sticky>} options.onStick
* @param {Function.<Sticky>} options.onCancelStick
*/
constructor(target, options) {
this.target = target;
this.options = options;
this.onScroll = this.onScroll.bind(this);
this.onResize = this.onResize.bind(this);
this.backup = {
width: this.target.style.getPropertyValue('width'),
top: this.target.style.getPropertyValue('top'),
left: this.target.style.getPropertyValue('left')
};
this.init();
}
/**
* Initialize the plugin
*
* @fires onInit
*/
init() {
if (!this.initalized) {
this.initialized = true;
window.addEventListener('scroll', this.onScroll);
window.addEventListener('resize', this.onResize);
this.computeOffsets();
this.updatePosition();
this.trigger('onInit', this);
}
}
/**
* Destroy initialized
*
* @fires onDestroy
*/
destroy() {
if (this.initialized) {
window.removeEventListener('scroll', this.onScroll);
window.removeEventListener('resize', this.onResize);
this.initialized = false;
this.cancel(false);
this.trigger('onDestroy', this);
}
}
/**
* Handling window resize event
*
* @param {Event} event The event object
*/
onResize(event) {
this.computeOffsets();
this.updatePosition();
}
/**
* Handling window scroll event
*
* @param {Event} event The event object
*/
onScroll(event) {
this.updatePosition();
}
/**
* Compute target offset on initialize or window resize
*
* @fires onComputeOffset
*/
computeOffsets() {
this.cancel(false);
let computedStyle = getComputedStyle(this.target);
this.topSpacing = parseFloat(computedStyle.marginTop);
this.bottomSpacing = parseFloat(computedStyle.marginBottom);
this.originWidth = this.target.offsetWidth;
this.originHeight = this.target.offsetHeight;
this.originLeft = this.target.offsetLeft;
this.min = this.target.offsetTop;
this.max = Infinity;
if (this.options.hasOwnProperty('containment')) {
let clientRect = this.options.containment.getBoundingClientRect();
this.min = Math.max(this.min, clientRect.top + window.scrollY);
this.max = Math.min(this.max, clientRect.top + window.scrollY - this.topSpacing + this.options.containment.offsetHeight - this.originHeight);
}
if (this.options.hasOwnProperty('min')) {
if (typeof this.options.min === 'function') {
this.min = this.options.min(this);
} else if (this.options.min instanceof HTMLElement) {
let clientRect = this.options.min.getBoundingClientRect();
this.min = Math.max(this.min, clientRect.top + window.scrollY - this.topSpacing + clientRect.height);
} else {
this.min = Math.max(this.min, this.options.min);
}
}
if (this.options.hasOwnProperty('max')) {
if (typeof this.options.max === 'function') {
this.max = this.options.max(this);
} else if (this.options.max instanceof HTMLElement) {
let clientRect = this.options.max.getBoundingClientRect();
this.max = Math.min(this.max, clientRect.top + window.scrollY - this.topSpacing - this.target.offsetHeight);
} else {
this.max = Math.min(this.max, this.options.max);
}
}
if (this.options.hasOwnProperty('inverseMin')) {
if (this.options.inverseMin instanceof HTMLElement) {
let clientRect = this.options.inverseMin.getBoundingClientRect();
this.inverseMin = clientRect.top + window.scrollY - this.bottomSpacing - this.target.offsetHeight;
} else {
this.inverseMin = this.options.inverseMin;
}
}
this.trigger('onComputeOffset', this);
}
/**
* Update target position on scroll or resize window
*/
updatePosition() {
if (this.inverseMin && window.scrollY + window.innerHeight - this.originHeight < this.inverseMin) {
this.stick(true);
} else if (window.scrollY > this.min || this.options.min) {
if (window.scrollY < this.min) {
this.target.style.top = (this.min - window.scrollY) + 'px';
} else if (window.scrollY > this.max) {
this.target.style.top = -(window.scrollY - this.max) + 'px';
} else {
this.target.style.removeProperty('top');
}
this.stick();
} else {
this.cancel();
}
}
/**
* Start stick registered element
*
* @fires onStick
*
* @param {boolen} inverse
*/
stick(inverse = false) {
if (!this.stuck) {
this.stuck = true;
this.target.parentElement.style.minHeight = this.originHeight + 'px';
this.target.style.width = this.originWidth + 'px';
this.target.style.left = this.originLeft + 'px';
this.target.classList.add('md-stuck');
if (inverse) {
this.target.classList.add('md-inversed');
}
this.trigger('onStick', this);
}
}
/**
* Cancel a stuck element
*
* @fires onCancelStick
*
* @param {boolean} trigger
*/
cancel(trigger = true) {
if (this.stuck) {
this.stuck = false;
this.target.classList.remove('md-stuck');
this.target.classList.remove('md-inversed');
this.target.parentElement.style.removeProperty('min-height');
for (let prop in this.backup) {
if (this.backup.prop) {
this.target.style[prop] = this.backup.prop;
} else {
this.target.style.removeProperty(prop);
}
}
if (trigger) {
this.trigger('onCancelStick', this);
}
}
}
/**
* Trigger a registered hook by name with optional arguments
*
* @private
*
* @param {string} hook The hook name
* @param {...any} args The event arguments will be pass through event handler
*/
trigger(hook, ...args) {
if (typeof this.options[hook] === 'function') {
this.options[hook].apply(this, args);
}
}
}