GianlucaGuarini/parallax

View on GitHub
src/index.js

Summary

Maintainability
A
0 mins
Test Coverage
import { $, $$, extend, isUndefined, elementData } from './helpers/helpers'
import Stage from './Stage'
import Canvas from './Canvas'
import o from 'riot-observable'

/**
 * There is no need to listen several times all the window events
 * with this class we listen them once and we subscribe/unsubscribe all the Parallax instances to the main events dispatcher
 * @type {Stage}
 */
let stage

/**
 * Parallax class
 * @param { String } selector - the tag selector where we will bind the parallax
 * @param { Object } opts - parallax options
 */
class Parallax {
  constructor(selector = null, opts = {}) {

    // make this object observable
    o(this)
    // set the options extending the _defaults
    this.opts = opts
    this.selector = selector
    this.canvases = []
    this.bound = false

    // allow to initialize without adding any dom elements
    if (selector !== null) {
      this.add(selector)
    }

    // lazy stage instance initialization
    if (!stage)
      stage = new Stage()

    return this
  }

  /**
   * Initialize the parallax
   * @returns { Object } - Parallax
   */
  init() {
    if (this.bound) {
      throw 'The parallax instance has already been initialized'
    }

    if (!this.canvases.length && this.selector !== null) {
      console.warn(`No elements were found with the selector "${this.selector}"`)
    } else {
      this.elementsLoaded = 0
      this.bind()
    }

    return this
  }

  /**
   * Bind the instance events setting all the callbacks
   * @returns { Object } - Parallax
   */
  bind() {

    // cache these function in order to unbind them when
    // this instance will be destroyed
    this._onResize = (...args) => this.resize.apply(this, args)
    this._onScroll = (...args) => this.scroll.apply(this, args)

    stage.on('resize', this._onResize)
    stage.on('scroll', this._onScroll)

    this.canvases.forEach((canvas) => {
      canvas.one('loaded', () => this.onCanvasLoaded(canvas))
      canvas.load()
    })

    this.bound = true

    return this
  }

  /**
   * Force manually a redraw
   * @returns { Object } - Parallax
   */
  refresh() {
    this._onResize(stage.size)
    this._onScroll(stage.scrollTop)
    return this
  }

  /**
   * Callback triggered once a canvas has loaded its element
   * @param   { Object } canvas - canvas instance
   * @returns { Object } - Parallax
   */
  onCanvasLoaded(canvas) {
    this.trigger('element:loaded', canvas.element, canvas)
    this.elementsLoaded++
    canvas.draw(stage)
    if (this.elementsLoaded == this.canvases.length) this.trigger('elements:loaded')
    return this
  }

  /**
   * Callback triggered on scroll
   * @param   { Number } scrollTop - page offset top
   * @returns { Object } - Parallax
   */
  scroll(scrollTop) {
    const offsetYBounds = this.opts.offsetYBounds,
      { height, width } = stage

    let i = this.canvases.length

    while (i--) {
      let canvas = this.canvases[i],
        canvasHeight = canvas.size.height,
        canvasOffset = canvas.offset

      if (
        canvas.isLoaded &&
          scrollTop + stage.height + offsetYBounds > canvasOffset.top &&
          canvasOffset.top + canvasHeight > scrollTop - offsetYBounds
      ) {
        canvas.draw({ height, scrollTop, width })
        this.trigger('draw', canvas.element)
      }

    }

    this.trigger('update', scrollTop)

    return this
  }

  /**
   * Add parallax elements to this parallax instance
   * @param { String|Array } els - DOM selector or node list
   * @returns { Object } - Parallax
   */
  add(els) {
    const canvases = this.createCanvases($$(els))

    if (this.bound) {
      canvases.forEach((canvas) => {
        canvas.one('loaded', () => this.onCanvasLoaded(canvas))
        canvas.load()
      })
    }

    this.canvases = this.canvases.concat(canvases)

    return this
  }

  /**
   * Remove parallax elements from this parallax instance
   * @param { String|Array } els - DOM selector or node list
   * @returns { Object } - Parallax
   */
  remove(els) {
    $$(els).forEach((el) => {
      let i = this.canvases.length
      while (i--) {
        if (el == this.canvases[i].element) {
          this.canvases[i].destroy()
          this.canvases.splice(i, 1)
          break
        }
      }
    })
    return this
  }

  /**
   * Kill all the internal and external callbacks listening this instance events
   * @returns { Object } - Parallax
   */
  destroy() {
    this.off('*')
    this.canvases = []
    stage.off('resize', this._onResize).off('scroll', this._onScroll)
    return this
  }

  /**
   * Callback triggered on window resize
   * @param   { Object } size - object containing the window width and height
   * @returns { Object } - Parallax
   */
  resize(size) {
    let i = this.canvases.length
    while (i--) {
      const canvas = this.canvases[i]
      if (!canvas.isLoaded) return
      canvas.update().draw(stage)
    }
    this.trigger('resize')
    return this
  }

  /**
   * Set the canvases instances
   * @param   { Array } els - list of the elements we want to parallax
   * @returns { Array } - list of canvas instances
   */
  createCanvases(els) {
    return els.map(el => {
      const data = elementData(el)
      return new Canvas(el, {
        intensity: !isUndefined(data.intensity) ? +data.intensity : this.opts.intensity,
        center: !isUndefined(data.center) ? +data.center : this.opts.center,
        safeHeight: !isUndefined(data.safeHeight) ? +data.safeHeight : this.opts.safeHeight
      })
    })
  }

  /**
   * The options will be always set extending the script _defaults
   * @param   { Object } opts - custom options
   */
  set opts(opts) {
    this._defaults = {
      offsetYBounds: 50,
      intensity: 30,
      center: 0.5,
      // make sure that the elements can always properly parallax
      // They should be at least 15% higher than their wrappers (7.5% bottom + 7.5% top)
      safeHeight: 0.15 // 15%
    }
    extend(this._defaults, opts)
  }

  /**
   * Get the script options object
   * @returns { Object } - current options
   */
  get opts() {
    return this._defaults
  }
}

export default Parallax