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

La propagation des événements (Event Bubbling)

10 min de lecture
Dans cette leçon7

« Regarde », Ray Arnold te conduit jusqu'au panneau d'alarme central de Jurassic Park. « Quand un capteur détecte une menace dans l'enclos 7-B, le signal ne s'arrête pas là. Il remonte - vers le système du secteur, puis vers le centre de contrôle principal, et enfin vers l'alarme centrale. C'est ce qu'on appelle la propagation du signal. »

Ray montre le schéma du système d'alarme. « Dans le DOM, ça fonctionne exactement de la même façon. Quand tu cliques sur un élément, l'événement "bouillonne" vers le haut à travers tout l'arbre du DOM - depuis l'élément cible, à travers ses parents, jusqu'au document lui-même. »

Qu'est-ce que le bouillonnement d'événements ?

Le bouillonnement d'événements (event bubbling) est un mécanisme fondamental en JavaScript où les événements se propagent depuis l'élément sur lequel ils se sont produits, vers le haut, à travers tous ses ancêtres dans l'arbre du DOM.

Les trois phases de la propagation d'un événement

« La plupart des événements », explique Ray, « passent par trois phases. C'est comme un protocole de sécurité - on vérifie tout dans un ordre précis. » Certains événements, comme focus, blur, mouseenter et mouseleave, ne bouillonnent pas : ils passent par la phase de capture et atteignent la cible, mais ne remontent pas. Les équivalents de focus et blur qui bouillonnent sont focusin et focusout.

1// Phases de l'événement :
2// 1. CAPTURE (capturing) - du document jusqu'à l'élément cible
3// 2. TARGET (cible) - sur l'élément cible lui-même
4// 3. BUBBLE (bubbling) - de l'élément cible jusqu'au document
5
6document.addEventListener('click', function(event) {
7  // eventPhase indique la phase courante :
8  // 1 = CAPTURING_PHASE
9  // 2 = AT_TARGET
10  // 3 = BUBBLING_PHASE
11  console.log('Phase de l\'événement :', event.eventPhase);
12}, true); // true = écouter en phase de capture

Exemple pratique de propagation

« Voyons cela en action », dit Ray en ouvrant dans le système la structure de l'enclos des dinosaures.

1// Structure HTML :
2// <div id="park-zone" class="zone">
3//   <div id="sector-b" class="sector">
4//     <div id="enclosure-7" class="enclosure">
5//       <button id="emergency-btn" class="button">ALARME</button>
6//     </div>
7//   </div>
8// </div>
9
10const parkZone = document.getElementById('park-zone');
11const sectorB = document.getElementById('sector-b');
12const enclosure7 = document.getElementById('enclosure-7');
13const emergencyBtn = document.getElementById('emergency-btn');
14
15// Écouteur sur le bouton (le plus imbriqué)
16emergencyBtn.addEventListener('click', function(event) {
17  console.log('1. ALARME ! Bouton d\'urgence pressé');
18  console.log('   Target :', event.target.id);
19  console.log('   CurrentTarget :', event.currentTarget.id);
20  console.log('   Phase :', event.eventPhase); // 2 (AT_TARGET)
21
22  activateLocalAlarm();
23});
24
25// Écouteur sur l'enclos
26enclosure7.addEventListener('click', function(event) {
27  console.log('2. Enclos 7 - activation d\'alarme détectée');
28  console.log('   Target :', event.target.id); // emergency-btn
29  console.log('   CurrentTarget :', event.currentTarget.id); // enclosure-7
30  console.log('   Phase :', event.eventPhase); // 3 (BUBBLING_PHASE)
31
32  closeEnclosureGates();
33});
34
35// Écouteur sur le secteur
36sectorB.addEventListener('click', function(event) {
37  console.log('3. Secteur B - alarme propagée au secteur');
38  console.log('   Target :', event.target.id); // emergency-btn
39  console.log('   CurrentTarget :', event.currentTarget.id); // sector-b
40  console.log('   Phase :', event.eventPhase); // 3 (BUBBLING_PHASE)
41
42  lockSectorGates();
43});
44
45// Écouteur sur toute la zone du parc
46parkZone.addEventListener('click', function(event) {
47  console.log('4. Zone du parc - l\'alarme a atteint le contrôle central');
48  console.log('   Target :', event.target.id); // emergency-btn
49  console.log('   CurrentTarget :', event.currentTarget.id); // park-zone
50  console.log('   Phase :', event.eventPhase); // 3 (BUBBLING_PHASE)
51
52  notifyCentralControl();
53});
54
55// Écouteur sur tout le document
56document.addEventListener('click', function(event) {
57  console.log('5. DOCUMENT - le plus haut niveau du système notifié');
58  console.log('   L\'événement a atteint le sommet de la hiérarchie');
59
60  logSystemEvent(event);
61});
62
63function activateLocalAlarm() {
64  console.log('   → Activation de l\'alarme locale dans l\'enclos 7');
65}
66
67function closeEnclosureGates() {
68  console.log('   → Fermeture des portails de l\'enclos 7');
69}
70
71function lockSectorGates() {
72  console.log('   → Verrouillage de tous les portails du secteur B');
73}
74
75function notifyCentralControl() {
76  console.log('   → Notification du centre de contrôle central');
77}
78
79function logSystemEvent(event) {
80  console.log('   → Enregistrement de l\'événement dans le système principal');
81}
82
83// Après avoir cliqué sur le bouton, on verra les 5 messages dans l'ordre !

