PageRenderTime 67ms CodeModel.GetById 40ms RepoModel.GetById 0ms app.codeStats 0ms

/js/jquery.mobile.vmouse.js

https://github.com/lagartoflojo/jquery-mobile
JavaScript | 483 lines | 288 code | 97 blank | 98 comment | 56 complexity | 7d8840ae4b1f89591f8a3bbb741eeb0b MD5 | raw file
  1. /*
  2. * jQuery Mobile Framework : "mouse" plugin
  3. * Copyright (c) jQuery Project
  4. * Dual licensed under the MIT or GPL Version 2 licenses.
  5. * http://jquery.org/license
  6. */
  7. // This plugin is an experiment for abstracting away the touch and mouse
  8. // events so that developers don't have to worry about which method of input
  9. // the device their document is loaded on supports.
  10. //
  11. // The idea here is to allow the developer to register listeners for the
  12. // basic mouse events, such as mousedown, mousemove, mouseup, and click,
  13. // and the plugin will take care of registering the correct listeners
  14. // behind the scenes to invoke the listener at the fastest possible time
  15. // for that device, while still retaining the order of event firing in
  16. // the traditional mouse environment, should multiple handlers be registered
  17. // on the same element for different events.
  18. //
  19. // The current version exposes the following virtual events to jQuery bind methods:
  20. // "vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel"
  21. (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 = {},
  27. resetTimerID = 0,
  28. startX = 0,
  29. startY = 0,
  30. didScroll = false,
  31. clickBlockList = [],
  32. blockMouseTriggers = false,
  33. blockTouchTriggers = false,
  34. eventCaptureSupported = $.support.eventCapture,
  35. $document = $( document ),
  36. nextTouchID = 1,
  37. lastTouchID = 0;
  38. $.vmouse = {
  39. moveDistanceThreshold: 10,
  40. clickDistanceThreshold: 10,
  41. resetTimerDuration: 1500
  42. };
  43. function getNativeEvent( event ) {
  44. while ( event && typeof event.originalEvent !== "undefined" ) {
  45. event = event.originalEvent;
  46. }
  47. return event;
  48. }
  49. function createVirtualEvent( event, eventType ) {
  50. var t = event.type,
  51. oe, props, ne, prop, ct, touch, i, j;
  52. event = $.Event(event);
  53. event.type = eventType;
  54. oe = event.originalEvent;
  55. props = $.event.props;
  56. // copy original event properties over to the new event
  57. // this would happen if we could call $.event.fix instead of $.Event
  58. // but we don't have a way to force an event to be fixed multiple times
  59. if ( oe ) {
  60. for ( i = props.length, prop; i; ) {
  61. prop = props[ --i ];
  62. event[ prop ] = oe[ prop ];
  63. }
  64. }
  65. if ( t.search(/^touch/) !== -1 ) {
  66. ne = getNativeEvent( oe );
  67. t = ne.touches;
  68. ct = ne.changedTouches;
  69. touch = ( t && t.length ) ? t[0] : ( (ct && ct.length) ? ct[ 0 ] : undefined );
  70. if ( touch ) {
  71. for ( j = 0, len = touchEventProps.length; j < len; j++){
  72. prop = touchEventProps[ j ];
  73. event[ prop ] = touch[ prop ];
  74. }
  75. }
  76. }
  77. return event;
  78. }
  79. function getVirtualBindingFlags( element ) {
  80. var flags = {},
  81. b, k;
  82. while ( element ) {
  83. b = $.data( element, dataPropertyName );
  84. for ( k in b ) {
  85. if ( b[ k ] ) {
  86. flags[ k ] = flags.hasVirtualBinding = true;
  87. }
  88. }
  89. element = element.parentNode;
  90. }
  91. return flags;
  92. }
  93. function getClosestElementWithVirtualBinding( element, eventType ) {
  94. var b;
  95. while ( element ) {
  96. b = $.data( element, dataPropertyName );
  97. if ( b && ( !eventType || b[ eventType ] ) ) {
  98. return element;
  99. }
  100. element = element.parentNode;
  101. }
  102. return null;
  103. }
  104. function enableTouchBindings() {
  105. blockTouchTriggers = false;
  106. }
  107. function disableTouchBindings() {
  108. blockTouchTriggers = true;
  109. }
  110. function enableMouseBindings() {
  111. lastTouchID = 0;
  112. clickBlockList.length = 0;
  113. blockMouseTriggers = false;
  114. // When mouse bindings are enabled, our
  115. // touch bindings are disabled.
  116. disableTouchBindings();
  117. }
  118. function disableMouseBindings() {
  119. // When mouse bindings are disabled, our
  120. // touch bindings are enabled.
  121. enableTouchBindings();
  122. }
  123. function startResetTimer() {
  124. clearResetTimer();
  125. resetTimerID = setTimeout(function(){
  126. resetTimerID = 0;
  127. enableMouseBindings();
  128. }, $.vmouse.resetTimerDuration );
  129. }
  130. function clearResetTimer() {
  131. if ( resetTimerID ){
  132. clearTimeout( resetTimerID );
  133. resetTimerID = 0;
  134. }
  135. }
  136. function triggerVirtualEvent( eventType, event, flags ) {
  137. var defaultPrevented = false,
  138. ve;
  139. if ( ( flags && flags[ eventType ] ) ||
  140. ( !flags && getClosestElementWithVirtualBinding( event.target, eventType ) ) ) {
  141. ve = createVirtualEvent( event, eventType );
  142. $( event.target).trigger( ve );
  143. defaultPrevented = ve.isDefaultPrevented();
  144. }
  145. return defaultPrevented;
  146. }
  147. function mouseEventCallback( event ) {
  148. var touchID = $.data(event.target, touchTargetPropertyName);
  149. if ( !blockMouseTriggers && ( !lastTouchID || lastTouchID !== touchID ) ){
  150. triggerVirtualEvent( "v" + event.type, event );
  151. }
  152. }
  153. function handleTouchStart( event ) {
  154. var touches = getNativeEvent( event ).touches,
  155. target, flags;
  156. if ( touches && touches.length === 1 ) {
  157. target = event.target;
  158. flags = getVirtualBindingFlags( target );
  159. if ( flags.hasVirtualBinding ) {
  160. lastTouchID = nextTouchID++;
  161. $.data( target, touchTargetPropertyName, lastTouchID );
  162. clearResetTimer();
  163. disableMouseBindings();
  164. didScroll = false;
  165. var t = getNativeEvent( event ).touches[ 0 ];
  166. startX = t.pageX;
  167. startY = t.pageY;
  168. triggerVirtualEvent( "vmouseover", event, flags );
  169. triggerVirtualEvent( "vmousedown", event, flags );
  170. }
  171. }
  172. }
  173. function handleScroll( event ) {
  174. if ( blockTouchTriggers ) {
  175. return;
  176. }
  177. if ( !didScroll ) {
  178. triggerVirtualEvent( "vmousecancel", event, getVirtualBindingFlags( event.target ) );
  179. }
  180. didScroll = true;
  181. startResetTimer();
  182. }
  183. function handleTouchMove( event ) {
  184. if ( blockTouchTriggers ) {
  185. return;
  186. }
  187. var t = getNativeEvent( event ).touches[ 0 ],
  188. didCancel = didScroll,
  189. moveThreshold = $.vmouse.moveDistanceThreshold;
  190. didScroll = didScroll ||
  191. ( Math.abs(t.pageX - startX) > moveThreshold ||
  192. Math.abs(t.pageY - startY) > moveThreshold ),
  193. flags = getVirtualBindingFlags( event.target );
  194. if ( didScroll && !didCancel ) {
  195. triggerVirtualEvent( "vmousecancel", event, flags );
  196. }
  197. triggerVirtualEvent( "vmousemove", event, flags );
  198. startResetTimer();
  199. }
  200. function handleTouchEnd( event ) {
  201. if ( blockTouchTriggers ) {
  202. return;
  203. }
  204. disableTouchBindings();
  205. var flags = getVirtualBindingFlags( event.target ),
  206. t;
  207. triggerVirtualEvent( "vmouseup", event, flags );
  208. if ( !didScroll ) {
  209. if ( triggerVirtualEvent( "vclick", event, flags ) ) {
  210. // The target of the mouse events that follow the touchend
  211. // event don't necessarily match the target used during the
  212. // touch. This means we need to rely on coordinates for blocking
  213. // any click that is generated.
  214. t = getNativeEvent( event ).changedTouches[ 0 ];
  215. clickBlockList.push({
  216. touchID: lastTouchID,
  217. x: t.clientX,
  218. y: t.clientY
  219. });
  220. // Prevent any mouse events that follow from triggering
  221. // virtual event notifications.
  222. blockMouseTriggers = true;
  223. }
  224. }
  225. triggerVirtualEvent( "vmouseout", event, flags);
  226. didScroll = false;
  227. startResetTimer();
  228. }
  229. function hasVirtualBindings( ele ) {
  230. var bindings = $.data( ele, dataPropertyName ),
  231. k;
  232. if ( bindings ) {
  233. for ( k in bindings ) {
  234. if ( bindings[ k ] ) {
  235. return true;
  236. }
  237. }
  238. }
  239. return false;
  240. }
  241. function dummyMouseHandler(){}
  242. function getSpecialEventObject( eventType ) {
  243. var realType = eventType.substr( 1 );
  244. return {
  245. setup: function( data, namespace ) {
  246. // If this is the first virtual mouse binding for this element,
  247. // add a bindings object to its data.
  248. if ( !hasVirtualBindings( this ) ) {
  249. $.data( this, dataPropertyName, {});
  250. }
  251. // If setup is called, we know it is the first binding for this
  252. // eventType, so initialize the count for the eventType to zero.
  253. var bindings = $.data( this, dataPropertyName );
  254. bindings[ eventType ] = true;
  255. // If this is the first virtual mouse event for this type,
  256. // register a global handler on the document.
  257. activeDocHandlers[ eventType ] = ( activeDocHandlers[ eventType ] || 0 ) + 1;
  258. if ( activeDocHandlers[ eventType ] === 1 ) {
  259. $document.bind( realType, mouseEventCallback );
  260. }
  261. // Some browsers, like Opera Mini, won't dispatch mouse/click events
  262. // for elements unless they actually have handlers registered on them.
  263. // To get around this, we register dummy handlers on the elements.
  264. $( this ).bind( realType, dummyMouseHandler );
  265. // For now, if event capture is not supported, we rely on mouse handlers.
  266. if ( eventCaptureSupported ) {
  267. // If this is the first virtual mouse binding for the document,
  268. // register our touchstart handler on the document.
  269. activeDocHandlers[ "touchstart" ] = ( activeDocHandlers[ "touchstart" ] || 0) + 1;
  270. if (activeDocHandlers[ "touchstart" ] === 1) {
  271. $document.bind( "touchstart", handleTouchStart )
  272. .bind( "touchend", handleTouchEnd )
  273. // On touch platforms, touching the screen and then dragging your finger
  274. // causes the window content to scroll after some distance threshold is
  275. // exceeded. On these platforms, a scroll prevents a click event from being
  276. // dispatched, and on some platforms, even the touchend is suppressed. To
  277. // mimic the suppression of the click event, we need to watch for a scroll
  278. // event. Unfortunately, some platforms like iOS don't dispatch scroll
  279. // events until *AFTER* the user lifts their finger (touchend). This means
  280. // we need to watch both scroll and touchmove events to figure out whether
  281. // or not a scroll happenens before the touchend event is fired.
  282. .bind( "touchmove", handleTouchMove )
  283. .bind( "scroll", handleScroll );
  284. }
  285. }
  286. },
  287. teardown: function( data, namespace ) {
  288. // If this is the last virtual binding for this eventType,
  289. // remove its global handler from the document.
  290. --activeDocHandlers[ eventType ];
  291. if ( !activeDocHandlers[ eventType ] ) {
  292. $document.unbind( realType, mouseEventCallback );
  293. }
  294. if ( eventCaptureSupported ) {
  295. // If this is the last virtual mouse binding in existence,
  296. // remove our document touchstart listener.
  297. --activeDocHandlers[ "touchstart" ];
  298. if ( !activeDocHandlers[ "touchstart" ] ) {
  299. $document.unbind( "touchstart", handleTouchStart )
  300. .unbind( "touchmove", handleTouchMove )
  301. .unbind( "touchend", handleTouchEnd )
  302. .unbind( "scroll", handleScroll );
  303. }
  304. }
  305. var $this = $( this ),
  306. bindings = $.data( this, dataPropertyName );
  307. // teardown may be called when an element was
  308. // removed from the DOM. If this is the case,
  309. // jQuery core may have already stripped the element
  310. // of any data bindings so we need to check it before
  311. // using it.
  312. if ( bindings ) {
  313. bindings[ eventType ] = false;
  314. }
  315. // Unregister the dummy event handler.
  316. $this.unbind( realType, dummyMouseHandler );
  317. // If this is the last virtual mouse binding on the
  318. // element, remove the binding data from the element.
  319. if ( !hasVirtualBindings( this ) ) {
  320. $this.removeData( dataPropertyName );
  321. }
  322. }
  323. };
  324. }
  325. // Expose our custom events to the jQuery bind/unbind mechanism.
  326. for ( var i = 0; i < virtualEventNames.length; i++ ){
  327. $.event.special[ virtualEventNames[ i ] ] = getSpecialEventObject( virtualEventNames[ i ] );
  328. }
  329. // Add a capture click handler to block clicks.
  330. // Note that we require event capture support for this so if the device
  331. // doesn't support it, we punt for now and rely solely on mouse events.
  332. if ( eventCaptureSupported ) {
  333. document.addEventListener( "click", function( e ){
  334. var cnt = clickBlockList.length,
  335. target = e.target,
  336. x, y, ele, i, o, touchID;
  337. if ( cnt ) {
  338. x = e.clientX;
  339. y = e.clientY;
  340. threshold = $.vmouse.clickDistanceThreshold;
  341. // The idea here is to run through the clickBlockList to see if
  342. // the current click event is in the proximity of one of our
  343. // vclick events that had preventDefault() called on it. If we find
  344. // one, then we block the click.
  345. //
  346. // Why do we have to rely on proximity?
  347. //
  348. // Because the target of the touch event that triggered the vclick
  349. // can be different from the target of the click event synthesized
  350. // by the browser. The target of a mouse/click event that is syntehsized
  351. // from a touch event seems to be implementation specific. For example,
  352. // some browsers will fire mouse/click events for a link that is near
  353. // a touch event, even though the target of the touchstart/touchend event
  354. // says the user touched outside the link. Also, it seems that with most
  355. // browsers, the target of the mouse/click event is not calculated until the
  356. // time it is dispatched, so if you replace an element that you touched
  357. // with another element, the target of the mouse/click will be the new
  358. // element underneath that point.
  359. //
  360. // Aside from proximity, we also check to see if the target and any
  361. // of its ancestors were the ones that blocked a click. This is necessary
  362. // because of the strange mouse/click target calculation done in the
  363. // Android 2.1 browser, where if you click on an element, and there is a
  364. // mouse/click handler on one of its ancestors, the target will be the
  365. // innermost child of the touched element, even if that child is no where
  366. // near the point of touch.
  367. ele = target;
  368. while ( ele ) {
  369. for ( i = 0; i < cnt; i++ ) {
  370. o = clickBlockList[ i ];
  371. touchID = 0;
  372. if ( ( ele === target && Math.abs( o.x - x ) < threshold && Math.abs( o.y - y ) < threshold ) ||
  373. $.data( ele, touchTargetPropertyName ) === o.touchID ) {
  374. // XXX: We may want to consider removing matches from the block list
  375. // instead of waiting for the reset timer to fire.
  376. e.preventDefault();
  377. e.stopPropagation();
  378. return;
  379. }
  380. }
  381. ele = ele.parentNode;
  382. }
  383. }
  384. }, true);
  385. }
  386. })( jQuery, window, document );