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

Usuwanie event listenerów i czyszczenie pamięci

4 min czytania

Czasami musimy usunąć event listenery, żeby nie zajmowały pamięci lub żeby przestały reagować na wydarzenia. To jak wyłączanie niepotrzebnych systemów alarmowych w parku.

"Najgorszy alarm to taki, który pilnuje pustej zagrody," mówi Ray Arnold. "Zagrodę rozebrano miesiąc temu, a czujnik wciąż ciągnie prąd i od czasu do czasu budzi całą ochronę." W aplikacji wygląda to podobnie: listener, którego nikt nie zdjął, dalej reaguje na wydarzenia i trzyma w pamięci wszystko, czego dotyka.

Właściwe zarządzanie event listenerami

Do usunięcia listenera służy removeEventListener(). Przyjmuje te same argumenty co addEventListener(): typ wydarzenia i funkcję obsługi.

1// Funkcja do obsługi kliknięć
2function handleEmergencyClick() {
3  console.log('Aktywacja protokołu awaryjnego!');
4  evacuatePark();
5}
6
7const emergencyButton = document.getElementById('emergency-btn');
8
9// Dodanie event listenera
10emergencyButton.addEventListener('click', handleEmergencyClick);
11
12// Usunięcie event listenera (musi być ta sama funkcja!)
13emergencyButton.removeEventListener('click', handleEmergencyClick);

Przycisk alarmowy jest z powrotem głuchy, bo przeglądarka znalazła na liście dokładnie tę funkcję, którą wcześniej podpięliśmy. Tak wygląda pełny cykl życia listenera: dodanie, wywołanie wydarzenia, wykonanie funkcji i na końcu usunięcie. Sam przycisk nigdzie nie znika, znika tylko jego czujnik.

Pułapka: funkcja anonimowa

Najczęstszy błąd przy sprzątaniu wygląda niewinnie. Dwie identyczne funkcje strzałkowe wydają się tym samym, ale dla JavaScriptu są dwoma różnymi obiektami.

1// To NIE usunie listenera - dwie różne funkcje o tym samym kodzie
2emergencyButton.addEventListener('click', () => evacuatePark());
3emergencyButton.removeEventListener('click', () => evacuatePark());

Listener zostaje na miejscu, bo JavaScript porównuje referencje do funkcji, a nie ich kod źródłowy. Jeśli planujesz usunięcie, zapisz funkcję pod nazwą albo w zmiennej. Pamiętaj też o fladze fazy przechwytywania: listener dodany z true usuniesz tylko wywołaniem, które także przekazuje true.

Listener czasowy

Czasem czujnik ma działać tylko przez określony czas, na przykład podczas próbnego alarmu. Wtedy zdejmujemy go po upływie limitu w setTimeout().

1// Przykład z czasowym event listenerem
2function temporaryEventHandler() {
3  console.log('Ten event listener będzie aktywny tylko przez 10 sekund');
4}
5
6const tempButton = document.getElementById('temp-button');
7tempButton.addEventListener('click', temporaryEventHandler);
8
9// Automatyczne usunięcie po 10 sekundach
10setTimeout(() => {
11  tempButton.removeEventListener('click', temporaryEventHandler);
12  console.log('Event listener został usunięty');
13}, 10000);

Po dziesięciu sekundach kliknięcia w przycisk nie robią już nic, a przycisk dalej stoi na stronie. Gdy listener ma zadziałać tylko raz, prościej jest użyć opcji { once: true } z lekcji o listenerach, bo przeglądarka zdejmie go sama po pierwszym wywołaniu.

AbortController - jeden wyłącznik dla wielu czujników

Gdy listenerów jest kilka, pilnowanie wszystkich referencji robi się męczące. Wtedy pomaga AbortController: tworzysz kontroler, przekazujesz jego signal w opcjach listenera, a jedno wywołanie abort() zdejmuje wszystko naraz. Zobaczmy to na nieskończonym przewijaniu katalogu dinozaurów.

1const scrollController = new AbortController();
2
3window.addEventListener('scroll', () => {
4  const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
5
6  if (scrollTop + clientHeight >= scrollHeight - 100) {
7    loadMoreDinosaurs();
8  }
9}, { signal: scrollController.signal });
10
11// Katalog się skończył - wyłączamy czujnik jednym ruchem
12function onCatalogEnd() {
13  scrollController.abort();
14}

