prantlf/jquery.mousehover

View on GitHub
jquery.mousehover.js

Summary

Maintainability
A
1 hr
Test Coverage
/*
 * jquery.mousehover 0.2.1
 * https://github.com/prantlf/jquery.mousehover
 *
 * Copyright (c) 2017 Ferdinand Prantl
 * Licensed under the MIT license.
 */

(function (factory) {
  if (typeof define === 'function' && define.amd) {
    define(['jquery'], factory);
  } else if (typeof exports === 'object') {
    factory(require('jquery'));
  } else {
    factory(jQuery);
  }
}(function ($) {
  'use strict';

  // Remember the existing mousehover plugin, if there is any, to be able
  // to restore it by calling noConflict.
  var oldMousehover = $.fn.mousehover,
      eventTimeProperty;

  // Convert any combination of input parametrs to the parameters object,
  // which has the same structure and all values available.
  // Arguments:
  // .mousehover(handlerIn)
  // .mousehover(handlerIn, handlerOut)
  // .mousehover(handlerIn, options)
  // .mousehover(handlerIn, handlerOut, options)
  // .mousehover({handlerIn, handlerOut, options})
  // .mousehover('off')
  // .mousehover('off', options)
  // Result:
  // .handlerIn:  function
  // .handlerOut: function
  // .namespace:  string (empty or starting with '.')
  function normalizeParameters(handlerIn, handlerOut, options) {
    var namespace;
    if (typeof handlerIn === 'object') {
      return handlerIn;
    }
    // The first parameter has to be always present. Function handler,
    // string method name, or an object literal.
    if (!handlerIn) {
      throw new Error('Missing event handler or method.');
    }
    if (!options) {
      options = typeof handlerOut === 'object' ? handlerOut : {};
    }
    // Prepend '.' to allow simple concatenation of the namespace.
    namespace = options.namespace;
    namespace = namespace ? '.' + namespace : '';
    // There is just one method supported right now - "off". It will be
    // detected by the missing "handlerIn"" parameter.
    if (typeof handlerIn === 'string') {
      if (handlerIn !== 'off') {
        throw new Error('Unsupported method.')
      }
      return {
        namespace: namespace
      };
    }
    return {
      handlerIn: handlerIn,
      handlerOut: typeof handlerOut === 'function' ? handlerOut : handlerIn,
      namespace: namespace
    };
  }

  // If the browser supports pointer events, we can detect mouse reliably.
  if ('onpointerenter' in window) {
    $.fn.mousehover = function (handlerIn, handlerOut, options) {
      var parameters = normalizeParameters(handlerIn, handlerOut, options),
          namespace = parameters.namespace;
      handlerIn = parameters.handlerIn;
      // Missing "handlerIn" parameter means, that existing event handlers
      // should be unregistered.
      if (!handlerIn) {
        return this.off('pointerenter' + namespace +
                        ' pointerleave' + namespace);
      }
      handlerOut = parameters.handlerOut;
      return this.on('pointerenter' + namespace, function (event) {
                   if (event.originalEvent.pointerType === 'mouse') {
                     handlerIn.call(this, event);
                   }
                 })
                 .on('pointerleave' + namespace, function (event) {
                   if (event.originalEvent.pointerType === 'mouse') {
                     handlerOut.call(this, event);
                   }
                 });
    };

  // If the browser has support for touch events, the mouseenter and
  // mouseleave events can be emulated on tapping the display.
  } else if ('ontouchstart' in window) {
    eventTimeProperty = 'mousehover-start';
    $.fn.mousehover = function (handlerIn, handlerOut, options) {
      var parameters = normalizeParameters(handlerIn, handlerOut, options),
          namespace = parameters.namespace;
      handlerIn = parameters.handlerIn;
      // Missing "handlerIn" parameter means, that existing event handlers
      // should be unregistered.
      if (!handlerIn) {
        return this.off('touchend' + namespace +
                        ' mouseenter' + namespace +
                        ' mouseleave' + namespace);
      }
      handlerOut = parameters.handlerOut;
      // Store the time of the event, which would shortly preceed the
      // mouseenter event on touch-capable devices, if it were an emulated
      // mouse event caused by tapping the display.
      return this.on('touchend' + namespace, function () {
                   $(this).data(eventTimeProperty, new Date().getTime());
                 })
                 // If the first event handler remembered its time and the
                 // mouseenter event comes too soon, it was triggered by
                 // tapping and should be ignored.
                 .on('mouseenter' + namespace, function (event) {
                   var $this = $(this),
                       once = $this.data(eventTimeProperty),
                       now;
                   if (once) {
                     now = new Date().getTime();
                     // The time interval between the touchend event, which
                     // can identify, that the mouseenter event was emulated,
                     // and the mouseenter event itself can vary.
                     if (now - once < 50) {
                       return $(this).removeData(eventTimeProperty);
                     }
                   }
                   // Enable handling of the complementary mouseleave event.
                   $(this).data(eventTimeProperty, true);
                   handlerIn.call(this, event);
                 })
                 // The mouseleave event should call the hover event handler
                 // only if it was triggered by the mouseenter event earlier.
                 .on('mouseleave' + namespace, function (event) {
                   var $this = $(this);
                   if ($this.data(eventTimeProperty)) {
                     $this.removeData(eventTimeProperty);
                     handlerOut.call(this, event);
                   }
                 });
    };

  // If the browser has no support for touch events, we can assume, that
  // the only device, which can trigger the mouseenter eveny, is the mouse.
  } else {
    // Default to jQuery.hover behaviour on devices without touch capability.
    $.fn.mousehover = function (handlerIn, handlerOut, options) {
      var parameters = normalizeParameters(handlerIn, handlerOut, options),
          namespace = parameters.namespace;
      handlerIn = parameters.handlerIn;
      // Missing "handlerIn" parameter means, that existing event handlers
      // should be unregistered.
      if (!handlerIn) {
        return this.off('mouseenter' + namespace +
                        ' mouseleave' + namespace);
      }
      handlerOut = parameters.handlerOut;
      return this.on('mouseenter' + namespace, handlerIn)
                 .on('mouseleave' + namespace, handlerOut);
    };
  }

  // Restores the earlier mousehover plugin, which had been registered in
  // jQuery before this one. This plugin is returned for explicit usage.
  $.fn.mousehover.noConflict = function () {
    $.fn.mousehover = oldMousehover;
    return this;
  };

  return $;
}));