Home Manual Reference Source Repository

src/presets/data-wrapper.js

/* global document */
/**
 * @file presets/data-wrapper.js
 * @module  presets
 * @author Gregor Adams <greg@pixelass.com>
 */
import Glider from '../glider'
import {PLUGIN_DEFAULTS} from '../config'
import {
  findAll as $,
  findFirst as $$,
  parseObject,
  preventDefault
} from '../helpers'

const classNames = {
  pager: 'pager',
  dot: 'dot',
  active: 'active',
  previousButton: 'previousButton',
  nextButton: 'nextButton',
  disabled: 'disabled',
  dragging: 'dragging',
  draggable: 'draggable',
  caption: 'caption',
  headline: 'headline',
  subline: 'subline',
  description: 'description'
}

/**
 * Defaults for the data wrapper
 * @private
 * @type {object}
 */
const DATA_DEFAULTS = {
  PLUGIN_DEFAULTS,
  classNames: {
    ...PLUGIN_DEFAULTS.classNames,
    ...classNames
  }
}

const {documentElement} = document

/**
 * Autorunner. Continiously loops play. Disables when interacting or hovering
 * @param {Element} el
 * @param {contructor} instance
 * @param {number} duration
 * @param {number} rewind
 */
const autoRunner = (el, instance, duration, rewind) => {
  let running = true
  let down
  let inside
  let timer
  let runHandler
  let slideCount
  const autoplay = run => {
    global.requestAnimationFrame(run)
    return run
  }
  const runner = wait => {
    if (running) {
      timer = setTimeout(runner, duration)
      if (!wait) {
        if (slideCount === rewind) {
          slideCount = instance.goTo(0)
        } else {
          slideCount = instance.nextSlide()
        }
      }
    } else {
      timer = clearTimeout(timer)
    }
  }
  // Defines a waiting runner.
  // Does not trigger but calls the timeout
  const waitingRunner = () => runner(true)

  el.addEventListener('mouseenter', () => {
    running = false
    inside = true
    global.cancelAnimationFrame(runHandler)
    timer = clearTimeout(timer)
  })

  el.addEventListener('mousedown', () => {
    running = false
    down = true
    inside = true
    global.cancelAnimationFrame(runHandler)
    timer = clearTimeout(timer)
  })

  el.addEventListener('mousemove', () => {
    running = false
    inside = true
    global.cancelAnimationFrame(runHandler)
    timer = clearTimeout(timer)
  })

  el.addEventListener('mouseleave', () => {
    inside = false
    if (!down) {
      running = true
      runHandler = autoplay(waitingRunner)
    }
  })

  document.addEventListener('mouseup', () => {
    if (down && !inside) {
      running = true
      runHandler = autoplay(waitingRunner)
    }
    down = false
  })

  document.addEventListener('visibilitychange', e => {
    const {hidden} = e.target
    if (hidden) {
      running = false
      global.cancelAnimationFrame(runHandler)
      timer = clearTimeout(timer)
    } else {
      running = true
      runHandler = autoplay(waitingRunner)
    }
  })
  // The first handler waits
  runHandler = autoplay(waitingRunner)
}

/**
 * Wraps Paraglider to apply pagers and navigation buttons and autoplay.
 * This wrapper simplifies the usage of Paraglider by offering some basic
 * functionality.
 * Data attributes are used to configure the plugin.
 * @param {Element} glider
 * @param {DATA_DEFAULTS} opts
 * @returns {function} returns the destroy method
 */
