Comment déterminer si un objet de l'événement mouseover provient d'une touche d'écran tactile?

sur pratiquement tous les navigateurs actuels ( détails de patrickhlauke sur github , qui j'ai résumé dans une réponse SO , et aussi un peu plus d'info de QuirksMode ), touche écran déclencheur mouseover événements (créant parfois un pseudo-curseur invisible qui reste où l'utilisateur touché jusqu'à ce qu'ils touchent ailleurs).

parfois, cela provoque un comportement indésirable dans les cas où touch / click et mouseover sont destinés à faire des choses différentes.

de l'intérieur d'une fonction répondant à un événement mouseover, qui a passé l'objet event , est-ce que je peux vérifier s'il s'agit d'un" vrai " mouseover à partir d'un curseur en mouvement qui s'est déplacé de l'extérieur à l'intérieur d'un élément, ou s'il a été causé par ce comportement d'écran tactile à partir d'un toucher d'écran tactile?

l'objet event semble identique. Exemple, sur chrome, un événement mouseover causé par un utilisateur qui touche un écran tactile a type: "mouseover" et rien que je puisse voir qui pourrait l'identifier comme lié au toucher.

j'ai eu l'idée de lier un événement à touchstart qui modifie les événements mouseover puis un événement à touchend qui supprime cette modification. Malheureusement, cela ne fonctionne pas, parce que l'ordre d'événement semble être touchstarttouchendmouseoverclick (Je ne peux pas attacher le relooking normal fonction pour cliquer sans gâcher d'autres fonctionnalités).


Je m'attendais à ce que cette question ait déjà été posée, mais les questions existantes ne suffisent pas:

  • Comment gérer la souris et mouseleave dans Windows 8.1 écran Tactile est sur C# / ASP.Net des applications sur Windows, pas de pages web dans un navigateur
  • JQuery .("cliquez sur") déclencheurs "mouseover" sur le dispositif tactile est similaire, mais est sur jQuery et la réponse est une mauvaise approche (deviner une liste codée dur des agents utilisateurs d'écran tactile, qui se briserait lorsque de nouveaux dispositifs UAs sont créés, et qui suppose faussement tous les dispositifs sont souris ou écran tactile)
  • empêcher le contact de générer des événements mouseOver et mouseMove dans le navigateur Android est le plus proche que j'ai pu trouver, mais il est seulement à propos de Android, est sur la prévention de ne pas identifier mouseover sur le toucher, et n'a pas de réponse
  • le navigateur manipulant l'événement mouseover pour les appareils tactiles provoque l'événement de clic erroné de tirer est lié, mais ils essaient d'élumater le modèle d'interaction à deux claquements iOS, et aussi la seule réponse fait cette erreur de supposer que les touches et la souris/clics sont mutuellement exclusifs.

le mieux que je puisse penser est d'avoir un événement tactile qui fixe un drapeau de variable globalement accessible comme, par exemple, window.touchedRecently = true; sur touchstart mais ne clique pas, puis supprime ce drapeau après, disons, un 500ms setTimeout . C'est un vilain hack.


Note - Nous ne peut pas supposer que les dispositifs d'écran tactile n'ont pas de curseur mobile semblable à une souris ou vice versa, parce qu'il y a de nombreux dispositifs qui utilisent un écran tactile et un stylo mobile qui déplace un curseur en vol stationnaire près de l'écran, ou qui utilisent un écran tactile et une souris (p. ex. 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 mouseover? .

Note #2 - c'est pas une question de jQuery, Mes événements viennent de Raphael.js paths pour lequel jQuery n'est pas une option et qui donne un simple objet de navigateur event . S'il y a une solution spécifique à Raphaël, Je l'accepterais, mais c'est très peu probable et une solution javascript brut serait mieux.

21
demandé sur ℕʘʘḆḽḘ 2016-09-13 19:30:18

5 réponses

ce que nous savons est:

lorsque l'utilisateur n'utilise pas de souris

  • le mouseover est tiré directement (à moins de 800ms) après un touchend ou un touchstart (si l'utilisateur a tapé et tenue).
  • la position du mouseover et du touchstart / touchend sont identiques.

Lorsque l'utilisateur utilise une souris/stylet

  • le mouseover est déclenché avant les évènements de contact, même si ce n'est pas le cas, la position du mouseover ne correspond pas à la position des évènements de contact 99% du temps.

en gardant ces points à l'esprit, j'ai fait un extrait, qui ajoutera un drapeau triggeredByTouch = true à l'événement si les conditions énumérées sont remplies. En outre, vous pouvez ajouter ce comportement à d'autres événements de la souris ou Définir kill = true afin de rejeter les événements de souris déclenchés par le toucher complètement.

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

l'Essayer:

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>
4
répondu Manuel Otto 2018-04-19 15:11:41

étant donné la complexité de la question, j'ai pensé qu'il valait la peine de détailler les questions et les cas de contours impliqués dans toute solution potentielle.

Les questions:

1 - différentes implémentations d'événements tactiles à travers les appareils et les navigateurs . Ce qui fonctionne pour certains ne fonctionnera certainement pas pour d'autres. Il suffit de jeter un coup d'oeil à ces patrickhlauke ressources pour se faire une idée de la façon différente dont le processus de tapoter un écran tactile est actuellement traitée à travers les appareils et les navigateurs.

2 - le gestionnaire d'événements ne donne aucun indice quant à son déclencheur initial. vous avez aussi tout à fait raison de dire que l'objet event est identique (certainement dans la grande majorité des cas) entre les événements de souris envoyés par interaction avec une souris, et les événements de souris distribué par une interaction tactile.

3 - N'importe quelle solution à ce problème qui couvre tous les dispositifs pourrait bien être de courte durée comme les recommandations actuelles du W3C ne vont pas dans assez de détails sur la façon dont les événements de contact/clic devraient être manipulés ( https://www.w3.org/TR/touch-events / ), de sorte que les navigateurs continueront d'avoir différentes implémentations. Il semble également que le document sur les normes relatives aux événements tactiles n'a pas changé au cours des 5 dernières années, donc cela ne va pas se réparer de sitôt. https://www.w3.org/standards/history/touch-events

4 - idéalement, les solutions ne devraient pas utiliser les temps morts car il n'y a pas de temps défini entre l'événement tactile et l'événement de la souris, et étant donné les spécifications, il n'y aura probablement pas de temps avant. Malheureusement, les temps morts sont presque inévitables, comme je l'expliquerai plus tard.


une solution future:

à l'avenir, la solution sera probablement d'utiliser Pointer Events au lieu d'événements souris / toucher comme ceux-ci nous donnent le 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 cross-browser compatibilité ( ) https://caniuse.com/#search=pointer%20events ) est pauvre.


comment résoudre ce problème à l'heure actuelle

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 pouvions, il y a toujours le problème d'événements non tactiles sur un écran tactile

alors nous ne pouvons utiliser des données sur l'événement de la souris elle-même pour déterminer son origine. Comme nous l'avons établi, le navigateur ne fournit pas cela, donc nous devons l'Ajouter nous-mêmes. La seule façon de le faire est d'utiliser la touche événements qui sont déclenchés en même temps que l'événement de la souris.

en regardant les patrickhlauke encore une fois, nous pouvons faire quelques déclarations:

  1. mouseover est toujours suivi par les clics mousedown mouseup et click - toujours dans cet ordre. (Parfois séparés par d'autres événements). Ceci est confirmé 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é par pointerover , son équivalent MS MSPointerOver , ou touchstart
  3. les appareils / navigateurs dont l'ordre d'événement commence par mouseover doivent être ignorés. Nous ne pouvons pas établir que l'événement de la 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 placer un drapeau pendant pointerover , MSPointerOver , et touchstart , et l'enlever pendant 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 touch - le drapeau ne sera jamais désactivé car les événements click ne seront pas appelés, et donc tout futur événement click véritable sur cet élément serait encore marqué comme un événement touch
  2. si l'élément cible est déplacé lors de l'événement. Les recommandations du W3C indiquent

si le contenu du document a changé lors du traitement de la toucher les événements, puis l'agent utilisateur peut envoyer les événements de la souris à un autre cible que les événements tactiles.


malheureusement, cela signifie que nous aurons toujours besoin d'utiliser des temps morts. À ma connaissance , il n'y a aucun moyen d'établir quand un événement tactile a appelé event.preventDefault , ni de comprendre quand l'élément tactile a été déplacé dans le DOM et que l'événement de clic a été 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 code recommandé. Pour l'instant, je recommande la réponse de @ibowankenobi ou celle de @Manuel Otto.

4
répondu Perran Mitchell 2018-04-23 18:08:52

selon https://www.html5rocks.com/en/mobile/touchandmouse /

pour un simple clic l'ordre des événements est:

  1. évènements touchstart
  2. touchmove
  3. touchend
  4. mouseover
  5. mousemove
  6. mousedown
  7. mouseup
  8. , cliquez sur

ainsi vous pourriez être en mesure de définir un booléen arbitraire isFromTouchEvent = true; dans onTouchStart() et isFromTouchEvent = false; dans onClick() et de vérifier l'intérieur de onMouseOver(). Cela ne fonctionne pas très bien car nous ne sommes pas sûrs d'avoir tous ces événements dans l'élément que nous essayons d'écouter.

3
répondu Glen Pierce 2017-01-04 01:53:26

vous pouvez utiliser modernizr pour cela! Je viens de le tester sur un serveur de développement local et ça marche.

if (Modernizr.touch) { 
  console.log('Touch Screen');
} else { 
  console.log('No Touch Screen');
} 

donc je commencerais par là?

2
répondu Joseph Chambers 2016-09-13 16:58:07

j'ai habituellement 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é. Je vais expliquer celui-ci ici, mais d'abord essayer de raisonner sur l'utilisation de touchstart, touchmove et touchend sur les appareils tactiles et utiliser mouseover sur destop.

comme vous le savez, événement appelant.preventDefault (l'événement ne doit pas être passif pour que cela fonctionne avec touchstart) dans l'un des événements touchevents annulera les appels à souris suivants ainsi vous n'avez pas besoin de traiter avec eux. Mais au 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 votre é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 contre touchstart et mouseout contre touchend. Si l'ancien événements (les événements tactiles) permettra de définir une propriété, si les événements de souris détecter la propriété, alors qu'ils ne pas de feu et de réinitialiser la propriété à sa valeur initiale, et ainsi de suite. Dans ce cas, quelque chose de ce genre fera aussi 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 enlevé beaucoup de détails, mais je suppose que c'est l'idée principale.

2
répondu ibrahim tanyalcin 2018-04-23 00:56:59