Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

handle both mouse and touch events on touch screens

I'm writing web application which should support both mouse and touch interactions. For testing I use touch screen device with Windows 7. I've tried to sniff touch events in latest Firefox and Chrome canary and got the following results:

On touch Firefox fires touch and corresponding mouse event. Chrome fires touchstart/mousedown, touchend/mouseup pairs, but mousemove fired in very strange manner: one/two times while touchmove.

All mouse events handled as always.

Is there any way to handle mouse and touch evens simultaneously on modern touch screens? If Firefox fires a pair of touch and mouse event what happens on touchmove with mousemove in Chrome? Should I translate all mouse events to touch or vice versa? I hope to find right way to create responsive interface.

like image 926
johnny Avatar asked Jan 25 '13 21:01

johnny


5 Answers

You can't really predict in advance which events to listen for (eg. for all you know a USB touch screen could get plugged in after your page has loaded).

Instead, you should always listen to both the touch events and mouse events, but call preventDefault() on the touch events you handle to prevent (now redundant) mouse events from being fired for them. See http://www.html5rocks.com/en/mobile/touchandmouse/ for details.

like image 103
Rick Byers Avatar answered Sep 25 '22 23:09

Rick Byers


You should rather check availability of touch interface and bind events according to that.

You can do something like this:

(function () {
    if ('ontouchstart' in window) {
        window.Evt = {
            PUSH : 'touchstart',
            MOVE : 'touchmove',
            RELEASE : 'touchend'
        };
    } else {
        window.Evt = {
            PUSH : 'mousedown',
            MOVE : 'mousemove',
            RELEASE : 'mouseup'
        };
    }
}());

// and then...

document.getElementById('mydiv').addEventListener(Evt.PUSH, myStartDragHandler, false);


If you want to handle both in same time and browser does not translate well touch events into mouse events, you can catch touch events and stop them - then corresponding mouse event shouldn't be fired by browser (you won't have double events) and you can fire it yourself as mouse event or just handle it.

var mydiv = document.getElementsById('mydiv');
mydiv.addEventListener('mousemove', myMoveHandler, false);
mydiv.addEventListener('touchmove', function (e) {
    // stop touch event
    e.stopPropagation();
    e.preventDefault();

    // translate to mouse event
    var clkEvt = document.createEvent('MouseEvent');
    clkEvt.initMouseEvent('mousemove', true, true, window, e.detail, 
                 e.touches[0].screenX, e.touches[0].screenY, 
                 e.touches[0].clientX, e.touches[0].clientY, 
                 false, false, false, false, 
                 0, null);
    mydiv.dispatchEvent(clkEvt);

    // or just handle touch event
    myMoveHandler(e);
}, false);
like image 41
lupatus Avatar answered Sep 26 '22 23:09

lupatus


The solutions on this thread are outdated - for those (like me) who still land here in 2021, there is a new W3 specification for pointer events. These events combine mouse and touch into one.

https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events

https://www.w3.org/TR/pointerevents/

like image 37
Jenaca Willans Avatar answered Sep 24 '22 23:09

Jenaca Willans


MouseEvents and TouchEvents do not technically provide exactly the same functionality, but for most purposes , they can be used interchangeably. This solution does not favor one over the other, as the user may have both a mouse and a touch screen. Instead, it allows the user to use which ever input device they wish, as long as they wait at least five seconds before changing inputs. This solution ignores mouse pointer emulation on touchscreen devices when the screen is tapped.

var lastEvent = 3  ;
var MOUSE_EVENT = 1;
var TOUCH_EVENT = 2 ; 
 

element.addEventListener('touchstart', function(event)
		{
			 
			if (lastEvent === MOUSE_EVENT  )
			{
				var time =  Date.now() - eventTime  ;  
				if ( time > 5000 ) 
				{
					eventTime = Date.now() ;
					lastEvent = TOUCH_EVENT ;
					interactionStart(event) ;
				} 
			}
			else 
			{
				lastEvent = TOUCH_EVENT ; ;
				eventTime = Date.now() ;
				interactionStart(event)  ; 
			}
			 
		}) ;	

		element.addEventListener('mousedown',	function(event)
		{
			
			if (lastEvent === TOUCH_EVENT  )
			{
				var time =  Date.now() - eventTime  ;	
				if ( time > 5000 ) 
				{
					eventTime = Date.now() ;
					lastEvent = MOUSE_EVENT ;
					interactionStart(event) ;
				} 
			}
			else 
			{
				lastEvent=  MOUSE_EVENT ; 
				eventTime = Date.now() ;
				interactionStart(event)  ; 
			}
		}) ;  

function interactionStart(event) // handle interaction (touch or click ) here.
{...}

This is by no means a win all solution, I have used this a few times , and have not found problems with it, but to be fair i usually just use it to start animation when a canvas it tapped , or to provide logic to turn a div into a button. I leave it to you all to use this code , find improvements and help to improve this code.(If you do not find a better solution ).

like image 42
FutureSci Avatar answered Sep 26 '22 23:09

FutureSci


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.

like image 31
centurian Avatar answered Sep 25 '22 23:09

centurian