Cours JavaScript et TypeScript · Module 8: Événements et DOM

Techniques avancées de gestion des événements

5 min de lecture

Des techniques spécialisées pour les cas d'utilisation avancés.

Les visiteurs du parc parcourent de plus en plus souvent la galerie des dinosaures sur leur téléphone ou leur tablette. Là, il n'y a pas de souris, et l'événement click seul ne te dira pas dans quelle direction quelqu'un a fait glisser son doigt. En revanche, le navigateur signale successivement trois événements tactiles : touchstart quand le doigt touche l'écran, touchmove quand il s'y déplace, et touchend quand il le quitte. À partir de ces trois signaux, nous allons construire la reconnaissance des gestes, comme un pisteur qui lit la direction de marche d'un troupeau dans les empreintes laissées dans la boue.

Gérer les gestes tactiles

Toute la logique vivra dans la classe TouchGestureHandler. Le constructeur mémorise l'élément et prépare de la place pour les coordonnées du point de départ et de la position actuelle du doigt.

1// Gestion des gestes tactiles pour les appareils mobiles
2class TouchGestureHandler {
3  constructor(element) {
4    this.element = element;
5    this.startX = 0;
6    this.startY = 0;
7    this.currentX = 0;
8    this.currentY = 0;
9    this.isTracking = false;
10
11    this.bindEvents();
12  }
13
14  bindEvents() {
15    this.element.addEventListener('touchstart', (e) => this.handleTouchStart(e));
16    this.element.addEventListener('touchmove', (e) => this.handleTouchMove(e));
17    this.element.addEventListener('touchend', (e) => this.handleTouchEnd(e));
18  }

La méthode bindEvents() attache trois écouteurs, et les fonctions fléchées conservent this comme instance de la classe. Le drapeau isTracking indique si un geste est en cours. Le premier signal, c'est le contact avec l'écran.

1  handleTouchStart(e) {
2    this.isTracking = true;
3    this.startX = e.touches[0].clientX;
4    this.startY = e.touches[0].clientY;
5    this.currentX = this.startX; // un tapotement sans mouvement = déplacement nul
6    this.currentY = this.startY;
7  }

La propriété e.touches est la liste de tous les doigts qui touchent l'écran à cet instant, et touches[0] est le premier d'entre eux. Ses clientX et clientY fonctionnent exactement comme avec la souris. Les deux dernières lignes sont une correction : elles remettent le déplacement à zéro au départ, donc un simple tapotement sans mouvement ne comparera pas le départ avec la position du geste précédent et ne provoquera pas de glissement fantôme. Passons au mouvement du doigt.

1  handleTouchMove(e) {
2    if (!this.isTracking) return;
3
4    e.preventDefault();
5    this.currentX = e.touches[0].clientX;
6    this.currentY = e.touches[0].clientY;
7
8    const deltaX = this.currentX - this.startX;
9    const deltaY = this.currentY - this.startY;
10
11    // Émettre un événement personnalisé avec les données du geste
12    this.element.dispatchEvent(new CustomEvent('gesture', {
13      detail: { deltaX, deltaY, isActive: true }
14    }));
15  }

L'appel à preventDefault() bloque le défilement de la page pendant le geste, mais il ne fonctionne que dans un écouteur qui n'est pas passif. Ici, pas de problème, car on écoute sur un élément ordinaire. Sur window, document, document.documentElement et body, les navigateurs traitent touchstart et touchmove comme passifs par défaut et ignoreront un tel appel. Pendant le mouvement, la classe émet son propre événement gesture avec le déplacement dans detail, donc une carte peut suivre le doigt en temps réel. À la fin, le doigt quitte l'écran.

1  handleTouchEnd(e) {
2    if (!this.isTracking) return;
3
4    this.isTracking = false;
5    const deltaX = this.currentX - this.startX;
6    const deltaY = this.currentY - this.startY;
7
8    // Déterminer le type de geste
9    if (Math.abs(deltaX) > Math.abs(deltaY)) {
10      // Geste horizontal
11      if (deltaX > 50) {
12        this.element.dispatchEvent(new CustomEvent('swipeRight'));
13      } else if (deltaX < -50) {
14        this.element.dispatchEvent(new CustomEvent('swipeLeft'));
15      }
16    } else {
17      // Geste vertical
18      if (deltaY > 50) {
19        this.element.dispatchEvent(new CustomEvent('swipeDown'));
20      } else if (deltaY < -50) {
21        this.element.dispatchEvent(new CustomEvent('swipeUp'));
22      }
23    }
24
25    this.element.dispatchEvent(new CustomEvent('gestureEnd', {
26      detail: { deltaX, deltaY }
27    }));
28  }
29}