const dataWrapper = (glider, opts) => {
  // Get options from data attributes
  const data = parseObject(glider.dataset)
  // Find draggable elements to disable dragstart
  // This prevents images and anchors from affecting swiping
  const draggables = [
    ...$('img', glider),
    ...$('a', glider)
  ]
  const {
    pager,
    dot,
    nextButton,
    previousButton,
    caption,
    headline,
    subline,
    description
  } = opts.classNames
  // Elements potentially used by the wrapper
  const pagers = $(`.${pager}`, glider)
  const nextTrigger = $$(`.${nextButton}`, glider)
  const prevTrigger = $$(`.${previousButton}`, glider)
  // Get elements from context
  const dots = pagers.map(el => $$(`.${dot}`, el))
  const captions = $(`.${caption}`, glider)
  const headlines = captions.map(l => $$(`.${headline}`, l))
  const sublines = captions.map(l => $$(`.${subline}`, l))
  const descriptions = captions.map(l => $$(`.${description}`, l))
  // Flag do determine in clicks are allowed or blocked
  let block
  // Ensure instance identifier
  let instance = null
  // Collect handlers.
  // Allows removing them when destroying
  const pagerHandlers = []
  /**
   * Handle the previous button
   * @param {Event} e
   */
  const handlePrev = e => {
    if (block) {
      preventDefault(e)
    } else {
      block = true
      if (instance) {
        instance.prevSlide(e)
      }
    }
  }
  /**
   * Handle the next button
   * @param {Event} e
   */
  const handleNext = e => {
    if (block) {
      preventDefault(e)
    } else {
      block = true
      if (instance) {
        instance.nextSlide(e)
      }
    }
  }
  /**
   * Handle mousedown
   * Adds dragging class name
   */
  const handleMouseDown = () => documentElement.classList.add(opts.classNames.dragging)
  /**
   * Handle mouseup
   * Removes dragging class name
   */
  const handleMouseUp = () => documentElement.classList.remove(opts.classNames.dragging)
  /**
   * Paraglider instance
   *
   * A paraglider with several options.
   */
  instance = new Glider({
    ...DATA_DEFAULTS,
    ...opts,
    ...data,
    classNames: {...DATA_DEFAULTS.classNames, ...opts.classNames},
    onInit({previous, next, current, rest}, slides, options) {
      draggables.forEach(draggable => {
        draggable.addEventListener('dragstart', preventDefault)
      })
      if (options.autoplay) {
        autoRunner(glider, instance, options.autoplay, options.loop ? false : slides.length - 1)
      }
      pagers.forEach((pager, index) => {
        const handler = e => {
          preventDefault(e)
          if (!block) {
            block = true
            if (instance) {
              instance.goTo(index)
            }
          }
        }
        pager.addEventListener('click', handler)
        pagerHandlers.push(handler)
      })
      if (nextTrigger) {
        nextTrigger.addEventListener('click', handleNext)
      }
      if (prevTrigger) {
        prevTrigger.addEventListener('click', handlePrev)
      }
      if (options.enableSwipe !== 0) {
        slides.forEach(el => {
          el.classList.add(options.classNames.draggable)
          el.addEventListener('mousedown', handleMouseDown)
        })
      }
      document.addEventListener('mouseup', handleMouseUp)
      if (pagers[current]) {
        pagers[current].classList.add(options.classNames.active)
      }
      if (!options.loop && prevTrigger && nextTrigger) {
        if ((current === 0) && prevTrigger && nextTrigger) {
          prevTrigger.classList.add(options.classNames.disabled)
        } else if (current === (slides.length - 1)) {
          nextTrigger.classList.add(options.classNames.disabled)
        }
      }
      if (typeof opts.onInit === 'function') {
        opts.onInit({previous, next, current, rest}, {
          slides,
          captions,
          headlines,
          sublines,
          descriptions,
          pagers,
          dots,
          nextTrigger,
          prevTrigger
        }, options)
      }
    },
    onEnd({previous, next, current, rest}, slides, options) {
      const notCurrent = [previous, next, ...rest]
      notCurrent.forEach(id => {
        if (pagers[id]) {
          pagers[id].classList.remove(options.classNames.active)
        }
      })
      if (pagers[current]) {
        pagers[current].classList.remove(options.classNames.active)
      }
      if (!options.loop && prevTrigger && nextTrigger) {
        if (current === 0) {
          prevTrigger.classList.add(options.classNames.disabled)
          nextTrigger.classList.remove(options.classNames.disabled)
        } else if (current === (slides.length - 1)) {
          nextTrigger.classList.add(options.classNames.disabled)
          prevTrigger.classList.remove(options.classNames.disabled)
        } else {
          prevTrigger.classList.remove(options.classNames.disabled)
          nextTrigger.classList.remove(options.classNames.disabled)
        }
      }
      block = false
      if (typeof opts.onEnd === 'function') {
        opts.onEnd({previous, next, current, rest}, {
          slides,
          captions,
          headlines,
          sublines,
          descriptions,
          pagers,
          dots,
          nextTrigger,
          prevTrigger
        }, options)
      }
    },
    onSlide(progress, {previous, next, current, rest}, slides, options) {
      if (typeof opts.onSlide === 'function') {
        opts.onSlide(progress, {previous, next, current, rest}, {
          slides,
          captions,
          headlines,
          sublines,
          descriptions,
          pagers,
          dots,
          nextTrigger,
          prevTrigger
        }, options)
      }
    },
    onDestroy(options) {
      draggables.forEach(draggable => {
        draggable.removeEventListener('dragstart', preventDefault)
      })
      pagers.forEach((pager, index) => {
        pager.removeEventListener('click', pagerHandlers[index])
      })
      document.removeEventListener('mouseup', handleMouseUp)
      if (nextTrigger) {
        nextTrigger.removeEventListener('click', handleNext)
      }
      if (prevTrigger) {
        prevTrigger.removeEventListener('click', handlePrev)
      }
      if (options.enableSwipe !== 0) {
        $(`.${options.classNames.slide}`, glider).forEach(el => {
          el.classList.remove(options.classNames.draggable)
          el.removeEventListener('mousedown', handleMouseDown)
        })
      }
      if (typeof opts.onDestroy === 'function') {
        opts.onDestroy(options)
      }
    }
  })
  if (instance) {
    instance.init(glider)
    return instance.destroy
  }
}

export default dataWrapper