Kurs JavaScript i TypeScript · Moduł 8: Zdarzenia i DOM
Propagacja wydarzeń (Event Bubbling)
W tej lekcji7
"Zobacz," Ray Arnold prowadzi Cię do centralnego panelu alarmowego Parku Jurajskiego. "Gdy sensor wykryje zagrożenie w zagrodzie 7-B, sygnał nie zatrzymuje się tylko tam. Przechodzi wyżej - do systemu sektorowego, następnie do głównego centrum kontrolnego, aż w końcu do centralnego alarmu. To się nazywa propagacja sygnału."
Ray pokazuje schemat systemu alarmowego. "W DOM działa dokładnie tak samo. Gdy klikniesz element, wydarzenie 'bąbelkuje' w górę przez całe drzewo DOM - od elementu docelowego, przez jego rodziców, aż do samego dokumentu."
Czym jest event bubbling?
Event bubbling (bąbelkowanie wydarzeń) to fundamentalny mechanizm w JavaScript, gdzie wydarzenia rozprzestrzeniają się od elementu, na którym wystąpiły, w górę przez wszystkich jego przodków w drzewie DOM.
Trzy fazy propagacji wydarzenia
"Większość wydarzeń," wyjaśnia Ray, "przechodzi przez trzy fazy. To jak protokół bezpieczeństwa - sprawdzamy wszystko w określonej kolejności." Niektóre wydarzenia, na przykład focus, blur, mouseenter i mouseleave, nie bąbelkują: przechodzą fazę capture i docierają do celu, ale nie wracają w górę. Bąbelkujące odpowiedniki focus i blur to focusin i focusout.
1// Fazy wydarzenia:
2// 1. CAPTURE (przechwytywanie) - od document w dół do elementu docelowego
3// 2. TARGET (cel) - na samym elemencie docelowym
4// 3. BUBBLE (bąbelkowanie) - od elementu docelowego w górę do document
5
6document.addEventListener('click', function(event) {
7 // eventPhase pokazuje aktualną fazę:
8 // 1 = CAPTURING_PHASE
9 // 2 = AT_TARGET
10 // 3 = BUBBLING_PHASE
11 console.log('Faza wydarzenia:', event.eventPhase);
12}, true); // true = nasłuchuj w fazie capturePraktyczny przykład propagacji
"Zobaczmy to w akcji," mówi Ray, otwierając strukturę zagrody dinozaurów w systemie.
1// HTML struktura:
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 na przycisku (najbardziej zagnieżdżony)
16emergencyBtn.addEventListener('click', function(event) {
17 console.log('1. ALARM! Przycisk awaryjny wciśnięty');
18 console.log(' Target:', event.target.id);
19 console.log(' CurrentTarget:', event.currentTarget.id);
20 console.log(' Faza:', event.eventPhase); // 2 (AT_TARGET)
21
22 activateLocalAlarm();
23});
24
25// Listener na zagrodzie
26enclosure7.addEventListener('click', function(event) {
27 console.log('2. Zagroda 7 - wykryto aktywację alarmu');
28 console.log(' Target:', event.target.id); // emergency-btn
29 console.log(' CurrentTarget:', event.currentTarget.id); // enclosure-7
30 console.log(' Faza:', event.eventPhase); // 3 (BUBBLING_PHASE)
31
32 closeEnclosureGates();
33});
34
35// Listener na sektorze
36sectorB.addEventListener('click', function(event) {
37 console.log('3. Sektor B - propagacja alarmu do sektora');
38 console.log(' Target:', event.target.id); // emergency-btn
39 console.log(' CurrentTarget:', event.currentTarget.id); // sector-b
40 console.log(' Faza:', event.eventPhase); // 3 (BUBBLING_PHASE)
41
42 lockSectorGates();
43});
44
45// Listener na całej strefie parku
46parkZone.addEventListener('click', function(event) {
47 console.log('4. Strefa Parku - alarm dotarł do centrum kontrolnego');
48 console.log(' Target:', event.target.id); // emergency-btn
49 console.log(' CurrentTarget:', event.currentTarget.id); // park-zone
50 console.log(' Faza:', event.eventPhase); // 3 (BUBBLING_PHASE)
51
52 notifyCentralControl();
53});
54
55// Listener na całym dokumencie
56document.addEventListener('click', function(event) {
57 console.log('5. DOKUMENT - najwyższy poziom systemu powiadomiony');
58 console.log(' Wydarzenie dotarło na sam szczyt hierarchii');
59
60 logSystemEvent(event);
61});
62
63function activateLocalAlarm() {
64 console.log(' → Aktywacja lokalnego alarmu w zagrodzie 7');
65}
66
67function closeEnclosureGates() {
68 console.log(' → Zamykanie bram zagrody 7');
69}
70
71function lockSectorGates() {
72 console.log(' → Blokada wszystkich bram w sektorze B');
73}
74
75function notifyCentralControl() {
76 console.log(' → Powiadamianie centralnego centrum kontrolnego');
77}
78
79function logSystemEvent(event) {
80 console.log(' → Logowanie wydarzenia w systemie głównym');
81}
82
83// Po kliknięciu przycisku zobaczymy wszystkie 5 komunikatów w kolejności!stopPropagation() - zatrzymywanie propagacji
"Czasami," Ray przyciska czerwony przycisk, "chcemy zatrzymać propagację. Jak zawór bezpieczeństwa, który izoluje problem lokalnie."
Kontrolowane zatrzymywanie
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. Przycisk kliknięty');
7 performLocalAction();
8
9 // ZATRZYMAJ propagację - wydarzenie nie pójdzie wyżej
10 event.stopPropagation();
11 console.log(' Propagacja zatrzymana - wydarzenie nie dotrze do rodziców');
12});
13
14container.addEventListener('click', function(event) {
15 console.log('2. Ten listener SIĘ NIE WYKONA (z powodu stopPropagation)');
16 performContainerAction();
17});
18
19section.addEventListener('click', function(event) {
20 console.log('3. Ten listener też SIĘ NIE WYKONA');
21 performSectionAction();
22});
23
24// Po kliknięciu przycisku zobaczymy tylko komunikat "1."Praktyczny przypadek użycia - modalne okno
"Najczęstszy przypadek," wyjaśnia Ray, "to zamykanie okien modalnych."
1// Modal zamykający się po kliknięciu w tło (overlay)
2const modal = document.getElementById('dino-info-modal');
3const modalContent = document.getElementById('modal-content');
4const closeButton = document.getElementById('modal-close');
5
6// Kliknięcie w tło (overlay) zamyka modal
7modal.addEventListener('click', function(event) {
8 console.log('Kliknięto w tło - zamykanie modala');
9 closeModal();
10});
11
12// Kliknięcie w zawartość modala NIE zamyka go
13modalContent.addEventListener('click', function(event) {
14 event.stopPropagation(); // WAŻNE! Zatrzymaj propagację do overlay
15 console.log('Kliknięto w zawartość - modal pozostaje otwarty');
16});
17
18// Przycisk zamykania
19closeButton.addEventListener('click', function(event) {
20 event.stopPropagation(); // Opcjonalne, ale bezpieczne
21 console.log('Przycisk zamknięcia - zamykanie modala');
22 closeModal();
23});
24
25function closeModal() {
26 modal.style.display = 'none';
27 console.log('Modal zamknięty');
28}
29
30function openModal(dinosaurId) {
31 modal.style.display = 'block';
32 loadDinosaurData(dinosaurId);
33 console.log('Modal otwarty dla dinozaura:', dinosaurId);
34}Capture phase - faza przechwytywania
"Większość developerów," mówi Ray, "używa tylko fazy bubble. Ale czasami potrzebujemy fazy capture - sprawdzenia wszystkiego PRZED dotarciem do celu."
Nasłuchiwanie w fazie capture
1const outer = document.getElementById('outer');
2const middle = document.getElementById('middle');
3const inner = document.getElementById('inner');
4
5// Bubble phase (domyślnie, 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 włącza 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// Po kliknięciu inner zobaczymy:
32// CAPTURE: Outer (w dół)
33// CAPTURE: Middle (w dół)
34// CAPTURE: Inner (target) (cel)
35// BUBBLE: Inner (target) (cel)
36// BUBBLE: Middle (w górę)
37// BUBBLE: Outer (w górę)Praktyczne zastosowanie capture - globalne przechwytywanie
1// Globalny handler dla wszystkich kliknięć (np. do analytics)
2document.addEventListener('click', function(event) {
3 const targetElement = event.target;
4
5 console.log('=== GLOBALNY TRACKER KLIKNIĘĆ ===');
6 console.log('Element:', targetElement.tagName);
7 console.log('ID:', targetElement.id);
8 console.log('Klasy:', targetElement.className);
9 console.log('Pozycja:', event.clientX, event.clientY);
10 console.log('Czas:', new Date().toISOString());
11
12 // Wyślij do systemu 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 - wykona się PRZED wszystkimi innymi handlerami
21
22function trackClickEvent(data) {
23 console.log('Wysyłanie danych analytics:', data);
24 // Wyślij do serwera analytics
25}stopImmediatePropagation() - natychmiastowe zatrzymanie
"To bardziej drastyczny środek," ostrzega Ray. "Zatrzymuje NIE TYLKO propagację do rodziców, ale też wszystkie pozostałe listenery na tym samym elemencie."
1const criticalButton = document.getElementById('critical-system-btn');
2
3// Pierwszy listener
4criticalButton.addEventListener('click', function(event) {
5 console.log('1. Pierwszy listener - wykonuje się');
6
7 const systemStatus = checkSystemStatus();
8
9 if (systemStatus === 'CRITICAL') {
10 console.log(' SYSTEM KRYTYCZNY - zatrzymuję wszystkie pozostałe akcje!');
11 event.stopImmediatePropagation(); // STOP WSZYSTKO!
12 handleCriticalSituation();
13 return;
14 }
15});
16
17// Drugi listener - NIE wykona się jeśli stopImmediatePropagation został wywołany
18criticalButton.addEventListener('click', function(event) {
19 console.log('2. Drugi listener - normalny flow');
20 performNormalAction();
21});
22
23// Trzeci listener - NIE wykona się jeśli stopImmediatePropagation został wywołany
24criticalButton.addEventListener('click', function(event) {
25 console.log('3. Trzeci listener - logging');
26 logAction();
27});
28
29function checkSystemStatus() {
30 // Symulacja sprawdzania stanu systemu
31 const random = Math.random();
32 return random > 0.7 ? 'CRITICAL' : 'NORMAL';
33}
34
35function handleCriticalSituation() {
36 console.log(' → Aktywacja protokołów awaryjnych');
37 console.log(' → Blokada wszystkich systemów');
38 console.log(' → Powiadamianie administracji');
39}Praktyczny system powiadomień
"Zobaczmy jak wykorzystać propagację w realnym systemie," mówi Ray, otwierając kod systemu powiadomień parku.
1class ParkNotificationSystem {
2 constructor() {
3 this.notifications = [];
4 this.setupGlobalListeners();
5 }
6
7 setupGlobalListeners() {
8 // Globalny listener w fazie capture
9 document.addEventListener('click', (e) => {
10 this.trackInteraction(e);
11 }, true);
12
13 // Listener dla wszystkich powiadomień
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 // Śledź tylko kluczowe interakcje
27 if (target.matches('.trackable')) {
28 console.log('Interakcja z elementem trackable:', 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 // Zatrzymaj propagację - nie chcemy żeby kliknięcie w powiadomienie
44 // uruchamiało inne handlery
45 event.stopPropagation();
46
47 const notificationId = notification.dataset.id;
48 const action = event.target.dataset.action;
49
50 console.log(`Kliknięto powiadomienie: ${notificationId}`);
51 console.log(`Akcja: ${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('Kliknięto gdzieś w powiadomienie - pokazuję szczegóły');
68 this.showNotificationDetails(notificationId);
69 }
70 }
71
72 dismissNotification(id) {
73 console.log(`Zamykanie powiadomienia: ${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(`Powiadomienie ${id} usunięte`);
83 }, 300);
84 }
85 }
86
87 viewNotification(id) {
88 console.log(`Wyświetlanie szczegółów powiadomienia: ${id}`);
89 // Logika wyświetlania szczegółów
90 }
91
92 performNotificationAction(id) {
93 const notification = this.notifications.find(n => n.id === id);
94
95 if (notification && notification.actionCallback) {
96 console.log(`Wykonywanie akcji dla powiadomienia: ${id}`);
97 notification.actionCallback();
98 }
99 }
100
101 showNotificationDetails(id) {
102 console.log(`Pokazywanie pełnych szczegółów powiadomienia: ${id}`);
103 // Logika wyświetlania pełnych szczegółów
104 }
105
106 logInteraction(data) {
107 console.log('Logowanie interakcji:', data);
108 // Zapis do systemu 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('Utworzono nowe powiadomienie:', 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">Akcja</button>' : ''}
143 <button data-action="dismiss">Zamknij</button>
144 </div>
145 `;
146
147 container.appendChild(notifElement);
148 }
149}
150
151// Inicjalizacja systemu
152const parkNotifications = new ParkNotificationSystem();
153
154// Przykładowe użycie
155parkNotifications.createNotification(
156 'Wykryto nietypową aktywność w zagrodzie T-Rexa',
157 'warning',
158 () => {
159 console.log('Akcja: Otwieranie panelu monitoringu T-Rexa');
160 openTrexMonitoring();
161 }
162);Podsumowanie
"Propagacja wydarzeń," podsumowuje Ray, "to potężny mechanizm. Podobnie jak w systemach alarmowych parku, gdzie sygnał rozprzestrzenia się na odpowiednie poziomy, tak w JavaScript wydarzenia przepływają przez drzewo DOM. Zrozumienie tego mechanizmu jest kluczowe dla efektywnego event handlingu."
Kluczowe koncepcje:
- Większość wydarzeń "bąbelkuje" od elementu docelowego w górę do
documentiwindow(nie bąbelkują m.in.focus,blur,mouseenterimouseleave) - Trzy fazy: Capture → Target → Bubble
event.stopPropagation()zatrzymuje propagację do rodzicówevent.stopImmediatePropagation()zatrzymuje wszystkie pozostałe listenery- Capture phase (trzeci parametr
true) wykonuje się przed bubble event.targetvsevent.currentTarget- kluczowa różnica podczas propagacji
Kod do tej lekcji: index.html
1<!DOCTYPE html>
2<html lang="pl">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>Modal Dinozaur - Park Jurajski</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 dla modala */
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>Informacje o Dinozaurach</h1>
234
235 <div class="instructions">
236 Kliknij na przycisk dinozaura, aby zobaczyć szczegółowe informacje w modalnym oknie.
237 <div class="key-hint">
238 Możesz zamknąć modal klikając <kbd>×</kbd>, klikając poza oknem lub naciskając <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 (początkowo ukryty) -->
266 <div id="dinosaur-modal" class="modal">
267 <div class="modal-content">
268 <div class="modal-header">
269 <h2 id="modal-title">Informacje o dinozaurze</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 Opis dinozaura zostanie tutaj załadowany...
277 </div>
278
279 <div class="dino-stats">
280 <div class="stat-item">
281 <div class="stat-label">Era geologiczna</div>
282 <div class="stat-value" id="era-value">-</div>
283 </div>
284 <div class="stat-item">
285 <div class="stat-label">Dieta</div>
286 <div class="stat-value" id="diet-value">-</div>
287 </div>
288 <div class="stat-item">
289 <div class="stat-label">Długość</div>
290 <div class="stat-value" id="length-value">-</div>
291 </div>
292 <div class="stat-item">
293 <div class="stat-label">Waga</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 Zarezerwuj wycieczkę
301 </button>
302 <button class="action-btn add-favorite-btn" id="add-favorite">
303 Dodaj do ulubionych
304 </button>
305 </div>
306 </div>
307 </div>
308
309 <script>
310 // Dane o dinozaurach
311 const dinosaurData = {
312 trex: {
313 name: "Tyrannosaurus Rex",
314 emoji: "",
315 era: "Kreda późna",
316 diet: "Mięsożerca",
317 length: "12-13 m",
318 weight: "8-9 ton",
319 description: "Tyrannosaurus Rex, znany jako \"Król jaszczurów tyranów\", był jednym z największych drapieżników lądowych wszech czasów. Jego potężne szczęki mogły wywierać siłę do 12 800 funtów na cal kwadratowy. T-Rex żył około 68-66 milionów lat temu na terenach dzisiejszej Ameryki Północnej."
320 },
321 velociraptor: {
322 name: "Velociraptor",
323 emoji: "",
324 era: "Kreda późna",
325 diet: "Mięsożerca",
326 length: "2 m",
327 weight: "15-20 kg",
328 description: "Velociraptor był inteligentnym, szybkim drapieżnikiem żyjącym stadnie. Jego nazwa oznacza \"szybki łowca\". Charakteryzował się sierpowatymi pazurami na tylnych łapach, które służyły do zadawania śmiertelnych ran ofiarom. Żył około 75-71 milionów lat temu w Azji."
329 },
330 triceratops: {
331 name: "Triceratops",
332 emoji: "",
333 era: "Kreda późna",
334 diet: "Roślinożerca",
335 length: "8-9 m",
336 weight: "6-12 ton",
337 description: "Triceratops był jednym z ostatnich dinozaurów przed wielkim wymarciem. Jego charakterystyczne trzy rogi i kostna kryza służyły do obrony przed drapieżnikami i rywalizacji z innymi przedstawicielami gatunku. Żył około 68-66 milionów lat temu w Ameryce Północnej."
338 },
339 brachiosaurus: {
340 name: "Brachiosaurus",
341 emoji: "",
342 era: "Jura późna",
343 diet: "Roślinożerca",
344 length: "20-23 m",
345 weight: "35-40 ton",
346 description: "Brachiosaurus był jednym z największych dinozaurów, które kiedykolwiek chodziły po Ziemi. Jego długa szyja pozwalała mu sięgać do koron najwyższych drzew. W przeciwieństwie do innych zauropodów, jego przednie nogi były dłuższe od tylnych. Żył około 156-145 milionów lat temu."
347 }
348 };
349
350 // Pobieramy referencje do elementów
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 // Funkcja otwierania modala
364 function openModal(dinoType) {
365 const dino = dinosaurData[dinoType];
366
367 // Wypełniamy modal danymi dinozaura
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 // Pokazujemy modal z animacją
377 modal.style.display = 'block';
378 requestAnimationFrame(() => {
379 modal.classList.add('show');
380 });
381
382 // Blokujemy scroll na body
383 document.body.style.overflow = 'hidden';
384
385 console.log(`Otwarto modal dla: ${dino.name}`);
386 }
387
388 // Funkcja zamykania modala
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('Zamknięto modal');
398 }
399
400 // Event listenery dla przycisków dinozaurów
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 dla przycisku zamykania
409 closeBtn.addEventListener('click', closeModal);
410
411 // Zamykanie modala po kliknięciu poza zawartością
412 modal.addEventListener('click', function(event) {
413 if (event.target === modal) {
414 closeModal();
415 }
416 });
417
418 // Obsługa klawisza Escape
419 document.addEventListener('keydown', function(event) {
420 if (event.key === 'Escape' && modal.classList.contains('show')) {
421 closeModal();
422 }
423 });
424
425 // Event listenery dla akcji w modalu
426 bookTourBtn.addEventListener('click', function() {
427 const dinoName = modalTitle.textContent;
428 alert(`Wycieczkę do ${dinoName} zarezerwowano na najbliższy dostępny termin!\n\nSzczegóły zostaną wysłane na Twój email.`);
429 closeModal();
430 });
431
432 addFavoriteBtn.addEventListener('click', function() {
433 const dinoName = modalTitle.textContent;
434
435 if (this.textContent.includes('Dodano')) {
436 this.textContent = 'Dodaj do ulubionych';
437 this.style.background = '#f39c12';
438 alert(`Usunięto ${dinoName} z ulubionych.`);
439 } else {
440 this.textContent = 'Dodano do ulubionych';
441 this.style.background = '#27ae60';
442 alert(`${dinoName} został dodany do Twoich ulubionych dinozaurów!`);
443 }
444 });
445
446 // Demonstracja programowego otwierania modala
447 setTimeout(() => {
448 console.log('Demo: Automatyczne otwarcie modala T-Rex za 3 sekundy...');
449 }, 3000);
450
451 // Opcjonalnie: otwórz modal automatycznie po 5 sekundach
452 // setTimeout(() => openModal('trex'), 5000);
453 </script>
454</body>
455</html>Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jak nazywa się proces przechodzenia wydarzenia od elementu docelowego w górę przez jego rodziców?
2. W jakiej fazie event flow działa listener z opcją { capture: true }?
Zadania praktyczne w grze
- Edytor kodu
W pliku index.html jest przycisk #open-modal i ukryte okno modalne #dino-modal (style.css pokazuje je dopiero z klasą open). Uzupełnij luki w skrypcie: ___BLANK1___ to metoda classList, która dodaje klasę open po kliknięciu przycisku, ___BLANK2___ to zdarzenie naciśnięcia klawisza nasłuchiwane na dokumencie, a ___BLANK3___ to wartość event.key klawisza Escape. Okno ma się otwierać po kliknięciu i zamykać po naciśnięciu Escape, ale nie po naciśnięciu innego klawisza.
- Układanie w pionie
W jakiej kolejności występuje event flow w DOM?
- Układanie w pionie
Ułóż kroki zatrzymywania propagacji zdarzenia: