UNPKG

avvo-styleguide

Version:
272 lines (239 loc) 7.57 kB
const Container = require('./container') const Placeholder = require('./placeholder') const Sticky = require('./sticky') const PositionSticky = {} /** * Creates an instance of PositionSticky * * @param element * @returns {PositionSticky} */ PositionSticky.create = function create(element) { return Object.create(PositionSticky).init(element) } /** * Constructor method * * @param element {HTMLElement} * @returns {PositionSticky} */ PositionSticky.init = function init(element) { this.constructor = PositionSticky this.placeholder = Placeholder.create(element) this.sticky = Sticky.create(element, this) this.container = Container.create(element.parentNode) this.positionType = 'static' this.isTicking = false this.threshold = null this.leftPositionWhenAbsolute = null this.leftPositionWhenFixed = null this.latestKnownScrollY = global.pageYOffset this.setOffsetTop() this.setOffsetBottom() this.setLeftPositionWhenAbsolute() this.setLeftPositionWhenFixed() this.calcThreshold() this.subscribeToWindowScroll() this.subscribeToWindowResize() this.update() return this } /** * Sets the distance that the sticky element will have from the top of viewport * when it becomes sticky */ PositionSticky.setOffsetTop = function setOffsetTop() { this.offsetTop = this.container.borderTopWidth + this.container.paddingTop } /** * Sets the amount to subtract in #_canStickyFitInContainer and also sets the * distance that the sticky element will have from the bottom of its container * when it is positioned absolutely */ PositionSticky.setOffsetBottom = function setOffsetBottom() { this.offsetBottom = this.container.borderBottomWidth + this.container.paddingBottom } /** * Calculates the point where the sticky behaviour should start */ PositionSticky.calcThreshold = function calcThreshold() { this.threshold = this.getStickyDistanceFromDocumentTop() - this.offsetTop } /** * Gets the element's distance from its offset parent's left * and subtracts any horizontal margins and saves it */ PositionSticky.setLeftPositionWhenAbsolute = function setLeftPositionWhenAbsolute() { const marginLeft = parseFloat(this.sticky.$element.css('margin-left')) this.leftPositionWhenAbsolute = this.sticky.$element.position().left - marginLeft } /** * Gets the element's distance from document left and saves it * * @todo Write a test that is covering when the page is scrolled */ PositionSticky.setLeftPositionWhenFixed = function setLeftPositionWhenFixed() { const marginLeft = parseFloat(this.sticky.$element.css('margin-left')) this.leftPositionWhenFixed = (global.pageXOffset + this.sticky.$element.offset().left) - marginLeft } /** * Attaches #_onScroll method to Window.onscroll event */ PositionSticky.subscribeToWindowScroll = function subscribeToWindowScroll() { global.addEventListener('scroll', this.onScroll.bind(this)) } /** * Debounces the scroll event * * @see [Debouncing Scroll Events]{@link http://www.html5rocks.com/en/tutorials/speed/animations/#debouncing-scroll-events} * * @todo Don't run update when container is not visible */ PositionSticky.onScroll = function onScroll() { if (!this.isTicking) { this.latestKnownScrollY = global.pageYOffset this.isTicking = true global.requestAnimationFrame(this.update.bind(this)) } } /** * Attaches #_onResize method to Window.onresize event */ PositionSticky.subscribeToWindowResize = function subscribeToWindowResize() { global.addEventListener('resize', this.onResize.bind(this)) } /** * Debounces the resize event * * @see [Debouncing Scroll Events]{@link http://www.html5rocks.com/en/tutorials/speed/animations/#debouncing-scroll-events} * @instance * @private * * @todo Don't run update when container is not visible */ PositionSticky.onResize = function onResize() { if (!this.isTicking) { global.requestAnimationFrame(this.refresh.bind(this)) this.isTicking = true } } PositionSticky.isStatic = function isStatic() { return this.positionType === 'static' } PositionSticky.makeStatic = function makeStatic() { this.sticky.$element.css({ position: 'static', width: '', left: '', top: '', }) this.placeholder.$element.hide() this.positionType = 'static' } PositionSticky.isFixed = function isFixed() { return this.positionType === 'fixed' } PositionSticky.makeFixed = function makeFixed() { this.sticky.$element.css({ position: 'fixed', top: `${this.offsetTop}px`, bottom: '', left: `${this.leftPositionWhenFixed}px`, }) this.placeholder.$element.show() this.positionType = 'fixed' } PositionSticky.isAbsolute = function isAbsolute() { return this.positionType === 'absolute' } PositionSticky.makeAbsolute = function makeAbsolute() { this.sticky.$element.css({ position: 'absolute', top: 'auto', bottom: `${this.container.paddingBottom}px`, left: `${this.leftPositionWhenAbsolute}px`, }) this.placeholder.$element.show() this.positionType = 'absolute' } /** * This is the main method that runs on every animation frame during scroll. * It starts with checking whether the element is within the static range. * If not, it checks whether the element is within the fixed range. * Otherwise, it positions the element absolutely. */ PositionSticky.update = function update() { this.isTicking = false if (this.isBelowThreshold()) { if (!this.isStatic()) { this.makeStatic() } } else if (this.canStickyFitInContainer()) { if (!this.isFixed()) { this.makeFixed() } } else if (!this.isAbsolute()) { this.makeAbsolute() } } /** * Returns true when the page hasn't been scrolled to the threshold point yet. * Otherwise, returns false. * * @returns {boolean} */ PositionSticky.isBelowThreshold = function isBelowThreshold() { if (this.latestKnownScrollY < this.threshold) { return true } return false } /** * Checks whether the element can fit inside the visible portion of the container or not * * @returns {boolean} */ PositionSticky.canStickyFitInContainer = function canStickyFitInContainer() { return this.getAvailableSpaceInContainer() >= this.sticky.boundingBoxHeight } /** * Calculates the height of the visible portion of the container * that can be used to fit the sticky element * * @returns {number} */ PositionSticky.getAvailableSpaceInContainer = function getAvailableSpaceInContainer() { return this.container.$element[0].getBoundingClientRect().bottom - this.offsetBottom - this.offsetTop } /** * Calculates sticky element's total offset from the document top. * It uses placeholder if it is called when the sticky element is * not static (e.g. through #refresh) * * @returns {number} */ PositionSticky.getStickyDistanceFromDocumentTop = function getStickyDistanceFromDocumentTop() { const $element = (this.isStatic() ? this.sticky.$element : this.placeholder.$element) return $element.offset().top } /** * Re-measures the cached positions/dimensions that are used during scroll */ PositionSticky.refresh = function refresh() { this.sticky.refresh() this.placeholder.refresh() this.calcThreshold() this.isTicking = false if (!this.isStatic()) { this.makeStatic() if (this.isBelowThreshold()) return } if (this.canStickyFitInContainer()) { this.setLeftPositionWhenFixed() this.makeFixed() } else { this.setLeftPositionWhenAbsolute() this.makeAbsolute() } } module.exports = PositionSticky