"To jest moment prawdy," ogłasza Ray Arnold, główny inżynier systemów Parku Jurajskiego, wskazując na ciemny, wyłączony ekran centralnego panelu kontrolnego. "Nasz główny system monitoringu uległ awarii podczas ostatniej burzy. Musisz odbudować interaktywny panel kontrolny od zera, wykorzystując wszystko, czego nauczyłeś się o wydarzeniach i interakcjach w JavaScript."
W tym projekcie stworzysz kompletny, interaktywny panel kontrolny Parku Jurajskiego. Panel będzie obsługiwał klikanie, przeciąganie, zdarzenia klawiatury, formularze, delegację wydarzeń, propagację, throttling, custom events i odpowiednie zarządzanie pamięcią. To prawdziwy test wszystkich umiejętności z tego modułu - od podstawowych event listenerów po zaawansowane wzorce architektoniczne.
Zbudujesz panel kontrolny z następującymi funkcjonalnościami:
Zamiast dodawać event listener do każdego sektora osobno, wykorzystaj delegację wydarzeń - jeden listener na kontenerze mapy obsługuje kliknięcia we wszystkich sektorach. Pamiętaj też o kontrolowaniu propagacji, aby kliknięcie w dinozaura nie aktywowało jednocześnie sektora.
1// Struktura HTML mapy parku
2// <div id="park-map">
3// <div class="sector" data-sector="A" data-danger="high">
4// <span class="dino" data-dino-id="1">T-Rex</span>
5// <span class="dino" data-dino-id="2">Velociraptor</span>
6// </div>
7// <div class="sector" data-sector="B" data-danger="low">
8// <span class="dino" data-dino-id="3">Triceratops</span>
9// </div>
10// </div>
11
12class ParkMap {
13 constructor(mapElement) {
14 this.mapElement = mapElement;
15 this.selectedSector = null;
16 this.setupDelegation();
17 }
18
19 setupDelegation() {
20 // Jeden listener obsługuje wszystkie kliknięcia na mapie
21 this.mapElement.addEventListener('click', (event) => {
22 const dinoElement = event.target.closest('.dino');
23
24 if (dinoElement) {
25 // Kliknięto w dinozaura - zatrzymaj propagację do sektora
26 event.stopPropagation();
27 this.handleDinoClick(dinoElement);
28 return;
29 }
30
31 const sectorElement = event.target.closest('.sector');
32 if (sectorElement) {
33 this.handleSectorClick(sectorElement);
34 }
35 });
36 }
37
38 handleSectorClick(sectorEl) {
39 // Odznacz poprzedni sektor
40 if (this.selectedSector) {
41 this.selectedSector.classList.remove('selected');
42 }
43
44 sectorEl.classList.add('selected');
45 this.selectedSector = sectorEl;
46
47 const sectorId = sectorEl.dataset.sector;
48 const danger = sectorEl.dataset.danger;
49
50 console.log(`Sektor ${sectorId} wybrany [Zagrożenie: ${danger}]`);
51
52 // Emituj custom event z informacją o wybranym sektorze
53 const sectorEvent = new CustomEvent('sector:selected', {
54 detail: { sectorId, danger },
55 bubbles: true
56 });
57 sectorEl.dispatchEvent(sectorEvent);
58 }
59
60 handleDinoClick(dinoEl) {
61 const dinoId = dinoEl.dataset.dinoId;
62 const dinoName = dinoEl.textContent;
63
64 console.log(`Wybrany dinozaur: ${dinoName} (ID: ${dinoId})`);
65
66 // Custom event z danymi dinozaura
67 const dinoEvent = new CustomEvent('dino:selected', {
68 detail: { dinoId, dinoName },
69 bubbles: true
70 });
71 dinoEl.dispatchEvent(dinoEvent);
72 }
73
74 // Dynamiczne dodawanie nowych sektorów - dzięki delegacji
75 // nie trzeba dodawać nowych listenerów
76 addSector(sectorId, danger) {
77 const sector = document.createElement('div');
78 sector.className = 'sector';
79 sector.dataset.sector = sectorId;
80 sector.dataset.danger = danger;
81 sector.textContent = `Sektor ${sectorId}`;
82 this.mapElement.appendChild(sector);
83 // Event listener już działa automatycznie!
84 }
85}Stwórz system reagujący na skróty klawiszowe do szybkiego uruchamiania procedur awaryjnych. System musi obsługiwać kombinacje klawiszy, rozróżniać kontekst (czy użytkownik pisze w formularzu, czy steruje panelem) i reagować na różne typy wydarzeń myszy.
1class AlarmSystem {
2 constructor() {
3 this.alarmLevel = 0; // 0-4 (0 = brak, 4 = krytyczny)
4 this.isActive = true;
5 this.keyHandlers = new Map();
6 this.boundHandleKeyDown = this.handleKeyDown.bind(this);
7 this.boundHandleKeyUp = this.handleKeyUp.bind(this);
8 this.pressedKeys = new Set();
9
10 this.registerShortcuts();
11 this.attachListeners();
12 }
13
14 registerShortcuts() {
15 // Ctrl+1 do Ctrl+4: Poziomy alarmu
16 this.keyHandlers.set('ctrl+1', () => this.setAlarmLevel(1));
17 this.keyHandlers.set('ctrl+2', () => this.setAlarmLevel(2));
18 this.keyHandlers.set('ctrl+3', () => this.setAlarmLevel(3));
19 this.keyHandlers.set('ctrl+4', () => this.setAlarmLevel(4));
20
21 // Escape: Wyłącz alarm
22 this.keyHandlers.set('escape', () => this.setAlarmLevel(0));
23
24 // Ctrl+Shift+E: Ewakuacja
25 this.keyHandlers.set('ctrl+shift+e', () => this.triggerEvacuation());
26
27 // Ctrl+Shift+L: Zablokuj wszystkie zagrody
28 this.keyHandlers.set('ctrl+shift+l', () => this.lockAllEnclosures());
29 }
30
31 attachListeners() {
32 document.addEventListener('keydown', this.boundHandleKeyDown);
33 document.addEventListener('keyup', this.boundHandleKeyUp);
34 }
35
36 handleKeyDown(event) {
37 // Ignoruj skróty, gdy użytkownik pisze w formularzu
38 if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
39 return;
40 }
41
42 this.pressedKeys.add(event.key.toLowerCase());
43
44 // Buduj kombinację klawiszy
45 const combo = this.buildKeyCombo(event);
46
47 const handler = this.keyHandlers.get(combo);
48 if (handler) {
49 event.preventDefault(); // Zapobiegaj domyślnej akcji przeglądarki
50 handler();
51 }
52 }
53
54 handleKeyUp(event) {
55 this.pressedKeys.delete(event.key.toLowerCase());
56 }
57
58 buildKeyCombo(event) {
59 const parts = [];
60 if (event.ctrlKey || event.metaKey) parts.push('ctrl');
61 if (event.shiftKey) parts.push('shift');
62 if (event.altKey) parts.push('alt');
63
64 const key = event.key.toLowerCase();
65 if (!['control', 'shift', 'alt', 'meta'].includes(key)) {
66 parts.push(key);
67 }
68
69 return parts.join('+');
70 }
71
72 setAlarmLevel(level) {
73 const previousLevel = this.alarmLevel;
74 this.alarmLevel = level;
75
76 console.log(`⚠️ Poziom alarmu: ${previousLevel} → ${level}`);
77
78 // Emituj custom event o zmianie poziomu alarmu
79 document.dispatchEvent(new CustomEvent('alarm:levelChanged', {
80 detail: { previousLevel, currentLevel: level }
81 }));
82 }
83
84 triggerEvacuation() {
85 console.log('🚨 EWAKUACJA URUCHOMIONA!');
86 this.setAlarmLevel(4);
87 document.dispatchEvent(new CustomEvent('alarm:evacuation'));
88 }
89
90 lockAllEnclosures() {
91 console.log('🔒 Blokowanie wszystkich zagród...');
92 document.dispatchEvent(new CustomEvent('alarm:lockdown'));
93 }
94
95 // Czyszczenie pamięci - KRYTYCZNE
96 destroy() {
97 document.removeEventListener('keydown', this.boundHandleKeyDown);
98 document.removeEventListener('keyup', this.boundHandleKeyUp);
99 this.keyHandlers.clear();
100 this.pressedKeys.clear();
101 this.isActive = false;
102 console.log('AlarmSystem: Listenery usunięte, pamięć wyczyszczona');
103 }
104}Zbuduj formularz obsługujący zdarzenia
submit, input, change, focus i blur. Formularz powinien walidować dane w czasie rzeczywistym i zapobiegać wysłaniu niepoprawnych danych.1class IncidentForm {
2 constructor(formElement) {
3 this.form = formElement;
4 this.errors = new Map();
5 this.boundHandlers = [];
6
7 this.setupValidation();
8 this.setupSubmit();
9 }
10
11 setupValidation() {
12 // Walidacja pola "opis" przy każdym wpisywanym znaku
13 const descriptionInput = this.form.querySelector('#description');
14 const descHandler = (event) => {
15 const value = event.target.value;
16 if (value.length < 10) {
17 this.setError('description', 'Opis musi mieć minimum 10 znaków');
18 } else if (value.length > 500) {
19 this.setError('description', 'Opis nie może przekraczać 500 znaków');
20 } else {
21 this.clearError('description');
22 }
23 };
24 descriptionInput.addEventListener('input', descHandler);
25 this.boundHandlers.push({ element: descriptionInput, type: 'input', handler: descHandler });
26
27 // Walidacja selecta "sektor" przy zmianie
28 const sectorSelect = this.form.querySelector('#sector');
29 const sectorHandler = (event) => {
30 if (!event.target.value) {
31 this.setError('sector', 'Wybierz sektor');
32 } else {
33 this.clearError('sector');
34 }
35 };
36 sectorSelect.addEventListener('change', sectorHandler);
37 this.boundHandlers.push({ element: sectorSelect, type: 'change', handler: sectorHandler });
38
39 // Focus/blur efekty wizualne
40 const allInputs = this.form.querySelectorAll('input, select, textarea');
41 allInputs.forEach(input => {
42 const focusHandler = () => input.parentElement.classList.add('focused');
43 const blurHandler = () => {
44 input.parentElement.classList.remove('focused');
45 // Waliduj pole po opuszczeniu
46 input.dispatchEvent(new Event('input'));
47 };
48
49 input.addEventListener('focus', focusHandler);
50 input.addEventListener('blur', blurHandler);
51 this.boundHandlers.push(
52 { element: input, type: 'focus', handler: focusHandler },
53 { element: input, type: 'blur', handler: blurHandler }
54 );
55 });
56 }
57
58 setupSubmit() {
59 const submitHandler = (event) => {
60 event.preventDefault(); // Zapobiegaj domyślnemu wysłaniu formularza
61
62 if (this.errors.size > 0) {
63 console.log('Formularz zawiera błędy:', [...this.errors.values()]);
64 this.highlightErrors();
65 return;
66 }
67
68 const formData = new FormData(this.form);
69 const incident = Object.fromEntries(formData.entries());
70
71 console.log('Incydent zgłoszony:', incident);
72
73 // Custom event z danymi incydentu
74 document.dispatchEvent(new CustomEvent('incident:reported', {
75 detail: incident
76 }));
77
78 this.form.reset();
79 };
80
81 this.form.addEventListener('submit', submitHandler);
82 this.boundHandlers.push({ element: this.form, type: 'submit', handler: submitHandler });
83 }
84
85 setError(field, message) {
86 this.errors.set(field, message);
87 const errorEl = this.form.querySelector(`#${field}-error`);
88 if (errorEl) errorEl.textContent = message;
89 }
90
91 clearError(field) {
92 this.errors.delete(field);
93 const errorEl = this.form.querySelector(`#${field}-error`);
94 if (errorEl) errorEl.textContent = '';
95 }
96
97 highlightErrors() {
98 this.errors.forEach((message, field) => {
99 const input = this.form.querySelector(`#${field}`);
100 if (input) {
101 input.classList.add('error');
102 setTimeout(() => input.classList.remove('error'), 2000);
103 }
104 });
105 }
106
107 // Czyszczenie WSZYSTKICH listenerów
108 destroy() {
109 this.boundHandlers.forEach(({ element, type, handler }) => {
110 element.removeEventListener(type, handler);
111 });
112 this.boundHandlers = [];
113 this.errors.clear();
114 console.log('IncidentForm: Wszystkie listenery usunięte');
115 }
116}Stwórz system monitorowania aktywności dinozaurów, który wykorzystuje throttling do ograniczania częstotliwości aktualizacji oraz Event Bus do komunikacji między wszystkimi modułami panelu.
1// Event Bus - centralny system komunikacji
2class EventBus {
3 constructor() {
4 this.listeners = new Map();
5 }
6
7 on(event, callback) {
8 if (!this.listeners.has(event)) {
9 this.listeners.set(event, new Set());
10 }
11 this.listeners.get(event).add(callback);
12
13 // Zwróć funkcję do odsubskrybowania
14 return () => this.off(event, callback);
15 }
16
17 off(event, callback) {
18 const callbacks = this.listeners.get(event);
19 if (callbacks) {
20 callbacks.delete(callback);
21 if (callbacks.size === 0) {
22 this.listeners.delete(event);
23 }
24 }
25 }
26
27 emit(event, data) {
28 const callbacks = this.listeners.get(event);
29 if (callbacks) {
30 callbacks.forEach(cb => {
31 try {
32 cb(data);
33 } catch (error) {
34 console.error(`Błąd w listenerze "${event}":`, error);
35 }
36 });
37 }
38 }
39
40 // Czyszczenie wszystkich subskrypcji
41 clear() {
42 this.listeners.clear();
43 console.log('EventBus: Wszystkie subskrypcje wyczyszczone');
44 }
45}
46
47// Throttle utility
48function throttle(fn, delay) {
49 let lastCall = 0;
50 let timeoutId = null;
51
52 const throttled = function(...args) {
53 const now = Date.now();
54 const remaining = delay - (now - lastCall);
55
56 if (remaining <= 0) {
57 if (timeoutId) {
58 clearTimeout(timeoutId);
59 timeoutId = null;
60 }
61 lastCall = now;
62 fn.apply(this, args);
63 } else if (!timeoutId) {
64 timeoutId = setTimeout(() => {
65 lastCall = Date.now();
66 timeoutId = null;
67 fn.apply(this, args);
68 }, remaining);
69 }
70 };
71
72 // Metoda do czyszczenia
73 throttled.cancel = () => {
74 if (timeoutId) {
75 clearTimeout(timeoutId);
76 timeoutId = null;
77 }
78 };
79
80 return throttled;
81}
82
83// Monitor aktywności z throttlingiem
84class ActivityMonitor {
85 constructor(eventBus, displayElement) {
86 this.eventBus = eventBus;
87 this.display = displayElement;
88 this.unsubscribers = [];
89 this.throttledFunctions = [];
90
91 this.setupMonitoring();
92 }
93
94 setupMonitoring() {
95 // Throttlowane aktualizacje pozycji myszy na mapie
96 const updatePosition = throttle((data) => {
97 this.display.querySelector('.position').textContent =
98 `Kursor: (${data.x}, ${data.y})`;
99 }, 100); // Maksymalnie 10 aktualizacji na sekundę
100 this.throttledFunctions.push(updatePosition);
101
102 // Nasłuchuj na zdarzenia z EventBus
103 const unsub1 = this.eventBus.on('dino:moved', (data) => {
104 this.logActivity(`Dinozaur ${data.name} przemieścił się do (${data.x}, ${data.y})`);
105 });
106
107 const unsub2 = this.eventBus.on('alarm:levelChanged', (data) => {
108 this.logActivity(`Alarm: poziom ${data.previousLevel} → ${data.currentLevel}`);
109 });
110
111 const unsub3 = this.eventBus.on('incident:reported', (data) => {
112 this.logActivity(`Nowy incydent w sektorze ${data.sector}: ${data.description}`);
113 });
114
115 this.unsubscribers.push(unsub1, unsub2, unsub3);
116
117 // Mousemove na mapie z throttlingiem
118 const mapElement = document.getElementById('park-map');
119 if (mapElement) {
120 const mouseMoveHandler = (event) => {
121 const rect = mapElement.getBoundingClientRect();
122 updatePosition({
123 x: Math.round(event.clientX - rect.left),
124 y: Math.round(event.clientY - rect.top)
125 });
126 };
127 mapElement.addEventListener('mousemove', mouseMoveHandler);
128 this.unsubscribers.push(() => {
129 mapElement.removeEventListener('mousemove', mouseMoveHandler);
130 });
131 }
132 }
133
134 logActivity(message) {
135 const entry = document.createElement('div');
136 entry.className = 'log-entry';
137 const time = new Date().toLocaleTimeString();
138 entry.textContent = `[${time}] ${message}`;
139
140 this.display.querySelector('.log').prepend(entry);
141
142 // Ogranicz liczbę wpisów w logu
143 const entries = this.display.querySelectorAll('.log-entry');
144 if (entries.length > 50) {
145 entries[entries.length - 1].remove();
146 }
147 }
148
149 // Czyszczenie WSZYSTKIEGO
150 destroy() {
151 // Odsubskrybuj z EventBus
152 this.unsubscribers.forEach(unsub => unsub());
153 this.unsubscribers = [];
154
155 // Anuluj throttlowane funkcje
156 this.throttledFunctions.forEach(fn => fn.cancel());
157 this.throttledFunctions = [];
158
159 console.log('ActivityMonitor: Pełne czyszczenie zakończone');
160 }
161}Zacznij od stworzenia centralnego
EventBus, który będzie łączył wszystkie moduły. Następnie buduj poszczególne komponenty w kolejności:1// 1. Inicjalizacja Event Bus
2const eventBus = new EventBus();
3
4// 2. Mapa parku z delegacją
5const parkMap = new ParkMap(document.getElementById('park-map'));
6
7// 3. System alarmowy
8const alarmSystem = new AlarmSystem();
9
10// 4. Formularz incydentów
11const incidentForm = new IncidentForm(document.getElementById('incident-form'));
12
13// 5. Monitor aktywności
14const activityMonitor = new ActivityMonitor(
15 eventBus,
16 document.getElementById('activity-panel')
17);
18
19// 6. Połącz moduły przez EventBus
20// Nasłuchuj custom events z DOM i przekieruj do EventBus
21document.addEventListener('sector:selected', (e) => {
22 eventBus.emit('sector:selected', e.detail);
23});
24
25document.addEventListener('alarm:levelChanged', (e) => {
26 eventBus.emit('alarm:levelChanged', e.detail);
27});
28
29document.addEventListener('incident:reported', (e) => {
30 eventBus.emit('incident:reported', e.detail);
31});
32
33// 7. Czyszczenie przy zamykaniu (np. przy zmianie widoku w SPA)
34function destroyPanel() {
35 parkMap.destroy?.();
36 alarmSystem.destroy();
37 incidentForm.destroy();
38 activityMonitor.destroy();
39 eventBus.clear();
40 console.log('Panel kontrolny zamknięty - wszystkie zasoby zwolnione');
41}Pamiętaj o kluczowych zasadach:
event.stopPropagation() gdy kliknięcie nie powinno "bąbelkować" wyżejevent.preventDefault() dla formularzy i skrótów klawiszowychdestroy() w każdym module do czyszczenia pamięci"Systemy Parku Jurajskiego nie wybaczają błędów," mówi Ray Arnold, zapalając kolejnego papierosa. "Wyciek pamięci w monitoringu zagród oznacza, że po kilku godzinach system się zawiesi. Nieusunięty event listener może blokować garbage collector i zużywać coraz więcej RAM-u. A źle obsłużona propagacja wydarzeń może sprawić, że kliknięcie przycisku ewakuacji otworzy bramę zagrody z T-Rexem zamiast ją zamknąć."
"Zbuduj ten panel tak, jakby od niego zależało życie ludzi w parku. Bo w pewnym sensie - tak właśnie jest."
Otwórz edytor i stwórz swój interaktywny panel kontrolny!