JavaScript and TypeScript course ยท Module 8: Events and the DOM

Event Propagation (Event Bubbling)

9 min read
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 phase

Practical 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 document and window (focus, blur, mouseenter and mouseleave do not)
  • Three phases: Capture โ†’ Target โ†’ Bubble
  • event.stopPropagation() stops propagation to parents
  • event.stopImmediatePropagation() stops all remaining listeners
  • Capture phase (third parameter true) executes before bubble
  • event.target vs event.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">&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">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. 1. What is the process called when an event travels upward from the target element through its parents?

  2. 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:

Useful articles