6fa6d8f36bd6a5250b56652d947ebf493c12ee77
[platform/framework/web/web-ui-fw.git] / libs / js / jquery-mobile-1.0.1pre / js / jquery.mobile.vmouse.js
1 /*
2 * "mouse" plugin
3 */
4
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.
8 //
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.
16 //
17 // The current version exposes the following virtual events to jQuery bind methods:
18 // "vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel"
19
20 (function( $, window, document, undefined ) {
21
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 = {},
27         resetTimerID = 0,
28         startX = 0,
29         startY = 0,
30         didScroll = false,
31         clickBlockList = [],
32         blockMouseTriggers = false,
33         blockTouchTriggers = false,
34         eventCaptureSupported = "addEventListener" in document,
35         $document = $( document ),
36         nextTouchID = 1,
37         lastTouchID = 0;
38
39 $.vmouse = {
40         moveDistanceThreshold: 10,
41         clickDistanceThreshold: 10,
42         resetTimerDuration: 1500
43 };
44
45 function getNativeEvent( event ) {
46
47         while ( event && typeof event.originalEvent !== "undefined" ) {
48                 event = event.originalEvent;
49         }
50         return event;
51 }
52
53 function createVirtualEvent( event, eventType ) {
54
55         var t = event.type,
56                 oe, props, ne, prop, ct, touch, i, j;
57
58         event = $.Event(event);
59         event.type = eventType;
60
61         oe = event.originalEvent;
62         props = $.event.props;
63
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
67         if ( oe ) {
68                 for ( i = props.length, prop; i; ) {
69                         prop = props[ --i ];
70                         event[ prop ] = oe[ prop ];
71                 }
72         }
73
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 ){
77                 event.which = 1;
78         }
79
80         if ( t.search(/^touch/) !== -1 ) {
81                 ne = getNativeEvent( oe );
82                 t = ne.touches;
83                 ct = ne.changedTouches;
84                 touch = ( t && t.length ) ? t[0] : ( (ct && ct.length) ? ct[ 0 ] : undefined );
85
86                 if ( touch ) {
87                         for ( j = 0, len = touchEventProps.length; j < len; j++){
88                                 prop = touchEventProps[ j ];
89                                 event[ prop ] = touch[ prop ];
90                         }
91                 }
92         }
93
94         return event;
95 }
96
97 function getVirtualBindingFlags( element ) {
98
99         var flags = {},
100                 b, k;
101
102         while ( element ) {
103
104                 b = $.data( element, dataPropertyName );
105
106                 for (  k in b ) {
107                         if ( b[ k ] ) {
108                                 flags[ k ] = flags.hasVirtualBinding = true;
109                         }
110                 }
111                 element = element.parentNode;
112         }
113         return flags;
114 }
115
116 function getClosestElementWithVirtualBinding( element, eventType ) {
117         var b;
118         while ( element ) {
119
120                 b = $.data( element, dataPropertyName );
121
122                 if ( b && ( !eventType || b[ eventType ] ) ) {
123                         return element;
124                 }
125                 element = element.parentNode;
126         }
127         return null;
128 }
129
130 function enableTouchBindings() {
131         blockTouchTriggers = false;
132 }
133
134 function disableTouchBindings() {
135         blockTouchTriggers = true;
136 }
137
138 function enableMouseBindings() {
139         lastTouchID = 0;
140         clickBlockList.length = 0;
141         blockMouseTriggers = false;
142
143         // When mouse bindings are enabled, our
144         // touch bindings are disabled.
145         disableTouchBindings();
146 }
147
148 function disableMouseBindings() {
149         // When mouse bindings are disabled, our
150         // touch bindings are enabled.
151         enableTouchBindings();
152 }
153
154 function startResetTimer() {
155         clearResetTimer();
156         resetTimerID = setTimeout(function(){
157                 resetTimerID = 0;
158                 enableMouseBindings();
159         }, $.vmouse.resetTimerDuration );
160 }
161
162 function clearResetTimer() {
163         if ( resetTimerID ){
164                 clearTimeout( resetTimerID );
165                 resetTimerID = 0;
166         }
167 }
168
169 function triggerVirtualEvent( eventType, event, flags ) {
170         var ve;
171
172         if ( ( flags && flags[ eventType ] ) ||
173                                 ( !flags && getClosestElementWithVirtualBinding( event.target, eventType ) ) ) {
174
175                 ve = createVirtualEvent( event, eventType );
176
177                 $( event.target).trigger( ve );
178         }
179
180         return ve;
181 }
182
183 function mouseEventCallback( event ) {
184         var touchID = $.data(event.target, touchTargetPropertyName);
185
186         if ( !blockMouseTriggers && ( !lastTouchID || lastTouchID !== touchID ) ){
187                 var ve = triggerVirtualEvent( "v" + event.type, event );
188                 if ( ve ) {
189                         if ( ve.isDefaultPrevented() ) {
190                                 event.preventDefault();
191                         }
192                         if ( ve.isPropagationStopped() ) {
193                                 event.stopPropagation();
194                         }
195                         if ( ve.isImmediatePropagationStopped() ) {
196                                 event.stopImmediatePropagation();
197                         }
198                 }
199         }
200 }
201
202 function handleTouchStart( event ) {
203
204         var touches = getNativeEvent( event ).touches,
205                 target, flags;
206
207         if ( touches && touches.length === 1 ) {
208
209                 target = event.target;
210                 flags = getVirtualBindingFlags( target );
211
212                 if ( flags.hasVirtualBinding ) {
213
214                         lastTouchID = nextTouchID++;
215                         $.data( target, touchTargetPropertyName, lastTouchID );
216
217                         clearResetTimer();
218
219                         disableMouseBindings();
220                         didScroll = false;
221
222                         var t = getNativeEvent( event ).touches[ 0 ];
223                         startX = t.pageX;
224                         startY = t.pageY;
225
226                         triggerVirtualEvent( "vmouseover", event, flags );
227                         triggerVirtualEvent( "vmousedown", event, flags );
228                 }
229         }
230 }
231
232 function handleScroll( event ) {
233         if ( blockTouchTriggers ) {
234                 return;
235         }
236
237         if ( !didScroll ) {
238                 triggerVirtualEvent( "vmousecancel", event, getVirtualBindingFlags( event.target ) );
239         }
240
241         didScroll = true;
242         startResetTimer();
243 }
244
245 function handleTouchMove( event ) {
246         if ( blockTouchTriggers ) {
247                 return;
248         }
249
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 );
257
258         if ( didScroll && !didCancel ) {
259                 triggerVirtualEvent( "vmousecancel", event, flags );
260         }
261
262         triggerVirtualEvent( "vmousemove", event, flags );
263         startResetTimer();
264 }
265
266 function handleTouchEnd( event ) {
267         if ( blockTouchTriggers ) {
268                 return;
269         }
270
271         disableTouchBindings();
272
273         var flags = getVirtualBindingFlags( event.target ),
274                 t;
275         triggerVirtualEvent( "vmouseup", event, flags );
276
277         if ( !didScroll ) {
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,
287                                 x: t.clientX,
288                                 y: t.clientY
289                         });
290
291                         // Prevent any mouse events that follow from triggering
292                         // virtual event notifications.
293                         blockMouseTriggers = true;
294                 }
295         }
296         triggerVirtualEvent( "vmouseout", event, flags);
297         didScroll = false;
298
299         startResetTimer();
300 }
301
302 function hasVirtualBindings( ele ) {
303         var bindings = $.data( ele, dataPropertyName ),
304                 k;
305
306         if ( bindings ) {
307                 for ( k in bindings ) {
308                         if ( bindings[ k ] ) {
309                                 return true;
310                         }
311                 }
312         }
313         return false;
314 }
315
316 function dummyMouseHandler(){}
317
318 function getSpecialEventObject( eventType ) {
319         var realType = eventType.substr( 1 );
320
321         return {
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.
325
326                         if ( !hasVirtualBindings( this ) ) {
327                                 $.data( this, dataPropertyName, {});
328                         }
329
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;
334
335                         // If this is the first virtual mouse event for this type,
336                         // register a global handler on the document.
337
338                         activeDocHandlers[ eventType ] = ( activeDocHandlers[ eventType ] || 0 ) + 1;
339
340                         if ( activeDocHandlers[ eventType ] === 1 ) {
341                                 $document.bind( realType, mouseEventCallback );
342                         }
343
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.
347
348                         $( this ).bind( realType, dummyMouseHandler );
349
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.
354
355                                 activeDocHandlers[ "touchstart" ] = ( activeDocHandlers[ "touchstart" ] || 0) + 1;
356
357                                 if (activeDocHandlers[ "touchstart" ] === 1) {
358                                         $document.bind( "touchstart", handleTouchStart )
359                                                 .bind( "touchend", handleTouchEnd )
360
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.
370
371                                                 .bind( "touchmove", handleTouchMove )
372                                                 .bind( "scroll", handleScroll );
373                                 }
374                         }
375                 },
376
377                 teardown: function( data, namespace ) {
378                         // If this is the last virtual binding for this eventType,
379                         // remove its global handler from the document.
380
381                         --activeDocHandlers[ eventType ];
382
383                         if ( !activeDocHandlers[ eventType ] ) {
384                                 $document.unbind( realType, mouseEventCallback );
385                         }
386
387                         if ( eventCaptureSupported ) {
388                                 // If this is the last virtual mouse binding in existence,
389                                 // remove our document touchstart listener.
390
391                                 --activeDocHandlers[ "touchstart" ];
392
393                                 if ( !activeDocHandlers[ "touchstart" ] ) {
394                                         $document.unbind( "touchstart", handleTouchStart )
395                                                 .unbind( "touchmove", handleTouchMove )
396                                                 .unbind( "touchend", handleTouchEnd )
397                                                 .unbind( "scroll", handleScroll );
398                                 }
399                         }
400
401                         var $this = $( this ),
402                                 bindings = $.data( this, dataPropertyName );
403
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
408                         // using it.
409                         if ( bindings ) {
410                                 bindings[ eventType ] = false;
411                         }
412
413                         // Unregister the dummy event handler.
414
415                         $this.unbind( realType, dummyMouseHandler );
416
417                         // If this is the last virtual mouse binding on the
418                         // element, remove the binding data from the element.
419
420                         if ( !hasVirtualBindings( this ) ) {
421                                 $this.removeData( dataPropertyName );
422                         }
423                 }
424         };
425 }
426
427 // Expose our custom events to the jQuery bind/unbind mechanism.
428
429 for ( var i = 0; i < virtualEventNames.length; i++ ){
430         $.event.special[ virtualEventNames[ i ] ] = getSpecialEventObject( virtualEventNames[ i ] );
431 }
432
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,
439                         target = e.target,
440                         x, y, ele, i, o, touchID;
441
442                 if ( cnt ) {
443                         x = e.clientX;
444                         y = e.clientY;
445                         threshold = $.vmouse.clickDistanceThreshold;
446
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.
451                         //
452                         // Why do we have to rely on proximity?
453                         //
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.
465                         //
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.
473
474                         ele = target;
475
476                         while ( ele ) {
477                                 for ( i = 0; i < cnt; i++ ) {
478                                         o = clickBlockList[ i ];
479                                         touchID = 0;
480
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.
485                                                 e.preventDefault();
486                                                 e.stopPropagation();
487                                                 return;
488                                         }
489                                 }
490                                 ele = ele.parentNode;
491                         }
492                 }
493         }, true);
494 }
495 })( jQuery, window, document );