UNPKG

@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
/** * 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); } } }