Kurs JavaScript i TypeScript · Moduł 8: Zdarzenia i DOM
Delegacja wydarzeń (Event Delegation)
W tej lekcji8
"W Parku Jurajskim," Ray Arnold wskazuje na centralny panel monitoringu, "nie instalujemy osobnego systemu alarmowego przy każdym dinozaurze. To byłoby kosztowne i nieefektywne. Zamiast tego mamy centralny system monitoringu sektorów, który obsługuje wszystkie zagrody w swoim obszarze."
Ray otwiera schemat systemu. "Event delegation działa na tej samej zasadzie. Zamiast przypisywać setki event listenerów do poszczególnych elementów, przypisujemy jeden listener do ich wspólnego rodzica. Ten rodzic 'deleguje' obsługę do właściwych elementów."
Czym jest event delegation?
Event delegation to technika wykorzystująca propagację wydarzeń (bubbling) do obsługi zdarzeń na wielu elementach przez jeden listener przypisany do ich wspólnego przodka. To jedna z najważniejszych i najwydajniejszych technik w JavaScript.
Problem: Wiele event listenerów
"Zobaczmy problem," mówi Ray, pokazując kod, w którym każda karta dinozaura dostaje własny listener w pętli forEach.
1// NIEEFEKTYWNE - osobny listener dla każdej karty
2const dinosaurCards = document.querySelectorAll('.dinosaur-card');
3
4console.log(`Przypisuję listenery do ${dinosaurCards.length} kart dinozaurów`);
5
6dinosaurCards.forEach((card, index) => {
7 card.addEventListener('click', function() {
8 const dinoId = this.dataset.dinoId;
9 const dinoName = this.dataset.dinoName;
10
11 console.log(`Kliknięto dinozaura: ${dinoName} (ID: ${dinoId})`);
12 showDinosaurDetails(dinoId);
13 });
14
15 console.log(`Listener ${index + 1}/${dinosaurCards.length} dodany`);
16});
17
18// Problemy tego podejścia:
19// 1. Jeśli mamy 100 kart = 100 listenerów w pamięci
20// 2. Dynamicznie dodane karty NIE będą miały listenerów
21// 3. Trzeba ręcznie usuwać listenery przy usuwaniu kart
22// 4. Większe zużycie pamięci i wolniejsza stronaDziała, ale sto kart to sto listenerów, a karta dodana później zostaje głucha, bo pętla wykonała się raz.
Rozwiązanie: Event Delegation
"Teraz obserwuj," Ray pokazuje lepsze rozwiązanie: jeden listener na galerii sam ustala, w którą kartę trafiło kliknięcie.
1// EFEKTYWNE - jeden listener na kontenerze
2const dinosaurGallery = document.getElementById('dinosaur-gallery');
3
4dinosaurGallery.addEventListener('click', function(event) {
5 // Znajdź najbliższą kartę dinozaura (nawet jeśli kliknięto w dziecko karty)
6 const card = event.target.closest('.dinosaur-card');
7
8 // Jeśli nie kliknięto w kartę dinozaura, zignoruj
9 if (!card) return;
10
11 // Upewnij się, że karta jest dzieckiem naszej galerii
12 if (!dinosaurGallery.contains(card)) return;
13
14 const dinoId = card.dataset.dinoId;
15 const dinoName = card.dataset.dinoName;
16
17 console.log(`Kliknięto dinozaura: ${dinoName} (ID: ${dinoId})`);
18 showDinosaurDetails(dinoId);
19});
20
21// Zalety tego podejścia:
22// 1. Tylko JEDEN listener niezależnie od liczby kart
23// 2. Automatycznie obsługuje dynamicznie dodane karty
24// 3. Lepsze zarządzanie pamięcią
25// 4. Łatwiejsze utrzymanie koduKliknięcie w obrazek karty bąbelkuje aż do galerii: event.target to element, w który kliknięto, a event.currentTarget to element z listenerem, czyli galeria. Metoda closest() idzie od target w górę i zwraca pasującą kartę albo null, a contains() sprawdza, czy karta leży w galerii. Cała technika to cztery kroki: znajdź kontener, dodaj na nim listener, sprawdź event.target i wykonaj akcję dla pasującego elementu.
Praktyczny przykład - System kart dinozaurów
"Zbudujmy kompletny system," mówi Ray. Oto struktura karty i początek listenera, który rozpoznaje przyciski szczegółów i karmienia.
1// HTML struktura:
2// <div id="dino-gallery">
3// <div class="dinosaur-card" data-dino-id="1" data-dino-name="T-Rex">
4// <img src="trex.jpg" alt="T-Rex">
5// <h3>Tyrannosaurus Rex</h3>
6// <div class="card-actions">
7// <button class="view-btn">Szczegóły</button>
8// <button class="feed-btn">Nakarm</button>
9// <button class="favorite-btn">★</button>
10// </div>
11// </div>
12// ... więcej kart
13// </div>
14
15const gallery = document.getElementById('dino-gallery');
16
17gallery.addEventListener('click', function(event) {
18 const target = event.target;
19
20 // Obsługa przycisku szczegółów
21 if (target.classList.contains('view-btn')) {
22 const card = target.closest('.dinosaur-card');
23 const dinoId = card.dataset.dinoId;
24 const dinoName = card.dataset.dinoName;
25
26 console.log(`Otwieranie szczegółów dla: ${dinoName}`);
27 openDinosaurModal(dinoId);
28 return; // Zatrzymaj dalsze sprawdzanie
29 }
30
31 // Obsługa przycisku karmienia
32 if (target.classList.contains('feed-btn')) {
33 const card = target.closest('.dinosaur-card');
34 const dinoId = card.dataset.dinoId;
35 const dinoName = card.dataset.dinoName;
36
37 console.log(`Rozpoczynam karmienie: ${dinoName}`);
38 feedDinosaur(dinoId);
39 return;
40 }Atrybut data-dino-id czytamy jako dataset.dinoId, bo myślniki zamieniają się na camelCase. Każda gałąź kończy się return, więc jedno kliknięcie uruchamia jedną akcję. Dalej ulubione i sama karta.
1 // Obsługa przycisku ulubione
2 if (target.classList.contains('favorite-btn')) {
3 const card = target.closest('.dinosaur-card');
4 const dinoId = card.dataset.dinoId;
5 const dinoName = card.dataset.dinoName;
6
7 // Toggle stanu ulubionego
8 target.classList.toggle('active');
9
10 if (target.classList.contains('active')) {
11 console.log(`Dodano do ulubionych: ${dinoName}`);
12 addToFavorites(dinoId);
13 target.textContent = '★';
14 } else {
15 console.log(`Usunięto z ulubionych: ${dinoName}`);
16 removeFromFavorites(dinoId);
17 target.textContent = '☆';
18 }
19 return;
20 }
21
22 // Obsługa kliknięcia w samą kartę (ale nie w przyciski)
23 const card = target.closest('.dinosaur-card');
24 if (card && gallery.contains(card)) {
25 const dinoId = card.dataset.dinoId;
26 const dinoName = card.dataset.dinoName;
27
28 console.log(`Kliknięto kartę: ${dinoName}`);
29 highlightCard(card);
30 }
31});Kartę sprawdzamy na końcu, inaczej kliknięcie w przycisk podświetlałoby ją przy okazji. Sprawdzenie classList.contains wystarcza, bo w przyciskach jest sam tekst. Ikona w środku przejęłaby kliknięcie i warunek by zawiódł. Na koniec pomocnicy.
1function openDinosaurModal(dinoId) {
2 console.log(`Ładowanie danych dinozaura ID: ${dinoId}`);
3 // Logika otwierania modala
4}
5
6function feedDinosaur(dinoId) {
7 console.log(`Aktywacja systemu karmienia dla dinozaura ID: ${dinoId}`);
8 console.log('Sprawdzanie bezpieczeństwa...');
9 console.log('Otwieranie podajnika...');
10 console.log('Karmienie zakończone');
11}
12
13function addToFavorites(dinoId) {
14 const favorites = JSON.parse(localStorage.getItem('favoriteDinosaurs') || '[]');
15 if (!favorites.includes(dinoId)) {
16 favorites.push(dinoId);
17 localStorage.setItem('favoriteDinosaurs', JSON.stringify(favorites));
18 console.log('Zapisano w ulubionych');
19 }
20}
21
22function removeFromFavorites(dinoId) {
23 let favorites = JSON.parse(localStorage.getItem('favoriteDinosaurs') || '[]');
24 favorites = favorites.filter(id => id !== dinoId);
25 localStorage.setItem('favoriteDinosaurs', JSON.stringify(favorites));
26 console.log('Usunięto z ulubionych');
27}
28
29function highlightCard(card) {
30 // Usuń highlight z innych kart
31 document.querySelectorAll('.dinosaur-card.highlighted').forEach(c => {
32 c.classList.remove('highlighted');
33 });
34
35 // Dodaj highlight do klikniętej karty
36 card.classList.add('highlighted');
37 console.log('Karta podświetlona');
38}Pomocnicy nie wiedzą nic o wydarzeniach. Zapis || '[]' ratuje pierwszą wizytę, gdy getItem() zwraca null.
Dynamiczne dodawanie elementów
"Największa zaleta delegacji," Ray dodaje nowe elementy do interfejsu, "to automatyczna obsługa dynamicznie dodawanych elementów."
1const gallery = document.getElementById('dino-gallery');
2
3// Event delegation - działa dla wszystkich kart, nawet przyszłych
4gallery.addEventListener('click', function(event) {
5 const card = event.target.closest('.dinosaur-card');
6 if (!card) return;
7
8 const dinoName = card.dataset.dinoName;
9 console.log(`Kliknięto: ${dinoName}`);
10});
11
12// Funkcja dodająca nowe karty dynamicznie
13function addNewDinosaur(dinosaur) {
14 const newCard = document.createElement('div');
15 newCard.className = 'dinosaur-card';
16 newCard.dataset.dinoId = dinosaur.id;
17 newCard.dataset.dinoName = dinosaur.name;
18
19 newCard.innerHTML = `
20 <img src="${dinosaur.image}" alt="${dinosaur.name}">
21 <h3>${dinosaur.name}</h3>
22 <p>${dinosaur.species}</p>
23 <div class="card-actions">
24 <button class="view-btn">Szczegóły</button>
25 <button class="feed-btn">Nakarm</button>
26 <button class="favorite-btn">☆</button>
27 </div>
28 `;
29
30 gallery.appendChild(newCard);
31 console.log(`Dodano nowego dinozaura: ${dinosaur.name}`);
32
33 // NIE TRZEBA dodawać event listenerów!
34 // Delegation automatycznie obsłuży nową kartę!
35}
36
37// Przykład użycia
38setTimeout(() => {
39 addNewDinosaur({
40 id: '15',
41 name: 'Spinosaurus',
42 species: 'Spinosaurus aegyptiacus',
43 image: 'spinosaurus.jpg'
44 });
45
46 // Nowa karta już działa z kliknięciami!
47}, 3000);W funkcji addNewDinosaur() nie ma ani jednego addEventListener, a Spinosaurus dodany po trzech sekundach od razu reaguje na kliknięcia, bo bąbelkują one do listenera czekającego na galerii od początku.
Zaawansowana delegacja - filtry i warunki
"Czasami," wyjaśnia Ray, "potrzebujemy bardziej precyzyjnej kontroli." Każdy przycisk panelu mówi przez data-action, co ma zrobić, a listener rozdziela zadania.
1const parkDashboard = document.getElementById('park-dashboard');
2
3parkDashboard.addEventListener('click', function(event) {
4 const target = event.target;
5
6 // Delegacja z atrybutem data-action
7 const action = target.dataset.action;
8
9 if (action) {
10 console.log(`Akcja: ${action}`);
11
12 switch(action) {
13 case 'open-gate':
14 handleOpenGate(target);
15 break;
16
17 case 'close-gate':
18 handleCloseGate(target);
19 break;
20
21 case 'start-feeding':
22 handleStartFeeding(target);
23 break;
24
25 case 'emergency-protocol':
26 handleEmergencyProtocol(target);
27 break;
28
29 case 'view-stats':
30 handleViewStats(target);
31 break;
32
33 default:
34 console.log('Nieznana akcja:', action);
35 }
36
37 return;
38 }Nowa akcja to nowy case, a nie nowy listener. Gdy element nie ma akcji, listener sprawdza klasy metodą matches() i znaczniki przez tagName, który zwraca nazwę wielkimi literami, stąd 'A'.
1 // Delegacja z klasami
2 if (target.matches('.enclosure-zone')) {
3 const enclosureId = target.dataset.enclosureId;
4 console.log(`Kliknięto strefę zagrody: ${enclosureId}`);
5 showEnclosureDetails(enclosureId);
6 return;
7 }
8
9 // Delegacja z tagami
10 if (target.tagName === 'A' && target.classList.contains('internal-link')) {
11 event.preventDefault();
12 const href = target.getAttribute('href');
13 console.log(`Nawigacja wewnętrzna: ${href}`);
14 navigateInternal(href);
15 return;
16 }
17});
18
19function handleOpenGate(button) {
20 const gateId = button.dataset.gateId;
21 const enclosureId = button.dataset.enclosureId;
22
23 console.log(`Otwieranie bramy ${gateId} w zagrodzie ${enclosureId}`);
24 console.log('Sprawdzanie statusu bezpieczeństwa...');
25 console.log('Brama otwarta');
26}
27
28function handleCloseGate(button) {
29 const gateId = button.dataset.gateId;
30 console.log(`Zamykanie bramy ${gateId}`);
31 console.log('Brama zamknięta i zabezpieczona');
32}
33
34function handleStartFeeding(button) {
35 const enclosureId = button.dataset.enclosureId;
36 const feedType = button.dataset.feedType;
37
38 console.log(`Rozpoczęcie karmienia w zagrodzie ${enclosureId}`);
39 console.log(`Typ pokarmu: ${feedType}`);
40}
41
42function handleEmergencyProtocol(button) {
43 const protocolId = button.dataset.protocolId;
44
45 console.log('ALARM! Aktywacja protokołu awaryjnego');
46 console.log(`Protokół: ${protocolId}`);
47 console.log('Zamykanie wszystkich bram...');
48 console.log('Powiadamianie personelu...');
49}Wywołanie preventDefault() blokuje zwykłą nawigację linku, żeby panel przełączył widok sam. Każdy handler czyta dane przycisku przez dataset: znaczniki mówią, co zrobić, funkcje wiedzą jak.
Wydajność - porównanie
Ray pokazuje testy wydajności. "Liczby nie kłamią, ale każdy musi je zmierzyć u siebie." Funkcje console.time() i console.timeEnd() mierzą czas między wywołaniami z tą samą etykietą.
1// Test 1: Bez delegation (osobne listenery)
2console.time('Przypisywanie bez delegation');
3
4const cards1 = document.querySelectorAll('.card');
5cards1.forEach(card => {
6 card.addEventListener('click', function() {
7 console.log('Kliknięto');
8 });
9});
10
11console.timeEnd('Przypisywanie bez delegation');
12// Czas rośnie razem z liczbą kart: 100 kart = 100 wywołań addEventListener
13
14// Test 2: Z delegation (jeden listener)
15console.time('Przypisywanie z delegation');
16
17const container = document.getElementById('container');
18container.addEventListener('click', function(event) {
19 if (event.target.matches('.card')) {
20 console.log('Kliknięto');
21 }
22});
23
24console.timeEnd('Przypisywanie z delegation');
25// Jedno wywołanie addEventListener, niezależnie od liczby kart
26
27// Zużycie pamięci:
28// Bez delegation: osobny listener i osobna funkcja dla każdej karty
29// Z delegation: jeden listener na kontenerze
30
31console.log('Liczba listenerów dla 100 kart:');
32console.log('Bez delegation: 100');
33console.log('Z delegation: 1');
34console.log('Dokładny czas i pamięć zmierz w DevTools swojej przeglądarki');Celowo nie podajemy milisekund ani kilobajtów, bo zależą od przeglądarki i sprzętu. Pewne jest jedno: bez delegacji listenerów przybywa z każdą kartą, a z delegacją zostaje jeden.
matches() vs closest() - wybór metody
"Dwie najważniejsze metody w delegacji," wyjaśnia Ray. Obie przyjmują selektor CSS, ale patrzą w różne strony drzewa.
1const container = document.getElementById('enclosure-list');
2
3container.addEventListener('click', function(event) {
4 const target = event.target;
5
6 // matches() - sprawdza czy SAM element pasuje
7 if (target.matches('.enclosure-item')) {
8 console.log('Kliknięto BEZPOŚREDNIO w .enclosure-item');
9 // Zadziała tylko jeśli kliknięto dokładnie w element z klasą
10 }
11
12 // closest() - szuka w górę do pierwszego pasującego
13 const item = target.closest('.enclosure-item');
14 if (item) {
15 console.log('Kliknięto w .enclosure-item LUB jego dziecko');
16 // Zadziała jeśli kliknięto w element lub jego dzieci
17 }
18});
19
20// HTML:
21// <div class="enclosure-item">
22// <h3>Zagroda T-Rex</h3> ← matches() NIE, closest() TAK
23// <p>Status: Aktywna</p> ← matches() NIE, closest() TAK
24// <button>Szczegóły</button> ← matches() NIE, closest() TAK
25// </div> ← matches() TAK, closest() TAK
26
27// WNIOSEK: Używaj closest() dla delegation - jest bardziej elastyczneMetoda matches() sprawdza tylko sam element, a closest() zaczyna od niego i idzie w górę. Dlatego polecam closest() jako domyślny wybór w delegacji.
Kompletny system zarządzania parkiem
"Połączmy wszystko w działający system," mówi Ray. Jeden listener na dashboardzie przekazuje kliknięcie kolejnym metodom klasy.
1class JurassicParkManagementSystem {
2 constructor() {
3 this.selectedEnclosures = new Set();
4 this.initialize();
5 }
6
7 initialize() {
8 console.log('Inicjalizacja systemu zarządzania parkiem');
9 this.setupDelegation();
10 }
11
12 setupDelegation() {
13 // Jeden główny listener dla całego dashboardu
14 const dashboard = document.getElementById('park-dashboard');
15
16 dashboard.addEventListener('click', (event) => {
17 this.handleDashboardClick(event);
18 });
19
20 console.log('System delegation aktywny');
21 }
22
23 handleDashboardClick(event) {
24 const target = event.target;
25
26 // Obsługa różnych typów kliknięć
27 this.handleActionButton(target) ||
28 this.handleTabNavigation(target) ||
29 this.handleStatusIndicator(target) ||
30 this.handleEnclosureSelection(target);
31 }Każda metoda zwraca true, gdy obsłużyła kliknięcie, a operator || zatrzymuje się na pierwszym true. Dlatego przyciski pytamy najpierw, a całą kartę na końcu, jak w galerii wyżej. Funkcja strzałkowa zachowuje this jako instancję klasy.
1 handleEnclosureSelection(target) {
2 const enclosure = target.closest('.enclosure-card');
3 if (!enclosure) return false;
4
5 const enclosureId = enclosure.dataset.enclosureId;
6 const enclosureName = enclosure.dataset.enclosureName;
7
8 console.log(`Wybrano zagrodę: ${enclosureName} (ID: ${enclosureId})`);
9
10 // Toggle selekcji
11 if (this.selectedEnclosures.has(enclosureId)) {
12 this.selectedEnclosures.delete(enclosureId);
13 enclosure.classList.remove('selected');
14 console.log('Usunięto z selekcji');
15 } else {
16 this.selectedEnclosures.add(enclosureId);
17 enclosure.classList.add('selected');
18 console.log('Dodano do selekcji');
19 }
20
21 this.updateSelectionDisplay();
22 return true;
23 }
24
25 handleActionButton(target) {
26 const action = target.dataset.action;
27 if (!action) return false;
28
29 const enclosure = target.closest('.enclosure-card');
30 const enclosureId = enclosure?.dataset.enclosureId;
31
32 console.log(`Akcja: ${action} dla zagrody: ${enclosureId}`);
33
34 const actions = {
35 'feed': () => this.feedDinosaur(enclosureId),
36 'clean': () => this.cleanEnclosure(enclosureId),
37 'maintenance': () => this.scheduleMaintenance(enclosureId),
38 'emergency': () => this.activateEmergency(enclosureId)
39 };
40
41 const handler = actions[action];
42 if (handler) {
43 handler();
44 return true;
45 }
46
47 return false;
48 }Zbiór Set trzyma zaznaczenia bez duplikatów, obiekt actions zastępuje switch, a ?. chroni przed błędem, gdy przycisk nie leży w karcie. Zakładki i wskaźniki statusu działają tak samo.
1 handleTabNavigation(target) {
2 if (!target.classList.contains('tab-button')) return false;
3
4 const tabName = target.dataset.tab;
5 console.log(`Przełączanie na kartę: ${tabName}`);
6
7 // Usuń active ze wszystkich tabów
8 document.querySelectorAll('.tab-button').forEach(tab => {
9 tab.classList.remove('active');
10 });
11
12 // Dodaj active do klikniętego
13 target.classList.add('active');
14
15 // Pokaż odpowiednią zawartość
16 this.showTabContent(tabName);
17 return true;
18 }
19
20 handleStatusIndicator(target) {
21 if (!target.classList.contains('status-indicator')) return false;
22
23 const enclosure = target.closest('.enclosure-card');
24 const enclosureId = enclosure?.dataset.enclosureId;
25
26 console.log(`Pokazywanie szczegółów statusu dla zagrody: ${enclosureId}`);
27 this.showStatusDetails(enclosureId);
28 return true;
29 }Żaden z tych elementów nie ma własnego listenera, bo ich kliknięcia docierają do dashboardu. Na końcu metody wykonawcze i start systemu.
1 feedDinosaur(enclosureId) {
2 console.log(`Karmienie dinozaura w zagrodzie: ${enclosureId}`);
3 }
4
5 cleanEnclosure(enclosureId) {
6 console.log(`Czyszczenie zagrody: ${enclosureId}`);
7 }
8
9 scheduleMaintenance(enclosureId) {
10 console.log(`Planowanie konserwacji dla zagrody: ${enclosureId}`);
11 }
12
13 activateEmergency(enclosureId) {
14 console.log(`ALARM! Protokół awaryjny dla zagrody: ${enclosureId}`);
15 }
16
17 showTabContent(tabName) {
18 console.log(`Ładowanie zawartości karty: ${tabName}`);
19 }
20
21 showStatusDetails(enclosureId) {
22 console.log(`Szczegóły statusu zagrody: ${enclosureId}`);
23 }
24
25 updateSelectionDisplay() {
26 const count = this.selectedEnclosures.size;
27 const display = document.getElementById('selection-counter');
28
29 if (display) {
30 display.textContent = `Wybrano: ${count} zagród`;
31 }
32
33 console.log(`Aktualnie wybrane zagrody: ${Array.from(this.selectedEnclosures).join(', ')}`);
34 }
35}
36
37// Inicjalizacja systemu
38const parkSystem = new JurassicParkManagementSystem();Jeden listener pilnuje całego dashboardu, niezależnie od liczby zagród.
Podsumowanie
"Event delegation," kończy Ray, "to nie tylko optymalizacja. To fundamentalna technika w nowoczesnym JavaScript. Jeden listener obsługujący tysiące elementów - to jak centralny system monitoringu obsługujący cały park. Wydajne, skalowalne i łatwe w utrzymaniu."
Kluczowe zasady:
- Używaj
closest()do znajdowania docelowych elementów - Sprawdzaj czy element jest dzieckiem kontenera z listenerem
- Używaj data-atrybutów dla akcji i identyfikatorów
- Pamiętaj o
returnpo obsłużeniu wydarzenia - Delegacja działa automatycznie z dynamicznymi elementami
- Oszczędza pamięć i poprawia wydajność
W piaskownicy listener dragstart wisi na całym dokumencie, więc dinozaury dokładane co dziesięć sekund od razu dają się przeciągać. Następna lekcja: wydarzenia myszy i klawiatury.
Pamiętaj: jeden czujnik w centrum kontroli sektora słyszy każdą zagrodę, także tę zbudowaną wczoraj.
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>Drag & Drop - Przenoszenie Dinozaurów</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 .container {
17 max-width: 1200px;
18 margin: 0 auto;
19 }
20
21 h1 {
22 text-align: center;
23 margin-bottom: 30px;
24 }
25
26 .instructions {
27 background: rgba(52, 152, 219, 0.2);
28 border: 1px solid #3498db;
29 border-radius: 10px;
30 padding: 20px;
31 margin-bottom: 30px;
32 text-align: center;
33 }
34
35 .enclosures-container {
36 display: grid;
37 grid-template-columns: 1fr 1fr;
38 gap: 30px;
39 margin-bottom: 30px;
40 }
41
42 .enclosure {
43 background: rgba(0, 0, 0, 0.6);
44 border: 3px dashed #7f8c8d;
45 border-radius: 15px;
46 padding: 20px;
47 min-height: 300px;
48 transition: all 0.3s ease;
49 }
50
51 .enclosure.drag-over {
52 border-color: #3498db;
53 background: rgba(52, 152, 219, 0.2);
54 border-style: solid;
55 transform: scale(1.02);
56 }
57
58 .enclosure h3 {
59 text-align: center;
60 margin-bottom: 20px;
61 color: #ecf0f1;
62 }
63
64 .herbivore-enclosure {
65 border-color: #27ae60;
66 }
67
68 .carnivore-enclosure {
69 border-color: #e74c3c;
70 }
71
72 .herbivore-enclosure.drag-over {
73 border-color: #2ecc71;
74 background: rgba(46, 204, 113, 0.2);
75 }
76
77 .carnivore-enclosure.drag-over {
78 border-color: #e74c3c;
79 background: rgba(231, 76, 60, 0.2);
80 }
81
82 .dinosaur-card {
83 background: linear-gradient(45deg, #34495e, #2c3e50);
84 border-radius: 10px;
85 padding: 15px;
86 margin: 10px 0;
87 cursor: grab;
88 transition: all 0.3s ease;
89 border: 2px solid transparent;
90 user-select: none;
91 }
92
93 .dinosaur-card:hover {
94 transform: translateY(-3px);
95 box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
96 }
97
98 .dinosaur-card.dragging {
99 opacity: 0.5;
100 transform: rotate(5deg);
101 cursor: grabbing;
102 }
103
104 .dinosaur-card.herbivore {
105 border-color: #27ae60;
106 }
107
108 .dinosaur-card.carnivore {
109 border-color: #e74c3c;
110 }
111
112 .dino-header {
113 display: flex;
114 align-items: center;
115 gap: 10px;
116 margin-bottom: 10px;
117 }
118
119 .dino-emoji {
120 font-size: 24px;
121 }
122
123 .dino-name {
124 font-weight: bold;
125 color: #ecf0f1;
126 }
127
128 .dino-info {
129 font-size: 14px;
130 color: #bdc3c7;
131 margin: 5px 0;
132 }
133
134 .dino-diet {
135 display: inline-block;
136 padding: 3px 8px;
137 border-radius: 12px;
138 font-size: 12px;
139 font-weight: bold;
140 }
141
142 .herbivore .dino-diet {
143 background: #27ae60;
144 color: white;
145 }
146
147 .carnivore .dino-diet {
148 background: #e74c3c;
149 color: white;
150 }
151
152 .drag-instructions {
153 text-align: center;
154 color: #bdc3c7;
155 font-style: italic;
156 margin: 20px 0;
157 }
158
159 .stats-panel {
160 background: rgba(0, 0, 0, 0.8);
161 border-radius: 10px;
162 padding: 20px;
163 margin-top: 30px;
164 }
165
166 .stats-grid {
167 display: grid;
168 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
169 gap: 20px;
170 }
171
172 .stat-item {
173 background: rgba(255, 255, 255, 0.1);
174 padding: 15px;
175 border-radius: 8px;
176 text-align: center;
177 }
178
179 .stat-value {
180 font-size: 24px;
181 font-weight: bold;
182 color: #3498db;
183 }
184
185 .stat-label {
186 color: #bdc3c7;
187 font-size: 14px;
188 margin-top: 5px;
189 }
190
191 .danger-warning {
192 background: #e74c3c;
193 color: white;
194 padding: 10px;
195 border-radius: 8px;
196 margin: 10px 0;
197 text-align: center;
198 font-weight: bold;
199 display: none;
200 }
201
202 @media (max-width: 768px) {
203 .enclosures-container {
204 grid-template-columns: 1fr;
205 }
206 }
207 </style>
208</head>
209<body>
210 <div class="container">
211 <h1>Przenoszenie Dinozaurów Między Zagrodami</h1>
212
213 <div class="instructions">
214 <strong>Zadanie:</strong> Przeciągnij dinozaury do odpowiednich zagród na podstawie ich diety.<br>
215 <span style="color: #27ae60;">■</span> Roślinożercy → Zagroda bezpieczna |
216 <span style="color: #e74c3c;">■</span> Mięsożercy → Zagroda wysokiego bezpieczeństwa
217 </div>
218
219 <div class="enclosures-container">
220 <!-- Zagroda dla roślinożerców -->
221 <div class="enclosure herbivore-enclosure" data-diet="herbivore" id="herbivore-enclosure">
222 <h3>Zagroda Bezpieczna (Roślinożercy)</h3>
223 <div class="drag-instructions">Przeciągnij tutaj roślinożerców</div>
224
225 <!-- Początkowe dinozaury roślinożerne -->
226 <div class="dinosaur-card herbivore" draggable="true" data-dino="triceratops" data-diet="herbivore">
227 <div class="dino-header">
228 <span class="dino-emoji">◆</span>
229 <span class="dino-name">Triceratops</span>
230 </div>
231 <div class="dino-info">Długość: 9m | Era: Kreda</div>
232 <span class="dino-diet">Roślinożerca</span>
233 </div>
234
235 <div class="dinosaur-card herbivore" draggable="true" data-dino="brachiosaurus" data-diet="herbivore">
236 <div class="dino-header">
237 <span class="dino-emoji">◆</span>
238 <span class="dino-name">Brachiosaurus</span>
239 </div>
240 <div class="dino-info">Długość: 23m | Era: Jura</div>
241 <span class="dino-diet">Roślinożerca</span>
242 </div>
243 </div>
244
245 <!-- Zagroda dla mięsożerców -->
246 <div class="enclosure carnivore-enclosure" data-diet="carnivore" id="carnivore-enclosure">
247 <h3>Zagroda Wysokiego Bezpieczeństwa (Mięsożercy)</h3>
248 <div class="drag-instructions">Przeciągnij tutaj mięsożerców</div>
249 <div class="danger-warning" id="danger-warning">
250 UWAGA! Mieszanie mięsożerców i roślinożerców jest niebezpieczne!
251 </div>
252
253 <!-- Początkowe dinozaury mięsożerne -->
254 <div class="dinosaur-card carnivore" draggable="true" data-dino="trex" data-diet="carnivore">
255 <div class="dino-header">
256 <span class="dino-emoji">◆</span>
257 <span class="dino-name">Tyrannosaurus Rex</span>
258 </div>
259 <div class="dino-info">Długość: 13m | Era: Kreda</div>
260 <span class="dino-diet">Mięsożerca</span>
261 </div>
262
263 <div class="dinosaur-card carnivore" draggable="true" data-dino="velociraptor" data-diet="carnivore">
264 <div class="dino-header">
265 <span class="dino-emoji">◆</span>
266 <span class="dino-name">Velociraptor</span>
267 </div>
268 <div class="dino-info">Długość: 2m | Era: Kreda</div>
269 <span class="dino-diet">Mięsożerca</span>
270 </div>
271 </div>
272 </div>
273
274 <!-- Panel statystyk -->
275 <div class="stats-panel">
276 <h3>Statystyki Zagród</h3>
277 <div class="stats-grid">
278 <div class="stat-item">
279 <div class="stat-value" id="herbivore-count">2</div>
280 <div class="stat-label">Roślinożercy</div>
281 </div>
282 <div class="stat-item">
283 <div class="stat-value" id="carnivore-count">2</div>
284 <div class="stat-label">Mięsożercy</div>
285 </div>
286 <div class="stat-item">
287 <div class="stat-value" id="total-moves">0</div>
288 <div class="stat-label">Całkowite przeniesienia</div>
289 </div>
290 <div class="stat-item">
291 <div class="stat-value" id="safety-status">OK</div>
292 <div class="stat-label">Status bezpieczeństwa</div>
293 </div>
294 </div>
295 </div>
296 </div>
297
298 <script>
299 let draggedElement = null;
300 let totalMoves = 0;
301
302 // Pobieramy referencje do elementów
303 const herbivoreEnclosure = document.getElementById('herbivore-enclosure');
304 const carnivoreEnclosure = document.getElementById('carnivore-enclosure');
305 const dangerWarning = document.getElementById('danger-warning');
306 const herbivoreCount = document.getElementById('herbivore-count');
307 const carnivoreCount = document.getElementById('carnivore-count');
308 const totalMovesCount = document.getElementById('total-moves');
309 const safetyStatus = document.getElementById('safety-status');
310
311 // Event listenery dla przeciąganego elementu
312 document.addEventListener('dragstart', function(e) {
313 if (e.target.classList.contains('dinosaur-card')) {
314 draggedElement = e.target;
315 e.target.classList.add('dragging');
316
317 console.log(`Rozpoczęto przeciąganie: ${e.target.dataset.dino}`);
318
319 // Ustawiamy dane dla transferu
320 e.dataTransfer.setData('text/plain', e.target.dataset.dino);
321 e.dataTransfer.effectAllowed = 'move';
322 }
323 });
324
325 document.addEventListener('dragend', function(e) {
326 if (e.target.classList.contains('dinosaur-card')) {
327 e.target.classList.remove('dragging');
328 draggedElement = null;
329 }
330 });
331
332 // Event listenery dla obszarów drop
333 document.querySelectorAll('.enclosure').forEach(enclosure => {
334 enclosure.addEventListener('dragover', function(e) {
335 e.preventDefault(); // Pozwalamy na drop
336 e.dataTransfer.dropEffect = 'move';
337
338 if (!this.classList.contains('drag-over')) {
339 this.classList.add('drag-over');
340 }
341 });
342
343 enclosure.addEventListener('dragleave', function(e) {
344 // Sprawdzamy czy opuszczamy rzeczywiście enclosure
345 if (!this.contains(e.relatedTarget)) {
346 this.classList.remove('drag-over');
347 }
348 });
349
350 enclosure.addEventListener('drop', function(e) {
351 e.preventDefault();
352 this.classList.remove('drag-over');
353
354 if (draggedElement) {
355 const draggedDiet = draggedElement.dataset.diet;
356 const targetDiet = this.dataset.diet;
357 const dinoName = draggedElement.querySelector('.dino-name').textContent;
358
359 // Sprawdzamy czy dinozaur pasuje do zagrody
360 if (draggedDiet === targetDiet) {
361 // Przenosimy element
362 this.appendChild(draggedElement);
363 totalMoves++;
364
365 console.log(`${dinoName} przeniesiony do odpowiedniej zagrody!`);
366
367 // Animacja sukcesu
368 const movedCard = draggedElement; // dragend wyzeruje draggedElement przed upływem sekundy
369 movedCard.style.border = '2px solid #27ae60';
370 setTimeout(() => {
371 movedCard.style.border = '';
372 }, 1000);
373
374 } else {
375 // Nieprawidłowe przeniesienie
376 console.log(`BŁĄD: ${dinoName} nie może być w tej zagrodzie!`);
377
378 // Animacja błędu
379 const refusedCard = draggedElement;
380 refusedCard.style.border = '2px solid #e74c3c';
381 refusedCard.style.background = 'rgba(231, 76, 60, 0.3)';
382
383 setTimeout(() => {
384 refusedCard.style.border = '';
385 refusedCard.style.background = '';
386 }, 1000);
387
388 // Pokazujemy ostrzeżenie
389 alert(`OSTRZEŻENIE!\n\n${dinoName} nie może być umieszczony w tej zagrodzie!\n${draggedDiet === 'carnivore' ? 'Mięsożercy' : 'Roślinożercy'} muszą być w odpowiedniej zagrodzie dla bezpieczeństwa parku.`);
390 }
391
392 updateStats();
393 }
394 });
395 });
396
397 // Funkcja aktualizacji statystyk
398 function updateStats() {
399 const herbivores = document.querySelectorAll('#herbivore-enclosure .dinosaur-card').length;
400 const carnivores = document.querySelectorAll('#carnivore-enclosure .dinosaur-card').length;
401
402 herbivoreCount.textContent = herbivores;
403 carnivoreCount.textContent = carnivores;
404 totalMovesCount.textContent = totalMoves;
405
406 // Sprawdzamy bezpieczeństwo
407 const herbivoreEnclosureHasCarnivores = document.querySelectorAll('#herbivore-enclosure .carnivore').length > 0;
408 const carnivoreEnclosureHasHerbivores = document.querySelectorAll('#carnivore-enclosure .herbivore').length > 0;
409
410 const isDangerous = herbivoreEnclosureHasCarnivores || carnivoreEnclosureHasHerbivores;
411
412 if (isDangerous) {
413 safetyStatus.textContent = '!';
414 safetyStatus.style.color = '#e74c3c';
415 dangerWarning.style.display = 'block';
416 } else {
417 safetyStatus.textContent = 'OK';
418 safetyStatus.style.color = '#27ae60';
419 dangerWarning.style.display = 'none';
420 }
421
422 // Sprawdzamy czy wszystkie dinozaury są w odpowiednich zagrodach
423 const allInCorrectEnclosures = !isDangerous;
424 if (allInCorrectEnclosures && totalMoves > 0) {
425 setTimeout(() => {
426 alert('Gratulacje!\n\nWszystkie dinozaury są teraz w odpowiednich zagrodach.\nPark jest bezpieczny!');
427 }, 500);
428 }
429 }
430
431 // Dodajemy nowe dinozaury po kilku sekundach
432 function addNewDinosaur() {
433 const newDinos = [
434 {
435 name: 'Stegosaurus',
436 emoji: '',
437 diet: 'herbivore',
438 length: '9m',
439 era: 'Jura'
440 },
441 {
442 name: 'Allosaurus',
443 emoji: '',
444 diet: 'carnivore',
445 length: '9m',
446 era: 'Jura'
447 }
448 ];
449
450 const randomDino = newDinos[Math.floor(Math.random() * newDinos.length)];
451
452 const newDinoCard = document.createElement('div');
453 newDinoCard.className = `dinosaur-card ${randomDino.diet}`;
454 newDinoCard.draggable = true;
455 newDinoCard.dataset.dino = randomDino.name.toLowerCase();
456 newDinoCard.dataset.diet = randomDino.diet;
457
458 newDinoCard.innerHTML = `
459 <div class="dino-header">
460 <span class="dino-emoji">${randomDino.emoji}</span>
461 <span class="dino-name">${randomDino.name}</span>
462 </div>
463 <div class="dino-info">Długość: ${randomDino.length} | Era: ${randomDino.era}</div>
464 <span class="dino-diet">${randomDino.diet === 'herbivore' ? 'Roślinożerca' : 'Mięsożerca'}</span>
465 `;
466
467 // Dodajemy do nieprawidłowej zagrody dla wyzwania
468 if (randomDino.diet === 'herbivore') {
469 carnivoreEnclosure.appendChild(newDinoCard);
470 } else {
471 herbivoreEnclosure.appendChild(newDinoCard);
472 }
473
474 updateStats();
475
476 console.log(`Dodano nowego dinozaura: ${randomDino.name}`);
477 }
478
479 // Dodajemy nowego dinozaura co 10 sekund
480 setInterval(addNewDinosaur, 10000);
481
482 // Początkowa aktualizacja statystyk
483 updateStats();
484
485 // Touch events dla urządzeń mobilnych (podstawowa obsługa)
486 let touchItem = null;
487 let touchOffset = { x: 0, y: 0 };
488
489 document.addEventListener('touchstart', function(e) {
490 if (e.target.classList.contains('dinosaur-card')) {
491 touchItem = e.target;
492 const touch = e.touches[0];
493 const rect = touchItem.getBoundingClientRect();
494
495 touchOffset.x = touch.clientX - rect.left;
496 touchOffset.y = touch.clientY - rect.top;
497
498 touchItem.style.position = 'fixed';
499 touchItem.style.zIndex = '1000';
500 touchItem.classList.add('dragging');
501
502 e.preventDefault();
503 }
504 });
505
506 document.addEventListener('touchmove', function(e) {
507 if (touchItem) {
508 const touch = e.touches[0];
509 touchItem.style.left = (touch.clientX - touchOffset.x) + 'px';
510 touchItem.style.top = (touch.clientY - touchOffset.y) + 'px';
511
512 e.preventDefault();
513 }
514 });
515
516 document.addEventListener('touchend', function(e) {
517 if (touchItem) {
518 const touch = e.changedTouches[0];
519 const elementBelow = document.elementFromPoint(touch.clientX, touch.clientY);
520 const enclosure = elementBelow ? elementBelow.closest('.enclosure') : null;
521
522 if (enclosure) {
523 draggedElement = touchItem;
524
525 // Symulujemy drop event
526 const dropEvent = new Event('drop');
527 enclosure.dispatchEvent(dropEvent);
528 }
529
530 // Reset styles
531 touchItem.style.position = '';
532 touchItem.style.left = '';
533 touchItem.style.top = '';
534 touchItem.style.zIndex = '';
535 touchItem.classList.remove('dragging');
536
537 touchItem = null;
538 draggedElement = null;
539 }
540 });
541 </script>
542</body>
543</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. Jakie są główne zalety event delegation (delegacji wydarzeń)?
2. Jaka jest różnica między event.target a event.currentTarget?
Zadania praktyczne w grze
- Edytor kodu
W pliku index.html jest lista kart dinozaurów #dino-list i przycisk, który dodaje nową kartę po załadowaniu strony. Zastosuj delegację zdarzeń: w miejscu ___BLANK1___ wpisz zmienną kontenera, na którym ma wisieć jedyny listener click, a w miejscu ___BLANK2___ metodę, która od event.target szuka najbliższego elementu pasującego do selektora .dino-card. Kliknięcie karty (także jej środka i karty dodanej później) ma pokazać nazwę dinozaura w #selected-dino, a karta z rezerwy poza listą ma zostać bez reakcji.
- Układanie w pionie
Ułóż kroki konfiguracji event delegation:
- Układanie w poziomie
Ułóż elementy delegacji zdarzeń: