Preventing mouse emulation events (i.e. click) from touch events in Mobile Safari / iPhone using Javascript
Asked Answered
M

11

47

In doing a single page Javascript app with interactive DOM elements I've found that the "mouseover-mousemove-mousedown-mouseup-click" sequence happens all in a bunch after the "touchstart-touchmove-touchend" sequence of events.

I've also found that it is possible to prevent the "mouse*-click" events from happening by doing an "event.preventDefault()" during the touchstart event, but only then, and not during the touchmove and touchend. This is a strange design, because because it is not possible to know during the touchstart yet whether the user intents to drag or swipe or just tap/click on the item.

I ended up setting up a "ignore_next_click" flag somewhere tied to a timestamp, but this is obviously not very clean.

Does anybody know of a better way of doing this, or are we missing something?

Note that while a "click" can be recognized as a "touchstart-touchend" sequence (ie no "touchmove"), there are certain things, such as keyboard input focus, that can only happen during a proper click event.

Melbourne answered 23/5, 2010 at 6:58 Comment(4)
I am interested in the iPad Safari touch events too, but it's not clear to me what specific problem you are trying to solve. If you're still working on this issue, or have solved it, care to elaborate?Eupepsia
I want to be able to handle certain events such as drag-and-drop, and also be able to handle "click" events. I have to handle the "click" events as proper "click" events (rather than touchstart/touchend) because certain things, like keyboard input focus can only be activated within a click event handler.Melbourne
This problem is extremely annoying and affects Android too.Gabrielegabriell
I agree with @RogerBinns, extremely annoying. Another consequence of doing a preventDefault() on the touchstart event is that page scrolling gets disabled.Michellmichella
T
13

Just prevent the touchend event. It will let the browser scroll the page when you touch the element but won't let it emit artificial mouse events.

element.addEventListener('touchend', event => {
  event.preventDefault();
});
Tarkington answered 15/4, 2019 at 9:12 Comment(2)
This might be the easiest solution since mouseevents are fired after touch events. Reference: developer.mozilla.org/en-US/docs/Web/API/Touch_events/…Hendon
Tested on mobile firefox 68.4.1, sometimes the preventDefault() doesn't stop the following 'mouseover' if I swipe with two fingers very quickly (with zooming disabled.)Valerio
L
6

I've run into similar problems making cross-platform HTML5/JS apps. The only real answer for me was to preventDefault on the touch events, and actually manage the touch states and fire click, drags, etc. events on my own according to my logic. This sounds much much more daunting than it really is, but the mimicked click/mouse events work perfectly on most mobile browsers.

Click and the extra mouse sequence are all there for your convenience (and compatibility). My rule of thumb- if it's for your convenience but it's inconvenient, best kill it.

As far as the input boxes, they only need the touchend events. I've killed click/mouse events and was still able to let mobile browsers respond correctly to touches on inputs. If it's still giving you issues, you can modify the event handler to only supress events on non-inputs:

function touchHandler(event) {
    var shouldIgnore = event.target != null 
          && ( event.target.tagName.toLowerCase() == "input" || event.target.tagName.toLowerCase() == "textarea" );

    if(!shouldIgnore) e.preventDefault();
}
Logistic answered 6/4, 2012 at 21:32 Comment(3)
I'm testing this solution both on iOS5 and Android 2.2.1, and the problem is that if I preventDefault on touchstart, the click doesn't fire, but I'm unable to scroll the page. If I preventDefault on touchend it works grat on iOS (only if the tap is short) but fires the click on Android. Have you noticed this same problem? Test it here: jsfiddle.net/3TBVcMichellmichella
yeah, i've run into this problem (manifested in several ways). It seems to be one of the bigger pains in iOS<->Android cross-platform. It takes some tweaking, but by canceling the right things you can generally get the desired result. jsfiddle.net/3TBVc/9 Seems to work fine for me on android. The input fields may eat some 'drag' events (on android), but its functional.Logistic
It's still firing click events on my Android device (and also on the iPhone for the first button).Michellmichella
B
4

I've made a solution myself, since I have not found a sufficient solution elsewhere:

   var isTouch = ('ontouchstart' in window);

   function kill(type){
     window.document.body.addEventListener(type, function(e){
       e.preventDefault();
       e.stopPropagation();
       return false;
     }, true);
   }

   if( isTouch ){
     kill('mousedown');
     kill('mouseup');
     kill('click');
     kill('mousemove');
   }

The check of isTouch lets things work as normal on mouse-input devices but kills the emulated events on Safari/iOS. The trick is to use useCapture = true in the call to addEventListener so we scoop up all the mouse events in the page without hacking the code all over the web app. See the docs for the function here: https://developer.mozilla.org/en-US/docs/DOM/EventTarget.addEventListener?redirectlocale=en-US&redirectslug=DOM%2Felement.addEventListener

