/** * The gestures module provides gesture events such as "flick", which normalize user interactions * across touch and mouse or pointer based input devices. This layer can be used by application developers * to build input device agnostic components which behave the same in response to either touch or mouse based * interaction. * * <p>Documentation for events added by this module can be found in the event document for the <a href="../classes/YUI.html#events">YUI</a> global.</p> * * @example YUI().use('event-flick', function (Y) { Y.one('#myNode').on('flick', function (e) { Y.log('flick event fired. The event payload has goodies.'); }); }); * * @module event-gestures */ /** * Adds support for a "flick" event, which is fired at the end of a touch or mouse based flick gesture, and provides * velocity of the flick, along with distance and time information. * * <p>Documentation for the flick event can be found on the <a href="../classes/YUI.html#event_flick">YUI</a> global, * along with the other supported events.</p> * * @module event-gestures * @submodule event-flick */ var GESTURE_MAP = Y.Event._GESTURE_MAP, EVENT = { start: GESTURE_MAP.start, end: GESTURE_MAP.end, move: GESTURE_MAP.move }, START = "start", END = "end", MOVE = "move", OWNER_DOCUMENT = "ownerDocument", MIN_VELOCITY = "minVelocity", MIN_DISTANCE = "minDistance", PREVENT_DEFAULT = "preventDefault", _FLICK_START = "_fs", _FLICK_START_HANDLE = "_fsh", _FLICK_END_HANDLE = "_feh", _FLICK_MOVE_HANDLE = "_fmh", NODE_TYPE = "nodeType"; /** * Sets up a "flick" event, that is fired whenever the user initiates a flick gesture on the node * where the listener is attached. The subscriber can specify a minimum distance or velocity for * which the event is to be fired. The subscriber can also specify if there is a particular axis which * they are interested in - "x" or "y". If no axis is specified, the axis along which there was most distance * covered is used. * * <p>It is recommended that you use Y.bind to set up context and additional arguments for your event handler, * however if you want to pass the context and arguments as additional signature arguments to "on", * you need to provide a null value for the configuration object, e.g: <code>node.on("flick", fn, null, context, arg1, arg2, arg3)</code></p> * * @event flick * @for YUI * @param type {string} "flick" * @param fn {function} The method the event invokes. It receives an event facade with an e.flick object containing the flick related properties: e.flick.time, e.flick.distance, e.flick.velocity and e.flick.axis, e.flick.start. * @param cfg {Object} Optional. An object which specifies any of the following: * <dl> * <dt>minDistance (in pixels, defaults to 10)</dt> * <dd>The minimum distance between start and end points, which would qualify the gesture as a flick.</dd> * <dt>minVelocity (in pixels/ms, defaults to 0)</dt> * <dd>The minimum velocity which would qualify the gesture as a flick.</dd> * <dt>preventDefault (defaults to false)</dt> * <dd>Can be set to true/false to prevent default behavior as soon as the touchstart/touchend or mousedown/mouseup is received so that things like scrolling or text selection can be * prevented. This property can also be set to a function, which returns true or false, based on the event facade passed to it.</dd> * <dt>axis (no default)</dt> * <dd>Can be set to "x" or "y" if you want to constrain the flick velocity and distance to a single axis. If not * defined, the axis along which the maximum distance was covered is used.</dd> * </dl> * @return {EventHandle} the detach handle */ Y.Event.define('flick', { on: function (node, subscriber, ce) { var startHandle = node.on(EVENT[START], this._onStart, this, node, subscriber, ce); subscriber[_FLICK_START_HANDLE] = startHandle; }, detach: function (node, subscriber, ce) { var startHandle = subscriber[_FLICK_START_HANDLE], endHandle = subscriber[_FLICK_END_HANDLE]; if (startHandle) { startHandle.detach(); subscriber[_FLICK_START_HANDLE] = null; } if (endHandle) { endHandle.detach(); subscriber[_FLICK_END_HANDLE] = null; } }, processArgs: function(args) { var params = (args.length > 3) ? Y.merge(args.splice(3, 1)[0]) : {}; if (!(MIN_VELOCITY in params)) { params[MIN_VELOCITY] = this.MIN_VELOCITY; } if (!(MIN_DISTANCE in params)) { params[MIN_DISTANCE] = this.MIN_DISTANCE; } if (!(PREVENT_DEFAULT in params)) { params[PREVENT_DEFAULT] = this.PREVENT_DEFAULT; } return params; }, _onStart: function(e, node, subscriber, ce) { var start = true, // always true for mouse endHandle, moveHandle, doc, preventDefault = subscriber._extra.preventDefault, origE = e; if (e.touches) { start = (e.touches.length === 1); e = e.touches[0]; } if (start) { if (preventDefault) { // preventDefault is a boolean or function if (!preventDefault.call || preventDefault(e)) { origE.preventDefault(); } } e.flick = { time : new Date().getTime() }; subscriber[_FLICK_START] = e; endHandle = subscriber[_FLICK_END_HANDLE]; doc = (node.get(NODE_TYPE) === 9) ? node : node.get(OWNER_DOCUMENT); if (!endHandle) { endHandle = doc.on(EVENT[END], Y.bind(this._onEnd, this), null, node, subscriber, ce); subscriber[_FLICK_END_HANDLE] = endHandle; } subscriber[_FLICK_MOVE_HANDLE] = doc.once(EVENT[MOVE], Y.bind(this._onMove, this), null, node, subscriber, ce); } }, _onMove: function(e, node, subscriber, ce) { var start = subscriber[_FLICK_START]; // Start timing from first move. if (start && start.flick) { start.flick.time = new Date().getTime(); } }, _onEnd: function(e, node, subscriber, ce) { var endTime = new Date().getTime(), start = subscriber[_FLICK_START], valid = !!start, endEvent = e, startTime, time, preventDefault, params, xyDistance, distance, velocity, axis, moveHandle = subscriber[_FLICK_MOVE_HANDLE]; if (moveHandle) { moveHandle.detach(); delete subscriber[_FLICK_MOVE_HANDLE]; } if (valid) { if (e.changedTouches) { if (e.changedTouches.length === 1 && e.touches.length === 0) { endEvent = e.changedTouches[0]; } else { valid = false; } } if (valid) { params = subscriber._extra; preventDefault = params[PREVENT_DEFAULT]; if (preventDefault) { // preventDefault is a boolean or function if (!preventDefault.call || preventDefault(e)) { e.preventDefault(); } } startTime = start.flick.time; endTime = new Date().getTime(); time = endTime - startTime; xyDistance = [ endEvent.pageX - start.pageX, endEvent.pageY - start.pageY ]; if (params.axis) { axis = params.axis; } else { axis = (Math.abs(xyDistance[0]) >= Math.abs(xyDistance[1])) ? 'x' : 'y'; } distance = xyDistance[(axis === 'x') ? 0 : 1]; velocity = (time !== 0) ? distance/time : 0; if (isFinite(velocity) && (Math.abs(distance) >= params[MIN_DISTANCE]) && (Math.abs(velocity) >= params[MIN_VELOCITY])) { e.type = "flick"; e.flick = { time:time, distance: distance, velocity:velocity, axis: axis, start : start }; ce.fire(e); } subscriber[_FLICK_START] = null; } } }, MIN_VELOCITY : 0, MIN_DISTANCE : 0, PREVENT_DEFAULT : false });