├── .gitignore ├── jquery.hammer.js ├── LICENSE ├── jquery.specialevent.hammer.js ├── demo └── jquery.demo.html ├── README.md └── hammer.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /jquery.hammer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Hammer.JS jQuery plugin 3 | * version 0.3 4 | * author: Eight Media 5 | * https://github.com/EightMedia/hammer.js 6 | */ 7 | jQuery.fn.hammer = function(options) 8 | { 9 | return this.each(function() 10 | { 11 | var hammer = new Hammer(this, options); 12 | 13 | var $el = jQuery(this); 14 | $el.data("hammer", hammer); 15 | 16 | var events = ['hold','tap','doubletap','transformstart','transform','transformend','dragstart','drag','dragend','swipe','release']; 17 | 18 | for(var e=0; e 2 | 3 | 4 | jQuery special event API on hammerjs 5 | 6 | 7 | 8 | 18 | 19 | 20 |
21 | Touch me :) 22 |
23 |
24 |
25 | 44 | 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hammer.js 2 | 3 | ## A javascript library for multi-touch gestures 4 | 5 | > I told you, homeboy / 6 | > You *CAN* touch this / 7 | > Yeah, that's how we living and you know / 8 | > You *CAN* touch this 9 | 10 | Hammer.js is a javascript library that can be used to control gestures on touch devices. It supports the following gestures: 11 | 12 | - Tap 13 | - Double tap 14 | - Hold 15 | - Drag 16 | - Swipe 17 | - Transform (pinch) 18 | 19 | 20 | ## Demo's 21 | > While it's rollin', hold on / 22 | > Pump a little bit and let 'em know it's goin' on / 23 | > Like that, like that 24 | 25 | We've created some demo's to show you the immense power of hammer.js: 26 | 27 | ### Basic demo 28 | A simple demo that demonstrates that hammer.js works and is able to recognize gestures. We output the gestures that are recognized. [Check it out](http://eightmedia.github.com/hammer.js/demo/) 29 | 30 | ### Slideshow 31 | A slideshow that uses hammer.js to switch slides. Note that the drag event in the slideshow is non-blocking for the scrolling of the page. [Check it out](http://eightmedia.github.com/hammer.js/slideshow/) 32 | 33 | ### Scroll content 34 | A touch-scrollable div. [Check it out](http://eightmedia.github.com/hammer.js/scroll/) 35 | 36 | ### Drag 37 | Move boxes around. [Check it out](http://eightmedia.github.com/hammer.js/drag/) 38 | 39 | ### Color traces 40 | We use hammer.js to generate beautiful traces with colorful balls. Balls! [Check it out](http://eightmedia.github.com/hammer.js/draw/) 41 | 42 | ### Pinch to zoom 43 | We use hammer.js to zoom in and out on an image by pinching. [Check it out](http://eightmedia.github.com/hammer.js/zoom/) 44 | 45 | ### Momentum 46 | Simulate the drag/throw experience on iOS/Android. [Try it](http://eightmedia.github.com/hammer.js/momentum/) 47 | 48 | ### Sites using Hammer.js 49 | [Eight Media](http://www.eight.nl) 50 | 51 | Want to add your site? Contact me at [Twitter](http://www.twitter.com/jorikdelaporik) 52 | 53 | 54 | ## Documentation 55 | > So wave your hands in the air / 56 | > Bust a few moves / 57 | > Run your fingers through your hair 58 | 59 | A step by step guide on how to use hammer.js: 60 | 61 | * [Download the hammer javascript](https://github.com/EightMedia/hammer.js/zipball/master) or clone the latest version from our github repository: 62 | 63 | ```$ git clone git@github.com:eightmedia/hammer.js.git``` 64 | 65 | * Import jquery and import hammer.js in your project: 66 | 67 | `````` 68 | 69 | * Hammertime! Bind hammer to a container element: 70 | 71 | ```var hammer = new Hammer(document.getElementById("container"));``` 72 | 73 | Now, on every gesture that is performed on the container element, you'll receive a callback object with information on the gesture. 74 | 75 | hammer.ondragstart = function(ev) { }; 76 | hammer.ondrag = function(ev) { }; 77 | hammer.ondragend = function(ev) { }; 78 | hammer.onswipe = function(ev) { }; 79 | 80 | hammer.ontap = function(ev) { }; 81 | hammer.ondoubletap = function(ev) { }; 82 | hammer.onhold = function(ev) { }; 83 | 84 | hammer.ontransformstart = function(ev) { }; 85 | hammer.ontransform = function(ev) { }; 86 | hammer.ontransformend = function(ev) { }; 87 | 88 | hammer.onrelease = function(ev) { }; 89 | 90 | A jQuery plugin is also available and can be found in this repos. 91 | 92 | `````` 93 | 94 | $("#element") 95 | .hammer({ 96 | // options... 97 | }) 98 | .bind("tap", function(ev) { 99 | console.log(ev); 100 | }); 101 | 102 | 103 | [Brian Rinaldi](https://twitter.com/remotesynth) has written a blogpost about Hammer.js, it explains things a bit more and has a nice looking demo. [Read it here.](http://www.remotesynthesis.com/post.cfm/add-gesture-support-to-your-web-application-via-hammer-js) 104 | 105 | 106 | ### The Hammer callback objects: 107 | 108 | All gestures return: 109 | 110 | - originalEvent: The original DOM event. 111 | - position: An object with the x and y position of the gesture (e.g. the position of a tap and the center position of a transform). 112 | - touches: An array of touches, containing an object with the x and the y position for every finger. 113 | 114 | Besides these, the Transform gesture returns: 115 | 116 | - scale: The distance between two fingers since the start of an event as a multiplier of the initial distance. The initial value is 1.0. If less than 1.0 the gesture is pinch close to zoom out. If greater than 1.0 the gesture is pinch open to zoom in. 117 | - rotation: A delta rotation since the start of an event in degrees where clockwise is positive and counter-clockwise is negative. The initial value is 0.0. 118 | 119 | The Drag and Swipe gesture also returns: 120 | 121 | - angle: The angle of the drag movement, where right is 0 degrees, left is -180 degrees, up is -90 degrees and down is 90 degrees. [This picture makes this approach somewhat clearer](http://paperjs.org/tutorials/geometry/vector-geometry/resources/Angles.gif) 122 | - direction: Based on the angle, we return a simplified direction, which can be either up, right, down or left. 123 | - distance: The distance of the drag in pixels. 124 | - distanceX: The distance on the X axis of the drag in pixels. 125 | - distanceY: The distance on the Y axis of the drag in pixels. 126 | 127 | In addition to this the Transform and Drag gestures return start and end events. 128 | 129 | 130 | ### Defaults 131 | | | default | | 132 | |:-----------------------------------|:--------------------------|-----------------| 133 | | prevent_default | false | when true all default browser actions are blocked. For instance if you want to drag vertically, try setting this to true. | 134 | | css_hacks | true | css userSelect, touchCallout, userDrag, tapHighlightColor are added | 135 | | drag | true | | 136 | | drag_vertical | true | | 137 | | drag_horizontal | true | | 138 | | drag_min_distance | 20 | pixels | 139 | | swipe | true | | 140 | | swipe_time | 200 | max ms of movement | 141 | | swipe_min_distance | 20 | minimal movement | 142 | | transform | true | | 143 | | scale_treshold | 0.1 | how much scaling needs to be done before firing the transform event | 144 | | rotation_treshold | 15 | degrees before firing the transform event | 145 | | tap | true | | 146 | | tap_double | true | | 147 | | tap_max_interval | 300 | ms | 148 | | tap_double_distance:| 20 | pixels, distance between taps | 149 | | hold | true | | 150 | | hold_timeout | 500 | ms | 151 | 152 | 153 | ### Compatibility 154 | | | Tap | Double Tap | Hold | Drag | Transform | 155 | |:----------------------------------|:----|:-----------|:-----|:-----|:----------| 156 | | **Windows** | 157 | | Internet Explorer 8 | X | X | X | X | | 158 | | Internet Explorer 9 | X | X | X | X | | 159 | | | 160 | | **OSX** | 161 | | Firefox 11 | X | X | X | X | | 162 | | Opera 11 | X | X | X | X | | 163 | | Chrome 16 | X | X | X | X | | 164 | | Safari 5 | X | X | X | X | | 165 | | | 166 | | **iOS** | 167 | | iPad iOS 5 | X | X | X | X | X | 168 | | iPhone iOS 5 | X | X | X | X | X | 169 | | | 170 | | **Android 4** | 171 | | Default browser | X | X | X | X | X | 172 | | | 173 | | **Android 3** | 174 | | Default browser | X | X | X | X | X | 175 | | | 176 | | **Android 2** | 177 | | Default browser | X | X | X | X | | 178 | | Firefox 10 | X | X | X | X | | 179 | | Opera Mobile 12 | X | X | X | X | | 180 | | Opera Mini 6.5 | X | | | | | 181 | | Opera Mini 7.0 | X | | | | | 182 | | | 183 | | **Others** | 184 | | Windows Phone 7.5 | X | | | | | 185 | | Kindle Fire | X | X | X | X | X | 186 | | Nokia N900 - Firefox 1.1 | X | | | | | 187 | 188 | 189 | On a desktop browser the mouse can be used to simulate touch events with one finger. 190 | On Android 2 (and 3?) doesn't support multi-touch events, so there's no transform callback on these Android versions. 191 | Firefox 1.1 (Nokia N900) and Windows Phone 7.5 doesnt support touch events, and mouse events are badly supported. 192 | 193 | Not all gestures are supported on every device. This matrix shows the support we have tested. This is ofcourse far from extensive. 194 | If you've tested hammer.js on a different device, please let us know. 195 | 196 | 197 | ## Further notes 198 | Created by [Jorik Tangelder](http://twitter.com/jorikdelaporik) and developed further by everyone at [Eight Media](http://www.eight.nl/) in Arnhem, the Netherlands. 199 | 200 | Add your feature suggestions and bug reports on [Github](http://github.com/eightmedia/hammer.js/issues). 201 | 202 | We recommend listening to [this loop](http://soundcloud.com/eightmedia/hammerhammerhammer) while using hammer.js. -------------------------------------------------------------------------------- /hammer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Hammer.JS 3 | * version 0.6.1 4 | * author: Eight Media 5 | * https://github.com/EightMedia/hammer.js 6 | */ 7 | function Hammer(element, options, undefined) 8 | { 9 | var self = this; 10 | 11 | var defaults = { 12 | // prevent the default event or not... might be buggy when false 13 | prevent_default : false, 14 | css_hacks : true, 15 | 16 | swipe : true, 17 | swipe_time : 200, // ms 18 | swipe_min_distance : 20, // pixels 19 | 20 | drag : true, 21 | drag_vertical : true, 22 | drag_horizontal : true, 23 | // minimum distance before the drag event starts 24 | drag_min_distance : 20, // pixels 25 | 26 | // pinch zoom and rotation 27 | transform : true, 28 | scale_treshold : 0.1, 29 | rotation_treshold : 15, // degrees 30 | 31 | tap : true, 32 | tap_double : true, 33 | tap_max_interval : 300, 34 | tap_max_distance : 10, 35 | tap_double_distance: 20, 36 | 37 | hold : true, 38 | hold_timeout : 500 39 | }; 40 | options = mergeObject(defaults, options); 41 | 42 | // some css hacks 43 | (function() { 44 | if(!options.css_hacks) { 45 | return false; 46 | } 47 | 48 | var vendors = ['webkit','moz','ms','o','']; 49 | var css_props = { 50 | "userSelect": "none", 51 | "touchCallout": "none", 52 | "userDrag": "none", 53 | "tapHighlightColor": "rgba(0,0,0,0)" 54 | }; 55 | 56 | var prop = ''; 57 | for(var i = 0; i < vendors.length; i++) { 58 | for(var p in css_props) { 59 | prop = p; 60 | if(vendors[i]) { 61 | prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1); 62 | } 63 | element.style[ prop ] = css_props[p]; 64 | } 65 | } 66 | })(); 67 | 68 | // holds the distance that has been moved 69 | var _distance = 0; 70 | 71 | // holds the exact angle that has been moved 72 | var _angle = 0; 73 | 74 | // holds the diraction that has been moved 75 | var _direction = 0; 76 | 77 | // holds position movement for sliding 78 | var _pos = { }; 79 | 80 | // how many fingers are on the screen 81 | var _fingers = 0; 82 | 83 | var _first = false; 84 | 85 | var _gesture = null; 86 | var _prev_gesture = null; 87 | 88 | var _touch_start_time = null; 89 | var _prev_tap_pos = {x: 0, y: 0}; 90 | var _prev_tap_end_time = null; 91 | 92 | var _hold_timer = null; 93 | 94 | var _offset = {}; 95 | 96 | // keep track of the mouse status 97 | var _mousedown = false; 98 | 99 | var _event_start; 100 | var _event_move; 101 | var _event_end; 102 | 103 | var _has_touch = ('ontouchstart' in window); 104 | 105 | 106 | /** 107 | * option setter/getter 108 | * @param string key 109 | * @param mixed value 110 | * @return mixed value 111 | */ 112 | this.option = function(key, val) { 113 | if(val != undefined) { 114 | options[key] = val; 115 | } 116 | 117 | return options[key]; 118 | }; 119 | 120 | 121 | /** 122 | * angle to direction define 123 | * @param float angle 124 | * @return string direction 125 | */ 126 | this.getDirectionFromAngle = function( angle ) 127 | { 128 | var directions = { 129 | down: angle >= 45 && angle < 135, //90 130 | left: angle >= 135 || angle <= -135, //180 131 | up: angle < -45 && angle > -135, //270 132 | right: angle >= -45 && angle <= 45 //0 133 | }; 134 | 135 | var direction, key; 136 | for(key in directions){ 137 | if(directions[key]){ 138 | direction = key; 139 | break; 140 | } 141 | } 142 | return direction; 143 | }; 144 | 145 | 146 | /** 147 | * count the number of fingers in the event 148 | * when no fingers are detected, one finger is returned (mouse pointer) 149 | * @param event 150 | * @return int fingers 151 | */ 152 | function countFingers( event ) 153 | { 154 | // there is a bug on android (until v4?) that touches is always 1, 155 | // so no multitouch is supported, e.g. no, zoom and rotation... 156 | return event.touches ? event.touches.length : 1; 157 | } 158 | 159 | 160 | /** 161 | * get the x and y positions from the event object 162 | * @param event 163 | * @return array [{ x: int, y: int }] 164 | */ 165 | function getXYfromEvent( event ) 166 | { 167 | event = event || window.event; 168 | 169 | // no touches, use the event pageX and pageY 170 | if(!_has_touch) { 171 | var doc = document, 172 | body = doc.body; 173 | 174 | return [{ 175 | x: event.pageX || event.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && doc.clientLeft || 0 ), 176 | y: event.pageY || event.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && doc.clientTop || 0 ) 177 | }]; 178 | } 179 | // multitouch, return array with positions 180 | else { 181 | var pos = [], src, touches = event.touches.length > 0 ? event.touches : event.changedTouches; 182 | for(var t=0, len=touches.length; t touch_time) && (_distance > options.swipe_min_distance)) { 344 | // calculate the angle 345 | _angle = getAngle(_pos.start[0], _pos.move[0]); 346 | _direction = self.getDirectionFromAngle(_angle); 347 | 348 | _gesture = 'swipe'; 349 | 350 | var position = { x: _pos.move[0].x - _offset.left, 351 | y: _pos.move[0].y - _offset.top }; 352 | 353 | var event_obj = { 354 | originalEvent : event, 355 | position : position, 356 | direction : _direction, 357 | distance : _distance, 358 | distanceX : _distance_x, 359 | distanceY : _distance_y, 360 | angle : _angle 361 | }; 362 | 363 | // normal slide event 364 | triggerEvent("swipe", event_obj); 365 | } 366 | }, 367 | 368 | 369 | // drag gesture 370 | // fired on mousemove 371 | drag : function(event) 372 | { 373 | // get the distance we moved 374 | var _distance_x = _pos.move[0].x - _pos.start[0].x; 375 | var _distance_y = _pos.move[0].y - _pos.start[0].y; 376 | _distance = Math.sqrt(_distance_x * _distance_x + _distance_y * _distance_y); 377 | 378 | // drag 379 | // minimal movement required 380 | if(options.drag && (_distance > options.drag_min_distance) || _gesture == 'drag') { 381 | // calculate the angle 382 | _angle = getAngle(_pos.start[0], _pos.move[0]); 383 | _direction = self.getDirectionFromAngle(_angle); 384 | 385 | // check the movement and stop if we go in the wrong direction 386 | var is_vertical = (_direction == 'up' || _direction == 'down'); 387 | if(((is_vertical && !options.drag_vertical) || (!is_vertical && !options.drag_horizontal)) 388 | && (_distance > options.drag_min_distance)) { 389 | return; 390 | } 391 | 392 | _gesture = 'drag'; 393 | 394 | var position = { x: _pos.move[0].x - _offset.left, 395 | y: _pos.move[0].y - _offset.top }; 396 | 397 | var event_obj = { 398 | originalEvent : event, 399 | position : position, 400 | direction : _direction, 401 | distance : _distance, 402 | distanceX : _distance_x, 403 | distanceY : _distance_y, 404 | angle : _angle 405 | }; 406 | 407 | // on the first time trigger the start event 408 | if(_first) { 409 | triggerEvent("dragstart", event_obj); 410 | 411 | _first = false; 412 | } 413 | 414 | // normal slide event 415 | triggerEvent("drag", event_obj); 416 | 417 | cancelEvent(event); 418 | } 419 | }, 420 | 421 | 422 | // transform gesture 423 | // fired on touchmove 424 | transform : function(event) 425 | { 426 | if(options.transform) { 427 | if(countFingers(event) != 2) { 428 | return false; 429 | } 430 | 431 | var rotation = calculateRotation(_pos.start, _pos.move); 432 | var scale = calculateScale(_pos.start, _pos.move); 433 | 434 | if(_gesture != 'drag' && 435 | (_gesture == 'transform' || Math.abs(1-scale) > options.scale_treshold || Math.abs(rotation) > options.rotation_treshold)) { 436 | _gesture = 'transform'; 437 | 438 | _pos.center = { x: ((_pos.move[0].x + _pos.move[1].x) / 2) - _offset.left, 439 | y: ((_pos.move[0].y + _pos.move[1].y) / 2) - _offset.top }; 440 | 441 | var event_obj = { 442 | originalEvent : event, 443 | position : _pos.center, 444 | scale : scale, 445 | rotation : rotation 446 | }; 447 | 448 | // on the first time trigger the start event 449 | if(_first) { 450 | triggerEvent("transformstart", event_obj); 451 | _first = false; 452 | } 453 | 454 | triggerEvent("transform", event_obj); 455 | 456 | cancelEvent(event); 457 | 458 | return true; 459 | } 460 | } 461 | 462 | return false; 463 | }, 464 | 465 | 466 | // tap and double tap gesture 467 | // fired on touchend 468 | tap : function(event) 469 | { 470 | // compare the kind of gesture by time 471 | var now = new Date().getTime(); 472 | var touch_time = now - _touch_start_time; 473 | 474 | // dont fire when hold is fired 475 | if(options.hold && !(options.hold && options.hold_timeout > touch_time)) { 476 | return; 477 | } 478 | 479 | // when previous event was tap and the tap was max_interval ms ago 480 | var is_double_tap = (function(){ 481 | if (_prev_tap_pos && 482 | options.tap_double && 483 | _prev_gesture == 'tap' && 484 | (_touch_start_time - _prev_tap_end_time) < options.tap_max_interval) 485 | { 486 | var x_distance = Math.abs(_prev_tap_pos[0].x - _pos.start[0].x); 487 | var y_distance = Math.abs(_prev_tap_pos[0].y - _pos.start[0].y); 488 | return (_prev_tap_pos && _pos.start && Math.max(x_distance, y_distance) < options.tap_double_distance); 489 | } 490 | return false; 491 | })(); 492 | 493 | if(is_double_tap) { 494 | _gesture = 'double_tap'; 495 | _prev_tap_end_time = null; 496 | 497 | triggerEvent("doubletap", { 498 | originalEvent : event, 499 | position : _pos.start 500 | }); 501 | cancelEvent(event); 502 | } 503 | 504 | // single tap is single touch 505 | else { 506 | var x_distance = (_pos.move) ? Math.abs(_pos.move[0].x - _pos.start[0].x) : 0; 507 | var y_distance = (_pos.move) ? Math.abs(_pos.move[0].y - _pos.start[0].y) : 0; 508 | _distance = Math.max(x_distance, y_distance); 509 | 510 | if(_distance < options.tap_max_distance) { 511 | _gesture = 'tap'; 512 | _prev_tap_end_time = now; 513 | _prev_tap_pos = _pos.start; 514 | 515 | if(options.tap) { 516 | triggerEvent("tap", { 517 | originalEvent : event, 518 | position : _pos.start 519 | }); 520 | cancelEvent(event); 521 | } 522 | } 523 | } 524 | 525 | } 526 | 527 | }; 528 | 529 | 530 | function handleEvents(event) 531 | { 532 | switch(event.type) 533 | { 534 | case 'mousedown': 535 | case 'touchstart': 536 | _pos.start = getXYfromEvent(event); 537 | _touch_start_time = new Date().getTime(); 538 | _fingers = countFingers(event); 539 | _first = true; 540 | _event_start = event; 541 | 542 | // borrowed from jquery offset https://github.com/jquery/jquery/blob/master/src/offset.js 543 | var box = element.getBoundingClientRect(); 544 | var clientTop = element.clientTop || document.body.clientTop || 0; 545 | var clientLeft = element.clientLeft || document.body.clientLeft || 0; 546 | var scrollTop = window.pageYOffset || element.scrollTop || document.body.scrollTop; 547 | var scrollLeft = window.pageXOffset || element.scrollLeft || document.body.scrollLeft; 548 | 549 | _offset = { 550 | top: box.top + scrollTop - clientTop, 551 | left: box.left + scrollLeft - clientLeft 552 | }; 553 | 554 | _mousedown = true; 555 | 556 | // hold gesture 557 | gestures.hold(event); 558 | 559 | if(options.prevent_default) { 560 | cancelEvent(event); 561 | } 562 | break; 563 | 564 | case 'mousemove': 565 | case 'touchmove': 566 | if(!_mousedown) { 567 | return false; 568 | } 569 | _event_move = event; 570 | _pos.move = getXYfromEvent(event); 571 | 572 | if(!gestures.transform(event)) { 573 | gestures.drag(event); 574 | } 575 | break; 576 | 577 | case 'mouseup': 578 | case 'mouseout': 579 | case 'touchcancel': 580 | case 'touchend': 581 | if(!_mousedown || (_gesture != 'transform' && event.touches && event.touches.length > 0)) { 582 | return false; 583 | } 584 | 585 | _mousedown = false; 586 | _event_end = event; 587 | 588 | var dragging = _gesture == 'drag'; 589 | 590 | // swipe gesture 591 | gestures.swipe(event); 592 | 593 | 594 | // drag gesture 595 | // dragstart is triggered, so dragend is possible 596 | if(dragging) { 597 | triggerEvent("dragend", { 598 | originalEvent : event, 599 | direction : _direction, 600 | distance : _distance, 601 | angle : _angle 602 | }); 603 | } 604 | 605 | // transform 606 | // transformstart is triggered, so transformed is possible 607 | else if(_gesture == 'transform') { 608 | triggerEvent("transformend", { 609 | originalEvent : event, 610 | position : _pos.center, 611 | scale : calculateScale(_pos.start, _pos.move), 612 | rotation : calculateRotation(_pos.start, _pos.move) 613 | }); 614 | } 615 | else { 616 | gestures.tap(_event_start); 617 | } 618 | 619 | _prev_gesture = _gesture; 620 | 621 | // trigger release event 622 | triggerEvent("release", { 623 | originalEvent : event, 624 | gesture : _gesture 625 | }); 626 | 627 | // reset vars 628 | reset(); 629 | break; 630 | } 631 | } 632 | 633 | 634 | // bind events for touch devices 635 | // except for windows phone 7.5, it doesnt support touch events..! 636 | if(_has_touch) { 637 | addEvent(element, "touchstart touchmove touchend touchcancel", handleEvents); 638 | } 639 | // for non-touch 640 | else { 641 | addEvent(element, "mouseup mousedown mousemove", handleEvents); 642 | addEvent(element, "mouseout", function(event) { 643 | if(!isInsideHammer(element, event.relatedTarget)) { 644 | handleEvents(event); 645 | } 646 | }); 647 | } 648 | 649 | 650 | /** 651 | * find if element is (inside) given parent element 652 | * @param object element 653 | * @param object parent 654 | * @return bool inside 655 | */ 656 | function isInsideHammer(parent, child) { 657 | // get related target for IE 658 | if(!child && window.event && window.event.toElement){ 659 | child = window.event.toElement; 660 | } 661 | 662 | if(parent === child){ 663 | return true; 664 | } 665 | 666 | // loop over parentNodes of child until we find hammer element 667 | if(child){ 668 | var node = child.parentNode; 669 | while(node !== null){ 670 | if(node === parent){ 671 | return true; 672 | }; 673 | node = node.parentNode; 674 | } 675 | } 676 | return false; 677 | } 678 | 679 | 680 | /** 681 | * merge 2 objects into a new object 682 | * @param object obj1 683 | * @param object obj2 684 | * @return object merged object 685 | */ 686 | function mergeObject(obj1, obj2) { 687 | var output = {}; 688 | 689 | if(!obj2) { 690 | return obj1; 691 | } 692 | 693 | for (var prop in obj1) { 694 | if (prop in obj2) { 695 | output[prop] = obj2[prop]; 696 | } else { 697 | output[prop] = obj1[prop]; 698 | } 699 | } 700 | return output; 701 | } 702 | 703 | 704 | /** 705 | * check if object is a function 706 | * @param object obj 707 | * @return bool is function 708 | */ 709 | function isFunction( obj ){ 710 | return Object.prototype.toString.call( obj ) == "[object Function]"; 711 | } 712 | 713 | 714 | /** 715 | * attach event 716 | * @param node element 717 | * @param string types 718 | * @param object callback 719 | */ 720 | function addEvent(element, types, callback) { 721 | types = types.split(" "); 722 | for(var t= 0,len=types.length; t