class UiSticky {
constructor(holder) {
this._holder = holder
this._parent = this._getParent()
this._elem = this._holder.firstElementChild
this._elemStyle = this._elem.style
this._elemHeight = this._elem.offsetHeight
this._holder.style.height = `${this._elemHeight}px`
this._position = this._holder.getAttribute('data-ui-sticky-position') || 'top'
this._stack = UiSticky.stack[this._position]
this._render()
document.addEventListener('scroll', this._render.bind(this))
window.addEventListener('resize', this._onResize.bind(this))
}
_render() {
this._holderRect = this._holder.getBoundingClientRect()
this._parentRect = this._parent.getBoundingClientRect()
if (this._matchCondition()) {
this._makeSticky()
this._slideOnEdge()
} else this._clearSticky()
}
_matchCondition() {
let stackOffset
if (this._position === 'top') {
!this._isSticky
? stackOffset = this._stack.offsetHeight
: stackOffset = this._stack.offsetHeight - this._elemHeight
return this._holderRect.top - stackOffset < 0 && this._parentRect.bottom > 0
} else {
!this._isSticky
? stackOffset = this._holderRect.bottom + this._stack.offsetHeight > UiSticky._viewHeight
: stackOffset = this._holderRect.bottom + this._stack.offsetHeight - this._elemHeight > UiSticky._viewHeight
return stackOffset && this._parentRect.top < UiSticky._viewHeight
}
}
_slideOnEdge() {
let slideOffset
this._stackStyle = this._stack.style
this._position === 'top'
? slideOffset = this._parentRect.bottom - this._stack.offsetHeight
: slideOffset = UiSticky._viewHeight - (this._parentRect.top + this._stack.offsetHeight)
if (slideOffset < 0) {
if (this._position !== 'top') slideOffset *= -1
this._stackStyle.transform = `translateY(${slideOffset}px)`
} else this._stackStyle.transform = 'none'
}
_makeSticky() {
if (this._isSticky) return
this._alignX()
this._stack.appendChild(this._elem)
this._elem.classList.add('ui-sticky__elem_sticked')
this._isSticky = true
}
_clearSticky() {
if (!this._isSticky) return
this._elemStyle.marginLeft = ''
this._elemStyle.marginRight = ''
this._holder.appendChild(this._elem)
this._elem.classList.remove('ui-sticky__elem_sticked')
this._isSticky = false
}
_alignX() {
this._elemStyle.marginLeft = `${this._holderRect.left}px`
this._elemStyle.marginRight = `${UiSticky._viewWidth - this._holderRect.right}px`
}
_getParent() {
const customParent = this._holder.getAttribute('data-ui-sticky-parent')
let parent
customParent
? parent = document.querySelector(customParent)
: parent = this._holder.offsetParent
return parent
}
_onResize() {
UiSticky._updateViewMetrics()
this._render()
}
static _updateViewMetrics() {
UiSticky._viewHeight = document.documentElement.clientHeight
UiSticky._viewWidth = document.documentElement.clientWidth
}
static _createStacks() {
const stack_top = document.createElement('div')
const stack_bottom = document.createElement('div')
const fragment = document.createDocumentFragment()
stack_top.className = 'ui-sticky__stack ui-sticky__stack_top'
stack_bottom.className = 'ui-sticky__stack ui-sticky__stack_bottom'
fragment.appendChild(stack_top)
fragment.appendChild(stack_bottom)
document.body.appendChild(fragment)
UiSticky.stack = {
top: stack_top,
bottom: stack_bottom
}
}
static init() {
UiSticky._createStacks()
UiSticky._updateViewMetrics()
document.querySelectorAll('.ui-sticky').forEach(holder => new this(holder))
}
}
// TODO add debounce and throttle
UiSticky.init()