hammer.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. /*
  2. * Hammer.JS
  3. * version 0.4
  4. * author: Eight Media
  5. * https://github.com/EightMedia/hammer.js
  6. */
  7. function Hammer(element, options, undefined)
  8. {
  9. var self = this;
  10. var defaults = {
  11. // prevent the default event or not... might be buggy when false
  12. prevent_default : false,
  13. css_hacks : true,
  14. drag : true,
  15. drag_vertical : true,
  16. drag_horizontal : true,
  17. // minimum distance before the drag event starts
  18. drag_min_distance : 20, // pixels
  19. // pinch zoom and rotation
  20. transform : true,
  21. scale_treshold : 0.1,
  22. rotation_treshold : 15, // degrees
  23. tap : true,
  24. tap_double : true,
  25. tap_max_interval : 300,
  26. tap_double_distance: 20,
  27. hold : true,
  28. hold_timeout : 500
  29. };
  30. options = mergeObject(defaults, options);
  31. // some css hacks
  32. (function() {
  33. if(!options.css_hacks) {
  34. return false;
  35. }
  36. var vendors = ['webkit','moz','ms','o',''];
  37. var css_props = {
  38. "userSelect": "none",
  39. "touchCallout": "none",
  40. "userDrag": "none",
  41. "tapHighlightColor": "rgba(0,0,0,0)"
  42. };
  43. var prop = '';
  44. for(var i = 0; i < vendors.length; i++) {
  45. for(var p in css_props) {
  46. prop = p;
  47. if(vendors[i]) {
  48. prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1);
  49. }
  50. element.style[ prop ] = css_props[p];
  51. }
  52. }
  53. })();
  54. // holds the distance that has been moved
  55. var _distance = 0;
  56. // holds the exact angle that has been moved
  57. var _angle = 0;
  58. // holds the diraction that has been moved
  59. var _direction = 0;
  60. // holds position movement for sliding
  61. var _pos = { };
  62. // how many fingers are on the screen
  63. var _fingers = 0;
  64. var _first = false;
  65. var _gesture = null;
  66. var _prev_gesture = null;
  67. var _touch_start_time = null;
  68. var _prev_tap_pos = {x: 0, y: 0};
  69. var _prev_tap_end_time = null;
  70. var _hold_timer = null;
  71. var _offset = {};
  72. // keep track of the mouse status
  73. var _mousedown = false;
  74. var _event_start;
  75. var _event_move;
  76. var _event_end;
  77. /**
  78. * angle to direction define
  79. * @param float angle
  80. * @return string direction
  81. */
  82. this.getDirectionFromAngle = function( angle )
  83. {
  84. var directions = {
  85. down: angle >= 45 && angle < 135, //90
  86. left: angle >= 135 || angle <= -135, //180
  87. up: angle < -45 && angle > -135, //270
  88. right: angle >= -45 && angle <= 45 //0
  89. };
  90. var direction, key;
  91. for(key in directions){
  92. if(directions[key]){
  93. direction = key;
  94. break;
  95. }
  96. }
  97. return direction;
  98. };
  99. /**
  100. * count the number of fingers in the event
  101. * when no fingers are detected, one finger is returned (mouse pointer)
  102. * @param event
  103. * @return int fingers
  104. */
  105. function countFingers( event )
  106. {
  107. // there is a bug on android (until v4?) that touches is always 1,
  108. // so no multitouch is supported, e.g. no, zoom and rotation...
  109. return event.touches ? event.touches.length : 1;
  110. }
  111. /**
  112. * get the x and y positions from the event object
  113. * @param event
  114. * @return array [{ x: int, y: int }]
  115. */
  116. function getXYfromEvent( event )
  117. {
  118. event = event || window.event;
  119. // no touches, use the event pageX and pageY
  120. if(!event.touches) {
  121. var doc = document,
  122. body = doc.body;
  123. return [{
  124. x: event.pageX || event.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && doc.clientLeft || 0 ),
  125. y: event.pageY || event.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && doc.clientTop || 0 )
  126. }];
  127. }
  128. // multitouch, return array with positions
  129. else {
  130. var pos = [], src;
  131. for(var t=0, len=event.touches.length; t<len; t++) {
  132. src = event.touches[t];
  133. pos.push({ x: src.pageX, y: src.pageY });
  134. }
  135. return pos;
  136. }
  137. }
  138. /**
  139. * calculate the angle between two points
  140. * @param object pos1 { x: int, y: int }
  141. * @param object pos2 { x: int, y: int }
  142. */
  143. function getAngle( pos1, pos2 )
  144. {
  145. return Math.atan2(pos2.y - pos1.y, pos2.x - pos1.x) * 180 / Math.PI;
  146. }
  147. /**
  148. * trigger an event/callback by name with params
  149. * @param string name
  150. * @param array params
  151. */
  152. function triggerEvent( eventName, params )
  153. {
  154. // return touches object
  155. params.touches = getXYfromEvent(params.originalEvent);
  156. params.type = eventName;
  157. // trigger callback
  158. if(isFunction(self["on"+ eventName])) {
  159. self["on"+ eventName].call(self, params);
  160. }
  161. }
  162. /**
  163. * cancel event
  164. * @param object event
  165. * @return void
  166. */
  167. function cancelEvent(event){
  168. event = event || window.event;
  169. if(event.preventDefault){
  170. event.preventDefault();
  171. }else{
  172. event.returnValue = false;
  173. event.cancelBubble = true;
  174. }
  175. }
  176. /**
  177. * reset the internal vars to the start values
  178. */
  179. function reset()
  180. {
  181. _pos = {};
  182. _first = false;
  183. _fingers = 0;
  184. _distance = 0;
  185. _angle = 0;
  186. _gesture = null;
  187. }
  188. var gestures = {
  189. // hold gesture
  190. // fired on touchstart
  191. hold : function(event)
  192. {
  193. // only when one finger is on the screen
  194. if(options.hold) {
  195. _gesture = 'hold';
  196. clearTimeout(_hold_timer);
  197. _hold_timer = setTimeout(function() {
  198. if(_gesture == 'hold') {
  199. triggerEvent("hold", {
  200. originalEvent : event,
  201. position : _pos.start
  202. });
  203. }
  204. }, options.hold_timeout);
  205. }
  206. },
  207. // drag gesture
  208. // fired on mousemove
  209. drag : function(event)
  210. {
  211. // get the distance we moved
  212. var _distance_x = _pos.move[0].x - _pos.start[0].x;
  213. var _distance_y = _pos.move[0].y - _pos.start[0].y;
  214. _distance = Math.sqrt(_distance_x * _distance_x + _distance_y * _distance_y);
  215. // drag
  216. // minimal movement required
  217. if(options.drag && (_distance > options.drag_min_distance) || _gesture == 'drag') {
  218. // calculate the angle
  219. _angle = getAngle(_pos.start[0], _pos.move[0]);
  220. _direction = self.getDirectionFromAngle(_angle);
  221. // check the movement and stop if we go in the wrong direction
  222. var is_vertical = (_direction == 'up' || _direction == 'down');
  223. if(((is_vertical && !options.drag_vertical) || (!is_vertical && !options.drag_horizontal))
  224. && (_distance > options.drag_min_distance)) {
  225. return;
  226. }
  227. _gesture = 'drag';
  228. var position = { x: _pos.move[0].x - _offset.left,
  229. y: _pos.move[0].y - _offset.top };
  230. var event_obj = {
  231. originalEvent : event,
  232. position : position,
  233. direction : _direction,
  234. distance : _distance,
  235. distanceX : _distance_x,
  236. distanceY : _distance_y,
  237. angle : _angle
  238. };
  239. // on the first time trigger the start event
  240. if(_first) {
  241. triggerEvent("dragstart", event_obj);
  242. _first = false;
  243. }
  244. // normal slide event
  245. triggerEvent("drag", event_obj);
  246. cancelEvent(event);
  247. }
  248. },
  249. // transform gesture
  250. // fired on touchmove
  251. transform : function(event)
  252. {
  253. if(options.transform) {
  254. var scale = event.scale || 1;
  255. var rotation = event.rotation || 0;
  256. if(countFingers(event) != 2) {
  257. return false;
  258. }
  259. if(_gesture != 'drag' &&
  260. (_gesture == 'transform' || Math.abs(1-scale) > options.scale_treshold
  261. || Math.abs(rotation) > options.rotation_treshold)) {
  262. _gesture = 'transform';
  263. _pos.center = { x: ((_pos.move[0].x + _pos.move[1].x) / 2) - _offset.left,
  264. y: ((_pos.move[0].y + _pos.move[1].y) / 2) - _offset.top };
  265. var event_obj = {
  266. originalEvent : event,
  267. position : _pos.center,
  268. scale : scale,
  269. rotation : rotation
  270. };
  271. // on the first time trigger the start event
  272. if(_first) {
  273. triggerEvent("transformstart", event_obj);
  274. _first = false;
  275. }
  276. triggerEvent("transform", event_obj);
  277. cancelEvent(event);
  278. return true;
  279. }
  280. }
  281. return false;
  282. },
  283. // tap and double tap gesture
  284. // fired on touchend
  285. tap : function(event)
  286. {
  287. // compare the kind of gesture by time
  288. var now = new Date().getTime();
  289. var touch_time = now - _touch_start_time;
  290. // dont fire when hold is fired
  291. if(options.hold && !(options.hold && options.hold_timeout > touch_time)) {
  292. return;
  293. }
  294. // when previous event was tap and the tap was max_interval ms ago
  295. var is_double_tap = (function(){
  296. if (_prev_tap_pos && options.tap_double && _prev_gesture == 'tap' && (_touch_start_time - _prev_tap_end_time) < options.tap_max_interval) {
  297. var x_distance = Math.abs(_prev_tap_pos[0].x - _pos.start[0].x);
  298. var y_distance = Math.abs(_prev_tap_pos[0].y - _pos.start[0].y);
  299. return (_prev_tap_pos && _pos.start && Math.max(x_distance, y_distance) < options.tap_double_distance);
  300. }
  301. return false;
  302. })();
  303. if(is_double_tap) {
  304. _gesture = 'double_tap';
  305. _prev_tap_end_time = null;
  306. triggerEvent("doubletap", {
  307. originalEvent : event,
  308. position : _pos.start
  309. });
  310. cancelEvent(event);
  311. }
  312. // single tap is single touch
  313. else {
  314. _gesture = 'tap';
  315. _prev_tap_end_time = now;
  316. _prev_tap_pos = _pos.start;
  317. if(options.tap) {
  318. triggerEvent("tap", {
  319. originalEvent : event,
  320. position : _pos.start
  321. });
  322. cancelEvent(event);
  323. }
  324. }
  325. }
  326. };
  327. function handleEvents(event)
  328. {
  329. switch(event.type)
  330. {
  331. case 'mousedown':
  332. case 'touchstart':
  333. _pos.start = getXYfromEvent(event);
  334. _touch_start_time = new Date().getTime();
  335. _fingers = countFingers(event);
  336. _first = true;
  337. _event_start = event;
  338. // borrowed from jquery offset https://github.com/jquery/jquery/blob/master/src/offset.js
  339. var box = element.getBoundingClientRect();
  340. var clientTop = element.clientTop || document.body.clientTop || 0;
  341. var clientLeft = element.clientLeft || document.body.clientLeft || 0;
  342. var scrollTop = window.pageYOffset || element.scrollTop || document.body.scrollTop;
  343. var scrollLeft = window.pageXOffset || element.scrollLeft || document.body.scrollLeft;
  344. _offset = {
  345. top: box.top + scrollTop - clientTop,
  346. left: box.left + scrollLeft - clientLeft
  347. };
  348. _mousedown = true;
  349. // hold gesture
  350. gestures.hold(event);
  351. if(options.prevent_default) {
  352. cancelEvent(event);
  353. }
  354. break;
  355. case 'mousemove':
  356. case 'touchmove':
  357. if(!_mousedown) {
  358. return false;
  359. }
  360. _event_move = event;
  361. _pos.move = getXYfromEvent(event);
  362. if(!gestures.transform(event)) {
  363. gestures.drag(event);
  364. }
  365. break;
  366. case 'mouseup':
  367. case 'mouseout':
  368. case 'touchcancel':
  369. case 'touchend':
  370. if(!_mousedown || (_gesture != 'transform' && event.touches && event.touches.length > 0)) {
  371. return false;
  372. }
  373. _mousedown = false;
  374. _event_end = event;
  375. // drag gesture
  376. // dragstart is triggered, so dragend is possible
  377. if(_gesture == 'drag') {
  378. triggerEvent("dragend", {
  379. originalEvent : event,
  380. direction : _direction,
  381. distance : _distance,
  382. angle : _angle
  383. });
  384. }
  385. // transform
  386. // transformstart is triggered, so transformed is possible
  387. else if(_gesture == 'transform') {
  388. triggerEvent("transformend", {
  389. originalEvent : event,
  390. position : _pos.center,
  391. scale : event.scale,
  392. rotation : event.rotation
  393. });
  394. }
  395. else {
  396. gestures.tap(_event_start);
  397. }
  398. _prev_gesture = _gesture;
  399. // reset vars
  400. reset();
  401. break;
  402. }
  403. }
  404. // bind events for touch devices
  405. // except for windows phone 7.5, it doesnt support touch events..!
  406. if('ontouchstart' in window) {
  407. element.addEventListener("touchstart", handleEvents, false);
  408. element.addEventListener("touchmove", handleEvents, false);
  409. element.addEventListener("touchend", handleEvents, false);
  410. element.addEventListener("touchcancel", handleEvents, false);
  411. }
  412. // for non-touch
  413. else {
  414. if(element.addEventListener){ // prevent old IE errors
  415. element.addEventListener("mouseout", function(event) {
  416. if(!isInsideHammer(element, event.relatedTarget)) {
  417. handleEvents(event);
  418. }
  419. }, false);
  420. element.addEventListener("mouseup", handleEvents, false);
  421. element.addEventListener("mousedown", handleEvents, false);
  422. element.addEventListener("mousemove", handleEvents, false);
  423. // events for older IE
  424. }else if(document.attachEvent){
  425. element.attachEvent("onmouseout", function(event) {
  426. if(!isInsideHammer(element, event.relatedTarget)) {
  427. handleEvents(event);
  428. }
  429. }, false);
  430. element.attachEvent("onmouseup", handleEvents);
  431. element.attachEvent("onmousedown", handleEvents);
  432. element.attachEvent("onmousemove", handleEvents);
  433. }
  434. }
  435. /**
  436. * find if element is (inside) given parent element
  437. * @param object element
  438. * @param object parent
  439. * @return bool inside
  440. */
  441. function isInsideHammer(parent, child) {
  442. // get related target for IE
  443. if(!child && window.event && window.event.toElement){
  444. child = window.event.toElement;
  445. }
  446. if(parent === child){
  447. return true;
  448. }
  449. // loop over parentNodes of child until we find hammer element
  450. if(child){
  451. var node = child.parentNode;
  452. while(node !== null){
  453. if(node === parent){
  454. return true;
  455. };
  456. node = node.parentNode;
  457. }
  458. }
  459. return false;
  460. }
  461. /**
  462. * merge 2 objects into a new object
  463. * @param object obj1
  464. * @param object obj2
  465. * @return object merged object
  466. */
  467. function mergeObject(obj1, obj2) {
  468. var output = {};
  469. if(!obj2) {
  470. return obj1;
  471. }
  472. for (var prop in obj1) {
  473. if (prop in obj2) {
  474. output[prop] = obj2[prop];
  475. } else {
  476. output[prop] = obj1[prop];
  477. }
  478. }
  479. return output;
  480. }
  481. function isFunction( obj ){
  482. return Object.prototype.toString.call( obj ) == "[object Function]";
  483. }
  484. }