fix-wheel.js

/**
 * Mousewheel fix for certain browsers that just don't get it right
 * Includes:
 *
 * - fix for Safari 9:
 *   in Safari 9 the default event is debounced, therefore broken.
 *   The issue seems to occur due to the eleastic scroll and appeared with Safari 9
 *   which allows eleasic scroll in nested containers.
 *   to fix this issue the `html` and/or `body` element usually has to be set to `overflow: hidden;`.
 *   In many cases this comes with drawbacks or breaks other things.
 *   Preventing the default event and then applying the `deltaY` to the `scrollTop`
 *   of the rootNode fixes the issue without other hacks.
 *   - Attemt: prevent the mousewheel event and move the rootNode manually.
 *   - Concern: impact on performance?
 * @see  https://bugs.webkit.org/show_bug.cgi?id=149526
 * @author  Gregor Adams <greg@pixelass.com>
 * @license  MIT
 */

import browser from 'detect-browser'

/**
 * major browser version parsed from `detect-browser.js`'s result
 * @type {Number}
 */
const majorVersion = parseInt(browser.version.split('.')[0], 10)

/**
 * flag to find Safari 9 since it is known to have a bug
 * @see  https://bugs.webkit.org/show_bug.cgi?id=149526
 * @type {Boolean}
 */
const isSafari9 = (browser.name === 'safari' && majorVersion === 9)

/**
 * flag to determine if we need to modify the `mousewheel` event
 * currently only Safari 9 gets this wrong.
 * @type {Boolean}
 */
const mouseNeedsHelp = isSafari9

/**
 * allows to create an instance that can be initialised and destroyed when needed
 * usually initialized on `document.ready` or when mounting a root component
 * and destroyed when a root component is unmounted
 */
class FixWheel {
  constructor (eventName) {
    this.eventName = eventName
    this.fixWheel = this.fixWheel.bind(this)
  }

  /**
   * this method applys the fix for wheel events in browsers with faulty implementation
   * @param  {HTMLElement} rootNode a DOM node, usually `document.body` but can be any other root element.
   * @return {Boolean} returns `true` when applied, otherwise `false`.
   *                   Mainly used for debugging but can be used as a flag.
    */
  init (rootNode) {
    if (mouseNeedsHelp) {
      this.rootNode = rootNode
      window.addEventListener(this.eventName, this.fixWheel, false)
      this.isFixed = true
    }
    return this.isFixed
  }

  /**
   * destroys the helper. removes the fix if it has been applied
   * @return {Boolean} should always return `false`.
   *                   Mainly used for debugging but can be used as a flag.
   */
  destroy () {
    if (this.isFixed) {
      window.removeEventListener(this.eventName, this.fixWheel)
      this.isFixed = false
    }
    return this.isFixed
  }

  /**
   * prevent the default event
   * @param  {Object} e mousewheel event
   */
  preventDefault (e) {
    e.preventDefault()
  }

  /**
   * the actual fix for the issue.
   * @param  {Object} e mousewheel event
   * @param  {Number} e.deltaY wheel delta on the y-axis (if undefined simply does nothing)
   */
  fixWheel (e) {
    this.preventDefault(e)
    this.rootNode.scrollTop += e.deltaY
  }
}

export default FixWheel