Cours JavaScript et TypeScript · Module 8: Événements et DOM
Supprimer les écouteurs d'événements et nettoyer la mémoire
Parfois, il faut supprimer des écouteurs d'événements pour qu'ils n'occupent plus de mémoire ou qu'ils cessent de réagir aux événements. C'est comme éteindre les systèmes d'alarme devenus inutiles dans le parc.
« La pire alarme, c'est celle qui surveille un enclos vide », dit Ray Arnold. « L'enclos a été démonté il y a un mois, mais le capteur consomme toujours du courant et réveille de temps en temps toute l'équipe de sécurité. » Dans une application, c'est pareil : un écouteur que personne n'a retiré continue de réagir aux événements et garde en mémoire tout ce qu'il touche.
Bien gérer les écouteurs d'événements
Pour supprimer un écouteur, on utilise la méthode removeEventListener(). Elle prend les mêmes arguments que addEventListener() : le type d'événement et la fonction de gestion.
1// Fonction qui gère les clics
2function handleEmergencyClick() {
3 console.log("Activation du protocole d'urgence !");
4 evacuatePark();
5}
6
7const emergencyButton = document.getElementById('emergency-btn');
8
9// Ajout de l'écouteur d'événement
10emergencyButton.addEventListener('click', handleEmergencyClick);
11
12// Suppression de l'écouteur d'événement (il faut la même fonction !)
13emergencyButton.removeEventListener('click', handleEmergencyClick);Le bouton d'alarme est de nouveau sourd, car le navigateur a trouvé dans sa liste exactement la fonction qu'on avait attachée plus tôt. Voici le cycle de vie complet d'un écouteur : l'ajout, le déclenchement de l'événement, l'exécution de la fonction et, pour finir, la suppression. Le bouton lui-même ne disparaît nulle part, seul son capteur disparaît.
Le piège : la fonction anonyme
L'erreur la plus fréquente lors du nettoyage a l'air innocente. Deux fonctions fléchées identiques semblent être la même chose, mais pour JavaScript, ce sont deux objets différents.
1// Ceci NE supprimera PAS l'écouteur - deux fonctions différentes avec le même code
2emergencyButton.addEventListener('click', () => evacuatePark());
3emergencyButton.removeEventListener('click', () => evacuatePark());L'écouteur reste en place, car JavaScript compare les références des fonctions, et non leur code source. Si tu prévois une suppression, enregistre la fonction sous un nom ou dans une variable. Pense aussi au drapeau de la phase de capture : un écouteur ajouté avec true ne peut être supprimé que par un appel qui passe lui aussi true.
Un écouteur temporaire
Parfois, un capteur ne doit fonctionner que pendant une durée précise, par exemple pendant un exercice d'alarme. On le retire alors une fois le délai écoulé, dans setTimeout().
1// Exemple avec un écouteur d'événement temporaire
2function temporaryEventHandler() {
3 console.log('Cet écouteur ne sera actif que pendant 10 secondes');
4}
5
6const tempButton = document.getElementById('temp-button');
7tempButton.addEventListener('click', temporaryEventHandler);
8
9// Suppression automatique au bout de 10 secondes
10setTimeout(() => {
11 tempButton.removeEventListener('click', temporaryEventHandler);
12 console.log("L'écouteur a été supprimé");
13}, 10000);Au bout de dix secondes, les clics sur le bouton ne font plus rien, alors que le bouton est toujours sur la page. Quand un écouteur ne doit agir qu'une fois, il est plus simple d'utiliser l'option { once: true } vue dans la leçon sur les écouteurs, car le navigateur le retirera lui-même après le premier appel.
AbortController - un seul interrupteur pour plusieurs capteurs
Quand il y a plusieurs écouteurs, garder la trace de toutes les références devient fatigant. C'est là que AbortController vient à la rescousse : tu crées un contrôleur, tu passes son signal dans les options de l'écouteur, et un seul appel à abort() retire tout d'un coup. Voyons cela avec le défilement infini du catalogue des dinosaures.
1const scrollController = new AbortController();
2
3window.addEventListener('scroll', () => {
4 const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
5
6 if (scrollTop + clientHeight >= scrollHeight - 100) {
7 loadMoreDinosaurs();
8 }
9}, { signal: scrollController.signal });
10
11// Le catalogue est terminé - on coupe le capteur d'un seul geste
12function onCatalogEnd() {
13 scrollController.abort();
14}La propriété scrollTop indique de combien la page a défilé, clientHeight est la hauteur de la fenêtre visible, et scrollHeight la hauteur de toute la page. Quand il reste moins de 100 pixels jusqu'à la fin, on charge le lot suivant. Remarque que la fonction de gestion n'a pas de nom, et pourtant on peut la supprimer, car le navigateur l'a liée au signal du contrôleur. Tu peux passer le même signal à autant d'écouteurs que tu veux. Un contrôleur déjà utilisé ne fonctionne plus, alors pour de nouveaux écouteurs, tu en crées un nouveau.
D'où viennent les fuites de mémoire
Une fuite de mémoire, c'est une situation dans laquelle un objet devenu inutile ne peut pas être libéré, car quelque chose pointe encore vers lui. Le moteur JavaScript ne libère que ce vers quoi plus aucune référence ne mène. Avec les événements, le scénario typique est le suivant : un écouteur est attaché à window ou à document, qui vivent aussi longtemps que la page, et sa fonction fait référence à une carte supprimée. La carte a disparu de l'écran, mais pas de la mémoire, car l'écouteur la retient toujours. Une variable oubliée qui pointe encore vers un élément supprimé fait la même chose.
Le nettoyage en quatre étapes
Supprimer en toute sécurité un élément équipé d'un capteur suit toujours la même procédure, comme la fermeture d'un enclos selon une checklist.
1let raptorCard = document.getElementById('raptor-card');
2
3function handleCardClick() {
4 console.log('Carte du raptor cliquée');
5}
6
7raptorCard.addEventListener('click', handleCardClick);
8
9// Nettoyage, quand la carte doit disparaître
10raptorCard.removeEventListener('click', handleCardClick);
11raptorCard.remove();
12raptorCard = null;Cette procédure est une bonne habitude : conserve une référence à la fonction de gestion, appelle removeEventListener() avec cette même référence, retire l'élément du DOM, et pour finir, remets les variables à zéro en leur affectant null. Les moteurs actuels libèrent un élément retiré avec ses propres écouteurs dès que plus rien n'y mène, donc pour la mémoire ce qui compte le plus, ce sont les variables et les écouteurs sur window ou document qui font encore référence à la carte.
Mon ordre de choix des outils : { once: true } pour les réactions ponctuelles, AbortController quand un composant a plusieurs écouteurs, et removeEventListener() quand tu retires un capteur précis. Dans le bac à sable ci-dessous, tu peux ajouter et supprimer des écouteurs en observant ce qui se passe. Dans la leçon sur les composants, nous réunirons tout le nettoyage dans une seule méthode destroy().
Retiens bien : chaque capteur que tu allumes dans le parc doit avoir une extinction prévue.
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>Supprimer les écouteurs d'événements</title>
7 <style>
8 body {
9 font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
10 background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
11 margin: 0;
12 padding: 20px;
13 color: white;
14 min-height: 100vh;
15 }
16
17 .container {
18 max-width: 800px;
19 margin: 0 auto;
20 }
21
22 h1 {
23 text-align: center;
24 margin-bottom: 30px;
25 }
26
27 .demo-section {
28 background: rgba(0, 0, 0, 0.7);
29 border-radius: 15px;
30 padding: 30px;
31 margin-bottom: 20px;
32 border: 2px solid #34495e;
33 }
34
35 h2 {
36 color: #3498db;
37 margin-bottom: 20px;
38 }
39
40 .button-group {
41 display: grid;
42 grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
43 gap: 15px;
44 margin: 20px 0;
45 }
46
47 button {
48 padding: 12px 20px;
49 border: none;
50 border-radius: 8px;
51 font-size: 16px;
52 font-weight: bold;
53 cursor: pointer;
54 transition: all 0.3s ease;
55 }
56
57 .btn-start {
58 background: linear-gradient(45deg, #27ae60, #2ecc71);
59 color: white;
60 }
61
62 .btn-stop {
63 background: linear-gradient(45deg, #e74c3c, #c0392b);
64 color: white;
65 }
66
67 .btn-action {
68 background: linear-gradient(45deg, #3498db, #2980b9);
69 color: white;
70 }
71
72 button:hover {
73 transform: translateY(-2px);
74 box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
75 }
76
77 button:disabled {
78 background: #7f8c8d;
79 cursor: not-allowed;
80 transform: none;
81 }
82
83 .dino-box {
84 background: rgba(52, 152, 219, 0.2);
85 border: 2px solid #3498db;
86 border-radius: 10px;
87 padding: 30px;
88 text-align: center;
89 margin: 20px 0;
90 cursor: pointer;
91 transition: all 0.3s ease;
92 }
93
94 .dino-box:hover {
95 transform: scale(1.05);
96 box-shadow: 0 10px 30px rgba(52, 152, 219, 0.3);
97 }
98
99 .dino-box.inactive {
100 opacity: 0.5;
101 border-color: #7f8c8d;
102 cursor: not-allowed;
103 }
104
105 .dino-emoji {
106 font-size: 80px;
107 display: block;
108 margin-bottom: 10px;
109 }
110
111 .status {
112 display: inline-block;
113 padding: 5px 15px;
114 border-radius: 20px;
115 margin-top: 10px;
116 font-weight: bold;
117 }
118
119 .status.active {
120 background: #27ae60;
121 }
122
123 .status.inactive {
124 background: #e74c3c;
125 }
126
127 .log {
128 background: #000;
129 border-radius: 8px;
130 padding: 15px;
131 max-height: 200px;
132 overflow-y: auto;
133 font-family: 'Courier New', monospace;
134 font-size: 13px;
135 margin-top: 20px;
136 }
137
138 .log-entry {
139 padding: 3px 0;
140 border-bottom: 1px solid #333;
141 }
142
143 .log-entry.add {
144 color: #2ecc71;
145 }
146
147 .log-entry.remove {
148 color: #e74c3c;
149 }
150
151 .log-entry.event {
152 color: #3498db;
153 }
154
155 .timer-display {
156 font-size: 48px;
157 text-align: center;
158 margin: 20px 0;
159 color: #3498db;
160 font-weight: bold;
161 }
162
163 .info-box {
164 background: rgba(241, 196, 15, 0.1);
165 border: 1px solid #f1c40f;
166 border-radius: 8px;
167 padding: 15px;
168 margin: 20px 0;
169 color: #f1c40f;
170 }
171 </style>
172</head>
173<body>
174 <div class="container">
175 <h1>Supprimer les écouteurs d'événements</h1>
176
177 <!-- Demo 1: Ajout/suppression basique -->
178 <div class="demo-section">
179 <h2>Démo 1 : zone de dinosaure cliquable</h2>
180 <p>Clique sur « Démarrer l'écouteur » pour activer le champ, puis « Arrêter l'écouteur » pour le désactiver.</p>
181
182 <div class="button-group">
183 <button id="start-listener" class="btn-start">Démarrer l'écouteur</button>
184 <button id="stop-listener" class="btn-stop">Arrêter l'écouteur</button>
185 </div>
186
187 <div id="dino-box" class="dino-box inactive">
188 <span class="dino-emoji">◆</span>
189 <h3>T-Rex</h3>
190 <p>Clique-moi si je suis actif !</p>
191 <span class="status inactive" id="dino-status">INACTIF</span>
192 </div>
193
194 <div class="log" id="demo1-log"></div>
195 </div>
196
197 <!-- Demo 2: Timer avec suppression automatique -->
198 <div class="demo-section">
199 <h2>Démo 2 : minuteur temporaire</h2>
200 <p>Le minuteur ne sera actif que 10 secondes, puis s'éteindra automatiquement.</p>
201
202 <div class="button-group">
203 <button id="start-timer" class="btn-start">Démarrer le minuteur (10s)</button>
204 <button id="stop-timer" class="btn-stop">Arrêter le minuteur</button>
205 </div>
206
207 <div class="timer-display" id="timer-display">00:00</div>
208
209 <div class="info-box" id="timer-info">
210 Press "Start Timer" to begin countdown. Event listener will be automatically removed after 10 seconds.
211 </div>
212
213 <div class="log" id="demo2-log"></div>
214 </div>
215
216 <!-- Demo 3: Multiple listeners -->
217 <div class="demo-section">
218 <h2>Démo 3 : gérer plusieurs écouteurs</h2>
219 <p>Contrôle différents types d'événements indépendamment.</p>
220
221 <div class="button-group">
222 <button id="add-click" class="btn-start">Ajouter Click</button>
223 <button id="remove-click" class="btn-stop">Retirer Click</button>
224 <button id="add-hover" class="btn-start">Ajouter Hover</button>
225 <button id="remove-hover" class="btn-stop">Retirer Hover</button>
226 </div>
227
228 <div id="multi-dino" class="dino-box">
229 <span class="dino-emoji">◆</span>
230 <h3>Triceratops</h3>
231 <p>Teste différents événements !</p>
232 <div>
233 <span class="status inactive" id="click-status">Click: OFF</span>
234 <span class="status inactive" id="hover-status">Hover: OFF</span>
235 </div>
236 </div>
237
238 <div class="log" id="demo3-log"></div>
239 </div>
240 </div>
241
242 <script>
243 console.log('Event Listener Management System - START');
244
245 // DEMO 1: Ajout/suppression basique
246 const dinoBox = document.getElementById('dino-box');
247 const dinoStatus = document.getElementById('dino-status');
248 const demo1Log = document.getElementById('demo1-log');
249
250 function logDemo1(message, type = 'event') {
251 const entry = document.createElement('div');
252 entry.className = `log-entry ${type}`;
253 entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
254 demo1Log.appendChild(entry);
255 demo1Log.scrollTop = demo1Log.scrollHeight;
256 }
257
258 // Click handler function - MUST be named to be removable
259 function handleDinoClick() {
260 console.log('T-Rex was clicked!');
261 logDemo1('T-Rex was clicked!', 'event');
262 dinoBox.style.transform = 'scale(1.1)';
263 setTimeout(() => {
264 dinoBox.style.transform = 'scale(1)';
265 }, 200);
266 }
267
268 // Start listener
269 document.getElementById('start-listener').addEventListener('click', function() {
270 dinoBox.addEventListener('click', handleDinoClick);
271 dinoBox.classList.remove('inactive');
272 dinoStatus.textContent = 'ACTIF';
273 dinoStatus.className = 'status active';
274 logDemo1('Event listener AJOUTÉ', 'add');
275 console.log('Event listener ajouté au T-Rex');
276 });
277
278 // Stop listener
279 document.getElementById('stop-listener').addEventListener('click', function() {
280 dinoBox.removeEventListener('click', handleDinoClick);
281 dinoBox.classList.add('inactive');
282 dinoStatus.textContent = 'INACTIF';
283 dinoStatus.className = 'status inactive';
284 logDemo1('Event listener REMOVED', 'remove');
285 console.log('Event listener removed from T-Rex');
286 });
287
288 // DEMO 2: Timer avec suppression automatique
289 const timerDisplay = document.getElementById('timer-display');
290 const timerInfo = document.getElementById('timer-info');
291 const demo2Log = document.getElementById('demo2-log');
292
293 let timerInterval = null;
294 let timerTimeout = null;
295 let seconds = 0;
296
297 function logDemo2(message, type = 'event') {
298 const entry = document.createElement('div');
299 entry.className = `log-entry ${type}`;
300 entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
301 demo2Log.appendChild(entry);
302 demo2Log.scrollTop = demo2Log.scrollHeight;
303 }
304
305 function updateTimer() {
306 seconds++;
307 const mins = Math.floor(seconds / 60).toString().padStart(2, '0');
308 const secs = (seconds % 60).toString().padStart(2, '0');
309 timerDisplay.textContent = `${mins}:${secs}`;
310 }
311
312 function stopTimer() {
313 if (timerInterval) {
314 clearInterval(timerInterval);
315 timerInterval = null;
316 logDemo2('Timer ARRÊTÉ', 'remove');
317 timerInfo.textContent = 'Timer has been stopped.';
318 timerInfo.style.borderColor = '#e74c3c';
319 }
320 if (timerTimeout) {
321 clearTimeout(timerTimeout);
322 timerTimeout = null;
323 }
324 }
325
326 document.getElementById('start-timer').addEventListener('click', function() {
327 stopTimer(); // Arrête le timer précédent
328 seconds = 0;
329 timerDisplay.textContent = '00:00';
330
331 timerInterval = setInterval(updateTimer, 1000);
332 logDemo2('Timer DÉMARRÉ (arrêt automatique dans 10s)', 'add');
333 timerInfo.textContent = 'Timer active! Auto-disable in 10 seconds...';
334 timerInfo.style.borderColor = '#f1c40f';
335
336 // Auto-remove after 10 seconds
337 timerTimeout = setTimeout(() => {
338 stopTimer();
339 logDemo2('Timer automatically DISABLED after 10 seconds', 'remove');
340 timerInfo.textContent = 'Timer was automatically disabled after 10 seconds. Event listener has been removed!';
341 timerInfo.style.borderColor = '#27ae60';
342 console.log('Timer automatiquement arrêté et nettoyé');
343 }, 10000);
344 });
345
346 document.getElementById('stop-timer').addEventListener('click', stopTimer);
347
348 // DEMO 3: Multiple listeners
349 const multiDino = document.getElementById('multi-dino');
350 const clickStatus = document.getElementById('click-status');
351 const hoverStatus = document.getElementById('hover-status');
352 const demo3Log = document.getElementById('demo3-log');
353
354 function logDemo3(message, type = 'event') {
355 const entry = document.createElement('div');
356 entry.className = `log-entry ${type}`;
357 entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
358 demo3Log.appendChild(entry);
359 demo3Log.scrollTop = demo3Log.scrollHeight;
360 }
361
362 // Handler functions - must be named
363 function handleMultiClick() {
364 logDemo3('Triceratops CLICKED!', 'event');
365 console.log('Click event');
366 }
367
368 function handleMultiMouseEnter() {
369 logDemo3('Survol du Triceratops', 'event');
370 multiDino.style.borderColor = '#2ecc71';
371 }
372
373 function handleMultiMouseLeave() {
374 multiDino.style.borderColor = '#3498db';
375 }
376
377 // Ajout du Click Listener
378 document.getElementById('add-click').addEventListener('click', function() {
379 multiDino.addEventListener('click', handleMultiClick);
380 clickStatus.textContent = 'Click: ON';
381 clickStatus.className = 'status active';
382 logDemo3('Click listener AJOUTÉ', 'add');
383 });
384
385 // Suppression du Click Listener
386 document.getElementById('remove-click').addEventListener('click', function() {
387 multiDino.removeEventListener('click', handleMultiClick);
388 clickStatus.textContent = 'Click: OFF';
389 clickStatus.className = 'status inactive';
390 logDemo3('Click listener REMOVED', 'remove');
391 });
392
393 // Adding Hover Listeners
394 document.getElementById('add-hover').addEventListener('click', function() {
395 multiDino.addEventListener('mouseenter', handleMultiMouseEnter);
396 multiDino.addEventListener('mouseleave', handleMultiMouseLeave);
397 hoverStatus.textContent = 'Hover: ON';
398 hoverStatus.className = 'status active';
399 logDemo3('Hover listeners AJOUTÉS', 'add');
400 });
401
402 // Removing Hover Listeners
403 document.getElementById('remove-hover').addEventListener('click', function() {
404 multiDino.removeEventListener('mouseenter', handleMultiMouseEnter);
405 multiDino.removeEventListener('mouseleave', handleMultiMouseLeave);
406 hoverStatus.textContent = 'Hover: OFF';
407 hoverStatus.className = 'status inactive';
408 multiDino.style.borderColor = '#3498db';
409 logDemo3('Hover listeners REMOVED', 'remove');
410 });
411
412 // Cleanup on page close
413 window.addEventListener('beforeunload', function() {
414 stopTimer();
415 console.log('All event listeners cleaned up before closing');
416 });
417
418 logDemo1('Système prêt à fonctionner', 'add');
419 logDemo2('Système prêt à fonctionner', 'add');
420 logDemo3('Système prêt à fonctionner', 'add');
421 </script>
422</body>
423</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. Qu'est-ce qui peut provoquer une fuite mémoire liée aux écouteurs d'événements ?
2. Pourquoi est-il important de supprimer les écouteurs d'événements quand un élément est retiré du DOM ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Tri vertical
Place les éléments pour utiliser AbortController afin de supprimer un écouteur d'événement :
- Éditeur de code
Le fichier index.html contient une galerie #gallery avec trois miniatures (l'attribut data-full indique la grande version) et une lightbox cachée #lightbox (style.css l'affiche avec la classe open). Complète les blancs du script : ___BLANK1___ est l'événement de clic, et ___BLANK2___ est la propriété de l'objet event qui désigne l'élément cliqué (on vérifie si c'est un IMG). Un clic sur une photo doit ouvrir la lightbox avec la grande version de cette photo et la légende tirée de alt, et un clic dans l'espace entre les photos ne doit pas l'ouvrir.
- Tri vertical
Place dans l'ordre les phases du cycle de vie d'un écouteur d'événement :
- Éditeur de code
Le fichier index.html contient une liste défilante #dino-list (style.css lui donne une hauteur de 300 px et overflow-y: auto). Le script charge les dinosaures par lots de 10. Complète les blancs : ___BLANK1___ est la méthode qui ajoute un nouvel élément li à la fin de la liste, et ___BLANK2___ est la propriété qui donne la hauteur de tout le contenu de la liste (scrollTop correspond aux pixels défilés, clientHeight à la hauteur de la zone visible). Quand il reste moins de 50 pixels avant la fin de la liste, elle doit charger les 10 éléments suivants, et ne rien ajouter avant.
- Éditeur de code
Le fichier index.html contient un bouton #menu-toggle et un menu #main-menu. Sur un écran étroit, style.css n'affiche le menu que s'il a la classe open (sur un écran de plus de 900 px, le menu est toujours visible et le bouton est caché). Complète les blancs : ___BLANK1___ est l'événement de clic, et ___BLANK2___ est la méthode de classList qui ajoute la classe open quand elle manque et la retire quand elle est présente (en renvoyant true ou false). Le premier clic doit déplier le menu et mettre aria-expanded="true", le second le replier et mettre aria-expanded="false".
- Éditeur de code
Le fichier index.html contient un champ #dino-search et une liste de suggestions #suggestions, et le script a un tableau d'espèces de dinosaures. Complète les blancs : ___BLANK1___ est l'événement que le champ envoie après chaque modification du texte saisi, et ___BLANK2___ est la méthode de chaîne qui vérifie si un nom commence par le fragment saisi. Après avoir tapé « tri », la liste doit afficher seulement Triceratops (les majuscules ne comptent pas), un champ vide ou un texte sans correspondance vide la liste, et un clic sur une suggestion la place dans le champ.
- Éditeur de code
Le fichier index.html contient un panneau avec un paragraphe #status, et le script a un seul écouteur keydown sur le document. Complète les blancs : ___BLANK1___ est la méthode de l'objet event qui empêche le navigateur d'enregistrer la page avec Ctrl+S, ___BLANK2___ est la valeur de event.key pour le raccourci Ctrl+O (lettre minuscule), et ___BLANK3___ est la valeur de event.key pour la touche Échap. Ctrl+S doit enregistrer, Ctrl+O ouvrir le rapport, Échap fermer le panneau, et la touche S seule, sans Ctrl, ne doit rien changer.
- Éditeur de code
Le fichier index.html contient trois onglets (role="tab") avec leurs panneaux et un écouteur keydown sur le conteneur #dino-tabs. La flèche droite déplace le focus vers l'onglet suivant, et Entrée choisit l'onglet qui a le focus. Complète les blancs : ___BLANK1___ est la valeur de event.key pour la flèche gauche (elle déplace le focus vers l'onglet précédent, du premier vers le dernier), et ___BLANK2___ est une condition vraie quand on appuie sur Espace (vérifie event.key ou event.code). Entrée et Espace doivent afficher le panneau de l'onglet qui a le focus, et les flèches et Espace ne doivent pas faire défiler la page.
- Tri horizontal
Place les éléments d'un gestionnaire qui arrête la propagation d'un événement :
- Tri vertical
Place dans l'ordre les étapes pour nettoyer les écouteurs d'événements lors de la suppression d'un élément du DOM :