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 //>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
18 //>>description: Normalizes touch/mouse events.
19 //>>label: Virtual Mouse (vmouse) Bindings
22 define( [ "jquery" ], function( $ ) {
23 //>>excludeEnd("jqmBuildExclude");
24 (function( $, window, document, undefined ) {
26 var dataPropertyName = "virtualMouseBindings",
27 touchTargetPropertyName = "virtualTouchID",
28 virtualEventNames = "vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel".split( " " ),
29 touchEventProps = "clientX clientY pageX pageY screenX screenY".split( " " ),
30 mouseHookProps = $.event.mouseHooks ? $.event.mouseHooks.props : [],
31 mouseEventProps = $.event.props.concat( mouseHookProps ),
32 activeDocHandlers = {},
38 blockMouseTriggers = false,
39 blockTouchTriggers = false,
40 eventCaptureSupported = "addEventListener" in document,
41 $document = $( document ),
46 moveDistanceThreshold: 10,
47 clickDistanceThreshold: 10,
48 resetTimerDuration: 1500
51 function getNativeEvent( event ) {
53 while ( event && typeof event.originalEvent !== "undefined" ) {
54 event = event.originalEvent;
59 function createVirtualEvent( event, eventType ) {
62 oe, props, ne, prop, ct, touch, i, j;
64 event = $.Event(event);
65 event.type = eventType;
67 oe = event.originalEvent;
68 props = $.event.props;
70 // addresses separation of $.event.props in to $.event.mouseHook.props and Issue 3280
71 // https://github.com/jquery/jquery-mobile/issues/3280
72 if ( t.search( /^(mouse|click)/ ) > -1 ) {
73 props = mouseEventProps;
76 // copy original event properties over to the new event
77 // this would happen if we could call $.event.fix instead of $.Event
78 // but we don't have a way to force an event to be fixed multiple times
80 for ( i = props.length, prop; i; ) {
82 event[ prop ] = oe[ prop ];
86 // make sure that if the mouse and click virtual events are generated
87 // without a .which one is defined
88 if ( t.search(/mouse(down|up)|click/) > -1 && !event.which ){
92 if ( t.search(/^touch/) !== -1 ) {
93 ne = getNativeEvent( oe );
95 ct = ne.changedTouches;
96 touch = ( t && t.length ) ? t[0] : ( (ct && ct.length) ? ct[ 0 ] : undefined );
99 for ( j = 0, len = touchEventProps.length; j < len; j++){
100 prop = touchEventProps[ j ];
101 event[ prop ] = touch[ prop ];
109 function getVirtualBindingFlags( element ) {
116 b = $.data( element, dataPropertyName );
120 flags[ k ] = flags.hasVirtualBinding = true;
123 element = element.parentNode;
128 function getClosestElementWithVirtualBinding( element, eventType ) {
132 b = $.data( element, dataPropertyName );
134 if ( b && ( !eventType || b[ eventType ] ) ) {
137 element = element.parentNode;
142 function enableTouchBindings() {
143 blockTouchTriggers = false;
146 function disableTouchBindings() {
147 blockTouchTriggers = true;
150 function enableMouseBindings() {
152 clickBlockList.length = 0;
153 blockMouseTriggers = false;
155 // When mouse bindings are enabled, our
156 // touch bindings are disabled.
157 disableTouchBindings();
160 function disableMouseBindings() {
161 // When mouse bindings are disabled, our
162 // touch bindings are enabled.
163 enableTouchBindings();
166 function startResetTimer() {
168 resetTimerID = setTimeout(function(){
170 enableMouseBindings();
171 }, $.vmouse.resetTimerDuration );
174 function clearResetTimer() {
176 clearTimeout( resetTimerID );
181 function triggerVirtualEvent( eventType, event, flags ) {
184 if ( ( flags && flags[ eventType ] ) ||
185 ( !flags && getClosestElementWithVirtualBinding( event.target, eventType ) ) ) {
187 ve = createVirtualEvent( event, eventType );
189 $( event.target).trigger( ve );
195 function mouseEventCallback( event ) {
196 var touchID = $.data(event.target, touchTargetPropertyName);
198 if ( !blockMouseTriggers && ( !lastTouchID || lastTouchID !== touchID ) ){
199 var ve = triggerVirtualEvent( "v" + event.type, event );
201 if ( ve.isDefaultPrevented() ) {
202 event.preventDefault();
204 if ( ve.isPropagationStopped() ) {
205 event.stopPropagation();
207 if ( ve.isImmediatePropagationStopped() ) {
208 event.stopImmediatePropagation();
214 function handleTouchStart( event ) {
216 var touches = getNativeEvent( event ).touches,
219 if ( touches && touches.length === 1 ) {
221 target = event.target;
222 flags = getVirtualBindingFlags( target );
224 if ( flags.hasVirtualBinding ) {
226 lastTouchID = nextTouchID++;
227 $.data( target, touchTargetPropertyName, lastTouchID );
231 disableMouseBindings();
234 var t = getNativeEvent( event ).touches[ 0 ];
238 triggerVirtualEvent( "vmouseover", event, flags );
239 triggerVirtualEvent( "vmousedown", event, flags );
244 function handleScroll( event ) {
245 if ( blockTouchTriggers ) {
250 triggerVirtualEvent( "vmousecancel", event, getVirtualBindingFlags( event.target ) );
257 function handleTouchMove( event ) {
258 if ( blockTouchTriggers ) {
262 var t = getNativeEvent( event ).touches[ 0 ],
263 didCancel = didScroll,
264 moveThreshold = $.vmouse.moveDistanceThreshold;
265 didScroll = didScroll ||
266 ( Math.abs(t.pageX - startX) > moveThreshold ||
267 Math.abs(t.pageY - startY) > moveThreshold ),
268 flags = getVirtualBindingFlags( event.target );
270 if ( didScroll && !didCancel ) {
271 triggerVirtualEvent( "vmousecancel", event, flags );
274 triggerVirtualEvent( "vmousemove", event, flags );
278 function handleTouchEnd( event ) {
279 if ( blockTouchTriggers ) {
283 disableTouchBindings();
285 var flags = getVirtualBindingFlags( event.target ),
287 triggerVirtualEvent( "vmouseup", event, flags );
290 var ve = triggerVirtualEvent( "vclick", event, flags );
291 if ( ve && ve.isDefaultPrevented() ) {
292 // The target of the mouse events that follow the touchend
293 // event don't necessarily match the target used during the
294 // touch. This means we need to rely on coordinates for blocking
295 // any click that is generated.
296 t = getNativeEvent( event ).changedTouches[ 0 ];
297 clickBlockList.push({
298 touchID: lastTouchID,
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 && 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 );
508 //>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
510 //>>excludeEnd("jqmBuildExclude");