avvo-styleguide
Version:
Avvo styleguide
272 lines (239 loc) • 7.57 kB
JavaScript
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