jquery-sticky-element
Version:
Simple and lightweight plugin to pin/sticky elements inside a container so they move as the user scrolls
250 lines (223 loc) • 7.27 kB
JavaScript
;(function ($,win){
'use strict';
const requestFrame = (function (){
const raf = win.requestAnimationFrame
|| win.mozRequestAnimationFrame
|| win.webkitRequestAnimationFrame
|| function (fn){ return win.setTimeout(fn,20); };
return function (fn){ return raf(fn); };
}());
// Check if the browser supports the transition CSS property
const style = (win.document.body || win.document.documentElement).style;
const prop = 'transition';
const supportsTransition = typeof style[prop] == 'string';
const events = {
created:'sticky-created',
update:'sticky-update',
top:'sticky-hit-top',
bottom:'sticky-hit-bottom',
frozen:'sticky-frozen',
unfrozen:'sticky-unfrozen'
};
/**
* Sticky Element constructor
* @param {String} elm
* @param {String} par
* @param {Object} [options]
* @constructor
*/
function Sticky(elm,par,options){
this.element = elm;
this.parent = par;
this._frozen = false;
this._stopped = true;
this.options = $.extend({
useTransition:true,
animate:true,
animTime:200,
animDelay:300
},options);
const offset = parseInt(options.offset, 10);
this.options.offset = isNaN(offset) ? 0 : offset;
this.init();
}
Sticky.prototype.init = function (){
let transition = '';
if(this.options.useTransition && supportsTransition){
transition = `top ${this.options.animTime}ms ease-in-out`;
this.options.animate = false;
}
this.parent.css({position:'relative'});
this.element
.addClass('sticky-scroll')
.css({
transition,
position:'relative'
});
this.element.trigger(events.created);
this.update();
};
/**
* This will handle any resizing of the container the sticky scroll is in and update the boundaries if necessary
*/
Sticky.prototype.update = function (){
this.setBoundaries(0);
this.moveIt();
this.element.trigger(events.update);
};
/**
* This will decide whether to move the stickied item
*/
Sticky.prototype.moveIt = function (){
const scrollTop = (win.document.documentElement.scrollTop || win.document.body.scrollTop) + this.options.offset;
const height = this.element.outerHeight(true);
const realStop = this._stop - height;
if(this._parentHeight - this._offset > height && !this._frozen){
if(scrollTop >= this._start && scrollTop <= realStop){
// Element is between top and bottom
this.updateOffset(scrollTop - this._start);
this._stopped = false;
} else {
if(scrollTop < this._start){
// Element is at top
this.updateOffset(0);
if(!this._stopped){
this.element.trigger(events.top);
}
this._stopped = true;
} else if(scrollTop > realStop){
// Element is at bottom
this.updateOffset(this._parentHeight - height - this._offset);
if(!this._stopped){
this.element.trigger(events.bottom);
}
this._stopped = true;
}
}
}
};
/**
* This will set the boundaries the stickied item can move between and it's left position
* @param {Number} [offset] Manually set the element offset
*/
Sticky.prototype.setBoundaries = function (offset){
this._offset = typeof offset === 'undefined' ? this.element.position().top : offset;
this._start = this.parent.offset().top + this._offset;
this._parentHeight = this.parent[0].offsetHeight;
this._stop = this._start + this._parentHeight - this._offset;
};
/**
* Update the Y offset to calculate against
* @param {Number} newOffset
*/
Sticky.prototype.setOffset = function (newOffset){
newOffset = parseInt(newOffset, 10);
if(!isNaN(newOffset)){
this.options.offset = newOffset;
this.moveIt();
}
};
/**
* Update Stickied Element's offset thereby moving it up/down the page
* @param {Number} yOffset
*/
Sticky.prototype.updateOffset = function (yOffset){
if(this._lastPosition !== yOffset){
if(this.options.animate){
this.element.stop(true,false).delay(this.options.animDelay).animate({
top:yOffset
},this.animTime);
} else {
this.element.css('top',yOffset);
}
this._lastPosition = yOffset;
}
};
/**
* This will freeze/unfreeze the stickied item
*/
Sticky.prototype.toggleFreeze = function (){
this._frozen = !this._frozen;
this.element.stop(true,false);
if(!this._frozen){
this.element.trigger(events.unfrozen);
this.moveIt();
} else {
this.element.trigger(events.frozen);
}
};
$.fn.sticky = function (parentName,options){
const method = parentName;
let ret = false;
this.each(function (){
// eslint-disable-next-line no-invalid-this
const self = $(this);
let instance = self.data('stickyInstance');
if(instance && (options || method)){
if(typeof options === 'object'){
ret = $.extend(instance.options,options);
} else if(options === 'options'){
ret = instance.options;
} else if(typeof instance[method] === 'function'){
ret = instance[method](options);
} else {
console.error(`Sticky Element has no option/method named ${method}`);
}
} else {
let parent = null;
if(parent){
parent = self.parent().closest(parent);
} else {
parent = self.parent();
}
instance = new Sticky(self,parent,options || {});
self.data('stickyInstance',instance);
$.fn.sticky._instances.push(instance);
}
});
return ret || this;
};
/**
* Update the position/offset changed on resize and move, applies to all instances
*/
function updateAll(){
const len = $.fn.sticky._instances.length;
for(let i = 0; i < len; i++){
$.fn.sticky._instances[i].update();
}
}
$.fn.sticky._instances = [];
$.fn.sticky.updateAll = updateAll;
$(win).on({
resize(){
// Update the boundaries is the browser window is resized
updateAll();
},
scroll(){
// Move each unfrozen instance on scroll
const len = $.fn.sticky._instances.length;
for(let i = 0; i < len; i++){
const element = $.fn.sticky._instances[i];
if(!element._frozen){
element.moveIt();
}
}
}
});
$(win.document).on({
ready(){
// Start an interval to check the heights of sticky elements and update boundaries if necessary
win.setInterval(function (){
requestFrame(function (){
const len = $.fn.sticky._instances.length;
for(let i = 0; i < len; i++){
const element = $.fn.sticky._instances[i];
if(element._parentHeight !== element.parent[0].offsetHeight){
element.update();
}
}
});
},1000);
}
});
}(jQuery,window));