/** * 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); } };