web-dev-qa-db-fra.com

Comment savoir si un objet événement survolant la souris provient d'un écran tactile tactile?

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 touchstarttouchendmouseoverclick (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.

26

É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:

  1. Vous ne pouvez pas détecter un écran tactile ( http://www.stucox.com/blog/you-cant-detect-a-touchscreen/ )
  2. Même si nous le pouvions, il y a toujours le problème des événements non tactiles sur un écran tactile

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:

  1. mouseover est toujours suivi des événements de clic mousedownmouseup 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/ .
  2. Pour la plupart des appareils/navigateurs, l'événement mouseover est toujours précédé de pointerover, son homologue MS MSPointerOver ou touchstart
  3. Les appareils/navigateurs dont l'ordre des événements commence par 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:

  1. 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 tactile
  2. si l'élément cible est déplacé pendant l'événement. Les recommandations du W3C indiquent

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

7
Perran Mitchell

Ce que nous savons, c'est:

Lorsque l'utilisateur n'utilise pas de souris

  • le mouseover est directement (dans les 800 ms) déclenché après un touchend ou un touchstart (si l'utilisateur a tapé et maintenu).
  • la position du mouseover et du touchstart/touchend sont identiques.

Lorsque l'utilisateur utilise une souris/un stylo

  • Le 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>
5
Manuel Otto

Selon https://www.html5rocks.com/en/mobile/touchandmouse/
Pour un seul clic, l'ordre des événements est le suivant:

  1. toucher
  2. toucher
  3. toucher
  4. survoler
  5. déplacer la souris
  6. souris vers le bas
  7. souris
  8. cliquez sur

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.

4
Glen Pierce

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

avec 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
})

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:

sans 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*/
                });

J'ai supprimé beaucoup de détails mais je suppose que c'est l'idée principale.

3
ibrahim tanyalcin

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à?

2
Joseph Chambers