(function( $, window, undefined ) { var $document = $( document ), supporttouch = "ontouchend" in document, scrollevent = "touchmove scroll", touchstartevent = supporttouch ? "touchstart" : "mousedown", touchstopevent = supporttouch ? "touchend" : "mouseup", touchmoveevent = supporttouch ? "touchmove" : "mousemove"; // setup new event shortcuts $.each( ( "touchstart touchmove touchend " + "tap taphold " + "swipe swipeleft swiperight " + "scrollstart scrollstop" ).split( " " ), function( i, name ) { $.fn[ name ] = function( fn ) { return fn ? this.bind( name, fn ) : this.trigger( name ); }; // jquery < 1.8 if ( $.attrfn ) { $.attrfn[ name ] = true; } }); function triggercustomevent( obj, eventtype, event, bubble ) { var originaltype = event.type; event.type = eventtype; if ( bubble ) { $.event.trigger( event, undefined, obj ); } else { $.event.dispatch.call( obj, event ); } event.type = originaltype; } // also handles scrollstop $.event.special.scrollstart = { enabled: true, setup: function() { var thisobject = this, $this = $( thisobject ), scrolling, timer; function trigger( event, state ) { scrolling = state; triggercustomevent( thisobject, scrolling ? "scrollstart" : "scrollstop", event ); } // iphone triggers scroll after a small delay; use touchmove instead $this.bind( scrollevent, function( event ) { if ( !$.event.special.scrollstart.enabled ) { return; } if ( !scrolling ) { trigger( event, true ); } cleartimeout( timer ); timer = settimeout( function() { trigger( event, false ); }, 50 ); }); }, teardown: function() { $( this ).unbind( scrollevent ); } }; // also handles taphold $.event.special.tap = { tapholdthreshold: 750, emittapontaphold: true, setup: function() { var thisobject = this, $this = $( thisobject ), istaphold = false; $this.bind( "vmousedown", function( event ) { istaphold = false; if ( event.which && event.which !== 1 ) { return false; } var origtarget = event.target, timer; function cleartaptimer() { cleartimeout( timer ); } function cleartaphandlers() { cleartaptimer(); $this.unbind( "vclick", clickhandler ) .unbind( "vmouseup", cleartaptimer ); $document.unbind( "vmousecancel", cleartaphandlers ); } function clickhandler( event ) { cleartaphandlers(); // only trigger a 'tap' event if the start target is // the same as the stop target. if ( !istaphold && origtarget === event.target ) { triggercustomevent( thisobject, "tap", event ); } else if ( istaphold ) { event.preventdefault(); } } $this.bind( "vmouseup", cleartaptimer ) .bind( "vclick", clickhandler ); $document.bind( "vmousecancel", cleartaphandlers ); timer = settimeout( function() { if ( !$.event.special.tap.emittapontaphold ) { istaphold = true; } triggercustomevent( thisobject, "taphold", $.event( "taphold", { target: origtarget } ) ); }, $.event.special.tap.tapholdthreshold ); }); }, teardown: function() { $( this ).unbind( "vmousedown" ).unbind( "vclick" ).unbind( "vmouseup" ); $document.unbind( "vmousecancel" ); } }; // also handles swipeleft, swiperight $.event.special.swipe = { // more than this horizontal displacement, and we will suppress scrolling. scrollsupressionthreshold: 30, // more time than this, and it isn't a swipe. durationthreshold: 1000, // swipe horizontal displacement must be more than this. horizontaldistancethreshold: 30, // swipe vertical displacement must be less than this. verticaldistancethreshold: 30, getlocation: function ( event ) { var winpagex = window.pagexoffset, winpagey = window.pageyoffset, x = event.clientx, y = event.clienty; if ( event.pagey === 0 && math.floor( y ) > math.floor( event.pagey ) || event.pagex === 0 && math.floor( x ) > math.floor( event.pagex ) ) { // ios4 clientx/clienty have the value that should have been // in pagex/pagey. while pagex/page/ have the value 0 x = x - winpagex; y = y - winpagey; } else if ( y < ( event.pagey - winpagey) || x < ( event.pagex - winpagex ) ) { // some android browsers have totally bogus values for clientx/y // when scrolling/zooming a page. detectable since clientx/clienty // should never be smaller than pagex/pagey minus page scroll x = event.pagex - winpagex; y = event.pagey - winpagey; } return { x: x, y: y }; }, start: function( event ) { var data = event.originalevent.touches ? event.originalevent.touches[ 0 ] : event, location = $.event.special.swipe.getlocation( data ); return { time: ( new date() ).gettime(), coords: [ location.x, location.y ], origin: $( event.target ) }; }, stop: function( event ) { var data = event.originalevent.touches ? event.originalevent.touches[ 0 ] : event, location = $.event.special.swipe.getlocation( data ); return { time: ( new date() ).gettime(), coords: [ location.x, location.y ] }; }, handleswipe: function( start, stop, thisobject, origtarget ) { if ( stop.time - start.time < $.event.special.swipe.durationthreshold && math.abs( start.coords[ 0 ] - stop.coords[ 0 ] ) > $.event.special.swipe.horizontaldistancethreshold && math.abs( start.coords[ 1 ] - stop.coords[ 1 ] ) < $.event.special.swipe.verticaldistancethreshold ) { var direction = start.coords[0] > stop.coords[ 0 ] ? "swipeleft" : "swiperight"; triggercustomevent( thisobject, "swipe", $.event( "swipe", { target: origtarget, swipestart: start, swipestop: stop }), true ); triggercustomevent( thisobject, direction,$.event( direction, { target: origtarget, swipestart: start, swipestop: stop } ), true ); return true; } return false; }, // this serves as a flag to ensure that at most one swipe event event is // in work at any given time eventinprogress: false, setup: function() { var events, thisobject = this, $this = $( thisobject ), context = {}; // retrieve the events data for this element and add the swipe context events = $.data( this, "mobile-events" ); if ( !events ) { events = { length: 0 }; $.data( this, "mobile-events", events ); } events.length++; events.swipe = context; context.start = function( event ) { // bail if we're already working on a swipe event if ( $.event.special.swipe.eventinprogress ) { return; } $.event.special.swipe.eventinprogress = true; var stop, start = $.event.special.swipe.start( event ), origtarget = event.target, emitted = false; context.move = function( event ) { if ( !start || event.isdefaultprevented() ) { return; } stop = $.event.special.swipe.stop( event ); if ( !emitted ) { emitted = $.event.special.swipe.handleswipe( start, stop, thisobject, origtarget ); if ( emitted ) { // reset the context to make way for the next swipe event $.event.special.swipe.eventinprogress = false; } } // prevent scrolling if ( math.abs( start.coords[ 0 ] - stop.coords[ 0 ] ) > $.event.special.swipe.scrollsupressionthreshold ) { event.preventdefault(); } }; context.stop = function() { emitted = true; // reset the context to make way for the next swipe event $.event.special.swipe.eventinprogress = false; $document.off( touchmoveevent, context.move ); context.move = null; }; $document.on( touchmoveevent, context.move ) .one( touchstopevent, context.stop ); }; $this.on( touchstartevent, context.start ); }, teardown: function() { var events, context; events = $.data( this, "mobile-events" ); if ( events ) { context = events.swipe; delete events.swipe; events.length--; if ( events.length === 0 ) { $.removedata( this, "mobile-events" ); } } if ( context ) { if ( context.start ) { $( this ).off( touchstartevent, context.start ); } if ( context.move ) { $document.off( touchmoveevent, context.move ); } if ( context.stop ) { $document.off( touchstopevent, context.stop ); } } } }; $.each({ scrollstop: "scrollstart", taphold: "tap", swipeleft: "swipe.left", swiperight: "swipe.right" }, function( event, sourceevent ) { $.event.special[ event ] = { setup: function() { $( this ).bind( sourceevent, $.noop ); }, teardown: function() { $( this ).unbind( sourceevent ); } }; }); })( jquery, this ); // throttled resize event (function( $ ) { $.event.special.throttledresize = { setup: function() { $( this ).bind( "resize", handler ); }, teardown: function() { $( this ).unbind( "resize", handler ); } }; var throttle = 250, handler = function() { curr = ( new date() ).gettime(); diff = curr - lastcall; if ( diff >= throttle ) { lastcall = curr; $( this ).trigger( "throttledresize" ); } else { if ( heldcall ) { cleartimeout( heldcall ); } // promise a held call will still execute heldcall = settimeout( handler, throttle - diff ); } }, lastcall = 0, heldcall, curr, diff; })( jquery );