// Copyright 2012 Tart. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview GestureHandler adds the ability to capture gesture events on touch enabled devices.
* It listens to 'touchstart', 'touchmove' and 'touchend' events and generates 'tap' or 'swipe' events with
* inherent heuristics.
*
* Currently, the tap algorithm begins with a touchstart, checks for touchend. Any touchmove greater than 3px
* cancels the tap event, and if a touchend is captured without a touchmove after a touchstart;
* it's registered as a tap, and the GestureHandler dispatches a tap event on the touchend target.
*
* Swipe up, left, right and down gestures are also supported.
*
* Example usage:
*
* goog.events.listen(document.body, tart.events.EventType.TAP, function() {
* console.log('tapped!');
* });
*
*/
goog.provide('tart.events.GestureHandler');
goog.require('goog.dom');
goog.require('goog.events.EventTarget');
goog.require('goog.math.Coordinate');
/**
* Tracks and fires gestures on touch enabled devices.
*
* @constructor
* @param {Element=} opt_el Provided, gesture handler will track gesture events on this element. The default
* value is document.body; but an optional root element is inevitable for iframe's.
*/
tart.events.GestureHandler = function(opt_el) {
this.el = opt_el || document.body;
goog.events.listen(this.el, goog.events.EventType.TOUCHSTART, this.onTouchstart, false, this);
goog.events.listen(this.el, goog.events.EventType.TOUCHMOVE, this.onTouchmove, false, this);
goog.events.listen(this.el, goog.events.EventType.TOUCHEND, this.onTouchend, false, this);
};
goog.addSingletonGetter(tart.events.GestureHandler);
/**
* iOS 6.0(+?) requires the target element to be manually derived.
* @type {?boolean}
*/
tart.events.GestureHandler.prototype.deviceIsIOSWithBadTarget = navigator.userAgent.match(/iPhone/i) &&
(/OS ([6-9]|\d{2})_\d/).test(navigator.userAgent);
tart.events.GestureHandler.prototype.onTouchstart = function(e) {
this.isInMotion = true;
this.canTap = true;
this.canSwipe = true;
var browserEvent = e.getBrowserEvent();
var changedTouch = browserEvent.changedTouches[0];
this.touches = [browserEvent.timeStamp, changedTouch.pageX, changedTouch.pageY];
};
tart.events.GestureHandler.prototype.onTouchmove = function(e) {
var touches = this.touches,
browserEvent = e.getBrowserEvent(),
changedTouch = browserEvent.changedTouches[0];
if (Math.abs(changedTouch.pageX - touches[1]) > 20 ||
Math.abs(changedTouch.pageY - touches[2]) > 20)
this.canTap = false;
if (this.canSwipe) {
touches.push(browserEvent.timeStamp, changedTouch.pageX, changedTouch.pageY);
if (+new Date() > touches[0] + 100) {
this.canSwipe = false;
return;
}
// Filter the touches
var date = browserEvent.timeStamp;
touches = goog.array.filter(touches, function(touch, index, arr) {
var relatedTimeStamp = arr[index - (index % 3)];
return relatedTimeStamp > date - 250;
});
if ((touches.length / 3) > 1) {
var firstTouch = new goog.math.Coordinate(touches[1], touches[2]);
var lastTouch = new goog.math.Coordinate(touches[touches.length - 2],
touches[touches.length - 1]);
// calculate distance. must be min 60px
var distance = goog.math.Coordinate.distance(firstTouch, lastTouch);
if (distance < 60) return;
// calculate angle.
var angle = goog.math.angle(firstTouch.x, firstTouch.y, lastTouch.x, lastTouch.y);
var eventType = tart.events.EventType.SWIPE_RIGHT;
if (angle > 45 && angle < 135) {
eventType = tart.events.EventType.SWIPE_DOWN;
}
else if (angle > 135 && angle < 225) {
eventType = tart.events.EventType.SWIPE_LEFT;
}
else if (angle > 225 && angle < 315) {
eventType = tart.events.EventType.SWIPE_UP;
}
var swipe = document.createEvent("Event");
swipe.initEvent(eventType, true, true);
e.target.dispatchEvent(swipe);
this.canSwipe = false;
}
}
};
tart.events.GestureHandler.prototype.onTouchend = function(e) {
this.isInMotion = false;
if (this.canTap) {
var touches = this.touches,
browserEvent = e.getBrowserEvent(),
changedTouch = browserEvent.changedTouches[0];
if (Math.abs(changedTouch.pageX - touches[1]) > 20 ||
Math.abs(changedTouch.pageY - touches[2]) > 20) {
this.canTap = false;
return;
}
var tap = document.createEvent("Event");
tap.initEvent(tart.events.EventType.TAP, true, true);
// Target element fix for iOS6+
var targetElement = e.target;
if (this.deviceIsIOSWithBadTarget)
targetElement = document.elementFromPoint(changedTouch.pageX - window.pageXOffset,
changedTouch.pageY - window.pageYOffset);
targetElement.dispatchEvent(tap);
}
};