diff --git a/example/events.html b/example/events.html index b7a6291375..1dc83e3fbd 100644 --- a/example/events.html +++ b/example/events.html @@ -16,8 +16,8 @@
- Tap me! - Swipe me! + Tap me! + Swipe me!
diff --git a/example/events.js b/example/events.js index a0a0ca324f..dba0246e7c 100644 --- a/example/events.js +++ b/example/events.js @@ -5,35 +5,47 @@ var logEvent = function(data) { console.log(data.event); e.appendChild(l); } -window.FM.on('tap', function(e) { + +var tb = document.getElementById('tap-button'); +var sb = document.getElementById('swipe-button'); + +window.FM.onGesture('tap', function(e) { console.log('GOT TAP', e); logEvent({ type: 'tap', event: e }); -}); -window.FM.on('touch', function(e) { +}, tb); +window.FM.onGesture('touch', function(e) { console.log('GOT TOUCH', e); logEvent({ type: 'touch', event: e }); -}); -window.FM.on('release', function(e) { +}, tb); + +window.FM.onGesture('tap', function(e) { + console.log('GOT TAP', e); + logEvent({ + type: 'tap', + event: e + }); +}, tb); +window.FM.onGesture('release', function(e) { console.log('GOT RELEASE', e); logEvent({ type: 'release', event: e }); -}); -window.FM.on('swipe', function(e) { +}, tb); +window.FM.onGesture('swipe', function(e) { console.log('GOT SWIPE', e); logEvent({ type: 'swipe', event: e }); -}); -window.FM.on('swiperight', function(e) { +}, sb); +window.FM.onGesture('swiperight', function(e) { console.log('GOT SWIPE RIGHT', e); logEvent({ type: 'swiperight', @@ -41,8 +53,8 @@ window.FM.on('swiperight', function(e) { }); e.target.classList.add('swiperight'); -}); -window.FM.on('swipeleft', function(e) { +}, sb); +window.FM.onGesture('swipeleft', function(e) { console.log('GOT SWIPE LEFT', e); logEvent({ type: 'swipeleft', @@ -50,18 +62,18 @@ window.FM.on('swipeleft', function(e) { }); e.target.classList.add('swipeleft'); -}); -window.FM.on('swipeup', function(e) { +}, sb); +window.FM.onGesture('swipeup', function(e) { console.log('GOT SWIPE UP', e); logEvent({ type: 'swipeup', event: e }); -}); -window.FM.on('swipedown', function(e) { +}, sb); +window.FM.onGesture('swipedown', function(e) { console.log('GOT SWIPE DOWN', e); logEvent({ type: 'swipedown', event: e }); -}); +}, sb); diff --git a/js/framework/framework-events.js b/js/framework/framework-events.js index f74e8ef6b4..543eb64fa4 100644 --- a/js/framework/framework-events.js +++ b/js/framework/framework-events.js @@ -13,75 +13,36 @@ (function(window, document, framework) { framework.EventController = { - // A map of event types that we virtually detect and emit - VIRTUAL_EVENT_TYPES: ['tap', 'swipeleft', 'swiperight'], - - /** - * Trigger a new event. - */ + // Trigger a new event trigger: function(eventType, data) { // TODO: Do we need to use the old-school createEvent stuff? var event = new CustomEvent(eventType, data); + // Make sure to trigger the event on the given target, or dispatch it from + // the window if we don't have an event target data.target && data.target.dispatchEvent(event) || window.dispatchEvent(event); }, - /** - * Shorthand for binding a new event listener to the given - * event type. - */ + // Bind an event on: function(type, callback, element) { - var i; var e = element || window; - /* - var virtualTypes = framework.EventController.VIRTUAL_EVENT_TYPES; - - for(i = 0; i < virtualTypes.length; i++) { - if(type.toLowerCase() == virtualTypes[i]) { - // TODO: listen for virtual event - return; - } - } - */ - - // Native listener e.addEventListener(type, callback); }, - - /** - * Process a touchstart event. - */ - handleTouchStart: function(e) { - console.log("EVENT: touchstart", e); - framework.GestureController.startGesture(e); + off: function(type, callback, element) { + element.removeEventListener(type, callback); }, - /** - * Process a touchmove event. - */ - handleTouchMove: function(e) { - console.log("EVENT: touchmove", e); - framework.GestureController.detectGesture(e); - + // Register for a new gesture event on the given element + onGesture: function(type, callback, element) { + var gesture = new framework.Gesture(element); + gesture.on(type, callback); + return gesture; }, - /** - * Process a touchend event. - */ - handleTouchEnd: function(e) { - console.log("EVENT: touchend", e); - framework.GestureController.detectGesture(e); - }, - - - /** - * Process a touchcancel event. - */ - handleTouchCancel: function(e) { - this._hasMoved = false; - this._touchStartX = null; - this._touchStartY = null; + // Unregister a previous gesture event + offGesture: function(gesture, type, callback) { + gesture.off(type, callback); }, // With a click event, we need to check the target @@ -122,13 +83,12 @@ // Map some convenient top-level functions for event handling framework.on = framework.EventController.on; + framework.off = framework.EventController.off; framework.trigger = framework.EventController.trigger; + framework.onGesture = framework.EventController.onGesture; + framework.offGesture = framework.EventController.offGesture; // Set up various listeners - window.addEventListener('touchstart', framework.EventController.handleTouchStart); - window.addEventListener('touchmove', framework.EventController.handleTouchMove); - window.addEventListener('touchcancel', framework.EventController.handleTouchCancel); - window.addEventListener('touchend', framework.EventController.handleTouchEnd); window.addEventListener('click', framework.EventController.handleClick); window.addEventListener('popstate', framework.EventController.handlePopState); diff --git a/js/framework/framework-gestures.js b/js/framework/framework-gestures.js index 680b20e84a..a9e494687a 100644 --- a/js/framework/framework-gestures.js +++ b/js/framework/framework-gestures.js @@ -2,222 +2,557 @@ * Simple gesture controllers with some common gestures that emit * gesture events. * - * Much adapted from github.com/EightMedia/Hammer.js - thanks! + * Ported from github.com/EightMedia/framework.Gestures.js - thanks! */ (function(window, document, framework) { - // Gesture support - framework.Gesture = {} - - // Simple touch gesture that triggers an event when an element is touched - framework.Gesture.Touch = { - handle: function(e) { - if(e.type == 'touchstart') { - framework.GestureController.triggerGestureEvent('touch', e); - } - } + + /** + * framework.Gestures + * use this to create instances + * @param {HTMLElement} element + * @param {Object} options + * @returns {framework.Gestures.Instance} + * @constructor + */ + framework.Gesture = function(element, options) { + return new framework.Gestures.Instance(element, options || {}); }; - // Simple tap gesture - framework.Gesture.Tap = { - handle: function(e) { - switch(e.type) { - case 'touchstart': - this._touchStartX = e.touches[0].clientX; - this._touchStartY = e.touches[0].clientY; + framework.Gestures = {}; - // We are now touching - this._isTouching = true; - // Reset the movement indicator - this._hasMoved = false; - break; - case 'touchmove': - var x = e.touches[0].clientX; - y = e.touches[0].clientY; - - // Check if the finger moved more than 10px, and then indicate we should cancel the tap - if (Math.abs(x - this._touchStartX) > 10 || Math.abs(y - this._touchStartY) > 10) { - console.log('HAS MOVED'); - this._hasMoved = true; - } - break; - case 'touchend': - if(this._hasMoved == false) { - framework.GestureController.triggerGestureEvent('tap', e); - } - break; - } + // default settings + framework.Gestures.defaults = { + // add styles and attributes to the element to prevent the browser from doing + // its native behavior. this doesnt prevent the scrolling, but cancels + // the contextmenu, tap highlighting etc + // set to false to disable this + stop_browser_behavior: { + // this also triggers onselectstart=false for IE + userSelect: 'none', + // this makes the element blocking in IE10 >, you could experiment with the value + // see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241 + touchAction: 'none', + touchCallout: 'none', + contentZooming: 'none', + userDrag: 'none', + tapHighlightColor: 'rgba(0,0,0,0)' } + + // more settings are defined per gesture at gestures.js }; - // The gesture is over, trigger a release event - framework.Gesture.Release = { - handle: function(e) { - if(e.type === 'touchend') { - framework.GestureController.triggerGestureEvent('release', e); + // detect touchevents + framework.Gestures.HAS_POINTEREVENTS = window.navigator.pointerEnabled || window.navigator.msPointerEnabled; + framework.Gestures.HAS_TOUCHEVENTS = ('ontouchstart' in window); + + // dont use mouseevents on mobile devices + framework.Gestures.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android|silk/i; + framework.Gestures.NO_MOUSEEVENTS = framework.Gestures.HAS_TOUCHEVENTS && window.navigator.userAgent.match(framework.Gestures.MOBILE_REGEX); + + // eventtypes per touchevent (start, move, end) + // are filled by framework.Gestures.event.determineEventTypes on setup + framework.Gestures.EVENT_TYPES = {}; + + // direction defines + framework.Gestures.DIRECTION_DOWN = 'down'; + framework.Gestures.DIRECTION_LEFT = 'left'; + framework.Gestures.DIRECTION_UP = 'up'; + framework.Gestures.DIRECTION_RIGHT = 'right'; + + // pointer type + framework.Gestures.POINTER_MOUSE = 'mouse'; + framework.Gestures.POINTER_TOUCH = 'touch'; + framework.Gestures.POINTER_PEN = 'pen'; + + // touch event defines + framework.Gestures.EVENT_START = 'start'; + framework.Gestures.EVENT_MOVE = 'move'; + framework.Gestures.EVENT_END = 'end'; + + // hammer document where the base events are added at + framework.Gestures.DOCUMENT = window.document; + + // plugins namespace + framework.Gestures.plugins = {}; + + // if the window events are set... + framework.Gestures.READY = false; + + /** + * setup events to detect gestures on the document + */ + function setup() { + if(framework.Gestures.READY) { + return; + } + + // find what eventtypes we add listeners to + framework.Gestures.event.determineEventTypes(); + + // Register all gestures inside framework.Gestures.gestures + for(var name in framework.Gestures.gestures) { + if(framework.Gestures.gestures.hasOwnProperty(name)) { + framework.Gestures.detection.register(framework.Gestures.gestures[name]); } } - }; - // A swipe gesture that emits the 'swipe' event when a left swipe happens - framework.Gesture.Swipe = { - swipe_velocity: 0.7, - handle: function(e) { - if(e.type == 'touchend') { + // Add touch events on the document + framework.Gestures.event.onTouch(framework.Gestures.DOCUMENT, framework.Gestures.EVENT_MOVE, framework.Gestures.detection.detect); + framework.Gestures.event.onTouch(framework.Gestures.DOCUMENT, framework.Gestures.EVENT_END, framework.Gestures.detection.detect); - if(e.velocityX > this.swipe_velocity || - e.velocityY > this.swipe_velocity) { + // framework.Gestures is ready...! + framework.Gestures.READY = true; + } - // trigger swipe events, both a general swipe, - // and a directional swipe - framework.GestureController.triggerGestureEvent('swipe', e); - framework.GestureController.triggerGestureEvent('swipe' + e.direction, e); - } - } + /** + * create new hammer instance + * all methods should return the instance itself, so it is chainable. + * @param {HTMLElement} element + * @param {Object} [options={}] + * @returns {framework.Gestures.Instance} + * @constructor + */ + framework.Gestures.Instance = function(element, options) { + var self = this; + + // setup framework.GesturesJS window events and register all gestures + // this also sets up the default options + setup(); + + this.element = element; + + // start/stop detection option + this.enabled = true; + + // merge options + this.options = framework.Gestures.utils.extend( + framework.Gestures.utils.extend({}, framework.Gestures.defaults), + options || {}); + + // add some css to the element to prevent the browser from doing its native behavoir + if(this.options.stop_browser_behavior) { + framework.Gestures.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior); } + + // start detection on touchstart + framework.Gestures.event.onTouch(element, framework.Gestures.EVENT_START, function(ev) { + if(self.enabled) { + framework.Gestures.detection.startDetect(self, ev); + } + }); + + // return instance + return this; }; - framework.GestureController = { - gestures: [ - framework.Gesture.Touch, - framework.Gesture.Tap, - framework.Gesture.Swipe, - framework.Gesture.Release, - ], - - triggerGestureEvent: function(type, e) { - framework.EventController.trigger(type, framework.Utils.extend({}, e)); + framework.Gestures.Instance.prototype = { + /** + * bind events to the instance + * @param {String} gesture + * @param {Function} handler + * @returns {framework.Gestures.Instance} + */ + on: function onEvent(gesture, handler){ + var gestures = gesture.split(' '); + for(var t=0; t 0 && eventType == framework.Gestures.EVENT_END) { + eventType = framework.Gestures.EVENT_MOVE; + } + // no touches, force the end event + else if(!count_touches) { + eventType = framework.Gestures.EVENT_END; + } + + // store the last move event + if(count_touches || last_move_event === null) { + last_move_event = ev; + } + + // trigger the handler + handler.call(framework.Gestures.detection, self.collectEventData(element, eventType, self.getTouchList(last_move_event, eventType), ev)); + + // remove pointerevent from list + if(framework.Gestures.HAS_POINTEREVENTS && eventType == framework.Gestures.EVENT_END) { + count_touches = framework.Gestures.PointerEvent.updatePointer(eventType, ev); + } + } + + //debug(sourceEventType +" "+ eventType); + + // on the end we reset everything + if(!count_touches) { + last_move_event = null; + enable_detect = false; + touch_triggered = false; + framework.Gestures.PointerEvent.reset(); + } + }); + }, + + + /** + * we have different events for each device/browser + * determine what we need and set them in the framework.Gestures.EVENT_TYPES constant + */ + determineEventTypes: function determineEventTypes() { + // determine the eventtype we want to set + var types; + + // pointerEvents magic + if(framework.Gestures.HAS_POINTEREVENTS) { + types = framework.Gestures.PointerEvent.getEvents(); + } + // on Android, iOS, blackberry, windows mobile we dont want any mouseevents + else if(framework.Gestures.NO_MOUSEEVENTS) { + types = [ + 'touchstart', + 'touchmove', + 'touchend touchcancel']; + } + // for non pointer events browsers and mixed browsers, + // like chrome on windows8 touch laptop + else { + types = [ + 'touchstart mousedown', + 'touchmove mousemove', + 'touchend touchcancel mouseup']; } - if(this.currentGesture) { - // Store this event so we can access it again later - this.currentGesture.lastEvent = eventData; + framework.Gestures.EVENT_TYPES[framework.Gestures.EVENT_START] = types[0]; + framework.Gestures.EVENT_TYPES[framework.Gestures.EVENT_MOVE] = types[1]; + framework.Gestures.EVENT_TYPES[framework.Gestures.EVENT_END] = types[2]; + }, + + + /** + * create touchlist depending on the event + * @param {Object} ev + * @param {String} eventType used by the fakemultitouch plugin + */ + getTouchList: function getTouchList(ev/*, eventType*/) { + // get the fake pointerEvent touchlist + if(framework.Gestures.HAS_POINTEREVENTS) { + return framework.Gestures.PointerEvent.getTouchList(); + } + // get the touchlist + else if(ev.touches) { + return ev.touches; + } + // make fake touchlist from mouse position + else { + ev.indentifier = 1; + return [ev]; + } + }, + + + /** + * collect event data for framework.Gestures js + * @param {HTMLElement} element + * @param {String} eventType like framework.Gestures.EVENT_MOVE + * @param {Object} eventData + */ + collectEventData: function collectEventData(element, eventType, touches, ev) { + + // find out pointerType + var pointerType = framework.Gestures.POINTER_TOUCH; + if(ev.type.match(/mouse/) || framework.Gestures.PointerEvent.matchType(framework.Gestures.POINTER_MOUSE, ev)) { + pointerType = framework.Gestures.POINTER_MOUSE; } - // It's over! - if(e.type === 'touchend' || e.type === 'touchcancel') { - this.endGesture(eventData); + return { + center : framework.Gestures.utils.getCenter(touches), + timeStamp : new Date().getTime(), + 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() { + if(this.srcEvent.preventManipulation) { + this.srcEvent.preventManipulation(); + } + + if(this.srcEvent.preventDefault) { + this.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 framework.Gestures.detection.stopDetect(); + } + }; + } + }; + + framework.Gestures.PointerEvent = { + /** + * holds all pointers + * @type {Object} + */ + pointers: {}, + + /** + * get a list of pointers + * @returns {Array} touchlist + */ + getTouchList: function() { + var self = this; + var touchlist = []; + + // we can use forEach since pointerEvents only is in IE10 + Object.keys(self.pointers).sort().forEach(function(id) { + touchlist.push(self.pointers[id]); + }); + return touchlist; + }, + + /** + * update the position of a pointer + * @param {String} type framework.Gestures.EVENT_END + * @param {Object} pointerEvent + */ + updatePointer: function(type, pointerEvent) { + if(type == framework.Gestures.EVENT_END) { + this.pointers = {}; } + else { + pointerEvent.identifier = pointerEvent.pointerId; + this.pointers[pointerEvent.pointerId] = pointerEvent; + } + + return Object.keys(this.pointers).length; }, - endGesture: function(e) { - this.currentGesture = null; - this._lastMoveEvent = null; + + /** + * check if ev matches pointertype + * @param {String} pointerType framework.Gestures.POINTER_MOUSE + * @param {PointerEvent} ev + */ + matchType: function(pointerType, ev) { + if(!ev.pointerType) { + return false; + } + + var types = {}; + types[framework.Gestures.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == framework.Gestures.POINTER_MOUSE); + types[framework.Gestures.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == framework.Gestures.POINTER_TOUCH); + types[framework.Gestures.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == framework.Gestures.POINTER_PEN); + return types[pointerType]; }, + + /** + * get events + */ + getEvents: function() { + return [ + 'pointerdown MSPointerDown', + 'pointermove MSPointerMove', + 'pointerup pointercancel MSPointerUp MSPointerCancel' + ]; + }, + + /** + * reset the list + */ + reset: function() { + this.pointers = {}; + } + }; + + + framework.Gestures.utils = { + /** + * extend method, + * also used for cloning when dest is an empty object + * @param {Object} dest + * @param {Object} src + * @parm {Boolean} merge do a merge + * @returns {Object} dest + */ + extend: function extend(dest, src, merge) { + for (var key in src) { + if(dest[key] !== undefined && merge) { + continue; + } + dest[key] = src[key]; + } + return dest; + }, + + /** * find if a node is in the given parent * used for event delegation tricks @@ -226,13 +561,13 @@ * @returns {boolean} has_parent */ hasParent: function(node, parent) { - while(node){ - if(node == parent) { - return true; - } - node = node.parentNode; + while(node){ + if(node == parent) { + return true; } - return false; + node = node.parentNode; + } + return false; }, @@ -242,17 +577,17 @@ * @returns {Object} center */ getCenter: function getCenter(touches) { - var valuesX = [], valuesY = []; + var valuesX = [], valuesY = []; - for(var t= 0,len=touches.length; t= y) { - return touch1.pageX - touch2.pageX > 0 ? 'left' : 'right'; - } - else { - return touch1.pageY - touch2.pageY > 0 ? 'up': 'down'; - } + if(x >= y) { + return touch1.pageX - touch2.pageX > 0 ? framework.Gestures.DIRECTION_LEFT : framework.Gestures.DIRECTION_RIGHT; + } + else { + return touch1.pageY - touch2.pageY > 0 ? framework.Gestures.DIRECTION_UP : framework.Gestures.DIRECTION_DOWN; + } }, @@ -310,9 +645,9 @@ * @returns {Number} distance */ getDistance: function getDistance(touch1, touch2) { - var x = touch2.pageX - touch1.pageX, - y = touch2.pageY - touch1.pageY; - return Math.sqrt((x*x) + (y*y)); + var x = touch2.pageX - touch1.pageX, + y = touch2.pageY - touch1.pageY; + return Math.sqrt((x*x) + (y*y)); }, @@ -324,12 +659,12 @@ * @returns {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; + // 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; }, @@ -340,12 +675,12 @@ * @returns {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; + // 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; }, @@ -355,7 +690,7 @@ * @returns {Boolean} is_vertical */ isVertical: function isVertical(direction) { - return (direction == Hammer.DIRECTION_UP || direction == Hammer.DIRECTION_DOWN); + return (direction == framework.Gestures.DIRECTION_UP || direction == framework.Gestures.DIRECTION_DOWN); }, @@ -365,46 +700,721 @@ * @param {Object} css_props */ stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_props) { - var prop, - vendors = ['webkit','khtml','moz','Moz','ms','o','']; + var prop, + vendors = ['webkit','khtml','moz','Moz','ms','o','']; - if(!css_props || !element.style) { - return; - } + if(!css_props || !element.style) { + return; + } - // with css properties for modern browsers - for(var i = 0; i < vendors.length; i++) { - for(var p in css_props) { - if(css_props.hasOwnProperty(p)) { - prop = p; + // with css properties for modern browsers + for(var i = 0; i < vendors.length; i++) { + for(var p in css_props) { + if(css_props.hasOwnProperty(p)) { + prop = p; - // vender prefix at the property - if(vendors[i]) { - prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1); - } - - // set the style - element.style[prop] = css_props[p]; - } + // vender prefix at the property + if(vendors[i]) { + prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1); } - } - // also the disable onselectstart - if(css_props.userSelect == 'none') { - element.onselectstart = function() { - return false; - }; - } - - // and disable ondragstart - if(css_props.userDrag == 'none') { - element.ondragstart = function() { - return false; - }; + // set the style + element.style[prop] = css_props[p]; + } } + } + + // also the disable onselectstart + if(css_props.userSelect == 'none') { + element.onselectstart = function() { + return false; + }; + } } - - } + }; + framework.Gestures.detection = { + // contains all registred framework.Gestures.gestures in the correct order + gestures: [], + + // data of the current framework.Gestures.gesture detection session + current: null, + + // the previous framework.Gestures.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 framework.Gestures.gesture detection + * @param {framework.Gestures.Instance} inst + * @param {Object} eventData + */ + startDetect: function startDetect(inst, eventData) { + // already busy with a framework.Gestures.gesture detection on an element + if(this.current) { + return; + } + + this.stopped = false; + + this.current = { + inst : inst, // reference to framework.GesturesInstance we're working for + startEvent : framework.Gestures.utils.extend({}, eventData), // start eventData for distances, timing etc + lastEvent : false, // last eventData + name : '' // current gesture we're in/detected, can be 'tap', 'hold' etc + }; + + this.detect(eventData); + }, + + + /** + * framework.Gestures.gesture detection + * @param {Object} eventData + */ + detect: function detect(eventData) { + if(!this.current || this.stopped) { + return; + } + + // extend event data with calculations about scale, distance etc + eventData = this.extendEventData(eventData); + + // instance options + var inst_options = this.current.inst.options; + + // call framework.Gestures.gesture handlers + for(var g=0,len=this.gestures.length; g b.index) { + return 1; + } + return 0; + }); + + return this.gestures; + } + }; + + + framework.Gestures.gestures = framework.Gestures.gestures || {}; + + /** + * Custom gestures + * ============================== + * + * Gesture object + * -------------------- + * The object structure of a gesture: + * + * { name: 'mygesture', + * index: 1337, + * defaults: { + * mygesture_option: true + * } + * handler: function(type, ev, inst) { + * // trigger gesture event + * inst.trigger(this.name, ev); + * } + * } + + * @param {String} name + * this should be the name of the gesture, lowercase + * it is also being used to disable/enable the gesture per instance config. + * + * @param {Number} [index=1000] + * the index of the gesture, where it is going to be in the stack of gestures detection + * like when you build an gesture that depends on the drag gesture, it is a good + * idea to place it after the index of the drag gesture. + * + * @param {Object} [defaults={}] + * the default settings of the gesture. these are added to the instance settings, + * and can be overruled per instance. you can also add the name of the gesture, + * but this is also added by default (and set to true). + * + * @param {Function} handler + * this handles the gesture detection of your custom gesture and receives the + * following arguments: + * + * @param {Object} eventData + * event data containing the following properties: + * timeStamp {Number} time the event occurred + * target {HTMLElement} target element + * touches {Array} touches (fingers, pointers, mouse) on the screen + * pointerType {String} kind of pointer that was used. matches framework.Gestures.POINTER_MOUSE|TOUCH + * center {Object} center position of the touches. contains pageX and pageY + * deltaTime {Number} the total time of the touches in the screen + * deltaX {Number} the delta on x axis we haved moved + * deltaY {Number} the delta on y axis we haved moved + * velocityX {Number} the velocity on the x + * velocityY {Number} the velocity on y + * angle {Number} the angle we are moving + * direction {String} the direction we are moving. matches framework.Gestures.DIRECTION_UP|DOWN|LEFT|RIGHT + * distance {Number} the distance we haved moved + * scale {Number} scaling of the touches, needs 2 touches + * rotation {Number} rotation of the touches, needs 2 touches * + * eventType {String} matches framework.Gestures.EVENT_START|MOVE|END + * srcEvent {Object} the source event, like TouchStart or MouseDown * + * startEvent {Object} contains the same properties as above, + * but from the first touch. this is used to calculate + * distances, deltaTime, scaling etc + * + * @param {framework.Gestures.Instance} inst + * the instance we are doing the detection for. you can get the options from + * the inst.options object and trigger the gesture event by calling inst.trigger + * + * + * Handle gestures + * -------------------- + * inside the handler you can get/set framework.Gestures.detection.current. This is the current + * detection session. It has the following properties + * @param {String} name + * contains the name of the gesture we have detected. it has not a real function, + * only to check in other gestures if something is detected. + * like in the drag gesture we set it to 'drag' and in the swipe gesture we can + * check if the current gesture is 'drag' by accessing framework.Gestures.detection.current.name + * + * @readonly + * @param {framework.Gestures.Instance} inst + * the instance we do the detection for + * + * @readonly + * @param {Object} startEvent + * contains the properties of the first gesture detection in this session. + * Used for calculations about timing, distance, etc. + * + * @readonly + * @param {Object} lastEvent + * contains all the properties of the last gesture detect in this session. + * + * after the gesture detection session has been completed (user has released the screen) + * the framework.Gestures.detection.current object is copied into framework.Gestures.detection.previous, + * this is usefull for gestures like doubletap, where you need to know if the + * previous gesture was a tap + * + * options that have been set by the instance can be received by calling inst.options + * + * You can trigger a gesture event by calling inst.trigger("mygesture", event). + * The first param is the name of your gesture, the second the event argument + * + * + * Register gestures + * -------------------- + * When an gesture is added to the framework.Gestures.gestures object, it is auto registered + * at the setup of the first framework.Gestures instance. You can also call framework.Gestures.detection.register + * manually and pass your gesture object as a param + * + */ + + /** + * Hold + * Touch stays at the same place for x time + * @events hold + */ + framework.Gestures.gestures.Hold = { + name: 'hold', + index: 10, + defaults: { + hold_timeout : 500, + hold_threshold : 1 + }, + timer: null, + handler: function holdGesture(ev, inst) { + switch(ev.eventType) { + case framework.Gestures.EVENT_START: + // clear any running timers + clearTimeout(this.timer); + + // set the gesture so we can check in the timeout if it still is + framework.Gestures.detection.current.name = this.name; + + // set timer and if after the timeout it still is hold, + // we trigger the hold event + this.timer = setTimeout(function() { + if(framework.Gestures.detection.current.name == 'hold') { + inst.trigger('hold', ev); + } + }, inst.options.hold_timeout); + break; + + // when you move or end we clear the timer + case framework.Gestures.EVENT_MOVE: + if(ev.distance > inst.options.hold_threshold) { + clearTimeout(this.timer); + } + break; + + case framework.Gestures.EVENT_END: + clearTimeout(this.timer); + break; + } + } + }; + + + /** + * Tap/DoubleTap + * Quick touch at a place or double at the same place + * @events tap, doubletap + */ + framework.Gestures.gestures.Tap = { + name: 'tap', + index: 100, + defaults: { + tap_max_touchtime : 250, + tap_max_distance : 10, + tap_always : true, + doubletap_distance : 20, + doubletap_interval : 300 + }, + handler: function tapGesture(ev, inst) { + if(ev.eventType == framework.Gestures.EVENT_END) { + // previous gesture, for the double tap since these are two different gesture detections + var prev = framework.Gestures.detection.previous, + did_doubletap = false; + + // when the touchtime is higher then the max touch time + // or when the moving distance is too much + if(ev.deltaTime > inst.options.tap_max_touchtime || + ev.distance > inst.options.tap_max_distance) { + return; + } + + // check if double tap + if(prev && prev.name == 'tap' && + (ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval && + ev.distance < inst.options.doubletap_distance) { + inst.trigger('doubletap', ev); + did_doubletap = true; + } + + // do a single tap + if(!did_doubletap || inst.options.tap_always) { + framework.Gestures.detection.current.name = 'tap'; + inst.trigger(framework.Gestures.detection.current.name, ev); + } + } + } + }; + + + /** + * Swipe + * triggers swipe events when the end velocity is above the threshold + * @events swipe, swipeleft, swiperight, swipeup, swipedown + */ + framework.Gestures.gestures.Swipe = { + name: 'swipe', + index: 40, + defaults: { + // set 0 for unlimited, but this can conflict with transform + swipe_max_touches : 1, + swipe_velocity : 0.7 + }, + handler: function swipeGesture(ev, inst) { + if(ev.eventType == framework.Gestures.EVENT_END) { + // max touches + if(inst.options.swipe_max_touches > 0 && + ev.touches.length > inst.options.swipe_max_touches) { + return; + } + + // when the distance we moved is too small we skip this gesture + // or we can be already in dragging + if(ev.velocityX > inst.options.swipe_velocity || + ev.velocityY > inst.options.swipe_velocity) { + // trigger swipe events + inst.trigger(this.name, ev); + inst.trigger(this.name + ev.direction, ev); + } + } + } + }; + + + /** + * Drag + * Move with x fingers (default 1) around on the page. Blocking the scrolling when + * moving left and right is a good practice. When all the drag events are blocking + * you disable scrolling on that area. + * @events drag, drapleft, dragright, dragup, dragdown + */ + framework.Gestures.gestures.Drag = { + name: 'drag', + index: 50, + defaults: { + drag_min_distance : 10, + // Set correct_for_drag_min_distance 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. + correct_for_drag_min_distance : true, + // set 0 for unlimited, but this can conflict with transform + drag_max_touches : 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 + drag_block_horizontal : false, + drag_block_vertical : false, + // drag_lock_to_axis keeps the drag gesture on the axis that it started on, + // It disallows vertical directions if the initial direction was horizontal, and vice versa. + drag_lock_to_axis : false, + // drag lock only kicks in when distance > drag_lock_min_distance + // This way, locking occurs only when the distance has become large enough to reliably determine the direction + drag_lock_min_distance : 25 + }, + triggered: false, + handler: function dragGesture(ev, inst) { + // current gesture isnt drag, but dragged is true + // this means an other gesture is busy. now call dragend + if(framework.Gestures.detection.current.name != this.name && this.triggered) { + inst.trigger(this.name +'end', ev); + this.triggered = false; + return; + } + + // max touches + if(inst.options.drag_max_touches > 0 && + ev.touches.length > inst.options.drag_max_touches) { + return; + } + + switch(ev.eventType) { + case framework.Gestures.EVENT_START: + this.triggered = false; + break; + + case framework.Gestures.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.drag_min_distance && + framework.Gestures.detection.current.name != this.name) { + return; + } + + // we are dragging! + if(framework.Gestures.detection.current.name != this.name) { + framework.Gestures.detection.current.name = this.name; + if (inst.options.correct_for_drag_min_distance) { + // When a drag is triggered, set the event center to drag_min_distance pixels from the original event center. + // Without this correction, the dragged distance would jumpstart at drag_min_distance pixels instead of at 0. + // It might be useful to save the original start point somewhere + var factor = Math.abs(inst.options.drag_min_distance/ev.distance); + framework.Gestures.detection.current.startEvent.center.pageX += ev.deltaX * factor; + framework.Gestures.detection.current.startEvent.center.pageY += ev.deltaY * factor; + + // recalculate event data using new start point + ev = framework.Gestures.detection.extendEventData(ev); + } + } + + // lock drag to axis? + if(framework.Gestures.detection.current.lastEvent.drag_locked_to_axis || (inst.options.drag_lock_to_axis && inst.options.drag_lock_min_distance<=ev.distance)) { + ev.drag_locked_to_axis = true; + } + var last_direction = framework.Gestures.detection.current.lastEvent.direction; + if(ev.drag_locked_to_axis && last_direction !== ev.direction) { + // keep direction on the axis that the drag gesture started on + if(framework.Gestures.utils.isVertical(last_direction)) { + ev.direction = (ev.deltaY < 0) ? framework.Gestures.DIRECTION_UP : framework.Gestures.DIRECTION_DOWN; + } + else { + ev.direction = (ev.deltaX < 0) ? framework.Gestures.DIRECTION_LEFT : framework.Gestures.DIRECTION_RIGHT; + } + } + + // first time, trigger dragstart event + if(!this.triggered) { + inst.trigger(this.name +'start', ev); + this.triggered = true; + } + + // trigger normal event + inst.trigger(this.name, ev); + + // direction event, like dragdown + inst.trigger(this.name + ev.direction, ev); + + // block the browser events + if( (inst.options.drag_block_vertical && framework.Gestures.utils.isVertical(ev.direction)) || + (inst.options.drag_block_horizontal && !framework.Gestures.utils.isVertical(ev.direction))) { + ev.preventDefault(); + } + break; + + case framework.Gestures.EVENT_END: + // trigger dragend + if(this.triggered) { + inst.trigger(this.name +'end', ev); + } + + this.triggered = false; + break; + } + } + }; + + + /** + * Transform + * User want to scale or rotate with 2 fingers + * @events transform, pinch, pinchin, pinchout, rotate + */ + framework.Gestures.gestures.Transform = { + name: 'transform', + index: 45, + defaults: { + // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1 + transform_min_scale : 0.01, + // rotation in degrees + transform_min_rotation : 1, + // prevent default browser behavior when two touches are on the screen + // but it makes the element a blocking element + // when you are using the transform gesture, it is a good practice to set this true + transform_always_block : false + }, + triggered: false, + handler: function transformGesture(ev, inst) { + // current gesture isnt drag, but dragged is true + // this means an other gesture is busy. now call dragend + if(framework.Gestures.detection.current.name != this.name && this.triggered) { + inst.trigger(this.name +'end', ev); + this.triggered = false; + return; + } + + // atleast multitouch + if(ev.touches.length < 2) { + return; + } + + // prevent default when two fingers are on the screen + if(inst.options.transform_always_block) { + ev.preventDefault(); + } + + switch(ev.eventType) { + case framework.Gestures.EVENT_START: + this.triggered = false; + break; + + case framework.Gestures.EVENT_MOVE: + var scale_threshold = Math.abs(1-ev.scale); + var rotation_threshold = Math.abs(ev.rotation); + + // when the distance we moved is too small we skip this gesture + // or we can be already in dragging + if(scale_threshold < inst.options.transform_min_scale && + rotation_threshold < inst.options.transform_min_rotation) { + return; + } + + // we are transforming! + framework.Gestures.detection.current.name = this.name; + + // first time, trigger dragstart event + if(!this.triggered) { + inst.trigger(this.name +'start', ev); + this.triggered = true; + } + + inst.trigger(this.name, ev); // basic transform event + + // trigger rotate event + if(rotation_threshold > inst.options.transform_min_rotation) { + inst.trigger('rotate', ev); + } + + // trigger pinch event + if(scale_threshold > inst.options.transform_min_scale) { + inst.trigger('pinch', ev); + inst.trigger('pinch'+ ((ev.scale < 1) ? 'in' : 'out'), ev); + } + break; + + case framework.Gestures.EVENT_END: + // trigger dragend + if(this.triggered) { + inst.trigger(this.name +'end', ev); + } + + this.triggered = false; + break; + } + } + }; + + + /** + * Touch + * Called as first, tells the user has touched the screen + * @events touch + */ + framework.Gestures.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 + prevent_default: false, + + // disable mouse events, so only touch (or pen!) input triggers events + prevent_mouseevents: false + }, + handler: function touchGesture(ev, inst) { + if(inst.options.prevent_mouseevents && ev.pointerType == framework.Gestures.POINTER_MOUSE) { + ev.stopDetect(); + return; + } + + if(inst.options.prevent_default) { + ev.preventDefault(); + } + + if(ev.eventType == framework.Gestures.EVENT_START) { + inst.trigger(this.name, ev); + } + } + }; + + + /** + * Release + * Called as last, tells the user has released the screen + * @events release + */ + framework.Gestures.gestures.Release = { + name: 'release', + index: Infinity, + handler: function releaseGesture(ev, inst) { + if(ev.eventType == framework.Gestures.EVENT_END) { + inst.trigger(this.name, ev); + } + } + }; })(this, document, FM = this.FM || {});