Le gestionnaire d’événements tactiles annule les gestionnaires de clics

Je crée une directive personnalisable dans AngularJS. C’est une combinaison d’événements jQuery et de javascript vanillé. J’essaie de le rendre aussi générique et réutilisable que possible, et il doit également être tactile.

TL; DR

Je ne peux pas cliquer sur le bouton de ma directive déplaçable dans les environnements tactiles.

Étapes à suivre pour reproduire:

  1. Ouvrez l’exemple codepen : CodePen
  2. Sur Chrome, F12, émuler iPad 3/4
  3. Faites glisser le panneau par le titre = fonctionne!
  4. Cliquez sur le bouton = pas d’alerte.

Explication plus longue

La directive rend éventuellement l’élément entier sur lequel il est placé déplaçable, à moins qu’un élément avec la classe “drag-handle” soit placé, auquel cas il est utilisé comme poignée de glissement pour l’élément. J’utilise normalement cela avec les panneaux de démarrage, comme c’est un exemple simple.

La directive fonctionne très bien sur le bureau, mais sur les périphériques tactiles, s’il existe des éléments cliquables sur une poignée de glisser, le gestionnaire de glissement annule l’événement click et il n’est jamais appelé.

Exemple HTML serait:

Example Title
Example body

Ainsi, sur les ordinateurs de bureau, vous pouvez faire glisser le panneau et cliquer sur le bouton pour obtenir l’alerte. Toutefois, lorsque j’émule l’iPad 3/4 sur Chrome (ou que je l’utilise sur un véritable iPad), le clic n’est jamais déclenché.

Ma directive est ci-dessous. Il définit le conteneur sur absolu (à moins que le conteneur ne soit déjà fixé, auquel cas il compensera et le rendra toujours déplaçable.

  /* * @summary * Directive that makes an element draggable. * @description * This directive should be used in conjunction with specifying a drag handle * on the element. If not, then entire element will be draggable. * @example * 
*
This will be the drag handle
*
This will be dragged
*
*/ angular.module("app") .directive('appDraggable', appDraggable); function appDraggable() { var directive = { ressortingct: 'A', link: link }; function link(scope, element) { var startX = 0, startY = 0, x = 0, y = 0; var startTop; var startLeft; var dragHandle = element[0].querySelector(".drag-handle"); var dragHandleElement; /* * If there is a dragHandle specified, add the touch events to it. * Otherwise, make the entire element draggable. */ if (dragHandle) { dragHandleElement = angular.element(dragHandle); addTouchHandlers(dragHandle); } else { dragHandleElement = element; addTouchHandlers(element[0]); } var position = element.css('position'); if (position !== "absolute") { if (position === "fixed") { // If fixed, get the start offset relative to the document. startTop = element.offset().top; startLeft = element.offset().left; /* * Explicitly set the height and width of the element to prevent * overrides by preset values. */ var height = parseInt(element.height(), 10); var width = parseInt(element.width(), 10); element.css({ height: height, width: width }); } else { // If it's not fixed, it needs to be absolute. element.css({ position: 'absolute', }); // And positioned originally relative to the parent. startTop = element.position().top; startLeft = element.position().left; } } /* * @function * @description * Add event handlers to the drag handle to capture events. */ dragHandleElement.on('mousedown', function (event) { /* * Prevent default dragging of selected content */ event.preventDefault(); startX = event.pageX - x; startY = event.pageY - y; dragHandleElement.on('mousemove', mousemove); dragHandleElement.on('mouseup', mouseup); }); function mousemove(event) { y = event.pageY - startY; x = event.pageX - startX; var finalTop = y + startTop; var finalLeft = x + startLeft; element.css({ top: finalTop + 'px', left: finalLeft + 'px' }); } function mouseup() { dragHandleElement.off('mousemove', mousemove); dragHandleElement.off('mouseup', mouseup); } function touchHandler(event) { var touch = event.changedTouches[0]; if (event.target !== dragHandleElement) { //////////////// HACK /////////////////////////// //event.target.click(); // Hack as a work around. } var simulatedEvent = document.createEvent("MouseEvent"); simulatedEvent.initMouseEvent({ touchstart: "mousedown", touchmove: "mousemove", touchend: "mouseup" }[event.type], true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null); touch.target.dispatchEvent(simulatedEvent); event.preventDefault(); } function addTouchHandlers(element) { element.addEventListener("touchstart", touchHandler, true); element.addEventListener("touchmove", touchHandler, true); element.addEventListener("touchend", touchHandler, true); element.addEventListener("touchcancel", touchHandler, true); } } return directive; }

Vous remarquerez qu’il y a un bidouillage dans la directive ci-dessus:

 if (event.target !== dragHandleElement) { //////////////// HACK /////////////////////////// //event.target.click(); // Hack as a work around. } 

Si je ne commente pas cela, cela fonctionne sur les appareils tactiles, car cela vérifie si la cible tactile est le dragHandle et si ce n’est pas le cas, clique manuellement sur la cible. Cela fonctionne, mais me semble méchant et j’aimerais vraiment une meilleure solution. Il ne retourne pas false ou stopPropagation car la cible n’est pas toujours le dragHandle directement, mais elle doit tout de même faire glisser.

Je ne sais pas pourquoi cela ne fonctionne pas, car il n’arrête pas manuellement la propagation de l’événement touch, car il utilise event.preventDefault au lieu de event.stopPropagation, mais je suis certain qu’il me manque quelque chose.

Vous pouvez reproduire ici .

De plus, toute autre recommandation sur la façon d’améliorer le code ci-dessus afin d’être plus agnostique ou plus robuste pour les périphériques de plate-forme est la bienvenue!

Pensées?

Merci!

Trouvé le problème.

Ma fonction touchHandler ci-dessus transmet toujours un événement “mousedown” au toucher, même s’il s’agissait plus précisément d’un événement “clic” qu’il devrait transmettre. Étant donné que tous mes gestionnaires d’événements recherchaient un événement “clic”, ils ignoraient l’événement “mousedown” en cours de transmission.

J’ai modifié ma fonction touchHandler en bas et cela fonctionne à merveille.

  var mouseMoved = false; function touchHandler(event) { // Declare the default mouse event. var mouseEvent = "mousedown"; // Create the event to transmit. var simulatedEvent = document.createEvent("MouseEvent"); switch (event.type) { case "touchstart": mouseEvent = "mousedown"; break; case "touchmove": /* * If this has been hit, then it's a move and a mouseup, not a click * will be transmitted. */ mouseMoved = true; mouseEvent = "mousemove"; break; case "touchend": /* * Check to see if a touchmove event has been fired. If it has * it means this have been a move and not a click, if not * transmit a mouse click event. */ if (!mouseMoved) { mouseEvent = "click"; } else { mouseEvent = "mouseup"; } // Initialize the mouseMove flag back to false. mouseMoved = false; break; } var touch = event.changedTouches[0]; /* * Build the simulated mouse event to fire on touch events. */ simulatedEvent.initMouseEvent(mouseEvent, true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null); /* * Transmit the simulated event to the target. This, in combination * with the case statement above, ensures that click events are still * transmitted and bubbled up the chain. */ touch.target.dispatchEvent(simulatedEvent); /* * Prevent default dragging of element. */ event.preventDefault(); } 

Cette implémentation recherche un événement touchstart entre touchstart et touchend . S’il en existe un, il définit un indicateur et transmet un événement de click au lieu d’un événement de mousedown .

Il pourrait également être utilisé en conjonction avec une timer, de sorte que même si la souris était déplacée légèrement, elle transmettrait un événement de clic, mais pour les besoins de mon travail, cela fonctionne à merveille.