//>>built define("dojox/gesture/Base", [ "dojo/_base/kernel", "dojo/_base/declare", "dojo/_base/array", "dojo/_base/lang", "dojo/dom", "dojo/on", "dojo/touch", "dojo/has", "../main" ], function(kernel, declare, array, lang, dom, on, touch, has, dojox){ // module: // dojox/gesture/Base // summary: // This module provides an abstract parental class for various gesture implementations. /*===== dojox.gesture.Base = { // summary: // An abstract parental class for various gesture implementations. // // It's mainly responsible for: // // 1. Binding on() listening handlers for supported gesture events. // // 2. Monitoring underneath events and process different phases - 'press'|'move'|'release'|'cancel'. // // 3. Firing and bubbling gesture events with on() API. // // A gesture implementation only needs to extend this class and overwrite appropriate phase handlers: // // - press()|move()|release()|cancel for recognizing and firing gestures // // example: // 1. A typical gesture implementation. // // Suppose we have dojox/gesture/a which provides 3 gesture events:"a", "a.x", "a.y" to be used as: // | dojo.connect(node, dojox.gesture.a, function(e){}); // | dojo.connect(node, dojox.gesture.a.x, function(e){}); // | dojo.connect(node, dojox.gesture.a.y, function(e){}); // // The definition of the gesture "a" may look like: // | define([..., "./Base"], function(..., Base){ // | var clz = declare(Base, { // | defaultEvent: "a", // | // | subEvents: ["x", "y"], // | // | press: function(data, e){ // | this.fire(node, {type: "a.x", ...}); // | }, // | move: function(data, e){ // | this.fire(node, {type: "a.y", ...}); // | }, // | release: function(data, e){ // | this.fire(node, {type: "a", ...}); // | }, // | cancel: function(data, e){ // | // clean up // | } // | }); // | // | // in order to have a default instance for handy use // | dojox.gesture.a = new clz(); // | // | // so that we can create new instances like // | // var mine = new dojox.gesture.a.A({...}) // | dojox.gesture.a.A = clz; // | // | return dojox.gesture.a; // | }); // // 2. A gesture can be used in the following ways(taking dojox.gestre.tap for example): // // A. Used with dojo.connect() // | dojo.connect(node, dojox.gesture.tap, function(e){}); // | dojo.connect(node, dojox.gesture.tap.hold, function(e){}); // | dojo.connect(node, dojox.gesture.tap.doubletap, function(e){}); // // B. Used with dojo.on // | define(["dojo/on", "dojox/gesture/tap"], function(on, tap){ // | on(node, tap, function(e){}); // | on(node, tap.hold, function(e){}); // | on(node, tap.doubletap, function(e){}); // // C. Used with dojox.gesture.tap directly // | dojox.gesture.tap(node, function(e){}); // | dojox.gesture.tap.hold(node, function(e){}); // | dojox.gesture.tap.doubletap(node, function(e){}); // // Though there is always a default gesture instance after being required, e.g // | require(["dojox/gesture/tap"], function(){...}); // // It's possible to create a new one with different parameter setting: // | var myTap = new dojox.gesture.tap.Tap({holdThreshold: 300}); // | dojo.connect(node, myTap, function(e){}); // | dojo.connect(node, myTap.hold, function(e){}); // | dojo.connect(node, myTap.doubletap, function(e){}); // // Please refer to dojox/gesture/ for more gesture usages }; =====*/ kernel.experimental("dojox.gesture.Base"); lang.getObject("gesture", true, dojox); // Declare an internal anonymous class which will only be exported by module return value return declare(/*===== "dojox.gesture.Base", =====*/null, { // defaultEvent: [readonly] String // Default event e.g. 'tap' is a default event of dojox.gesture.tap defaultEvent: " ", // subEvents: [readonly] Array // A list of sub events e.g ['hold', 'doubletap'], // used by being combined with defaultEvent like 'tap.hold', 'tap.doubletap' etc. subEvents: [], // touchOnly: boolean // Whether the gesture is touch-device only touchOnly : false, // _elements: Array // List of elements that wraps target node and gesture data _elements: null, /*===== // _lock: Dom // The dom node whose descendants are all locked for processing _lock: null, // _events: [readonly] Array // The complete list of supported gesture events with full name space // e.g ['tap', 'tap.hold', 'tap.doubletap'] _events: null, =====*/ constructor: function(args){ lang.mixin(this, args); this.init(); }, init: function(){ // summary: // Initialization works this._elements = []; if(!has("touch") && this.touchOnly){ console.warn("Gestures:[", this.defaultEvent, "] is only supported on touch devices!"); return; } // bind on() handlers for various events var evt = this.defaultEvent; this.call = this._handle(evt); this._events = [evt]; array.forEach(this.subEvents, function(subEvt){ this[subEvt] = this._handle(evt + '.' + subEvt); this._events.push(evt + '.' + subEvt); }, this); }, _handle: function(/*String*/eventType){ // summary: // Bind listen handler for the given gesture event(e.g. 'tap', 'tap.hold' etc.) // the returned handle will be used internally by dojo/on var self = this; //called by dojo/on return function(node, listener){ // normalize, arguments might be (null, node, listener) var a = arguments; if(a.length > 2){ node = a[1]; listener = a[2]; } var isNode = node && (node.nodeType || node.attachEvent || node.addEventListener); if(!isNode){ return on(node, eventType, listener); }else{ var onHandle = self._add(node, eventType, listener); // FIXME - users are supposed to explicitly call either // disconnect(signal) or signal.remove() to release resources var signal = { remove: function(){ onHandle.remove(); self._remove(node, eventType); } }; return signal; } }; // dojo/on handle }, _add: function(/*Dom*/node, /*String*/type, /*function*/listener){ // summary: // Bind dojo/on handlers for both gesture event(e.g 'tab.hold') // and underneath 'press'|'move'|'release' events var element = this._getGestureElement(node); if(!element){ // the first time listening to the node element = { target: node, data: {}, handles: {} }; var _press = lang.hitch(this, "_process", element, "press"); var _move = lang.hitch(this, "_process", element, "move"); var _release = lang.hitch(this, "_process", element, "release"); var _cancel = lang.hitch(this, "_process", element, "cancel"); var handles = element.handles; if(this.touchOnly){ handles.press = on(node, 'touchstart', _press); handles.move = on(node, 'touchmove', _move); handles.release = on(node, 'touchend', _release); handles.cancel = on(node, 'touchcancel', _cancel); }else{ handles.press = touch.press(node, _press); handles.move = touch.move(node, _move); handles.release = touch.release(node, _release); handles.cancel = touch.cancel(node, _cancel); } this._elements.push(element); } // track num of listeners for the gesture event - type // so that we can release element if no more gestures being monitored element.handles[type] = !element.handles[type] ? 1 : ++element.handles[type]; return on(node, type, listener); //handle }, _getGestureElement: function(/*Dom*/node){ // summary: // Obtain a gesture element for the give node var i = 0, element; for(; i < this._elements.length; i++){ element = this._elements[i]; if(element.target === node){ return element; } } }, _process: function(element, phase, e){ // summary: // Process and dispatch to appropriate phase handlers. // Also provides the machinery for managing gesture bubbling. // description: // 1. e._locking is used to make sure only the most inner node // will be processed for the same gesture, suppose we have: // | on(inner, dojox.gesture.tap, func1); // | on(outer, dojox.gesture.tap, func2); // only the inner node will be processed by tap gesture, once matched, // the 'tap' event will be bubbled up from inner to outer, dojo.StopEvent(e) // can be used at any level to stop the 'tap' event. // // 2. Once a node starts being processed, all it's descendant nodes will be locked. // The same gesture won't be processed on its descendant nodes until the lock is released. // element: Object // Gesture element // phase: String // Phase of a gesture to be processed, might be 'press'|'move'|'release'|'cancel' // e: Event // Native event e._locking = e._locking || {}; if(e._locking[this.defaultEvent] || this.isLocked(e.currentTarget)){ return; } // invoking gesture.press()|move()|release()|cancel() e.preventDefault(); e._locking[this.defaultEvent] = true; this[phase](element.data, e); }, press: function(data, e){ // summary: // Process the 'press' phase of a gesture }, move: function(data, e){ // summary: // Process the 'move' phase of a gesture }, release: function(data, e){ // summary: // Process the 'release' phase of a gesture }, cancel: function(data, e){ // summary: // Process the 'cancel' phase of a gesture }, fire: function(node, event){ // summary: // Fire a gesture event and invoke registered listeners // a simulated GestureEvent will also be sent along // node: DomNode // Target node to fire the gesture // event: Object // An object containing specific gesture info e.g {type: 'tap.hold'|'swipe.left'), ...} // all these properties will be put into a simulated GestureEvent when fired. // Note - Default properties in a native Event won't be overwritten, see on.emit() for more details. if(!node || !event){ return; } event.bubbles = true; event.cancelable = true; on.emit(node, event.type, event); }, _remove: function(/*Dom*/node, /*String*/type){ // summary: // Check and remove underneath handlers if node // is not being listened for 'this' gesture anymore, // this happens when user removed all previous on() handlers. var element = this._getGestureElement(node); if(!element || !element.handles){ return; } element.handles[type]--; var handles = element.handles; if(!array.some(this._events, function(evt){ return handles[evt] > 0; })){ // clean up if node is not being listened anymore this._cleanHandles(handles); var i = array.indexOf(this._elements, element); if(i >= 0){ this._elements.splice(i, 1); } } }, _cleanHandles: function(/*Object*/handles){ // summary: // Clean up on handles for(var x in handles){ //remove handles for "press"|"move"|"release"|"cancel" if(handles[x].remove){ handles[x].remove(); } delete handles[x]; } }, lock: function(/*Dom*/node){ // summary: // Lock all descendants of the node. // tags: // protected this._lock = node; }, unLock: function(){ // summary: // Release the lock // tags: // protected this._lock = null; }, isLocked: function(node){ // summary: // Check if the node is locked, isLocked(node) means // whether it's a descendant of the currently locked node. // tags: // protected if(!this._lock || !node){ return false; } return this._lock !== node && dom.isDescendant(node, this._lock); }, destroy: function(){ // summary: // Release all handlers and resources array.forEach(this._elements, function(element){ this._cleanHandles(element.handles); }, this); this._elements = null; } }); });