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

Les événements souris et clavier

15 min de lecture
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 Escape

event.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 - clics
  • mousedown, mouseup - contrôle des boutons
  • mousemove - 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 touche
  • event.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. 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. 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 :

Articles utiles