5 // This plugin is an experiment for abstracting away the touch and mouse
6 // events so that developers don't have to worry about which method of input
7 // the device their document is loaded on supports.
9 // The idea here is to allow the developer to register listeners for the
10 // basic mouse events, such as mousedown, mousemove, mouseup, and click,
11 // and the plugin will take care of registering the correct listeners
12 // behind the scenes to invoke the listener at the fastest possible time
13 // for that device, while still retaining the order of event firing in
14 // the traditional mouse environment, should multiple handlers be registered
15 // on the same element for different events.
17 // The current version exposes the following virtual events to jQuery bind methods:
18 // "vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel"
20 (function( $, window, document, undefined ) {
22 var dataPropertyName = "virtualMouseBindings",
23 touchTargetPropertyName = "virtualTouchID",
24 virtualEventNames = "vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel".split( " " ),
25 touchEventProps = "clientX clientY pageX pageY screenX screenY".split( " " ),
26 activeDocHandlers = {},
32 blockMouseTriggers = false,
33 blockTouchTriggers = false,
34 eventCaptureSupported = "addEventListener" in document,
35 $document = $( document ),
40 moveDistanceThreshold: 10,
41 clickDistanceThreshold: 10,
42 resetTimerDuration: 1500
45 function getNativeEvent( event ) {
47 while ( event && typeof event.originalEvent !== "undefined" ) {
48 event = event.originalEvent;
53 function createVirtualEvent( event, eventType ) {
56 oe, props, ne, prop, ct, touch, i, j;
58 event = $.Event(event);
59 event.type = eventType;
61 oe = event.originalEvent;
62 props = $.event.props;
64 // copy original event properties over to the new event
65 // this would happen if we could call $.event.fix instead of $.Event
66 // but we don't have a way to force an event to be fixed multiple times
68 for ( i = props.length, prop; i; ) {
70 event[ prop ] = oe[ prop ];
74 // make sure that if the mouse and click virtual events are generated
75 // without a .which one is defined
76 if ( t.search(/mouse(down|up)|click/) > -1 && !event.which ){
80 if ( t.search(/^touch/) !== -1 ) {
81 ne = getNativeEvent( oe );
83 ct = ne.changedTouches;
84 touch = ( t && t.length ) ? t[0] : ( (ct && ct.length) ? ct[ 0 ] : undefined );
87 for ( j = 0, len = touchEventProps.length; j < len; j++){
88 prop = touchEventProps[ j ];
89 event[ prop ] = touch[ prop ];
97 function getVirtualBindingFlags( element ) {
104 b = $.data( element, dataPropertyName );
108 flags[ k ] = flags.hasVirtualBinding = true;
111 element = element.parentNode;
116 function getClosestElementWithVirtualBinding( element, eventType ) {
120 b = $.data( element, dataPropertyName );
122 if ( b && ( !eventType || b[ eventType ] ) ) {
125 element = element.parentNode;
130 function enableTouchBindings() {
131 blockTouchTriggers = false;
134 function disableTouchBindings() {
135 blockTouchTriggers = true;
138 function enableMouseBindings() {
140 clickBlockList.length = 0;
141 blockMouseTriggers = false;
143 // When mouse bindings are enabled, our
144 // touch bindings are disabled.
145 disableTouchBindings();
148 function disableMouseBindings() {
149 // When mouse bindings are disabled, our
150 // touch bindings are enabled.
151 enableTouchBindings();
154 function startResetTimer() {
156 resetTimerID = setTimeout(function(){
158 enableMouseBindings();
159 }, $.vmouse.resetTimerDuration );
162 function clearResetTimer() {
164 clearTimeout( resetTimerID );
169 function triggerVirtualEvent( eventType, event, flags ) {
172 if ( ( flags && flags[ eventType ] ) ||
173 ( !flags && getClosestElementWithVirtualBinding( event.target, eventType ) ) ) {
175 ve = createVirtualEvent( event, eventType );
177 $( event.target).trigger( ve );
183 function mouseEventCallback( event ) {
184 var touchID = $.data(event.target, touchTargetPropertyName);
186 if ( !blockMouseTriggers && ( !lastTouchID || lastTouchID !== touchID ) ){
187 var ve = triggerVirtualEvent( "v" + event.type, event );
189 if ( ve.isDefaultPrevented() ) {
190 event.preventDefault();
192 if ( ve.isPropagationStopped() ) {
193 event.stopPropagation();
195 if ( ve.isImmediatePropagationStopped() ) {
196 event.stopImmediatePropagation();
202 function handleTouchStart( event ) {
204 var touches = getNativeEvent( event ).touches,
207 if ( touches && touches.length === 1 ) {
209 target = event.target;
210 flags = getVirtualBindingFlags( target );
212 if ( flags.hasVirtualBinding ) {
214 lastTouchID = nextTouchID++;
215 $.data( target, touchTargetPropertyName, lastTouchID );
219 disableMouseBindings();
222 var t = getNativeEvent( event ).touches[ 0 ];
226 triggerVirtualEvent( "vmouseover", event, flags );
227 triggerVirtualEvent( "vmousedown", event, flags );
232 function handleScroll( event ) {
233 if ( blockTouchTriggers ) {
238 triggerVirtualEvent( "vmousecancel", event, getVirtualBindingFlags( event.target ) );
245 function handleTouchMove( event ) {
246 if ( blockTouchTriggers ) {
250 var t = getNativeEvent( event ).touches[ 0 ],
251 didCancel = didScroll,
252 moveThreshold = $.vmouse.moveDistanceThreshold;
253 didScroll = didScroll ||
254 ( Math.abs(t.pageX - startX) > moveThreshold ||
255 Math.abs(t.pageY - startY) > moveThreshold ),
256 flags = getVirtualBindingFlags( event.target );
258 if ( didScroll && !didCancel ) {
259 triggerVirtualEvent( "vmousecancel", event, flags );
262 triggerVirtualEvent( "vmousemove", event, flags );
266 function handleTouchEnd( event ) {
267 if ( blockTouchTriggers ) {
271 disableTouchBindings();
273 var flags = getVirtualBindingFlags( event.target ),
275 triggerVirtualEvent( "vmouseup", event, flags );
278 var ve = triggerVirtualEvent( "vclick", event, flags );
279 if ( ve && ve.isDefaultPrevented() ) {
280 // The target of the mouse events that follow the touchend
281 // event don't necessarily match the target used during the
282 // touch. This means we need to rely on coordinates for blocking
283 // any click that is generated.
284 t = getNativeEvent( event ).changedTouches[ 0 ];
285 clickBlockList.push({
286 touchID: lastTouchID,
291 // Prevent any mouse events that follow from triggering
292 // virtual event notifications.
293 blockMouseTriggers = true;
296 triggerVirtualEvent( "vmouseout", event, flags);
302 function hasVirtualBindings( ele ) {
303 var bindings = $.data( ele, dataPropertyName ),
307 for ( k in bindings ) {
308 if ( bindings[ k ] ) {
316 function dummyMouseHandler(){}
318 function getSpecialEventObject( eventType ) {
319 var realType = eventType.substr( 1 );
322 setup: function( data, namespace ) {
323 // If this is the first virtual mouse binding for this element,
324 // add a bindings object to its data.
326 if ( !hasVirtualBindings( this ) ) {
327 $.data( this, dataPropertyName, {});
330 // If setup is called, we know it is the first binding for this
331 // eventType, so initialize the count for the eventType to zero.
332 var bindings = $.data( this, dataPropertyName );
333 bindings[ eventType ] = true;
335 // If this is the first virtual mouse event for this type,
336 // register a global handler on the document.
338 activeDocHandlers[ eventType ] = ( activeDocHandlers[ eventType ] || 0 ) + 1;
340 if ( activeDocHandlers[ eventType ] === 1 ) {
341 $document.bind( realType, mouseEventCallback );
344 // Some browsers, like Opera Mini, won't dispatch mouse/click events
345 // for elements unless they actually have handlers registered on them.
346 // To get around this, we register dummy handlers on the elements.
348 $( this ).bind( realType, dummyMouseHandler );
350 // For now, if event capture is not supported, we rely on mouse handlers.
351 if ( eventCaptureSupported ) {
352 // If this is the first virtual mouse binding for the document,
353 // register our touchstart handler on the document.
355 activeDocHandlers[ "touchstart" ] = ( activeDocHandlers[ "touchstart" ] || 0) + 1;
357 if (activeDocHandlers[ "touchstart" ] === 1) {
358 $document.bind( "touchstart", handleTouchStart )
359 .bind( "touchend", handleTouchEnd )
361 // On touch platforms, touching the screen and then dragging your finger
362 // causes the window content to scroll after some distance threshold is
363 // exceeded. On these platforms, a scroll prevents a click event from being
364 // dispatched, and on some platforms, even the touchend is suppressed. To
365 // mimic the suppression of the click event, we need to watch for a scroll
366 // event. Unfortunately, some platforms like iOS don't dispatch scroll
367 // events until *AFTER* the user lifts their finger (touchend). This means
368 // we need to watch both scroll and touchmove events to figure out whether
369 // or not a scroll happenens before the touchend event is fired.
371 .bind( "touchmove", handleTouchMove )
372 .bind( "scroll", handleScroll );
377 teardown: function( data, namespace ) {
378 // If this is the last virtual binding for this eventType,
379 // remove its global handler from the document.
381 --activeDocHandlers[ eventType ];
383 if ( !activeDocHandlers[ eventType ] ) {
384 $document.unbind( realType, mouseEventCallback );
387 if ( eventCaptureSupported ) {
388 // If this is the last virtual mouse binding in existence,
389 // remove our document touchstart listener.
391 --activeDocHandlers[ "touchstart" ];
393 if ( !activeDocHandlers[ "touchstart" ] ) {
394 $document.unbind( "touchstart", handleTouchStart )
395 .unbind( "touchmove", handleTouchMove )
396 .unbind( "touchend", handleTouchEnd )
397 .unbind( "scroll", handleScroll );
401 var $this = $( this ),
402 bindings = $.data( this, dataPropertyName );
404 // teardown may be called when an element was
405 // removed from the DOM. If this is the case,
406 // jQuery core may have already stripped the element
407 // of any data bindings so we need to check it before
410 bindings[ eventType ] = false;
413 // Unregister the dummy event handler.
415 $this.unbind( realType, dummyMouseHandler );
417 // If this is the last virtual mouse binding on the
418 // element, remove the binding data from the element.
420 if ( !hasVirtualBindings( this ) ) {
421 $this.removeData( dataPropertyName );
427 // Expose our custom events to the jQuery bind/unbind mechanism.
429 for ( var i = 0; i < virtualEventNames.length; i++ ){
430 $.event.special[ virtualEventNames[ i ] ] = getSpecialEventObject( virtualEventNames[ i ] );
433 // Add a capture click handler to block clicks.
434 // Note that we require event capture support for this so if the device
435 // doesn't support it, we punt for now and rely solely on mouse events.
436 if ( eventCaptureSupported ) {
437 document.addEventListener( "click", function( e ){
438 var cnt = clickBlockList.length,
440 x, y, ele, i, o, touchID;
445 threshold = $.vmouse.clickDistanceThreshold;
447 // The idea here is to run through the clickBlockList to see if
448 // the current click event is in the proximity of one of our
449 // vclick events that had preventDefault() called on it. If we find
450 // one, then we block the click.
452 // Why do we have to rely on proximity?
454 // Because the target of the touch event that triggered the vclick
455 // can be different from the target of the click event synthesized
456 // by the browser. The target of a mouse/click event that is syntehsized
457 // from a touch event seems to be implementation specific. For example,
458 // some browsers will fire mouse/click events for a link that is near
459 // a touch event, even though the target of the touchstart/touchend event
460 // says the user touched outside the link. Also, it seems that with most
461 // browsers, the target of the mouse/click event is not calculated until the
462 // time it is dispatched, so if you replace an element that you touched
463 // with another element, the target of the mouse/click will be the new
464 // element underneath that point.
466 // Aside from proximity, we also check to see if the target and any
467 // of its ancestors were the ones that blocked a click. This is necessary
468 // because of the strange mouse/click target calculation done in the
469 // Android 2.1 browser, where if you click on an element, and there is a
470 // mouse/click handler on one of its ancestors, the target will be the
471 // innermost child of the touched element, even if that child is no where
472 // near the point of touch.
477 for ( i = 0; i < cnt; i++ ) {
478 o = clickBlockList[ i ];
481 if ( ( ele === target && Math.abs( o.x - x ) < threshold && Math.abs( o.y - y ) < threshold ) ||
482 $.data( ele, touchTargetPropertyName ) === o.touchID ) {
483 // XXX: We may want to consider removing matches from the block list
484 // instead of waiting for the reset timer to fire.
490 ele = ele.parentNode;
495 })( jQuery, window, document );