2 Copyright (c) 2010, Yahoo! Inc. All rights reserved.
3 Code licensed under the BSD License:
4 http://developer.yahoo.com/yui/license.html
8 YUI.add('event-flick', function(Y) {
11 * The gestures module provides gesture events such as "flick", which normalize user interactions
12 * across touch and mouse or pointer based input devices. This layer can be used by application developers
13 * to build input device agnostic components which behave the same in response to either touch or mouse based
16 * <p>Documentation for events added by this module can be found in the event document for the <a href="YUI.html#events">YUI</a> global.</p>
18 * @module event-gestures
22 * Adds support for a "flick" event, which is fired at the end of a touch or mouse based flick gesture, and provides
23 * velocity of the flick, along with distance and time information.
25 * @module event-gestures
26 * @submodule event-flick
29 var EVENT = ("ontouchstart" in Y.config.win && !Y.UA.chrome) ? {
40 OWNER_DOCUMENT = "ownerDocument",
41 MIN_VELOCITY = "minVelocity",
42 MIN_DISTANCE = "minDistance",
43 PREVENT_DEFAULT = "preventDefault",
46 _FLICK_START_HANDLE = "_fsh",
47 _FLICK_END_HANDLE = "_feh",
49 NODE_TYPE = "nodeType";
52 * Sets up a "flick" event, that is fired whenever the user initiates a flick gesture on the node
53 * where the listener is attached. The subscriber can specify a minimum distance or velocity for
54 * which the event is to be fired. The subscriber can also specify if there is a particular axis which
55 * they are interested in - "x" or "y". If no axis is specified, the axis along which there was most distance
58 * <p>It is recommended that you use Y.bind to set up context and additional arguments for your event handler,
59 * however if you want to pass the context and arguments as additional signature arguments to "on",
60 * 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>
64 * @param type {string} "flick"
65 * @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.
66 * @param cfg {Object} Optional. An object which specifies any of the following:
68 * <dt>minDistance (in pixels, defaults to 10)</dt>
69 * <dd>The minimum distance between start and end points, which would qualify the gesture as a flick.</dd>
70 * <dt>minVelocity (in pixels/ms, defaults to 0)</dt>
71 * <dd>The minimum velocity which would qualify the gesture as a flick.</dd>
72 * <dt>preventDefault (defaults to false)</dt>
73 * <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
74 * prevented. This property can also be set to a function, which returns true or false, based on the event facade passed to it.</dd>
75 * <dt>axis (no default)</dt>
76 * <dd>Can be set to "x" or "y" if you want to constrain the flick velocity and distance to a single axis. If not
77 * defined, the axis along which the maximum distance was covered is used.</dd>
79 * @return {EventHandle} the detach handle
82 Y.Event.define('flick', {
84 on: function (node, subscriber, ce) {
86 var startHandle = node.on(EVENT[START],
93 subscriber[_FLICK_START_HANDLE] = startHandle;
96 detach: function (node, subscriber, ce) {
98 var startHandle = subscriber[_FLICK_START_HANDLE],
99 endHandle = subscriber[_FLICK_END_HANDLE];
102 startHandle.detach();
103 subscriber[_FLICK_START_HANDLE] = null;
108 subscriber[_FLICK_END_HANDLE] = null;
112 processArgs: function(args) {
113 var params = (args.length > 3) ? Y.merge(args.splice(3, 1)[0]) : {};
115 if (!(MIN_VELOCITY in params)) {
116 params[MIN_VELOCITY] = this.MIN_VELOCITY;
119 if (!(MIN_DISTANCE in params)) {
120 params[MIN_DISTANCE] = this.MIN_DISTANCE;
123 if (!(PREVENT_DEFAULT in params)) {
124 params[PREVENT_DEFAULT] = this.PREVENT_DEFAULT;
130 _onStart: function(e, node, subscriber, ce) {
132 var start = true, // always true for mouse
135 preventDefault = subscriber._extra.preventDefault,
139 start = (e.touches.length === 1);
145 if (preventDefault) {
146 // preventDefault is a boolean or function
147 if (!preventDefault.call || preventDefault(e)) {
148 origE.preventDefault();
153 time : new Date().getTime()
156 subscriber[_FLICK_START] = e;
158 endHandle = subscriber[_FLICK_END_HANDLE];
161 doc = (node.get(NODE_TYPE) === 9) ? node : node.get(OWNER_DOCUMENT);
163 endHandle = doc.on(EVENT[END], Y.bind(this._onEnd, this), null, node, subscriber, ce);
164 subscriber[_FLICK_END_HANDLE] = endHandle;
169 _onEnd: function(e, node, subscriber, ce) {
171 var endTime = new Date().getTime(),
172 start = subscriber[_FLICK_START],
186 if (e.changedTouches) {
187 if (e.changedTouches.length === 1 && e.touches.length === 0) {
188 endEvent = e.changedTouches[0];
196 params = subscriber._extra;
197 preventDefault = params[PREVENT_DEFAULT];
199 if (preventDefault) {
200 // preventDefault is a boolean or function
201 if (!preventDefault.call || preventDefault(e)) {
202 endEvent.preventDefault();
206 startTime = start.flick.time;
207 endTime = new Date().getTime();
208 time = endTime - startTime;
212 endEvent.pageX - start.pageX,
213 endEvent.pageY - start.pageY
219 axis = (Math.abs(xyDistance[0]) >= Math.abs(xyDistance[1])) ? 'x' : 'y';
222 distance = xyDistance[(axis === 'x') ? 0 : 1];
223 velocity = (time !== 0) ? distance/time : 0;
225 if (isFinite(velocity) && (Math.abs(distance) >= params[MIN_DISTANCE]) && (Math.abs(velocity) >= params[MIN_VELOCITY])) {
240 subscriber[_FLICK_START] = null;
247 PREVENT_DEFAULT : false
251 }, '3.3.0' ,{requires:['node-base','event-touch','event-synthetic']});
252 YUI.add('event-move', function(Y) {
255 * Adds lower level support for "gesturemovestart", "gesturemove" and "gesturemoveend" events, which can be used to create drag/drop
256 * interactions which work across touch and mouse input devices. They correspond to "touchstart", "touchmove" and "touchend" on a touch input
257 * device, and "mousedown", "mousemove", "mouseup" on a mouse based input device.
259 * @module event-gestures
260 * @submodule event-move
263 var EVENT = ("ontouchstart" in Y.config.win && !Y.UA.chrome) ? {
277 GESTURE_MOVE = "gesture" + MOVE,
278 GESTURE_MOVE_END = GESTURE_MOVE + END,
279 GESTURE_MOVE_START = GESTURE_MOVE + START,
281 _MOVE_START_HANDLE = "_msh",
282 _MOVE_HANDLE = "_mh",
283 _MOVE_END_HANDLE = "_meh",
285 _DEL_MOVE_START_HANDLE = "_dmsh",
286 _DEL_MOVE_HANDLE = "_dmh",
287 _DEL_MOVE_END_HANDLE = "_dmeh",
292 MIN_TIME = "minTime",
293 MIN_DISTANCE = "minDistance",
294 PREVENT_DEFAULT = "preventDefault",
296 OWNER_DOCUMENT = "ownerDocument",
298 CURRENT_TARGET = "currentTarget",
301 NODE_TYPE = "nodeType",
303 _defArgsProcessor = function(se, args, delegate) {
304 var iConfig = (delegate) ? 4 : 3,
305 config = (args.length > iConfig) ? Y.merge(args.splice(iConfig,1)[0]) : {};
307 if (!(PREVENT_DEFAULT in config)) {
308 config[PREVENT_DEFAULT] = se.PREVENT_DEFAULT;
314 _getRoot = function(node, subscriber) {
315 return subscriber._extra.root || (node.get(NODE_TYPE) === 9) ? node : node.get(OWNER_DOCUMENT);
318 _normTouchFacade = function(touchFacade, touch, params) {
319 touchFacade.pageX = touch.pageX;
320 touchFacade.pageY = touch.pageY;
321 touchFacade.screenX = touch.screenX;
322 touchFacade.screenY = touch.screenY;
323 touchFacade.clientX = touch.clientX;
324 touchFacade.clientY = touch.clientY;
325 touchFacade[TARGET] = touchFacade[TARGET] || touch[TARGET];
326 touchFacade[CURRENT_TARGET] = touchFacade[CURRENT_TARGET] || touch[CURRENT_TARGET];
328 touchFacade[BUTTON] = (params && params[BUTTON]) || 1; // default to left (left as per vendors, not W3C which is 0)
331 _prevent = function(e, preventDefault) {
332 if (preventDefault) {
333 // preventDefault is a boolean or a function
334 if (!preventDefault.call || preventDefault(e)) {
340 define = Y.Event.define;
343 * Sets up a "gesturemovestart" event, that is fired on touch devices in response to a single finger "touchstart",
344 * and on mouse based devices in response to a "mousedown". The subscriber can specify the minimum time
345 * and distance thresholds which should be crossed before the "gesturemovestart" is fired and for the mouse,
346 * which button should initiate a "gesturemovestart". This event can also be listened for using node.delegate().
348 * <p>It is recommended that you use Y.bind to set up context and additional arguments for your event handler,
349 * however if you want to pass the context and arguments as additional signature arguments to on/delegate,
350 * you need to provide a null value for the configuration object, e.g: <code>node.on("gesturemovestart", fn, null, context, arg1, arg2, arg3)</code></p>
352 * @event gesturemovestart
354 * @param type {string} "gesturemovestart"
355 * @param fn {function} The method the event invokes. It receives the event facade of the underlying DOM event (mousedown or touchstart.touches[0]) which contains position co-ordinates.
356 * @param cfg {Object} Optional. An object which specifies:
359 * <dt>minDistance (defaults to 0)</dt>
360 * <dd>The minimum distance threshold which should be crossed before the gesturemovestart is fired</dd>
361 * <dt>minTime (defaults to 0)</dt>
362 * <dd>The minimum time threshold for which the finger/mouse should be help down before the gesturemovestart is fired</dd>
363 * <dt>button (no default)</dt>
364 * <dd>In the case of a mouse input device, if the event should only be fired for a specific mouse button.</dd>
365 * <dt>preventDefault (defaults to false)</dt>
366 * <dd>Can be set to true/false to prevent default behavior as soon as the touchstart or mousedown is received (that is before minTime or minDistance thresholds are crossed, and so before the gesturemovestart listener is notified) so that things like text selection and context popups (on touch devices) can be
367 * prevented. This property can also be set to a function, which returns true or false, based on the event facade passed to it (for example, DragDrop can determine if the target is a valid handle or not before preventing default).</dd>
370 * @return {EventHandle} the detach handle
373 define(GESTURE_MOVE_START, {
375 on: function (node, subscriber, ce) {
377 subscriber[_MOVE_START_HANDLE] = node.on(EVENT[START],
385 delegate : function(node, subscriber, ce, filter) {
389 subscriber[_DEL_MOVE_START_HANDLE] = node.delegate(EVENT[START],
391 se._onStart(e, node, subscriber, ce, true);
396 detachDelegate : function(node, subscriber, ce, filter) {
397 var handle = subscriber[_DEL_MOVE_START_HANDLE];
401 subscriber[_DEL_MOVE_START_HANDLE] = null;
405 detach: function (node, subscriber, ce) {
406 var startHandle = subscriber[_MOVE_START_HANDLE];
409 startHandle.detach();
410 subscriber[_MOVE_START_HANDLE] = null;
414 processArgs : function(args, delegate) {
415 var params = _defArgsProcessor(this, args, delegate);
417 if (!(MIN_TIME in params)) {
418 params[MIN_TIME] = this.MIN_TIME;
421 if (!(MIN_DISTANCE in params)) {
422 params[MIN_DISTANCE] = this.MIN_DISTANCE;
428 _onStart : function(e, node, subscriber, ce, delegate) {
431 node = e[CURRENT_TARGET];
434 var params = subscriber._extra,
436 minTime = params[MIN_TIME],
437 minDistance = params[MIN_DISTANCE],
438 button = params.button,
439 preventDefault = params[PREVENT_DEFAULT],
440 root = _getRoot(node, subscriber),
444 if (e.touches.length === 1) {
445 _normTouchFacade(e, e.touches[0], params);
450 fireStart = (button === undefined) || (button === e.button);
456 _prevent(e, preventDefault);
458 if (minTime === 0 || minDistance === 0) {
459 this._start(e, node, ce, params);
463 startXY = [e.pageX, e.pageY];
468 params._ht = Y.later(minTime, this, this._start, [e, node, ce, params]);
470 params._hme = root.on(EVENT[END], Y.bind(function() {
471 this._cancel(params);
475 if (minDistance > 0) {
478 params._hm = root.on(EVENT[MOVE], Y.bind(function(em) {
479 if (Math.abs(em.pageX - startXY[0]) > minDistance || Math.abs(em.pageY - startXY[1]) > minDistance) {
480 this._start(e, node, ce, params);
488 _cancel : function(params) {
494 params._hme.detach();
503 _start : function(e, node, ce, params) {
506 this._cancel(params);
509 e.type = GESTURE_MOVE_START;
512 node.setData(_MOVE_START, e);
518 PREVENT_DEFAULT : false
522 * Sets up a "gesturemove" event, that is fired on touch devices in response to a single finger "touchmove",
523 * and on mouse based devices in response to a "mousemove".
525 * <p>By default this event is only fired when the same node
526 * has received a "gesturemovestart" event. The subscriber can set standAlone to true, in the configuration properties,
527 * if they want to listen for this event without an initial "gesturemovestart".</p>
529 * <p>By default this event sets up it's internal "touchmove" and "mousemove" DOM listeners on the document element. The subscriber
530 * can set the root configuration property, to specify which node to attach DOM listeners to, if different from the document.</p>
532 * <p>This event can also be listened for using node.delegate().</p>
534 * <p>It is recommended that you use Y.bind to set up context and additional arguments for your event handler,
535 * however if you want to pass the context and arguments as additional signature arguments to on/delegate,
536 * you need to provide a null value for the configuration object, e.g: <code>node.on("gesturemove", fn, null, context, arg1, arg2, arg3)</code></p>
540 * @param type {string} "gesturemove"
541 * @param fn {function} The method the event invokes. It receives the event facade of the underlying DOM event (mousemove or touchmove.touches[0]) which contains position co-ordinates.
542 * @param cfg {Object} Optional. An object which specifies:
544 * <dt>standAlone (defaults to false)</dt>
545 * <dd>true, if the subscriber should be notified even if a "gesturemovestart" has not occured on the same node.</dd>
546 * <dt>root (defaults to document)</dt>
547 * <dd>The node to which the internal DOM listeners should be attached.</dd>
548 * <dt>preventDefault (defaults to false)</dt>
549 * <dd>Can be set to true/false to prevent default behavior as soon as the touchmove or mousemove is received. As with gesturemovestart, can also be set to function which returns true/false based on the event facade passed to it.</dd>
552 * @return {EventHandle} the detach handle
554 define(GESTURE_MOVE, {
556 on : function (node, subscriber, ce) {
558 var root = _getRoot(node, subscriber),
560 moveHandle = root.on(EVENT[MOVE],
567 subscriber[_MOVE_HANDLE] = moveHandle;
570 delegate : function(node, subscriber, ce, filter) {
574 subscriber[_DEL_MOVE_HANDLE] = node.delegate(EVENT[MOVE],
576 se._onMove(e, node, subscriber, ce, true);
581 detach : function (node, subscriber, ce) {
582 var moveHandle = subscriber[_MOVE_HANDLE];
586 subscriber[_MOVE_HANDLE] = null;
590 detachDelegate : function(node, subscriber, ce, filter) {
591 var handle = subscriber[_DEL_MOVE_HANDLE];
595 subscriber[_DEL_MOVE_HANDLE] = null;
600 processArgs : function(args, delegate) {
601 return _defArgsProcessor(this, args, delegate);
604 _onMove : function(e, node, subscriber, ce, delegate) {
607 node = e[CURRENT_TARGET];
610 var fireMove = subscriber._extra.standAlone || node.getData(_MOVE_START),
611 preventDefault = subscriber._extra.preventDefault;
617 if (e.touches.length === 1) {
618 _normTouchFacade(e, e.touches[0]);
626 _prevent(e, preventDefault);
629 e.type = GESTURE_MOVE;
635 PREVENT_DEFAULT : false
639 * Sets up a "gesturemoveend" event, that is fired on touch devices in response to a single finger "touchend",
640 * and on mouse based devices in response to a "mouseup".
642 * <p>By default this event is only fired when the same node
643 * has received a "gesturemove" or "gesturemovestart" event. The subscriber can set standAlone to true, in the configuration properties,
644 * if they want to listen for this event without a preceding "gesturemovestart" or "gesturemove".</p>
646 * <p>By default this event sets up it's internal "touchend" and "mouseup" DOM listeners on the document element. The subscriber
647 * can set the root configuration property, to specify which node to attach DOM listeners to, if different from the document.</p>
649 * <p>This event can also be listened for using node.delegate().</p>
651 * <p>It is recommended that you use Y.bind to set up context and additional arguments for your event handler,
652 * however if you want to pass the context and arguments as additional signature arguments to on/delegate,
653 * you need to provide a null value for the configuration object, e.g: <code>node.on("gesturemoveend", fn, null, context, arg1, arg2, arg3)</code></p>
656 * @event gesturemoveend
658 * @param type {string} "gesturemoveend"
659 * @param fn {function} The method the event invokes. It receives the event facade of the underlying DOM event (mouseup or touchend.changedTouches[0]).
660 * @param cfg {Object} Optional. An object which specifies:
662 * <dt>standAlone (defaults to false)</dt>
663 * <dd>true, if the subscriber should be notified even if a "gesturemovestart" or "gesturemove" has not occured on the same node.</dd>
664 * <dt>root (defaults to document)</dt>
665 * <dd>The node to which the internal DOM listeners should be attached.</dd>
666 * <dt>preventDefault (defaults to false)</dt>
667 * <dd>Can be set to true/false to prevent default behavior as soon as the touchend or mouseup is received. As with gesturemovestart, can also be set to function which returns true/false based on the event facade passed to it.</dd>
670 * @return {EventHandle} the detach handle
672 define(GESTURE_MOVE_END, {
674 on : function (node, subscriber, ce) {
676 var root = _getRoot(node, subscriber),
678 endHandle = root.on(EVENT[END],
685 subscriber[_MOVE_END_HANDLE] = endHandle;
688 delegate : function(node, subscriber, ce, filter) {
692 subscriber[_DEL_MOVE_END_HANDLE] = node.delegate(EVENT[END],
694 se._onEnd(e, node, subscriber, ce, true);
699 detachDelegate : function(node, subscriber, ce, filter) {
700 var handle = subscriber[_DEL_MOVE_END_HANDLE];
704 subscriber[_DEL_MOVE_END_HANDLE] = null;
709 detach : function (node, subscriber, ce) {
710 var endHandle = subscriber[_MOVE_END_HANDLE];
714 subscriber[_MOVE_END_HANDLE] = null;
718 processArgs : function(args, delegate) {
719 return _defArgsProcessor(this, args, delegate);
722 _onEnd : function(e, node, subscriber, ce, delegate) {
725 node = e[CURRENT_TARGET];
728 var fireMoveEnd = subscriber._extra.standAlone || node.getData(_MOVE) || node.getData(_MOVE_START),
729 preventDefault = subscriber._extra.preventDefault;
733 if (e.changedTouches) {
734 if (e.changedTouches.length === 1) {
735 _normTouchFacade(e, e.changedTouches[0]);
743 _prevent(e, preventDefault);
745 e.type = GESTURE_MOVE_END;
748 node.clearData(_MOVE_START);
749 node.clearData(_MOVE);
754 PREVENT_DEFAULT : false
758 }, '3.3.0' ,{requires:['node-base','event-touch','event-synthetic']});
761 YUI.add('event-gestures', function(Y){}, '3.3.0' ,{use:['event-flick', 'event-move']});