/** * @fileOverview * Copyright (c) 2013 Aaron Gloege * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without restriction, * including without limitation the rights to use, copy, modify, merge, * publish, distribute, sublicense, and/or sell copies of the Software, * and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE * OR OTHER DEALINGS IN THE SOFTWARE. * * jQuery Tap Plugin * Using the tap event, this plugin will properly simulate a click event * in touch browsers using touch events, and on non-touch browsers, * click will automatically be used instead. * * @author Aaron Gloege * @version 1.1.0 */ (function(document, $) { 'use strict'; /** * Event namespace * * @type String * @final */ var HELPER_NAMESPACE = '._tap'; /** * Event namespace * * @type String * @final */ var HELPER_ACTIVE_NAMESPACE = '._tapActive'; /** * Event name * * @type String * @final */ var EVENT_NAME = 'tap'; /** * Max distance between touchstart and touchend to be considered a tap * * @type Number * @final */ var MAX_TAP_DELTA = 10; /** * Max duration between touchstart and touchend to be considered a tap * * @type Number * @final */ var MAX_TAP_TIME = 400; /** * Event variables to copy to touches * * @type String[] * @final */ var EVENT_VARIABLES = 'clientX clientY screenX screenY pageX pageY'.split(' '); /** * jQuery body object * * @type jQuery */ var $BODY; /** * Last canceled tap event * * @type jQuery.Event * @private */ var _lastTap; /** * Last touchstart event * * @type jQuery.Event * @private */ var _lastTouch; /** * Object for tracking current touch * * @type Object * @static */ var TOUCH_VALUES = { /** * Number of touches currently active on touchstart * * @property count * @type Number */ count: 0, /** * touchstart/mousedown jQuery.Event object * * @property event * @type jQuery.Event */ event: 0 }; /** * Create a new event from the original event * Copy over EVENT_VARIABLES from the original jQuery.Event * * @param {String} type * @param {jQuery.Event} e * @return {jQuery.Event} * @private */ var _createEvent = function(type, e) { var originalEvent = e.originalEvent; var event = $.Event(originalEvent); event.type = type; var i = 0; var length = EVENT_VARIABLES.length; for (; i < length; i++) { event[EVENT_VARIABLES[i]] = e[EVENT_VARIABLES[i]]; } return event; }; /** * Determine if a valid tap event * * @param {jQuery.Event} e * @return {Boolean} * @private */ var _isTap = function(e) { if (e.isTrigger) { return false; } var startEvent = TOUCH_VALUES.event; var xDelta = Math.abs(e.pageX - startEvent.pageX); var yDelta = Math.abs(e.pageY - startEvent.pageY); var delta = Math.max(xDelta, yDelta); return ( e.timeStamp - startEvent.timeStamp < MAX_TAP_TIME && delta < MAX_TAP_DELTA && (!startEvent.touches || TOUCH_VALUES.count === 1) && Tap.isTracking ); }; /** * Determine if mousedown event was emulated from the last touchstart event * * @function * @param {jQuery.Event} e * @returns {Boolean} * @private */ var _isEmulated = function(e) { if (!_lastTouch) { return false; } var xDelta = Math.abs(e.pageX - _lastTouch.pageX); var yDelta = Math.abs(e.pageY - _lastTouch.pageY); var delta = Math.max(xDelta, yDelta); return ( Math.abs(e.timeStamp - _lastTouch.timeStamp) < 750 && delta < MAX_TAP_DELTA ); }; /** * Normalize touch events with data from first touch in the jQuery.Event * * This could be done using the `jQuery.fixHook` api, but to avoid conflicts * with other libraries that might already have applied a fix hook, this * approach is used instead. * * @param {jQuery.Event} event * @private */ var _normalizeEvent = function(event) { if (event.type.indexOf('touch') === 0) { event.touches = event.originalEvent.changedTouches; var touch = event.touches[0]; var i = 0; var length = EVENT_VARIABLES.length; for (; i < length; i++) { event[EVENT_VARIABLES[i]] = touch[EVENT_VARIABLES[i]]; } } // Normalize timestamp event.timeStamp = Date.now ? Date.now() : +new Date(); }; /** * Tap object that will track touch events and * trigger the tap event when necessary * * @class Tap * @static */ var Tap = { /** * Flag to determine if touch events are currently enabled * * @property isEnabled * @type Boolean */ isEnabled: false, /** * Are we currently tracking a tap event? * * @property isTracking * @type Boolean */ isTracking: false, /** * Enable touch event listeners * * @method enable */ enable: function() { if (Tap.isEnabled) { return; } Tap.isEnabled = true; // Set body element $BODY = $(document.body) .on('touchstart' + HELPER_NAMESPACE, Tap.onStart) .on('mousedown' + HELPER_NAMESPACE, Tap.onStart) .on('click' + HELPER_NAMESPACE, Tap.onClick); }, /** * Disable touch event listeners * * @method disable */ disable: function() { if (!Tap.isEnabled) { return; } Tap.isEnabled = false; // unbind all events with namespace $BODY.off(HELPER_NAMESPACE); }, /** * Store touch start values and target * * @method onTouchStart * @param {jQuery.Event} e */ onStart: function(e) { if (e.isTrigger) { return; } _normalizeEvent(e); if (e.touches) { TOUCH_VALUES.count = e.touches.length; } if (Tap.isTracking) { return; } if (!e.touches && _isEmulated(e)) { return; } Tap.isTracking = true; TOUCH_VALUES.event = e; if (e.touches) { _lastTouch = e; $BODY .on('touchend' + HELPER_NAMESPACE + HELPER_ACTIVE_NAMESPACE, Tap.onEnd) .on('touchcancel' + HELPER_NAMESPACE + HELPER_ACTIVE_NAMESPACE, Tap.onCancel); } else { $BODY.on('mouseup' + HELPER_NAMESPACE + HELPER_ACTIVE_NAMESPACE, Tap.onEnd); } }, /** * If touch has not been canceled, create a * tap event and trigger it on the target element * * @method onTouchEnd * @param {jQuery.Event} e */ onEnd: function(e) { var event; if (e.isTrigger) { return; } _normalizeEvent(e); if (_isTap(e)) { event = _createEvent(EVENT_NAME, e); _lastTap = event; $(TOUCH_VALUES.event.target).trigger(event); } // Cancel active tap tracking Tap.onCancel(e); }, /** * Cancel tap and remove event listeners for active tap tracking * * @method onTouchCancel * @param {jQuery.Event} e */ onCancel: function(e) { if (e && e.type === 'touchcancel') { e.preventDefault(); } Tap.isTracking = false; $BODY.off(HELPER_ACTIVE_NAMESPACE); }, /** * If tap was canceled, cancel click event * * @method onClick * @param {jQuery.Event} e * @return {void|Boolean} */ onClick: function(e) { if ( !e.isTrigger && _lastTap && _lastTap.isDefaultPrevented() && _lastTap.target === e.target && _lastTap.pageX === e.pageX && _lastTap.pageY === e.pageY && e.timeStamp - _lastTap.timeStamp < 750 ) { _lastTap = null; return false; } } }; // Enable tab when document is ready $(document).ready(Tap.enable); }(document, jQuery));