stopPropagation() - arrêter la propagation

« Parfois », Ray appuie sur un bouton rouge, « on veut arrêter la propagation. Comme une vanne de sécurité qui isole le problème localement. »

Arrêt contrôlé

1const button = document.getElementById('local-action-btn');
2const container = document.getElementById('action-container');
3const section = document.getElementById('main-section');
4
5button.addEventListener('click', function(event) {
6  console.log('1. Bouton cliqué');
7  performLocalAction();
8
9  // ARRÊTER la propagation - l'événement ne remontera pas
10  event.stopPropagation();
11  console.log('   Propagation arrêtée - l\'événement n\'atteindra pas les parents');
12});
13
14container.addEventListener('click', function(event) {
15  console.log('2. Cet écouteur NE S\'EXÉCUTERA PAS (à cause de stopPropagation)');
16  performContainerAction();
17});
18
19section.addEventListener('click', function(event) {
20  console.log('3. Cet écouteur non plus NE S\'EXÉCUTERA PAS');
21  performSectionAction();
22});
23
24// Après avoir cliqué sur le bouton, on ne verra que le message « 1. »

Cas d'usage pratique - fenêtre modale

« Le cas le plus fréquent », explique Ray, « c'est la fermeture des fenêtres modales. »

1// Modale qui se ferme quand on clique sur l'arrière-plan (overlay)
2const modal = document.getElementById('dino-info-modal');
3const modalContent = document.getElementById('modal-content');
4const closeButton = document.getElementById('modal-close');
5
6// Cliquer sur l'arrière-plan (overlay) ferme la modale
7modal.addEventListener('click', function(event) {
8  console.log('Arrière-plan cliqué - fermeture de la modale');
9  closeModal();
10});
11
12// Cliquer sur le contenu de la modale NE la ferme PAS
13modalContent.addEventListener('click', function(event) {
14  event.stopPropagation(); // IMPORTANT ! Arrêter la propagation vers l'overlay
15  console.log('Contenu cliqué - la modale reste ouverte');
16});
17
18// Bouton de fermeture
19closeButton.addEventListener('click', function(event) {
20  event.stopPropagation(); // Facultatif, mais plus sûr
21  console.log('Bouton de fermeture - fermeture de la modale');
22  closeModal();
23});
24
25function closeModal() {
26  modal.style.display = 'none';
27  console.log('Modale fermée');
28}
29
30function openModal(dinosaurId) {
31  modal.style.display = 'block';
32  loadDinosaurData(dinosaurId);
33  console.log('Modale ouverte pour le dinosaure :', dinosaurId);
34}

