stickybits
Version:
Stickybits is a lightweight alternative to `position: sticky` polyfills
564 lines (492 loc) β’ 17.1 kB
JavaScript
/**
stickybits - Stickybits is a lightweight alternative to `position: sticky` polyfills
@version v3.7.11
@link https://github.com/yowainwright/stickybits#readme
@author Jeff Wainwright <yowainwright@gmail.com> (https://jeffry.in)
@license MIT
**/
(function (factory) {
typeof define === 'function' && define.amd ? define(factory) :
factory();
}((function () { 'use strict';
function _extends() {
_extends = Object.assign ? Object.assign.bind() : function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
return _extends.apply(this, arguments);
}
/*
STICKYBITS π
--------
> a lightweight alternative to `position: sticky` polyfills π¬
--------
- each method is documented above it our view the readme
- Stickybits does not manage polymorphic functionality (position like properties)
* polymorphic functionality: (in the context of describing Stickybits)
means making things like `position: sticky` be loosely supported with position fixed.
It also means that features like `useStickyClasses` takes on styles like `position: fixed`.
--------
defaults π
--------
- version = `package.json` version
- userAgent = viewer browser agent
- target = DOM element selector
- noStyles = boolean
- offset = number
- parentClass = 'string'
- scrollEl = window || DOM element selector || DOM element
- stickyClass = 'string'
- stuckClass = 'string'
- useStickyClasses = boolean
- useFixed = boolean
- useGetBoundingClientRect = boolean
- verticalPosition = 'string'
- applyStyle = function
--------
propsπ
--------
- p = props {object}
--------
instance note
--------
- stickybits parent methods return this
- stickybits instance methods return an instance item
--------
nomenclature
--------
- target => el => e
- props => o || p
- instance => item => it
--------
methods
--------
- .definePosition = defines sticky or fixed
- .addInstance = an array of objects for each Stickybits Target
- .getClosestParent = gets the parent for non-window scroll
- .getTopPosition = gets the element top pixel position from the viewport
- .computeScrollOffsets = computes scroll position
- .toggleClasses = older browser toggler
- .manageState = manages sticky state
- .removeInstance = removes an instance
- .cleanup = removes all Stickybits instances and cleans up dom from stickybits
*/
var Stickybits = /*#__PURE__*/function () {
function Stickybits(target, obj) {
var _this = this;
var o = typeof obj !== 'undefined' ? obj : {};
this.version = '3.7.11';
this.userAgent = window.navigator.userAgent || 'no `userAgent` provided by the browser';
this.props = {
customStickyChangeNumber: o.customStickyChangeNumber || null,
noStyles: o.noStyles || false,
stickyBitStickyOffset: o.stickyBitStickyOffset || 0,
parentClass: o.parentClass || 'js-stickybit-parent',
scrollEl: typeof o.scrollEl === 'string' ? document.querySelector(o.scrollEl) : o.scrollEl || window,
stickyClass: o.stickyClass || 'js-is-sticky',
stuckClass: o.stuckClass || 'js-is-stuck',
stickyChangeClass: o.stickyChangeClass || 'js-is-sticky--change',
useStickyClasses: o.useStickyClasses || false,
useFixed: o.useFixed || false,
useGetBoundingClientRect: o.useGetBoundingClientRect || false,
verticalPosition: o.verticalPosition || 'top',
applyStyle: o.applyStyle || function (item, style) {
return _this.applyStyle(item, style);
}
};
/*
define positionVal after the setting of props, because definePosition looks at the props.useFixed
----
- uses a computed (`.definePosition()`)
- defined the position
*/
this.props.positionVal = this.definePosition() || 'fixed';
this.instances = [];
var _this$props = this.props,
positionVal = _this$props.positionVal,
verticalPosition = _this$props.verticalPosition,
noStyles = _this$props.noStyles,
stickyBitStickyOffset = _this$props.stickyBitStickyOffset;
var verticalPositionStyle = verticalPosition === 'top' && !noStyles ? stickyBitStickyOffset + "px" : '';
var positionStyle = positionVal !== 'fixed' ? positionVal : '';
this.els = typeof target === 'string' ? document.querySelectorAll(target) : target;
if (!('length' in this.els)) this.els = [this.els];
for (var i = 0; i < this.els.length; i++) {
var _styles;
var el = this.els[i];
var instance = this.addInstance(el, this.props); // set vertical position
this.props.applyStyle({
styles: (_styles = {}, _styles[verticalPosition] = verticalPositionStyle, _styles.position = positionStyle, _styles),
classes: {}
}, instance);
this.manageState(instance); // instances are an array of objects
this.instances.push(instance);
}
}
/*
setStickyPosition βοΈ
--------
β most basic thing stickybits does
=> checks to see if position sticky is supported
=> defined the position to be used
=> stickybits works accordingly
*/
var _proto = Stickybits.prototype;
_proto.definePosition = function definePosition() {
var stickyProp;
if (this.props.useFixed) {
stickyProp = 'fixed';
} else {
var prefix = ['', '-o-', '-webkit-', '-moz-', '-ms-'];
var test = document.head.style;
for (var i = 0; i < prefix.length; i += 1) {
test.position = prefix[i] + "sticky";
}
stickyProp = test.position ? test.position : 'fixed';
test.position = '';
}
return stickyProp;
}
/*
addInstance βοΈ
--------
β manages instances of items
- takes in an el and props
- returns an item object
---
- target = el
- o = {object} = props
- scrollEl = 'string' | object
- verticalPosition = number
- off = boolean
- parentClass = 'string'
- stickyClass = 'string'
- stuckClass = 'string'
---
- defined later
- parent = dom element
- state = 'string'
- offset = number
- stickyStart = number
- stickyStop = number
- returns an instance object
*/
;
_proto.addInstance = function addInstance(el, props) {
var _this2 = this;
var item = {
el: el,
parent: el.parentNode,
props: props
};
if (props.positionVal === 'fixed' || props.useStickyClasses) {
this.isWin = this.props.scrollEl === window;
var se = this.isWin ? window : this.getClosestParent(item.el, item.props.scrollEl);
this.computeScrollOffsets(item);
this.toggleClasses(item.parent, '', props.parentClass);
item.state = 'default';
item.stateChange = 'default';
item.stateContainer = function () {
return _this2.manageState(item);
};
se.addEventListener('scroll', item.stateContainer);
}
return item;
}
/*
--------
getParent π¨β
--------
- a helper function that gets the target element's parent selected el
- only used for non `window` scroll elements
- supports older browsers
*/
;
_proto.getClosestParent = function getClosestParent(el, match) {
// p = parent element
var p = match;
var e = el;
if (e.parentElement === p) return p; // traverse up the dom tree until we get to the parent
while (e.parentElement !== p) {
e = e.parentElement;
} // return parent element
return p;
}
/*
--------
getTopPosition
--------
- a helper function that gets the topPosition of a Stickybit element
- from the top level of the DOM
*/
;
_proto.getTopPosition = function getTopPosition(el) {
if (this.props.useGetBoundingClientRect) {
return el.getBoundingClientRect().top + (this.props.scrollEl.pageYOffset || document.documentElement.scrollTop);
}
var topPosition = 0;
do {
topPosition = el.offsetTop + topPosition;
} while (el = el.offsetParent);
return topPosition;
}
/*
computeScrollOffsets π
---
computeScrollOffsets for Stickybits
- defines
- offset
- start
- stop
*/
;
_proto.computeScrollOffsets = function computeScrollOffsets(item) {
var it = item;
var p = it.props;
var el = it.el;
var parent = it.parent;
var isCustom = !this.isWin && p.positionVal === 'fixed';
var isTop = p.verticalPosition !== 'bottom';
var scrollElOffset = isCustom ? this.getTopPosition(p.scrollEl) : 0;
var stickyStart = isCustom ? this.getTopPosition(parent) - scrollElOffset : this.getTopPosition(parent);
var stickyChangeOffset = p.customStickyChangeNumber !== null ? p.customStickyChangeNumber : el.offsetHeight;
var parentBottom = stickyStart + parent.offsetHeight;
it.offset = !isCustom ? scrollElOffset + p.stickyBitStickyOffset : 0;
it.stickyStart = isTop ? stickyStart - it.offset : 0;
it.stickyChange = it.stickyStart + stickyChangeOffset;
it.stickyStop = isTop ? parentBottom - (el.offsetHeight + it.offset) : parentBottom - window.innerHeight;
}
/*
toggleClasses βοΈ
---
toggles classes (for older browser support)
r = removed class
a = added class
*/
;
_proto.toggleClasses = function toggleClasses(el, r, a) {
var e = el;
var cArray = e.className.split(' ');
if (a && cArray.indexOf(a) === -1) cArray.push(a);
var rItem = cArray.indexOf(r);
if (rItem !== -1) cArray.splice(rItem, 1);
e.className = cArray.join(' ');
}
/*
manageState π
---
- defines the state
- normal
- sticky
- stuck
*/
;
_proto.manageState = function manageState(item) {
var _this3 = this;
// cache object
var it = item;
var p = it.props;
var state = it.state;
var stateChange = it.stateChange;
var start = it.stickyStart;
var change = it.stickyChange;
var stop = it.stickyStop; // cache props
var pv = p.positionVal;
var se = p.scrollEl;
var sticky = p.stickyClass;
var stickyChange = p.stickyChangeClass;
var stuck = p.stuckClass;
var vp = p.verticalPosition;
var isTop = vp !== 'bottom';
var aS = p.applyStyle;
var ns = p.noStyles;
/*
requestAnimationFrame
---
- use rAF
- or stub rAF
*/
var rAFStub = function rAFDummy(f) {
f();
};
var rAF = !this.isWin ? rAFStub : window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame || rAFStub;
/*
define scroll vars
---
- scroll
- notSticky
- isSticky
- isStuck
*/
var scroll = this.isWin ? window.scrollY || window.pageYOffset : se.scrollTop;
var notSticky = scroll > start && scroll < stop && (state === 'default' || state === 'stuck');
var isSticky = isTop && scroll <= start && (state === 'sticky' || state === 'stuck');
var isStuck = scroll >= stop && state === 'sticky';
/*
Unnamed arrow functions within this block
---
- help wanted or discussion
- view test.stickybits.js
- `stickybits .manageState `position: fixed` interface` for more awareness π
*/
if (notSticky) {
it.state = 'sticky';
} else if (isSticky) {
it.state = 'default';
} else if (isStuck) {
it.state = 'stuck';
}
var isStickyChange = scroll >= change && scroll <= stop;
var isNotStickyChange = scroll < change / 2 || scroll > stop;
if (isNotStickyChange) {
it.stateChange = 'default';
} else if (isStickyChange) {
it.stateChange = 'sticky';
} // Only apply new styles if the state has changed
if (state === it.state && stateChange === it.stateChange) return;
rAF(function () {
var _styles2, _classes, _styles3, _extends2, _classes2, _style$classes;
var stateStyles = {
sticky: {
styles: (_styles2 = {
position: pv,
top: '',
bottom: ''
}, _styles2[vp] = p.stickyBitStickyOffset + "px", _styles2),
classes: (_classes = {}, _classes[sticky] = true, _classes)
},
default: {
styles: (_styles3 = {}, _styles3[vp] = '', _styles3),
classes: {}
},
stuck: {
styles: _extends((_extends2 = {}, _extends2[vp] = '', _extends2), pv === 'fixed' && !ns || !_this3.isWin ? {
position: 'absolute',
top: '',
bottom: '0'
} : {}),
classes: (_classes2 = {}, _classes2[stuck] = true, _classes2)
}
};
if (pv === 'fixed') {
stateStyles.default.styles.position = '';
}
var style = stateStyles[it.state];
style.classes = (_style$classes = {}, _style$classes[stuck] = !!style.classes[stuck], _style$classes[sticky] = !!style.classes[sticky], _style$classes[stickyChange] = isStickyChange, _style$classes);
aS(style, item);
});
}
/*
applyStyle
---
- apply the given styles and classes to the element
*/
;
_proto.applyStyle = function applyStyle(_ref, item) {
var styles = _ref.styles,
classes = _ref.classes;
// cache object
var it = item;
var e = it.el;
var p = it.props;
var stl = e.style; // cache props
var ns = p.noStyles;
var cArray = e.className.split(' '); // Disable due to bug with old versions of eslint-scope and for ... in
// https://github.com/eslint/eslint/issues/12117
// eslint-disable-next-line no-unused-vars
for (var cls in classes) {
var addClass = classes[cls];
if (addClass) {
if (cArray.indexOf(cls) === -1) cArray.push(cls);
} else {
var idx = cArray.indexOf(cls);
if (idx !== -1) cArray.splice(idx, 1);
}
}
e.className = cArray.join(' ');
if (styles['position']) {
stl['position'] = styles['position'];
}
if (ns) return; // eslint-disable-next-line no-unused-vars
for (var key in styles) {
stl[key] = styles[key];
}
};
_proto.update = function update(updatedProps) {
var _this4 = this;
if (updatedProps === void 0) {
updatedProps = null;
}
this.instances.forEach(function (instance) {
_this4.computeScrollOffsets(instance);
if (updatedProps) {
// eslint-disable-next-line no-unused-vars
for (var updatedProp in updatedProps) {
instance.props[updatedProp] = updatedProps[updatedProp];
}
}
});
return this;
}
/*
removes an instance π
--------
- cleanup instance
*/
;
_proto.removeInstance = function removeInstance(instance) {
var _styles4, _classes3;
var e = instance.el;
var p = instance.props;
this.applyStyle({
styles: (_styles4 = {
position: ''
}, _styles4[p.verticalPosition] = '', _styles4),
classes: (_classes3 = {}, _classes3[p.stickyClass] = '', _classes3[p.stuckClass] = '', _classes3)
}, instance);
this.toggleClasses(e.parentNode, p.parentClass);
}
/*
cleanup π
--------
- cleans up each instance
- clears instance
*/
;
_proto.cleanup = function cleanup() {
for (var i = 0; i < this.instances.length; i += 1) {
var instance = this.instances[i];
if (instance.stateContainer) {
instance.props.scrollEl.removeEventListener('scroll', instance.stateContainer);
}
this.removeInstance(instance);
}
this.manageState = false;
this.instances = [];
};
return Stickybits;
}();
/*
export
--------
exports StickBits to be used π
*/
function stickybits(target, o) {
return new Stickybits(target, o);
}
if (typeof window !== 'undefined') {
var plugin = window.u;
if (plugin) {
plugin.prototype.stickybits = function stickybitsPlugin(opts) {
return stickybits(this, opts);
};
}
}
})));