18

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.

johnny
  • 1,241
  • 1
  • 15
  • 32

7 Answers7

19

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.

Rick Byers
  • 3,136
  • 2
  • 16
  • 13
  • This is a much cleaner solution than the accepted answer. Thanks! – keithjgrant May 20 '13 at 21:25
  • Thanks for the answer. Helpful and better answer indeed! – user1106995 May 17 '14 at 15:57
  • This works, but in Chrome I am getting this warning: `Player.js:52 [Intervention] Unable to preventDefault inside passive event listener due to target being treated as passive. See https://www.chromestatus.com/features/5093566007214080` – David Callanan Dec 08 '17 at 19:31
9

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);
lupatus
  • 4,208
  • 17
  • 19
  • this way I don't handle mouse if it's connected to touch screen – johnny Jan 26 '13 at 08:22
  • I wouldn’t use the first approach. Read more: https://plus.google.com/+PaulIrish/posts/Y2jydx31Bor – zachleat Jul 11 '13 at 22:27
  • 2
    Unfortunately this approach doesn't work in Firefox. This browser sends the mouse events even when you call `preventDefault` and `stopPropagation` on the touch events... Only workaround I found so far is ignoring the `mousedown` event when it is started at the same position as the previous `touchstart` event and ignoring `mousemove` and `mouseup` without a prior `mousedown`. To bad that Microsofts Pointer API isn't supported by the other browsers. It is much easier to use then this darn Touch API... – kayahr Sep 03 '13 at 14:03
  • 1
    Unfortunately, too, I have indeed many many cases on my ipad (iOS7) where both events are fired even with e.preventDefault() and e.stopPropagation() :-( – Garavani Jul 10 '14 at 10:15
  • To stop mouse events from being fired in Firefox, I found that you must call preventDefault() in ALL THREE touch events, touchstart, touchmove AND touchend. It's not intuitive, but works. – CaptureWiz Aug 17 '17 at 17:06
  • initMouseEvent is deprecated: https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/initMouseEvent better use new MouseEvent(...) – Bruno Marotta Apr 01 '20 at 07:45
  • NEVER use Event.stopPropagation(). Unless you really know what you're doing. This seems not to be the case. – Roko C. Buljan Sep 28 '21 at 11:48
6

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/

  • 1
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Oct 22 '21 at 17:36
  • Is there a reason why this is not the top answer? – Tom Jan 11 '22 at 02:30
1

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 ).

FutureSci
  • 1,750
  • 1
  • 19
  • 27
1

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.

centurian
  • 1,168
  • 13
  • 25
0

I have been using this jQuery helper to bind both touch and click events.

(function ($) {
$.fn.tclick = function (onclick) {
    this.bind("touchstart", function (e) { onclick.call(this, e); e.stopPropagation(); e.preventDefault(); });
    this.bind("click", function (e) { onclick.call(this, e); });   //substitute mousedown event for exact same result as touchstart         
    return this;
  };
})(jQuery);
Joshua
  • 3,465
  • 3
  • 28
  • 19
0

Better Solution and Latest use Cases

If your Mouse and Touch event do similar activity then Better to use Pointer Event it will handle both the cases.

e.g PointerDown will work for both MouseDown and TouchStart

https://usefulangle.com/post/27/javascript-advantage-of-using-pointer-events-over-mouse-touch-events

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

Manas Sahu
  • 779
  • 8
  • 8