Właściwość scrollTop mówi, ile przewinięto, clientHeight to wysokość widocznego okna, a scrollHeight wysokość całej strony. Gdy do końca zostaje mniej niż 100 pikseli, ładujemy kolejną partię. Zauważ, że funkcja obsługi nie ma nazwy, a mimo to da się ją usunąć, bo przeglądarka powiązała ją z sygnałem kontrolera. Ten sam signal możesz przekazać do dowolnej liczby listenerów. Zużyty kontroler już nie działa, więc do nowych listenerów tworzysz nowy.

Skąd biorą się wycieki pamięci

Wyciek pamięci to sytuacja, w której niepotrzebny obiekt nie może zostać zwolniony, bo coś wciąż na niego wskazuje. Silnik JavaScriptu sprząta tylko to, do czego nie prowadzi już żadna referencja. Przy wydarzeniach typowy scenariusz wygląda tak: listener wisi na window albo document, które żyją tak długo jak strona, a jego funkcja odwołuje się do usuniętej karty. Karta zniknęła z ekranu, ale nie z pamięci, bo listener wciąż ją trzyma. To samo robi zapomniana zmienna, która nadal wskazuje na usunięty element.

Sprzątanie w czterech krokach

Bezpieczne usuwanie elementu z czujnikiem to zawsze ta sama procedura, jak wygaszanie zagrody według checklisty.

1let raptorCard = document.getElementById('raptor-card');
2
3function handleCardClick() {
4  console.log('Karta raptora kliknięta');
5}
6
7raptorCard.addEventListener('click', handleCardClick);
8
9// Sprzątanie, gdy karta ma zniknąć
10raptorCard.removeEventListener('click', handleCardClick);
11raptorCard.remove();
12raptorCard = null;

Ta procedura to dobry nawyk: zapisz referencję do funkcji handlera, wywołaj removeEventListener() z tą samą referencją, usuń element z DOM, a na końcu wyzeruj zmienne przypisaniem null. Współczesne silniki zwalniają usunięty element razem z jego własnymi listenerami, gdy nic innego już do niego nie prowadzi, więc dla pamięci najważniejsze są zmienne i listenery na window albo document, które wciąż odwołują się do karty.

Moja kolejność wyboru narzędzi: { once: true } dla jednorazowych reakcji, AbortController, gdy komponent ma kilka listenerów, a removeEventListener(), gdy zdejmujesz jeden konkretny czujnik. W piaskownicy poniżej możesz dodawać i usuwać listenery, obserwując, co się dzieje. W lekcji o komponentach zbierzemy całe sprzątanie w jednej metodzie destroy().

Pamiętaj: każdy czujnik, który włączasz w parku, musi mieć zaplanowane wyłączenie.

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>Usuwanie Event Listeneró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            min-height: 100vh;
15        }
16
17        .container {
18            max-width: 800px;
19            margin: 0 auto;
20        }
21
22        h1 {
23            text-align: center;
24            margin-bottom: 30px;
25        }
26
27        .demo-section {
28            background: rgba(0, 0, 0, 0.7);
29            border-radius: 15px;
30            padding: 30px;
31            margin-bottom: 20px;
32            border: 2px solid #34495e;
33        }
34
35        h2 {
36            color: #3498db;
37            margin-bottom: 20px;
38        }
39
40        .button-group {
41            display: grid;
42            grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
43            gap: 15px;
44            margin: 20px 0;
45        }
46
47        button {
48            padding: 12px 20px;
49            border: none;
50            border-radius: 8px;
51            font-size: 16px;
52            font-weight: bold;
53            cursor: pointer;
54            transition: all 0.3s ease;
55        }
56
57        .btn-start {
58            background: linear-gradient(45deg, #27ae60, #2ecc71);
59            color: white;
60        }
61
62        .btn-stop {
63            background: linear-gradient(45deg, #e74c3c, #c0392b);
64            color: white;
65        }
66
67        .btn-action {
68            background: linear-gradient(45deg, #3498db, #2980b9);
69            color: white;
70        }
71
72        button:hover {
73            transform: translateY(-2px);
74            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
75        }
76
77        button:disabled {
78            background: #7f8c8d;
79            cursor: not-allowed;
80            transform: none;
81        }
82
83        .dino-box {
84            background: rgba(52, 152, 219, 0.2);
85            border: 2px solid #3498db;
86            border-radius: 10px;
87            padding: 30px;
88            text-align: center;
89            margin: 20px 0;
90            cursor: pointer;
91            transition: all 0.3s ease;
92        }
93
94        .dino-box:hover {
95            transform: scale(1.05);
96            box-shadow: 0 10px 30px rgba(52, 152, 219, 0.3);
97        }
98
99        .dino-box.inactive {
100            opacity: 0.5;
101            border-color: #7f8c8d;
102            cursor: not-allowed;
103        }
104
105        .dino-emoji {
106            font-size: 80px;
107            display: block;
108            margin-bottom: 10px;
109        }
110
111        .status {
112            display: inline-block;
113            padding: 5px 15px;
114            border-radius: 20px;
115            margin-top: 10px;
116            font-weight: bold;
117        }
118
119        .status.active {
120            background: #27ae60;
121        }
122
123        .status.inactive {
124            background: #e74c3c;
125        }
126
127        .log {
128            background: #000;
129            border-radius: 8px;
130            padding: 15px;
131            max-height: 200px;
132            overflow-y: auto;
133            font-family: 'Courier New', monospace;
134            font-size: 13px;
135            margin-top: 20px;
136        }
137
138        .log-entry {
139            padding: 3px 0;
140            border-bottom: 1px solid #333;
141        }
142
143        .log-entry.add {
144            color: #2ecc71;
145        }
146
147        .log-entry.remove {
148            color: #e74c3c;
149        }
150
151        .log-entry.event {
152            color: #3498db;
153        }
154
155        .timer-display {
156            font-size: 48px;
157            text-align: center;
158            margin: 20px 0;
159            color: #3498db;
160            font-weight: bold;
161        }
162
163        .info-box {
164            background: rgba(241, 196, 15, 0.1);
165            border: 1px solid #f1c40f;
166            border-radius: 8px;
167            padding: 15px;
168            margin: 20px 0;
169            color: #f1c40f;
170        }
171    </style>
172</head>
173<body>
174    <div class="container">
175        <h1>Usuwanie Event Listenerów</h1>
176
177        <!-- Demo 1: Podstawowe dodawanie/usuwanie -->
178        <div class="demo-section">
179            <h2>Demo 1: Klikalne pole dinozaura</h2>
180            <p>Kliknij "Start Listenera" aby aktywować pole, następnie "Stop Listenera" aby je dezaktywować.</p>
181
182            <div class="button-group">
183                <button id="start-listener" class="btn-start">Start Listenera</button>
184                <button id="stop-listener" class="btn-stop">Stop Listenera</button>
185            </div>
186
187            <div id="dino-box" class="dino-box inactive">
188                <span class="dino-emoji">◆</span>
189                <h3>T-Rex</h3>
190                <p>Kliknij mnie, jeśli jestem aktywny!</p>
191                <span class="status inactive" id="dino-status">NIEAKTYWNY</span>
192            </div>
193
194            <div class="log" id="demo1-log"></div>
195        </div>
196
197        <!-- Demo 2: Timer z automatycznym usuwaniem -->
198        <div class="demo-section">
199            <h2>Demo 2: Tymczasowy Timer</h2>
200            <p>Timer będzie aktywny tylko przez 10 sekund, potem automatycznie się wyłączy.</p>
201
202            <div class="button-group">
203                <button id="start-timer" class="btn-start">Start Timer (10s)</button>
204                <button id="stop-timer" class="btn-stop">Stop Timer</button>
205            </div>
206
207            <div class="timer-display" id="timer-display">00:00</div>
208
209            <div class="info-box" id="timer-info">
210                Naciśnij "Start Timer" aby rozpocząć odliczanie. Event listener zostanie automatycznie usunięty po 10 sekundach.
211            </div>
212
213            <div class="log" id="demo2-log"></div>
214        </div>
215
216        <!-- Demo 3: Wiele listenerów -->
217        <div class="demo-section">
218            <h2>Demo 3: Zarządzanie wieloma listenerami</h2>
219            <p>Kontroluj różne typy wydarzeń niezależnie.</p>
220
221            <div class="button-group">
222                <button id="add-click" class="btn-start">Dodaj Click</button>
223                <button id="remove-click" class="btn-stop">Usuń Click</button>
224                <button id="add-hover" class="btn-start">Dodaj Hover</button>
225                <button id="remove-hover" class="btn-stop">Usuń Hover</button>
226            </div>
227
228            <div id="multi-dino" class="dino-box">
229                <span class="dino-emoji">◆</span>
230                <h3>Triceratops</h3>
231                <p>Testuj różne eventy!</p>
232                <div>
233                    <span class="status inactive" id="click-status">Click: OFF</span>
234                    <span class="status inactive" id="hover-status">Hover: OFF</span>
235                </div>
236            </div>
237
238            <div class="log" id="demo3-log"></div>
239        </div>
240    </div>
241
242    <script>
243        console.log('System zarządzania Event Listenerami - START');
244
245        // DEMO 1: Podstawowe dodawanie/usuwanie
246        const dinoBox = document.getElementById('dino-box');
247        const dinoStatus = document.getElementById('dino-status');
248        const demo1Log = document.getElementById('demo1-log');
249
250        function logDemo1(message, type = 'event') {
251            const entry = document.createElement('div');
252            entry.className = `log-entry ${type}`;
253            entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
254            demo1Log.appendChild(entry);
255            demo1Log.scrollTop = demo1Log.scrollHeight;
256        }
257
258        // Funkcja obsługi kliknięcia - MUSI być nazwana, aby móc ją usunąć
259        function handleDinoClick() {
260            console.log('T-Rex został kliknięty!');
261            logDemo1('T-Rex został kliknięty!', 'event');
262            dinoBox.style.transform = 'scale(1.1)';
263            setTimeout(() => {
264                dinoBox.style.transform = 'scale(1)';
265            }, 200);
266        }
267
268        // Start listener
269        document.getElementById('start-listener').addEventListener('click', function() {
270            dinoBox.addEventListener('click', handleDinoClick);
271            dinoBox.classList.remove('inactive');
272            dinoStatus.textContent = 'AKTYWNY';
273            dinoStatus.className = 'status active';
274            logDemo1('Event listener DODANY', 'add');
275            console.log('Event listener dodany do T-Rex');
276        });
277
278        // Stop listener
279        document.getElementById('stop-listener').addEventListener('click', function() {
280            dinoBox.removeEventListener('click', handleDinoClick);
281            dinoBox.classList.add('inactive');
282            dinoStatus.textContent = 'NIEAKTYWNY';
283            dinoStatus.className = 'status inactive';
284            logDemo1('Event listener USUNIĘTY', 'remove');
285            console.log('Event listener usunięty z T-Rex');
286        });
287
288        // DEMO 2: Timer z automatycznym usuwaniem
289        const timerDisplay = document.getElementById('timer-display');
290        const timerInfo = document.getElementById('timer-info');
291        const demo2Log = document.getElementById('demo2-log');
292
293        let timerInterval = null;
294        let timerTimeout = null;
295        let seconds = 0;
296
297        function logDemo2(message, type = 'event') {
298            const entry = document.createElement('div');
299            entry.className = `log-entry ${type}`;
300            entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
301            demo2Log.appendChild(entry);
302            demo2Log.scrollTop = demo2Log.scrollHeight;
303        }
304
305        function updateTimer() {
306            seconds++;
307            const mins = Math.floor(seconds / 60).toString().padStart(2, '0');
308            const secs = (seconds % 60).toString().padStart(2, '0');
309            timerDisplay.textContent = `${mins}:${secs}`;
310        }
311
312        function stopTimer() {
313            if (timerInterval) {
314                clearInterval(timerInterval);
315                timerInterval = null;
316                logDemo2('Timer ZATRZYMANY', 'remove');
317                timerInfo.textContent = 'Timer został zatrzymany.';
318                timerInfo.style.borderColor = '#e74c3c';
319            }
320            if (timerTimeout) {
321                clearTimeout(timerTimeout);
322                timerTimeout = null;
323            }
324        }
325
326        document.getElementById('start-timer').addEventListener('click', function() {
327            stopTimer(); // Zatrzymaj poprzedni timer
328            seconds = 0;
329            timerDisplay.textContent = '00:00';
330
331            timerInterval = setInterval(updateTimer, 1000);
332            logDemo2('Timer URUCHOMIONY (automatyczny stop za 10s)', 'add');
333            timerInfo.textContent = 'Timer aktywny! Automatyczne wyłączenie za 10 sekund...';
334            timerInfo.style.borderColor = '#f1c40f';
335
336            // Automatyczne usunięcie po 10 sekundach
337            timerTimeout = setTimeout(() => {
338                stopTimer();
339                logDemo2('Timer automatycznie WYŁĄCZONY po 10 sekundach', 'remove');
340                timerInfo.textContent = 'Timer został automatycznie wyłączony po 10 sekundach. Event listener został usunięty!';
341                timerInfo.style.borderColor = '#27ae60';
342                console.log('Timer automatycznie zatrzymany i wyczyszczony');
343            }, 10000);
344        });
345
346        document.getElementById('stop-timer').addEventListener('click', stopTimer);
347
348        // DEMO 3: Wiele listenerów
349        const multiDino = document.getElementById('multi-dino');
350        const clickStatus = document.getElementById('click-status');
351        const hoverStatus = document.getElementById('hover-status');
352        const demo3Log = document.getElementById('demo3-log');
353
354        function logDemo3(message, type = 'event') {
355            const entry = document.createElement('div');
356            entry.className = `log-entry ${type}`;
357            entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
358            demo3Log.appendChild(entry);
359            demo3Log.scrollTop = demo3Log.scrollHeight;
360        }
361
362        // Funkcje obsługi - muszą być nazwane
363        function handleMultiClick() {
364            logDemo3('Triceratops KLIKNIĘTY!', 'event');
365            console.log('Click event');
366        }
367
368        function handleMultiMouseEnter() {
369            logDemo3('Najechano na Triceratops', 'event');
370            multiDino.style.borderColor = '#2ecc71';
371        }
372
373        function handleMultiMouseLeave() {
374            multiDino.style.borderColor = '#3498db';
375        }
376
377        // Dodawanie Click Listenera
378        document.getElementById('add-click').addEventListener('click', function() {
379            multiDino.addEventListener('click', handleMultiClick);
380            clickStatus.textContent = 'Click: ON';
381            clickStatus.className = 'status active';
382            logDemo3('Click listener DODANY', 'add');
383        });
384
385        // Usuwanie Click Listenera
386        document.getElementById('remove-click').addEventListener('click', function() {
387            multiDino.removeEventListener('click', handleMultiClick);
388            clickStatus.textContent = 'Click: OFF';
389            clickStatus.className = 'status inactive';
390            logDemo3('Click listener USUNIĘTY', 'remove');
391        });
392
393        // Dodawanie Hover Listenerów
394        document.getElementById('add-hover').addEventListener('click', function() {
395            multiDino.addEventListener('mouseenter', handleMultiMouseEnter);
396            multiDino.addEventListener('mouseleave', handleMultiMouseLeave);
397            hoverStatus.textContent = 'Hover: ON';
398            hoverStatus.className = 'status active';
399            logDemo3('Hover listeners DODANE', 'add');
400        });
401
402        // Usuwanie Hover Listenerów
403        document.getElementById('remove-hover').addEventListener('click', function() {
404            multiDino.removeEventListener('mouseenter', handleMultiMouseEnter);
405            multiDino.removeEventListener('mouseleave', handleMultiMouseLeave);
406            hoverStatus.textContent = 'Hover: OFF';
407            hoverStatus.className = 'status inactive';
408            multiDino.style.borderColor = '#3498db';
409            logDemo3('Hover listeners USUNIĘTE', 'remove');
410        });
411
412        // Cleanup przy zamknięciu strony
413        window.addEventListener('beforeunload', function() {
414            stopTimer();
415            console.log('Wszystkie event listenery wyczyszczone przed zamknięciem');
416        });
417
418        logDemo1('System gotowy do pracy', 'add');
419        logDemo2('System gotowy do pracy', 'add');
420        logDemo3('System gotowy do pracy', 'add');
421    </script>
422</body>
423</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. Co może spowodować wyciek pamięci związany z event listenerami?

  2. 2. Dlaczego ważne jest usuwanie event listenerów, gdy element jest usuwany z DOM?

To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Układanie w pionie

    Ułóż elementy użycia AbortController do usunięcia event listenera:

  • Edytor kodu

    W pliku index.html jest galeria #gallery z trzema miniaturami (atrybut data-full wskazuje dużą wersję) i ukryty lightbox #lightbox (style.css pokazuje go z klasą open). Uzupełnij luki w skrypcie: ___BLANK1___ to zdarzenie kliknięcia, a ___BLANK2___ to właściwość obiektu event wskazująca element, który kliknięto (sprawdzamy, czy to IMG). Kliknięcie zdjęcia ma otworzyć lightbox z dużą wersją tego zdjęcia i podpisem z alt, a kliknięcie w odstęp między zdjęciami nie może go otwierać.

  • Układanie w pionie

    Ułóż fazy życia event listenera:

  • Edytor kodu

    W pliku index.html jest przewijana lista #dino-list (style.css daje jej wysokość 300 px i overflow-y: auto). Skrypt ładuje po 10 dinozaurów. Uzupełnij luki: ___BLANK1___ to metoda, która dodaje nowy element li na koniec listy, a ___BLANK2___ to właściwość z wysokością całej zawartości listy (scrollTop to przewinięte piksele, clientHeight wysokość widocznego okna). Gdy do końca listy zostanie mniej niż 50 pikseli, lista ma dociągnąć kolejne 10 elementów, a wcześniej nic nie dokładać.

  • Edytor kodu

    W pliku index.html jest przycisk #menu-toggle i menu #main-menu. Na wąskim ekranie style.css pokazuje menu tylko wtedy, gdy ma klasę open (na ekranie szerszym niż 900 px menu jest zawsze widoczne, a przycisk ukryty). Uzupełnij luki: ___BLANK1___ to zdarzenie kliknięcia, a ___BLANK2___ to metoda classList, która dodaje klasę open, gdy jej nie ma, i usuwa ją, gdy jest (zwraca przy tym true albo false). Pierwsze kliknięcie ma rozwinąć menu i ustawić aria-expanded="true", drugie zwinąć je i ustawić aria-expanded="false".

  • Edytor kodu

    W pliku index.html jest pole #dino-search i lista podpowiedzi #suggestions, a skrypt ma tablicę gatunków dinozaurów. Uzupełnij luki: ___BLANK1___ to zdarzenie, które pole wysyła po każdej zmianie wpisanego tekstu, a ___BLANK2___ to metoda tekstu, która sprawdza, czy nazwa zaczyna się od wpisanego fragmentu. Po wpisaniu "tri" lista ma pokazać tylko Triceratops (wielkość liter nie ma znaczenia), puste pole i tekst bez dopasowań czyszczą listę, a kliknięcie podpowiedzi wpisuje ją do pola.

  • Edytor kodu

    W pliku index.html jest panel z akapitem #status, a skrypt ma jeden listener keydown na dokumencie. Uzupełnij luki: ___BLANK1___ to metoda obiektu event, która blokuje domyślny zapis strony przez przeglądarkę przy Ctrl+S, ___BLANK2___ to wartość event.key dla skrótu Ctrl+O (mała litera), a ___BLANK3___ to wartość event.key klawisza Esc. Ctrl+S ma zapisać, Ctrl+O otworzyć raport, Esc zamknąć panel, a samo S bez Ctrl nie może niczego zmieniać.

  • Edytor kodu

    W pliku index.html są trzy zakładki (role="tab") z panelami i listener keydown na kontenerze #dino-tabs. Strzałka w prawo przesuwa fokus na następną zakładkę, a Enter wybiera zakładkę z fokusem. Uzupełnij luki: ___BLANK1___ to wartość event.key strzałki w lewo (przesuwa fokus na poprzednią zakładkę, z pierwszej na ostatnią), a ___BLANK2___ to warunek, który jest prawdziwy po naciśnięciu spacji (sprawdź event.key albo event.code). Enter i spacja mają pokazać panel zakładki z fokusem, a strzałki i spacja nie mogą przewijać strony.

  • Układanie w poziomie

    Ułóż elementy handlera zatrzymującego propagację zdarzenia:

  • Układanie w pionie

    Ułóż kroki czyszczenia event listenerów przy usuwaniu elementu z DOM:

Przydatne artykuły