Dans touchend, la liste e.touches ne contient plus le doigt levé, qui se retrouve dans e.changedTouches, c'est pourquoi on utilise les valeurs currentX et currentY mémorisées. La comparaison avec Math.abs() détermine l'axe dominant du mouvement. Le seuil de 50 pixels distingue un geste volontaire d'un tremblement du doigt. Sur l'écran, l'axe Y augmente vers le bas, donc un deltaY positif signifie un glissement vers le bas. La séquence complète est toujours la même : touchstart, une série de touchmove, touchend, puis l'analyse de deltaX et deltaY et l'émission d'un événement personnalisé. Il existe aussi un quatrième événement, touchcancel, qui arrive quand le système interrompt le contact, par exemple quand trop de doigts touchent l'écran à la fois. Dans son gestionnaire, il suffit de réinitialiser le drapeau isTracking.

Utilisation dans la galerie des dinosaures

La classe ne sait rien de la galerie, elle se contente de diffuser des événements. La galerie écoute ceux qui l'intéressent.

1const dinoGallery = document.getElementById('dino-gallery');
2new TouchGestureHandler(dinoGallery);
3
4dinoGallery.addEventListener('swipeLeft', () => showNextDinosaur());
5dinoGallery.addEventListener('swipeRight', () => showPreviousDinosaur());

Un glissement vers la gauche affiche le dinosaure suivant, et vers la droite le précédent. N'oublie pas que CustomEvent ne bouillonne pas par défaut, donc les écouteurs doivent être attachés au même élément que celui qu'on a passé au gestionnaire. C'est exactement cette répartition des rôles que tu as découverte avec les événements personnalisés : un module détecte, l'autre réagit.

Mon conseil : si tu construis de zéro un composant qui doit fonctionner aussi bien à la souris qu'au doigt, jette un œil aux Pointer Events (pointerdown, pointermove, pointerup). Ils gèrent la souris, le stylet et le tactile avec un seul ensemble d'événements. Les Touch Events restent utiles là où tu as besoin de la liste de tous les doigts, par exemple pour le geste de pincement. Dans le bac à sable ci-dessous, tu testeras les gestes sur une zone tactile, et sur ordinateur, aussi avec la souris. Dans la prochaine leçon, nous nous occuperons des performances, y compris des écouteurs passifs.

Retiens bien : un geste est une empreinte dans la boue, que tu ne lis qu'au moment où le dinosaure lève la patte, c'est-à-dire dans touchend.

