Cours JavaScript et TypeScript · Module 8: Événements et DOM
La délégation d'événements
Dans cette leçon8
« À Jurassic Park », Ray Arnold désigne le panneau de surveillance central, « on n'installe pas un système d'alarme séparé à côté de chaque dinosaure. Ce serait coûteux et inefficace. À la place, on a un système central de surveillance par secteur, qui gère tous les enclos de sa zone. »
Ray ouvre le schéma du système. « La délégation d'événements fonctionne sur le même principe. Au lieu d'attacher des centaines d'écouteurs d'événements à des éléments individuels, on attache un seul écouteur à leur parent commun. Ce parent "délègue" la gestion aux éléments concernés. »
Qu'est-ce que la délégation d'événements ?
La délégation d'événements est une technique qui exploite la propagation des événements (le bouillonnement, ou bubbling) pour gérer les événements de nombreux éléments grâce à un seul écouteur attaché à leur ancêtre commun. C'est l'une des techniques les plus importantes et les plus efficaces de JavaScript.
Le problème : trop d'écouteurs d'événements
« Voyons le problème », dit Ray en montrant un code dans lequel chaque carte de dinosaure reçoit son propre écouteur, dans une boucle forEach.
1// INEFFICACE - un écouteur séparé pour chaque carte
2const dinosaurCards = document.querySelectorAll('.dinosaur-card');
3
4console.log(`Ajout d'écouteurs à ${dinosaurCards.length} cartes de dinosaures`);
5
6dinosaurCards.forEach((card, index) => {
7 card.addEventListener('click', function() {
8 const dinoId = this.dataset.dinoId;
9 const dinoName = this.dataset.dinoName;
10
11 console.log(`Dinosaure cliqué : ${dinoName} (ID : ${dinoId})`);
12 showDinosaurDetails(dinoId);
13 });
14
15 console.log(`Écouteur ${index + 1}/${dinosaurCards.length} ajouté`);
16});
17
18// Problèmes de cette approche :
19// 1. Si on a 100 cartes = 100 écouteurs en mémoire
20// 2. Les cartes ajoutées dynamiquement N'AURONT PAS d'écouteurs
21// 3. Il faut supprimer les écouteurs à la main quand on supprime des cartes
22// 4. Plus de mémoire consommée et une page plus lenteÇa fonctionne, mais cent cartes, ce sont cent écouteurs, et une carte ajoutée plus tard reste sourde, car la boucle ne s'est exécutée qu'une seule fois.
La solution : la délégation d'événements
« Maintenant, regarde », Ray montre la meilleure solution : un seul écouteur sur la galerie détermine lui-même sur quelle carte on a cliqué.
1// EFFICACE - un seul écouteur sur le conteneur
2const dinosaurGallery = document.getElementById('dinosaur-gallery');
3
4dinosaurGallery.addEventListener('click', function(event) {
5 // Trouver la carte de dinosaure la plus proche (même si on a cliqué sur un enfant de la carte)
6 const card = event.target.closest('.dinosaur-card');
7
8 // Si on n'a pas cliqué sur une carte de dinosaure, ignorer
9 if (!card) return;
10
11 // S'assurer que la carte est un enfant de notre galerie
12 if (!dinosaurGallery.contains(card)) return;
13
14 const dinoId = card.dataset.dinoId;
15 const dinoName = card.dataset.dinoName;
16
17 console.log(`Dinosaure cliqué : ${dinoName} (ID : ${dinoId})`);
18 showDinosaurDetails(dinoId);
19});
20
21// Avantages de cette approche :
22// 1. Un SEUL écouteur, quel que soit le nombre de cartes
23// 2. Gère automatiquement les cartes ajoutées dynamiquement
24// 3. Meilleure gestion de la mémoire
25// 4. Maintenance du code plus facileUn clic sur l'image d'une carte bouillonne jusqu'à la galerie : event.target est l'élément sur lequel on a cliqué, et event.currentTarget l'élément qui porte l'écouteur, c'est-à-dire la galerie. La méthode closest() part de target, remonte l'arbre et renvoie la carte correspondante ou null, tandis que contains() vérifie que la carte se trouve bien dans la galerie. Toute la technique tient en quatre étapes : trouver le conteneur, lui ajouter un écouteur, vérifier event.target et exécuter l'action pour l'élément correspondant.
Exemple pratique - système de cartes de dinosaures
« Construisons un système complet », dit Ray. Voici la structure d'une carte et le début d'un écouteur qui reconnaît les boutons de détails et de nourrissage.
1// Structure HTML :
2// <div id="dino-gallery">
3// <div class="dinosaur-card" data-dino-id="1" data-dino-name="T-Rex">
4// <img src="trex.jpg" alt="T-Rex">
5// <h3>Tyrannosaurus Rex</h3>
6// <div class="card-actions">
7// <button class="view-btn">Détails</button>
8// <button class="feed-btn">Nourrir</button>
9// <button class="favorite-btn">★</button>
10// </div>
11// </div>
12// ... d'autres cartes
13// </div>
14
15const gallery = document.getElementById('dino-gallery');
16
17gallery.addEventListener('click', function(event) {
18 const target = event.target;
19
20 // Gérer le bouton de détails
21 if (target.classList.contains('view-btn')) {
22 const card = target.closest('.dinosaur-card');
23 const dinoId = card.dataset.dinoId;
24 const dinoName = card.dataset.dinoName;
25
26 console.log(`Ouverture des détails pour : ${dinoName}`);
27 openDinosaurModal(dinoId);
28 return; // Arrêter la suite des vérifications
29 }
30
31 // Gérer le bouton de nourrissage
32 if (target.classList.contains('feed-btn')) {
33 const card = target.closest('.dinosaur-card');
34 const dinoId = card.dataset.dinoId;
35 const dinoName = card.dataset.dinoName;
36
37 console.log(`Début du nourrissage : ${dinoName}`);
38 feedDinosaur(dinoId);
39 return;
40 }On lit l'attribut data-dino-id sous la forme dataset.dinoId, car les tirets se transforment en camelCase. Chaque branche se termine par return, donc un clic déclenche une seule action. Viennent ensuite les favoris et la carte elle-même.
1 // Gérer le bouton des favoris
2 if (target.classList.contains('favorite-btn')) {
3 const card = target.closest('.dinosaur-card');
4 const dinoId = card.dataset.dinoId;
5 const dinoName = card.dataset.dinoName;
6
7 // Basculer l'état de favori
8 target.classList.toggle('active');
9
10 if (target.classList.contains('active')) {
11 console.log(`Ajouté aux favoris : ${dinoName}`);
12 addToFavorites(dinoId);
13 target.textContent = '★';
14 } else {
15 console.log(`Retiré des favoris : ${dinoName}`);
16 removeFromFavorites(dinoId);
17 target.textContent = '☆';
18 }
19 return;
20 }
21
22 // Gérer le clic sur la carte elle-même (mais pas sur les boutons)
23 const card = target.closest('.dinosaur-card');
24 if (card && gallery.contains(card)) {
25 const dinoId = card.dataset.dinoId;
26 const dinoName = card.dataset.dinoName;
27
28 console.log(`Carte cliquée : ${dinoName}`);
29 highlightCard(card);
30 }
31});On vérifie la carte en dernier, sinon un clic sur un bouton la mettrait au passage en surbrillance. La vérification classList.contains suffit, car les boutons ne contiennent que du texte. Une icône à l'intérieur intercepterait le clic, et la condition échouerait. Pour finir, les fonctions utilitaires.
1function openDinosaurModal(dinoId) {
2 console.log(`Chargement des données du dinosaure ID : ${dinoId}`);
3 // Logique d'ouverture de la modale
4}
5
6function feedDinosaur(dinoId) {
7 console.log(`Activation du système de nourrissage pour le dinosaure ID : ${dinoId}`);
8 console.log('Vérification de la sécurité...');
9 console.log('Ouverture du distributeur...');
10 console.log('Nourrissage terminé');
11}
12
13function addToFavorites(dinoId) {
14 const favorites = JSON.parse(localStorage.getItem('favoriteDinosaurs') || '[]');
15 if (!favorites.includes(dinoId)) {
16 favorites.push(dinoId);
17 localStorage.setItem('favoriteDinosaurs', JSON.stringify(favorites));
18 console.log('Enregistré dans les favoris');
19 }
20}
21
22function removeFromFavorites(dinoId) {
23 let favorites = JSON.parse(localStorage.getItem('favoriteDinosaurs') || '[]');
24 favorites = favorites.filter(id => id !== dinoId);
25 localStorage.setItem('favoriteDinosaurs', JSON.stringify(favorites));
26 console.log('Retiré des favoris');
27}
28
29function highlightCard(card) {
30 // Retirer la surbrillance des autres cartes
31 document.querySelectorAll('.dinosaur-card.highlighted').forEach(c => {
32 c.classList.remove('highlighted');
33 });
34
35 // Mettre en surbrillance la carte cliquée
36 card.classList.add('highlighted');
37 console.log('Carte mise en surbrillance');
38}Les fonctions utilitaires ne savent rien des événements. L'écriture || '[]' sauve la toute première visite, quand getItem() renvoie null.
Ajouter des éléments dynamiquement
« Le plus grand avantage de la délégation », Ray ajoute de nouveaux éléments à l'interface, « c'est la gestion automatique des éléments ajoutés dynamiquement. »
1const gallery = document.getElementById('dino-gallery');
2
3// Délégation d'événements - fonctionne pour toutes les cartes, même futures
4gallery.addEventListener('click', function(event) {
5 const card = event.target.closest('.dinosaur-card');
6 if (!card) return;
7
8 const dinoName = card.dataset.dinoName;
9 console.log(`Cliqué : ${dinoName}`);
10});
11
12// Fonction qui ajoute dynamiquement de nouvelles cartes
13function addNewDinosaur(dinosaur) {
14 const newCard = document.createElement('div');
15 newCard.className = 'dinosaur-card';
16 newCard.dataset.dinoId = dinosaur.id;
17 newCard.dataset.dinoName = dinosaur.name;
18
19 newCard.innerHTML = `
20 <img src="${dinosaur.image}" alt="${dinosaur.name}">
21 <h3>${dinosaur.name}</h3>
22 <p>${dinosaur.species}</p>
23 <div class="card-actions">
24 <button class="view-btn">Détails</button>
25 <button class="feed-btn">Nourrir</button>
26 <button class="favorite-btn">☆</button>
27 </div>
28 `;
29
30 gallery.appendChild(newCard);
31 console.log(`Nouveau dinosaure ajouté : ${dinosaur.name}`);
32
33 // PAS BESOIN d'ajouter des écouteurs d'événements !
34 // La délégation gère automatiquement la nouvelle carte !
35}
36
37// Exemple d'utilisation
38setTimeout(() => {
39 addNewDinosaur({
40 id: '15',
41 name: 'Spinosaurus',
42 species: 'Spinosaurus aegyptiacus',
43 image: 'spinosaurus.jpg'
44 });
45
46 // La nouvelle carte réagit déjà aux clics !
47}, 3000);Dans la fonction addNewDinosaur(), il n'y a pas un seul addEventListener, et pourtant le Spinosaurus ajouté au bout de trois secondes réagit tout de suite aux clics, car ceux-ci bouillonnent jusqu'à l'écouteur qui attend sur la galerie depuis le début.
Délégation avancée - filtres et conditions
« Parfois », explique Ray, « on a besoin d'un contrôle plus précis. » Chaque bouton du panneau indique par data-action ce qu'il doit faire, et l'écouteur répartit les tâches.
1const parkDashboard = document.getElementById('park-dashboard');
2
3parkDashboard.addEventListener('click', function(event) {
4 const target = event.target;
5
6 // Délégation avec l'attribut data-action
7 const action = target.dataset.action;
8
9 if (action) {
10 console.log(`Action : ${action}`);
11
12 switch(action) {
13 case 'open-gate':
14 handleOpenGate(target);
15 break;
16
17 case 'close-gate':
18 handleCloseGate(target);
19 break;
20
21 case 'start-feeding':
22 handleStartFeeding(target);
23 break;
24
25 case 'emergency-protocol':
26 handleEmergencyProtocol(target);
27 break;
28
29 case 'view-stats':
30 handleViewStats(target);
31 break;
32
33 default:
34 console.log('Action inconnue :', action);
35 }
36
37 return;
38 }Une nouvelle action, c'est un nouveau case, et non un nouvel écouteur. Quand l'élément n'a pas d'action, l'écouteur vérifie les classes avec la méthode matches() et les balises avec tagName, qui renvoie le nom en majuscules, d'où 'A'.
1 // Délégation avec les classes
2 if (target.matches('.enclosure-zone')) {
3 const enclosureId = target.dataset.enclosureId;
4 console.log(`Zone d'enclos cliquée : ${enclosureId}`);
5 showEnclosureDetails(enclosureId);
6 return;
7 }
8
9 // Délégation avec les balises
10 if (target.tagName === 'A' && target.classList.contains('internal-link')) {
11 event.preventDefault();
12 const href = target.getAttribute('href');
13 console.log(`Navigation interne : ${href}`);
14 navigateInternal(href);
15 return;
16 }
17});
18
19function handleOpenGate(button) {
20 const gateId = button.dataset.gateId;
21 const enclosureId = button.dataset.enclosureId;
22
23 console.log(`Ouverture du portail ${gateId} dans l'enclos ${enclosureId}`);
24 console.log('Vérification du statut de sécurité...');
25 console.log('Portail ouvert');
26}
27
28function handleCloseGate(button) {
29 const gateId = button.dataset.gateId;
30 console.log(`Fermeture du portail ${gateId}`);
31 console.log('Portail fermé et sécurisé');
32}
33
34function handleStartFeeding(button) {
35 const enclosureId = button.dataset.enclosureId;
36 const feedType = button.dataset.feedType;
37
38 console.log(`Début du nourrissage dans l'enclos ${enclosureId}`);
39 console.log(`Type de nourriture : ${feedType}`);
40}
41
42function handleEmergencyProtocol(button) {
43 const protocolId = button.dataset.protocolId;
44
45 console.log("ALARME ! Activation du protocole d'urgence");
46 console.log(`Protocole : ${protocolId}`);
47 console.log('Fermeture de tous les portails...');
48 console.log('Notification du personnel...');
49}L'appel à preventDefault() bloque la navigation normale du lien, pour que le panneau change de vue lui-même. Chaque gestionnaire lit les données du bouton via dataset : le balisage dit quoi faire, les fonctions savent comment.
Performance - comparaison
Ray montre des tests de performance. « Les chiffres ne mentent pas, mais chacun doit les mesurer chez soi. » Les fonctions console.time() et console.timeEnd() mesurent le temps écoulé entre deux appels qui portent la même étiquette.
1// Test 1 : Sans délégation (écouteurs séparés)
2console.time('Ajout sans délégation');
3
4const cards1 = document.querySelectorAll('.card');
5cards1.forEach(card => {
6 card.addEventListener('click', function() {
7 console.log('Cliqué');
8 });
9});
10
11console.timeEnd('Ajout sans délégation');
12// Le temps augmente avec le nombre de cartes : 100 cartes = 100 appels à addEventListener
13
14// Test 2 : Avec délégation (un seul écouteur)
15console.time('Ajout avec délégation');
16
17const container = document.getElementById('container');
18container.addEventListener('click', function(event) {
19 if (event.target.matches('.card')) {
20 console.log('Cliqué');
21 }
22});
23
24console.timeEnd('Ajout avec délégation');
25// Un seul appel à addEventListener, quel que soit le nombre de cartes
26
27// Consommation de mémoire :
28// Sans délégation : un écouteur et une fonction séparés pour chaque carte
29// Avec délégation : un seul écouteur sur le conteneur
30
31console.log('Écouteurs nécessaires pour 100 cartes :');
32console.log('Sans délégation : 100');
33console.log('Avec délégation : 1');
34console.log('Mesure le temps et la mémoire exacts dans les DevTools de ton navigateur');Nous ne donnons volontairement ni millisecondes ni kilo-octets, car ils dépendent du navigateur et du matériel. Une chose est sûre : sans délégation, les écouteurs se multiplient avec chaque carte, alors qu'avec la délégation, il n'en reste qu'un seul.
matches() vs closest() - choisir la méthode
« Les deux méthodes les plus importantes pour la délégation », explique Ray. Toutes deux acceptent un sélecteur CSS, mais elles regardent dans des directions différentes de l'arbre.
1const container = document.getElementById('enclosure-list');
2
3container.addEventListener('click', function(event) {
4 const target = event.target;
5
6 // matches() - vérifie si l'élément LUI-MÊME correspond
7 if (target.matches('.enclosure-item')) {
8 console.log('Clic DIRECTEMENT sur .enclosure-item');
9 // Ne fonctionne que si on a cliqué exactement sur l'élément qui porte la classe
10 }
11
12 // closest() - remonte jusqu'au premier élément correspondant
13 const item = target.closest('.enclosure-item');
14 if (item) {
15 console.log('Clic sur .enclosure-item OU sur un de ses enfants');
16 // Fonctionne si on a cliqué sur l'élément ou sur ses enfants
17 }
18});
19
20// HTML:
21// <div class="enclosure-item">
22// <h3>Enclos du T-Rex</h3> ← matches() NON, closest() OUI
23// <p>Statut : Actif</p> ← matches() NON, closest() OUI
24// <button>Détails</button> ← matches() NON, closest() OUI
25// </div> ← matches() OUI, closest() OUI
26
27// CONCLUSION : Utilise closest() pour la délégation - c'est plus flexibleLa méthode matches() ne vérifie que l'élément lui-même, tandis que closest() commence par lui et remonte ensuite. C'est pourquoi je te recommande closest() comme choix par défaut pour la délégation.
Système complet de gestion du parc
« Réunissons tout dans un système qui fonctionne », dit Ray. Un seul écouteur sur le tableau de bord transmet le clic aux méthodes successives de la classe.
1class JurassicParkManagementSystem {
2 constructor() {
3 this.selectedEnclosures = new Set();
4 this.initialize();
5 }
6
7 initialize() {
8 console.log('Initialisation du système de gestion du parc');
9 this.setupDelegation();
10 }
11
12 setupDelegation() {
13 // Un seul écouteur principal pour tout le tableau de bord
14 const dashboard = document.getElementById('park-dashboard');
15
16 dashboard.addEventListener('click', (event) => {
17 this.handleDashboardClick(event);
18 });
19
20 console.log('Système de délégation actif');
21 }
22
23 handleDashboardClick(event) {
24 const target = event.target;
25
26 // Gérer les différents types de clics
27 this.handleActionButton(target) ||
28 this.handleTabNavigation(target) ||
29 this.handleStatusIndicator(target) ||
30 this.handleEnclosureSelection(target);
31 }Chaque méthode renvoie true quand elle a pris le clic en charge, et l'opérateur || s'arrête au premier true. C'est pourquoi on interroge d'abord les boutons, et la carte entière en dernier, comme dans la galerie plus haut. La fonction fléchée conserve this comme instance de la classe.
1 handleEnclosureSelection(target) {
2 const enclosure = target.closest('.enclosure-card');
3 if (!enclosure) return false;
4
5 const enclosureId = enclosure.dataset.enclosureId;
6 const enclosureName = enclosure.dataset.enclosureName;
7
8 console.log(`Enclos sélectionné : ${enclosureName} (ID : ${enclosureId})`);
9
10 // Basculer la sélection
11 if (this.selectedEnclosures.has(enclosureId)) {
12 this.selectedEnclosures.delete(enclosureId);
13 enclosure.classList.remove('selected');
14 console.log('Retiré de la sélection');
15 } else {
16 this.selectedEnclosures.add(enclosureId);
17 enclosure.classList.add('selected');
18 console.log('Ajouté à la sélection');
19 }
20
21 this.updateSelectionDisplay();
22 return true;
23 }
24
25 handleActionButton(target) {
26 const action = target.dataset.action;
27 if (!action) return false;
28
29 const enclosure = target.closest('.enclosure-card');
30 const enclosureId = enclosure?.dataset.enclosureId;
31
32 console.log(`Action : ${action} pour l'enclos : ${enclosureId}`);
33
34 const actions = {
35 'feed': () => this.feedDinosaur(enclosureId),
36 'clean': () => this.cleanEnclosure(enclosureId),
37 'maintenance': () => this.scheduleMaintenance(enclosureId),
38 'emergency': () => this.activateEmergency(enclosureId)
39 };
40
41 const handler = actions[action];
42 if (handler) {
43 handler();
44 return true;
45 }
46
47 return false;
48 }Le Set conserve la sélection sans doublons, l'objet actions remplace un switch, et ?. évite une erreur quand le bouton ne se trouve pas dans une carte. Les onglets et les indicateurs de statut fonctionnent de la même façon.
1 handleTabNavigation(target) {
2 if (!target.classList.contains('tab-button')) return false;
3
4 const tabName = target.dataset.tab;
5 console.log(`Passage à l'onglet : ${tabName}`);
6
7 // Retirer active de tous les onglets
8 document.querySelectorAll('.tab-button').forEach(tab => {
9 tab.classList.remove('active');
10 });
11
12 // Ajouter active à l'onglet cliqué
13 target.classList.add('active');
14
15 // Afficher le contenu correspondant
16 this.showTabContent(tabName);
17 return true;
18 }
19
20 handleStatusIndicator(target) {
21 if (!target.classList.contains('status-indicator')) return false;
22
23 const enclosure = target.closest('.enclosure-card');
24 const enclosureId = enclosure?.dataset.enclosureId;
25
26 console.log(`Affichage des détails du statut pour l'enclos : ${enclosureId}`);
27 this.showStatusDetails(enclosureId);
28 return true;
29 }Aucun de ces éléments n'a son propre écouteur, car leurs clics arrivent jusqu'au tableau de bord. Pour finir, les méthodes d'exécution et le démarrage du système.
1 feedDinosaur(enclosureId) {
2 console.log(`Nourrissage du dinosaure dans l'enclos : ${enclosureId}`);
3 }
4
5 cleanEnclosure(enclosureId) {
6 console.log(`Nettoyage de l'enclos : ${enclosureId}`);
7 }
8
9 scheduleMaintenance(enclosureId) {
10 console.log(`Planification de la maintenance pour l'enclos : ${enclosureId}`);
11 }
12
13 activateEmergency(enclosureId) {
14 console.log(`ALARME ! Protocole d'urgence pour l'enclos : ${enclosureId}`);
15 }
16
17 showTabContent(tabName) {
18 console.log(`Chargement du contenu de l'onglet : ${tabName}`);
19 }
20
21 showStatusDetails(enclosureId) {
22 console.log(`Détails du statut de l'enclos : ${enclosureId}`);
23 }
24
25 updateSelectionDisplay() {
26 const count = this.selectedEnclosures.size;
27 const display = document.getElementById('selection-counter');
28
29 if (display) {
30 display.textContent = `Sélectionnés : ${count} enclos`;
31 }
32
33 console.log(`Enclos actuellement sélectionnés : ${Array.from(this.selectedEnclosures).join(', ')}`);
34 }
35}
36
37// Initialisation du système
38const parkSystem = new JurassicParkManagementSystem();Un seul écouteur surveille tout le tableau de bord, quel que soit le nombre d'enclos.
Récapitulatif
« La délégation d'événements », conclut Ray, « n'est pas qu'une optimisation. C'est une technique fondamentale du JavaScript moderne. Un seul écouteur qui gère des milliers d'éléments - c'est comme un système de surveillance central qui gère tout le parc. Efficace, évolutif et facile à maintenir. »
Règles clés :
- Utilise
closest()pour trouver les éléments cibles - Vérifie que l'élément est bien un enfant du conteneur qui porte l'écouteur
- Utilise les attributs data pour les actions et les identifiants
- Pense au
returnaprès avoir géré un événement - La délégation fonctionne automatiquement avec les éléments dynamiques
- Elle économise la mémoire et améliore les performances
Dans le bac à sable, l'écouteur dragstart est attaché à tout le document, donc les dinosaures ajoutés toutes les dix secondes peuvent être glissés immédiatement. Prochaine leçon : les événements souris et clavier.
Retiens bien : un seul capteur au centre de contrôle du secteur entend chaque enclos, y compris celui qu'on a construit hier.
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>Glisser-déposer - Déplacer les dinosaures</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: 1200px;
18 margin: 0 auto;
19 }
20
21 h1 {
22 text-align: center;
23 margin-bottom: 30px;
24 }
25
26 .instructions {
27 background: rgba(52, 152, 219, 0.2);
28 border: 1px solid #3498db;
29 border-radius: 10px;
30 padding: 20px;
31 margin-bottom: 30px;
32 text-align: center;
33 }
34
35 .enclosures-container {
36 display: grid;
37 grid-template-columns: 1fr 1fr;
38 gap: 30px;
39 margin-bottom: 30px;
40 }
41
42 .enclosure {
43 background: rgba(0, 0, 0, 0.6);
44 border: 3px dashed #7f8c8d;
45 border-radius: 15px;
46 padding: 20px;
47 min-height: 300px;
48 transition: all 0.3s ease;
49 }
50
51 .enclosure.drag-over {
52 border-color: #3498db;
53 background: rgba(52, 152, 219, 0.2);
54 border-style: solid;
55 transform: scale(1.02);
56 }
57
58 .enclosure h3 {
59 text-align: center;
60 margin-bottom: 20px;
61 color: #ecf0f1;
62 }
63
64 .herbivore-enclosure {
65 border-color: #27ae60;
66 }
67
68 .carnivore-enclosure {
69 border-color: #e74c3c;
70 }
71
72 .herbivore-enclosure.drag-over {
73 border-color: #2ecc71;
74 background: rgba(46, 204, 113, 0.2);
75 }
76
77 .carnivore-enclosure.drag-over {
78 border-color: #e74c3c;
79 background: rgba(231, 76, 60, 0.2);
80 }
81
82 .dinosaur-card {
83 background: linear-gradient(45deg, #34495e, #2c3e50);
84 border-radius: 10px;
85 padding: 15px;
86 margin: 10px 0;
87 cursor: grab;
88 transition: all 0.3s ease;
89 border: 2px solid transparent;
90 user-select: none;
91 }
92
93 .dinosaur-card:hover {
94 transform: translateY(-3px);
95 box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
96 }
97
98 .dinosaur-card.dragging {
99 opacity: 0.5;
100 transform: rotate(5deg);
101 cursor: grabbing;
102 }
103
104 .dinosaur-card.herbivore {
105 border-color: #27ae60;
106 }
107
108 .dinosaur-card.carnivore {
109 border-color: #e74c3c;
110 }
111
112 .dino-header {
113 display: flex;
114 align-items: center;
115 gap: 10px;
116 margin-bottom: 10px;
117 }
118
119 .dino-emoji {
120 font-size: 24px;
121 }
122
123 .dino-name {
124 font-weight: bold;
125 color: #ecf0f1;
126 }
127
128 .dino-info {
129 font-size: 14px;
130 color: #bdc3c7;
131 margin: 5px 0;
132 }
133
134 .dino-diet {
135 display: inline-block;
136 padding: 3px 8px;
137 border-radius: 12px;
138 font-size: 12px;
139 font-weight: bold;
140 }
141
142 .herbivore .dino-diet {
143 background: #27ae60;
144 color: white;
145 }
146
147 .carnivore .dino-diet {
148 background: #e74c3c;
149 color: white;
150 }
151
152 .drag-instructions {
153 text-align: center;
154 color: #bdc3c7;
155 font-style: italic;
156 margin: 20px 0;
157 }
158
159 .stats-panel {
160 background: rgba(0, 0, 0, 0.8);
161 border-radius: 10px;
162 padding: 20px;
163 margin-top: 30px;
164 }
165
166 .stats-grid {
167 display: grid;
168 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
169 gap: 20px;
170 }
171
172 .stat-item {
173 background: rgba(255, 255, 255, 0.1);
174 padding: 15px;
175 border-radius: 8px;
176 text-align: center;
177 }
178
179 .stat-value {
180 font-size: 24px;
181 font-weight: bold;
182 color: #3498db;
183 }
184
185 .stat-label {
186 color: #bdc3c7;
187 font-size: 14px;
188 margin-top: 5px;
189 }
190
191 .danger-warning {
192 background: #e74c3c;
193 color: white;
194 padding: 10px;
195 border-radius: 8px;
196 margin: 10px 0;
197 text-align: center;
198 font-weight: bold;
199 display: none;
200 }
201
202 @media (max-width: 768px) {
203 .enclosures-container {
204 grid-template-columns: 1fr;
205 }
206 }
207 </style>
208</head>
209<body>
210 <div class="container">
211 <h1>Déplacer les dinosaures entre les enclos</h1>
212
213 <div class="instructions">
214 <strong>Task:</strong> Drag dinosaurs to the appropriate enclosures based on their diet.<br>
215 <span style="color: #27ae60;">■</span> Herbivores → Safe Enclosure |
216 <span style="color: #e74c3c;">■</span> Carnivores → High Security Enclosure
217 </div>
218
219 <div class="enclosures-container">
220 <!-- Enclosure for herbivores -->
221 <div class="enclosure herbivore-enclosure" data-diet="herbivore" id="herbivore-enclosure">
222 <h3>Enclos sécurisé (herbivores)</h3>
223 <div class="drag-instructions">Drag herbivores here</div>
224
225 <!-- Initial herbivore dinosaurs -->
226 <div class="dinosaur-card herbivore" draggable="true" data-dino="triceratops" data-diet="herbivore">
227 <div class="dino-header">
228 <span class="dino-emoji">◆</span>
229 <span class="dino-name">Triceratops</span>
230 </div>
231 <div class="dino-info">Length: 9m | Era: Crétacé</div>
232 <span class="dino-diet">Herbivore</span>
233 </div>
234
235 <div class="dinosaur-card herbivore" draggable="true" data-dino="brachiosaurus" data-diet="herbivore">
236 <div class="dino-header">
237 <span class="dino-emoji">◆</span>
238 <span class="dino-name">Brachiosaurus</span>
239 </div>
240 <div class="dino-info">Length: 23m | Era: Jurassique</div>
241 <span class="dino-diet">Herbivore</span>
242 </div>
243 </div>
244
245 <!-- Enclosure for carnivores -->
246 <div class="enclosure carnivore-enclosure" data-diet="carnivore" id="carnivore-enclosure">
247 <h3>Enclos haute sécurité (carnivores)</h3>
248 <div class="drag-instructions">Drag carnivores here</div>
249 <div class="danger-warning" id="danger-warning">
250 WARNING! Mixing carnivores and herbivores is dangerous!
251 </div>
252
253 <!-- Initial carnivore dinosaurs -->
254 <div class="dinosaur-card carnivore" draggable="true" data-dino="trex" data-diet="carnivore">
255 <div class="dino-header">
256 <span class="dino-emoji">◆</span>
257 <span class="dino-name">Tyrannosaurus Rex</span>
258 </div>
259 <div class="dino-info">Length: 13m | Era: Crétacé</div>
260 <span class="dino-diet">Carnivore</span>
261 </div>
262
263 <div class="dinosaur-card carnivore" draggable="true" data-dino="velociraptor" data-diet="carnivore">
264 <div class="dino-header">
265 <span class="dino-emoji">◆</span>
266 <span class="dino-name">Velociraptor</span>
267 </div>
268 <div class="dino-info">Length: 2m | Era: Crétacé</div>
269 <span class="dino-diet">Carnivore</span>
270 </div>
271 </div>
272 </div>
273
274 <!-- Panneau de statistiques -->
275 <div class="stats-panel">
276 <h3>Statistiques des enclos</h3>
277 <div class="stats-grid">
278 <div class="stat-item">
279 <div class="stat-value" id="herbivore-count">2</div>
280 <div class="stat-label">Herbivores</div>
281 </div>
282 <div class="stat-item">
283 <div class="stat-value" id="carnivore-count">2</div>
284 <div class="stat-label">Carnivores</div>
285 </div>
286 <div class="stat-item">
287 <div class="stat-value" id="total-moves">0</div>
288 <div class="stat-label">Total transfers</div>
289 </div>
290 <div class="stat-item">
291 <div class="stat-value" id="safety-status">OK</div>
292 <div class="stat-label">Security status</div>
293 </div>
294 </div>
295 </div>
296 </div>
297
298 <script>
299 let draggedElement = null;
300 let totalMoves = 0;
301
302 // Get references to elements
303 const herbivoreEnclosure = document.getElementById('herbivore-enclosure');
304 const carnivoreEnclosure = document.getElementById('carnivore-enclosure');
305 const dangerWarning = document.getElementById('danger-warning');
306 const herbivoreCount = document.getElementById('herbivore-count');
307 const carnivoreCount = document.getElementById('carnivore-count');
308 const totalMovesCount = document.getElementById('total-moves');
309 const safetyStatus = document.getElementById('safety-status');
310
311 // Event listeners for dragged element
312 document.addEventListener('dragstart', function(e) {
313 if (e.target.classList.contains('dinosaur-card')) {
314 draggedElement = e.target;
315 e.target.classList.add('dragging');
316
317 console.log(`Started dragging: ${e.target.dataset.dino}`);
318
319 // Configuration des données pour le transfert
320 e.dataTransfer.setData('text/plain', e.target.dataset.dino);
321 e.dataTransfer.effectAllowed = 'move';
322 }
323 });
324
325 document.addEventListener('dragend', function(e) {
326 if (e.target.classList.contains('dinosaur-card')) {
327 e.target.classList.remove('dragging');
328 draggedElement = null;
329 }
330 });
331
332 // Event listeners for drop areas
333 document.querySelectorAll('.enclosure').forEach(enclosure => {
334 enclosure.addEventListener('dragover', function(e) {
335 e.preventDefault(); // Autorisation du drop
336 e.dataTransfer.dropEffect = 'move';
337
338 if (!this.classList.contains('drag-over')) {
339 this.classList.add('drag-over');
340 }
341 });
342
343 enclosure.addEventListener('dragleave', function(e) {
344 // Check if we're actually leaving the enclosure
345 if (!this.contains(e.relatedTarget)) {
346 this.classList.remove('drag-over');
347 }
348 });
349
350 enclosure.addEventListener('drop', function(e) {
351 e.preventDefault();
352 this.classList.remove('drag-over');
353
354 if (draggedElement) {
355 const draggedDiet = draggedElement.dataset.diet;
356 const targetDiet = this.dataset.diet;
357 const dinoName = draggedElement.querySelector('.dino-name').textContent;
358
359 // Vérification si le dinosaur correspond à l'enclosure
360 if (draggedDiet === targetDiet) {
361 // Déplacement de l'élément
362 this.appendChild(draggedElement);
363 totalMoves++;
364
365 console.log(`${dinoName} déplacé vers l'enclosure appropriée !`);
366
367 // Animation de succès
368 const movedCard = draggedElement; // dragend remet draggedElement à null avant la fin de la seconde
369 movedCard.style.border = '2px solid #27ae60';
370 setTimeout(() => {
371 movedCard.style.border = '';
372 }, 1000);
373
374 } else {
375 // Invalid transfer
376 console.log(`ERROR: ${dinoName} cannot be in this enclosure!`);
377
378 // Error animation
379 const refusedCard = draggedElement;
380 refusedCard.style.border = '2px solid #e74c3c';
381 refusedCard.style.background = 'rgba(231, 76, 60, 0.3)';
382
383 setTimeout(() => {
384 refusedCard.style.border = '';
385 refusedCard.style.background = '';
386 }, 1000);
387
388 // Show warning
389 alert(`WARNING!\n\n${dinoName} cannot be placed in this enclosure!\n${draggedDiet === 'carnivore' ? 'Carnivores' : 'Herbivores'} doivent être dans l'enclos approprié pour la sécurité du parc.`);
390 }
391
392 updateStats();
393 }
394 });
395 });
396
397 // Fonction de mise à jour des statistiques
398 function updateStats() {
399 const herbivores = document.querySelectorAll('#herbivore-enclosure .dinosaur-card').length;
400 const carnivores = document.querySelectorAll('#carnivore-enclosure .dinosaur-card').length;
401
402 herbivoreCount.textContent = herbivores;
403 carnivoreCount.textContent = carnivores;
404 totalMovesCount.textContent = totalMoves;
405
406 // Vérification de la security
407 const herbivoreEnclosureHasCarnivores = document.querySelectorAll('#herbivore-enclosure .carnivore').length > 0;
408 const carnivoreEnclosureHasHerbivores = document.querySelectorAll('#carnivore-enclosure .herbivore').length > 0;
409
410 const isDangerous = herbivoreEnclosureHasCarnivores || carnivoreEnclosureHasHerbivores;
411
412 if (isDangerous) {
413 safetyStatus.textContent = '!';
414 safetyStatus.style.color = '#e74c3c';
415 dangerWarning.style.display = 'block';
416 } else {
417 safetyStatus.textContent = 'OK';
418 safetyStatus.style.color = '#27ae60';
419 dangerWarning.style.display = 'none';
420 }
421
422 // Check if all dinosaurs are in the correct enclosures
423 const allInCorrectEnclosures = !isDangerous;
424 if (allInCorrectEnclosures && totalMoves > 0) {
425 setTimeout(() => {
426 alert('Congratulations!\n\nAll dinosaurs are now in the correct enclosures.\nThe park is safe!');
427 }, 500);
428 }
429 }
430
431 // Ajout de nouveaux dinosaurs après quelques secondes
432 function addNewDinosaur() {
433 const newDinos = [
434 {
435 name: 'Stegosaurus',
436 emoji: '',
437 diet: 'herbivore',
438 length: '9m',
439 era: 'Jurassique'
440 },
441 {
442 name: 'Allosaurus',
443 emoji: '',
444 diet: 'carnivore',
445 length: '9m',
446 era: 'Jurassique'
447 }
448 ];
449
450 const randomDino = newDinos[Math.floor(Math.random() * newDinos.length)];
451
452 const newDinoCard = document.createElement('div');
453 newDinoCard.className = `dinosaur-card ${randomDino.diet}`;
454 newDinoCard.draggable = true;
455 newDinoCard.dataset.dino = randomDino.name.toLowerCase();
456 newDinoCard.dataset.diet = randomDino.diet;
457
458 newDinoCard.innerHTML = `
459 <div class="dino-header">
460 <span class="dino-emoji">${randomDino.emoji}</span>
461 <span class="dino-name">${randomDino.name}</span>
462 </div>
463 <div class="dino-info">Length: ${randomDino.length} | Era: ${randomDino.era}</div>
464 <span class="dino-diet">${randomDino.diet === 'herbivore' ? 'Herbivore' : 'Carnivore'}</span>
465 `;
466
467 // Add to incorrect enclosures for challenge
468 if (randomDino.diet === 'herbivore') {
469 carnivoreEnclosure.appendChild(newDinoCard);
470 } else {
471 herbivoreEnclosure.appendChild(newDinoCard);
472 }
473
474 updateStats();
475
476 console.log(`Nouveau dinosaure ajouté: ${randomDino.name}`);
477 }
478
479 // Ajout d'un nouveau dinosaure toutes les 10 secondes
480 setInterval(addNewDinosaur, 10000);
481
482 // Initial statistics update
483 updateStats();
484
485 // Touch events for mobile devices (basic support)
486 let touchItem = null;
487 let touchOffset = { x: 0, y: 0 };
488
489 document.addEventListener('touchstart', function(e) {
490 if (e.target.classList.contains('dinosaur-card')) {
491 touchItem = e.target;
492 const touch = e.touches[0];
493 const rect = touchItem.getBoundingClientRect();
494
495 touchOffset.x = touch.clientX - rect.left;
496 touchOffset.y = touch.clientY - rect.top;
497
498 touchItem.style.position = 'fixed';
499 touchItem.style.zIndex = '1000';
500 touchItem.classList.add('dragging');
501
502 e.preventDefault();
503 }
504 });
505
506 document.addEventListener('touchmove', function(e) {
507 if (touchItem) {
508 const touch = e.touches[0];
509 touchItem.style.left = (touch.clientX - touchOffset.x) + 'px';
510 touchItem.style.top = (touch.clientY - touchOffset.y) + 'px';
511
512 e.preventDefault();
513 }
514 });
515
516 document.addEventListener('touchend', function(e) {
517 if (touchItem) {
518 const touch = e.changedTouches[0];
519 const elementBelow = document.elementFromPoint(touch.clientX, touch.clientY);
520 const enclosure = elementBelow ? elementBelow.closest('.enclosure') : null;
521
522 if (enclosure) {
523 draggedElement = touchItem;
524
525 // Simulation de l'event drop
526 const dropEvent = new Event('drop');
527 enclosure.dispatchEvent(dropEvent);
528 }
529
530 // Reset styles
531 touchItem.style.position = '';
532 touchItem.style.left = '';
533 touchItem.style.top = '';
534 touchItem.style.zIndex = '';
535 touchItem.classList.remove('dragging');
536
537 touchItem = null;
538 draggedElement = null;
539 }
540 });
541 </script>
542</body>
543</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. Quels sont les principaux avantages de la délégation d'événements ?
2. Quelle est la différence entre event.target et event.currentTarget ?
Tâches pratiques dans le jeu
- Éditeur de code
Le fichier index.html contient une liste de cartes de dinosaures #dino-list et un bouton qui ajoute une nouvelle carte après le chargement de la page. Utilise la délégation d'événements : remplace ___BLANK1___ par la variable du conteneur qui porte le seul écouteur click, et ___BLANK2___ par la méthode qui part de event.target et cherche l'élément le plus proche correspondant au sélecteur .dino-card. Un clic sur une carte (aussi à l'intérieur et sur une carte ajoutée plus tard) doit afficher le nom du dinosaure dans #selected-dino, et la carte de la réserve, hors de la liste, ne doit pas réagir.
- Tri vertical
Place dans l'ordre les étapes de configuration de la délégation d'événements :
- Tri horizontal
Place les éléments de la délégation d'événements :