2 // This plugin is an experiment for abstracting away the touch and mouse
3 // events so that developers don't have to worry about which method of input
4 // the device their document is loaded on supports.
6 // The idea here is to allow the developer to register listeners for the
7 // basic mouse events, such as mousedown, mousemove, mouseup, and click,
8 // and the plugin will take care of registering the correct listeners
9 // behind the scenes to invoke the listener at the fastest possible time
10 // for that device, while still retaining the order of event firing in
11 // the traditional mouse environment, should multiple handlers be registered
12 // on the same element for different events.
14 // The current version exposes the following virtual events to jQuery bind methods:
15 // "vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel"
17 (function( $, window, document, undefined ) {
19 var dataPropertyName = "virtualMouseBindings",
20 touchTargetPropertyName = "virtualTouchID",
21 virtualEventNames = "vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel".split( " " ),
22 touchEventProps = "clientX clientY pageX pageY screenX screenY".split( " " ),
23 mouseHookProps = $.event.mouseHooks ? $.event.mouseHooks.props : [],
24 mouseEventProps = $.event.props.concat( mouseHookProps ),
25 activeDocHandlers = {},
31 blockMouseTriggers = false,
32 blockTouchTriggers = false,
33 eventCaptureSupported = "addEventListener" in document,
34 $document = $.mobile.$document,
36 lastTouchID = 0, threshold;
39 moveDistanceThreshold: 10,
40 clickDistanceThreshold: 10,
41 resetTimerDuration: 1500
44 function getNativeEvent( event ) {
46 while ( event && typeof event.originalEvent !== "undefined" ) {
47 event = event.originalEvent;
52 function createVirtualEvent( event, eventType ) {
55 oe, props, ne, prop, ct, touch, i, j, len;
57 event = $.Event( event );
58 event.type = eventType;
60 oe = event.originalEvent;
61 props = $.event.props;
63 // addresses separation of $.event.props in to $.event.mouseHook.props and Issue 3280
64 // https://github.com/jquery/jquery-mobile/issues/3280
65 if ( t.search( /^(mouse|click)/ ) > -1 ) {
66 props = mouseEventProps;
69 // copy original event properties over to the new event
70 // this would happen if we could call $.event.fix instead of $.Event
71 // but we don't have a way to force an event to be fixed multiple times
73 for ( i = props.length, prop; i; ) {
75 event[ prop ] = oe[ prop ];
79 // make sure that if the mouse and click virtual events are generated
80 // without a .which one is defined
81 if ( t.search(/mouse(down|up)|click/) > -1 && !event.which ) {
85 if ( t.search(/^touch/) !== -1 ) {
86 ne = getNativeEvent( oe );
88 ct = ne.changedTouches;
89 touch = ( t && t.length ) ? t[0] : ( ( ct && ct.length ) ? ct[ 0 ] : undefined );
92 for ( j = 0, len = touchEventProps.length; j < len; j++) {
93 prop = touchEventProps[ j ];
94 event[ prop ] = touch[ prop ];
102 function getVirtualBindingFlags( element ) {
109 b = $.data( element, dataPropertyName );
113 flags[ k ] = flags.hasVirtualBinding = true;
116 element = element.parentNode;
121 function getClosestElementWithVirtualBinding( element, eventType ) {
125 b = $.data( element, dataPropertyName );
127 if ( b && ( !eventType || b[ eventType ] ) ) {
130 element = element.parentNode;
135 function enableTouchBindings() {
136 blockTouchTriggers = false;
139 function disableTouchBindings() {
140 blockTouchTriggers = true;
143 function enableMouseBindings() {
145 clickBlockList.length = 0;
146 blockMouseTriggers = false;
148 // When mouse bindings are enabled, our
149 // touch bindings are disabled.
150 disableTouchBindings();
153 function disableMouseBindings() {
154 // When mouse bindings are disabled, our
155 // touch bindings are enabled.
156 enableTouchBindings();
159 function startResetTimer() {
161 resetTimerID = setTimeout( function() {
163 enableMouseBindings();
164 }, $.vmouse.resetTimerDuration );
167 function clearResetTimer() {
168 if ( resetTimerID ) {
169 clearTimeout( resetTimerID );
174 function triggerVirtualEvent( eventType, event, flags ) {
177 if ( ( flags && flags[ eventType ] ) ||
178 ( !flags && getClosestElementWithVirtualBinding( event.target, eventType ) ) ) {
180 ve = createVirtualEvent( event, eventType );
182 $( event.target).trigger( ve );
188 function mouseEventCallback( event ) {
189 var touchID = $.data( event.target, touchTargetPropertyName );
191 if ( ( $.support.touch === true ) && ( touchID === undefined ) ) {
195 if ( !blockMouseTriggers && ( !lastTouchID || lastTouchID !== touchID ) ) {
196 var ve = triggerVirtualEvent( "v" + event.type, event );
198 if ( ve.isDefaultPrevented() ) {
199 event.preventDefault();
201 if ( ve.isPropagationStopped() ) {
202 event.stopPropagation();
204 if ( ve.isImmediatePropagationStopped() ) {
205 event.stopImmediatePropagation();
211 function handleTouchStart( event ) {
213 var touches = getNativeEvent( event ).touches,
216 if ( touches && touches.length === 1 ) {
218 target = event.target;
219 flags = getVirtualBindingFlags( target );
221 if ( flags.hasVirtualBinding ) {
223 lastTouchID = nextTouchID++;
224 $.data( target, touchTargetPropertyName, lastTouchID );
228 disableMouseBindings();
231 var t = getNativeEvent( event ).touches[ 0 ];
235 triggerVirtualEvent( "vmouseover", event, flags );
236 triggerVirtualEvent( "vmousedown", event, flags );
241 function handleScroll( event ) {
242 if ( blockTouchTriggers ) {
247 triggerVirtualEvent( "vmousecancel", event, getVirtualBindingFlags( event.target ) );
254 function handleTouchMove( event ) {
255 if ( blockTouchTriggers ) {
259 var t = getNativeEvent( event ).touches[ 0 ],
260 didCancel = didScroll,
261 moveThreshold = $.vmouse.moveDistanceThreshold,
262 flags = getVirtualBindingFlags( event.target );
264 didScroll = didScroll ||
265 ( Math.abs( t.pageX - startX ) > moveThreshold ||
266 Math.abs( t.pageY - startY ) > moveThreshold );
269 if ( didScroll && !didCancel ) {
270 triggerVirtualEvent( "vmousecancel", event, flags );
273 triggerVirtualEvent( "vmousemove", event, flags );
277 function handleTouchEnd( event ) {
278 if ( blockTouchTriggers ) {
282 disableTouchBindings();
284 var flags = getVirtualBindingFlags( event.target ),
286 triggerVirtualEvent( "vmouseup", event, flags );
289 var ve = triggerVirtualEvent( "vclick", event, flags );
290 if ( ve && ve.isDefaultPrevented() ) {
291 // The target of the mouse events that follow the touchend
292 // event don't necessarily match the target used during the
293 // touch. This means we need to rely on coordinates for blocking
294 // any click that is generated.
295 t = getNativeEvent( event ).changedTouches[ 0 ];
296 clickBlockList.push({
297 touchID: lastTouchID,
298 target: event.target,
303 // Prevent any mouse events that follow from triggering
304 // virtual event notifications.
305 blockMouseTriggers = true;
308 triggerVirtualEvent( "vmouseout", event, flags);
314 function hasVirtualBindings( ele ) {
315 var bindings = $.data( ele, dataPropertyName ),
319 for ( k in bindings ) {
320 if ( bindings[ k ] ) {
328 function dummyMouseHandler() {}
330 function getSpecialEventObject( eventType ) {
331 var realType = eventType.substr( 1 );
334 setup: function( data, namespace ) {
335 // If this is the first virtual mouse binding for this element,
336 // add a bindings object to its data.
338 if ( !hasVirtualBindings( this ) ) {
339 $.data( this, dataPropertyName, {} );
342 // If setup is called, we know it is the first binding for this
343 // eventType, so initialize the count for the eventType to zero.
344 var bindings = $.data( this, dataPropertyName );
345 bindings[ eventType ] = true;
347 // If this is the first virtual mouse event for this type,
348 // register a global handler on the document.
350 activeDocHandlers[ eventType ] = ( activeDocHandlers[ eventType ] || 0 ) + 1;
352 if ( activeDocHandlers[ eventType ] === 1 ) {
353 $document.bind( realType, mouseEventCallback );
356 // Some browsers, like Opera Mini, won't dispatch mouse/click events
357 // for elements unless they actually have handlers registered on them.
358 // To get around this, we register dummy handlers on the elements.
360 $( this ).bind( realType, dummyMouseHandler );
362 // For now, if event capture is not supported, we rely on mouse handlers.
363 if ( eventCaptureSupported ) {
364 // If this is the first virtual mouse binding for the document,
365 // register our touchstart handler on the document.
367 activeDocHandlers[ "touchstart" ] = ( activeDocHandlers[ "touchstart" ] || 0) + 1;
369 if ( activeDocHandlers[ "touchstart" ] === 1 ) {
370 $document.bind( "touchstart", handleTouchStart )
371 .bind( "touchend", handleTouchEnd )
373 // On touch platforms, touching the screen and then dragging your finger
374 // causes the window content to scroll after some distance threshold is
375 // exceeded. On these platforms, a scroll prevents a click event from being
376 // dispatched, and on some platforms, even the touchend is suppressed. To
377 // mimic the suppression of the click event, we need to watch for a scroll
378 // event. Unfortunately, some platforms like iOS don't dispatch scroll
379 // events until *AFTER* the user lifts their finger (touchend). This means
380 // we need to watch both scroll and touchmove events to figure out whether
381 // or not a scroll happenens before the touchend event is fired.
383 .bind( "touchmove", handleTouchMove )
384 .bind( "scroll", handleScroll );
389 teardown: function( data, namespace ) {
390 // If this is the last virtual binding for this eventType,
391 // remove its global handler from the document.
393 --activeDocHandlers[ eventType ];
395 if ( !activeDocHandlers[ eventType ] ) {
396 $document.unbind( realType, mouseEventCallback );
399 if ( eventCaptureSupported ) {
400 // If this is the last virtual mouse binding in existence,
401 // remove our document touchstart listener.
403 --activeDocHandlers[ "touchstart" ];
405 if ( !activeDocHandlers[ "touchstart" ] ) {
406 $document.unbind( "touchstart", handleTouchStart )
407 .unbind( "touchmove", handleTouchMove )
408 .unbind( "touchend", handleTouchEnd )
409 .unbind( "scroll", handleScroll );
413 var $this = $( this ),
414 bindings = $.data( this, dataPropertyName );
416 // teardown may be called when an element was
417 // removed from the DOM. If this is the case,
418 // jQuery core may have already stripped the element
419 // of any data bindings so we need to check it before
422 bindings[ eventType ] = false;
425 // Unregister the dummy event handler.
427 $this.unbind( realType, dummyMouseHandler );
429 // If this is the last virtual mouse binding on the
430 // element, remove the binding data from the element.
432 if ( !hasVirtualBindings( this ) ) {
433 $this.removeData( dataPropertyName );
439 // Expose our custom events to the jQuery bind/unbind mechanism.
441 for ( var i = 0; i < virtualEventNames.length; i++ ) {
442 $.event.special[ virtualEventNames[ i ] ] = getSpecialEventObject( virtualEventNames[ i ] );
445 // Add a capture click handler to block clicks.
446 // Note that we require event capture support for this so if the device
447 // doesn't support it, we punt for now and rely solely on mouse events.
448 if ( eventCaptureSupported ) {
449 document.addEventListener( "click", function( e ) {
450 var cnt = clickBlockList.length,
452 x, y, ele, i, o, touchID;
457 threshold = $.vmouse.clickDistanceThreshold;
459 // The idea here is to run through the clickBlockList to see if
460 // the current click event is in the proximity of one of our
461 // vclick events that had preventDefault() called on it. If we find
462 // one, then we block the click.
464 // Why do we have to rely on proximity?
466 // Because the target of the touch event that triggered the vclick
467 // can be different from the target of the click event synthesized
468 // by the browser. The target of a mouse/click event that is syntehsized
469 // from a touch event seems to be implementation specific. For example,
470 // some browsers will fire mouse/click events for a link that is near
471 // a touch event, even though the target of the touchstart/touchend event
472 // says the user touched outside the link. Also, it seems that with most
473 // browsers, the target of the mouse/click event is not calculated until the
474 // time it is dispatched, so if you replace an element that you touched
475 // with another element, the target of the mouse/click will be the new
476 // element underneath that point.
478 // Aside from proximity, we also check to see if the target and any
479 // of its ancestors were the ones that blocked a click. This is necessary
480 // because of the strange mouse/click target calculation done in the
481 // Android 2.1 browser, where if you click on an element, and there is a
482 // mouse/click handler on one of its ancestors, the target will be the
483 // innermost child of the touched element, even if that child is no where
484 // near the point of touch.
489 for ( i = 0; i < cnt; i++ ) {
490 o = clickBlockList[ i ];
493 if ( ( ele === target && target === o.target && Math.abs( o.x - x ) < threshold && Math.abs( o.y - y ) < threshold ) ||
494 $.data( ele, touchTargetPropertyName ) === o.touchID ) {
495 // XXX: We may want to consider removing matches from the block list
496 // instead of waiting for the reset timer to fire.
502 ele = ele.parentNode;
507 })( jQuery, window, document );