La phase de capture

« La plupart des développeurs », dit Ray, « n'utilisent que la phase de bouillonnement. Mais parfois, on a besoin de la phase de capture - vérifier tout AVANT que l'événement n'atteigne la cible. »

Écouter dans la phase de capture

1const outer = document.getElementById('outer');
2const middle = document.getElementById('middle');
3const inner = document.getElementById('inner');
4
5// Phase de bouillonnement (par défaut, capture: false)
6outer.addEventListener('click', function(event) {
7  console.log('BUBBLE : Outer');
8});
9
10middle.addEventListener('click', function(event) {
11  console.log('BUBBLE : Middle');
12});
13
14inner.addEventListener('click', function(event) {
15  console.log('BUBBLE : Inner (cible)');
16});
17
18// Phase de capture (capture: true)
19outer.addEventListener('click', function(event) {
20  console.log('CAPTURE : Outer');
21}, true); // true active la phase de capture
22
23middle.addEventListener('click', function(event) {
24  console.log('CAPTURE : Middle');
25}, true);
26
27inner.addEventListener('click', function(event) {
28  console.log('CAPTURE : Inner (cible)');
29}, true);
30
31// Après avoir cliqué sur inner, on verra :
32// CAPTURE : Outer (descente)
33// CAPTURE : Middle (descente)
34// CAPTURE : Inner (cible) (cible)
35// BUBBLE : Inner (cible) (cible)
36// BUBBLE : Middle (remontée)
37// BUBBLE : Outer (remontée)

Utilisation pratique de la capture - interception globale

1// Gestionnaire global pour tous les clics (ex : pour les analytics)
2document.addEventListener('click', function(event) {
3  const targetElement = event.target;
4
5  console.log('=== TRACKER DE CLICS GLOBAL ===');
6  console.log('Élément :', targetElement.tagName);
7  console.log('ID :', targetElement.id);
8  console.log('Classes :', targetElement.className);
9  console.log('Position :', event.clientX, event.clientY);
10  console.log('Heure :', new Date().toISOString());
11
12  // Envoyer au système d'analytics
13  trackClickEvent({
14    element: targetElement.tagName,
15    elementId: targetElement.id,
16    elementClass: targetElement.className,
17    timestamp: Date.now(),
18    position: { x: event.clientX, y: event.clientY }
19  });
20}, true); // Capture - s'exécute AVANT tous les autres gestionnaires
21
22function trackClickEvent(data) {
23  console.log('Envoi des données analytics :', data);
24  // Envoyer au serveur d'analytics
25}

stopImmediatePropagation() - arrêt immédiat

« C'est une mesure plus radicale », prévient Ray. « Elle arrête NON SEULEMENT la propagation vers les parents, mais aussi tous les écouteurs restants sur le même élément. »

1const criticalButton = document.getElementById('critical-system-btn');
2
3// Premier écouteur
4criticalButton.addEventListener('click', function(event) {
5  console.log('1. Premier écouteur - s\'exécute');
6
7  const systemStatus = checkSystemStatus();
8
9  if (systemStatus === 'CRITICAL') {
10    console.log('   SYSTÈME CRITIQUE - arrêt de toutes les actions restantes !');
11    event.stopImmediatePropagation(); // TOUT ARRÊTER !
12    handleCriticalSituation();
13    return;
14  }
15});
16
17// Deuxième écouteur - NE S'EXÉCUTERA PAS si stopImmediatePropagation a été appelé
18criticalButton.addEventListener('click', function(event) {
19  console.log('2. Deuxième écouteur - flux normal');
20  performNormalAction();
21});
22
23// Troisième écouteur - NE S'EXÉCUTERA PAS si stopImmediatePropagation a été appelé
24criticalButton.addEventListener('click', function(event) {
25  console.log('3. Troisième écouteur - journalisation');
26  logAction();
27});
28
29function checkSystemStatus() {
30  // Simulation d'une vérification du statut du système
31  const random = Math.random();
32  return random > 0.7 ? 'CRITICAL' : 'NORMAL';
33}
34
35function handleCriticalSituation() {
36  console.log('   → Activation des protocoles d\'urgence');
37  console.log('   → Verrouillage de tous les systèmes');
38  console.log('   → Notification de l\'administration');
39}

