I found this thread because I have a similar & more complex problem:
supposing we create a js enabled scrollable area with arrows NEXT/PREVIOUS which we want not only to respond to touch and mouse events but also to fire them repeatedly while the user continues to press the screen or hold down his/her mouse!
Repetition of events would make my next button to advance 2 positions instead one!
With the help of closures everything seems possible:
(1) First create a self invoking function for variable isolation:
(function(myScroll, $, window, document, undefined){
...
}(window.myScroll = window.myScroll || {}, jQuery, window, document));
(2) Then, add your private variables that will hold internal state from setTimeout()
:
/*
* Primary events for handlers that respond to more than one event and devices
* that produce more than one, like touch devices.
* The first event in browser's queue hinders all subsequent for the specific
* key intended to be used by a handler.
* Every key points to an object '{primary: <event type>}'.
*/
var eventLock = {};
// Process ids based on keys.
var pids = {};
// Some defaults
var defaults = {
pressDelay: 100 // ms between successive calls for continuous press by mouse or touch
}
(3) The event lock functions:
function getEventLock(evt, key){
if(typeof(eventLock[key]) == 'undefined'){
eventLock[key] = {};
eventLock[key].primary = evt.type;
return true;
}
if(evt.type == eventLock[key].primary)
return true;
else
return false;
}
function primaryEventLock(evt, key){
eventLock[key].primary = evt.type;
}
(4) Attach your event handlers:
function init(){
$('sth').off('mousedown touchstart', previousStart).on('mousedown touchstart', previousStart);
$('sth').off('mouseup touchend', previousEnd).on('mouseup touchend', previousEnd);
// similar for 'next*' handlers
}
Firing of events mousedown
and touchstart
will produce double calls for handlers on devices that support both (probably touch fires first). The same applies to mouseup
and touchend
.
We know that input devices (whole graphic environments actually) produce events sequentially so we don't care which fires first as long a special key is set at private eventLock.next.primary
and eventLock.previous.primary
for the first events captured from handlers next*()
and previous*()
respectively.
That key is the event type so that the second, third etc. event are always losers, they don't acquire the lock with the help of the lock functions eventLock()
and primaryEventLock()
.
(5) The above can be seen at the definition of the event handlers:
function previousStart(evt){
// 'race' condition/repetition between 'mousedown' and 'touchstart'
if(!getEventLock(evt, 'previous'))
return;
// a. !!!you have to implement this!!!
previous(evt.target);
// b. emulate successive events of this type
pids.previous = setTimeout(closure, defaults.pressDelay);
// internal function repeats steps (a), (b)
function closure(){
previous(evt.target);
primaryEventLock(evt, 'previous');
pids.previous = setTimeout(closure, defaults.pressDelay);
}
};
function previousEnd(evt){
clearTimeout(pids.previous);
};
Similar for nextStart
and nextEnd
.
The idea is that whoever comes after the first (touch or mouse) does not acquire a lock with the help of function eventLock(evt, key)
and stops there.
The only way to open this lock is to fire the termination event handlers *End()
at step (4): previousEnd
and nextEnd
.
I also handle the problem of touch devices attached in the middle of the session with a very smart way: I noticed that a continuous press longer than defaults.pressDelay
produces successive calls of the callback function only for the primary event at that time (the reason is that no end event handler terminates the callabck)!
touchstart event
closure
closure
....
touchend event
I define primary the device the user is using so, all you have to do is just press longer and immediately your device becomes primary with the help of primaryEventLock(evt, 'previous')
inside the closure!
Also, note that the time it takes to execute previous(event.target)
should be smaller than defaults.pressDelay
.
(6) Finally, let's expose init()
to the global scope:
myScroll.init = init;
You should replace the call to previous(event.target)
with the problem at hand: fiddle.
Also, note that at (5b) there is a solution to another popular question how do we pass arguments to a function called from setTimeout()
, i.e. setTimeout(previous, defaults.pressDelay)
lacks an argument passing mechanism.