Revert "Export"
[framework/web/web-ui-fw.git] / libs / js / jquery-mobile-1.1.0 / js / jquery.mobile.vmouse.js
1
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.
5 //
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.
13 //
14 // The current version exposes the following virtual events to jQuery bind methods:
15 // "vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel"
16
17 //>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
18 //>>description: Normalizes touch/mouse events.
19 //>>label: Virtual Mouse (vmouse) Bindings
20 //>>group: Core
21
22 define( [ "jquery" ], function( $ ) {
23 //>>excludeEnd("jqmBuildExclude");
24 (function( $, window, document, undefined ) {
25
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 = {},
33         resetTimerID = 0,
34         startX = 0,
35         startY = 0,
36         didScroll = false,
37         clickBlockList = [],
38         blockMouseTriggers = false,
39         blockTouchTriggers = false,
40         eventCaptureSupported = "addEventListener" in document,
41         $document = $( document ),
42         nextTouchID = 1,
43         lastTouchID = 0;
44
45 $.vmouse = {
46         moveDistanceThreshold: 10,
47         clickDistanceThreshold: 10,
48         resetTimerDuration: 1500
49 };
50
51 function getNativeEvent( event ) {
52
53         while ( event && typeof event.originalEvent !== "undefined" ) {
54                 event = event.originalEvent;
55         }
56         return event;
57 }
58
59 function createVirtualEvent( event, eventType ) {
60
61         var t = event.type,
62                 oe, props, ne, prop, ct, touch, i, j;
63
64         event = $.Event(event);
65         event.type = eventType;
66
67         oe = event.originalEvent;
68         props = $.event.props;
69
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;
74         }
75
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
79         if ( oe ) {
80                 for ( i = props.length, prop; i; ) {
81                         prop = props[ --i ];
82                         event[ prop ] = oe[ prop ];
83                 }
84         }
85
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 ){
89                 event.which = 1;
90         }
91
92         if ( t.search(/^touch/) !== -1 ) {
93                 ne = getNativeEvent( oe );
94                 t = ne.touches;
95                 ct = ne.changedTouches;
96                 touch = ( t && t.length ) ? t[0] : ( (ct && ct.length) ? ct[ 0 ] : undefined );
97
98                 if ( touch ) {
99                         for ( j = 0, len = touchEventProps.length; j < len; j++){
100                                 prop = touchEventProps[ j ];
101                                 event[ prop ] = touch[ prop ];
102                         }
103                 }
104         }
105
106         return event;
107 }
108
109 function getVirtualBindingFlags( element ) {
110
111         var flags = {},
112                 b, k;
113
114         while ( element ) {
115
116                 b = $.data( element, dataPropertyName );
117
118                 for (  k in b ) {
119                         if ( b[ k ] ) {
120                                 flags[ k ] = flags.hasVirtualBinding = true;
121                         }
122                 }
123                 element = element.parentNode;
124         }
125         return flags;
126 }
127
128 function getClosestElementWithVirtualBinding( element, eventType ) {
129         var b;
130         while ( element ) {
131
132                 b = $.data( element, dataPropertyName );
133
134                 if ( b && ( !eventType || b[ eventType ] ) ) {
135                         return element;
136                 }
137                 element = element.parentNode;
138         }
139         return null;
140 }
141
142 function enableTouchBindings() {
143         blockTouchTriggers = false;
144 }
145
146 function disableTouchBindings() {
147         blockTouchTriggers = true;
148 }
149
150 function enableMouseBindings() {
151         lastTouchID = 0;
152         clickBlockList.length = 0;
153         blockMouseTriggers = false;
154
155         // When mouse bindings are enabled, our
156         // touch bindings are disabled.
157         disableTouchBindings();
158 }
159
160 function disableMouseBindings() {
161         // When mouse bindings are disabled, our
162         // touch bindings are enabled.
163         enableTouchBindings();
164 }
165
166 function startResetTimer() {
167         clearResetTimer();
168         resetTimerID = setTimeout(function(){
169                 resetTimerID = 0;
170                 enableMouseBindings();
171         }, $.vmouse.resetTimerDuration );
172 }
173
174 function clearResetTimer() {
175         if ( resetTimerID ){
176                 clearTimeout( resetTimerID );
177                 resetTimerID = 0;
178         }
179 }
180
181 function triggerVirtualEvent( eventType, event, flags ) {
182         var ve;
183
184         if ( ( flags && flags[ eventType ] ) ||
185                                 ( !flags && getClosestElementWithVirtualBinding( event.target, eventType ) ) ) {
186
187                 ve = createVirtualEvent( event, eventType );
188
189                 $( event.target).trigger( ve );
190         }
191
192         return ve;
193 }
194
195 function mouseEventCallback( event ) {
196         var touchID = $.data(event.target, touchTargetPropertyName);
197
198         if ( !blockMouseTriggers && ( !lastTouchID || lastTouchID !== touchID ) ){
199                 var ve = triggerVirtualEvent( "v" + event.type, event );
200                 if ( ve ) {
201                         if ( ve.isDefaultPrevented() ) {
202                                 event.preventDefault();
203                         }
204                         if ( ve.isPropagationStopped() ) {
205                                 event.stopPropagation();
206                         }
207                         if ( ve.isImmediatePropagationStopped() ) {
208                                 event.stopImmediatePropagation();
209                         }
210                 }
211         }
212 }
213
214 function handleTouchStart( event ) {
215
216         var touches = getNativeEvent( event ).touches,
217                 target, flags;
218
219         if ( touches && touches.length === 1 ) {
220
221                 target = event.target;
222                 flags = getVirtualBindingFlags( target );
223
224                 if ( flags.hasVirtualBinding ) {
225
226                         lastTouchID = nextTouchID++;
227                         $.data( target, touchTargetPropertyName, lastTouchID );
228
229                         clearResetTimer();
230
231                         disableMouseBindings();
232                         didScroll = false;
233
234                         var t = getNativeEvent( event ).touches[ 0 ];
235                         startX = t.pageX;
236                         startY = t.pageY;
237
238                         triggerVirtualEvent( "vmouseover", event, flags );
239                         triggerVirtualEvent( "vmousedown", event, flags );
240                 }
241         }
242 }
243
244 function handleScroll( event ) {
245         if ( blockTouchTriggers ) {
246                 return;
247         }
248
249         if ( !didScroll ) {
250                 triggerVirtualEvent( "vmousecancel", event, getVirtualBindingFlags( event.target ) );
251         }
252
253         didScroll = true;
254         startResetTimer();
255 }
256
257 function handleTouchMove( event ) {
258         if ( blockTouchTriggers ) {
259                 return;
260         }
261
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 );
269
270         if ( didScroll && !didCancel ) {
271                 triggerVirtualEvent( "vmousecancel", event, flags );
272         }
273
274         triggerVirtualEvent( "vmousemove", event, flags );
275         startResetTimer();
276 }
277
278 function handleTouchEnd( event ) {
279         if ( blockTouchTriggers ) {
280                 return;
281         }
282
283         disableTouchBindings();
284
285         var flags = getVirtualBindingFlags( event.target ),
286                 t;
287         triggerVirtualEvent( "vmouseup", event, flags );
288
289         if ( !didScroll ) {
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,
299                                 x: t.clientX,
300                                 y: t.clientY
301                         });
302
303                         // Prevent any mouse events that follow from triggering
304                         // virtual event notifications.
305                         blockMouseTriggers = true;
306                 }
307         }
308         triggerVirtualEvent( "vmouseout", event, flags);
309         didScroll = false;
310
311         startResetTimer();
312 }
313
314 function hasVirtualBindings( ele ) {
315         var bindings = $.data( ele, dataPropertyName ),
316                 k;
317
318         if ( bindings ) {
319                 for ( k in bindings ) {
320                         if ( bindings[ k ] ) {
321                                 return true;
322                         }
323                 }
324         }
325         return false;
326 }
327
328 function dummyMouseHandler(){}
329
330 function getSpecialEventObject( eventType ) {
331         var realType = eventType.substr( 1 );
332
333         return {
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.
337
338                         if ( !hasVirtualBindings( this ) ) {
339                                 $.data( this, dataPropertyName, {});
340                         }
341
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;
346
347                         // If this is the first virtual mouse event for this type,
348                         // register a global handler on the document.
349
350                         activeDocHandlers[ eventType ] = ( activeDocHandlers[ eventType ] || 0 ) + 1;
351
352                         if ( activeDocHandlers[ eventType ] === 1 ) {
353                                 $document.bind( realType, mouseEventCallback );
354                         }
355
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.
359
360                         $( this ).bind( realType, dummyMouseHandler );
361
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.
366
367                                 activeDocHandlers[ "touchstart" ] = ( activeDocHandlers[ "touchstart" ] || 0) + 1;
368
369                                 if (activeDocHandlers[ "touchstart" ] === 1) {
370                                         $document.bind( "touchstart", handleTouchStart )
371                                                 .bind( "touchend", handleTouchEnd )
372
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.
382
383                                                 .bind( "touchmove", handleTouchMove )
384                                                 .bind( "scroll", handleScroll );
385                                 }
386                         }
387                 },
388
389                 teardown: function( data, namespace ) {
390                         // If this is the last virtual binding for this eventType,
391                         // remove its global handler from the document.
392
393                         --activeDocHandlers[ eventType ];
394
395                         if ( !activeDocHandlers[ eventType ] ) {
396                                 $document.unbind( realType, mouseEventCallback );
397                         }
398
399                         if ( eventCaptureSupported ) {
400                                 // If this is the last virtual mouse binding in existence,
401                                 // remove our document touchstart listener.
402
403                                 --activeDocHandlers[ "touchstart" ];
404
405                                 if ( !activeDocHandlers[ "touchstart" ] ) {
406                                         $document.unbind( "touchstart", handleTouchStart )
407                                                 .unbind( "touchmove", handleTouchMove )
408                                                 .unbind( "touchend", handleTouchEnd )
409                                                 .unbind( "scroll", handleScroll );
410                                 }
411                         }
412
413                         var $this = $( this ),
414                                 bindings = $.data( this, dataPropertyName );
415
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
420                         // using it.
421                         if ( bindings ) {
422                                 bindings[ eventType ] = false;
423                         }
424
425                         // Unregister the dummy event handler.
426
427                         $this.unbind( realType, dummyMouseHandler );
428
429                         // If this is the last virtual mouse binding on the
430                         // element, remove the binding data from the element.
431
432                         if ( !hasVirtualBindings( this ) ) {
433                                 $this.removeData( dataPropertyName );
434                         }
435                 }
436         };
437 }
438
439 // Expose our custom events to the jQuery bind/unbind mechanism.
440
441 for ( var i = 0; i < virtualEventNames.length; i++ ){
442         $.event.special[ virtualEventNames[ i ] ] = getSpecialEventObject( virtualEventNames[ i ] );
443 }
444
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,
451                         target = e.target,
452                         x, y, ele, i, o, touchID;
453
454                 if ( cnt ) {
455                         x = e.clientX;
456                         y = e.clientY;
457                         threshold = $.vmouse.clickDistanceThreshold;
458
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.
463                         //
464                         // Why do we have to rely on proximity?
465                         //
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.
477                         //
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.
485
486                         ele = target;
487
488                         while ( ele ) {
489                                 for ( i = 0; i < cnt; i++ ) {
490                                         o = clickBlockList[ i ];
491                                         touchID = 0;
492
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.
497                                                 e.preventDefault();
498                                                 e.stopPropagation();
499                                                 return;
500                                         }
501                                 }
502                                 ele = ele.parentNode;
503                         }
504                 }
505         }, true);
506 }
507 })( jQuery, window, document );
508 //>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
509 });
510 //>>excludeEnd("jqmBuildExclude");