t3kkitz
1/17/2018 - 8:05 AM

sticky stack realization

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()