Code de cette leçon : index.html
1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>Gestes tactiles</title>
7    <style>
8        body { font-family: Arial, sans-serif; background: linear-gradient(135deg, #2c3e50, #34495e); color: white; padding: 20px; touch-action: none; }
9        .touch-area { width: 100%; height: 300px; background: rgba(52, 152, 219, 0.2); border: 2px dashed #3498db; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 24px; margin: 20px 0; }
10        .info { background: rgba(0,0,0,0.6); padding: 15px; border-radius: 8px; margin: 10px 0; }
11    </style>
12</head>
13<body>
14    <h1>Gestes tactiles - Contrôle au toucher</h1>
15    <div class="info">
16        <p>Balaie : ←→ (horizontal) ou ↑↓ (vertical)</p>
17        <p id="gesture-info">En attente d'un geste...</p>
18    </div>
19    <div class="touch-area" id="touch-area">Touch and drag</div>
20    <div class="info">
21        <strong>Gestes détectés :</strong>
22        <ul id="gesture-log"></ul>
23    </div>
24
25    <script>
26        console.log('Touch Gesture Handler initialized');
27
28        const touchArea = document.getElementById('touch-area');
29        const gestureInfo = document.getElementById('gesture-info');
30        const gestureLog = document.getElementById('gesture-log');
31
32        let startX = 0, startY = 0;
33        let currentX = 0, currentY = 0;
34        let isTracking = false;
35
36        function logGesture(message) {
37            const li = document.createElement('li');
38            li.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
39            gestureLog.insertBefore(li, gestureLog.firstChild);
40            if (gestureLog.children.length > 5) {
41                gestureLog.removeChild(gestureLog.lastChild);
42            }
43        }
44
45        touchArea.addEventListener('touchstart', (e) => {
46            isTracking = true;
47            startX = e.touches[0].clientX;
48            startY = e.touches[0].clientY;
49            currentX = startX;
50            currentY = startY;
51            gestureInfo.textContent = 'Gesture started...';
52            touchArea.style.background = 'rgba(46, 204, 113, 0.2)';
53            console.log('Touch start:', startX, startY);
54        });
55
56        touchArea.addEventListener('touchmove', (e) => {
57            if (!isTracking) return;
58            e.preventDefault();
59
60            currentX = e.touches[0].clientX;
61            currentY = e.touches[0].clientY;
62
63            const deltaX = currentX - startX;
64            const deltaY = currentY - startY;
65
66            gestureInfo.textContent = `Δ X: ${Math.round(deltaX)}px, Y: ${Math.round(deltaY)}px`;
67        });
68
69        touchArea.addEventListener('touchend', (e) => {
70            if (!isTracking) return;
71            isTracking = false;
72
73            const deltaX = currentX - startX;
74            const deltaY = currentY - startY;
75
76            touchArea.style.background = 'rgba(52, 152, 219, 0.2)';
77
78            let gesture = 'Non reconnu';
79
80            if (Math.abs(deltaX) > Math.abs(deltaY)) {
81                if (deltaX > 50) {
82                    gesture = '→ Swipe Right';
83                    touchArea.textContent = '→ À droite';
84                } else if (deltaX < -50) {
85                    gesture = '← Swipe Left';
86                    touchArea.textContent = '← À gauche';
87                }
88            } else {
89                if (deltaY > 50) {
90                    gesture = '↓ Swipe Down';
91                    touchArea.textContent = '↓ Down';
92                } else if (deltaY < -50) {
93                    gesture = '↑ Swipe Up';
94                    touchArea.textContent = '↑ Up';
95                }
96            }
97
98            logGesture(gesture);
99            console.log(`Gesture detected: ${gesture}`);
100
101            setTimeout(() => {
102                touchArea.textContent = 'Touch and drag';
103                gestureInfo.textContent = "J'attends le prochain geste...";
104            }, 1500);
105        });
106
107        // Fallback pour la souris (pour tester sur desktop)
108        let mouseDown = false;
109        touchArea.addEventListener('mousedown', (e) => {
110            mouseDown = true;
111            startX = e.clientX;
112            startY = e.clientY;
113            currentX = startX;
114            currentY = startY;
115        });
116
117        touchArea.addEventListener('mousemove', (e) => {
118            if (!mouseDown) return;
119            currentX = e.clientX;
120            currentY = e.clientY;
121            const deltaX = currentX - startX;
122            const deltaY = currentY - startY;
123            gestureInfo.textContent = `Mouse Δ X: ${Math.round(deltaX)}px, Y: ${Math.round(deltaY)}px`;
124        });
125
126        touchArea.addEventListener('mouseup', () => {
127            if (mouseDown) {
128                mouseDown = false;
129                const deltaX = currentX - startX;
130                const deltaY = currentY - startY;
131
132                if (Math.abs(deltaX) > 50 || Math.abs(deltaY) > 50) {
133                    logGesture('Mouse gesture detected');
134                }
135            }
136        });
137
138        console.log('Touch gestures ready - swipe to test!');
139    </script>
140</body>
141</html>

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Quel événement est déclenché lorsque l'utilisateur fait glisser son doigt sur un écran tactile ?

Tâches pratiques dans le jeu

  • Éditeur de code

    Le fichier index.js contient la classe TouchGestureHandler de la leçon sur les gestes. Complète les blancs : ___BLANK1___ est l'événement tactile envoyé pendant que le doigt se déplace sur l'écran, ___BLANK2___ est le nom de la direction quand le doigt est monté de plus de 50 px, et ___BLANK3___ est le champ des options de CustomEvent dans lequel l'événement swipe transporte les données { direction, deltaX, deltaY }. Un déplacement de plus de 50 px doit envoyer un seul événement swipe avec la direction left, right, up ou down, et un simple toucher ou un petit mouvement aucun.

  • Tri vertical

    Place les événements tactiles dans l'ordre où ils se produisent lors d'un balayage du doigt sur l'écran :

Articles utiles