JavaScript and TypeScript course ยท Module 8: Events and the DOM
Event Propagation (Event Bubbling)
In this lesson7
"Look," Ray Arnold leads you to the central alarm panel of Jurassic Park. "When a sensor detects a threat in enclosure 7-B, the signal doesn't stop there. It goes higher - to the sector system, then to the main control center, and finally to the central alarm. This is called signal propagation."
Ray shows the alarm system diagram. "In the DOM, it works exactly the same way. When you click an element, the event 'bubbles' up through the entire DOM tree - from the target element, through its parents, all the way to the document itself."
What Is Event Bubbling?
Event bubbling is a fundamental mechanism in JavaScript where events propagate from the element on which they occurred, upward through all its ancestors in the DOM tree.
Three Phases of Event Propagation
"Most events," Ray explains, "go through three phases. It's like a security protocol - we check everything in a specific order." Some events, such as focus, blur, mouseenter and mouseleave, do not bubble: they go through the capture phase and reach the target, but do not travel back up. The bubbling counterparts of focus and blur are focusin and focusout.
1// Event phases:
2// 1. CAPTURE (capturing) - from document down to the target element
3// 2. TARGET (target) - on the target element itself
4// 3. BUBBLE (bubbling) - from the target element up to document
5
6document.addEventListener('click', function(event) {
7 // eventPhase shows the current phase:
8 // 1 = CAPTURING_PHASE
9 // 2 = AT_TARGET
10 // 3 = BUBBLING_PHASE
11 console.log('Event phase:', event.eventPhase);
12}, true); // true = listen in capture phasePractical Propagation Example
"Let's see it in action," says Ray, opening the dinosaur enclosure structure in the system.
1// HTML structure:
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">ALARM</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// Listener on the button (most nested)
16emergencyBtn.addEventListener('click', function(event) {
17 console.log('1. ALARM! Emergency button pressed');
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// Listener on the enclosure
26enclosure7.addEventListener('click', function(event) {
27 console.log('2. Enclosure 7 - alarm activation detected');
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// Listener on the sector
36sectorB.addEventListener('click', function(event) {
37 console.log('3. Sector B - alarm propagated to sector');
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// Listener on the entire park zone
46parkZone.addEventListener('click', function(event) {
47 console.log('4. Park Zone - alarm reached central control');
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// Listener on the entire document
56document.addEventListener('click', function(event) {
57 console.log('5. DOCUMENT - highest level of the system notified');
58 console.log(' The event reached the very top of the hierarchy');
59
60 logSystemEvent(event);
61});
62
63function activateLocalAlarm() {
64 console.log(' โ Activating local alarm in enclosure 7');
65}
66
67function closeEnclosureGates() {
68 console.log(' โ Closing enclosure 7 gates');
69}
70
71function lockSectorGates() {
72 console.log(' โ Locking all gates in sector B');
73}
74
75function notifyCentralControl() {
76 console.log(' โ Notifying central control center');
77}
78
79function logSystemEvent(event) {
80 console.log(' โ Logging event in the main system');
81}
82
83// After clicking the button, we'll see all 5 messages in order!stopPropagation() - Stopping Propagation
"Sometimes," Ray presses a red button, "we want to stop propagation. Like a safety valve that isolates the problem locally."
Controlled Stopping
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. Button clicked');
7 performLocalAction();
8
9 // STOP propagation - the event won't go higher
10 event.stopPropagation();
11 console.log(' Propagation stopped - the event won\'t reach parents');
12});
13
14container.addEventListener('click', function(event) {
15 console.log('2. This listener WON\'T EXECUTE (due to stopPropagation)');
16 performContainerAction();
17});
18
19section.addEventListener('click', function(event) {
20 console.log('3. This listener also WON\'T EXECUTE');
21 performSectionAction();
22});
23
24// After clicking the button, we'll only see message "1."Practical Use Case - Modal Window
"The most common case," Ray explains, "is closing modal windows."
1// Modal that closes when clicking the background (overlay)
2const modal = document.getElementById('dino-info-modal');
3const modalContent = document.getElementById('modal-content');
4const closeButton = document.getElementById('modal-close');
5
6// Clicking the background (overlay) closes the modal
7modal.addEventListener('click', function(event) {
8 console.log('Background clicked - closing modal');
9 closeModal();
10});
11
12// Clicking the modal content does NOT close it
13modalContent.addEventListener('click', function(event) {
14 event.stopPropagation(); // IMPORTANT! Stop propagation to overlay
15 console.log('Content clicked - modal stays open');
16});
17
18// Close button
19closeButton.addEventListener('click', function(event) {
20 event.stopPropagation(); // Optional, but safe
21 console.log('Close button - closing modal');
22 closeModal();
23});
24
25function closeModal() {
26 modal.style.display = 'none';
27 console.log('Modal closed');
28}
29
30function openModal(dinosaurId) {
31 modal.style.display = 'block';
32 loadDinosaurData(dinosaurId);
33 console.log('Modal opened for dinosaur:', dinosaurId);
34}Capture Phase - The Capturing Phase
"Most developers," says Ray, "only use the bubble phase. But sometimes we need the capture phase - checking everything BEFORE it reaches the target."
Listening in the Capture Phase
1const outer = document.getElementById('outer');
2const middle = document.getElementById('middle');
3const inner = document.getElementById('inner');
4
5// Bubble phase (default, 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 (target)');
16});
17
18// Capture phase (capture: true)
19outer.addEventListener('click', function(event) {
20 console.log('CAPTURE: Outer');
21}, true); // true enables capture phase
22
23middle.addEventListener('click', function(event) {
24 console.log('CAPTURE: Middle');
25}, true);
26
27inner.addEventListener('click', function(event) {
28 console.log('CAPTURE: Inner (target)');
29}, true);
30
31// After clicking inner, we'll see:
32// CAPTURE: Outer (going down)
33// CAPTURE: Middle (going down)
34// CAPTURE: Inner (target) (target)
35// BUBBLE: Inner (target) (target)
36// BUBBLE: Middle (going up)
37// BUBBLE: Outer (going up)Practical Use of Capture - Global Interception
1// Global handler for all clicks (e.g., for analytics)
2document.addEventListener('click', function(event) {
3 const targetElement = event.target;
4
5 console.log('=== GLOBAL CLICK TRACKER ===');
6 console.log('Element:', 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('Time:', new Date().toISOString());
11
12 // Send to analytics system
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 - executes BEFORE all other handlers
21
22function trackClickEvent(data) {
23 console.log('Sending analytics data:', data);
24 // Send to analytics server
25}stopImmediatePropagation() - Immediate Stop
"This is a more drastic measure," Ray warns. "It stops NOT ONLY propagation to parents, but also all remaining listeners on the same element."
1const criticalButton = document.getElementById('critical-system-btn');
2
3// First listener
4criticalButton.addEventListener('click', function(event) {
5 console.log('1. First listener - executes');
6
7 const systemStatus = checkSystemStatus();
8
9 if (systemStatus === 'CRITICAL') {
10 console.log(' SYSTEM CRITICAL - stopping all remaining actions!');
11 event.stopImmediatePropagation(); // STOP EVERYTHING!
12 handleCriticalSituation();
13 return;
14 }
15});
16
17// Second listener - WON'T execute if stopImmediatePropagation was called
18criticalButton.addEventListener('click', function(event) {
19 console.log('2. Second listener - normal flow');
20 performNormalAction();
21});
22
23// Third listener - WON'T execute if stopImmediatePropagation was called
24criticalButton.addEventListener('click', function(event) {
25 console.log('3. Third listener - logging');
26 logAction();
27});
28
29function checkSystemStatus() {
30 // Simulation of system status check
31 const random = Math.random();
32 return random > 0.7 ? 'CRITICAL' : 'NORMAL';
33}
34
35function handleCriticalSituation() {
36 console.log(' โ Activating emergency protocols');
37 console.log(' โ Locking all systems');
38 console.log(' โ Notifying administration');
39}Practical Notification System
"Let's see how to use propagation in a real system," says Ray, opening the park notification system code.
1class ParkNotificationSystem {
2 constructor() {
3 this.notifications = [];
4 this.setupGlobalListeners();
5 }
6
7 setupGlobalListeners() {
8 // Global listener in capture phase
9 document.addEventListener('click', (e) => {
10 this.trackInteraction(e);
11 }, true);
12
13 // Listener for all 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 // Track only key interactions
27 if (target.matches('.trackable')) {
28 console.log('Interaction with trackable element:', 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 // Stop propagation - we don't want the notification click
44 // to trigger other handlers
45 event.stopPropagation();
46
47 const notificationId = notification.dataset.id;
48 const action = event.target.dataset.action;
49
50 console.log(`Notification clicked: ${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('Clicked somewhere in the notification - showing details');
68 this.showNotificationDetails(notificationId);
69 }
70 }
71
72 dismissNotification(id) {
73 console.log(`Closing 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} removed`);
83 }, 300);
84 }
85 }
86
87 viewNotification(id) {
88 console.log(`Displaying notification details: ${id}`);
89 // Details display logic
90 }
91
92 performNotificationAction(id) {
93 const notification = this.notifications.find(n => n.id === id);
94
95 if (notification && notification.actionCallback) {
96 console.log(`Executing action for notification: ${id}`);
97 notification.actionCallback();
98 }
99 }
100
101 showNotificationDetails(id) {
102 console.log(`Showing full notification details: ${id}`);
103 // Full details display logic
104 }
105
106 logInteraction(data) {
107 console.log('Logging interaction:', data);
108 // Save to analytics system
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('New notification created:', 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">Close</button>
144 </div>
145 `;
146
147 container.appendChild(notifElement);
148 }
149}
150
151// System initialization
152const parkNotifications = new ParkNotificationSystem();
153
154// Example usage
155parkNotifications.createNotification(
156 'Unusual activity detected in the T-Rex enclosure',
157 'warning',
158 () => {
159 console.log('Action: Opening T-Rex monitoring panel');
160 openTrexMonitoring();
161 }
162);Summary
"Event propagation," Ray summarizes, "is a powerful mechanism. Just as in the park's alarm systems, where a signal propagates to the appropriate levels, events in JavaScript flow through the DOM tree. Understanding this mechanism is crucial for effective event handling."
Key concepts:
- Most events "bubble" from the target element up to
documentandwindow(focus,blur,mouseenterandmouseleavedo not) - Three phases: Capture โ Target โ Bubble
event.stopPropagation()stops propagation to parentsevent.stopImmediatePropagation()stops all remaining listeners- Capture phase (third parameter
true) executes before bubble event.targetvsevent.currentTarget- crucial difference during propagation
Code for this lesson: 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>Dinosaur Modal - Jurassic Park</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 for the modal */
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>Dinosaur Information</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">Dinosaur information</h2>
270 <button class="close-btn" id="close-modal">×</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">Geological era</div>
282 <div class="stat-value" id="era-value">-</div>
283 </div>
284 <div class="stat-item">
285 <div class="stat-label">Diet</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">Weight</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 Add to favorites
304 </button>
305 </div>
306 </div>
307 </div>
308
309 <script>
310 // Dinosaur data
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 tons",
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 tons",
337 description: "Triceratops was one of the last dinosaurs before the great extinction. Its characteristic three horns and bony frill served for defense against predators and for rivalry with other members of the species. It lived about 68-66 million years ago in North America."
338 },
339 brachiosaurus: {
340 name: "Brachiosaurus",
341 emoji: "",
342 era: "Late Jurassic",
343 diet: "Herbivore",
344 length: "20-23 m",
345 weight: "35-40 tons",
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. Unlike other sauropods, its front legs were longer than its hind legs. It lived about 156-145 million years ago."
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 // Function that opens the modal
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 // Blocking scrolling on the body
383 document.body.style.overflow = 'hidden';
384
385 console.log(`Modal opened for: ${dino.name}`);
386 }
387
388 // Function that closes the modal
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 for the close button
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 for the modal actions
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('Added')) {
436 this.textContent = 'Add to favorites';
437 this.style.background = '#f39c12';
438 alert(`Removed ${dinoName} from favorites.`);
439 } else {
440 this.textContent = 'Added to favorites';
441 this.style.background = '#27ae60';
442 alert(`${dinoName} has been added to your favorite dinosaurs!`);
443 }
444 });
445
446 // Demo of opening the modal from code
447 setTimeout(() => {
448 console.log('Demo: The T-Rex modal opens automatically in 3 seconds...');
449 }, 3000);
450
451 // Optional: open modal automatically after 5 seconds
452 // setTimeout(() => openModal('trex'), 5000);
453 </script>
454</body>
455</html>Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What is the process called when an event travels upward from the target element through its parents?
2. In which phase of the event flow does a listener with { capture: true } option operate?
Hands-on tasks in the game
- Code editor
The index.html file has a #open-modal button and a hidden modal dialog #dino-modal (style.css shows it only with the open class). Fill in the blanks in the script: ___BLANK1___ is the classList method that adds the open class when the button is clicked, ___BLANK2___ is the key press event listened to on the document, and ___BLANK3___ is the event.key value of the Escape key. The dialog should open on click and close when Escape is pressed, but not when another key is pressed.
- Vertical ordering
In what order does event flow occur in the DOM?
- Vertical ordering
Arrange the steps for stopping event propagation: