Cours JavaScript et TypeScript · Module 8: Événements et DOM
Les événements souris et clavier
Dans cette leçon5
« Dans les systèmes de sécurité de Jurassic Park », Ray Arnold montre le grand panneau de contrôle, « on a différents types de capteurs. Les détecteurs de mouvement réagissent au mouvement, les capteurs de pression au toucher, les capteurs sonores au bruit. Chaque type demande une réaction différente. En JavaScript, la situation est similaire - différents types d'entrée demandent une gestion différente. »
Ray désigne l'écran et le clavier. « La souris et le clavier sont les deux sources principales d'interaction de l'utilisateur. Chacun a ses propres événements et comportements. »
Les événements souris - toute la gamme des interactions
La souris génère une bonne dizaine de types d'événements différents, des simples clics aux gestes de glissement avancés. Un seul clic, ce sont trois signaux : mousedown, mouseup et seulement à la fin click.
Les événements de clic de base
La zone d'observation fait la différence entre un clic simple et un double clic.
1const dinosaurViewingArea = document.getElementById('viewing-area');
2
3// click - clic simple (le plus courant)
4dinosaurViewingArea.addEventListener('click', (event) => {
5 console.log("Clic simple dans la zone d'observation");
6 console.log('Position :', event.clientX, event.clientY);
7
8 selectDinosaurAtPosition(event.clientX, event.clientY);
9});
10
11// dblclick - double clic
12dinosaurViewingArea.addEventListener('dblclick', (event) => {
13 console.log('Double clic - zoom avant');
14
15 // Attention : cela NE bloque PAS la sélection du texte, qui démarre déjà au mousedown
16 event.preventDefault();
17
18 zoomInOnDinosaur(event.clientX, event.clientY);
19});Les champs clientX et clientY donnent la position du curseur dans la fenêtre. Attention : preventDefault() dans dblclick ne bloquera pas la sélection du texte, car elle démarre déjà au mousedown. C'est le rôle de la règle CSS user-select: none.
1// auxclick - clic avec le bouton du milieu ou un autre (pas le gauche)
2dinosaurViewingArea.addEventListener('auxclick', (event) => {
3 if (event.button === 1) {
4 console.log('Bouton du milieu - ouverture dans une nouvelle fenêtre');
5 openInNewWindow(event.target);
6 }
7});
8
9// contextmenu - bouton droit de la souris (menu contextuel)
10dinosaurViewingArea.addEventListener('contextmenu', (event) => {
11 event.preventDefault(); // Bloquer le menu par défaut
12
13 console.log('Bouton droit - menu contextuel personnalisé');
14 showCustomContextMenu(event.clientX, event.clientY);
15});
16
17function selectDinosaurAtPosition(x, y) {
18 console.log(`Vérification de la présence d'un dinosaure à la position (${x}, ${y})`);
19 // Logique de sélection
20}
21
22function zoomInOnDinosaur(x, y) {
23 console.log(`Zoom sur la position (${x}, ${y})`);
24 // Logique de zoom
25}
26
27function showCustomContextMenu(x, y) {
28 const menu = document.getElementById('custom-menu');
29 menu.style.left = x + 'px';
30 menu.style.top = y + 'px';
31 menu.style.display = 'block';
32
33 console.log('Menu contextuel affiché');
34}Une valeur event.button égale à 1 correspond au bouton du milieu, et preventDefault() sur contextmenu masque le menu du navigateur pour laisser la place au tien.
Les événements des boutons de la souris
Entre l'appui sur le bouton et son relâchement, le glissement est en cours, donc on mémorise le point de départ.
1const controlPanel = document.getElementById('control-panel');
2
3// mousedown - bouton de la souris enfoncé (avant d'être relâché)
4controlPanel.addEventListener('mousedown', (event) => {
5 console.log('Bouton de la souris enfoncé');
6 console.log('Quel bouton :', event.button); // 0=gauche, 1=milieu, 2=droit
7
8 if (event.button === 0) {
9 console.log('Bouton gauche - début du glissement');
10 startDragging(event);
11 }
12});
13
14// mouseup - bouton de la souris relâché
15controlPanel.addEventListener('mouseup', (event) => {
16 console.log('Bouton de la souris relâché');
17
18 if (isDragging) {
19 console.log('Fin du glissement');
20 stopDragging(event);
21 }
22});
23
24let isDragging = false;
25let dragStartX = 0;
26let dragStartY = 0;
27
28function startDragging(event) {
29 isDragging = true;
30 dragStartX = event.clientX;
31 dragStartY = event.clientY;
32
33 console.log(`Début du glissement depuis la position (${dragStartX}, ${dragStartY})`);
34}
35
36function stopDragging(event) {
37 const deltaX = event.clientX - dragStartX;
38 const deltaY = event.clientY - dragStartY;
39
40 console.log(`Déplacement de : X=${deltaX}px, Y=${deltaY}px`);
41
42 isDragging = false;
43}Le drapeau isDragging relie les deux événements en une seule histoire, et la différence entre les coordonnées indique jusqu'où le curseur s'est déplacé.
Les événements de mouvement de la souris
Le mouvement du curseur au-dessus de la carte du parc est suivi par mousemove, un capteur qui se manifeste au moindre frémissement de la souris.
1const parkMap = document.getElementById('interactive-park-map');
2
3// mousemove - mouvement de la souris (ATTENTION : très fréquent !)
4// À utiliser avec prudence - peut générer des centaines d'événements par seconde
5
6let mouseMoveCount = 0;
7
8parkMap.addEventListener('mousemove', (event) => {
9 mouseMoveCount++;
10
11 // Mettre à jour la position du curseur
12 updateCursorPosition(event.clientX, event.clientY);
13
14 // Si on est en train de glisser, mettre à jour la position
15 if (isDragging) {
16 updateDragPosition(event.clientX, event.clientY);
17 }
18
19 // Journaliser un événement sur 10 (pour ne pas encombrer la console)
20 if (mouseMoveCount % 10 === 0) {
21 console.log(`Mouvement de la souris #${mouseMoveCount} : (${event.clientX}, ${event.clientY})`);
22 }
23});
24
25function updateCursorPosition(x, y) {
26 const coordsDisplay = document.getElementById('mouse-coords');
27 if (coordsDisplay) {
28 coordsDisplay.textContent = `X: ${x}, Y: ${y}`;
29 }
30}
31
32function updateDragPosition(x, y) {
33 const draggableElement = document.getElementById('draggable');
34 if (draggableElement) {
35 draggableElement.style.left = x + 'px';
36 draggableElement.style.top = y + 'px';
37 }
38}C'est pourquoi le journal n'arrive dans la console qu'un événement sur dix, et la leçon sur le throttling te montrera comment limiter de telles avalanches. Passons maintenant à l'entrée et à la sortie du curseur.
1// mouseenter - la souris entre dans la zone de l'élément (ne bouillonne pas !)
2parkMap.addEventListener('mouseenter', (event) => {
3 console.log('La souris est entrée sur la carte du parc');
4 parkMap.classList.add('mouse-active');
5 showMapControls();
6});
7
8// mouseleave - la souris quitte la zone de l'élément (ne bouillonne pas !)
9parkMap.addEventListener('mouseleave', (event) => {
10 console.log('La souris a quitté la carte du parc');
11 parkMap.classList.remove('mouse-active');
12 hideMapControls();
13});
14
15// mouseover - la souris entre (bouillonne - se déclenche aussi pour les enfants !)
16parkMap.addEventListener('mouseover', (event) => {
17 if (event.target.classList.contains('enclosure-marker')) {
18 const enclosureName = event.target.dataset.name;
19 console.log(`Survol du marqueur d'enclos : ${enclosureName}`);
20 showEnclosureTooltip(event.target, enclosureName);
21 }
22});
23
24// mouseout - la souris sort (bouillonne)
25parkMap.addEventListener('mouseout', (event) => {
26 if (event.target.classList.contains('enclosure-marker')) {
27 console.log("Sortie du marqueur d'enclos");
28 hideEnclosureTooltip();
29 }
30});
31
32function showMapControls() {
33 const controls = document.getElementById('map-controls');
34 if (controls) {
35 controls.style.display = 'block';
36 }
37}
38
39function hideMapControls() {
40 const controls = document.getElementById('map-controls');
41 if (controls) {
42 controls.style.display = 'none';
43 }
44}La paire mouseenter et mouseleave ne bouillonne pas, elle ne réagit donc qu'à la carte elle-même, alors que mouseover et mouseout bouillonnent depuis chaque enfant, d'où la vérification de event.target.
Exemple pratique - système de glisser-déposer
« L'une des fonctionnalités les plus utiles », dit Ray, « c'est le glisser-déposer (drag & drop). » La première partie de la classe démarre le glissement.
1class DinosaurCardDragSystem {
2 constructor() {
3 this.draggedElement = null;
4 this.dragOffsetX = 0;
5 this.dragOffsetY = 0;
6 this.initialize();
7 }
8
9 initialize() {
10 const cards = document.querySelectorAll('.dinosaur-card');
11
12 cards.forEach(card => {
13 card.addEventListener('mousedown', (e) => this.handleDragStart(e));
14 });
15
16 document.addEventListener('mousemove', (e) => this.handleDragMove(e));
17 document.addEventListener('mouseup', (e) => this.handleDragEnd(e));
18
19 console.log('Système de glisser-déposer initialisé');
20 }
21
22 handleDragStart(event) {
23 this.draggedElement = event.currentTarget;
24
25 const rect = this.draggedElement.getBoundingClientRect();
26 this.dragOffsetX = event.clientX - rect.left;
27 this.dragOffsetY = event.clientY - rect.top;
28
29 this.draggedElement.style.position = 'fixed';
30 this.draggedElement.style.zIndex = '1000';
31 this.draggedElement.classList.add('dragging');
32
33 const dinoName = this.draggedElement.dataset.dinoName;
34 console.log(`Début du glissement de la carte : ${dinoName}`);
35
36 event.preventDefault(); // Prevent text selection
37 }Les écouteurs mousemove et mouseup sont attachés au document, car un mouvement rapide fera sortir le curseur de la carte. Ici, preventDefault() dans mousedown bloque vraiment la sélection du texte, et getBoundingClientRect() permet de calculer le point de saisie.
1 handleDragMove(event) {
2 if (!this.draggedElement) return;
3
4 const x = event.clientX - this.dragOffsetX;
5 const y = event.clientY - this.dragOffsetY;
6
7 this.draggedElement.style.left = x + 'px';
8 this.draggedElement.style.top = y + 'px';
9 }
10
11 handleDragEnd(event) {
12 if (!this.draggedElement) return;
13
14 const dinoName = this.draggedElement.dataset.dinoName;
15 console.log(`Fin du glissement de la carte : ${dinoName}`);
16
17 // Vérifier si la carte a été déposée dans une zone appropriée
18 const dropZone = this.findDropZone(event.clientX, event.clientY);
19
20 if (dropZone) {
21 console.log('Carte déposée dans la zone :', dropZone.id);
22 this.handleDrop(this.draggedElement, dropZone);
23 } else {
24 console.log('Carte déposée hors zone - retour à la position de départ');
25 this.returnToOriginalPosition();
26 }
27
28 this.draggedElement.classList.remove('dragging');
29 this.draggedElement = null;
30 }La carte suit le curseur, et une fois le bouton relâché, la classe cherche la zone dans laquelle elle a atterri.
1 findDropZone(x, y) {
2 const dropZones = document.querySelectorAll('.drop-zone');
3
4 for (const zone of dropZones) {
5 const rect = zone.getBoundingClientRect();
6
7 if (x >= rect.left && x <= rect.right &&
8 y >= rect.top && y <= rect.bottom) {
9 return zone;
10 }
11 }
12
13 return null;
14 }
15
16 handleDrop(card, dropZone) {
17 console.log('Ajout de la carte à la zone de dépôt');
18 dropZone.appendChild(card);
19
20 // Réinitialisation des styles
21 card.style.position = '';
22 card.style.left = '';
23 card.style.top = '';
24 card.style.zIndex = '';
25 }
26
27 returnToOriginalPosition() {
28 // Retour à sa place - sans animation, car un changement de position ne s'anime pas.
29 // Pas de setTimeout : handleDragEnd remet this.draggedElement à null juste après
30 this.draggedElement.style.position = '';
31 this.draggedElement.style.left = '';
32 this.draggedElement.style.top = '';
33 this.draggedElement.style.zIndex = '';
34 }
35}
36
37// Initialisation
38const dragSystem = new DinosaurCardDragSystem();Une carte déposée dans une zone passe dans le nouvel enclos, et en dehors, elle retourne à sa place.
Le glisser-déposer intégré au navigateur
Le navigateur possède aussi sa propre API Drag and Drop. Un élément doté de l'attribut draggable="true" envoie dragstart, et la zone cible reçoit dragover et drop.
1// HTML: <div class="dino-item" draggable="true">T-Rex</div>
2const dino = document.querySelector('.dino-item');
3const zone = document.getElementById('zone-c');
4
5dino.addEventListener('dragstart', (e) => {
6 e.dataTransfer.setData('text/plain', dino.textContent);
7 dino.classList.add('dragging');
8});
9dino.addEventListener('dragend', () => dino.classList.remove('dragging'));
10
11zone.addEventListener('dragover', (e) => {
12 e.preventDefault(); // sans cela, la zone n'acceptera pas le dépôt
13});
14
15zone.addEventListener('drop', (e) => {
16 e.preventDefault(); // bloque l'action par défaut, par exemple l'ouverture d'un lien
17 zone.appendChild(dino);
18});L'élément clé est preventDefault() dans dragover, car sans lui le dépôt est interdit, et dragend fait le ménage après chaque glissement.
Les événements clavier - le contrôle au clavier
« Le clavier », Ray désigne le panneau de contrôle, « est le deuxième moyen clé d'interaction. Raccourcis clavier, navigation, saisie de données. »
Les événements clavier de base
Chaque appui laisse un rapport complet dans l'objet de l'événement.
1// keydown - touche enfoncée (se répète quand on la maintient)
2document.addEventListener('keydown', (event) => {
3 console.log('=== KEYDOWN ===');
4 console.log('Touche :', event.key); // 'a', 'Enter', 'ArrowUp', etc.
5 console.log('Code :', event.code); // 'KeyA', 'Enter', 'ArrowUp', etc.
6 console.log('KeyCode (deprecated):', event.keyCode);
7 console.log('Répétition :', event.repeat);
8 console.log('Ctrl:', event.ctrlKey);
9 console.log('Shift:', event.shiftKey);
10 console.log('Alt:', event.altKey);
11 console.log('Meta (Cmd/Win):', event.metaKey);
12});
13
14// keyup - touche relâchée
15document.addEventListener('keyup', (event) => {
16 console.log(`Touche relâchée : ${event.key}`);
17});
18
19// keypress - OBSOLÈTE ! Ne l'utilise pas (utilise keydown à la place)
20// keypress est obsolète et ne se déclenche pas, par exemple, pour les flèches ni pour Escapeevent.key est le caractère ou le nom de la touche, et event.code la touche physique, indépendante de la disposition du clavier. Une touche maintenue envoie des keydown successifs avec event.repeat égal à true, alors que keyup n'arrive qu'une seule fois.
Raccourcis clavier pratiques
Un seul écouteur sur le document gère les raccourcis. D'abord l'enregistrement, la recherche et Escape.
1// Système de raccourcis clavier pour le parc
2document.addEventListener('keydown', (event) => {
3 // Ctrl/Cmd + S - Enregistrer
4 if ((event.ctrlKey || event.metaKey) && event.key === 's') {
5 event.preventDefault();
6 console.log('Enregistrement de la configuration du parc...');
7 saveParkConfiguration();
8 return;
9 }
10
11 // Ctrl/Cmd + F - Rechercher
12 if ((event.ctrlKey || event.metaKey) && event.key === 'f') {
13 event.preventDefault();
14 console.log('Ouverture de la recherche de dinosaures');
15 openSearchDialog();
16 return;
17 }
18
19 // Escape - Fermer toutes les fenêtres
20 if (event.key === 'Escape') {
21 console.log('Fermeture de toutes les boîtes de dialogue');
22 closeAllModals();
23 return;
24 }On vérifie ctrlKey ou metaKey pour que le raccourci fonctionne aussi avec Cmd sur Mac, et preventDefault() empêche le navigateur d'enregistrer la page. Viennent ensuite les flèches, la barre d'espace, F1 et les chiffres.
1 // Flèches - Navigation sur la carte
2 const arrowKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
3 if (arrowKeys.includes(event.key)) {
4 event.preventDefault();
5
6 const direction = {
7 'ArrowUp': 'north',
8 'ArrowDown': 'south',
9 'ArrowLeft': 'west',
10 'ArrowRight': 'east'
11 }[event.key];
12
13 console.log(`Déplacement de la carte vers : ${direction}`);
14 moveMap(direction);
15 return;
16 }
17
18 // Espace - Pause/reprise de la surveillance
19 if (event.key === ' ') {
20 event.preventDefault();
21 console.log('Bascule de la pause de surveillance');
22 toggleMonitoringPause();
23 return;
24 }
25
26 // F1 - Aide
27 if (event.key === 'F1') {
28 event.preventDefault();
29 console.log("Ouverture de l'aide");
30 openHelp();
31 return;
32 }
33
34 // Chiffres 1-9 - Accès rapide aux enclos
35 if (event.key >= '1' && event.key <= '9') {
36 const enclosureNumber = parseInt(event.key);
37 console.log(`Passage à l'enclos #${enclosureNumber}`);
38 jumpToEnclosure(enclosureNumber);
39 return;
40 }
41});La barre d'espace porte le nom ' ', et la table des directions transforme la touche en point cardinal. Viennent ensuite de simples fonctions d'exécution.
1function saveParkConfiguration() {
2 console.log('Configuration enregistrée');
3}
4
5function openSearchDialog() {
6 console.log('Boîte de recherche ouverte');
7}
8
9function closeAllModals() {
10 document.querySelectorAll('.modal').forEach(modal => {
11 modal.style.display = 'none';
12 });
13 console.log('Toutes les fenêtres sont fermées');
14}
15
16function moveMap(direction) {
17 console.log(`Carte déplacée vers : ${direction}`);
18}
19
20function toggleMonitoringPause() {
21 console.log('Surveillance mise en pause/reprise');
22}
23
24function openHelp() {
25 console.log('Aide ouverte');
26}
27
28function jumpToEnclosure(number) {
29 console.log(`Passage à l'enclos #${number}`);
30}L'écouteur reconnaît le raccourci, et la logique vit dans les fonctions.
Validation de la saisie au clavier
L'écouteur keydown peut refuser un caractère avant qu'il n'arrive dans le champ : le code de sécurité n'accepte que des chiffres, le téléphone accepte aussi les espaces, les parenthèses et les tirets, et le nom reçoit une majuscule.
1const securityCodeInput = document.getElementById('security-code');
2const phoneInput = document.getElementById('phone-number');
3const nameInput = document.getElementById('visitor-name');
4
5// Uniquement des chiffres - code de sécurité
6securityCodeInput.addEventListener('keydown', (event) => {
7 // Autoriser les touches de fonction
8 const allowedKeys = [
9 'Backspace', 'Delete', 'Tab', 'Escape', 'Enter',
10 'ArrowLeft', 'ArrowRight', 'Home', 'End'
11 ];
12
13 if (allowedKeys.includes(event.key)) {
14 return; // Autoriser
15 }
16
17 // Uniquement des chiffres
18 if (event.key >= '0' && event.key <= '9') {
19 // 6 chiffres maximum
20 if (securityCodeInput.value.length >= 6 && !event.ctrlKey && !event.metaKey) {
21 event.preventDefault();
22 console.log('6 chiffres maximum');
23 }
24 return;
25 }
26
27 // Tout le reste - bloquer
28 event.preventDefault();
29 console.log('Seuls les chiffres sont autorisés');
30});
31
32// Formatage du numéro de téléphone
33phoneInput.addEventListener('keydown', (event) => {
34 const allowedKeys = [
35 'Backspace', 'Delete', 'Tab', 'Escape', 'Enter',
36 'ArrowLeft', 'ArrowRight', 'Home', 'End'
37 ];
38
39 if (allowedKeys.includes(event.key)) return;
40
41 // Uniquement des chiffres, des espaces, des tirets et des parenthèses
42 const phoneChars = /^[0-9 ()-]$/;
43
44 if (!phoneChars.test(event.key)) {
45 event.preventDefault();
46 console.log('Caractère interdit dans le numéro de téléphone');
47 }
48});
49
50// Majuscule automatique pour la première lettre
51nameInput.addEventListener('keyup', (event) => {
52 const value = nameInput.value;
53
54 if (value.length === 1) {
55 nameInput.value = value.toUpperCase();
56 }
57});Un tel filtre n'est qu'un confort, car on peut coller du texte depuis le menu contextuel, alors vérifie aussi la valeur lors de input ou de submit.
Combiner souris et clavier
« Les meilleures interfaces », dit Ray, « combinent les deux types d'entrée. » Les touches de modification changent le sens d'un clic.
1const enclosureCards = document.querySelectorAll('.enclosure-card');
2let selectedCards = new Set();
3
4enclosureCards.forEach(card => {
5 card.addEventListener('click', (event) => {
6 const enclosureId = card.dataset.enclosureId;
7
8 // Comportements différents selon les touches de modification
9 if (event.ctrlKey || event.metaKey) {
10 // Ctrl + Clic = Basculer la sélection
11 console.log('Ctrl+Clic : basculer la sélection');
12
13 if (selectedCards.has(enclosureId)) {
14 selectedCards.delete(enclosureId);
15 card.classList.remove('selected');
16 } else {
17 selectedCards.add(enclosureId);
18 card.classList.add('selected');
19 }
20 } else if (event.shiftKey) {
21 // Shift + Clic = Sélection d'une plage
22 console.log("Shift+Clic : sélection d'une plage");
23 selectRange(lastSelected, enclosureId);
24 } else {
25 // Clic normal = Uniquement cette carte
26 console.log('Clic normal : sélection unique');
27
28 selectedCards.clear();
29 document.querySelectorAll('.enclosure-card.selected').forEach(c => {
30 c.classList.remove('selected');
31 });
32
33 selectedCards.add(enclosureId);
34 card.classList.add('selected');
35 }
36
37 lastSelected = enclosureId;
38 updateSelectionInfo();
39 });
40});
41
42let lastSelected = null;
43
44function selectRange(startId, endId) {
45 console.log(`Sélection de la plage de ${startId} à ${endId}`);
46 // Logique de sélection d'une plage
47}
48
49function updateSelectionInfo() {
50 console.log(`Sélectionnés : ${selectedCards.size} enclos`);
51}Ctrl ou Cmd bascule la sélection, Shift sélectionne une plage, et un clic normal ne laisse qu'une seule carte sélectionnée. Viennent ensuite les flèches.
1// Navigation au clavier entre les cartes
2let focusedCardIndex = 0;
3const cards = Array.from(enclosureCards);
4
5document.addEventListener('keydown', (event) => {
6 // Seulement quand aucun champ input n'est actif
7 if (document.activeElement.tagName === 'INPUT') return;
8
9 switch(event.key) {
10 case 'ArrowRight':
11 case 'ArrowDown':
12 event.preventDefault();
13 focusedCardIndex = Math.min(focusedCardIndex + 1, cards.length - 1);
14 focusCard(focusedCardIndex);
15 break;
16
17 case 'ArrowLeft':
18 case 'ArrowUp':
19 event.preventDefault();
20 focusedCardIndex = Math.max(focusedCardIndex - 1, 0);
21 focusCard(focusedCardIndex);
22 break;
23
24 case 'Enter':
25 case ' ':
26 event.preventDefault();
27 cards[focusedCardIndex].click();
28 break;
29 }
30});
31
32function focusCard(index) {
33 cards.forEach(card => card.classList.remove('focused'));
34 cards[index].classList.add('focused');
35 cards[index].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
36
37 console.log(`Focus sur la carte #${index} : ${cards[index].dataset.enclosureName}`);
38}La condition sur activeElement ne gêne pas la saisie dans les champs, et click() sur une carte déclenche le même écouteur que la souris.
Les événements de la fenêtre et du tactile
DOMContentLoaded se déclenche quand le HTML est chargé, même si les images peuvent encore être en cours de chargement. load sur window attend la page entière avec toutes ses ressources. Le défilement signale scroll, et window.scrollY donne le nombre de pixels défilés, donc une valeur plus grande que la précédente signifie un mouvement vers le bas. Sur les écrans tactiles, le doigt envoie successivement touchstart, touchmove et touchend, sur lesquels nous reviendrons avec les gestes.
Récapitulatif
« Les événements souris et clavier », résume Ray, « sont le fondement de l'interactivité. Tout comme les différents capteurs de Jurassic Park détectent différents types de menaces, les différents événements permettent des interactions variées avec l'utilisateur. La clé, c'est d'utiliser les bons événements pour les bonnes tâches. »
Événements souris clés :
click,dblclick,contextmenu- clicsmousedown,mouseup- contrôle des boutonsmousemove- suivi de la position (à utiliser avec prudence !)mouseenter,mouseleave- survol (ne bouillonnent pas)mouseover,mouseout- survol (bouillonnent)
Événements clavier clés :
keydown- appui sur une touche (utilise celui-ci)keyup- relâchement d'une toucheevent.key- caractère/nom de la touche ('a', 'Enter', 'ArrowUp')event.code- touche physique ('KeyA', 'Enter', 'ArrowUp')- Touches de modification :
ctrlKey,shiftKey,altKey,metaKey
Mon conseil : base les raccourcis sur keydown, et le déplacement entre les zones sur le Drag and Drop intégré. Prochaine leçon : les formulaires.
Retiens bien : chaque capteur du parc réagit à un signal différent, alors choisis l'événement en fonction de la tâche.
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>Événements souris et clavier - Contrôle de la caméra</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 }
15
16 .container {
17 max-width: 1000px;
18 margin: 0 auto;
19 }
20
21 h1 {
22 text-align: center;
23 margin-bottom: 20px;
24 }
25
26 .instructions {
27 background: rgba(52, 152, 219, 0.2);
28 border: 1px solid #3498db;
29 border-radius: 10px;
30 padding: 15px;
31 margin-bottom: 20px;
32 text-align: center;
33 }
34
35 .instructions kbd {
36 background: rgba(255, 255, 255, 0.2);
37 padding: 2px 8px;
38 border-radius: 4px;
39 font-family: monospace;
40 margin: 0 3px;
41 }
42
43 .camera-viewport {
44 position: relative;
45 width: 100%;
46 height: 400px;
47 background: linear-gradient(180deg, #34495e 0%, #2c3e50 100%);
48 border-radius: 15px;
49 overflow: hidden;
50 cursor: move;
51 margin-bottom: 20px;
52 border: 3px solid #7f8c8d;
53 }
54
55 .camera-content {
56 position: absolute;
57 width: 200%;
58 height: 200%;
59 transition: transform 0.3s ease;
60 }
61
62 .dinosaur {
63 position: absolute;
64 font-size: 60px;
65 cursor: pointer;
66 transition: transform 0.2s ease;
67 user-select: none;
68 }
69
70 .dinosaur:hover {
71 transform: scale(1.2);
72 filter: drop-shadow(0 0 10px rgba(255, 255, 255, 0.5));
73 }
74
75 .dinosaur.clicked {
76 animation: bounce 0.5s ease;
77 }
78
79 @keyframes bounce {
80 0%, 100% { transform: scale(1); }
81 50% { transform: scale(1.3); }
82 }
83
84 .controls-panel {
85 display: grid;
86 grid-template-columns: 1fr 1fr;
87 gap: 20px;
88 margin-bottom: 20px;
89 }
90
91 .control-box {
92 background: rgba(0, 0, 0, 0.6);
93 border-radius: 10px;
94 padding: 20px;
95 border: 1px solid #34495e;
96 }
97
98 .control-box h3 {
99 margin-top: 0;
100 color: #3498db;
101 }
102
103 .stats {
104 display: grid;
105 grid-template-columns: repeat(2, 1fr);
106 gap: 10px;
107 }
108
109 .stat-item {
110 background: rgba(255, 255, 255, 0.1);
111 padding: 10px;
112 border-radius: 5px;
113 }
114
115 .stat-label {
116 color: #bdc3c7;
117 font-size: 12px;
118 }
119
120 .stat-value {
121 color: #ecf0f1;
122 font-size: 18px;
123 font-weight: bold;
124 }
125
126 .arrow-buttons {
127 display: grid;
128 grid-template-areas:
129 ". up ."
130 "left . right"
131 ". down .";
132 gap: 5px;
133 justify-content: center;
134 align-items: center;
135 }
136
137 .arrow-btn {
138 width: 60px;
139 height: 60px;
140 background: linear-gradient(45deg, #3498db, #2980b9);
141 border: none;
142 border-radius: 10px;
143 color: white;
144 font-size: 24px;
145 cursor: pointer;
146 transition: all 0.2s ease;
147 }
148
149 .arrow-btn:hover {
150 background: linear-gradient(45deg, #2980b9, #21618c);
151 transform: scale(1.05);
152 }
153
154 .arrow-btn:active {
155 transform: scale(0.95);
156 }
157
158 .arrow-btn.up { grid-area: up; }
159 .arrow-btn.down { grid-area: down; }
160 .arrow-btn.left { grid-area: left; }
161 .arrow-btn.right { grid-area: right; }
162
163 .event-log {
164 background: rgba(0, 0, 0, 0.8);
165 border-radius: 10px;
166 padding: 15px;
167 max-height: 200px;
168 overflow-y: auto;
169 font-family: 'Courier New', monospace;
170 font-size: 12px;
171 }
172
173 .log-entry {
174 padding: 5px;
175 border-bottom: 1px solid rgba(255, 255, 255, 0.1);
176 color: #bdc3c7;
177 }
178
179 .log-entry.mouse {
180 color: #3498db;
181 }
182
183 .log-entry.keyboard {
184 color: #e74c3c;
185 }
186
187 .log-entry.click {
188 color: #f39c12;
189 }
190
191 @media (max-width: 768px) {
192 .controls-panel {
193 grid-template-columns: 1fr;
194 }
195 }
196 </style>
197</head>
198<body>
199 <div class="container">
200 <h1>Contrôle de la caméra à Jurassic Park</h1>
201
202 <div class="instructions">
203 <strong>Controls:</strong> Arrows <kbd>↑</kbd> <kbd>↓</kbd> <kbd>←</kbd> <kbd>→</kbd> |
204 <strong>Pause:</strong> <kbd>Space</kbd> |
205 <strong>Zoom:</strong> Double click |
206 <strong>Close:</strong> <kbd>Escape</kbd>
207 </div>
208
209 <div class="camera-viewport" id="viewport">
210 <div class="camera-content" id="camera">
211 <div class="dinosaur" style="top: 50px; left: 100px;" data-name="T-Rex">T</div>
212 <div class="dinosaur" style="top: 150px; left: 400px;" data-name="Velociraptor">V</div>
213 <div class="dinosaur" style="top: 300px; left: 200px;" data-name="Triceratops">Tr</div>
214 <div class="dinosaur" style="top: 100px; left: 600px;" data-name="Brachiosaurus">B</div>
215 <div class="dinosaur" style="top: 250px; left: 700px;" data-name="Stegosaurus">S</div>
216 </div>
217 </div>
218
219 <div class="controls-panel">
220 <div class="control-box">
221 <h3>Statistiques</h3>
222 <div class="stats">
223 <div class="stat-item">
224 <div class="stat-label">Position de la souris</div>
225 <div class="stat-value" id="mouse-pos">0, 0</div>
226 </div>
227 <div class="stat-item">
228 <div class="stat-label">Clicks</div>
229 <div class="stat-value" id="click-count">0</div>
230 </div>
231 <div class="stat-item">
232 <div class="stat-label">Position de la caméra</div>
233 <div class="stat-value" id="camera-pos">0, 0</div>
234 </div>
235 <div class="stat-item">
236 <div class="stat-label">Dernière touche</div>
237 <div class="stat-value" id="last-key">-</div>
238 </div>
239 </div>
240 </div>
241
242 <div class="control-box">
243 <h3>Contrôle de la caméra</h3>
244 <div class="arrow-buttons">
245 <button class="arrow-btn up" data-direction="up">↑</button>
246 <button class="arrow-btn left" data-direction="left">←</button>
247 <button class="arrow-btn right" data-direction="right">→</button>
248 <button class="arrow-btn down" data-direction="down">↓</button>
249 </div>
250 </div>
251 </div>
252
253 <div class="control-box">
254 <h3>Journal des événements</h3>
255 <div class="event-log" id="event-log"></div>
256 </div>
257 </div>
258
259 <script>
260 // References to elements
261 const camera = document.getElementById('camera');
262 const viewport = document.getElementById('viewport');
263 const mousePosDisplay = document.getElementById('mouse-pos');
264 const clickCountDisplay = document.getElementById('click-count');
265 const cameraPosDisplay = document.getElementById('camera-pos');
266 const lastKeyDisplay = document.getElementById('last-key');
267 const eventLog = document.getElementById('event-log');
268
269 // État de la caméra
270 let cameraX = 0;
271 let cameraY = 0;
272 let clickCount = 0;
273 let isPaused = false;
274 const moveSpeed = 50;
275
276 // Event logging function
277 function logEvent(message, type = 'mouse') {
278 const logEntry = document.createElement('div');
279 logEntry.className = `log-entry ${type}`;
280 logEntry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
281 eventLog.insertBefore(logEntry, eventLog.firstChild);
282
283 // Limit number of entries
284 if (eventLog.children.length > 20) {
285 eventLog.removeChild(eventLog.lastChild);
286 }
287 }
288
289 // Fonction de déplacement de la caméra
290 function moveCamera(direction) {
291 if (isPaused) {
292 logEvent('Camera paused! Press Space to resume', 'keyboard');
293 return;
294 }
295
296 switch(direction) {
297 case 'up':
298 cameraY = Math.max(cameraY - moveSpeed, -200);
299 break;
300 case 'down':
301 cameraY = Math.min(cameraY + moveSpeed, 200);
302 break;
303 case 'left':
304 cameraX = Math.max(cameraX - moveSpeed, -200);
305 break;
306 case 'right':
307 cameraX = Math.min(cameraX + moveSpeed, 200);
308 break;
309 }
310
311 camera.style.transform = `translate(${cameraX}px, ${cameraY}px)`;
312 cameraPosDisplay.textContent = `${cameraX}, ${cameraY}`;
313 logEvent(`Camera movement: ${direction}`, 'keyboard');
314 }
315
316 // MOUSE HANDLING
317
318 // Mousemove - tracking mouse position
319 viewport.addEventListener('mousemove', function(e) {
320 const rect = viewport.getBoundingClientRect();
321 const x = Math.floor(e.clientX - rect.left);
322 const y = Math.floor(e.clientY - rect.top);
323 mousePosDisplay.textContent = `${x}, ${y}`;
324 });
325
326 // Click - single click
327 viewport.addEventListener('click', function(e) {
328 clickCount++;
329 clickCountDisplay.textContent = clickCount;
330
331 const rect = viewport.getBoundingClientRect();
332 const x = Math.floor(e.clientX - rect.left);
333 const y = Math.floor(e.clientY - rect.top);
334
335 logEvent(`Click at position: (${x}, ${y})`, 'click');
336 });
337
338 // Double click - double click (zoom)
339 viewport.addEventListener('dblclick', function(e) {
340 const currentScale = camera.style.transform.includes('scale') ? 1 : 1.5;
341 const newScale = currentScale === 1 ? 1.5 : 1;
342
343 camera.style.transform = `translate(${cameraX}px, ${cameraY}px) scale(${newScale})`;
344 logEvent(`Zoom: ${newScale === 1 ? 'Off' : 'On'}`, 'click');
345 });
346
347 // Mouseenter/mouseleave - survol du viewport
348 viewport.addEventListener('mouseenter', function() {
349 viewport.style.borderColor = '#3498db';
350 logEvent('Mouse entered camera area', 'mouse');
351 });
352
353 viewport.addEventListener('mouseleave', function() {
354 viewport.style.borderColor = '#7f8c8d';
355 logEvent('Mouse left camera area', 'mouse');
356 });
357
358 // Click on dinosaur
359 document.querySelectorAll('.dinosaur').forEach(dino => {
360 dino.addEventListener('click', function(e) {
361 e.stopPropagation(); // Stop propagation vers le viewport
362
363 const name = this.dataset.name;
364 this.classList.add('clicked');
365
366 setTimeout(() => {
367 this.classList.remove('clicked');
368 }, 500);
369
370 logEvent(`Clicked dinosaur: ${name}`, 'click');
371 console.log(`Observation: ${name}`);
372 });
373
374 dino.addEventListener('mouseenter', function() {
375 const name = this.dataset.name;
376 logEvent(`Survol de: ${name}`, 'mouse');
377 });
378 });
379
380 // KEYBOARD HANDLING
381
382 // Keydown - key press
383 document.addEventListener('keydown', function(e) {
384 lastKeyDisplay.textContent = e.key;
385
386 switch(e.key) {
387 case 'ArrowUp':
388 e.preventDefault();
389 moveCamera('up');
390 break;
391 case 'ArrowDown':
392 e.preventDefault();
393 moveCamera('down');
394 break;
395 case 'ArrowLeft':
396 e.preventDefault();
397 moveCamera('left');
398 break;
399 case 'ArrowRight':
400 e.preventDefault();
401 moveCamera('right');
402 break;
403 case ' ':
404 e.preventDefault();
405 isPaused = !isPaused;
406 logEvent(`Pause: ${isPaused ? 'ON' : 'OFF'}`, 'keyboard');
407 viewport.style.opacity = isPaused ? '0.5' : '1';
408 break;
409 case 'Escape':
410 logEvent('Escape pressed - closing view', 'keyboard');
411 alert('Camera view will be closed');
412 break;
413 case 'r':
414 case 'R':
415 // Reset de la caméra
416 cameraX = 0;
417 cameraY = 0;
418 camera.style.transform = 'translate(0px, 0px)';
419 cameraPosDisplay.textContent = '0, 0';
420 logEvent('Reset de la position de la caméra', 'keyboard');
421 break;
422 default:
423 logEvent(`Key pressed: ${e.key}`, 'keyboard');
424 }
425 });
426
427 // Handle arrow buttons
428 document.querySelectorAll('.arrow-btn').forEach(btn => {
429 btn.addEventListener('click', function() {
430 const direction = this.dataset.direction;
431 moveCamera(direction);
432 });
433 });
434
435 // Initialisation
436 logEvent('Système de caméra démarré', 'keyboard');
437 console.log('Système de monitoring du Parc Jurassique - ONLINE');
438 console.log('Use arrows to move the camera');
439 console.log('Click on dinosaurs to observe them');
440 </script>
441</body>
442</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. Quel événement clavier le navigateur envoie-t-il à l'appui sur n'importe quelle touche (y compris Shift, Ctrl ou les flèches) et répète-t-il tant que la touche reste enfoncée ?
2. Quel événement est déclenché une fois la page entièrement chargée ?
Tâches pratiques dans le jeu
- Éditeur de code
Le fichier index.html contient deux enclos (.enclosure) et des cartes de dinosaures avec l'attribut draggable="true". Au dragstart, chaque carte enregistre son id dans dataTransfer. Complète les blancs : ___BLANK1___ est l'événement que reçoit un enclos pendant qu'une carte glissée le survole (preventDefault() y autorise le dépôt), et ___BLANK2___ est la méthode de dataTransfer qui lit l'id enregistré dans drop. Une carte déposée doit arriver dans l'enclos sur lequel on l'a lâchée.
- Tri vertical
Place les événements souris dans l'ordre où ils se produisent lors d'un clic :
- Clique dans l'ordre
Place le code permettant de vérifier la combinaison Ctrl+S.
- Tri vertical
Place les événements tactiles dans l'ordre où ils se produisent :