From 28ce177e178f906fe61c424a83fb41fa3110dbb8 Mon Sep 17 00:00:00 2001 From: Jeena Date: Thu, 22 May 2014 20:11:54 +0200 Subject: [PATCH] replaced jetser with hammertime which hopefully fixes #32 --- index.html | 4 +- js/App.js | 12 +- js/hammer.js | 2163 ++++++++++++++++++++++++++++++++++++++++++++++++++ js/jester.js | 599 -------------- 4 files changed, 2172 insertions(+), 606 deletions(-) create mode 100644 js/hammer.js delete mode 100644 js/jester.js diff --git a/index.html b/index.html index feaa4f4..0a79f6a 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + FeedMonkey @@ -15,7 +15,7 @@ - + diff --git a/js/App.js b/js/App.js index c874b84..abcd735 100644 --- a/js/App.js +++ b/js/App.js @@ -84,11 +84,13 @@ App.prototype.after_login = function(backend) { } // set up swiping - jester($("#full")).flick(function(touches, direction) { - if(direction == "left") _this.showNext(); - else _this.showPrevious(); - }); - + var options = { + dragLockToAxis: true, + dragBlockHorizontal: true + }; + var hammertime = new Hammer($("#full"), options); + hammertime.on("swipeleft", function(ev){ _this.showNext() }); + hammertime.on("swiperight", function(ev){ _this.showPrevious(); }); this.changeToPage("#list"); diff --git a/js/hammer.js b/js/hammer.js new file mode 100644 index 0000000..e03acdc --- /dev/null +++ b/js/hammer.js @@ -0,0 +1,2163 @@ +/*! Hammer.JS - v1.1.3 - 2014-05-22 + * http://eightmedia.github.io/hammer.js + * + * Copyright (c) 2014 Jorik Tangelder ; + * Licensed under the MIT license */ + +(function(window, undefined) { + 'use strict'; + +/** + * @main + * @module hammer + * + * @class Hammer + * @static + */ + +/** + * Hammer, use this to create instances + * ```` + * var hammertime = new Hammer(myElement); + * ```` + * + * @method Hammer + * @param {HTMLElement} element + * @param {Object} [options={}] + * @return {Hammer.Instance} + */ +var Hammer = function Hammer(element, options) { + return new Hammer.Instance(element, options || {}); +}; + +/** + * version, as defined in package.json + * the value will be set at each build + * @property VERSION + * @final + * @type {String} + */ +Hammer.VERSION = '1.1.3'; + +/** + * default settings. + * more settings are defined per gesture at `/gestures`. Each gesture can be disabled/enabled + * by setting it's name (like `swipe`) to false. + * You can set the defaults for all instances by changing this object before creating an instance. + * @example + * ```` + * Hammer.defaults.drag = false; + * Hammer.defaults.behavior.touchAction = 'pan-y'; + * delete Hammer.defaults.behavior.userSelect; + * ```` + * @property defaults + * @type {Object} + */ +Hammer.defaults = { + /** + * this setting object adds styles and attributes to the element to prevent the browser from doing + * its native behavior. The css properties are auto prefixed for the browsers when needed. + * @property defaults.behavior + * @type {Object} + */ + behavior: { + /** + * Disables text selection to improve the dragging gesture. When the value is `none` it also sets + * `onselectstart=false` for IE on the element. Mainly for desktop browsers. + * @property defaults.behavior.userSelect + * @type {String} + * @default 'none' + */ + userSelect: 'none', + + /** + * Specifies whether and how a given region can be manipulated by the user (for instance, by panning or zooming). + * Used by Chrome 35> and IE10>. By default this makes the element blocking any touch event. + * @property defaults.behavior.touchAction + * @type {String} + * @default: 'pan-y' + */ + touchAction: 'pan-y', + + /** + * Disables the default callout shown when you touch and hold a touch target. + * On iOS, when you touch and hold a touch target such as a link, Safari displays + * a callout containing information about the link. This property allows you to disable that callout. + * @property defaults.behavior.touchCallout + * @type {String} + * @default 'none' + */ + touchCallout: 'none', + + /** + * Specifies whether zooming is enabled. Used by IE10> + * @property defaults.behavior.contentZooming + * @type {String} + * @default 'none' + */ + contentZooming: 'none', + + /** + * Specifies that an entire element should be draggable instead of its contents. + * Mainly for desktop browsers. + * @property defaults.behavior.userDrag + * @type {String} + * @default 'none' + */ + userDrag: 'none', + + /** + * Overrides the highlight color shown when the user taps a link or a JavaScript + * clickable element in Safari on iPhone. This property obeys the alpha value, if specified. + * + * If you don't specify an alpha value, Safari on iPhone applies a default alpha value + * to the color. To disable tap highlighting, set the alpha value to 0 (invisible). + * If you set the alpha value to 1.0 (opaque), the element is not visible when tapped. + * @property defaults.behavior.tapHighlightColor + * @type {String} + * @default 'rgba(0,0,0,0)' + */ + tapHighlightColor: 'rgba(0,0,0,0)' + } +}; + +/** + * hammer document where the base events are added at + * @property DOCUMENT + * @type {HTMLElement} + * @default window.document + */ +Hammer.DOCUMENT = document; + +/** + * detect support for pointer events + * @property HAS_POINTEREVENTS + * @type {Boolean} + */ +Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled; + +/** + * detect support for touch events + * @property HAS_TOUCHEVENTS + * @type {Boolean} + */ +Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window); + +/** + * detect mobile browsers + * @property IS_MOBILE + * @type {Boolean} + */ +Hammer.IS_MOBILE = /mobile|tablet|ip(ad|hone|od)|android|silk/i.test(navigator.userAgent); + +/** + * detect if we want to support mouseevents at all + * @property NO_MOUSEEVENTS + * @type {Boolean} + */ +Hammer.NO_MOUSEEVENTS = (Hammer.HAS_TOUCHEVENTS && Hammer.IS_MOBILE) || Hammer.HAS_POINTEREVENTS; + +/** + * interval in which Hammer recalculates current velocity/direction/angle in ms + * @property CALCULATE_INTERVAL + * @type {Number} + * @default 25 + */ +Hammer.CALCULATE_INTERVAL = 25; + +/** + * eventtypes per touchevent (start, move, end) are filled by `Event.determineEventTypes` on `setup` + * the object contains the DOM event names per type (`EVENT_START`, `EVENT_MOVE`, `EVENT_END`) + * @property EVENT_TYPES + * @private + * @writeOnce + * @type {Object} + */ +var EVENT_TYPES = {}; + +/** + * direction strings, for safe comparisons + * @property DIRECTION_DOWN|LEFT|UP|RIGHT + * @final + * @type {String} + * @default 'down' 'left' 'up' 'right' + */ +var DIRECTION_DOWN = Hammer.DIRECTION_DOWN = 'down'; +var DIRECTION_LEFT = Hammer.DIRECTION_LEFT = 'left'; +var DIRECTION_UP = Hammer.DIRECTION_UP = 'up'; +var DIRECTION_RIGHT = Hammer.DIRECTION_RIGHT = 'right'; + +/** + * pointertype strings, for safe comparisons + * @property POINTER_MOUSE|TOUCH|PEN + * @final + * @type {String} + * @default 'mouse' 'touch' 'pen' + */ +var POINTER_MOUSE = Hammer.POINTER_MOUSE = 'mouse'; +var POINTER_TOUCH = Hammer.POINTER_TOUCH = 'touch'; +var POINTER_PEN = Hammer.POINTER_PEN = 'pen'; + +/** + * eventtypes + * @property EVENT_START|MOVE|END|RELEASE|TOUCH + * @final + * @type {String} + * @default 'start' 'change' 'move' 'end' 'release' 'touch' + */ +var EVENT_START = Hammer.EVENT_START = 'start'; +var EVENT_MOVE = Hammer.EVENT_MOVE = 'move'; +var EVENT_END = Hammer.EVENT_END = 'end'; +var EVENT_RELEASE = Hammer.EVENT_RELEASE = 'release'; +var EVENT_TOUCH = Hammer.EVENT_TOUCH = 'touch'; + +/** + * if the window events are set... + * @property READY + * @writeOnce + * @type {Boolean} + * @default false + */ +Hammer.READY = false; + +/** + * plugins namespace + * @property plugins + * @type {Object} + */ +Hammer.plugins = Hammer.plugins || {}; + +/** + * gestures namespace + * see `/gestures` for the definitions + * @property gestures + * @type {Object} + */ +Hammer.gestures = Hammer.gestures || {}; + +/** + * setup events to detect gestures on the document + * this function is called when creating an new instance + * @private + */ +function setup() { + if(Hammer.READY) { + return; + } + + // find what eventtypes we add listeners to + Event.determineEventTypes(); + + // Register all gestures inside Hammer.gestures + Utils.each(Hammer.gestures, function(gesture) { + Detection.register(gesture); + }); + + // Add touch events on the document + Event.onTouch(Hammer.DOCUMENT, EVENT_MOVE, Detection.detect); + Event.onTouch(Hammer.DOCUMENT, EVENT_END, Detection.detect); + + // Hammer is ready...! + Hammer.READY = true; +} + +/** + * @module hammer + * + * @class Utils + * @static + */ +var Utils = Hammer.utils = { + /** + * extend method, could also be used for cloning when `dest` is an empty object. + * changes the dest object + * @method extend + * @param {Object} dest + * @param {Object} src + * @param {Boolean} [merge=false] do a merge + * @return {Object} dest + */ + extend: function extend(dest, src, merge) { + for(var key in src) { + if(!src.hasOwnProperty(key) || (dest[key] !== undefined && merge)) { + continue; + } + dest[key] = src[key]; + } + return dest; + }, + + /** + * simple addEventListener wrapper + * @method on + * @param {HTMLElement} element + * @param {String} type + * @param {Function} handler + */ + on: function on(element, type, handler) { + element.addEventListener(type, handler, false); + }, + + /** + * simple removeEventListener wrapper + * @method off + * @param {HTMLElement} element + * @param {String} type + * @param {Function} handler + */ + off: function off(element, type, handler) { + element.removeEventListener(type, handler, false); + }, + + /** + * forEach over arrays and objects + * @method each + * @param {Object|Array} obj + * @param {Function} iterator + * @param {any} iterator.item + * @param {Number} iterator.index + * @param {Object|Array} iterator.obj the source object + * @param {Object} context value to use as `this` in the iterator + */ + each: function each(obj, iterator, context) { + var i, len; + + // native forEach on arrays + if('forEach' in obj) { + obj.forEach(iterator, context); + // arrays + } else if(obj.length !== undefined) { + for(i = 0, len = obj.length; i < len; i++) { + if(iterator.call(context, obj[i], i, obj) === false) { + return; + } + } + // objects + } else { + for(i in obj) { + if(obj.hasOwnProperty(i) && + iterator.call(context, obj[i], i, obj) === false) { + return; + } + } + } + }, + + /** + * find if a string contains the string using indexOf + * @method inStr + * @param {String} src + * @param {String} find + * @return {Boolean} found + */ + inStr: function inStr(src, find) { + return src.indexOf(find) > -1; + }, + + /** + * find if a array contains the object using indexOf or a simple polyfill + * @method inArray + * @param {String} src + * @param {String} find + * @return {Boolean|Number} false when not found, or the index + */ + inArray: function inArray(src, find) { + if(src.indexOf) { + var index = src.indexOf(find); + return (index === -1) ? false : index; + } else { + for(var i = 0, len = src.length; i < len; i++) { + if(src[i] === find) { + return i; + } + } + return false; + } + }, + + /** + * convert an array-like object (`arguments`, `touchlist`) to an array + * @method toArray + * @param {Object} obj + * @return {Array} + */ + toArray: function toArray(obj) { + return Array.prototype.slice.call(obj, 0); + }, + + /** + * find if a node is in the given parent + * @method hasParent + * @param {HTMLElement} node + * @param {HTMLElement} parent + * @return {Boolean} found + */ + hasParent: function hasParent(node, parent) { + while(node) { + if(node == parent) { + return true; + } + node = node.parentNode; + } + return false; + }, + + /** + * get the center of all the touches + * @method getCenter + * @param {Array} touches + * @return {Object} center contains `pageX`, `pageY`, `clientX` and `clientY` properties + */ + getCenter: function getCenter(touches) { + var pageX = [], + pageY = [], + clientX = [], + clientY = [], + min = Math.min, + max = Math.max; + + // no need to loop when only one touch + if(touches.length === 1) { + return { + pageX: touches[0].pageX, + pageY: touches[0].pageY, + clientX: touches[0].clientX, + clientY: touches[0].clientY + }; + } + + Utils.each(touches, function(touch) { + pageX.push(touch.pageX); + pageY.push(touch.pageY); + clientX.push(touch.clientX); + clientY.push(touch.clientY); + }); + + return { + pageX: (min.apply(Math, pageX) + max.apply(Math, pageX)) / 2, + pageY: (min.apply(Math, pageY) + max.apply(Math, pageY)) / 2, + clientX: (min.apply(Math, clientX) + max.apply(Math, clientX)) / 2, + clientY: (min.apply(Math, clientY) + max.apply(Math, clientY)) / 2 + }; + }, + + /** + * calculate the velocity between two points. unit is in px per ms. + * @method getVelocity + * @param {Number} deltaTime + * @param {Number} deltaX + * @param {Number} deltaY + * @return {Object} velocity `x` and `y` + */ + getVelocity: function getVelocity(deltaTime, deltaX, deltaY) { + return { + x: Math.abs(deltaX / deltaTime) || 0, + y: Math.abs(deltaY / deltaTime) || 0 + }; + }, + + /** + * calculate the angle between two coordinates + * @method getAngle + * @param {Touch} touch1 + * @param {Touch} touch2 + * @return {Number} angle + */ + getAngle: function getAngle(touch1, touch2) { + var x = touch2.clientX - touch1.clientX, + y = touch2.clientY - touch1.clientY; + + return Math.atan2(y, x) * 180 / Math.PI; + }, + + /** + * do a small comparision to get the direction between two touches. + * @method getDirection + * @param {Touch} touch1 + * @param {Touch} touch2 + * @return {String} direction matches `DIRECTION_LEFT|RIGHT|UP|DOWN` + */ + getDirection: function getDirection(touch1, touch2) { + var x = Math.abs(touch1.clientX - touch2.clientX), + y = Math.abs(touch1.clientY - touch2.clientY); + + if(x >= y) { + return touch1.clientX - touch2.clientX > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT; + } + return touch1.clientY - touch2.clientY > 0 ? DIRECTION_UP : DIRECTION_DOWN; + }, + + /** + * calculate the distance between two touches + * @method getDistance + * @param {Touch}touch1 + * @param {Touch} touch2 + * @return {Number} distance + */ + getDistance: function getDistance(touch1, touch2) { + var x = touch2.clientX - touch1.clientX, + y = touch2.clientY - touch1.clientY; + + return Math.sqrt((x * x) + (y * y)); + }, + + /** + * calculate the scale factor between two touchLists + * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out + * @method getScale + * @param {Array} start array of touches + * @param {Array} end array of touches + * @return {Number} scale + */ + getScale: function getScale(start, end) { + // need two fingers... + if(start.length >= 2 && end.length >= 2) { + return this.getDistance(end[0], end[1]) / this.getDistance(start[0], start[1]); + } + return 1; + }, + + /** + * calculate the rotation degrees between two touchLists + * @method getRotation + * @param {Array} start array of touches + * @param {Array} end array of touches + * @return {Number} rotation + */ + getRotation: function getRotation(start, end) { + // need two fingers + if(start.length >= 2 && end.length >= 2) { + return this.getAngle(end[1], end[0]) - this.getAngle(start[1], start[0]); + } + return 0; + }, + + /** + * find out if the direction is vertical * + * @method isVertical + * @param {String} direction matches `DIRECTION_UP|DOWN` + * @return {Boolean} is_vertical + */ + isVertical: function isVertical(direction) { + return direction == DIRECTION_UP || direction == DIRECTION_DOWN; + }, + + /** + * set css properties with their prefixes + * @param {HTMLElement} element + * @param {String} prop + * @param {String} value + * @param {Boolean} [toggle=true] + * @return {Boolean} + */ + setPrefixedCss: function setPrefixedCss(element, prop, value, toggle) { + var prefixes = ['', 'Webkit', 'Moz', 'O', 'ms']; + prop = Utils.toCamelCase(prop); + + for(var i = 0; i < prefixes.length; i++) { + var p = prop; + // prefixes + if(prefixes[i]) { + p = prefixes[i] + p.slice(0, 1).toUpperCase() + p.slice(1); + } + + // test the style + if(p in element.style) { + element.style[p] = (toggle == null || toggle) && value || ''; + break; + } + } + }, + + /** + * toggle browser default behavior by setting css properties. + * `userSelect='none'` also sets `element.onselectstart` to false + * `userDrag='none'` also sets `element.ondragstart` to false + * + * @method toggleBehavior + * @param {HtmlElement} element + * @param {Object} props + * @param {Boolean} [toggle=true] + */ + toggleBehavior: function toggleBehavior(element, props, toggle) { + if(!props || !element || !element.style) { + return; + } + + // set the css properties + Utils.each(props, function(value, prop) { + Utils.setPrefixedCss(element, prop, value, toggle); + }); + + var falseFn = toggle && function() { + return false; + }; + + // also the disable onselectstart + if(props.userSelect == 'none') { + element.onselectstart = falseFn; + } + // and disable ondragstart + if(props.userDrag == 'none') { + element.ondragstart = falseFn; + } + }, + + /** + * convert a string with underscores to camelCase + * so prevent_default becomes preventDefault + * @param {String} str + * @return {String} camelCaseStr + */ + toCamelCase: function toCamelCase(str) { + return str.replace(/[_-]([a-z])/g, function(s) { + return s[1].toUpperCase(); + }); + } +}; + + +/** + * @module hammer + */ +/** + * @class Event + * @static + */ +var Event = Hammer.event = { + /** + * when touch events have been fired, this is true + * this is used to stop mouse events + * @property prevent_mouseevents + * @private + * @type {Boolean} + */ + preventMouseEvents: false, + + /** + * if EVENT_START has been fired + * @property started + * @private + * @type {Boolean} + */ + started: false, + + /** + * when the mouse is hold down, this is true + * @property should_detect + * @private + * @type {Boolean} + */ + shouldDetect: false, + + /** + * simple event binder with a hook and support for multiple types + * @method on + * @param {HTMLElement} element + * @param {String} type + * @param {Function} handler + * @param {Function} [hook] + * @param {Object} hook.type + */ + on: function on(element, type, handler, hook) { + var types = type.split(' '); + Utils.each(types, function(type) { + Utils.on(element, type, handler); + hook && hook(type); + }); + }, + + /** + * simple event unbinder with a hook and support for multiple types + * @method off + * @param {HTMLElement} element + * @param {String} type + * @param {Function} handler + * @param {Function} [hook] + * @param {Object} hook.type + */ + off: function off(element, type, handler, hook) { + var types = type.split(' '); + Utils.each(types, function(type) { + Utils.off(element, type, handler); + hook && hook(type); + }); + }, + + /** + * the core touch event handler. + * this finds out if we should to detect gestures + * @method onTouch + * @param {HTMLElement} element + * @param {String} eventType matches `EVENT_START|MOVE|END` + * @param {Function} handler + * @return onTouchHandler {Function} the core event handler + */ + onTouch: function onTouch(element, eventType, handler) { + var self = this; + + var onTouchHandler = function onTouchHandler(ev) { + var srcType = ev.type.toLowerCase(), + isPointer = Hammer.HAS_POINTEREVENTS, + isMouse = Utils.inStr(srcType, 'mouse'), + triggerType; + + // if we are in a mouseevent, but there has been a touchevent triggered in this session + // we want to do nothing. simply break out of the event. + if(isMouse && self.preventMouseEvents) { + return; + + // mousebutton must be down + } else if(isMouse && eventType == EVENT_START && ev.button === 0) { + self.preventMouseEvents = false; + self.shouldDetect = true; + } else if(isPointer && eventType == EVENT_START) { + self.shouldDetect = (ev.buttons === 1 || PointerEvent.matchType(POINTER_TOUCH, ev)); + // just a valid start event, but no mouse + } else if(!isMouse && eventType == EVENT_START) { + self.preventMouseEvents = true; + self.shouldDetect = true; + } + + // update the pointer event before entering the detection + if(isPointer && eventType != EVENT_END) { + PointerEvent.updatePointer(eventType, ev); + } + + // we are in a touch/down state, so allowed detection of gestures + if(self.shouldDetect) { + triggerType = self.doDetect.call(self, ev, eventType, element, handler); + } + + // ...and we are done with the detection + // so reset everything to start each detection totally fresh + if(triggerType == EVENT_END) { + self.preventMouseEvents = false; + self.shouldDetect = false; + PointerEvent.reset(); + // update the pointerevent object after the detection + } + + if(isPointer && eventType == EVENT_END) { + PointerEvent.updatePointer(eventType, ev); + } + }; + + this.on(element, EVENT_TYPES[eventType], onTouchHandler); + return onTouchHandler; + }, + + /** + * the core detection method + * this finds out what hammer-touch-events to trigger + * @method doDetect + * @param {Object} ev + * @param {String} eventType matches `EVENT_START|MOVE|END` + * @param {HTMLElement} element + * @param {Function} handler + * @return {String} triggerType matches `EVENT_START|MOVE|END` + */ + doDetect: function doDetect(ev, eventType, element, handler) { + var touchList = this.getTouchList(ev, eventType); + var touchListLength = touchList.length; + var triggerType = eventType; + var triggerChange = touchList.trigger; // used by fakeMultitouch plugin + var changedLength = touchListLength; + + // at each touchstart-like event we want also want to trigger a TOUCH event... + if(eventType == EVENT_START) { + triggerChange = EVENT_TOUCH; + // ...the same for a touchend-like event + } else if(eventType == EVENT_END) { + triggerChange = EVENT_RELEASE; + + // keep track of how many touches have been removed + changedLength = touchList.length - ((ev.changedTouches) ? ev.changedTouches.length : 1); + } + + // after there are still touches on the screen, + // we just want to trigger a MOVE event. so change the START or END to a MOVE + // but only after detection has been started, the first time we actualy want a START + if(changedLength > 0 && this.started) { + triggerType = EVENT_MOVE; + } + + // detection has been started, we keep track of this, see above + this.started = true; + + // generate some event data, some basic information + var evData = this.collectEventData(element, triggerType, touchList, ev); + + // trigger the triggerType event before the change (TOUCH, RELEASE) events + // but the END event should be at last + if(eventType != EVENT_END) { + handler.call(Detection, evData); + } + + // trigger a change (TOUCH, RELEASE) event, this means the length of the touches changed + if(triggerChange) { + evData.changedLength = changedLength; + evData.eventType = triggerChange; + + handler.call(Detection, evData); + + evData.eventType = triggerType; + delete evData.changedLength; + } + + // trigger the END event + if(triggerType == EVENT_END) { + handler.call(Detection, evData); + + // ...and we are done with the detection + // so reset everything to start each detection totally fresh + this.started = false; + } + + return triggerType; + }, + + /** + * we have different events for each device/browser + * determine what we need and set them in the EVENT_TYPES constant + * the `onTouch` method is bind to these properties. + * @method determineEventTypes + * @return {Object} events + */ + determineEventTypes: function determineEventTypes() { + var types; + if(Hammer.HAS_POINTEREVENTS) { + if(window.PointerEvent) { + types = [ + 'pointerdown', + 'pointermove', + 'pointerup pointercancel lostpointercapture' + ]; + } else { + types = [ + 'MSPointerDown', + 'MSPointerMove', + 'MSPointerUp MSPointerCancel MSLostPointerCapture' + ]; + } + } else if(Hammer.NO_MOUSEEVENTS) { + types = [ + 'touchstart', + 'touchmove', + 'touchend touchcancel' + ]; + } else { + types = [ + 'touchstart mousedown', + 'touchmove mousemove', + 'touchend touchcancel mouseup' + ]; + } + + EVENT_TYPES[EVENT_START] = types[0]; + EVENT_TYPES[EVENT_MOVE] = types[1]; + EVENT_TYPES[EVENT_END] = types[2]; + return EVENT_TYPES; + }, + + /** + * create touchList depending on the event + * @method getTouchList + * @param {Object} ev + * @param {String} eventType + * @return {Array} touches + */ + getTouchList: function getTouchList(ev, eventType) { + // get the fake pointerEvent touchlist + if(Hammer.HAS_POINTEREVENTS) { + return PointerEvent.getTouchList(); + } + + // get the touchlist + if(ev.touches) { + if(eventType == EVENT_MOVE) { + return ev.touches; + } + + var identifiers = []; + var concat = [].concat(Utils.toArray(ev.touches), Utils.toArray(ev.changedTouches)); + var touchList = []; + + Utils.each(concat, function(touch) { + if(Utils.inArray(identifiers, touch.identifier) === false) { + touchList.push(touch); + } + identifiers.push(touch.identifier); + }); + + return touchList; + } + + // make fake touchList from mouse position + ev.identifier = 1; + return [ev]; + }, + + /** + * collect basic event data + * @method collectEventData + * @param {HTMLElement} element + * @param {String} eventType matches `EVENT_START|MOVE|END` + * @param {Array} touches + * @param {Object} ev + * @return {Object} ev + */ + collectEventData: function collectEventData(element, eventType, touches, ev) { + // find out pointerType + var pointerType = POINTER_TOUCH; + if(Utils.inStr(ev.type, 'mouse') || PointerEvent.matchType(POINTER_MOUSE, ev)) { + pointerType = POINTER_MOUSE; + } else if(PointerEvent.matchType(POINTER_PEN, ev)) { + pointerType = POINTER_PEN; + } + + return { + center: Utils.getCenter(touches), + timeStamp: Date.now(), + target: ev.target, + touches: touches, + eventType: eventType, + pointerType: pointerType, + srcEvent: ev, + + /** + * prevent the browser default actions + * mostly used to disable scrolling of the browser + */ + preventDefault: function() { + var srcEvent = this.srcEvent; + srcEvent.preventManipulation && srcEvent.preventManipulation(); + srcEvent.preventDefault && srcEvent.preventDefault(); + }, + + /** + * stop bubbling the event up to its parents + */ + stopPropagation: function() { + this.srcEvent.stopPropagation(); + }, + + /** + * immediately stop gesture detection + * might be useful after a swipe was detected + * @return {*} + */ + stopDetect: function() { + return Detection.stopDetect(); + } + }; + } +}; + + +/** + * @module hammer + * + * @class PointerEvent + * @static + */ +var PointerEvent = Hammer.PointerEvent = { + /** + * holds all pointers, by `identifier` + * @property pointers + * @type {Object} + */ + pointers: {}, + + /** + * get the pointers as an array + * @method getTouchList + * @return {Array} touchlist + */ + getTouchList: function getTouchList() { + var touchlist = []; + // we can use forEach since pointerEvents only is in IE10 + Utils.each(this.pointers, function(pointer) { + touchlist.push(pointer); + }); + + return touchlist; + }, + + /** + * update the position of a pointer + * @method updatePointer + * @param {String} eventType matches `EVENT_START|MOVE|END` + * @param {Object} pointerEvent + */ + updatePointer: function updatePointer(eventType, pointerEvent) { + if(eventType == EVENT_END) { + delete this.pointers[pointerEvent.pointerId]; + } else { + pointerEvent.identifier = pointerEvent.pointerId; + this.pointers[pointerEvent.pointerId] = pointerEvent; + } + }, + + /** + * check if ev matches pointertype + * @method matchType + * @param {String} pointerType matches `POINTER_MOUSE|TOUCH|PEN` + * @param {PointerEvent} ev + */ + matchType: function matchType(pointerType, ev) { + if(!ev.pointerType) { + return false; + } + + var pt = ev.pointerType, + types = {}; + + types[POINTER_MOUSE] = (pt === (ev.MSPOINTER_TYPE_MOUSE || POINTER_MOUSE)); + types[POINTER_TOUCH] = (pt === (ev.MSPOINTER_TYPE_TOUCH || POINTER_TOUCH)); + types[POINTER_PEN] = (pt === (ev.MSPOINTER_TYPE_PEN || POINTER_PEN)); + return types[pointerType]; + }, + + /** + * reset the stored pointers + * @method reset + */ + reset: function resetList() { + this.pointers = {}; + } +}; + + +/** + * @module hammer + * + * @class Detection + * @static + */ +var Detection = Hammer.detection = { + // contains all registred Hammer.gestures in the correct order + gestures: [], + + // data of the current Hammer.gesture detection session + current: null, + + // the previous Hammer.gesture session data + // is a full clone of the previous gesture.current object + previous: null, + + // when this becomes true, no gestures are fired + stopped: false, + + /** + * start Hammer.gesture detection + * @method startDetect + * @param {Hammer.Instance} inst + * @param {Object} eventData + */ + startDetect: function startDetect(inst, eventData) { + // already busy with a Hammer.gesture detection on an element + if(this.current) { + return; + } + + this.stopped = false; + + // holds current session + this.current = { + inst: inst, // reference to HammerInstance we're working for + startEvent: Utils.extend({}, eventData), // start eventData for distances, timing etc + lastEvent: false, // last eventData + lastCalcEvent: false, // last eventData for calculations. + futureCalcEvent: false, // last eventData for calculations. + lastCalcData: {}, // last lastCalcData + name: '' // current gesture we're in/detected, can be 'tap', 'hold' etc + }; + + this.detect(eventData); + }, + + /** + * Hammer.gesture detection + * @method detect + * @param {Object} eventData + * @return {any} + */ + detect: function detect(eventData) { + if(!this.current || this.stopped) { + return; + } + + // extend event data with calculations about scale, distance etc + eventData = this.extendEventData(eventData); + + // hammer instance and instance options + var inst = this.current.inst, + instOptions = inst.options; + + // call Hammer.gesture handlers + Utils.each(this.gestures, function triggerGesture(gesture) { + // only when the instance options have enabled this gesture + if(!this.stopped && inst.enabled && instOptions[gesture.name]) { + gesture.handler.call(gesture, eventData, inst); + } + }, this); + + // store as previous event event + if(this.current) { + this.current.lastEvent = eventData; + } + + if(eventData.eventType == EVENT_END) { + this.stopDetect(); + } + + return eventData; + }, + + /** + * clear the Hammer.gesture vars + * this is called on endDetect, but can also be used when a final Hammer.gesture has been detected + * to stop other Hammer.gestures from being fired + * @method stopDetect + */ + stopDetect: function stopDetect() { + // clone current data to the store as the previous gesture + // used for the double tap gesture, since this is an other gesture detect session + this.previous = Utils.extend({}, this.current); + + // reset the current + this.current = null; + this.stopped = true; + }, + + /** + * calculate velocity, angle and direction + * @method getVelocityData + * @param {Object} ev + * @param {Object} center + * @param {Number} deltaTime + * @param {Number} deltaX + * @param {Number} deltaY + */ + getCalculatedData: function getCalculatedData(ev, center, deltaTime, deltaX, deltaY) { + var cur = this.current, + recalc = false, + calcEv = cur.lastCalcEvent, + calcData = cur.lastCalcData; + + if(calcEv && ev.timeStamp - calcEv.timeStamp > Hammer.CALCULATE_INTERVAL) { + center = calcEv.center; + deltaTime = ev.timeStamp - calcEv.timeStamp; + deltaX = ev.center.clientX - calcEv.center.clientX; + deltaY = ev.center.clientY - calcEv.center.clientY; + recalc = true; + } + + if(ev.eventType == EVENT_TOUCH || ev.eventType == EVENT_RELEASE) { + cur.futureCalcEvent = ev; + } + + if(!cur.lastCalcEvent || recalc) { + calcData.velocity = Utils.getVelocity(deltaTime, deltaX, deltaY); + calcData.angle = Utils.getAngle(center, ev.center); + calcData.direction = Utils.getDirection(center, ev.center); + + cur.lastCalcEvent = cur.futureCalcEvent || ev; + cur.futureCalcEvent = ev; + } + + ev.velocityX = calcData.velocity.x; + ev.velocityY = calcData.velocity.y; + ev.interimAngle = calcData.angle; + ev.interimDirection = calcData.direction; + }, + + /** + * extend eventData for Hammer.gestures + * @method extendEventData + * @param {Object} ev + * @return {Object} ev + */ + extendEventData: function extendEventData(ev) { + var cur = this.current, + startEv = cur.startEvent, + lastEv = cur.lastEvent || startEv; + + // update the start touchlist to calculate the scale/rotation + if(ev.eventType == EVENT_TOUCH || ev.eventType == EVENT_RELEASE) { + startEv.touches = []; + Utils.each(ev.touches, function(touch) { + startEv.touches.push({ + clientX: touch.clientX, + clientY: touch.clientY + }); + }); + } + + var deltaTime = ev.timeStamp - startEv.timeStamp, + deltaX = ev.center.clientX - startEv.center.clientX, + deltaY = ev.center.clientY - startEv.center.clientY; + + this.getCalculatedData(ev, lastEv.center, deltaTime, deltaX, deltaY); + + Utils.extend(ev, { + startEvent: startEv, + + deltaTime: deltaTime, + deltaX: deltaX, + deltaY: deltaY, + + distance: Utils.getDistance(startEv.center, ev.center), + angle: Utils.getAngle(startEv.center, ev.center), + direction: Utils.getDirection(startEv.center, ev.center), + scale: Utils.getScale(startEv.touches, ev.touches), + rotation: Utils.getRotation(startEv.touches, ev.touches) + }); + + return ev; + }, + + /** + * register new gesture + * @method register + * @param {Object} gesture object, see `gestures/` for documentation + * @return {Array} gestures + */ + register: function register(gesture) { + // add an enable gesture options if there is no given + var options = gesture.defaults || {}; + if(options[gesture.name] === undefined) { + options[gesture.name] = true; + } + + // extend Hammer default options with the Hammer.gesture options + Utils.extend(Hammer.defaults, options, true); + + // set its index + gesture.index = gesture.index || 1000; + + // add Hammer.gesture to the list + this.gestures.push(gesture); + + // sort the list by index + this.gestures.sort(function(a, b) { + if(a.index < b.index) { + return -1; + } + if(a.index > b.index) { + return 1; + } + return 0; + }); + + return this.gestures; + } +}; + + +/** + * @module hammer + */ + +/** + * create new hammer instance + * all methods should return the instance itself, so it is chainable. + * + * @class Instance + * @constructor + * @param {HTMLElement} element + * @param {Object} [options={}] options are merged with `Hammer.defaults` + * @return {Hammer.Instance} + */ +Hammer.Instance = function(element, options) { + var self = this; + + // setup HammerJS window events and register all gestures + // this also sets up the default options + setup(); + + /** + * @property element + * @type {HTMLElement} + */ + this.element = element; + + /** + * @property enabled + * @type {Boolean} + * @protected + */ + this.enabled = true; + + /** + * options, merged with the defaults + * options with an _ are converted to camelCase + * @property options + * @type {Object} + */ + Utils.each(options, function(value, name) { + delete options[name]; + options[Utils.toCamelCase(name)] = value; + }); + + this.options = Utils.extend(Utils.extend({}, Hammer.defaults), options || {}); + + // add some css to the element to prevent the browser from doing its native behavoir + if(this.options.behavior) { + Utils.toggleBehavior(this.element, this.options.behavior, true); + } + + /** + * event start handler on the element to start the detection + * @property eventStartHandler + * @type {Object} + */ + this.eventStartHandler = Event.onTouch(element, EVENT_START, function(ev) { + if(self.enabled && ev.eventType == EVENT_START) { + Detection.startDetect(self, ev); + } else if(ev.eventType == EVENT_TOUCH) { + Detection.detect(ev); + } + }); + + /** + * keep a list of user event handlers which needs to be removed when calling 'dispose' + * @property eventHandlers + * @type {Array} + */ + this.eventHandlers = []; +}; + +Hammer.Instance.prototype = { + /** + * bind events to the instance + * @method on + * @chainable + * @param {String} gestures multiple gestures by splitting with a space + * @param {Function} handler + * @param {Object} handler.ev event object + */ + on: function onEvent(gestures, handler) { + var self = this; + Event.on(self.element, gestures, handler, function(type) { + self.eventHandlers.push({ gesture: type, handler: handler }); + }); + return self; + }, + + /** + * unbind events to the instance + * @method off + * @chainable + * @param {String} gestures + * @param {Function} handler + */ + off: function offEvent(gestures, handler) { + var self = this; + + Event.off(self.element, gestures, handler, function(type) { + var index = Utils.inArray({ gesture: type, handler: handler }); + if(index !== false) { + self.eventHandlers.splice(index, 1); + } + }); + return self; + }, + + /** + * trigger gesture event + * @method trigger + * @chainable + * @param {String} gesture + * @param {Object} [eventData] + */ + trigger: function triggerEvent(gesture, eventData) { + // optional + if(!eventData) { + eventData = {}; + } + + // create DOM event + var event = Hammer.DOCUMENT.createEvent('Event'); + event.initEvent(gesture, true, true); + event.gesture = eventData; + + // trigger on the target if it is in the instance element, + // this is for event delegation tricks + var element = this.element; + if(Utils.hasParent(eventData.target, element)) { + element = eventData.target; + } + + element.dispatchEvent(event); + return this; + }, + + /** + * enable of disable hammer.js detection + * @method enable + * @chainable + * @param {Boolean} state + */ + enable: function enable(state) { + this.enabled = state; + return this; + }, + + /** + * dispose this hammer instance + * @method dispose + * @return {Null} + */ + dispose: function dispose() { + var i, eh; + + // undo all changes made by stop_browser_behavior + Utils.toggleBehavior(this.element, this.options.behavior, false); + + // unbind all custom event handlers + for(i = -1; (eh = this.eventHandlers[++i]);) { + Utils.off(this.element, eh.gesture, eh.handler); + } + + this.eventHandlers = []; + + // unbind the start event listener + Event.off(this.element, EVENT_TYPES[EVENT_START], this.eventStartHandler); + + return null; + } +}; + + +/** + * @module gestures + */ +/** + * Move with x fingers (default 1) around on the page. + * Preventing the default browser behavior is a good way to improve feel and working. + * ```` + * hammertime.on("drag", function(ev) { + * console.log(ev); + * ev.gesture.preventDefault(); + * }); + * ```` + * + * @class Drag + * @static + */ +/** + * @event drag + * @param {Object} ev + */ +/** + * @event dragstart + * @param {Object} ev + */ +/** + * @event dragend + * @param {Object} ev + */ +/** + * @event drapleft + * @param {Object} ev + */ +/** + * @event dragright + * @param {Object} ev + */ +/** + * @event dragup + * @param {Object} ev + */ +/** + * @event dragdown + * @param {Object} ev + */ + +/** + * @param {String} name + */ +(function(name) { + var triggered = false; + + function dragGesture(ev, inst) { + var cur = Detection.current; + + // max touches + if(inst.options.dragMaxTouches > 0 && + ev.touches.length > inst.options.dragMaxTouches) { + return; + } + + switch(ev.eventType) { + case EVENT_START: + triggered = false; + break; + + case EVENT_MOVE: + // when the distance we moved is too small we skip this gesture + // or we can be already in dragging + if(ev.distance < inst.options.dragMinDistance && + cur.name != name) { + return; + } + + var startCenter = cur.startEvent.center; + + // we are dragging! + if(cur.name != name) { + cur.name = name; + if(inst.options.dragDistanceCorrection && ev.distance > 0) { + // When a drag is triggered, set the event center to dragMinDistance pixels from the original event center. + // Without this correction, the dragged distance would jumpstart at dragMinDistance pixels instead of at 0. + // It might be useful to save the original start point somewhere + var factor = Math.abs(inst.options.dragMinDistance / ev.distance); + startCenter.pageX += ev.deltaX * factor; + startCenter.pageY += ev.deltaY * factor; + startCenter.clientX += ev.deltaX * factor; + startCenter.clientY += ev.deltaY * factor; + + // recalculate event data using new start point + ev = Detection.extendEventData(ev); + } + } + + // lock drag to axis? + if(cur.lastEvent.dragLockToAxis || + ( inst.options.dragLockToAxis && + inst.options.dragLockMinDistance <= ev.distance + )) { + ev.dragLockToAxis = true; + } + + // keep direction on the axis that the drag gesture started on + var lastDirection = cur.lastEvent.direction; + if(ev.dragLockToAxis && lastDirection !== ev.direction) { + if(Utils.isVertical(lastDirection)) { + ev.direction = (ev.deltaY < 0) ? DIRECTION_UP : DIRECTION_DOWN; + } else { + ev.direction = (ev.deltaX < 0) ? DIRECTION_LEFT : DIRECTION_RIGHT; + } + } + + // first time, trigger dragstart event + if(!triggered) { + inst.trigger(name + 'start', ev); + triggered = true; + } + + // trigger events + inst.trigger(name, ev); + inst.trigger(name + ev.direction, ev); + + var isVertical = Utils.isVertical(ev.direction); + + // block the browser events + if((inst.options.dragBlockVertical && isVertical) || + (inst.options.dragBlockHorizontal && !isVertical)) { + ev.preventDefault(); + } + break; + + case EVENT_RELEASE: + if(triggered && ev.changedLength <= inst.options.dragMaxTouches) { + inst.trigger(name + 'end', ev); + triggered = false; + } + break; + + case EVENT_END: + triggered = false; + break; + } + } + + Hammer.gestures.Drag = { + name: name, + index: 50, + handler: dragGesture, + defaults: { + /** + * minimal movement that have to be made before the drag event gets triggered + * @property dragMinDistance + * @type {Number} + * @default 10 + */ + dragMinDistance: 10, + + /** + * Set dragDistanceCorrection to true to make the starting point of the drag + * be calculated from where the drag was triggered, not from where the touch started. + * Useful to avoid a jerk-starting drag, which can make fine-adjustments + * through dragging difficult, and be visually unappealing. + * @property dragDistanceCorrection + * @type {Boolean} + * @default true + */ + dragDistanceCorrection: true, + + /** + * set 0 for unlimited, but this can conflict with transform + * @property dragMaxTouches + * @type {Number} + * @default 1 + */ + dragMaxTouches: 1, + + /** + * prevent default browser behavior when dragging occurs + * be careful with it, it makes the element a blocking element + * when you are using the drag gesture, it is a good practice to set this true + * @property dragBlockHorizontal + * @type {Boolean} + * @default false + */ + dragBlockHorizontal: false, + + /** + * same as `dragBlockHorizontal`, but for vertical movement + * @property dragBlockVertical + * @type {Boolean} + * @default false + */ + dragBlockVertical: false, + + /** + * dragLockToAxis keeps the drag gesture on the axis that it started on, + * It disallows vertical directions if the initial direction was horizontal, and vice versa. + * @property dragLockToAxis + * @type {Boolean} + * @default false + */ + dragLockToAxis: false, + + /** + * drag lock only kicks in when distance > dragLockMinDistance + * This way, locking occurs only when the distance has become large enough to reliably determine the direction + * @property dragLockMinDistance + * @type {Number} + * @default 25 + */ + dragLockMinDistance: 25 + } + }; +})('drag'); + +/** + * @module gestures + */ +/** + * trigger a simple gesture event, so you can do anything in your handler. + * only usable if you know what your doing... + * + * @class Gesture + * @static + */ +/** + * @event gesture + * @param {Object} ev + */ +Hammer.gestures.Gesture = { + name: 'gesture', + index: 1337, + handler: function releaseGesture(ev, inst) { + inst.trigger(this.name, ev); + } +}; + +/** + * @module gestures + */ +/** + * Touch stays at the same place for x time + * + * @class Hold + * @static + */ +/** + * @event hold + * @param {Object} ev + */ + +/** + * @param {String} name + */ +(function(name) { + var timer; + + function holdGesture(ev, inst) { + var options = inst.options, + current = Detection.current; + + switch(ev.eventType) { + case EVENT_START: + clearTimeout(timer); + + // set the gesture so we can check in the timeout if it still is + current.name = name; + + // set timer and if after the timeout it still is hold, + // we trigger the hold event + timer = setTimeout(function() { + if(current && current.name == name) { + inst.trigger(name, ev); + } + }, options.holdTimeout); + break; + + case EVENT_MOVE: + if(ev.distance > options.holdThreshold) { + clearTimeout(timer); + } + break; + + case EVENT_RELEASE: + clearTimeout(timer); + break; + } + } + + Hammer.gestures.Hold = { + name: name, + index: 10, + defaults: { + /** + * @property holdTimeout + * @type {Number} + * @default 500 + */ + holdTimeout: 500, + + /** + * movement allowed while holding + * @property holdThreshold + * @type {Number} + * @default 2 + */ + holdThreshold: 2 + }, + handler: holdGesture + }; +})('hold'); + +/** + * @module gestures + */ +/** + * when a touch is being released from the page + * + * @class Release + * @static + */ +/** + * @event release + * @param {Object} ev + */ +Hammer.gestures.Release = { + name: 'release', + index: Infinity, + handler: function releaseGesture(ev, inst) { + if(ev.eventType == EVENT_RELEASE) { + inst.trigger(this.name, ev); + } + } +}; + +/** + * @module gestures + */ +/** + * triggers swipe events when the end velocity is above the threshold + * for best usage, set `preventDefault` (on the drag gesture) to `true` + * ```` + * hammertime.on("dragleft swipeleft", function(ev) { + * console.log(ev); + * ev.gesture.preventDefault(); + * }); + * ```` + * + * @class Swipe + * @static + */ +/** + * @event swipe + * @param {Object} ev + */ +/** + * @event swipeleft + * @param {Object} ev + */ +/** + * @event swiperight + * @param {Object} ev + */ +/** + * @event swipeup + * @param {Object} ev + */ +/** + * @event swipedown + * @param {Object} ev + */ +Hammer.gestures.Swipe = { + name: 'swipe', + index: 40, + defaults: { + /** + * @property swipeMinTouches + * @type {Number} + * @default 1 + */ + swipeMinTouches: 1, + + /** + * @property swipeMaxTouches + * @type {Number} + * @default 1 + */ + swipeMaxTouches: 1, + + /** + * horizontal swipe velocity + * @property swipeVelocityX + * @type {Number} + * @default 0.6 + */ + swipeVelocityX: 0.6, + + /** + * vertical swipe velocity + * @property swipeVelocityY + * @type {Number} + * @default 0.6 + */ + swipeVelocityY: 0.6 + }, + + handler: function swipeGesture(ev, inst) { + if(ev.eventType == EVENT_RELEASE) { + var touches = ev.touches.length, + options = inst.options; + + // max touches + if(touches < options.swipeMinTouches || + touches > options.swipeMaxTouches) { + return; + } + + // when the distance we moved is too small we skip this gesture + // or we can be already in dragging + if(ev.velocityX > options.swipeVelocityX || + ev.velocityY > options.swipeVelocityY) { + // trigger swipe events + inst.trigger(this.name, ev); + inst.trigger(this.name + ev.direction, ev); + } + } + } +}; + +/** + * @module gestures + */ +/** + * Single tap and a double tap on a place + * + * @class Tap + * @static + */ +/** + * @event tap + * @param {Object} ev + */ +/** + * @event doubletap + * @param {Object} ev + */ + +/** + * @param {String} name + */ +(function(name) { + var hasMoved = false; + + function tapGesture(ev, inst) { + var options = inst.options, + current = Detection.current, + prev = Detection.previous, + sincePrev, + didDoubleTap; + + switch(ev.eventType) { + case EVENT_START: + hasMoved = false; + break; + + case EVENT_MOVE: + hasMoved = hasMoved || (ev.distance > options.tapMaxDistance); + break; + + case EVENT_END: + if(!Utils.inStr(ev.srcEvent.type, 'cancel') && ev.deltaTime < options.tapMaxTime && !hasMoved) { + // previous gesture, for the double tap since these are two different gesture detections + sincePrev = prev && prev.lastEvent && ev.timeStamp - prev.lastEvent.timeStamp; + didDoubleTap = false; + + // check if double tap + if(prev && prev.name == name && + (sincePrev && sincePrev < options.doubleTapInterval) && + ev.distance < options.doubleTapDistance) { + inst.trigger('doubletap', ev); + didDoubleTap = true; + } + + // do a single tap + if(!didDoubleTap || options.tapAlways) { + current.name = name; + inst.trigger(current.name, ev); + } + } + break; + } + } + + Hammer.gestures.Tap = { + name: name, + index: 100, + handler: tapGesture, + defaults: { + /** + * max time of a tap, this is for the slow tappers + * @property tapMaxTime + * @type {Number} + * @default 250 + */ + tapMaxTime: 250, + + /** + * max distance of movement of a tap, this is for the slow tappers + * @property tapMaxDistance + * @type {Number} + * @default 10 + */ + tapMaxDistance: 10, + + /** + * always trigger the `tap` event, even while double-tapping + * @property tapAlways + * @type {Boolean} + * @default true + */ + tapAlways: true, + + /** + * max distance between two taps + * @property doubleTapDistance + * @type {Number} + * @default 20 + */ + doubleTapDistance: 20, + + /** + * max time between two taps + * @property doubleTapInterval + * @type {Number} + * @default 300 + */ + doubleTapInterval: 300 + } + }; +})('tap'); + +/** + * @module gestures + */ +/** + * when a touch is being touched at the page + * + * @class Touch + * @static + */ +/** + * @event touch + * @param {Object} ev + */ +Hammer.gestures.Touch = { + name: 'touch', + index: -Infinity, + defaults: { + /** + * call preventDefault at touchstart, and makes the element blocking by disabling the scrolling of the page, + * but it improves gestures like transforming and dragging. + * be careful with using this, it can be very annoying for users to be stuck on the page + * @property preventDefault + * @type {Boolean} + * @default false + */ + preventDefault: false, + + /** + * disable mouse events, so only touch (or pen!) input triggers events + * @property preventMouse + * @type {Boolean} + * @default false + */ + preventMouse: false + }, + handler: function touchGesture(ev, inst) { + if(inst.options.preventMouse && ev.pointerType == POINTER_MOUSE) { + ev.stopDetect(); + return; + } + + if(inst.options.preventDefault) { + ev.preventDefault(); + } + + if(ev.eventType == EVENT_TOUCH) { + inst.trigger('touch', ev); + } + } +}; + +/** + * @module gestures + */ +/** + * User want to scale or rotate with 2 fingers + * Preventing the default browser behavior is a good way to improve feel and working. This can be done with the + * `preventDefault` option. + * + * @class Transform + * @static + */ +/** + * @event transform + * @param {Object} ev + */ +/** + * @event transformstart + * @param {Object} ev + */ +/** + * @event transformend + * @param {Object} ev + */ +/** + * @event pinchin + * @param {Object} ev + */ +/** + * @event pinchout + * @param {Object} ev + */ +/** + * @event rotate + * @param {Object} ev + */ + +/** + * @param {String} name + */ +(function(name) { + var triggered = false; + + function transformGesture(ev, inst) { + switch(ev.eventType) { + case EVENT_START: + triggered = false; + break; + + case EVENT_MOVE: + // at least multitouch + if(ev.touches.length < 2) { + return; + } + + var scaleThreshold = Math.abs(1 - ev.scale); + var rotationThreshold = Math.abs(ev.rotation); + + // when the distance we moved is too small we skip this gesture + // or we can be already in dragging + if(scaleThreshold < inst.options.transformMinScale && + rotationThreshold < inst.options.transformMinRotation) { + return; + } + + // we are transforming! + Detection.current.name = name; + + // first time, trigger dragstart event + if(!triggered) { + inst.trigger(name + 'start', ev); + triggered = true; + } + + inst.trigger(name, ev); // basic transform event + + // trigger rotate event + if(rotationThreshold > inst.options.transformMinRotation) { + inst.trigger('rotate', ev); + } + + // trigger pinch event + if(scaleThreshold > inst.options.transformMinScale) { + inst.trigger('pinch', ev); + inst.trigger('pinch' + (ev.scale < 1 ? 'in' : 'out'), ev); + } + break; + + case EVENT_RELEASE: + if(triggered && ev.changedLength < 2) { + inst.trigger(name + 'end', ev); + triggered = false; + } + break; + } + } + + Hammer.gestures.Transform = { + name: name, + index: 45, + defaults: { + /** + * minimal scale factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1 + * @property transformMinScale + * @type {Number} + * @default 0.01 + */ + transformMinScale: 0.01, + + /** + * rotation in degrees + * @property transformMinRotation + * @type {Number} + * @default 1 + */ + transformMinRotation: 1 + }, + + handler: transformGesture + }; +})('transform'); + +/** + * @module hammer + */ + +// AMD export +if(typeof define == 'function' && define.amd) { + define(function() { + return Hammer; + }); +// commonjs export +} else if(typeof module !== 'undefined' && module.exports) { + module.exports = Hammer; +// browser export +} else { + window.Hammer = Hammer; +} + +})(window); \ No newline at end of file diff --git a/js/jester.js b/js/jester.js deleted file mode 100644 index 4612443..0000000 --- a/js/jester.js +++ /dev/null @@ -1,599 +0,0 @@ -/* - * Jester JavaScript Library v0.3 - * http://github.com/plainview/Jester - * - * Easy JavaScript gesture recognition. - * - * Released under MIT License - * - * Copyright (C) 2011 by Scott Seaward - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -(function(container, undefined) { - var Jester = container.Jester = { - cache : {}, - cacheId : "Jester" + (new Date()).getTime(), - guid : 0, - - // The Jester constructor - Watcher : function(element, options) { - - var that = this, - cacheId = Jester.cacheId, - cache = Jester.cache, - gestures = "swipe flick tap doubletap pinchnarrow pinchwiden pinchend"; - - if(!element || !element.nodeType) { - throw new TypeError("Jester: no element given."); - } - - // if this element hasn't had Jester called on it before, - // set it up with a cache entry and give it the expando - if(typeof element[cacheId] !== "number") { - element[cacheId] = Jester.guid; - Jester.guid++; - } - - var elementId = element[cacheId]; - - if(!(elementId in cache)) { - Jester.cache[elementId] = {}; - } - - var elementCache = Jester.cache[elementId]; - - if(!("options" in elementCache)) { - elementCache.options = {}; - } - - options = options || elementCache.options || {}; - - // cache the option values for reuse or, if options already - // exist for this element, replace those that have been - // specified - if(elementCache.options !== options) { - for(var prop in options) { - if(elementCache.options[prop]) { - if(elementCache.options[prop] !== options[prop]) { - elementCache.options[prop] = options[prop]; - } - } - else { - elementCache.options[prop] = options[prop]; - } - } - } - - if(!("eventSet" in elementCache) || !(elementCache.eventSet instanceof Jester.EventSet)) { - elementCache.eventSet = new Jester.EventSet(element); - } - - if(!elementCache.touchMonitor) { - elementCache.touchMonitor = new Jester.TouchMonitor(element); - } - - var events = elementCache.eventSet; - var touches = elementCache.touchMonitor; - - this.id = element[cacheId]; - - this.bind = function(evt, fn) { - if(evt && typeof evt === "string" && typeof fn === "function") { - events.register(evt, fn); - } - return this; - }; - - // create shortcut bind methods for all gestures - gestures.split(" ").forEach(function(gesture) { - this[gesture] = function(fn) { - return this.bind(gesture, fn); - }; - }, that); - - this.start = function(fn) { - return this.bind("start", fn); - }; - - this.during = function(fn) { - return this.bind("during", fn); - }; - - this.end = function(fn) { - return this.bind("end", fn); - }; - - // wrapper to cover all three pinch methods - this.pinch = function(fns) { - if(typeof fns !== "undefined") { - // if its just a function it gets assigned to pinchend - if(typeof fns === "function") { - that.pinchend(fns); - } - else if(typeof fns === "object") { - var method; - "narrow widen end".split(" ").forEach(function(eventExt) { - method = "pinch" + eventExt; - if(typeof fns[eventExt] === "function") { - that[method](fns[eventExt]); - } - }); - } - } - }; - - this.halt = function() { - touches.stopListening(); - events.clear(); - delete elementCache.eventSet; - delete elementCache.touchMonitor; - }; - }, - EventSet : function(element) { - // all event names and their associated functions in an array i.e. "swipe" : [fn1, fn2, fn2] - var eventsTable = {}; - this.eventsTable = eventsTable; - - // register a handler with an event - this.register = function(eventName, fn) { - // if the event exists and has handlers attached to it, add this one to the array of them - if(eventsTable[eventName] && eventsTable[eventName].push) { - // make sure multiple copies of the same handler aren't inserted - if(!~eventsTable[eventName].indexOf(fn)) { - eventsTable[eventName].push(fn); - } - } - else { - // create a new array bound to the event containing only the handler passed in - eventsTable[eventName] = [fn]; - } - }; - - this.release = function(eventName, fn) { - if(typeof eventName === "undefined") return; - - // if a handler hasn't been specified, remove all handlers - if(typeof fn === "undefined") { - for(var handlers in eventsTable.eventName) { - delete eventsTable.eventName[handlers]; - } - } - else { - // pull the given handler from the given event - if(eventsTable[eventName] && ~eventsTable[eventName].indexOf(fn)) - { - eventsTable[eventName].splice(eventsTable[eventName].indexOf(fn), 1); - } - } - - // if the event has no more handlers registered to it, get rid of the event completely - if(eventsTable[eventName] && eventsTable[eventName].length === 0) { - delete eventsTable[eventName]; - } - }; - - // completely remove all events and their handlers - this.clear = function() { - var events; - for(events in eventsTable) { - delete eventsTable[events]; - } - }; - - // get all the handlers associated with an event - // return an empty array if nothing is registered with the given event name - this.getHandlers = function(eventName) { - if(eventsTable[eventName] && eventsTable[eventName].length) { - return eventsTable[eventName]; - } - else { - return []; - } - }; - - // inject an array of handlers into the event table for the given event - // this will klobber all current handlers associated with the event - this.setHandlers = function(eventName, handlers) { - eventsTable[eventName] = handlers; - }; - - // execute all handlers associated with an event, passing each handler the arguments provided after the event's name. - this.execute = function(eventName) { - if(typeof eventName === "undefined") return; - - // if the event asked for exists in the events table - if(eventsTable[eventName] && eventsTable[eventName].length) { - // get the arguments sent to the function - var args = Array.prototype.slice.call(arguments, 1); - - // iterate throuh all the handlers - for(var i = 0; i < eventsTable[eventName].length; i++) { - // check current handler is a function - if(typeof eventsTable[eventName][i] === "function") { - // execute handler with the provided arguments - eventsTable[eventName][i].apply(element, args); - } - } - } - }; - }, - - TouchMonitor : function(element) - { - var cacheId = Jester.cacheId, - elementId = element[cacheId], - cache = Jester.cache, - elementCache = cache[elementId], - opts = elementCache.options; - - opts.move = opts.move || {}; - opts.scale = opts.scale || {}; - - opts.tapDistance = opts.tapDistance || 0; - opts.tapTime = opts.tapTime || 20; - - opts.doubleTapTime = opts.doubleTapTime || 300; - - opts.swipeDistance = opts.swipeDistance || 200; - - opts.flickTime = opts.flickTime || 300; - opts.flickDistance = opts.flickDistance || 100; - - opts.deadX = opts.deadX || 0; - opts.deadY = opts.deadY || 0; - - if(opts.capture !== false) opts.capture = true; - if(typeof opts.preventDefault !== "undefined" && opts.preventDefault !== false) opts.preventDefault = true; - if(typeof opts.preventDefault !== "undefined" && opts.stopPropagation !== false) opts.stopPropagation = true; - - var eventSet = elementCache.eventSet; - - var touches; - var previousTapTime = 0; - - var touchStart = function(evt) { - touches = new Jester.TouchGroup(evt); - - eventSet.execute("start", touches, evt); - - if(opts.preventDefault) evt.preventDefault(); - if(opts.stopPropagation) evt.stopPropagation(); - }; - - var touchMove = function(evt) { - touches.update(evt); - - eventSet.execute("during", touches, evt); - - if(opts.preventDefault) evt.preventDefault(); - if(opts.stopPropagation) evt.stopPropagation(); - - if(touches.numTouches() == 2) { - // pinchnarrow - if(touches.delta.scale() < 0.0) { - eventSet.execute("pinchnarrow", touches); - } - - // pinchwiden - else if(touches.delta.scale() > 0.0) { - eventSet.execute("pinchwiden", touches); - } - } - }; - - var touchEnd = function(evt) { - - var swipeDirection; - - eventSet.execute("end", touches, evt); - - if(opts.preventDefault) evt.preventDefault(); - if(opts.stopPropagation) evt.stopPropagation(); - - if(touches.numTouches() == 1) { - // tap - if(touches.touch(0).total.x() <= opts.tapDistance && touches.touch(0).total.y() <= opts.tapDistance && touches.touch(0).total.time() < opts.tapTime) { - eventSet.execute("tap", touches); - } - - // doubletap - if(touches.touch(0).total.time() < opts.tapTime) { - var now = (new Date()).getTime(); - if(now - previousTapTime <= opts.doubleTapTime) { - eventSet.execute("doubletap", touches); - } - previousTapTime = now; - } - - // swipe left/right - if(Math.abs(touches.touch(0).total.x()) >= opts.swipeDistance) { - swipeDirection = touches.touch(0).total.x() < 0 ? "left" : "right"; - eventSet.execute("swipe", touches, swipeDirection); - } - - // swipe up/down - if(Math.abs(touches.touch(0).total.y()) >= opts.swipeDistance) { - swipeDirection = touches.touch(0).total.y() < 0 ? "up" : "down"; - eventSet.execute("swipe", touches, swipeDirection); - } - - // flick - if(Math.abs(touches.touch(0).total.x()) >= opts.flickDistance && touches.touch(0).total.time() <= opts.flickTime) { - var flickDirection = touches.touch(0).total.x() < 0 ? "left" : "right"; - eventSet.execute("flick", touches, flickDirection); - } - } - else if(touches.numTouches() == 2) { - // pinchend - if(touches.current.scale() !== 1.0) { - var pinchDirection = touches.current.scale() < 1.0 ? "narrowed" : "widened"; - eventSet.execute("pinchend", touches, pinchDirection); - } - } - }; - - var stopListening = function() { - element.removeEventListener("touchstart", touchStart, opts.capture); - element.removeEventListener("touchmove", touchMove, opts.capture); - element.removeEventListener("touchend", touchEnd, opts.capture); - }; - - element.addEventListener("touchstart", touchStart, opts.capture); - element.addEventListener("touchmove", touchMove, opts.capture); - element.addEventListener("touchend", touchEnd, opts.capture); - - return { - stopListening: stopListening - }; - }, - - TouchGroup : function(event) { - var that = this; - - var numTouches = event.touches.length; - - var midpointX = 0; - var midpointY = 0; - - var scale = event.scale; - var prevScale = scale; - var deltaScale = scale; - - for(var i = 0; i < numTouches; i++) { - this["touch" + i] = new Jester.Touch(event.touches[i].pageX, event.touches[i].pageY); - midpointX = event.touches[i].pageX; - midpointY = event.touches[i].pageY; - } - - function getNumTouches() { - return numTouches; - } - - function getTouch(num) { - return that["touch" + num]; - } - - function getMidPointX() { - return midpointX; - } - function getMidPointY() { - return midpointY; - } - function getScale() { - return scale; - } - function getDeltaScale() { - return deltaScale; - } - - function updateTouches(event) { - var mpX = 0; - var mpY = 0; - - for(var i = 0; i < event.touches.length; i++) { - if(i < numTouches) { - that["touch" + i].update(event.touches[i].pageX, event.touches[i].pageY); - mpX += event.touches[i].pageX; - mpY += event.touches[i].pageY; - } - } - midpointX = mpX / numTouches; - midpointY = mpY / numTouches; - - prevScale = scale; - scale = event.scale; - deltaScale = scale - prevScale; - } - - return { - numTouches: getNumTouches, - touch: getTouch, - current: { - scale: getScale, - midX: getMidPointX, - midY: getMidPointY - }, - delta: { - scale: getDeltaScale - }, - update: updateTouches - }; - }, - - Touch : function(_startX, _startY) { - var startX = _startX, - startY = _startY, - startTime = now(), - currentX = startX, - currentY = startY, - currentTime = startTime, - currentSpeedX = 0, - currentSpeedY = 0, - prevX = startX, - prevY = startX, - prevTime = startTime, - prevSpeedX = 0, - prevSpeedY = 0, - deltaX = 0, - deltaY = 0, - deltaTime = 0, - deltaSpeedX = 0, - deltaSpeedY = 0, - totalX = 0, - totalY = 0, - totalTime = 0; - - // position getters - function getStartX() { - return startX; - } - function getStartY() { - return startY; - } - function getCurrentX() { - return currentX; - } - function getCurrentY() { - return currentY; - } - function getPrevX() { - return prevX; - } - function getPrevY() { - return prevY; - } - function getDeltaX() { - return deltaX; - } - function getDeltaY() { - return deltaY; - } - function getTotalX() { - return totalX; - } - function getTotalY() { - return totalY; - } - - // time getters - function now() { - return (new Date()).getTime(); - } - function getStartTime() { - return startTime; - } - function getCurrentTime() { - return currentTime; - } - function getPrevTime() { - return prevTime; - } - function getDeltaTime() { - return deltaTime; - } - function getTotalTime() { - return totalTime; - } - - // speed getters - function getCurrentSpeedX() { - return currentSpeedX; - } - function getCurrentSpeedY() { - return currentSpeedY; - } - function getPrevSpeedX() { - return prevSpeedX; - } - function getPrevSpeedY() { - return prevSpeedY; - } - function getDeltaSpeedX() { - return deltaSpeedX; - } - function getDeltaSpeedY() { - return deltaSpeedY; - } - - return { - start: { - x: getStartX, - y: getStartY, - speedX: 0, - speedY: 0, - time: getStartTime - }, - current: { - x: getCurrentX, - y: getCurrentY, - time: getCurrentTime, - speedX: getCurrentSpeedX, - speedY: getCurrentSpeedY - }, - prev: { - x: getPrevX, - y: getPrevY, - time: getPrevTime, - speedX: getPrevSpeedX, - speedY: getPrevSpeedY - }, - delta: { - x: getDeltaX, - y: getDeltaY, - speedX: getDeltaSpeedX, - speedY: getDeltaSpeedY, - time: getDeltaTime - }, - total: { - x: getTotalX, - y: getTotalY, - time: getTotalTime - }, - update: function(_x, _y) { - prevX = currentX; - prevY = currentY; - currentX = _x; - currentY = _y; - deltaX = currentX - prevX; - deltaY = currentY - prevY; - totalX = currentX - startX; - totalY = currentY - startY; - - prevTime = currentTime; - currentTime = now(); - deltaTime = currentTime - prevTime; - totalTime = currentTime - startTime; - - prevSpeedX = currentSpeedX; - prevSpeedY = currentSpeedY; - currentSpeedX = deltaX / (deltaTime/1000); - currentSpeedY = deltaY / (deltaTime/1000); - deltaSpeedX = currentSpeedX - prevSpeedX; - deltaSpeedY = currentSpeedY - prevSpeedY; - } - }; - } - }; - - container.jester = function(el, opts) { - return new Jester.Watcher(el, opts); - }; - -}(window));