Système de notification pratique

« Voyons comment utiliser la propagation dans un vrai système », dit Ray en ouvrant le code du système de notifications du parc.

1class ParkNotificationSystem {
2  constructor() {
3    this.notifications = [];
4    this.setupGlobalListeners();
5  }
6
7  setupGlobalListeners() {
8    // Écouteur global en phase de capture
9    document.addEventListener('click', (e) => {
10      this.trackInteraction(e);
11    }, true);
12
13    // Écouteur pour toutes les notifications
14    const notificationContainer = document.getElementById('notifications');
15
16    if (notificationContainer) {
17      notificationContainer.addEventListener('click', (e) => {
18        this.handleNotificationClick(e);
19      });
20    }
21  }
22
23  trackInteraction(event) {
24    const target = event.target;
25
26    // Ne suivre que les interactions clés
27    if (target.matches('.trackable')) {
28      console.log('Interaction avec un élément suivi :', target.id);
29
30      this.logInteraction({
31        element: target.id,
32        type: event.type,
33        timestamp: Date.now()
34      });
35    }
36  }
37
38  handleNotificationClick(event) {
39    const notification = event.target.closest('.notification');
40
41    if (!notification) return;
42
43    // Arrêter la propagation - on ne veut pas que le clic sur la notification
44    // déclenche d'autres gestionnaires
45    event.stopPropagation();
46
47    const notificationId = notification.dataset.id;
48    const action = event.target.dataset.action;
49
50    console.log(`Notification cliquée : ${notificationId}`);
51    console.log(`Action : ${action}`);
52
53    switch(action) {
54      case 'dismiss':
55        this.dismissNotification(notificationId);
56        break;
57
58      case 'view':
59        this.viewNotification(notificationId);
60        break;
61
62      case 'action':
63        this.performNotificationAction(notificationId);
64        break;
65
66      default:
67        console.log('Cliqué quelque part dans la notification - affichage des détails');
68        this.showNotificationDetails(notificationId);
69    }
70  }
71
72  dismissNotification(id) {
73    console.log(`Fermeture de la notification : ${id}`);
74    const notification = document.querySelector(`[data-id="${id}"]`);
75
76    if (notification) {
77      notification.classList.add('dismissing');
78
79      setTimeout(() => {
80        notification.remove();
81        this.notifications = this.notifications.filter(n => n.id !== id);
82        console.log(`Notification ${id} supprimée`);
83      }, 300);
84    }
85  }
86
87  viewNotification(id) {
88    console.log(`Affichage des détails de la notification : ${id}`);
89    // Logique d'affichage des détails
90  }
91
92  performNotificationAction(id) {
93    const notification = this.notifications.find(n => n.id === id);
94
95    if (notification && notification.actionCallback) {
96      console.log(`Exécution de l'action pour la notification : ${id}`);
97      notification.actionCallback();
98    }
99  }
100
101  showNotificationDetails(id) {
102    console.log(`Affichage des détails complets de la notification : ${id}`);
103    // Logique d'affichage des détails complets
104  }
105
106  logInteraction(data) {
107    console.log('Journalisation de l\'interaction :', data);
108    // Sauvegarder dans le système d'analytics
109  }
110
111  createNotification(message, type = 'info', actionCallback = null) {
112    const id = 'notif-' + Date.now();
113
114    const notification = {
115      id,
116      message,
117      type,
118      actionCallback,
119      timestamp: new Date()
120    };
121
122    this.notifications.push(notification);
123    this.renderNotification(notification);
124
125    console.log('Nouvelle notification créée :', id);
126    return id;
127  }
128
129  renderNotification(notification) {
130    const container = document.getElementById('notifications');
131
132    const notifElement = document.createElement('div');
133    notifElement.className = `notification notification-${notification.type}`;
134    notifElement.dataset.id = notification.id;
135
136    notifElement.innerHTML = `
137      <div class="notification-content">
138        <p>${notification.message}</p>
139        <small>${notification.timestamp.toLocaleTimeString()}</small>
140      </div>
141      <div class="notification-actions">
142        ${notification.actionCallback ? '<button data-action="action">Action</button>' : ''}
143        <button data-action="dismiss">Fermer</button>
144      </div>
145    `;
146
147    container.appendChild(notifElement);
148  }
149}
150
151// Initialisation du système
152const parkNotifications = new ParkNotificationSystem();
153
154// Exemple d'utilisation
155parkNotifications.createNotification(
156  'Activité inhabituelle détectée dans l\'enclos du T-Rex',
157  'warning',
158  () => {
159    console.log('Action : ouverture du panneau de surveillance du T-Rex');
160    openTrexMonitoring();
161  }
162);

