简体   繁体   中英

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

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.

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();
});

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();
}

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

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.

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.

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');
    });
}

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

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

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.

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

});

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();
    }
});

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM