diff -r d334a616c023 -r e16a97fb364a src/cm/media/js/lib/yui/yui3-3.15.0/build/gesture-simulate/gesture-simulate.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/cm/media/js/lib/yui/yui3-3.15.0/build/gesture-simulate/gesture-simulate.js Mon Mar 10 15:19:48 2014 +0100 @@ -0,0 +1,1321 @@ +YUI.add('gesture-simulate', function (Y, NAME) { + +/** + * Simulate high-level user gestures by generating a set of native DOM events. + * + * @module gesture-simulate + * @requires event-simulate, async-queue, node-screen + */ + +var NAME = "gesture-simulate", + + // phantomjs check may be temporary, until we determine if it really support touch all the way through, like it claims to (http://code.google.com/p/phantomjs/issues/detail?id=375) + SUPPORTS_TOUCH = ((Y.config.win && ("ontouchstart" in Y.config.win)) && !(Y.UA.phantomjs) && !(Y.UA.chrome && Y.UA.chrome < 6)), + + gestureNames = { + tap: 1, + doubletap: 1, + press: 1, + move: 1, + flick: 1, + pinch: 1, + rotate: 1 + }, + + touchEvents = { + touchstart: 1, + touchmove: 1, + touchend: 1, + touchcancel: 1 + }, + + document = Y.config.doc, + emptyTouchList, + + EVENT_INTERVAL = 20, // 20ms + START_PAGEX, // will be adjusted to the node element center + START_PAGEY, // will be adjusted to the node element center + + // defaults that user can override. + DEFAULTS = { + // tap gestures + HOLD_TAP: 10, // 10ms + DELAY_TAP: 10, // 10ms + + // press gesture + HOLD_PRESS: 3000, // 3sec + MIN_HOLD_PRESS: 1000, // 1sec + MAX_HOLD_PRESS: 60000, // 1min + + // move gesture + DISTANCE_MOVE: 200, // 200 pixels + DURATION_MOVE: 1000, // 1sec + MAX_DURATION_MOVE: 5000,// 5sec + + // flick gesture + MIN_VELOCITY_FLICK: 1.3, + DISTANCE_FLICK: 200, // 200 pixels + DURATION_FLICK: 1000, // 1sec + MAX_DURATION_FLICK: 5000,// 5sec + + // pinch/rotation + DURATION_PINCH: 1000 // 1sec + }, + + TOUCH_START = 'touchstart', + TOUCH_MOVE = 'touchmove', + TOUCH_END = 'touchend', + + GESTURE_START = 'gesturestart', + GESTURE_CHANGE = 'gesturechange', + GESTURE_END = 'gestureend', + + MOUSE_UP = 'mouseup', + MOUSE_MOVE = 'mousemove', + MOUSE_DOWN = 'mousedown', + MOUSE_CLICK = 'click', + MOUSE_DBLCLICK = 'dblclick', + + X_AXIS = 'x', + Y_AXIS = 'y'; + + +function Simulations(node) { + if(!node) { + Y.error(NAME+': invalid target node'); + } + this.node = node; + this.target = Y.Node.getDOMNode(node); + + var startXY = this.node.getXY(), + dims = this._getDims(); + + START_PAGEX = startXY[0] + (dims[0])/2; + START_PAGEY = startXY[1] + (dims[1])/2; +} + +Simulations.prototype = { + + /** + * Helper method to convert a degree to a radian. + * + * @method _toRadian + * @private + * @param {Number} deg A degree to be converted to a radian. + * @return {Number} The degree in radian. + */ + _toRadian: function(deg) { + return deg * (Math.PI/180); + }, + + /** + * Helper method to get height/width while accounting for + * rotation/scale transforms where possible by using the + * bounding client rectangle height/width instead of the + * offsetWidth/Height which region uses. + * @method _getDims + * @private + * @return {Array} Array with [height, width] + */ + _getDims : function() { + var region, + width, + height; + + // Ideally, this should be in DOM somewhere. + if (this.target.getBoundingClientRect) { + region = this.target.getBoundingClientRect(); + + if ("height" in region) { + height = region.height; + } else { + // IE7,8 has getBCR, but no height. + height = Math.abs(region.bottom - region.top); + } + + if ("width" in region) { + width = region.width; + } else { + // IE7,8 has getBCR, but no width. + width = Math.abs(region.right - region.left); + } + } else { + region = this.node.get("region"); + width = region.width; + height = region.height; + } + + return [width, height]; + }, + + /** + * Helper method to convert a point relative to the node element into + * the point in the page coordination. + * + * @method _calculateDefaultPoint + * @private + * @param {Array} point A point relative to the node element. + * @return {Array} The same point in the page coordination. + */ + _calculateDefaultPoint: function(point) { + + var height; + + if(!Y.Lang.isArray(point) || point.length === 0) { + point = [START_PAGEX, START_PAGEY]; + } else { + if(point.length == 1) { + height = this._getDims[1]; + point[1] = height/2; + } + // convert to page(viewport) coordination + point[0] = this.node.getX() + point[0]; + point[1] = this.node.getY() + point[1]; + } + + return point; + }, + + /** + * The "rotate" and "pinch" methods are essencially same with the exact same + * arguments. Only difference is the required parameters. The rotate method + * requires "rotation" parameter while the pinch method requires "startRadius" + * and "endRadius" parameters. + * + * @method rotate + * @param {Function} cb The callback to execute when the gesture simulation + * is completed. + * @param {Array} center A center point where the pinch gesture of two fingers + * should happen. It is relative to the top left corner of the target + * node element. + * @param {Number} startRadius A radius of start circle where 2 fingers are + * on when the gesture starts. This is optional. The default is a fourth of + * either target node width or height whichever is smaller. + * @param {Number} endRadius A radius of end circle where 2 fingers will be on when + * the pinch or spread gestures are completed. This is optional. + * The default is a fourth of either target node width or height whichever is less. + * @param {Number} duration A duration of the gesture in millisecond. + * @param {Number} start A start angle(0 degree at 12 o'clock) where the + * gesture should start. Default is 0. + * @param {Number} rotation A rotation in degree. It is required. + */ + rotate: function(cb, center, startRadius, endRadius, duration, start, rotation) { + var radius, + r1 = startRadius, // optional + r2 = endRadius; // optional + + if(!Y.Lang.isNumber(r1) || !Y.Lang.isNumber(r2) || r1<0 || r2<0) { + radius = (this.target.offsetWidth < this.target.offsetHeight)? + this.target.offsetWidth/4 : this.target.offsetHeight/4; + r1 = radius; + r2 = radius; + } + + // required + if(!Y.Lang.isNumber(rotation)) { + Y.error(NAME+'Invalid rotation detected.'); + } + + this.pinch(cb, center, r1, r2, duration, start, rotation); + }, + + /** + * The "rotate" and "pinch" methods are essencially same with the exact same + * arguments. Only difference is the required parameters. The rotate method + * requires "rotation" parameter while the pinch method requires "startRadius" + * and "endRadius" parameters. + * + * The "pinch" gesture can simulate various 2 finger gestures such as pinch, + * spread and/or rotation. The "startRadius" and "endRadius" are required. + * If endRadius is larger than startRadius, it becomes a spread gesture + * otherwise a pinch gesture. + * + * @method pinch + * @param {Function} cb The callback to execute when the gesture simulation + * is completed. + * @param {Array} center A center point where the pinch gesture of two fingers + * should happen. It is relative to the top left corner of the target + * node element. + * @param {Number} startRadius A radius of start circle where 2 fingers are + * on when the gesture starts. This paramenter is required. + * @param {Number} endRadius A radius of end circle where 2 fingers will be on when + * the pinch or spread gestures are completed. This parameter is required. + * @param {Number} duration A duration of the gesture in millisecond. + * @param {Number} start A start angle(0 degree at 12 o'clock) where the + * gesture should start. Default is 0. + * @param {Number} rotation If rotation is desired during the pinch or + * spread gestures, this parameter can be used. Default is 0 degree. + */ + pinch: function(cb, center, startRadius, endRadius, duration, start, rotation) { + var eventQueue, + i, + interval = EVENT_INTERVAL, + touches, + id = 0, + r1 = startRadius, // required + r2 = endRadius, // required + radiusPerStep, + centerX, centerY, + startScale, endScale, scalePerStep, + startRot, endRot, rotPerStep, + path1 = {start: [], end: []}, // paths for 1st and 2nd fingers. + path2 = {start: [], end: []}, + steps, + touchMove; + + center = this._calculateDefaultPoint(center); + + if(!Y.Lang.isNumber(r1) || !Y.Lang.isNumber(r2) || r1<0 || r2<0) { + Y.error(NAME+'Invalid startRadius and endRadius detected.'); + } + + if(!Y.Lang.isNumber(duration) || duration <= 0) { + duration = DEFAULTS.DURATION_PINCH; + } + + if(!Y.Lang.isNumber(start)) { + start = 0.0; + } else { + start = start%360; + while(start < 0) { + start += 360; + } + } + + if(!Y.Lang.isNumber(rotation)) { + rotation = 0.0; + } + + Y.AsyncQueue.defaults.timeout = interval; + eventQueue = new Y.AsyncQueue(); + + // range determination + centerX = center[0]; + centerY = center[1]; + + startRot = start; + endRot = start + rotation; + + // 1st finger path + path1.start = [ + centerX + r1*Math.sin(this._toRadian(startRot)), + centerY - r1*Math.cos(this._toRadian(startRot)) + ]; + path1.end = [ + centerX + r2*Math.sin(this._toRadian(endRot)), + centerY - r2*Math.cos(this._toRadian(endRot)) + ]; + + // 2nd finger path + path2.start = [ + centerX - r1*Math.sin(this._toRadian(startRot)), + centerY + r1*Math.cos(this._toRadian(startRot)) + ]; + path2.end = [ + centerX - r2*Math.sin(this._toRadian(endRot)), + centerY + r2*Math.cos(this._toRadian(endRot)) + ]; + + startScale = 1.0; + endScale = endRadius/startRadius; + + // touch/gesture start + eventQueue.add({ + fn: function() { + var coord1, coord2, coord, touches; + + // coordinate for each touch object. + coord1 = { + pageX: path1.start[0], + pageY: path1.start[1], + clientX: path1.start[0], + clientY: path1.start[1] + }; + coord2 = { + pageX: path2.start[0], + pageY: path2.start[1], + clientX: path2.start[0], + clientY: path2.start[1] + }; + touches = this._createTouchList([Y.merge({ + identifier: (id++) + }, coord1), Y.merge({ + identifier: (id++) + }, coord2)]); + + // coordinate for top level event + coord = { + pageX: (path1.start[0] + path2.start[0])/2, + pageY: (path1.start[0] + path2.start[1])/2, + clientX: (path1.start[0] + path2.start[0])/2, + clientY: (path1.start[0] + path2.start[1])/2 + }; + + this._simulateEvent(this.target, TOUCH_START, Y.merge({ + touches: touches, + targetTouches: touches, + changedTouches: touches, + scale: startScale, + rotation: startRot + }, coord)); + + if(Y.UA.ios >= 2.0) { + /* gesture starts when the 2nd finger touch starts. + * The implementation will fire 1 touch start event for both fingers, + * simulating 2 fingers touched on the screen at the same time. + */ + this._simulateEvent(this.target, GESTURE_START, Y.merge({ + scale: startScale, + rotation: startRot + }, coord)); + } + }, + timeout: 0, + context: this + }); + + // gesture change + steps = Math.floor(duration/interval); + radiusPerStep = (r2 - r1)/steps; + scalePerStep = (endScale - startScale)/steps; + rotPerStep = (endRot - startRot)/steps; + + touchMove = function(step) { + var radius = r1 + (radiusPerStep)*step, + px1 = centerX + radius*Math.sin(this._toRadian(startRot + rotPerStep*step)), + py1 = centerY - radius*Math.cos(this._toRadian(startRot + rotPerStep*step)), + px2 = centerX - radius*Math.sin(this._toRadian(startRot + rotPerStep*step)), + py2 = centerY + radius*Math.cos(this._toRadian(startRot + rotPerStep*step)), + px = (px1+px2)/2, + py = (py1+py2)/2, + coord1, coord2, coord, touches; + + // coordinate for each touch object. + coord1 = { + pageX: px1, + pageY: py1, + clientX: px1, + clientY: py1 + }; + coord2 = { + pageX: px2, + pageY: py2, + clientX: px2, + clientY: py2 + }; + touches = this._createTouchList([Y.merge({ + identifier: (id++) + }, coord1), Y.merge({ + identifier: (id++) + }, coord2)]); + + // coordinate for top level event + coord = { + pageX: px, + pageY: py, + clientX: px, + clientY: py + }; + + this._simulateEvent(this.target, TOUCH_MOVE, Y.merge({ + touches: touches, + targetTouches: touches, + changedTouches: touches, + scale: startScale + scalePerStep*step, + rotation: startRot + rotPerStep*step + }, coord)); + + if(Y.UA.ios >= 2.0) { + this._simulateEvent(this.target, GESTURE_CHANGE, Y.merge({ + scale: startScale + scalePerStep*step, + rotation: startRot + rotPerStep*step + }, coord)); + } + }; + + for (i=0; i < steps; i++) { + eventQueue.add({ + fn: touchMove, + args: [i], + context: this + }); + } + + // gesture end + eventQueue.add({ + fn: function() { + var emptyTouchList = this._getEmptyTouchList(), + coord1, coord2, coord, touches; + + // coordinate for each touch object. + coord1 = { + pageX: path1.end[0], + pageY: path1.end[1], + clientX: path1.end[0], + clientY: path1.end[1] + }; + coord2 = { + pageX: path2.end[0], + pageY: path2.end[1], + clientX: path2.end[0], + clientY: path2.end[1] + }; + touches = this._createTouchList([Y.merge({ + identifier: (id++) + }, coord1), Y.merge({ + identifier: (id++) + }, coord2)]); + + // coordinate for top level event + coord = { + pageX: (path1.end[0] + path2.end[0])/2, + pageY: (path1.end[0] + path2.end[1])/2, + clientX: (path1.end[0] + path2.end[0])/2, + clientY: (path1.end[0] + path2.end[1])/2 + }; + + if(Y.UA.ios >= 2.0) { + this._simulateEvent(this.target, GESTURE_END, Y.merge({ + scale: endScale, + rotation: endRot + }, coord)); + } + + this._simulateEvent(this.target, TOUCH_END, Y.merge({ + touches: emptyTouchList, + targetTouches: emptyTouchList, + changedTouches: touches, + scale: endScale, + rotation: endRot + }, coord)); + }, + context: this + }); + + if(cb && Y.Lang.isFunction(cb)) { + eventQueue.add({ + fn: cb, + + // by default, the callback runs the node context where + // simulateGesture method is called. + context: this.node + + //TODO: Use args to pass error object as 1st param if there is an error. + //args: + }); + } + + eventQueue.run(); + }, + + /** + * The "tap" gesture can be used for various single touch point gestures + * such as single tap, N number of taps, long press. The default is a single + * tap. + * + * @method tap + * @param {Function} cb The callback to execute when the gesture simulation + * is completed. + * @param {Array} point A point(relative to the top left corner of the + * target node element) where the tap gesture should start. The default + * is the center of the taget node. + * @param {Number} times The number of taps. Default is 1. + * @param {Number} hold The hold time in milliseconds between "touchstart" and + * "touchend" event generation. Default is 10ms. + * @param {Number} delay The time gap in millisecond between taps if this + * gesture has more than 1 tap. Default is 10ms. + */ + tap: function(cb, point, times, hold, delay) { + var eventQueue = new Y.AsyncQueue(), + emptyTouchList = this._getEmptyTouchList(), + touches, + coord, + i, + touchStart, + touchEnd; + + point = this._calculateDefaultPoint(point); + + if(!Y.Lang.isNumber(times) || times < 1) { + times = 1; + } + + if(!Y.Lang.isNumber(hold)) { + hold = DEFAULTS.HOLD_TAP; + } + + if(!Y.Lang.isNumber(delay)) { + delay = DEFAULTS.DELAY_TAP; + } + + coord = { + pageX: point[0], + pageY: point[1], + clientX: point[0], + clientY: point[1] + }; + + touches = this._createTouchList([Y.merge({identifier: 0}, coord)]); + + touchStart = function() { + this._simulateEvent(this.target, TOUCH_START, Y.merge({ + touches: touches, + targetTouches: touches, + changedTouches: touches + }, coord)); + }; + + touchEnd = function() { + this._simulateEvent(this.target, TOUCH_END, Y.merge({ + touches: emptyTouchList, + targetTouches: emptyTouchList, + changedTouches: touches + }, coord)); + }; + + for (i=0; i < times; i++) { + eventQueue.add({ + fn: touchStart, + context: this, + timeout: (i === 0)? 0 : delay + }); + + eventQueue.add({ + fn: touchEnd, + context: this, + timeout: hold + }); + } + + if(times > 1 && !SUPPORTS_TOUCH) { + eventQueue.add({ + fn: function() { + this._simulateEvent(this.target, MOUSE_DBLCLICK, coord); + }, + context: this + }); + } + + if(cb && Y.Lang.isFunction(cb)) { + eventQueue.add({ + fn: cb, + + // by default, the callback runs the node context where + // simulateGesture method is called. + context: this.node + + //TODO: Use args to pass error object as 1st param if there is an error. + //args: + }); + } + + eventQueue.run(); + }, + + /** + * The "flick" gesture is a specialized "move" that has some velocity + * and the movement always runs either x or y axis. The velocity is calculated + * with "distance" and "duration" arguments. If the calculated velocity is + * below than the minimum velocity, the given duration will be ignored and + * new duration will be created to make a valid flick gesture. + * + * @method flick + * @param {Function} cb The callback to execute when the gesture simulation + * is completed. + * @param {Array} point A point(relative to the top left corner of the + * target node element) where the flick gesture should start. The default + * is the center of the taget node. + * @param {String} axis Either "x" or "y". + * @param {Number} distance A distance in pixels to flick. + * @param {Number} duration A duration of the gesture in millisecond. + * + */ + flick: function(cb, point, axis, distance, duration) { + var path; + + point = this._calculateDefaultPoint(point); + + if(!Y.Lang.isString(axis)) { + axis = X_AXIS; + } else { + axis = axis.toLowerCase(); + if(axis !== X_AXIS && axis !== Y_AXIS) { + Y.error(NAME+'(flick): Only x or y axis allowed'); + } + } + + if(!Y.Lang.isNumber(distance)) { + distance = DEFAULTS.DISTANCE_FLICK; + } + + if(!Y.Lang.isNumber(duration)){ + duration = DEFAULTS.DURATION_FLICK; // ms + } else { + if(duration > DEFAULTS.MAX_DURATION_FLICK) { + duration = DEFAULTS.MAX_DURATION_FLICK; + } + } + + /* + * Check if too slow for a flick. + * Adjust duration if the calculated velocity is less than + * the minimum velcocity to be claimed as a flick. + */ + if(Math.abs(distance)/duration < DEFAULTS.MIN_VELOCITY_FLICK) { + duration = Math.abs(distance)/DEFAULTS.MIN_VELOCITY_FLICK; + } + + path = { + start: Y.clone(point), + end: [ + (axis === X_AXIS) ? point[0]+distance : point[0], + (axis === Y_AXIS) ? point[1]+distance : point[1] + ] + }; + + this._move(cb, path, duration); + }, + + /** + * The "move" gesture simulate the movement of any direction between + * the straight line of start and end point for the given duration. + * The path argument is an object with "point", "xdist" and "ydist" properties. + * The "point" property is an array with x and y coordinations(relative to the + * top left corner of the target node element) while "xdist" and "ydist" + * properties are used for the distance along the x and y axis. A negative + * distance number can be used to drag either left or up direction. + * + * If no arguments are given, it will simulate the default move, which + * is moving 200 pixels from the center of the element to the positive X-axis + * direction for 1 sec. + * + * @method move + * @param {Function} cb The callback to execute when the gesture simulation + * is completed. + * @param {Object} path An object with "point", "xdist" and "ydist". + * @param {Number} duration A duration of the gesture in millisecond. + */ + move: function(cb, path, duration) { + var convertedPath; + + if(!Y.Lang.isObject(path)) { + path = { + point: this._calculateDefaultPoint([]), + xdist: DEFAULTS.DISTANCE_MOVE, + ydist: 0 + }; + } else { + // convert to the page coordination + if(!Y.Lang.isArray(path.point)) { + path.point = this._calculateDefaultPoint([]); + } else { + path.point = this._calculateDefaultPoint(path.point); + } + + if(!Y.Lang.isNumber(path.xdist)) { + path.xdist = DEFAULTS.DISTANCE_MOVE; + } + + if(!Y.Lang.isNumber(path.ydist)) { + path.ydist = 0; + } + } + + if(!Y.Lang.isNumber(duration)){ + duration = DEFAULTS.DURATION_MOVE; // ms + } else { + if(duration > DEFAULTS.MAX_DURATION_MOVE) { + duration = DEFAULTS.MAX_DURATION_MOVE; + } + } + + convertedPath = { + start: Y.clone(path.point), + end: [path.point[0]+path.xdist, path.point[1]+path.ydist] + }; + + this._move(cb, convertedPath, duration); + }, + + /** + * A base method on top of "move" and "flick" methods. The method takes + * the path with start/end properties and duration to generate a set of + * touch events for the movement gesture. + * + * @method _move + * @private + * @param {Function} cb The callback to execute when the gesture simulation + * is completed. + * @param {Object} path An object with "start" and "end" properties. Each + * property should be an array with x and y coordination (e.g. start: [100, 50]) + * @param {Number} duration A duration of the gesture in millisecond. + */ + _move: function(cb, path, duration) { + var eventQueue, + i, + interval = EVENT_INTERVAL, + steps, stepX, stepY, + id = 0, + touchMove; + + if(!Y.Lang.isNumber(duration)){ + duration = DEFAULTS.DURATION_MOVE; // ms + } else { + if(duration > DEFAULTS.MAX_DURATION_MOVE) { + duration = DEFAULTS.MAX_DURATION_MOVE; + } + } + + if(!Y.Lang.isObject(path)) { + path = { + start: [ + START_PAGEX, + START_PAGEY + ], + end: [ + START_PAGEX + DEFAULTS.DISTANCE_MOVE, + START_PAGEY + ] + }; + } else { + if(!Y.Lang.isArray(path.start)) { + path.start = [ + START_PAGEX, + START_PAGEY + ]; + } + if(!Y.Lang.isArray(path.end)) { + path.end = [ + START_PAGEX + DEFAULTS.DISTANCE_MOVE, + START_PAGEY + ]; + } + } + + Y.AsyncQueue.defaults.timeout = interval; + eventQueue = new Y.AsyncQueue(); + + // start + eventQueue.add({ + fn: function() { + var coord = { + pageX: path.start[0], + pageY: path.start[1], + clientX: path.start[0], + clientY: path.start[1] + }, + touches = this._createTouchList([ + Y.merge({identifier: (id++)}, coord) + ]); + + this._simulateEvent(this.target, TOUCH_START, Y.merge({ + touches: touches, + targetTouches: touches, + changedTouches: touches + }, coord)); + }, + timeout: 0, + context: this + }); + + // move + steps = Math.floor(duration/interval); + stepX = (path.end[0] - path.start[0])/steps; + stepY = (path.end[1] - path.start[1])/steps; + + touchMove = function(step) { + var px = path.start[0]+(stepX * step), + py = path.start[1]+(stepY * step), + coord = { + pageX: px, + pageY: py, + clientX: px, + clientY: py + }, + touches = this._createTouchList([ + Y.merge({identifier: (id++)}, coord) + ]); + + this._simulateEvent(this.target, TOUCH_MOVE, Y.merge({ + touches: touches, + targetTouches: touches, + changedTouches: touches + }, coord)); + }; + + for (i=0; i < steps; i++) { + eventQueue.add({ + fn: touchMove, + args: [i], + context: this + }); + } + + // last move + eventQueue.add({ + fn: function() { + var coord = { + pageX: path.end[0], + pageY: path.end[1], + clientX: path.end[0], + clientY: path.end[1] + }, + touches = this._createTouchList([ + Y.merge({identifier: id}, coord) + ]); + + this._simulateEvent(this.target, TOUCH_MOVE, Y.merge({ + touches: touches, + targetTouches: touches, + changedTouches: touches + }, coord)); + }, + timeout: 0, + context: this + }); + + // end + eventQueue.add({ + fn: function() { + var coord = { + pageX: path.end[0], + pageY: path.end[1], + clientX: path.end[0], + clientY: path.end[1] + }, + emptyTouchList = this._getEmptyTouchList(), + touches = this._createTouchList([ + Y.merge({identifier: id}, coord) + ]); + + this._simulateEvent(this.target, TOUCH_END, Y.merge({ + touches: emptyTouchList, + targetTouches: emptyTouchList, + changedTouches: touches + }, coord)); + }, + context: this + }); + + if(cb && Y.Lang.isFunction(cb)) { + eventQueue.add({ + fn: cb, + + // by default, the callback runs the node context where + // simulateGesture method is called. + context: this.node + + //TODO: Use args to pass error object as 1st param if there is an error. + //args: + }); + } + + eventQueue.run(); + }, + + /** + * Helper method to return a singleton instance of empty touch list. + * + * @method _getEmptyTouchList + * @private + * @return {TouchList | Array} An empty touch list object. + */ + _getEmptyTouchList: function() { + if(!emptyTouchList) { + emptyTouchList = this._createTouchList([]); + } + + return emptyTouchList; + }, + + /** + * Helper method to convert an array with touch points to TouchList object as + * defined in http://www.w3.org/TR/touch-events/ + * + * @method _createTouchList + * @private + * @param {Array} touchPoints + * @return {TouchList | Array} If underlaying platform support creating touch list + * a TouchList object will be returned otherwise a fake Array object + * will be returned. + */ + _createTouchList: function(touchPoints) { + /* + * Android 4.0.3 emulator: + * Native touch api supported starting in version 4.0 (Ice Cream Sandwich). + * However the support seems limited. In Android 4.0.3 emulator, I got + * "TouchList is not defined". + */ + var touches = [], + touchList, + self = this; + + if(!!touchPoints && Y.Lang.isArray(touchPoints)) { + if(Y.UA.android && Y.UA.android >= 4.0 || Y.UA.ios && Y.UA.ios >= 2.0) { + Y.each(touchPoints, function(point) { + if(!point.identifier) {point.identifier = 0;} + if(!point.pageX) {point.pageX = 0;} + if(!point.pageY) {point.pageY = 0;} + if(!point.screenX) {point.screenX = 0;} + if(!point.screenY) {point.screenY = 0;} + + touches.push(document.createTouch(Y.config.win, + self.target, + point.identifier, + point.pageX, point.pageY, + point.screenX, point.screenY)); + }); + + touchList = document.createTouchList.apply(document, touches); + } else if(Y.UA.ios && Y.UA.ios < 2.0) { + Y.error(NAME+': No touch event simulation framework present.'); + } else { + // this will inclide android(Y.UA.android && Y.UA.android < 4.0) + // and desktops among all others. + + /* + * Touch APIs are broken in androids older than 4.0. We will use + * simulated touch apis for these versions. + */ + touchList = []; + Y.each(touchPoints, function(point) { + if(!point.identifier) {point.identifier = 0;} + if(!point.clientX) {point.clientX = 0;} + if(!point.clientY) {point.clientY = 0;} + if(!point.pageX) {point.pageX = 0;} + if(!point.pageY) {point.pageY = 0;} + if(!point.screenX) {point.screenX = 0;} + if(!point.screenY) {point.screenY = 0;} + + touchList.push({ + target: self.target, + identifier: point.identifier, + clientX: point.clientX, + clientY: point.clientY, + pageX: point.pageX, + pageY: point.pageY, + screenX: point.screenX, + screenY: point.screenY + }); + }); + + touchList.item = function(i) { + return touchList[i]; + }; + } + } else { + Y.error(NAME+': Invalid touchPoints passed'); + } + + return touchList; + }, + + /** + * @method _simulateEvent + * @private + * @param {HTMLElement} target The DOM element that's the target of the event. + * @param {String} type The type of event or name of the supported gesture to simulate + * (i.e., "click", "doubletap", "flick"). + * @param {Object} options (Optional) Extra options to copy onto the event object. + * For gestures, options are used to refine the gesture behavior. + */ + _simulateEvent: function(target, type, options) { + var touches; + + if (touchEvents[type]) { + if(SUPPORTS_TOUCH) { + Y.Event.simulate(target, type, options); + } else { + // simulate using mouse events if touch is not applicable on this platform. + // but only single touch event can be simulated. + if(this._isSingleTouch(options.touches, options.targetTouches, options.changedTouches)) { + type = { + touchstart: MOUSE_DOWN, + touchmove: MOUSE_MOVE, + touchend: MOUSE_UP + }[type]; + + options.button = 0; + options.relatedTarget = null; // since we are not using mouseover event. + + // touchend has none in options.touches. + touches = (type === MOUSE_UP)? options.changedTouches : options.touches; + + options = Y.mix(options, { + screenX: touches.item(0).screenX, + screenY: touches.item(0).screenY, + clientX: touches.item(0).clientX, + clientY: touches.item(0).clientY + }, true); + + Y.Event.simulate(target, type, options); + + if(type == MOUSE_UP) { + Y.Event.simulate(target, MOUSE_CLICK, options); + } + } else { + Y.error("_simulateEvent(): Event '" + type + "' has multi touch objects that can't be simulated in your platform."); + } + } + } else { + // pass thru for all non touch events + Y.Event.simulate(target, type, options); + } + }, + + /** + * Helper method to check the single touch. + * @method _isSingleTouch + * @private + * @param {TouchList} touches + * @param {TouchList} targetTouches + * @param {TouchList} changedTouches + */ + _isSingleTouch: function(touches, targetTouches, changedTouches) { + return (touches && (touches.length <= 1)) && + (targetTouches && (targetTouches.length <= 1)) && + (changedTouches && (changedTouches.length <= 1)); + } +}; + +/* + * A gesture simulation class. + */ +Y.GestureSimulation = Simulations; + +/* + * Various simulation default behavior properties. If user override + * Y.GestureSimulation.defaults, overriden values will be used and this + * should be done before the gesture simulation. + */ +Y.GestureSimulation.defaults = DEFAULTS; + +/* + * The high level gesture names that YUI knows how to simulate. + */ +Y.GestureSimulation.GESTURES = gestureNames; + +/** + * Simulates the higher user level gesture of the given name on a target. + * This method generates a set of low level touch events(Apple specific gesture + * events as well for the iOS platforms) asynchronously. Note that gesture + * simulation is relying on `Y.Event.simulate()` method to generate + * the touch events under the hood. The `Y.Event.simulate()` method + * itself is a synchronous method. + * + * Users are suggested to use `Node.simulateGesture()` method which + * basically calls this method internally. Supported gestures are `tap`, + * `doubletap`, `press`, `move`, `flick`, `pinch` and `rotate`. + * + * The `pinch` gesture is used to simulate the pinching and spreading of two + * fingers. During a pinch simulation, rotation is also possible. Essentially + * `pinch` and `rotate` simulations share the same base implementation to allow + * both pinching and rotation at the same time. The only difference is `pinch` + * requires `start` and `end` option properties while `rotate` requires `rotation` + * option property. + * + * The `pinch` and `rotate` gestures can be described as placing 2 fingers along a + * circle. Pinching and spreading can be described by start and end circles while + * rotation occurs on a single circle. If the radius of the start circle is greater + * than the end circle, the gesture becomes a pinch, otherwise it is a spread spread. + * + * @example + * + * var node = Y.one("#target"); + * + * // double tap example + * node.simulateGesture("doubletap", function() { + * // my callback function + * }); + * + * // flick example from the center of the node, move 50 pixels down for 50ms) + * node.simulateGesture("flick", { + * axis: y, + * distance: -100 + * duration: 50 + * }, function() { + * // my callback function + * }); + * + * // simulate rotating a node 75 degrees counter-clockwise + * node.simulateGesture("rotate", { + * rotation: -75 + * }); + * + * // simulate a pinch and a rotation at the same time. + * // fingers start on a circle of radius 100 px, placed at top/bottom + * // fingers end on a circle of radius 50px, placed at right/left + * node.simulateGesture("pinch", { + * r1: 100, + * r2: 50, + * start: 0 + * rotation: 90 + * }); + * + * @method simulateGesture + * @param {HTMLElement|Node} node The YUI node or HTML element that's the target + * of the event. + * @param {String} name The name of the supported gesture to simulate. The + * supported gesture name is one of "tap", "doubletap", "press", "move", + * "flick", "pinch" and "rotate". + * @param {Object} [options] Extra options used to define the gesture behavior: + * + * Valid options properties for the `tap` gesture: + * + * @param {Array} [options.point] (Optional) Indicates the [x,y] coordinates + * where the tap should be simulated. Default is the center of the node + * element. + * @param {Number} [options.hold=10] (Optional) The hold time in milliseconds. + * This is the time between `touchstart` and `touchend` event generation. + * @param {Number} [options.times=1] (Optional) Indicates the number of taps. + * @param {Number} [options.delay=10] (Optional) The number of milliseconds + * before the next tap simulation happens. This is valid only when `times` + * is more than 1. + * + * Valid options properties for the `doubletap` gesture: + * + * @param {Array} [options.point] (Optional) Indicates the [x,y] coordinates + * where the doubletap should be simulated. Default is the center of the + * node element. + * + * Valid options properties for the `press` gesture: + * + * @param {Array} [options.point] (Optional) Indicates the [x,y] coordinates + * where the press should be simulated. Default is the center of the node + * element. + * @param {Number} [options.hold=3000] (Optional) The hold time in milliseconds. + * This is the time between `touchstart` and `touchend` event generation. + * Default is 3000ms (3 seconds). + * + * Valid options properties for the `move` gesture: + * + * @param {Object} [options.path] (Optional) Indicates the path of the finger + * movement. It's an object with three optional properties: `point`, + * `xdist` and `ydist`. + * @param {Array} [options.path.point] A starting point of the gesture. + * Default is the center of the node element. + * @param {Number} [options.path.xdist=200] A distance to move in pixels + * along the X axis. A negative distance value indicates moving left. + * @param {Number} [options.path.ydist=0] A distance to move in pixels + * along the Y axis. A negative distance value indicates moving up. + * @param {Number} [options.duration=1000] (Optional) The duration of the + * gesture in milliseconds. + * + * Valid options properties for the `flick` gesture: + * + * @param {Array} [options.point] (Optional) Indicates the [x, y] coordinates + * where the flick should be simulated. Default is the center of the + * node element. + * @param {String} [options.axis='x'] (Optional) Valid values are either + * "x" or "y". Indicates axis to move along. The flick can move to one of + * 4 directions(left, right, up and down). + * @param {Number} [options.distance=200] (Optional) Distance to move in pixels + * @param {Number} [options.duration=1000] (Optional) The duration of the + * gesture in milliseconds. User given value could be automatically + * adjusted by the framework if it is below the minimum velocity to be + * a flick gesture. + * + * Valid options properties for the `pinch` gesture: + * + * @param {Array} [options.center] (Optional) The center of the circle where + * two fingers are placed. Default is the center of the node element. + * @param {Number} [options.r1] (Required) Pixel radius of the start circle + * where 2 fingers will be on when the gesture starts. The circles are + * centered at the center of the element. + * @param {Number} [options.r2] (Required) Pixel radius of the end circle + * when this gesture ends. + * @param {Number} [options.duration=1000] (Optional) The duration of the + * gesture in milliseconds. + * @param {Number} [options.start=0] (Optional) Starting degree of the first + * finger. The value is relative to the path of the north. Default is 0 + * (i.e., 12:00 on a clock). + * @param {Number} [options.rotation=0] (Optional) Degrees to rotate from + * the starting degree. A negative value means rotation to the + * counter-clockwise direction. + * + * Valid options properties for the `rotate` gesture: + * + * @param {Array} [options.center] (Optional) The center of the circle where + * two fingers are placed. Default is the center of the node element. + * @param {Number} [options.r1] (Optional) Pixel radius of the start circle + * where 2 fingers will be on when the gesture starts. The circles are + * centered at the center of the element. Default is a fourth of the node + * element width or height, whichever is smaller. + * @param {Number} [options.r2] (Optional) Pixel radius of the end circle + * when this gesture ends. Default is a fourth of the node element width or + * height, whichever is smaller. + * @param {Number} [options.duration=1000] (Optional) The duration of the + * gesture in milliseconds. + * @param {Number} [options.start=0] (Optional) Starting degree of the first + * finger. The value is relative to the path of the north. Default is 0 + * (i.e., 12:00 on a clock). + * @param {Number} [options.rotation] (Required) Degrees to rotate from + * the starting degree. A negative value means rotation to the + * counter-clockwise direction. + * + * @param {Function} [cb] The callback to execute when the asynchronouse gesture + * simulation is completed. + * @param {Error} cb.err An error object if the simulation is failed. + * @for Event + * @static + */ +Y.Event.simulateGesture = function(node, name, options, cb) { + + node = Y.one(node); + + var sim = new Y.GestureSimulation(node); + name = name.toLowerCase(); + + if(!cb && Y.Lang.isFunction(options)) { + cb = options; + options = {}; + } + + options = options || {}; + + if (gestureNames[name]) { + switch(name) { + // single-touch: point gestures + case 'tap': + sim.tap(cb, options.point, options.times, options.hold, options.delay); + break; + case 'doubletap': + sim.tap(cb, options.point, 2); + break; + case 'press': + if(!Y.Lang.isNumber(options.hold)) { + options.hold = DEFAULTS.HOLD_PRESS; + } else if(options.hold < DEFAULTS.MIN_HOLD_PRESS) { + options.hold = DEFAULTS.MIN_HOLD_PRESS; + } else if(options.hold > DEFAULTS.MAX_HOLD_PRESS) { + options.hold = DEFAULTS.MAX_HOLD_PRESS; + } + sim.tap(cb, options.point, 1, options.hold); + break; + + // single-touch: move gestures + case 'move': + sim.move(cb, options.path, options.duration); + break; + case 'flick': + sim.flick(cb, options.point, options.axis, options.distance, + options.duration); + break; + + // multi-touch: pinch/rotation gestures + case 'pinch': + sim.pinch(cb, options.center, options.r1, options.r2, + options.duration, options.start, options.rotation); + break; + case 'rotate': + sim.rotate(cb, options.center, options.r1, options.r2, + options.duration, options.start, options.rotation); + break; + } + } else { + Y.error(NAME+': Not a supported gesture simulation: '+name); + } +}; + + +}, '@VERSION@', {"requires": ["async-queue", "event-simulate", "node-screen"]});