Sur pratiquement tous les navigateurs actuels (extensif détails de patrickhlauke sur github , qui j'ai résumé dans une réponse SO) , et aussi quelques informations supplémentaires - de QuirksMode ), les touches tactiles déclenchent des événements mouseover
(créant parfois un pseudo-curseur invisible qui reste là où l'utilisateur a touché jusqu'à ce qu'il touche ailleurs).
Parfois, cela provoque un comportement indésirable dans les cas où le toucher/clic et le survol sont destinés à faire des choses différentes.
De l'intérieur d'une fonction répondant à un événement de survol, auquel a été passé l'objet event
, existe-t-il un moyen de vérifier s'il s'agissait d'un "vrai" survol d'un curseur en mouvement qui s'est déplacé de l'extérieur d'un élément vers l'intérieur ou s'il a été causé par ce comportement de l'écran tactile à partir d'un écran tactile tactile?
L'objet event
semble identique. Par exemple, sur Chrome, un événement de survol de la souris provoqué par un utilisateur touchant un écran tactile a type: "mouseover"
et rien que je puisse voir qui l'identifierait comme lié au toucher.
J'ai eu l'idée de lier un événement à touchstart
qui modifie les événements de survol puis un événement à touchend
qui supprime cette altération. Malheureusement, cela ne fonctionne pas, car l'ordre des événements semble être touchstart
→ touchend
→ mouseover
→ click
(je ne peux pas joindre la normalisation -mouseover fonction pour cliquer sans gâcher d'autres fonctionnalités).
Je m'attendais à ce que cette question ait été posée auparavant, mais les questions existantes ne suffisent pas:
Le mieux que je puisse penser est d'avoir un événement tactile qui définit un indicateur de variable globalement accessible comme, par exemple, window.touchedRecently = true;
sur touchstart
mais sans cliquer, puis supprime cet indicateur après, disons, 500 ms setTimeout
. C'est un vilain hack cependant.
Remarque - nous ne pouvons pas supposer que les appareils à écran tactile n'ont pas de curseur itinérant semblable à une souris ou vice versa, car il existe de nombreux appareils qui utilisent un écran tactile et semblable à une souris stylo qui déplace un curseur tout en survolant près de l'écran, ou qui utilise un écran tactile et une souris (par exemple, des ordinateurs portables à écran tactile). Plus de détails dans ma réponse à Comment puis-je détecter si un navigateur prend en charge les événements de survol de la souris? .
Note # 2 - ce n'est pas une question jQuery, mes événements proviennent des chemins Raphael.js pour lesquels jQuery n'est pas une option et qui donnent un Navigateur Vanilla ordinaire event
objet. S'il y a une solution spécifique à Raphael, je l'accepterais, mais c'est très peu probable et une solution raw-javascript serait mieux.
Étant donné la complexité du problème, j'ai pensé qu'il valait la peine de détailler les problèmes et les cas Edge impliqués dans toute solution potentielle.
Les problèmes:
1 - Différentes implémentations d'événements tactiles sur les appareils et les navigateurs. Ce qui fonctionne pour certains ne fonctionnera certainement pas pour d'autres. Il vous suffit de jeter un coup d'œil à ces ressources patrickhlauke pour avoir une idée de la façon dont le processus de toucher un écran tactile est actuellement géré différemment sur les appareils et les navigateurs.
2 - Le gestionnaire d'événements ne donne aucune idée de son déclencheur initial. Vous avez également tout à fait raison de dire que le event
objet est identique (certainement dans la grande majorité des cas) entre les événements de souris distribués par interaction avec une souris et les événements de souris distribués par interaction tactile.
3 - Toute solution à ce problème qui couvre tous les appareils pourrait bien être de courte durée comme le font les recommandations actuelles du W3C n'entrer pas assez en détail sur la façon dont les événements tactiles/clics doivent être gérés ( https://www.w3.org/TR/touch-events/ ), donc les navigateurs continueront d'avoir différentes implémentations. Il semble également que le document sur les normes Touch Events n'a pas changé au cours des 5 dernières années, donc cela ne va pas se corriger bientôt. https://www.w3.org/standards/history/touch-events
4 - Idéalement, les solutions ne devraient pas utiliser de délais d'attente car il n'y a pas de temps défini d'un événement tactile à un événement souris, et étant donné les spécifications, il n'y aura probablement pas de sitôt. Malheureusement, les délais d'attente sont presque inévitables, comme je l'expliquerai plus tard.
Une solution future:
À l'avenir, la solution sera probablement d'utiliserPointer Events
au lieu d'événements souris/tactiles car ils nous donnent pointerType
( https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events ), mais malheureusement, nous ne sommes pas encore là en termes de norme établie, et donc la compatibilité entre les navigateurs ( https://caniuse.com/#search=pointer%20events ) est médiocre.
Comment résoudre ce problème pour le moment
Si nous acceptons cela:
Ensuite, nous pouvons uniquement utiliser des données sur l'événement de souris lui-même pour déterminer son origine. Comme nous l'avons établi, le navigateur ne fournit pas cela, nous devons donc l'ajouter nous-mêmes. La seule façon de le faire est d'utiliser les événements tactiles qui sont déclenchés à peu près en même temps que l'événement de souris.
En regardant à nouveau les ressources patrickhlauke , nous pouvons faire quelques déclarations:
mouseover
est toujours suivi des événements de clic mousedown
mouseup
et click
- toujours dans cet ordre. (Parfois séparé par d'autres événements). Ceci est soutenu par les recommandations du W3C: https://www.w3.org/TR/touch-events/ .mouseover
est toujours précédé de pointerover
, son homologue MS MSPointerOver
ou touchstart
mouseover
doivent être ignorés. Nous ne pouvons pas établir que l'événement de souris a été déclenché par un événement tactile avant l'événement tactile lui-même a été déclenché.Compte tenu de cela, nous pourrions définir un indicateur pendant pointerover
, MSPointerOver
et touchstart
, et le supprimer lors de l'un des événements de clic. Cela fonctionnerait bien, sauf pour une poignée de cas:
event.preventDefault
est appelé sur l'un des événements tactiles - le drapeau ne sera jamais désactivé car les événements de clic ne seront pas appelés, et donc tout futur événement de clic authentique sur cet élément sera toujours marqué comme événement tactileSi le contenu du document a changé pendant le traitement des événements tactiles, l'agent utilisateur peut alors répartir les événements de souris vers une cible différente de celle des événements tactiles.
Malheureusement, cela signifie que nous devrons toujours utiliser des délais d'attente. À ma connaissance, il n'y a aucun moyen de déterminer quand un événement tactile a appelé event.preventDefault
, ni comprendre quand l'élément tactile a été déplacé dans le DOM et l'événement click déclenché sur un autre élément.
Je pense que c'est un scénario fascinant, donc cette réponse sera modifiée sous peu pour contenir une réponse de code recommandée. Pour l'instant, je recommanderais la réponse fournie par @ibowankenobi ou la réponse fournie par @Manuel Otto.
Ce que nous savons, c'est:
Lorsque l'utilisateur n'utilise pas de souris
mouseover
est directement (dans les 800 ms) déclenché après un touchend
ou un touchstart
(si l'utilisateur a tapé et maintenu).mouseover
et du touchstart
/touchend
sont identiques.Lorsque l'utilisateur utilise une souris/un stylo
mouseover
est déclenché avant les événements tactiles, même si ce n'est pas le cas, la position du mouseover
ne correspondra pas à la position des événements tactiles 99% du temps.Gardant ces points à l'esprit, j'ai créé un extrait de code, qui ajoutera un indicateur triggeredByTouch = true
À l'événement si les conditions énumérées sont remplies. De plus, vous pouvez ajouter ce comportement à d'autres événements de souris ou définir kill = true
Afin de supprimer complètement les événements de souris déclenchés par le toucher.
(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)
Essayez-le:
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;
}
<a href="#" onmouseover="onMouseOver(event)">Click me</a>
Selon https://www.html5rocks.com/en/mobile/touchandmouse/
Pour un seul clic, l'ordre des événements est le suivant:
Ainsi, vous pourrez peut-être définir un booléen arbitraire isFromTouchEvent = true;
dans onTouchStart () et isFromTouchEvent = false;
dans onClick () et vérifiez cela à l'intérieur de onMouseOver (). Cela ne fonctionne pas très bien car nous ne sommes pas garantis d'avoir tous ces événements dans l'élément que nous essayons d'écouter.
J'ai généralement quelques schémas généraux que j'utilise pour cela, l'un d'eux utilise un principe manuel de setTimeout pour déclencher une propriété. J'expliquerai celui-ci ici, mais essayez d'abord de raisonner sur l'utilisation de touchstart, touchmove et touchend sur les appareils tactiles et utilisez la souris sur destop.
Comme vous le savez, l'appel à event.preventDefault (l'événement ne doit pas être passif pour que cela fonctionne avec touchstart) dans l'un des événements de touche annulera les appels de souris suivants, vous n'avez donc pas besoin de les traiter. Mais dans le cas où ce n'est pas ce que vous voulez, voici ce que j'utilise parfois (je me réfère comme "bibliothèque" à votre bibliothèque de manipulation dom, et "elem" comme élément):
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
})
Une autre option sans setTimeout est de penser que mousover est contraire au touchstart et mouseout au touchend. Ainsi, les anciens événements (les événements tactiles) définiront une propriété, si les événements de souris détectent cette propriété, ils ne se déclenchent pas et ne réinitialisent pas la propriété à sa valeur initiale, etc. Dans ce cas, quelque chose dans ce sens fera également l'affaire:
....
.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*/
});
J'ai supprimé beaucoup de détails mais je suppose que c'est l'idée principale.
Vous pouvez utiliser modernizr pour cela! Je viens de tester cela sur un serveur de développement local et cela fonctionne.
if (Modernizr.touch) {
console.log('Touch Screen');
} else {
console.log('No Touch Screen');
}
Alors je commencerais par là?