Related
On virtually all current browsers (extensive details from patrickhlauke on github, which I summarised in an SO answer, and also some more info from QuirksMode), touchscreen touches trigger mouseover events (sometimes creating an invisible pseudo-cursor that stays where the user touched until they touch elsewhere).
Sometimes this causes undesirable behaviour in cases where touch/click and mouseover are intended to do different things.
From inside a function responding to a mouseover event, that has been passed the event object, is there any way I can check if this was a "real" mouseover from a moving cursor that moved from outside an element to inside it, or if it was caused by this touchscreen behaviour from a touchscreen touch?
The event object looks identical. For example, on chrome, a mouseover event caused by a user touching a touchscreen has type: "mouseover" and nothing I can see that would identify it as touch related.
I had the idea of binding an event to touchstart that alters mouseover events then an event to touchend that removes this alteration. Unfortunately, this doesn't work, because the event order appears to be touchstart → touchend → mouseover → click (I can't attach the normalise-mouseover function to click without messing up other functionality).
I'd expected this question to have been asked before but existing questions don't quite cut it:
How to handle mouseover and mouseleave events in Windows 8.1 Touchscreen is about C# / ASP.Net applications on Windows, not web pages in a browser
JQuery .on(“click”) triggers “mouseover” on touch device is similar but is about jQuery and the answer is a bad approach (guessing a hard-coded list of touchscreen user agents, which would break when new device UAs are created, and which falsely assumes all devices are mouse or touchscreen)
Preventing touch from generating mouseOver and mouseMove events in Android browser is the closest I could find, but it is only about Android, is about preventing not identifying mouseover on touch, and has no answer
Browser handling mouseover event for touch devices causes wrong click event to fire is related, but they're trying to elumate the iOS two-tap interaction pattern, and also the only answer makes that mistake of assuming that touches and mouse/clicks are mutually exclusive.
The best I can think of is to have a touch event that sets some globally accessible variable flag like, say, window.touchedRecently = true; on touchstart but not click, then removes this flag after, say, a 500ms setTimeout. This is an ugly hack though.
Note - we cannot assume that touchscreen devices have no mouse-like roving cursor or visa versa, because there are many devices that use a touchscreen and mouse-like pen that moves a cursor while hovering near the screen, or that use a touchscreen and a mouse (e.g. touchscreen laptops). More details in my answer to How do I detect whether a browser supports mouseover events?.
Note #2 - this is not a jQuery question, my events are coming from Raphael.js paths for which jQuery isn't an option and which give a plain vanilla browser event object. If there is a Raphael-specific solution I'd accept that, but it's very unlikely and a raw-javascript solution would be better.
Given the complexity of the issue, I thought it was worth detailing the issues and edge cases involved in any potential solution.
The issues:
1 - Different implementations of touch events across devices and browsers. What works for some will definitely not work for others. You only need to glance at those patrickhlauke resources to get an idea of how differently the process of tapping a touch-screen is currently handled across devices and browsers.
2 - The event handler gives no clue as to its initial trigger. You are also absolutely right in saying that the event object is identical (certainly in the vast majority of cases) between mouse events dispatched by interaction with a mouse, and mouse events dispatched by a touch interaction.
3 - Any solution to this problem which covers all devices could well be short-lived as the current W3C Recommendations do not go into enough detail on how touch/click events should be handled (https://www.w3.org/TR/touch-events/), so browsers will continue to have different implementations. It also appears that the Touch Events standards document has not changed in the past 5 years, so this isn't going to fix itself soon. https://www.w3.org/standards/history/touch-events
4 - Ideally, solutions should not use timeouts as there is no defined time from touch event to mouse event, and given the spec, there most probably won't be any time soon. Unfortunately, timeouts are almost inevitable as I will explain later.
A future solution:
In the future, the solution will probably be to use Pointer Events instead of mouse / touch events as these give us the pointerType (https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events), but unfortunately we're not there yet in terms of an established standard, and so cross-browser compatibility (https://caniuse.com/#search=pointer%20events) is poor.
How do we solve this at the moment
If we accept that:
You can't detect a touchscreen (http://www.stucox.com/blog/you-cant-detect-a-touchscreen/)
Even if we could, there's still the issue of non-touch events on a touch capable screen
Then we can only use data about the mouse event itself to determine its origin. As we've established, the browser doesn't provide this, so we need to add it ourselves. The only way to do this is using the touch events which are triggered around the same time as the mouse event.
Looking at the patrickhlauke resources again, we can make some statements:
mouseover is always followed by the click events mousedown mouseup and click - always in that order. (Sometimes separated by other events). This is backed up by the W3C recommendations: https://www.w3.org/TR/touch-events/.
For most devices / browsers, the mouseover event is always preceded by either pointerover, its MS counterpart MSPointerOver, or touchstart
The devices / browsers whose event order begins with mouseover have to be ignored. We can't establish that the mouse event was triggered by a touch event before the touch event itself has been triggered.
Given this, we could set a flag during pointerover, MSPointerOver, and touchstart, and remove it during one of the click events. This would work well, except for a handfull of cases:
event.preventDefault is called on one of the touch events - the flag will never be unset as the click events will not be called, and so any future genuine click events on this element would still be marked as a touch event
if the target element is moved during the event. The W3C Recommendations state
If the contents of the document have changed during processing of the
touch events, then the user agent may dispatch the mouse events to a
different target than the touch events.
Unfortunately this means that we will always need to use timeouts. To my knowledge there is no way of either establishing when a touch event has called event.preventDefault, nor understanding when the touch element has been moved within the DOM and the click event triggered on another element.
I think this is a fascinating scenario, so this answer will be amended shortly to contain a recommended code response. For now, I would recommend the answer provided by #ibowankenobi or the answer provided by #Manuel Otto.
What we do know is:
When the user uses no mouse
the mouseover is directly (within 800ms) fired after either a touchend or a
touchstart (if the user tapped and held).
the position of the mouseover and the touchstart/touchend are identical.
When the user uses a mouse/pen
The mouseover is fired before the touch events, even if not, the position of the mouseover will not match the touch events' position 99% of time.
Keeping these points in mind, I made a snippet, which will add a flag triggeredByTouch = true to the event if the listed conditions are met. Additionally you can add this behaviour to other mouse events or set kill = true in order to discard mouseevents triggered by touch completely.
(function (target){
var keep_ms = 1000 // how long to keep the touchevents
var kill = false // wether to kill any mouse events triggered by touch
var touchpoints = []
function registerTouch(e){
var touch = e.touches[0] || e.changedTouches[0]
var point = {x:touch.pageX,y:touch.pageY}
touchpoints.push(point)
setTimeout(function (){
// remove touchpoint from list after keep_ms
touchpoints.splice(touchpoints.indexOf(point),1)
},keep_ms)
}
function handleMouseEvent(e){
for(var i in touchpoints){
//check if mouseevent's position is (almost) identical to any previously registered touch events' positions
if(Math.abs(touchpoints[i].x-e.pageX)<2 && Math.abs(touchpoints[i].y-e.pageY)<2){
//set flag on event
e.triggeredByTouch = true
//if wanted, kill the event
if(kill){
e.cancel = true
e.returnValue = false
e.cancelBubble = true
e.preventDefault()
e.stopPropagation()
}
return
}
}
}
target.addEventListener('touchstart',registerTouch,true)
target.addEventListener('touchend',registerTouch,true)
// which mouse events to monitor
target.addEventListener('mouseover',handleMouseEvent,true)
//target.addEventListener('click',handleMouseEvent,true) - uncomment or add others if wanted
})(document)
Try it out:
function onMouseOver(e){
console.log('triggered by touch:',e.triggeredByTouch ? 'yes' : 'no')
}
(function (target){
var keep_ms = 1000 // how long to keep the touchevents
var kill = false // wether to kill any mouse events triggered by touch
var touchpoints = []
function registerTouch(e){
var touch = e.touches[0] || e.changedTouches[0]
var point = {x:touch.pageX,y:touch.pageY}
touchpoints.push(point)
setTimeout(function (){
// remove touchpoint from list after keep_ms
touchpoints.splice(touchpoints.indexOf(point),1)
},keep_ms)
}
function handleMouseEvent(e){
for(var i in touchpoints){
//check if mouseevent's position is (almost) identical to any previously registered touch events' positions
if(Math.abs(touchpoints[i].x-e.pageX)<2 && Math.abs(touchpoints[i].y-e.pageY)<2){
//set flag on event
e.triggeredByTouch = true
//if wanted, kill the event
if(kill){
e.cancel = true
e.returnValue = false
e.cancelBubble = true
e.preventDefault()
e.stopPropagation()
}
return
}
}
}
target.addEventListener('touchstart',registerTouch,true)
target.addEventListener('touchend',registerTouch,true)
// which mouse events to monitor
target.addEventListener('mouseover',handleMouseEvent,true)
//target.addEventListener('click',handleMouseEvent,true) - uncomment or add others if wanted
})(document)
a{
font-family: Helvatica, Arial;
font-size: 21pt;
}
Click me
According to https://www.html5rocks.com/en/mobile/touchandmouse/
For a single click the order of events is:
touchstart
touchmove
touchend
mouseover
mousemove
mousedown
mouseup
click
So you might be able to set some arbitrary boolean isFromTouchEvent = true; in onTouchStart() and isFromTouchEvent = false; in onClick() and check for that inside of onMouseOver(). This doesn't work very well since we're not guaranteed to get all those events in the element that we're trying to listen on.
I usually have couple of general schemes which I use for this, one of them uses a manual principle of setTimeout to trigger a property. I will explain this one here, but first try to reason about using touchstart, touchmove and touchend on touch devices and use mouseover on destop.
As you know, calling event.preventDefault (event has to be not passive for this to work with touchstart) in any of the touchevents will cancel the subsequent mousecalls so you do not need to deal with them. But in case this is not what you want, here is what I use sometimes (I refer as "library" to your dom manipulation library, and "elem" as your element):
with setTimeout
library.select(elem) //select the element
.property("_detectTouch",function(){//add a _detectTouch method that will set a property on the element for an arbitrary time
return function(){
this._touchDetected = true;
clearTimeout(this._timeout);
this._timeout = setTimeout(function(self){
self._touchDetected = false;//set this accordingly, I deal with either touch or desktop so I can make this 10000. Otherwise make it ~400ms. (iOS mouse emulation delay is around 300ms)
},10000,this);
}
}).on("click",function(){
/*some action*/
}).on("mouseover",function(){
if (this._touchDetected) {
/*coming from touch device*/
} else {
/*desktop*/
}
}).on("touchstart",function(){
this._detectTouch();//the property method as described at the beginning
toggleClass(document.body,"lock-scroll",true);//disable scroll on body by overflow-y hidden;
}).on("touchmove",function(){
disableScroll();//if the above overflow-y hidden don't work, another function to disable scroll on iOS.
}).on("touchend",function(){
library.event.preventDefault();//now we call this, if you do this on touchstart chrome will complain (unless not passive)
this._detectTouch();
var touchObj = library.event.tagetTouches && library.event.tagetTouches.length
? library.event.tagetTouches[0]
: library.event.changedTouches[0];
if (elem.contains(document.elementFromPoint(touchObj.clientX,touchObj.clientY))) {//check if we are still on the element.
this.click();//click will never be fired since default prevented, so we call it here. Alternatively add the same function ref to this event.
}
toggleClass(document.body,"lock-scroll",false);//enable scroll
enableScroll();//enableScroll
})
Another option without setTimeout is to think mousover is counter to touchstart and mouseout counter to touchend. So former events (the touch events) will set a property, if the mouse events detect that property then they do not fire and reset the property to its initial value and so on. In that case something along these lines will also do:
without setTimeout
....
.on("mouseover",function(dd,ii){
if (this._touchStarted) {//touch device
this._touchStarted = false;//set it back to false, so that next round it can fire incase touch is not detected.
return;
}
/*desktop*/
})
.on("mouseout",function(dd,ii){//same as above
if(this._touchEnded){
this._touchEnded = false;
return;
}
})
.on("touchstart",function(dd,ii){
this._touchStarted = true;
/*some action*/
})
.on("touchend",function(dd,ii){
library.event.preventDefault();//at this point emulations should not fire at all, but incase they do, we have the attached properties
this._touchEnded = true;
/*some action*/
});
I removed a lot of details but I guess this is the main idea.
You can use modernizr for that! I just tested this on a local development server and it works.
if (Modernizr.touch) {
console.log('Touch Screen');
} else {
console.log('No Touch Screen');
}
So I would start there?
Pointer Events are widely supported now. So now we can use pointerenter and check event.pointerType:
const element = document.getElementById("hoverableElement")
element.addEventListener("pointerenter", (event) => {
if (event.pointerType === "mouse") {
alert("Hovered")
}
})
<div id="hoverableElement">Trigger on hover, but not on touch</div>
I have an app that uses my own approach to SVG buttons, which required some hackery to get to work but I've liked how it works. However when I add jQuery Mobile to the project, my buttons are no longer responding to clicks or touch.
My buttons are not <button> elements, but <object> tags that link an external SVG file. I have code to hook these up like so:
function buttonifySVG(id, clickHandler) {
var obj = document.getElementById(id);
var svgDoc = obj.getSVGDocument();
function addClickHandler() {
svgDoc.removeEventListener('touchstart', clickHandler);
svgDoc.removeEventListener('mousedown', clickHandler);
svgDoc.addEventListener('touchstart', clickHandler);
svgDoc.addEventListener('mousedown', clickHandler);
}
addClickHandler();
obj.addEventListener('load', addClickHandler, false);
}
Here's a sample "button":
<object id="stepForward"type="image/svg+xml" data="stepForward.svg"></object>
And just to be completely clear:
...
buttonifySVG('stepForward', function() { doTheThing(); })
I can confirm with logging that the buttons are still being hooked up by this code but that the passed in clickHandler is never called. Beyond that, poking around in jquery-mobile.js, looks like there's at least one place where clicks are being intercepted and stopped, but I can't tell when, and more to the point, I'd rather not start hacking jquery code to get things to work.
Can anyone tell me what's likely the problem? I may be able to hack around it if I know what's going on here.
Also, does jQuery Mobile do anything special with <object id="myButton" type="image/svg+xml" data="foo.svg"> elements? This approach has so many nice features I'd really like to get it to play well with jQuery Mobile -- the solution I'm seeking is not to replace my smart buttons with jQuery SVG icon buttons (though I plan to use those for other parts of the UI).
Thanks for any help!
I'm not sure what exactly in JQM is causing the problem described, but I was able to get my code to work with a little modification:
function buttonifySVG(id, clickHandler) {
var obj = document.getElementById(id);
function addClickHandler() {
var svgDoc = obj.getSVGDocument();
var rect = svgDoc.getElementsByTagName('rect')[0];
rect.removeEventListener('touchstart', clickHandler);
rect.removeEventListener('mousedown', clickHandler);
rect.addEventListener('touchstart', clickHandler);
rect.addEventListener('mousedown', clickHandler);
}
obj.addEventListener('load', addClickHandler, false);
}
This relies on the fact that I authored the SVG images myself to have a single rect element as the top-most object for simple mouse/touch events. Not sure if there is a more generic approach that would work, depends on how the SVG is made. Whatever JQM is doing seems to be blocking events where the target is the SVG document itself, but not blocking events within that document. I have noticed a new bug with my code on mobile devices where I'm getting 2 touch events for each button touch, which may or may not be due to the above code...
In the past, the best method to check for the presence of a mouse was to look for touch event support. However, desktop Chrome now supports touch events, making this test misfire.
Is there a way to test directly for mouseover event support, rather than inferring it based on the presence of touch events?
Resolution: Here is the code that worked, based on the answer from AshleysBrain.
jQuery(function()
{
// Has mouse
jQuery("body").one("mousemove", function(e)
{
attachMouseEvents();
});
// Has touchscreen
jQuery("body").one("touchstart", function(e)
{
// Unbind the mouse detector, as this will fire on some touch devices. Touchstart should always fire first.
jQuery("body").unbind("mousemove");
attachTouchEvents();
});
});
You could do the opposite of the solution for detecting keyboard or touch input. Just wait for an actual touch event or mouse move event and decide based off that. If you check the presence of an event handler, the browser may indicate it has the event even if it is not currently running on hardware that supports it, so the only reliable thing to do is wait and see which actual events fire.
You might want to think about using Modernizr, you could do something like the following using the Modernizer.hasEvent()(docs) method:
Modernizr.hasEvent("mouseover", document);
I have try this and it's work.
<html>
<head>
<script type="text/javascript">
function isEventSupported(eventName) {
var el = document.createElement("body"[eventName] || "div");
var isSupported = "on" + eventName.toLowerCase() in el || top.Event && typeof top.Event == "object" && eventName.toUpperCase() in top.Event;
el = null;
return isSupported;
}
</script>
</head>
<body onload="alert(isEventSupported('mouseover'));">TEST mouseover event</body>
</html>
I took the function isEventSupported from http://www.strictly-software.com/eventsupport.htm
If web sites can detect that you're using a mobile browser as accurately as they do why can't you use the same technique to infer they don't have mouseover support?
I have the following jQuery which works in all major browsers except Opera:
jQuery(document).ready(function () {
jQuery("#GetResults").live("click", function(e){
e.preventDefault(); //Opera doesn't execute anything here
});
};
Which is supposed to fire when clicking the following link:
<a id="GetResults" href="Folder/File/javascript:void(0);">Get Results</a>
Only Opera ignores this. Any ideas?
Edit:
I've just discovered that if I substitute out .live() for .bind() everything functions as expected. I can't find any documentation relating to .live() bugs in Opera though, and it does work in jsFiddle which would point at something environmental. What could be causing this behavour?
This needs clarification. The answers above are correct, but nobody clearly explained where your problem comes from.
In fact I think that you could probably reproduce the problem in other browsers too.
That's because of how .live works:
It binds to the event on document and waits for a particular event to bubble up to there. Then it checks if the event.target is what you wanted to handle. *
If you click on a link element it's quite possible that the browser goes to the new page before the event bubbles high enough to trigger your code. In an app with lots of HTML and event handlers all the browsers should have problems. Opera just starts displaying the new page and destroys the previous quicker in this case. It really depends on a particular situation more than on the browser. For example: you probably won't see this happen if you had a high network latency while connecting to the site.
To prevent default action on a a element you have to use .bind like in the old days ;) when a eveloper had to be aware of what he loads with AJAX and bind new events to that in a callback.
* There is more to that and .live is more complicated. I just described what is needed here.
What happens when you attach the handler using:
$ (something).bind ("click", function (e) {
// do something
})
You can also try to attach the handler using .click() method.
The following code works as expected in Opera 11.50.
<!doctype html>
<title></title>
<a id="GetResults" href="http://google.com">Get Results</a>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
<script>
jQuery(document).ready(function () {
jQuery("#GetResults").live("click", function(e){
alert('doing something');
e.preventDefault(); //Opera doesn't execute anything here
});
});
</script>
Either it is a corrected bug, or something more subtle.
Can you check whether the above works on your version of Opera / jQuery?
Read this article: http://jupiterjs.com/news/why-you-should-never-use-jquery-live
try use delegate instead
Not sure if you want to do it, or if it will work for you. I had similar issues with Opera 9.5 and e.preventDefault() not working, the only solution I found was to just return false...
jQuery(document).ready(function () {
jQuery("#GetResults").live("click", function(e){
e.preventDefault();
return false;
});
};
There are two aspects of an event bubbling worth considering in this case: propagation and the default action.
Propagation refers to the event bubbling. First the anchor tag gets the click event, then its parent element, then its parent's parent, and so forth, up to the document element. You can stop an event from propagating at any time by calling e.stopPropagation().
The default action is what the browser will do if nothing is done to prevent it. The most well-known case is when an anchor with an href is clicked, the browser will try to navigate there. There are other examples too, though, for example when you click and drag an image, many browsers will create a ghost image you can drop on another application. In both cases, you can stop the browser from doing the default action at any time by calling e.preventDefault()
As mentioned in other answers to this question, jQuery's .live() feature sets a handler at a high level element (like document) and takes action after events have propagated up. If a handler in between the anchor and the document calls e.stopPropagaiton() without calling e.preventDefault() it would stop the live handler from responding, while still allowing the browser to navigate (the default action).
I doubt this is what's happening, since it would affect all browsers, but it's one possible explanation.
Ensure that document.ready event happens before you click on link.
Try to put all lives in the top of the document.ready wrapper. It may help, if you have a lot of javascript code.
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).
i.e. 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).