Skip to content

Instantly share code, notes, and snippets.

@tilomitra
Last active December 23, 2015 11:39
Show Gist options
  • Save tilomitra/6630272 to your computer and use it in GitHub Desktop.
Save tilomitra/6630272 to your computer and use it in GitHub Desktop.
The hold module provides a gesture event, "hold", which fires when a user's finger/mouse stays at the same place for a given period of time. The release module provides a synthetic event, "release", which fires when a user's finger/mouse leaves the screen.
YUI.add('hold-unit-tests', function(Y) {
var HOLD_EVENT = Y.Node.DOM_EVENTS.hold.eventDef,
Assert = Y.Assert,
noop = function() {},
module = Y.one('#event-hold-module'),
submodule1 = Y.one('#event-hold-submodule1'),
submodule2 = Y.one('#event-hold-submodule2'),
HANDLES = {
START: 'Y_HOLD_ON_START_HANDLE',
MOVE: 'Y_HOLD_ON_MOVE_HANDLE',
END: 'Y_HOLD_ON_END_HANDLE',
CANCEL: 'Y_HOLD_ON_CANCEL_HANDLE'
},
hasMsTouchActionSupport = (module.getDOMNode().style && ("msTouchAction" in module.getDOMNode().style)),
event = {
target: module,
currentTarget: module,
touches: [
{
pageX: 100,
pageY: 100,
clientX: 100,
clientY: 100
}
],
type: 'touchstart'
},
eventMultipleTouches = {
target: module,
currentTarget: module,
touches: [
{
pageX: 100,
pageY: 100,
clientX: 100,
clientY: 100
},
{
pageX: 200,
pageY: 200,
clientX: 200,
clientY: 200
}
],
type: 'touchstart'
},
eventNoTouch = {
target: module,
currentTarget: module,
type: 'mousedown',
pageX: 100,
pageY: 100,
clientX: 100,
clientY: 100
},
suite = new Y.Test.Suite('event-hold Test Suite');
suite.add(new Y.Test.Case({
name: 'hold',
setUp: function() {
this.handles = [];
//this.handles.push(submodule1.on('hold', noop));
//this.handles.push(module.delegate('hold', noop, '.module'));
event.type = 'touchstart';
eventMultipleTouches.type = 'touchstart';
eventNoTouch.type = 'mousedown';
},
tearDown: function() {
Y.Array.each(this.handles, function(h) {
h.detach();
});
},
'test: on subscription': function () {
var sub = {},
notifier = {};
HOLD_EVENT.on(submodule2, sub, notifier);
Assert.isTrue(sub[HANDLES.START] instanceof Y.EventHandle);
},
'test: detach': function () {
var sub = {},
notifier = {};
HOLD_EVENT.on(submodule2, sub, notifier);
Assert.isTrue(sub[HANDLES.START] instanceof Y.EventHandle);
HOLD_EVENT.detach(submodule2, sub, notifier);
Assert.isNull(sub[HANDLES.START]);
},
'test: detachDelegate': function () {
var sub = {},
notifier = {};
HOLD_EVENT.delegate(module, sub, notifier, '.module');
Assert.isTrue(sub[HANDLES.START] instanceof Y.EventHandle);
HOLD_EVENT.detachDelegate(module, sub, notifier);
Assert.isNull(sub[HANDLES.START]);
},
'test: processArgs no delegate': function () {
var args = [{},{},{},{ duration: 100, threshold: 10 }];
var extras = HOLD_EVENT.processArgs(args);
Assert.areEqual(100, extras.duration);
Assert.areEqual(10, extras.threshold);
Assert.areSame(3, args.length);
},
'test: processArgs with delegate': function () {
var args = [{},{},{},{ duration: 100, threshold: 10 }];
var extras = HOLD_EVENT.processArgs(args, true);
Assert.isUndefined(extras);
Assert.areSame(4, args.length);
},
'test: _setTimer default': function () {
var sub = {},
flag = false,
notifier = {
fire: function () {
flag = true;
}
};
HOLD_EVENT._setTimer(event, module, sub, notifier);
//since the default hold duration is 500ms, we'll do something at 550ms.
this.wait(function(){
Assert.isTrue(flag);
}, 550);
},
'test: _setTimer with custom duration': function () {
var sub = {
_extra: {
duration: 200
}
},
flag = false,
notifier = {
fire: function () {
flag = true;
}
};
HOLD_EVENT._setTimer(event, module, sub, notifier);
//since the custom hold duration is 200ms, we'll do something at 250ms.
this.wait(function(){
Assert.isTrue(flag);
}, 250);
},
'test: _setTimer with no duration': function () {
var sub = {
_extra: {
duration: 0
}
},
flag = false,
notifier = {
fire: function () {
flag = true;
}
};
HOLD_EVENT._setTimer(event, module, sub, notifier);
//since the custom hold duration is 200ms, we'll do something at 250ms.
this.wait(function(){
Assert.isTrue(flag);
}, 100);
},
/* Tests with `touch` events */
'test: _hold touch standard': function () {
var sub = {},
flag = false,
notifier = {
fire: function () {
flag = true;
}
};
HOLD_EVENT._hold(event, module, sub, notifier, false);
this.wait(function(){
Assert.isTrue(flag, 'a single e.touch should fire a `hold` event.');
}, 550);
},
'test: _hold touch with multiple e.touches': function () {
var sub = {},
flag = false,
notifier = {
fire: function () {
flag = true;
}
};
HOLD_EVENT._hold(eventMultipleTouches, module, sub, notifier, false);
this.wait(function(){
Assert.isFalse(flag, 'multiple e.touches should not fire a `hold` event.');
}, 550);
},
'test: _setupListeners with touch': function () {
var sub = {},
context = {
eventType: 'touchstart'
};
//arguments are [event, node, subscription, notifier, delegate, context, timer]
HOLD_EVENT._setupListeners(event, module, sub, {}, false, context, {});
Assert.isTrue(sub[HANDLES.MOVE] instanceof Y.EventHandle);
Assert.areSame(sub[HANDLES.MOVE].evt.type, 'touchmove');
Assert.isTrue(sub[HANDLES.END] instanceof Y.EventHandle);
Assert.areSame(sub[HANDLES.END].evt.type, 'touchend');
Assert.isTrue(sub[HANDLES.CANCEL] instanceof Y.EventHandle);
Assert.areSame(sub[HANDLES.CANCEL].evt.type, 'touchcancel');
},
'test: _hold touch with preventDefault': function () {
var sub = {
_extra: {
preventDefault: true
}
},
flag = false,
prevFlag = false,
notifier = {
fire: function () {
flag = true;
}
};
var customEvent = event;
customEvent.preventDefault = function () {
prevFlag = true;
};
HOLD_EVENT._hold(customEvent, module, sub, notifier, false);
Assert.isTrue(prevFlag, '`preventDefault()` should be called');
this.wait(function(){
Assert.isTrue(flag, '`hold` event should fire');
}, 550);
},
/* tests with `mouse` events */
'test: _hold mouse standard': function () {
var sub = {},
flag = false,
notifier = {
fire: function () {
flag = true;
}
};
HOLD_EVENT._hold(eventNoTouch, module, sub, notifier, false);
this.wait(function(){
Assert.isTrue(flag, 'a regular mouse event should fire a `hold` event.');
}, 550);
},
'test: _hold mouse rightclick': function () {
var sub = {},
flag = false,
notifier = {
fire: function () {
flag = true;
}
},
buttonEvent = {
button: 3
};
HOLD_EVENT._hold(buttonEvent, module, sub, notifier, false);
this.wait(function(){
Assert.isFalse(flag, 'a right click should not fire a `hold` event.');
}, 550);
},
'test: _hold mouse with preventMouse = true': function () {
var sub = {
preventMouse: true
},
flag = false,
notifier = {
fire: function () {
flag = true;
}
};
HOLD_EVENT._hold(eventNoTouch, module, sub, notifier, false);
this.wait(function(){
Assert.isFalse(sub.preventMouse, '`preventMouse` should be reset to false');
Assert.isFalse(flag, '`hold` event should not fire if preventMouse = true');
}, 550);
},
'test: _hold mouse with preventDefault': function () {
var sub = {
_extra: {
preventDefault: true
}
},
flag = false,
prevFlag = false,
notifier = {
fire: function () {
flag = true;
}
};
var customEvent = eventNoTouch;
customEvent.preventDefault = function () {
prevFlag = true;
};
HOLD_EVENT._hold(customEvent, module, sub, notifier, false);
Assert.isTrue(prevFlag, '`preventDefault()` should be called');
this.wait(function(){
Assert.isTrue(flag, '`hold` event should fire');
}, 550);
},
'test: _setupListeners with mouse': function () {
var sub = {},
context = {
eventType: 'mousedown'
};
//arguments are [event, node, subscription, notifier, delegate, context, timer]
HOLD_EVENT._setupListeners(eventNoTouch, module, sub, {}, false, context, {});
Assert.isTrue(sub[HANDLES.MOVE] instanceof Y.EventHandle);
Assert.areSame(sub[HANDLES.MOVE].evt.type, 'mousemove');
Assert.isTrue(sub[HANDLES.END] instanceof Y.EventHandle);
Assert.areSame(sub[HANDLES.END].evt.type, 'mouseup');
Assert.isTrue(sub[HANDLES.CANCEL] instanceof Y.EventHandle);
Assert.areSame(sub[HANDLES.CANCEL].evt.type, 'mousecancel');
},
'test: _setupListeners with MSPointer': function () {
var sub = {},
context = {
eventType: 'MSPointerDown'
};
//arguments are [event, node, subscription, notifier, delegate, context, timer]
HOLD_EVENT._setupListeners(eventNoTouch, module, sub, {}, false, context, {});
Assert.isTrue(sub[HANDLES.MOVE] instanceof Y.EventHandle);
Assert.areSame(sub[HANDLES.MOVE].evt.type, 'MSPointerMove');
Assert.isTrue(sub[HANDLES.END] instanceof Y.EventHandle);
Assert.areSame(sub[HANDLES.END].evt.type, 'MSPointerUp');
Assert.isTrue(sub[HANDLES.CANCEL] instanceof Y.EventHandle);
Assert.areSame(sub[HANDLES.CANCEL].evt.type, 'MSPointerCancel');
},
'test: _move clearTimeout() with no params': function () {
var flag = false,
context = {
startXY: [90, 90] //the original events are [100, 100]
},
timer = setTimeout(function () {
flag = true;
}, 1000);
//arguments are [event, node, subscription, notifier, delegate, context, timer]
HOLD_EVENT._move(eventNoTouch, module, {}, {}, false, context, timer);
//wait until the above timer expires, and make sure that it wasn't executed.
this.wait(function(){
Assert.isFalse(flag);
}, 1100);
},
'test: _move clearTimeout() with custom threshold': function () {
var flag = false,
sub = {
_extra: {
threshold: 9 //need atleast a 9px difference to clearTimeout().
}
},
context = {
startXY: [90, 90] //the original events are [100, 100]
},
timer = setTimeout(function () {
flag = true;
}, 300);
//arguments are [event, node, subscription, notifier, delegate, context, timer]
HOLD_EVENT._move(eventNoTouch, module, {}, {}, false, context, timer);
//wait until the above timer expires, and make sure that it wasn't executed.
this.wait(function(){
Assert.isFalse(flag);
}, 350);
},
'test: _move no clearTimeout() called': function () {
var flag = false,
context = {
startXY: [98, 98] //the original events are [100, 100]
},
timer = setTimeout(function () {
flag = true;
}, 300);
//arguments are [event, node, subscription, notifier, delegate, context, timer]
HOLD_EVENT._move(eventNoTouch, module, {}, {}, false, context, timer);
//wait until the above timer expires, and make sure that it wasn't executed.
this.wait(function(){
Assert.isTrue(flag);
}, 350);
},
'test: _end': function () {
var flag = false,
sub = {},
timer = setTimeout(function () {
flag = true;
}, 300);
//populate subscription handles with some generic info
sub[HANDLES.MOVE] = new Y.EventHandle();
sub[HANDLES.END] = new Y.EventHandle();
sub[HANDLES.CANCEL] = new Y.EventHandle();
//arguments are [event, node, subscription, notifier, delegate, context, timer]
HOLD_EVENT._end(eventNoTouch, module, {}, {}, false, {}, timer);
//clearTimeout() will always get called.
this.wait(function(){
Assert.isFalse(flag);
}, 350);
Assert.isNull(sub[HANDLES.MOVE]);
Assert.isNull(sub[HANDLES.END]);
Assert.isNull(sub[HANDLES.CANCEL]);
}
}));
Y.Test.Runner.add(suite);
}, '', {requires:['event-hold', 'test', 'node']});
/**
The hold module provides a gesture event, "hold", which fires when a
user's finger/mouse stays at the same place for a given period of time.
@example
YUI().use('event-hold', function (Y) {
Y.one('#my-button').on('hold', function (e) {
Y.log('Button was held down.');
});
});
@module event
@submodule event-hold
@author tilo mitra
@since 3.12.0
*/
var doc = Y.config.doc,
GESTURE_MAP = Y.Event._GESTURE_MAP,
EVT_START = GESTURE_MAP.start,
EVT_HOLD = 'hold',
HOLD_THRESHOLD = 'threshold',
HOLD_DURATION = 'duration',
PREVENT_DEFAULT = 'preventDefault',
TOUCH = 'touch',
MOUSE = 'mouse',
POINTER = 'pointer',
HANDLES = {
START: 'Y_HOLD_ON_START_HANDLE',
MOVE: 'Y_HOLD_ON_MOVE_HANDLE',
END: 'Y_HOLD_ON_END_HANDLE',
CANCEL: 'Y_HOLD_ON_CANCEL_HANDLE'
};
function detachHandles(subscription, handles) {
handles = handles || Y.Object.values(HANDLES);
Y.Array.each(handles, function (item) {
var handle = subscription[item];
if (handle) {
handle.detach();
subscription[item] = null;
}
});
}
/**
Sets up a "hold" event, that is fired when a user has their mouse or
finger down on an element for a given amount of time. In mobile
devices (especially iOS), holding down on a DOM element can make that
node selectable. If you wish to prevent this behavior, add the
following CSS to the element in question:
-webkit-user-select: none;
-webkit-user-drag: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
@event hold
@param type {string} "hold"
@param fn {function} The method the event invokes. It receives the
event facade of the underlying DOM event.
@param config {object} Optional. An object which specifies any of the
following:
* duration {Integer}: The number of milliseconds before `hold` is fired.
Defaults to `500`
* threshold {Integer}: The max. number of pixels that the user can move
their mouse/finger before the `hold` event is cancelled. Defaults
to `5`.
* preventDefault {Boolean}: Optional. Defaults to `false`. Set this to
true if you want the `touchstart`/`mousedown` event to not trigger
default browser behavior. Setting this value to `true` will prevent
iOS from showing modals/tooltips when the element is held, but it
will also prevent users from flicking on that element to scroll the page.
@for Event
@return {EventHandle} the detach handle
*/
Y.Event.define(EVT_HOLD, {
processArgs: function (args, isDelegate) {
//if we return for the delegate use case, then the `filter`
//argument returns undefined, and we have to get the filter
//from sub._extra[0] (ugly)
if (!isDelegate) {
var extra = args[3];
// remove the extra arguments from the array as specified by
// http://yuilibrary.com/yui/docs/event/synths.html
args.splice(3,1);
return extra;
}
},
/**
This function should set up the node that will eventually fire the
event.
Usage:
node.on('hold', function (e) {
Y.log('the node was held.');
});
@method on
@param {Y.Node} node
@param {Y.Subscriber} subscription
@param {Object} notifier
@public
@static
**/
on: function (node, subscription, notifier) {
subscription[HANDLES.START] = node.on(EVT_START, this._hold, this, node, subscription, notifier);
},
/**
Detaches all event subscriptions set up by the event-hold module
@method detach
@param {Y.Node} node
@param {Y.Subscriber} subscription
@param {Object} notifier
@public
@static
**/
detach: function (node, subscription, notifier) {
detachHandles(subscription);
},
/**
Event delegation for the `hold` event. The delegated event will
use a supplied CSS selector or filtering function to test if the
event references at least one node that should trigger the subscription
callback.
Usage:
node.delegate('hold', function (e) {
Y.log('`li a` inside node was held.');
}, 'li a');
@method delegate
@param {Y.Node} node
@param {Y.Subscriber} subscription
@param {Object} notifier
@param {String | Function} filter
@public
@static
**/
delegate: function (node, subscription, notifier, filter) {
subscription[HANDLES.START] = node.delegate(EVT_START, function (e) {
this._hold(e, node, subscription, notifier, true);
}, filter, this);
},
/**
Detaches the delegated event subscriptions set up by the
event-hold module.
Only used if you use node.delegate(...) instead of node.on(...);
@method detachDelegate
@param {Y.Node} node
@param {Y.Subscriber} subscription
@param {Object} notifier
@public
@static
**/
detachDelegate: function (node, subscription, notifier) {
detachHandles(subscription);
},
/**
Called when the monitor(s) are touched, either through
`touchstart`, `mousedown`, `MSPointerDown` or a combination.
@method _hold
@param {DOMEventFacade} event
@param {Y.Node} node
@param {Y.Subscriber} subscription
@param {Object} notifier
@param {Boolean} delegate
@protected
@static
**/
_hold: function (event, node, subscription, notifier, delegate) {
var context = {
canceled: false,
//if it's a synthetic event, then get the underlying DOM event.
eventType: (event._event) ? event._event.type : event.type
},
params = subscription._extra,
//explicit undefined check here so that `0` passes through
prevDefault = (params && params[PREVENT_DEFAULT] !== undefined) ? params[PREVENT_DEFAULT] : this.preventDefault,
//preventMouse keeps track to see if a previous touch
//event occured, so that it prevents the corresponding
//mouse event from firing the event again.
preventMouse = subscription.preventMouse || false,
//Reference to a timer variable that will store the
//object passed from setTimeout()
timer;
//move ways to quit early to the top.
// no right clicks
if (event.button && event.button === 3) {
return;
}
// for now just support a 1 finger count
if (event.touches && event.touches.length !== 1) {
return;
}
context.node = delegate ? event.currentTarget : node;
if (event.touches) {
context.startXY = [ event.touches[0].pageX, event.touches[0].pageY ];
}
else {
context.startXY = [ event.pageX, event.pageY ];
}
//If `onTouchStart()` was called by a touch event, set up touch
//event subscriptions. Otherwise, set up mouse/pointer event
//event subscriptions.
if (event.touches) {
if (prevDefault) {
event.preventDefault();
}
timer = this._setTimer(event, node, subscription, notifier);
this._setupListeners(event, node, subscription, notifier, delegate, context, timer);
//Since this is a touch* event, there will be corresponding
//mouse events that will be fired. We don't want these
//events to get picked up and fire another `hold` event,
//so we'll set this variable to `true`.
subscription.preventMouse = true;
}
//If a mouse event comes in after a touch event, it will go in
//here and reset preventMouse to `false`.
//If a mouse event comes in without a prior touch event,
//preventMouse will be false in any case, so this block
//won't do anything.
else if (context.eventType.indexOf('mouse') !== -1 && preventMouse) {
subscription.preventMouse = false;
}
//Only add these listeners if preventMouse is `false`
//ie: not when touch events have already been subscribed to
else {
if (prevDefault) {
event.preventDefault();
}
timer = this._setTimer(event, node, subscription, notifier);
this._setupListeners(event, node, subscription, notifier, delegate, context, timer);
}
},
/**
Called internally to set up a timer that will eventually fire
the `hold` event after a set duration.
@method _setTimer
@param {DOMEventFacade} event
@param {Y.Node} node
@param {Y.Subscriber} subscription
@param {Object} notifier
@protected
@static
**/
_setTimer: function (event, node, subscription, notifier) {
var params = subscription._extra,
//explicit undefined check here so that `0` passes through
holdDuration = (params && params[HOLD_DURATION] !== undefined) ? params[HOLD_DURATION] : this.duration,
timer;
timer = setTimeout(function() {
event.type = EVT_HOLD;
notifier.fire(event);
}, holdDuration);
return timer;
},
/**
Called internally to set up event listeners that can clear the
timer, and prevent `hold` from being fired. These event listeners
vary based on target environment.
@method _setupListeners
@param {DOMEventFacade} event
@param {Y.Node} node
@param {Y.Subscriber} subscription
@param {Object} notifier,
@param {Boolean} delegate,
@param {Object} context,
@param {Number} timer
@protected
@static
**/
_setupListeners: function (event, node, subscription, notifier, delegate, context, timer) {
if (event.touches) {
subscription[HANDLES.MOVE] = node.on('touchmove', this._move, this, node, subscription, notifier, delegate, context, timer);
subscription[HANDLES.END] = node.once('touchend', this._end, this, node, subscription, notifier, delegate, context, timer);
subscription[HANDLES.CANCEL] = node.once('touchcancel', this._end, this, node, subscription, notifier, delegate, context, timer);
}
else if (context.eventType.indexOf('mouse') !== -1) {
subscription[HANDLES.MOVE] = node.on('mousemove', this._move, this, node, subscription, notifier, delegate, context, timer);
subscription[HANDLES.END] = node.once('mouseup', this._end, this, node, subscription, notifier, delegate, context, timer);
subscription[HANDLES.CANCEL] = node.once('mousecancel', this._end, this, node, subscription, notifier, delegate, context, timer);
}
else if (context.eventType.indexOf('MSPointer') !== -1) {
subscription[HANDLES.MOVE] = node.on('MSPointerMove', this._move, this, node, subscription, notifier, delegate, context, timer);
subscription[HANDLES.END] = node.once('MSPointerUp', this._end, this, node, subscription, notifier, delegate, context, timer);
subscription[HANDLES.CANCEL] = node.once('MSPointerCancel', this._end, this, node, subscription, notifier, delegate, context, timer);
}
},
/**
Event callback that fires for `touchmove`, `mousemove`, or
`MSPointerMove` events. The `hold` event is cancelled if the
user moves their finger/mouse over a certain threshold.
@method _move
@param {DOMEventFacade} event
@param {Y.Node} node
@param {Y.Subscriber} subscription
@param {Object} notifier,
@param {Boolean} delegate,
@param {Object} context,
@param {Number} timer
@protected
@static
**/
_move: function (event, node, subscription, notifier, delegate, context, timer) {
var currentXY = [ event.pageX, event.pageY ],
startXY = context.startXY,
params = subscription._extra,
//explicit undefined check here so that `0` passes through
holdThreshold = (params && params[HOLD_THRESHOLD] !== undefined) ? params[HOLD_THRESHOLD] : this.threshold;
if (Math.abs(currentXY[0] - startXY[0]) > holdThreshold ||
Math.abs(currentXY[1] - startXY[1]) > holdThreshold) {
clearTimeout(timer);
detachHandles(subscription, [HANDLES.END, HANDLES.CANCEL]);
}
},
/**
Event callback that fires for `touchend`, `mouseup`, or
`MSPointerUp` events. The `hold` event is cancelled if the
user lifts their finger/mouse off an element.
@method _end
@param {DOMEventFacade} event
@param {Y.Node} node
@param {Y.Subscriber} subscription
@param {Object} notifier,
@param {Boolean} delegate,
@param {Object} context,
@param {Number} timer
@protected
@static
**/
_end: function (event, node, subscription, notifier, delegate, context, timer) {
clearTimeout(timer);
detachHandles(subscription, [HANDLES.MOVE, HANDLES.END, HANDLES.CANCEL]);
},
threshold: 5,
duration: 500,
preventDefault: false
});
YUI.add('release-unit-tests', function(Y) {
var RELEASE_EVENT = Y.Node.DOM_EVENTS.release.eventDef,
Assert = Y.Assert,
noop = function() {},
module = Y.one('#event-release-module'),
submodule = Y.one('#event-release-submodule1'),
HANDLES = {
END: 'Y_RELEASE_ON_END_HANDLE'
},
hasMsTouchActionSupport = (module.getDOMNode().style && ("msTouchAction" in module.getDOMNode().style)),
event = {
target: module,
currentTarget: module,
changedTouches: [
{
pageX: 100,
pageY: 100,
clientX: 100,
clientY: 100
}
],
type: 'touchend'
},
eventMultipleTouches = {
target: module,
currentTarget: module,
changedTouches: [
{
pageX: 100,
pageY: 100,
clientX: 100,
clientY: 100
},
{
pageX: 200,
pageY: 200,
clientX: 200,
clientY: 200
}
],
type: 'touchend'
},
eventNoTouch = {
target: module,
currentTarget: module,
type: 'mouseup',
pageX: 100,
pageY: 100,
clientX: 100,
clientY: 100
},
suite = new Y.Test.Suite('event-release Test Suite');
suite.add(new Y.Test.Case({
name: 'hold',
setUp: function() {
this.handles = [];
//this.handles.push(submodule1.on('hold', noop));
//this.handles.push(module.delegate('hold', noop, '.module'));
event.type = 'touchend';
eventMultipleTouches.type = 'touchend';
eventNoTouch.type = 'mouseup';
},
tearDown: function() {
Y.Array.each(this.handles, function(h) {
h.detach();
});
},
'test: on subscription': function () {
var sub = {},
notifier = {};
RELEASE_EVENT.on(submodule, sub, notifier);
Assert.isTrue(sub[HANDLES.END] instanceof Y.EventHandle);
},
'test: detach': function () {
var sub = {},
notifier = {};
RELEASE_EVENT.on(submodule, sub, notifier);
Assert.isTrue(sub[HANDLES.END] instanceof Y.EventHandle);
RELEASE_EVENT.detach(submodule, sub, notifier);
Assert.isNull(sub[HANDLES.END]);
},
'test: detachDelegate': function () {
var sub = {},
notifier = {};
RELEASE_EVENT.delegate(module, sub, notifier, '.module');
Assert.isTrue(sub[HANDLES.END] instanceof Y.EventHandle);
RELEASE_EVENT.detachDelegate(module, sub, notifier);
Assert.isNull(sub[HANDLES.END]);
},
/* Tests with `touch` events */
'test: _release touch standard': function () {
var sub = {},
flag = false,
notifier = {
fire: function () {
flag = true;
}
};
RELEASE_EVENT._release(event, module, sub, notifier, false);
this.wait(function(){
Assert.isTrue(flag, 'a single e.touch should fire a `release` event.');
}, 550);
},
'test: _release touch with multiple e.touches': function () {
var sub = {},
flag = false,
notifier = {
fire: function () {
flag = true;
}
};
RELEASE_EVENT._release(eventMultipleTouches, module, sub, notifier, false);
this.wait(function(){
Assert.isFalse(flag, 'multiple e.touches should not fire a `release` event.');
}, 550);
},
/* tests with `mouse` events */
'test: _release mouse standard': function () {
var sub = {},
flag = false,
notifier = {
fire: function () {
flag = true;
}
};
RELEASE_EVENT._release(eventNoTouch, module, sub, notifier, false);
this.wait(function(){
Assert.isTrue(flag, 'a regular mouse event should fire a `release` event.');
}, 550);
},
'test: _release mouse rightclick': function () {
var sub = {},
flag = false,
notifier = {
fire: function () {
flag = true;
}
},
buttonEvent = {
button: 3
};
RELEASE_EVENT._release(buttonEvent, module, sub, notifier, false);
this.wait(function(){
Assert.isFalse(flag, 'a right click should not fire a `release` event.');
}, 550);
},
'test: _release mouse with preventMouse = true': function () {
var sub = {
preventMouse: true
},
flag = false,
notifier = {
fire: function () {
flag = true;
}
};
RELEASE_EVENT._release(eventNoTouch, module, sub, notifier, false);
this.wait(function(){
Assert.isFalse(sub.preventMouse, '`preventMouse` should be reset to false');
Assert.isFalse(flag, '`release` event should not fire if preventMouse = true');
}, 550);
},
/* make sure release isn't fired twice. */
'test: _release not fired twice': function () {
var sub = {},
flag = false,
notifier = {
fire: function () {
if (flag) {
flag = false;
}
else {
flag = true;
}
}
};
RELEASE_EVENT._release(event, module, sub, notifier, false);
sub.preventMouse = true;
this.wait(function(){
Assert.isTrue(flag, 'a regular mouse event should fire a `release` event.');
}, 550);
},
/* tests with `MSPointer` events */
'test: _release MSPointer standard': function () {
var sub = {},
flag = false,
notifier = {
fire: function () {
flag = true;
}
};
msPointerEvent = eventNoTouch;
msPointerEvent.type = 'MSPointerUp';
RELEASE_EVENT._release(msPointerEvent, module, sub, notifier, false);
this.wait(function(){
Assert.isTrue(flag, 'a regular MSPointerUp event should fire a `release` event.');
}, 550);
},
}));
Y.Test.Runner.add(suite);
}, '', {requires:['event-release', 'test', 'node']});
/**
The release module provides a synthetic event, "release", which fires when a user's finger/mouse leaves the screen.
@example
YUI().use('event-release', function (Y) {
Y.one('#my-button').on('release', function (e) {
Y.log('Button was released.');
});
});
@module event
@submodule event-release
@author tilo mitra
@since 3.12.0
*/
var doc = Y.config.doc,
EVT_END = Y.Event._GESTURE_MAP.end,
EVT_RELEASE = 'release',
HANDLES = {
END: 'Y_RELEASE_ON_END_HANDLE'
};
function detachHandles(subscription, handles) {
handles = handles || Y.Object.values(HANDLES);
Y.Array.each(handles, function (item) {
var handle = subscription[item];
if (handle) {
handle.detach();
subscription[item] = null;
}
});
}
/**
The release module provides a synthetic event, "release", which fires when a user's finger/mouse leaves the screen. This is a useful event to listen to when performing user interactions that require the user's mouse/finger to stay on the screen. For example, you could listen to a `hold` or `drag` event, and use the `release` event to reset your application's state.
@event release
@param type {string} "release"
@param fn {function} The method the event invokes. It receives the event facade of the underlying DOM event.
@for Event
@return {EventHandle} the detach handle
*/
Y.Event.define(EVT_RELEASE, {
/**
This function should set up the node that will eventually fire the event.
Usage:
node.on('release', function (e) {
Y.log('the node was released.');
});
@method on
@param {Y.Node} node
@param {Y.Subscriber} subscription
@param {Object} notifier
@public
@static
**/
on: function (node, subscription, notifier) {
subscription[HANDLES.END] = node.on(EVT_END, this._release, this, node, subscription, notifier);
},
/**
Detaches all event subscriptions set up by the event-release module
@method detach
@param {Y.Node} node
@param {Y.Subscriber} subscription
@param {Object} notifier
@public
@static
**/
detach: function (node, subscription, notifier) {
detachHandles(subscription);
},
/**
Event delegation for the 'release' event. The delegated event will use a
supplied CSS selector or filtering function to test if the event references at least one
node that should trigger the subscription callback.
Usage:
node.delegate('release', function (e) {
Y.log('`li a` inside node was released.');
}, 'li a');
@method delegate
@param {Y.Node} node
@param {Y.Subscriber} subscription
@param {Object} notifier
@param {String | Function} filter
@public
@static
**/
delegate: function (node, subscription, notifier, filter) {
subscription[HANDLES.END] = node.delegate(EVT_END, function (e) {
this._release(e, node, subscription, notifier, true);
}, filter, this);
},
/**
Detaches the delegated event subscriptions set up by the event-release module.
Only used if you use node.delegate(...) instead of node.on(...);
@method detachDelegate
@param {Y.Node} node
@param {Y.Subscriber} subscription
@param {Object} notifier
@public
@static
**/
detachDelegate: function (node, subscription, notifier) {
detachHandles(subscription);
},
/**
Called when a user's mouse/finger leaves the screen. Fires
the `release` event under-the-hood.
@method _release
@param {DOMEventFacade} event
@param {Y.Node} node
@param {Y.Subscriber} subscription
@param {Object} notifier
@param {Boolean} delegate
@protected
@static
**/
_release: function (event, node, subscription, notifier, delegate) {
var preventMouse = subscription.preventMouse || false,
context = {
eventType: event.type
};
//move ways to quit early to the top.
// no right clicks
if (event.button && event.button === 3) {
return;
}
// for now just support a 1 finger count
// A 'touchend' event does not have `event.touches` - it has event.changedTouches
// instead.
if (event.changedTouches && event.changedTouches.length !== 1) {
return;
}
//If `touchend` fired, fire `release` and prevent further mouse interactions
//from firing a second `release` event.
if (event.changedTouches) {
this._fireRelease(event, notifier);
//Since this is a touch* event, there will be corresponding mouse events
//that will be fired. We don't want these events to get picked up and fire
//another `release` event, so we'll set this variable to `true`.
subscription.preventMouse = true;
}
//Only add these listeners if preventMouse is `false`
//ie: not when touch events have already been subscribed to
else if (context.eventType.indexOf('mouse') !== -1 && !preventMouse) {
this._fireRelease(event, notifier);
}
//If a mouse event comes in after a touch event, it will go in here and
//reset preventMouse to `false`.
//If a mouse event comes in without a prior touch event, preventMouse will be
//false in any case, so this block doesn't do anything.
else if (context.eventType.indexOf('mouse') !== -1 && preventMouse) {
subscription.preventMouse = false;
}
else if (context.eventType.indexOf('MSPointer') !== -1) {
this._fireRelease(event, notifier);
}
},
/**
Fires the release event.
@method _fireRelease
@param {DOMEventFacade} event
@param {Object} notifier
@protected
@static
**/
_fireRelease: function (event, notifier) {
event.type = EVT_RELEASE;
notifier.fire(event);
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment