diff --git a/src/jquery.mobile-events.js b/src/jquery.mobile-events.js index 2756c5c..a5dbd8c 100644 --- a/src/jquery.mobile-events.js +++ b/src/jquery.mobile-events.js @@ -1,10 +1,12 @@ /*! * jQuery Mobile Events - * by Ben Major (www.ben-major.co.uk) * - * Copyright 2011, Ben Major + * @author Ben Major (www.ben-major.co.uk) + * @copyright 2011, Ben Major + * @license MIT + * * Licensed under the MIT License: - * + * * 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 @@ -22,718 +24,424 @@ * 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. - * + * */ (function($) { - $.attrFn = $.attrFn || {}; - - // navigator.userAgent.toLowerCase() isn't reliable for Chrome installs - // on mobile devices. As such, we will create a boolean isChromeDesktop - // The reason that we need to do this is because Chrome annoyingly - // purports support for touch events even if the underlying hardware - // does not! - - var isChromeDesktop = ((navigator.userAgent.toLowerCase().indexOf('chrome') > -1) && ( - (navigator.userAgent.toLowerCase().indexOf('windows') > -1) || - (navigator.userAgent.toLowerCase().indexOf('macintosh') > -1) || - (navigator.userAgent.toLowerCase().indexOf('linux') > -1) - )); - - var settings = { - swipe_h_threshold : 50, - swipe_v_threshold : 50, - taphold_threshold : 750, - doubletap_int : 500, - - touch_capable : ('ontouchstart' in document.documentElement && !isChromeDesktop), - orientation_support : ('orientation' in window && 'onorientationchange' in window), - - startevent : ('ontouchstart' in document.documentElement && !isChromeDesktop) ? 'touchstart' : 'mousedown', - endevent : ('ontouchstart' in document.documentElement && !isChromeDesktop) ? 'touchend' : 'mouseup', - moveevent : ('ontouchstart' in document.documentElement && !isChromeDesktop) ? 'touchmove' : 'mousemove', - tapevent : ('ontouchstart' in document.documentElement && !isChromeDesktop) ? 'tap' : 'click', - scrollevent : ('ontouchstart' in document.documentElement && !isChromeDesktop) ? 'touchmove' : 'scroll', - - hold_timer : null, - tap_timer : null - }; - - // Add Event shortcuts: - $.each(('tapstart tapend tap singletap doubletap taphold swipe swipeup swiperight swipedown swipeleft swipeend scrollstart scrollend orientationchange').split(' '), function(i, name) { - $.fn[name] = function(fn) - { - return fn ? this.bind(name, fn) : this.trigger(name); - }; - - $.attrFn[name] = true; - }); - - // tapstart Event: - $.event.special.tapstart = { - setup: function() { - var thisObject = this, - $this = $(thisObject); - - $this.bind(settings.startevent, function(e) { - if(e.which && e.which !== 1) - { - return false; - } - else - { - // Touch event data: - var origEvent = e.originalEvent; - var touchData = { - 'position': { - 'x': (settings.touch_capable) ? origEvent.touches[0].screenX : e.screenX, - 'y': (settings.touch_capable) ? origEvent.touches[0].screenY : e.screenY, - }, - 'offset': { - 'x': (settings.touch_capable) ? origEvent.touches[0].pageX - origEvent.touches[0].target.offsetLeft : e.offsetX, - 'y': (settings.touch_capable) ? origEvent.touches[0].pageY - origEvent.touches[0].target.offsetTop : e.offsetY, - }, - 'time': new Date().getTime(), - 'target': e.target - }; - - triggerCustomEvent(thisObject, 'tapstart', e, touchData); - return true; - } - }); - } - }; - - // tapend Event: - $.event.special.tapend = { - setup: function() { - var thisObject = this, - $this = $(thisObject); - - $this.bind(settings.endevent, function(e) { - // Touch event data: - var origEvent = e.originalEvent; - var touchData = { - 'position': { - 'x': (settings.touch_capable) ? origEvent.changedTouches[0].screenX : e.screenX, - 'y': (settings.touch_capable) ? origEvent.changedTouches[0].screenY : e.screenY - }, - 'offset': { - 'x': (settings.touch_capable) ? origEvent.changedTouches[0].pageX - origEvent.changedTouches[0].target.offsetLeft : e.offsetX, - 'y': (settings.touch_capable) ? origEvent.changedTouches[0].pageY - origEvent.changedTouches[0].target.offsetTop : e.offsetY - }, - 'time': new Date().getTime(), - 'target': e.target - }; - triggerCustomEvent(thisObject, 'tapend', e, touchData); - return true; - }); - } - }; - - // taphold Event: - $.event.special.taphold = { - setup: function() { - var thisObject = this, - $this = $(thisObject), - origTarget, - timer, - start_pos = { x : 0, y : 0 }; - - $this.bind(settings.startevent, function(e) { - if(e.which && e.which !== 1) - { - return false; - } - else - { - $this.data('tapheld', false); - origTarget = e.target; - - var origEvent = e.originalEvent; - var start_time = new Date().getTime(), - startPosition = { - 'x': (settings.touch_capable) ? origEvent.touches[0].screenX : e.screenX, - 'y': (settings.touch_capable) ? origEvent.touches[0].screenY : e.screenY - }, - startOffset = { - 'x': (settings.touch_capable) ? origEvent.touches[0].pageX - origEvent.touches[0].target.offsetLeft : e.offsetX, - 'y': (settings.touch_capable) ? origEvent.touches[0].pageY - origEvent.touches[0].target.offsetTop : e.offsetY - }; - - start_pos.x = (e.originalEvent.targetTouches) ? e.originalEvent.targetTouches[0].pageX : e.pageX; - start_pos.y = (e.originalEvent.targetTouches) ? e.originalEvent.targetTouches[0].pageY : e.pageY; - - settings.hold_timer = window.setTimeout(function() { - - var end_x = (e.originalEvent.targetTouches) ? e.originalEvent.targetTouches[0].pageX : e.pageX, - end_y = (e.originalEvent.targetTouches) ? e.originalEvent.targetTouches[0].pageY : e.pageY; - - if(e.target == origTarget && (start_pos.x == end_x && start_pos.y == end_y)) - { - $this.data('tapheld', true); - - var end_time = new Date().getTime(), - endPosition = { - 'x': (settings.touch_capable) ? origEvent.touches[0].screenX : e.screenX, - 'y': (settings.touch_capable) ? origEvent.touches[0].screenY : e.screenY - }, - endOffset = { - 'x': (settings.touch_capable) ? origEvent.touches[0].pageX - origEvent.touches[0].target.offsetLeft : e.offsetX, - 'y': (settings.touch_capable) ? origEvent.touches[0].pageY - origEvent.touches[0].target.offsetTop : e.offsetY - }; - duration = end_time - start_time; - - // Build the touch data: - var touchData = { - 'startTime': start_time, - 'endTime': end_time, - 'startPosition': startPosition, - 'startOffset': startOffset, - 'endPosition': endPosition, - 'endOffset': endOffset, - 'duration': duration, - 'target': e.target - } - - triggerCustomEvent(thisObject, 'taphold', e, touchData); - } - }, settings.taphold_threshold); - - return true; - } - }).bind(settings.endevent, function() { - $this.data('tapheld', false); - window.clearTimeout(settings.hold_timer); - }); - } - }; - - // doubletap Event: - $.event.special.doubletap = { - setup: function() { - var thisObject = this, - $this = $(thisObject), - origTarget, - action, - firstTap; - - $this.bind(settings.startevent, function(e) { - if(e.which && e.which !== 1) - { - return false; - } - else - { - $this.data('doubletapped', false); - origTarget = e.target; - var origEvent = e.originalEvent; - firstTap = { - 'position': { - 'x': (settings.touch_capable) ? origEvent.touches[0].screenX : e.screenX, - 'y': (settings.touch_capable) ? origEvent.touches[0].screenY : e.screenY - }, - 'offset': { - 'x': (settings.touch_capable) ? origEvent.touches[0].pageX - origEvent.touches[0].target.offsetLeft : e.offsetX, - 'y': (settings.touch_capable) ? origEvent.touches[0].pageY - origEvent.touches[0].target.offsetTop : e.offsetY - }, - 'time': new Date().getTime(), - 'target': e.target - }; - - return true; - } - }).bind(settings.endevent, function(e) { - var now = new Date().getTime(); - var lastTouch = $this.data('lastTouch') || now + 1; - var delta = now - lastTouch; - window.clearTimeout(action); - - if(delta < settings.doubletap_int && delta > 0 && (e.target == origTarget) && delta > 100) - { - $this.data('doubletapped', true); - window.clearTimeout(settings.tap_timer); - - // Now get the current event: - var lastTap = { - 'position': { - 'x': (settings.touch_capable) ? origEvent.touches[0].screenX : e.screenX, - 'y': (settings.touch_capable) ? origEvent.touches[0].screenY : e.screenY - }, - 'offset': { - 'x': (settings.touch_capable) ? origEvent.touches[0].pageX - origEvent.touches[0].target.offsetLeft : e.offsetX, - 'y': (settings.touch_capable) ? origEvent.touches[0].pageY - origEvent.touches[0].target.offsetTop : e.offsetY - }, - 'time': new Date().getTime(), - 'target': e.target - } - - var touchData = { - 'firstTap': firstTap, - 'secondTap': lastTap, - 'interval': lastTap.time - firstTap.time - }; - - triggerCustomEvent(thisObject, 'doubletap', e, touchData); - } - else - { - $this.data('lastTouch', now); - action = window.setTimeout(function(e){ window.clearTimeout(action); }, settings.doubletap_int, [e]); - } - $this.data('lastTouch', now); - }); - } - }; - - // singletap Event: - // This is used in conjuction with doubletap when both events are needed on the same element - $.event.special.singletap = { - setup: function() { - var thisObject = this, - $this = $(thisObject), - origTarget = null, - startTime = null, - start_pos = { x: 0, y: 0 }; - - $this.bind(settings.startevent, function(e) { - if(e.which && e.which !== 1) - { - return false; - } - else - { - startTime = new Date().getTime(); - origTarget = e.target; - - // Get the start x and y position: - start_pos.x = (e.originalEvent.targetTouches) ? e.originalEvent.targetTouches[0].pageX : e.pageX; - start_pos.y = (e.originalEvent.targetTouches) ? e.originalEvent.targetTouches[0].pageY : e.pageY; - return true; - } - }).bind(settings.endevent, function(e) { - - if(e.target == origTarget) - { - // Get the end point: - end_pos_x = (e.originalEvent.changedTouches) ? e.originalEvent.changedTouches[0].pageX : e.pageX; - end_pos_y = (e.originalEvent.changedTouches) ? e.originalEvent.changedTouches[0].pageY : e.pageY; - - settings.tap_timer = window.setTimeout(function() { - if(!$this.data('doubletapped') && !$this.data('tapheld') && (start_pos.x == end_pos_x) && (start_pos.y == end_pos_y)) - { - var origEvent = e.originalEvent; - var touchData = { - 'position': { - 'x': (settings.touch_capable) ? origEvent.changedTouches[0].screenX : e.screenX, - 'y': (settings.touch_capable) ? origEvent.changedTouches[0].screenY : e.screenY, - }, - 'offset': { - 'x': (settings.touch_capable) ? origEvent.changedTouches[0].pageX - origEvent.changedTouches[0].target.offsetLeft : e.offsetX, - 'y': (settings.touch_capable) ? origEvent.changedTouches[0].pageY - origEvent.changedTouches[0].target.offsetTop : e.offsetY, - }, - 'time': new Date().getTime(), - 'target': e.target - }; - triggerCustomEvent(thisObject, 'singletap', e, touchData); - } - }, settings.doubletap_int); - } - }); - } - }; - - // tap Event: - $.event.special.tap = { - setup: function() { - var thisObject = this, - $this = $(thisObject), - started = false, - origTarget = null, - start_time, - start_pos = { x : 0, y : 0 }; - - $this.bind(settings.startevent, function(e) { - if(e.which && e.which !== 1) - { - return false; - } - else - { - started = true; - start_pos.x = (e.originalEvent.targetTouches) ? e.originalEvent.targetTouches[0].pageX : e.pageX; - start_pos.y = (e.originalEvent.targetTouches) ? e.originalEvent.targetTouches[0].pageY : e.pageY; - start_time = new Date().getTime(); - origTarget = e.target; - return true; - } - }).bind(settings.endevent, function(e) { - // Only trigger if they've started, and the target matches: - var end_x = (e.originalEvent.targetTouches) ? e.originalEvent.changedTouches[0].pageX : e.pageX, - end_y = (e.originalEvent.targetTouches) ? e.originalEvent.changedTouches[0].pageY : e.pageY; - - if(origTarget == e.target && started && ((new Date().getTime() - start_time) < settings.taphold_threshold) && (start_pos.x == end_x && start_pos.y == end_y)) - { - var origEvent = e.originalEvent; - var touchData = { - 'position': { - 'x': (settings.touch_capable) ? origEvent.changedTouches[0].screenX : e.screenX, - 'y': (settings.touch_capable) ? origEvent.changedTouches[0].screenY : e.screenY, - }, - 'offset': { - 'x': (settings.touch_capable) ? origEvent.changedTouches[0].pageX - origEvent.changedTouches[0].target.offsetLeft : e.offsetX, - 'y': (settings.touch_capable) ? origEvent.changedTouches[0].pageY - origEvent.changedTouches[0].target.offsetTop : e.offsetY, - }, - 'time': new Date().getTime(), - 'target': e.target - }; - - triggerCustomEvent(thisObject, 'tap', e, touchData); - } - }); - } - }; - - // swipe Event (also handles swipeup, swiperight, swipedown and swipeleft): - $.event.special.swipe = { - setup: function() { - var thisObject = this, - $this = $(thisObject), - started = false, - hasSwiped = false, - originalCoord = { x: 0, y: 0 }, - finalCoord = { x: 0, y: 0 }, - startEvnt; - - // Screen touched, store the original coordinate - function touchStart(e) - { - originalCoord.x = (e.originalEvent.targetTouches) ? e.originalEvent.targetTouches[0].pageX : e.pageX; - originalCoord.y = (e.originalEvent.targetTouches) ? e.originalEvent.targetTouches[0].pageY : e.pageY; - finalCoord.x = originalCoord.x; - finalCoord.y = originalCoord.y; - started = true; - var origEvent = e.originalEvent; - // Read event data into our startEvt: - startEvnt = { - 'position': { - 'x': (settings.touch_capable) ? origEvent.touches[0].screenX : e.screenX, - 'y': (settings.touch_capable) ? origEvent.touches[0].screenY : e.screenY, - }, - 'offset': { - 'x': (settings.touch_capable) ? origEvent.touches[0].pageX - origEvent.touches[0].target.offsetLeft : e.offsetX, - 'y': (settings.touch_capable) ? origEvent.touches[0].pageY - origEvent.touches[0].target.offsetTop : e.offsetY, - }, - 'time': new Date().getTime(), - 'target': e.target - }; - - // For some reason, we need to add a 100ms pause in order to trigger swiping - // on Playbooks: - var dt = new Date(); - while ((new Date()) - dt < 100) { } - } - - // Store coordinates as finger is swiping - function touchMove(e) - { - finalCoord.x = (e.originalEvent.targetTouches) ? e.originalEvent.targetTouches[0].pageX : e.pageX; - finalCoord.y = (e.originalEvent.targetTouches) ? e.originalEvent.targetTouches[0].pageY : e.pageY; - window.clearTimeout(settings.hold_timer); - - var swipedir; - - // We need to check if the element to which the event was bound contains a data-xthreshold | data-vthreshold: - var ele_x_threshold = $this.attr('data-xthreshold'), - ele_y_threshold = $this.attr('data-ythreshold'), - h_threshold = (typeof ele_x_threshold !== 'undefined' && ele_x_threshold !== false && parseInt(ele_x_threshold)) ? parseInt(ele_x_threshold) : settings.swipe_h_threshold, - v_threshold = (typeof ele_y_threshold !== 'undefined' && ele_y_threshold !== false && parseInt(ele_y_threshold)) ? parseInt(ele_y_threshold) : settings.swipe_v_threshold; - - - if(originalCoord.y > finalCoord.y && (originalCoord.y - finalCoord.y > v_threshold)) { swipedir = 'swipeup'; } - if(originalCoord.x < finalCoord.x && (finalCoord.x - originalCoord.x > h_threshold)) { swipedir = 'swiperight'; } - if(originalCoord.y < finalCoord.y && (finalCoord.y - originalCoord.y > v_threshold)) { swipedir = 'swipedown'; } - if(originalCoord.x > finalCoord.x && (originalCoord.x - finalCoord.x > h_threshold)) { swipedir = 'swipeleft'; } - if(swipedir != undefined && started) - { - originalCoord.x = 0; - originalCoord.y = 0; - finalCoord.x = 0; - finalCoord.y = 0; - started = false; - - // Read event data into our endEvnt: - var origEvent = e.originalEvent; - endEvnt = { - 'position': { - 'x': (settings.touch_capable) ? origEvent.touches[0].screenX : e.screenX, - 'y': (settings.touch_capable) ? origEvent.touches[0].screenY : e.screenY, - }, - 'offset': { - 'x': (settings.touch_capable) ? origEvent.touches[0].pageX - origEvent.touches[0].target.offsetLeft : e.offsetX, - 'y': (settings.touch_capable) ? origEvent.touches[0].pageY - origEvent.touches[0].target.offsetTop : e.offsetY, - }, - 'time': new Date().getTime(), - 'target': e.target - }; - - // Calculate the swipe amount (normalized): - var xAmount = Math.abs(startEvnt.position.x - endEvnt.position.x), - yAmount = Math.abs(startEvnt.position.y - endEvnt.position.y); - - var touchData = { - 'startEvnt': startEvnt, - 'endEvnt': endEvnt, - 'direction': swipedir.replace('swipe', ''), - 'xAmount': xAmount, - 'yAmount': yAmount, - 'duration': endEvnt.time - startEvnt.time - } - hasSwiped = true; - $this.trigger('swipe', touchData).trigger(swipedir, touchData); - } - } - - function touchEnd(e) - { - if(hasSwiped) - { - // We need to check if the element to which the event was bound contains a data-xthreshold | data-vthreshold: - var ele_x_threshold = $this.attr('data-xthreshold'), - ele_y_threshold = $this.attr('data-ythreshold'), - h_threshold = (typeof ele_x_threshold !== 'undefined' && ele_x_threshold !== false && parseInt(ele_x_threshold)) ? parseInt(ele_x_threshold) : settings.swipe_h_threshold, - v_threshold = (typeof ele_y_threshold !== 'undefined' && ele_y_threshold !== false && parseInt(ele_y_threshold)) ? parseInt(ele_y_threshold) : settings.swipe_v_threshold; - - var origEvent = e.originalEvent; - endEvnt = { - 'position': { - 'x': (settings.touch_capable) ? origEvent.changedTouches[0].screenX : e.screenX, - 'y': (settings.touch_capable) ? origEvent.changedTouches[0].screenY : e.screenY, - }, - 'offset': { - 'x': (settings.touch_capable) ? origEvent.changedTouches[0].pageX - origEvent.changedTouches[0].target.offsetLeft : e.offsetX, - 'y': (settings.touch_capable) ? origEvent.changedTouches[0].pageY - origEvent.changedTouches[0].target.offsetTop : e.offsetY, - }, - 'time': new Date().getTime(), - 'target': e.target - }; - - // Read event data into our endEvnt: - if(startEvnt.position.y > endEvnt.position.y && (startEvnt.position.y - endEvnt.position.y > v_threshold)) { swipedir = 'swipeup'; } - if(startEvnt.position.x < endEvnt.position.x && (endEvnt.position.x - startEvnt.position.x > h_threshold)) { swipedir = 'swiperight'; } - if(startEvnt.position.y < endEvnt.position.y && (endEvnt.position.y - startEvnt.position.y > v_threshold)) { swipedir = 'swipedown'; } - if(startEvnt.position.x > endEvnt.position.x && (startEvnt.position.x - endEvnt.position.x > h_threshold)) { swipedir = 'swipeleft'; } - - // Calculate the swipe amount (normalized): - var xAmount = Math.abs(startEvnt.position.x - endEvnt.position.x), - yAmount = Math.abs(startEvnt.position.y - endEvnt.position.y); - - var touchData = { - 'startEvnt': startEvnt, - 'endEvnt': endEvnt, - 'direction': swipedir.replace('swipe', ''), - 'xAmount': xAmount, - 'yAmount': yAmount, - 'duration': endEvnt.time - startEvnt.time - } - $this.trigger('swipeend', touchData); - } - - started = false; - hasSwiped = false; - } - - $this.bind(settings.startevent, touchStart); - $this.bind(settings.moveevent, touchMove); - $this.bind(settings.endevent, touchEnd); - } - }; - - // scrollstart Event (also handles scrollend): - $.event.special.scrollstart = { - setup: function() { - var thisObject = this, - $this = $(thisObject), - scrolling, - timer; - - function trigger(event, state) - { - scrolling = state; - triggerCustomEvent(thisObject, scrolling ? 'scrollstart' : 'scrollend', event); - } - - // iPhone triggers scroll after a small delay; use touchmove instead - $this.bind(settings.scrollevent, function(event) { - if(!scrolling) - { - trigger(event, true); - } - - clearTimeout(timer); - timer = setTimeout(function() { trigger(event, false); }, 50); - }); - } - }; - - // This is the orientation change (largely borrowed from jQuery Mobile): - var win = $(window), - special_event, - get_orientation, - last_orientation, - initial_orientation_is_landscape, - initial_orientation_is_default, - portrait_map = { '0': true, '180': true }; - - if(settings.orientation_support) - { - var ww = window.innerWidth || $(window).width(), - wh = window.innerHeight || $(window).height(), - landscape_threshold = 50; - - initial_orientation_is_landscape = ww > wh && (ww - wh) > landscape_threshold; - initial_orientation_is_default = portrait_map[window.orientation]; - - if((initial_orientation_is_landscape && initial_orientation_is_default) || (!initial_orientation_is_landscape && !initial_orientation_is_default)) - { - portrait_map = { '-90': true, '90': true }; - } - } - - $.event.special.orientationchange = special_event = { - setup: function() { - // If the event is supported natively, return false so that jQuery - // will bind to the event using DOM methods. - if(settings.orientation_support) - { - return false; - } - - // Get the current orientation to avoid initial double-triggering. - last_orientation = get_orientation(); - - win.bind('throttledresize', handler); - return true; - }, - teardown: function() - { - if (settings.orientation_support) - { - return false; - } - - win.unbind('throttledresize', handler); - return true; - }, - add: function(handleObj) - { - // Save a reference to the bound event handler. - var old_handler = handleObj.handler; - - handleObj.handler = function(event) - { - event.orientation = get_orientation(); - return old_handler.apply(this, arguments); - }; - } - }; - - // If the event is not supported natively, this handler will be bound to - // the window resize event to simulate the orientationchange event. - function handler() - { - // Get the current orientation. - var orientation = get_orientation(); - - if(orientation !== last_orientation) - { - // The orientation has changed, so trigger the orientationchange event. - last_orientation = orientation; - win.trigger( "orientationchange" ); - } - } - - $.event.special.orientationchange.orientation = get_orientation = function() { - var isPortrait = true, - elem = document.documentElement; - - if(settings.orientation_support) - { - isPortrait = portrait_map[window.orientation]; - } - else - { - isPortrait = elem && elem.clientWidth / elem.clientHeight < 1.1; - } - - return isPortrait ? 'portrait' : 'landscape'; - }; - - // throttle Handler: - $.event.special.throttledresize = { - setup: function() - { - $(this).bind('resize', throttle_handler); - }, - teardown: function() - { - $(this).unbind('resize', throttle_handler); - } - }; - - var throttle = 250, - throttle_handler = function() - { - curr = (new Date()).getTime(); - diff = curr - lastCall; - - if(diff >= throttle) - { - lastCall = curr; - $(this).trigger('throttledresize'); - - } - else - { - if(heldCall) - { - window.clearTimeout(heldCall); - } - - // Promise a held call will still execute - heldCall = window.setTimeout(handler, throttle - diff); - } - }, - lastCall = 0, - heldCall, - curr, - diff; - - // Trigger a custom event: - function triggerCustomEvent( obj, eventType, event, touchData ) { - var originalType = event.type; - event.type = eventType; - - $.event.dispatch.call( obj, event, touchData ); - event.type = originalType; - } - - // Correctly bind anything we've overloaded: - $.each({ - scrollend: 'scrollstart', - swipeup: 'swipe', - swiperight: 'swipe', - swipedown: 'swipe', - swipeleft: 'swipe', - swipeend: 'swipe', - }, function(e, srcE, touchData) { - $.event.special[e] = - { - setup: function() { - $(this).bind(srcE, $.noop); - } - }; - }); - + $.attrFn = $.attrFn || {}; + + // navigator.userAgent.toLowerCase() isn't reliable for Chrome installs + // on mobile devices. As such, we will create a boolean isChromeDesktop + // The reason that we need to do this is because Chrome annoyingly + // purports support for touch events even if the underlying hardware + // does not! + + var isChromeDesktop = ((navigator.userAgent.toLowerCase().indexOf('chrome') > -1) && ( + (navigator.userAgent.toLowerCase().indexOf('windows') > -1) || + (navigator.userAgent.toLowerCase().indexOf('macintosh') > -1) || + (navigator.userAgent.toLowerCase().indexOf('linux') > -1) + )); + + var settings = { + taphold_threshold : 750, + tap_tremor : 10, + + touch_capable : ('ontouchstart' in document.documentElement && !isChromeDesktop), + orientation_support : ('orientation' in window && 'onorientationchange' in window), + + startevent : ('ontouchstart' in document.documentElement ) ? 'touchstart' : 'mousedown', + endevent : ('ontouchstart' in document.documentElement ) ? 'touchend' : 'mouseup', + moveevent : ('ontouchstart' in document.documentElement ) ? 'touchmove' : 'mousemove', + tapevent : ('ontouchstart' in document.documentElement ) ? 'tap' : 'click', + scrollevent : ('ontouchstart' in document.documentElement ) ? 'touchmove' : 'scroll', + + hold_timer : null, + tap_timer : null + }; + + // Add Event shortcuts: + $.each( ['tapstart', 'tapend', 'tap', 'taphold', 'scrollstart', 'scrollend', 'orientationchange'], function(i, name) { + $.fn[name] = function(fn) + { + return fn ? this.bind(name, fn) : this.trigger(name); + }; + + $.attrFn[name] = true; + }); + + // tapstart Event: + $.event.special.tapstart = { + setup: function() { + var thisObject = this, + $this = $(thisObject); + + $this.bind(settings.startevent, function(e) { + if(e.which && e.which !== 1) + { + return false; + } + else + { + // Touch event data: + var origEvent = e.originalEvent; + var touchData = { + 'position': { + 'x': (settings.touch_capable) ? origEvent.touches[0].screenX : e.screenX, + 'y': (settings.touch_capable) ? origEvent.touches[0].screenY : e.screenY + }, + 'offset': { + 'x': (settings.touch_capable) ? origEvent.touches[0].pageX - origEvent.touches[0].target.offsetLeft : e.offsetX, + 'y': (settings.touch_capable) ? origEvent.touches[0].pageY - origEvent.touches[0].target.offsetTop : e.offsetY + }, + 'time': new Date().getTime(), + 'target': e.target + }; + + triggerCustomEvent(thisObject, 'tapstart', e, touchData); + return true; + } + }); + } + }; + + // tapend Event: + $.event.special.tapend = { + setup: function() { + var thisObject = this, + $this = $(thisObject); + + $this.bind(settings.endevent, function(e) { + // Touch event data: + var origEvent = e.originalEvent; + var touchData = { + 'position': { + 'x': (settings.touch_capable) ? origEvent.changedTouches[0].screenX : e.screenX, + 'y': (settings.touch_capable) ? origEvent.changedTouches[0].screenY : e.screenY + }, + 'offset': { + 'x': (settings.touch_capable) ? origEvent.changedTouches[0].pageX - origEvent.changedTouches[0].target.offsetLeft : e.offsetX, + 'y': (settings.touch_capable) ? origEvent.changedTouches[0].pageY - origEvent.changedTouches[0].target.offsetTop : e.offsetY + }, + 'time': new Date().getTime(), + 'target': e.target + }; + triggerCustomEvent(thisObject, 'tapend', e, touchData); + return true; + }); + } + }; + + // taphold Event: + $.event.special.taphold = { + setup: function() { + var thisObject = this, + $this = $(thisObject), + origTarget, + timer, + start_pos = { x : 0, y : 0 }; + + $this.bind(settings.startevent, function(e) { + if(e.which && e.which !== 1) + { + return false; + } + else + { + $this.data('tapheld', false); + origTarget = e.target; + + var origEvent = e.originalEvent; + var start_time = new Date().getTime(), + startPosition = { + 'x': (settings.touch_capable) ? origEvent.touches[0].screenX : e.screenX, + 'y': (settings.touch_capable) ? origEvent.touches[0].screenY : e.screenY + }, + startOffset = { + 'x': (settings.touch_capable) ? origEvent.touches[0].pageX - origEvent.touches[0].target.offsetLeft : e.offsetX, + 'y': (settings.touch_capable) ? origEvent.touches[0].pageY - origEvent.touches[0].target.offsetTop : e.offsetY + }; + + start_pos.x = (e.originalEvent.targetTouches) ? e.originalEvent.targetTouches[0].pageX : e.pageX; + start_pos.y = (e.originalEvent.targetTouches) ? e.originalEvent.targetTouches[0].pageY : e.pageY; + + settings.hold_timer = window.setTimeout(function() { + + var end_x = (e.originalEvent.targetTouches) ? e.originalEvent.targetTouches[0].pageX : e.pageX, + end_y = (e.originalEvent.targetTouches) ? e.originalEvent.targetTouches[0].pageY : e.pageY, + shakeX = Math.abs( start_pos.x - end_x ) > settings.tap_tremor, + shakeY = Math.abs( start_pos.y - end_y ) > settings.tap_tremor, + shake = shakeY || shakeX; + + if( e.target == origTarget && !shake ) + { + $this.data('tapheld', true); + + var end_time = new Date().getTime(), + endPosition = { + 'x': (settings.touch_capable) ? origEvent.touches[0].screenX : e.screenX, + 'y': (settings.touch_capable) ? origEvent.touches[0].screenY : e.screenY + }, + endOffset = { + 'x': (settings.touch_capable) ? origEvent.touches[0].pageX - origEvent.touches[0].target.offsetLeft : e.offsetX, + 'y': (settings.touch_capable) ? origEvent.touches[0].pageY - origEvent.touches[0].target.offsetTop : e.offsetY + }; + duration = end_time - start_time; + + // Build the touch data: + var touchData = { + 'startTime': start_time, + 'endTime': end_time, + 'startPosition': startPosition, + 'startOffset': startOffset, + 'endPosition': endPosition, + 'endOffset': endOffset, + 'duration': duration, + 'target': e.target + } + + triggerCustomEvent(thisObject, 'taphold', e, touchData); + } + }, settings.taphold_threshold); + + return true; + } + }).bind(settings.endevent, function() { + $this.data('tapheld', false); + window.clearTimeout(settings.hold_timer); + }); + } + }; + + // tap Event: + $.event.special.tap = { + setup: function() { + var self = this, + $this = $( this ), + started = false, + origTarget = null, + start_time, + start_pos = { x : 0, y : 0 }; + + $this.bind(settings.startevent, function(e) { + if(e.which && e.which !== 1) + { + return false; + } + else + { + started = true; + start_pos.x = (e.originalEvent.targetTouches) ? e.originalEvent.targetTouches[0].pageX : e.pageX; + start_pos.y = (e.originalEvent.targetTouches) ? e.originalEvent.targetTouches[0].pageY : e.pageY; + start_time = new Date().getTime(); + origTarget = e.target; + return true; + } + }).bind(settings.endevent, function(e) { + // Only trigger if they've started, and the target matches: + var end_x = (e.originalEvent.targetTouches) ? e.originalEvent.changedTouches[0].pageX : e.pageX, + end_y = (e.originalEvent.targetTouches) ? e.originalEvent.changedTouches[0].pageY : e.pageY, + shakeX = Math.abs( start_pos.x - end_x ) > settings.tap_tremor, + shakeY = Math.abs( start_pos.y - end_y ) > settings.tap_tremor, + shake = shakeY || shakeX; + + if(origTarget == e.target && started && ((new Date().getTime() - start_time) < settings.taphold_threshold) && !shake ) + { + var origEvent = e.originalEvent; + var touchData = { + 'position': { + 'x': (settings.touch_capable) ? origEvent.changedTouches[0].screenX : e.screenX, + 'y': (settings.touch_capable) ? origEvent.changedTouches[0].screenY : e.screenY + }, + 'offset': { + 'x': (settings.touch_capable) ? origEvent.changedTouches[0].pageX - origEvent.changedTouches[0].target.offsetLeft : e.offsetX, + 'y': (settings.touch_capable) ? origEvent.changedTouches[0].pageY - origEvent.changedTouches[0].target.offsetTop : e.offsetY + }, + 'time': new Date().getTime(), + 'target': e.target + }; + + triggerCustomEvent( self, 'tap', e, touchData); + } + }); + } + }; + + // scrollstart Event (also handles scrollend): + $.event.special.scrollstart = { + setup: function() { + var thisObject = this, + $this = $(thisObject), + scrolling, + timer; + + function trigger(event, state) + { + scrolling = state; + triggerCustomEvent(thisObject, scrolling ? 'scrollstart' : 'scrollend', event); + } + + // iPhone triggers scroll after a small delay; use touchmove instead + $this.bind(settings.scrollevent, function(event) { + if(!scrolling) + { + trigger(event, true); + } + + clearTimeout(timer); + timer = setTimeout(function() { trigger(event, false); }, 50); + }); + } + }; + + // This is the orientation change (largely borrowed from jQuery Mobile): + var win = $(window), + special_event, + get_orientation, + last_orientation, + initial_orientation_is_landscape, + initial_orientation_is_default, + portrait_map = { '0': true, '180': true }; + + if(settings.orientation_support) + { + var ww = window.innerWidth || $(window).width(), + wh = window.innerHeight || $(window).height(), + landscape_threshold = 50; + + initial_orientation_is_landscape = ww > wh && (ww - wh) > landscape_threshold; + initial_orientation_is_default = portrait_map[window.orientation]; + + if((initial_orientation_is_landscape && initial_orientation_is_default) || (!initial_orientation_is_landscape && !initial_orientation_is_default)) + { + portrait_map = { '-90': true, '90': true }; + } + } + + $.event.special.orientationchange = special_event = { + setup: function() { + // If the event is supported natively, return false so that jQuery + // will bind to the event using DOM methods. + if(settings.orientation_support) + { + return false; + } + + // Get the current orientation to avoid initial double-triggering. + last_orientation = get_orientation(); + + win.bind('throttledresize', handler); + return true; + }, + teardown: function() + { + if (settings.orientation_support) + { + return false; + } + + win.unbind('throttledresize', handler); + return true; + }, + add: function(handleObj) + { + // Save a reference to the bound event handler. + var old_handler = handleObj.handler; + + handleObj.handler = function(event) + { + event.orientation = get_orientation(); + return old_handler.apply(this, arguments); + }; + } + }; + + // If the event is not supported natively, this handler will be bound to + // the window resize event to simulate the orientationchange event. + function handler() + { + // Get the current orientation. + var orientation = get_orientation(); + + if(orientation !== last_orientation) + { + // The orientation has changed, so trigger the orientationchange event. + last_orientation = orientation; + win.trigger( "orientationchange" ); + } + } + + $.event.special.orientationchange.orientation = get_orientation = function() { + var isPortrait = true, + elem = document.documentElement; + + if(settings.orientation_support) + { + isPortrait = portrait_map[window.orientation]; + } + else + { + isPortrait = elem && elem.clientWidth / elem.clientHeight < 1.1; + } + + return isPortrait ? 'portrait' : 'landscape'; + }; + + // throttle Handler: + $.event.special.throttledresize = { + setup: function() + { + $(this).bind('resize', throttle_handler); + }, + teardown: function() + { + $(this).unbind('resize', throttle_handler); + } + }; + + var throttle = 250, + throttle_handler = function() + { + curr = (new Date()).getTime(); + diff = curr - lastCall; + + if(diff >= throttle) + { + lastCall = curr; + $(this).trigger('throttledresize'); + + } + else + { + if(heldCall) + { + window.clearTimeout(heldCall); + } + + // Promise a held call will still execute + heldCall = window.setTimeout(handler, throttle - diff); + } + }, + lastCall = 0, + heldCall, + curr, + diff; + + // Trigger a custom event: + function triggerCustomEvent( obj, eventType, event, touchData ) { + var originalType = event.type; + event.type = eventType; + + $.event.dispatch.call( obj, event, touchData ); + event.type = originalType; + } + + // Correctly bind anything we've overloaded: + + $.event.special.scrollend = { + setup: function () { + $(this).bind('scrollstart', $.noop); + } + }; + }) (jQuery);