Edit:

Now that libraries for handling this issue are better, you can just use something like Fastclick as an alternative (https://github.com/ftlabs/fastclick).

Baltimore answered 3/4, 2013 at 20:50 Comment(3)
This is a good solution when you know the devices are either touch or mouse input. But when you have something like a Windows 8 laptop/tablet, you'll need to do something else.Disoblige
our solution to this was to listen for both click and touchstart/stop/move events and only register the click event if 'ontouchstart' was unavailable. Mobile safari registers both touch and click events, but this method only registers the touch event in that case. Also, we were ok with disregarding a mousemove event.Childers
voted down because the question was not to completly disable mouse events if touch events are available...Vignette
T
2

If you have to support devices which support both mouse and touch, another solution is to use a capture event listener which stops all mouse events which occur either

  • within a delay after the touch event
  • at the same position as the touch event
  • on the same target element as the touch event

The information (time, position or target element) of the touch event can be recorded in another capture event listener.

Thrave answered 18/3, 2015 at 14:12 Comment(0)
A
2

Wrapping your mouse-only code in a Window.matchesMedia function is the cleanest way I found.

if (window.matchMedia('(hover: hover), (any-hover: hover), (-moz-touch-enabled: 0)').matches) {
    el.addEventListener('mouseover', ev => {
         // mouse handler, no simulated hover
    }
}

This works for preventing simulated hovers but will likely prevent simulated clicks, too.

Note: -moz-touch-enabled part required on Firefox as of version 58.

Alsup answered 2/2, 2018 at 9:27 Comment(0)
A
2

This solution allows you to listen for PointerEvents if they exist, followed by TouchEvents if they exist, followed by MouseEvents if neither of the other two exist. Mobile Safari will still raise both touchstart and mousedown, but you'll only be listening for touchstart.

if (window.PointerEvent) {                                  /* decent browsers */
    etouch.addEventListener('pointerdown', (e) => {
        console.log('pointerdown');
    });
}
else if (window.TouchEvent) {                               /* mobile Safari */
    etouch.addEventListener('touchstart', (e) => {
        console.log('touchstart');
    });
}
else {                                                      /* desktop Safari */
    etouch.addEventListener('mousedown', (e) => {
        console.log('mousedown');
    });
}
Amadoamador answered 23/11, 2018 at 2:32 Comment(0)
A
2

Using 'pointerwhatever' instead of 'mousewhatever' seems to work fine on current browsers (2019).

i.e. they invented a way of having the same code for all the entry devices.

Ancient answered 9/7, 2019 at 10:38 Comment(0)
T
1

pointer... events have a pointerType property that mouse... events lack. You can use this property to detect and ignore events that were generated by touch rather than by a mouse.

Before:

window.addEventListner('mousemove', (e) => {
  /* No way to tell if this event came from a mouse or a finger */
  console.log(':(');
});

After:

window.addEventListner('pointermove', (e) => {
  if (e.pointerType !== 'mouse') return;
  /* This event definitely came from a mouse */
  console.log(':)');
});

You can take advantage of this property just by replacing your mouse... event listeners with pointer... listeners. pointer... events are well-supported in modern browsers (going back at least three years).

Tribalism answered 4/8, 2022 at 22:29 Comment(0)
C
0

Creating Fast Buttons for Mobile Web Applications has their solution to the problem.

Also beware that when using IE10 preventDefault() doesn't stop the ghost/synthetic/emulated mouse events after a MSPointerDown event, so a true cross-browser solution is harder.

Cella answered 5/8, 2013 at 3:59 Comment(0)
O
0

You could try to quit the function on "click", "mousedown" or "mouseup" events when the device supports touch events.

use.addEventListener("click",function(e){

  // EXIT FUNCTION IF DEVICE SUPPORTS TOUCH EVENTS
  if ("ontouchstart" in document.documentElement) return false;

  // YOURMOUSEONLY CODE HERE

});
Oeo answered 7/3, 2019 at 18:17 Comment(0)
P
0

Add an event listener to touchstart that adds attribute data-touched to the element. Add another event listener to click that checks for data-touched. If it's there, prevent default and remove it. Here's some JS from my implementation.

var menuLinks = document.querySelectorAll('#my-nav>li>a');
for (var i = 0; i < menuLinks.length; i++) {
    var menuLink = menuLinks[i];
    menuLink.addEventListener('touchstart', function () {
        menuLink.setAttribute('data-touched', '');
});
menuLink.addEventListener('click', function (event) {
    if (menuLink.hasAttribute('data-touched')) {
        menuLink.removeAttribute('data-touched');
        event.preventDefault();
    }
});
Protozoan answered 1/10, 2020 at 5:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.