Récapitulatif

« La propagation des événements », résume Ray, « est un mécanisme puissant. Tout comme dans les systèmes d'alarme du parc, où un signal se propage vers les niveaux appropriés, les événements en JavaScript circulent à travers l'arbre du DOM. Comprendre ce mécanisme est crucial pour une gestion efficace des événements. »

Concepts clés :

  • La plupart des événements "bouillonnent" depuis l'élément cible jusqu'au document et à window (focus, blur, mouseenter et mouseleave ne bouillonnent pas)
  • Trois phases : Capture → Cible → Bouillonnement
  • event.stopPropagation() arrête la propagation vers les parents
  • event.stopImmediatePropagation() arrête tous les écouteurs restants
  • La phase de capture (troisième paramètre true) s'exécute avant le bouillonnement
  • event.target vs event.currentTarget - différence cruciale pendant la propagation
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>Modale dinosaure - Parc Jurassique</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        .main-container {
17            max-width: 800px;
18            margin: 0 auto;
19        }
20
21        h1 {
22            text-align: center;
23            margin-bottom: 30px;
24        }
25
26        .dino-buttons {
27            display: grid;
28            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
29            gap: 20px;
30            margin-bottom: 30px;
31        }
32
33        .dino-button {
34            background: linear-gradient(45deg, #3498db, #2980b9);
35            color: white;
36            border: none;
37            border-radius: 10px;
38            padding: 20px;
39            cursor: pointer;
40            transition: all 0.3s ease;
41            font-size: 16px;
42            font-weight: bold;
43        }
44
45        .dino-button:hover {
46            transform: translateY(-5px);
47            box-shadow: 0 10px 25px rgba(52, 152, 219, 0.4);
48        }
49
50        .dino-emoji {
51            font-size: 48px;
52            display: block;
53            margin-bottom: 10px;
54        }
55
56        /* Styles pour la modale */
57        .modal {
58            display: none;
59            position: fixed;
60            z-index: 1000;
61            left: 0;
62            top: 0;
63            width: 100%;
64            height: 100%;
65            background: rgba(0, 0, 0, 0.8);
66            backdrop-filter: blur(5px);
67            opacity: 0;
68            transition: opacity 0.3s ease;
69        }
70
71        .modal.show {
72            opacity: 1;
73        }
74
75        .modal-content {
76            background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
77            margin: 5% auto;
78            padding: 0;
79            border-radius: 15px;
80            width: 90%;
81            max-width: 600px;
82            max-height: 80vh;
83            overflow-y: auto;
84            transform: scale(0.7);
85            transition: transform 0.3s ease;
86            box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
87        }
88
89        .modal.show .modal-content {
90            transform: scale(1);
91        }
92
93        .modal-header {
94            background: linear-gradient(45deg, #e74c3c, #c0392b);
95            padding: 20px;
96            color: white;
97            display: flex;
98            justify-content: space-between;
99            align-items: center;
100        }
101
102        .modal-header h2 {
103            margin: 0;
104            font-size: 24px;
105        }
106
107        .close-btn {
108            background: none;
109            border: none;
110            color: white;
111            font-size: 24px;
112            cursor: pointer;
113            padding: 0;
114            width: 30px;
115            height: 30px;
116            border-radius: 50%;
117            display: flex;
118            align-items: center;
119            justify-content: center;
120            transition: all 0.3s ease;
121        }
122
123        .close-btn:hover {
124            background: rgba(255, 255, 255, 0.2);
125            transform: rotate(90deg);
126        }
127
128        .modal-body {
129            padding: 30px;
130            color: white;
131            line-height: 1.6;
132        }
133
134        .dino-image {
135            width: 100%;
136            max-width: 300px;
137            height: 200px;
138            background: rgba(255, 255, 255, 0.1);
139            border-radius: 10px;
140            margin: 20px auto;
141            display: flex;
142            align-items: center;
143            justify-content: center;
144            font-size: 80px;
145        }
146
147        .dino-stats {
148            display: grid;
149            grid-template-columns: 1fr 1fr;
150            gap: 15px;
151            margin: 20px 0;
152        }
153
154        .stat-item {
155            background: rgba(255, 255, 255, 0.1);
156            padding: 15px;
157            border-radius: 8px;
158            text-align: center;
159        }
160
161        .stat-label {
162            color: #bdc3c7;
163            font-size: 14px;
164            margin-bottom: 5px;
165        }
166
167        .stat-value {
168            font-size: 18px;
169            font-weight: bold;
170            color: #ecf0f1;
171        }
172
173        .modal-footer {
174            padding: 20px 30px;
175            border-top: 1px solid rgba(255, 255, 255, 0.1);
176            display: flex;
177            gap: 15px;
178            justify-content: center;
179        }
180
181        .action-btn {
182            padding: 12px 24px;
183            border: none;
184            border-radius: 8px;
185            cursor: pointer;
186            font-weight: bold;
187            transition: all 0.3s ease;
188        }
189
190        .book-tour-btn {
191            background: #27ae60;
192            color: white;
193        }
194
195        .book-tour-btn:hover {
196            background: #2ecc71;
197        }
198
199        .add-favorite-btn {
200            background: #f39c12;
201            color: white;
202        }
203
204        .add-favorite-btn:hover {
205            background: #e67e22;
206        }
207
208        .instructions {
209            background: rgba(52, 152, 219, 0.2);
210            border: 1px solid #3498db;
211            border-radius: 10px;
212            padding: 20px;
213            margin-bottom: 30px;
214        }
215
216        .key-hint {
217            font-size: 14px;
218            color: #bdc3c7;
219            text-align: center;
220            margin-top: 20px;
221        }
222
223        .key-hint kbd {
224            background: rgba(255, 255, 255, 0.2);
225            padding: 2px 8px;
226            border-radius: 4px;
227            font-family: monospace;
228        }
229    </style>
230</head>
231<body>
232    <div class="main-container">
233        <h1>Informations sur les dinosaures</h1>
234
235        <div class="instructions">
236            Click on a dinosaur button to see detailed information in a modal window.
237            <div class="key-hint">
238                You can close the modal by clicking <kbd>×</kbd>, clicking outside the window, or pressing <kbd>Escape</kbd>
239            </div>
240        </div>
241
242        <div class="dino-buttons">
243            <button class="dino-button" data-dino="trex">
244                <span class="dino-emoji">◆</span>
245                Tyrannosaurus Rex
246            </button>
247
248            <button class="dino-button" data-dino="velociraptor">
249                <span class="dino-emoji">◆</span>
250                Velociraptor
251            </button>
252
253            <button class="dino-button" data-dino="triceratops">
254                <span class="dino-emoji">◆</span>
255                Triceratops
256            </button>
257
258            <button class="dino-button" data-dino="brachiosaurus">
259                <span class="dino-emoji">◆</span>
260                Brachiosaurus
261            </button>
262        </div>
263    </div>
264
265    <!-- Modal (initially hidden) -->
266    <div id="dinosaur-modal" class="modal">
267        <div class="modal-content">
268            <div class="modal-header">
269                <h2 id="modal-title">Informations sur le dinosaure</h2>
270                <button class="close-btn" id="close-modal">&times;</button>
271            </div>
272            <div class="modal-body">
273                <div id="dino-image" class="dino-image">◆</div>
274
275                <div id="dino-description">
276                    Dinosaur description will be loaded here...
277                </div>
278
279                <div class="dino-stats">
280                    <div class="stat-item">
281                        <div class="stat-label">Ère géologique</div>
282                        <div class="stat-value" id="era-value">-</div>
283                    </div>
284                    <div class="stat-item">
285                        <div class="stat-label">Régime</div>
286                        <div class="stat-value" id="diet-value">-</div>
287                    </div>
288                    <div class="stat-item">
289                        <div class="stat-label">Length</div>
290                        <div class="stat-value" id="length-value">-</div>
291                    </div>
292                    <div class="stat-item">
293                        <div class="stat-label">Poids</div>
294                        <div class="stat-value" id="weight-value">-</div>
295                    </div>
296                </div>
297            </div>
298            <div class="modal-footer">
299                <button class="action-btn book-tour-btn" id="book-tour">
300                    Book a tour
301                </button>
302                <button class="action-btn add-favorite-btn" id="add-favorite">
303                    Ajouter aux favoris
304                </button>
305            </div>
306        </div>
307    </div>
308
309    <script>
310        // Données sur les dinosaurs
311        const dinosaurData = {
312            trex: {
313                name: "Tyrannosaurus Rex",
314                emoji: "",
315                era: "Late Cretaceous",
316                diet: "Carnivore",
317                length: "12-13 m",
318                weight: "8-9 ton",
319                description: "Tyrannosaurus Rex, known as \"King of tyrant lizards\", was one of the largest land predators of all time. Its powerful jaws could exert a force of up to 12,800 pounds per square inch. T-Rex lived approximately 68-66 million years ago in what is now North America."
320            },
321            velociraptor: {
322                name: "Velociraptor",
323                emoji: "",
324                era: "Late Cretaceous",
325                diet: "Carnivore",
326                length: "2 m",
327                weight: "15-20 kg",
328                description: "Velociraptor was an intelligent, fast pack-hunting predator. Its name means \"swift hunter\". It was characterized by sickle-shaped claws on its hind legs, which were used to deliver lethal wounds to prey. It lived approximately 75-71 million years ago in Asia."
329            },
330            triceratops: {
331                name: "Triceratops",
332                emoji: "",
333                era: "Late Cretaceous",
334                diet: "Herbivore",
335                length: "8-9 m",
336                weight: "6-12 ton",
337                description: "Triceratops was one of the last dinosaurs before the great extinction. Its characteristic three horns and bony frill servaient à se défendre contre les prédateurs et à rivaliser avec d'autres membres de son espèce. Il vivait il y a environ 68 à 66 millions d'années en Amérique du Nord."
338            },
339            brachiosaurus: {
340                name: "Brachiosaurus",
341                emoji: "",
342                era: "Late Jurassic",
343                diet: "Herbivore",
344                length: "20-23 m",
345                weight: "35-40 ton",
346                description: "Brachiosaurus was one of the largest dinosaurs that ever walked the Earth. Its long neck allowed it to reach the tops of the tallest trees. Contrairement aux autres sauropodes, ses pattes avant étaient plus longues que ses pattes arrière. Il vivait il y a environ 156 à 145 millions d'années."
347            }
348        };
349
350        // Get references to elements
351        const modal = document.getElementById('dinosaur-modal');
352        const closeBtn = document.getElementById('close-modal');
353        const modalTitle = document.getElementById('modal-title');
354        const dinoImage = document.getElementById('dino-image');
355        const dinoDescription = document.getElementById('dino-description');
356        const eraValue = document.getElementById('era-value');
357        const dietValue = document.getElementById('diet-value');
358        const lengthValue = document.getElementById('length-value');
359        const weightValue = document.getElementById('weight-value');
360        const bookTourBtn = document.getElementById('book-tour');
361        const addFavoriteBtn = document.getElementById('add-favorite');
362
363        // Fonction d'ouverture de la modale
364        function openModal(dinoType) {
365            const dino = dinosaurData[dinoType];
366
367            // Fill modal with dinosaur data
368            modalTitle.textContent = dino.name;
369            dinoImage.textContent = dino.emoji;
370            dinoDescription.textContent = dino.description;
371            eraValue.textContent = dino.era;
372            dietValue.textContent = dino.diet;
373            lengthValue.textContent = dino.length;
374            weightValue.textContent = dino.weight;
375
376            // Show modal with animation
377            modal.style.display = 'block';
378            requestAnimationFrame(() => {
379                modal.classList.add('show');
380            });
381
382            // Blocage du scroll sur le body
383            document.body.style.overflow = 'hidden';
384
385            console.log(`Modale ouverte pour: ${dino.name}`);
386        }
387
388        // Fonction de fermeture de la modale
389        function closeModal() {
390            modal.classList.remove('show');
391
392            setTimeout(() => {
393                modal.style.display = 'none';
394                document.body.style.overflow = 'auto';
395            }, 300);
396
397            console.log('Modal closed');
398        }
399
400        // Event listeners for dinosaur buttons
401        document.querySelectorAll('.dino-button').forEach(button => {
402            button.addEventListener('click', function() {
403                const dinoType = this.dataset.dino;
404                openModal(dinoType);
405            });
406        });
407
408        // Event listener pour le bouton de fermeture
409        closeBtn.addEventListener('click', closeModal);
410
411        // Close modal when clicking outside content
412        modal.addEventListener('click', function(event) {
413            if (event.target === modal) {
414                closeModal();
415            }
416        });
417
418        // Handle Escape key
419        document.addEventListener('keydown', function(event) {
420            if (event.key === 'Escape' && modal.classList.contains('show')) {
421                closeModal();
422            }
423        });
424
425        // Event listeners pour les actions dans la modale
426        bookTourBtn.addEventListener('click', function() {
427            const dinoName = modalTitle.textContent;
428            alert(`Tour to ${dinoName} has been booked for the nearest available date!\n\nDetails will be sent to your email.`);
429            closeModal();
430        });
431
432        addFavoriteBtn.addEventListener('click', function() {
433            const dinoName = modalTitle.textContent;
434
435            if (this.textContent.includes('Ajouté')) {
436                this.textContent = 'Ajouter aux favoris';
437                this.style.background = '#f39c12';
438                alert(`Removed ${dinoName} from favorites.`);
439            } else {
440                this.textContent = 'Ajouté aux favoris';
441                this.style.background = '#27ae60';
442                alert(`${dinoName} has been added to your favorite dinosaurs!`);
443            }
444        });
445
446        // Démonstration de l'ouverture programmatique de la modale
447        setTimeout(() => {
448            console.log('Demo: Ouverture automatique de la modale T-Rex dans 3 secondes...');
449        }, 3000);
450
451        // Optional: open modal automatically after 5 seconds
452        // setTimeout(() => openModal('trex'), 5000);
453    </script>
454</body>
455</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. Comment appelle-t-on le processus durant lequel un événement remonte depuis l'élément cible à travers ses parents ?

  2. 2. Dans quelle phase du flux d'événement opère un écouteur avec l'option { capture: true } ?

Tâches pratiques dans le jeu

  • Éditeur de code

    Le fichier index.html contient un bouton #open-modal et une fenêtre modale cachée #dino-modal (style.css ne l'affiche qu'avec la classe open). Complète les blancs du script : ___BLANK1___ est la méthode de classList qui ajoute la classe open au clic sur le bouton, ___BLANK2___ est l'événement d'appui sur une touche écouté sur le document, et ___BLANK3___ est la valeur de event.key pour la touche Échap. La fenêtre doit s'ouvrir au clic et se fermer quand on appuie sur Échap, mais pas avec une autre touche.

  • Tri vertical

    Dans quel ordre se déroule le flux d'événement dans le DOM ?

  • Tri vertical

    Place dans l'ordre les étapes pour arrêter la propagation d'un événement :

Articles utiles