Kurs JavaScript i TypeScript · Moduł 8: Zdarzenia i DOM

Propagacja wydarzeń (Event Bubbling)

8 min czytania
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 capture

Praktyczny 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 document i window (nie bąbelkują m.in. focus, blur, mouseenter i mouseleave)
  • Trzy fazy: Capture → Target → Bubble
  • event.stopPropagation() zatrzymuje propagację do rodziców
  • event.stopImmediatePropagation() zatrzymuje wszystkie pozostałe listenery
  • Capture phase (trzeci parametr true) wykonuje się przed bubble
  • event.target vs event.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">&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                    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. 1. Jak nazywa się proces przechodzenia wydarzenia od elementu docelowego w górę przez jego rodziców?

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

Przydatne artykuły