Kurs JavaScript i TypeScript · Moduł 8: Zdarzenia i DOM
Wydarzenia myszy i klawiatury
W tej lekcji5
"W systemach bezpieczeństwa Parku Jurajskiego," Ray Arnold pokazuje wielki panel kontrolny, "mamy różne typy sensorów. Czujniki ruchu reagują na ruch, czujniki ciśnienia na dotyk, czujniki dźwięku na hałas. Każdy typ wymaga innej reakcji. W JavaScript mamy podobną sytuację - różne typy wejścia wymagają różnej obsługi."
Ray wskazuje na monitor i klawiaturę. "Mysz i klawiatura to dwa podstawowe źródła interakcji użytkownika. Każde ma swoje unikalne wydarzenia i zachowania."
Wydarzenia myszy - pełny zakres interakcji
Mysz generuje kilkanaście różnych typów wydarzeń, od prostych kliknięć po zaawansowane gesty przeciągania. Jedno kliknięcie to trzy sygnały: mousedown, mouseup i dopiero na końcu click.
Podstawowe wydarzenia kliknięć
Obszar obserwacyjny rozróżnia pojedyncze i podwójne kliknięcie.
1const dinosaurViewingArea = document.getElementById('viewing-area');
2
3// click - pojedyncze kliknięcie (najpopularniejsze)
4dinosaurViewingArea.addEventListener('click', (event) => {
5 console.log('Pojedyncze kliknięcie w obszar obserwacyjny');
6 console.log('Pozycja:', event.clientX, event.clientY);
7
8 selectDinosaurAtPosition(event.clientX, event.clientY);
9});
10
11// dblclick - podwójne kliknięcie
12dinosaurViewingArea.addEventListener('dblclick', (event) => {
13 console.log('Podwójne kliknięcie - zbliżenie');
14
15 // Uwaga: to NIE blokuje zaznaczania tekstu, które startuje już przy mousedown
16 event.preventDefault();
17
18 zoomInOnDinosaur(event.clientX, event.clientY);
19});Pola clientX i clientY to pozycja kursora w oknie. Uwaga: preventDefault() w dblclick nie zablokuje zaznaczania tekstu, bo startuje ono już przy mousedown. Do tego służy reguła CSS user-select: none.
1// auxclick - kliknięcie środkowym/innym przyciskiem (nie lewy)
2dinosaurViewingArea.addEventListener('auxclick', (event) => {
3 if (event.button === 1) {
4 console.log('Środkowy przycisk - otwieranie w nowym oknie');
5 openInNewWindow(event.target);
6 }
7});
8
9// contextmenu - prawy przycisk myszy (menu kontekstowe)
10dinosaurViewingArea.addEventListener('contextmenu', (event) => {
11 event.preventDefault(); // Zablokuj domyślne menu
12
13 console.log('Prawy przycisk - własne menu kontekstowe');
14 showCustomContextMenu(event.clientX, event.clientY);
15});
16
17function selectDinosaurAtPosition(x, y) {
18 console.log(`Sprawdzanie czy dinozaur znajduje się na pozycji (${x}, ${y})`);
19 // Logika selekcji
20}
21
22function zoomInOnDinosaur(x, y) {
23 console.log(`Zbliżenie na pozycję (${x}, ${y})`);
24 // Logika zoom
25}
26
27function showCustomContextMenu(x, y) {
28 const menu = document.getElementById('custom-menu');
29 menu.style.left = x + 'px';
30 menu.style.top = y + 'px';
31 menu.style.display = 'block';
32
33 console.log('Menu kontekstowe wyświetlone');
34}Wartość event.button równa 1 to środkowy przycisk, a preventDefault() przy contextmenu chowa menu przeglądarki, robiąc miejsce dla własnego.
Wydarzenia przycisków myszy
Między wciśnięciem a puszczeniem przycisku trwa przeciąganie, więc zapamiętujemy punkt startu.
1const controlPanel = document.getElementById('control-panel');
2
3// mousedown - naciśnięcie przycisku myszy (przed puszczeniem)
4controlPanel.addEventListener('mousedown', (event) => {
5 console.log('Przycisk myszy wciśnięty');
6 console.log('Który przycisk:', event.button); // 0=lewy, 1=środek, 2=prawy
7
8 if (event.button === 0) {
9 console.log('Lewy przycisk - rozpoczynam przeciąganie');
10 startDragging(event);
11 }
12});
13
14// mouseup - puszczenie przycisku myszy
15controlPanel.addEventListener('mouseup', (event) => {
16 console.log('Przycisk myszy puszczony');
17
18 if (isDragging) {
19 console.log('Kończę przeciąganie');
20 stopDragging(event);
21 }
22});
23
24let isDragging = false;
25let dragStartX = 0;
26let dragStartY = 0;
27
28function startDragging(event) {
29 isDragging = true;
30 dragStartX = event.clientX;
31 dragStartY = event.clientY;
32
33 console.log(`Rozpoczęcie przeciągania z pozycji (${dragStartX}, ${dragStartY})`);
34}
35
36function stopDragging(event) {
37 const deltaX = event.clientX - dragStartX;
38 const deltaY = event.clientY - dragStartY;
39
40 console.log(`Przeciągnięto o: X=${deltaX}px, Y=${deltaY}px`);
41
42 isDragging = false;
43}Flaga isDragging łączy oba wydarzenia w jedną historię, a różnica współrzędnych mówi, jak daleko przesunięto kursor.
Wydarzenia ruchu myszy
Ruch kursora nad mapą parku śledzi mousemove, czujnik odzywający się przy każdym drgnięciu myszy.
1const parkMap = document.getElementById('interactive-park-map');
2
3// mousemove - ruch myszy (UWAGA: bardzo częste!)
4// Używaj z rozwagą - może generować setki eventów na sekundę
5
6let mouseMoveCount = 0;
7
8parkMap.addEventListener('mousemove', (event) => {
9 mouseMoveCount++;
10
11 // Aktualizuj pozycję kursora
12 updateCursorPosition(event.clientX, event.clientY);
13
14 // Jeśli przeciągamy, aktualizuj pozycję
15 if (isDragging) {
16 updateDragPosition(event.clientX, event.clientY);
17 }
18
19 // Log co 10-te wydarzenie (żeby nie zaśmiecać konsoli)
20 if (mouseMoveCount % 10 === 0) {
21 console.log(`Ruch myszy #${mouseMoveCount}: (${event.clientX}, ${event.clientY})`);
22 }
23});
24
25function updateCursorPosition(x, y) {
26 const coordsDisplay = document.getElementById('mouse-coords');
27 if (coordsDisplay) {
28 coordsDisplay.textContent = `X: ${x}, Y: ${y}`;
29 }
30}
31
32function updateDragPosition(x, y) {
33 const draggableElement = document.getElementById('draggable');
34 if (draggableElement) {
35 draggableElement.style.left = x + 'px';
36 draggableElement.style.top = y + 'px';
37 }
38}Dlatego log trafia do konsoli co dziesiąte wydarzenie, a lekcja o throttlingu pokaże, jak ograniczać takie lawiny. Teraz wejście i wyjście kursora.
1// mouseenter - mysz wchodzi w obszar elementu (nie bąbelkuje!)
2parkMap.addEventListener('mouseenter', (event) => {
3 console.log('Mysz weszła na mapę parku');
4 parkMap.classList.add('mouse-active');
5 showMapControls();
6});
7
8// mouseleave - mysz opuszcza obszar elementu (nie bąbelkuje!)
9parkMap.addEventListener('mouseleave', (event) => {
10 console.log('Mysz opuściła mapę parku');
11 parkMap.classList.remove('mouse-active');
12 hideMapControls();
13});
14
15// mouseover - mysz wchodzi (bąbelkuje - uruchamia się też dla dzieci!)
16parkMap.addEventListener('mouseover', (event) => {
17 if (event.target.classList.contains('enclosure-marker')) {
18 const enclosureName = event.target.dataset.name;
19 console.log(`Najechano na marker zagrody: ${enclosureName}`);
20 showEnclosureTooltip(event.target, enclosureName);
21 }
22});
23
24// mouseout - mysz wychodzi (bąbelkuje)
25parkMap.addEventListener('mouseout', (event) => {
26 if (event.target.classList.contains('enclosure-marker')) {
27 console.log('Opuszczono marker zagrody');
28 hideEnclosureTooltip();
29 }
30});
31
32function showMapControls() {
33 const controls = document.getElementById('map-controls');
34 if (controls) {
35 controls.style.display = 'block';
36 }
37}
38
39function hideMapControls() {
40 const controls = document.getElementById('map-controls');
41 if (controls) {
42 controls.style.display = 'none';
43 }
44}Para mouseenter i mouseleave nie bąbelkuje, więc reaguje tylko na samą mapę, a mouseover i mouseout bąbelkują z każdego dziecka, stąd sprawdzanie event.target.
Praktyczny przykład - system przeciągania
"Jedną z najbardziej użytecznych funkcji," mówi Ray, "jest drag & drop." Pierwsza część klasy zaczyna przeciąganie.
1class DinosaurCardDragSystem {
2 constructor() {
3 this.draggedElement = null;
4 this.dragOffsetX = 0;
5 this.dragOffsetY = 0;
6 this.initialize();
7 }
8
9 initialize() {
10 const cards = document.querySelectorAll('.dinosaur-card');
11
12 cards.forEach(card => {
13 card.addEventListener('mousedown', (e) => this.handleDragStart(e));
14 });
15
16 document.addEventListener('mousemove', (e) => this.handleDragMove(e));
17 document.addEventListener('mouseup', (e) => this.handleDragEnd(e));
18
19 console.log('System drag & drop zainicjalizowany');
20 }
21
22 handleDragStart(event) {
23 this.draggedElement = event.currentTarget;
24
25 const rect = this.draggedElement.getBoundingClientRect();
26 this.dragOffsetX = event.clientX - rect.left;
27 this.dragOffsetY = event.clientY - rect.top;
28
29 this.draggedElement.style.position = 'fixed';
30 this.draggedElement.style.zIndex = '1000';
31 this.draggedElement.classList.add('dragging');
32
33 const dinoName = this.draggedElement.dataset.dinoName;
34 console.log(`Rozpoczęto przeciąganie karty: ${dinoName}`);
35
36 event.preventDefault(); // Prevent text selection
37 }Listenery mousemove i mouseup wiszą na dokumencie, bo szybki ruch wyprowadzi kursor poza kartę. Tu preventDefault() w mousedown naprawdę blokuje zaznaczanie tekstu, a getBoundingClientRect() pozwala wyliczyć punkt chwytu.
1 handleDragMove(event) {
2 if (!this.draggedElement) return;
3
4 const x = event.clientX - this.dragOffsetX;
5 const y = event.clientY - this.dragOffsetY;
6
7 this.draggedElement.style.left = x + 'px';
8 this.draggedElement.style.top = y + 'px';
9 }
10
11 handleDragEnd(event) {
12 if (!this.draggedElement) return;
13
14 const dinoName = this.draggedElement.dataset.dinoName;
15 console.log(`Zakończono przeciąganie karty: ${dinoName}`);
16
17 // Sprawdź czy karta została upuszczona w odpowiedniej strefie
18 const dropZone = this.findDropZone(event.clientX, event.clientY);
19
20 if (dropZone) {
21 console.log('Karta upuszczona w strefie:', dropZone.id);
22 this.handleDrop(this.draggedElement, dropZone);
23 } else {
24 console.log('Karta upuszczona poza strefą - powrót do pozycji początkowej');
25 this.returnToOriginalPosition();
26 }
27
28 this.draggedElement.classList.remove('dragging');
29 this.draggedElement = null;
30 }Karta podąża za kursorem, a po puszczeniu przycisku klasa szuka strefy, w której wylądowała.
1 findDropZone(x, y) {
2 const dropZones = document.querySelectorAll('.drop-zone');
3
4 for (const zone of dropZones) {
5 const rect = zone.getBoundingClientRect();
6
7 if (x >= rect.left && x <= rect.right &&
8 y >= rect.top && y <= rect.bottom) {
9 return zone;
10 }
11 }
12
13 return null;
14 }
15
16 handleDrop(card, dropZone) {
17 console.log('Dodawanie karty do strefy drop');
18 dropZone.appendChild(card);
19
20 // Reset stylów
21 card.style.position = '';
22 card.style.left = '';
23 card.style.top = '';
24 card.style.zIndex = '';
25 }
26
27 returnToOriginalPosition() {
28 // Powrót na miejsce - bez animacji, bo zmiany position nie da się animować.
29 // Bez setTimeout: handleDragEnd zaraz potem ustawia this.draggedElement na null
30 this.draggedElement.style.position = '';
31 this.draggedElement.style.left = '';
32 this.draggedElement.style.top = '';
33 this.draggedElement.style.zIndex = '';
34 }
35}
36
37// Inicjalizacja
38const dragSystem = new DinosaurCardDragSystem();Karta upuszczona w strefie przenosi się do nowej zagrody, a poza nią wraca na miejsce.
Przeciąganie wbudowane w przeglądarkę
Przeglądarka ma też własne API Drag and Drop. Element z atrybutem draggable="true" wysyła dragstart, a strefa docelowa dostaje dragover i drop.
1// HTML: <div class="dino-item" draggable="true">T-Rex</div>
2const dino = document.querySelector('.dino-item');
3const zone = document.getElementById('zone-c');
4
5dino.addEventListener('dragstart', (e) => {
6 e.dataTransfer.setData('text/plain', dino.textContent);
7 dino.classList.add('dragging');
8});
9dino.addEventListener('dragend', () => dino.classList.remove('dragging'));
10
11zone.addEventListener('dragover', (e) => {
12 e.preventDefault(); // bez tego strefa nie przyjmie upuszczenia
13});
14
15zone.addEventListener('drop', (e) => {
16 e.preventDefault(); // blokuje domyślną akcję, np. otwarcie linku
17 zone.appendChild(dino);
18});Kluczowy jest preventDefault() w dragover, bo bez niego upuszczenie jest zabronione, a dragend sprząta po każdym przeciąganiu.
Wydarzenia klawiatury - kontrola z klawiatury
"Klawiatura," Ray wskazuje na panel kontrolny, "to drugi kluczowy sposób interakcji. Skróty klawiszowe, nawigacja, wprowadzanie danych."
Podstawowe wydarzenia klawiatury
Każde naciśnięcie zostawia w obiekcie wydarzenia pełny raport.
1// keydown - klawisz naciśnięty (powtarza się gdy trzymany)
2document.addEventListener('keydown', (event) => {
3 console.log('=== KEYDOWN ===');
4 console.log('Klawisz:', event.key); // 'a', 'Enter', 'ArrowUp', etc.
5 console.log('Kod:', event.code); // 'KeyA', 'Enter', 'ArrowUp', etc.
6 console.log('KeyCode (deprecated):', event.keyCode);
7 console.log('Czy to powtórzenie:', event.repeat);
8 console.log('Ctrl:', event.ctrlKey);
9 console.log('Shift:', event.shiftKey);
10 console.log('Alt:', event.altKey);
11 console.log('Meta (Cmd/Win):', event.metaKey);
12});
13
14// keyup - klawisz puszczony
15document.addEventListener('keyup', (event) => {
16 console.log(`Puszczono klawisz: ${event.key}`);
17});
18
19// keypress - DEPRECATED! Nie używaj (użyj keydown zamiast tego)
20// keypress jest przestarzały i nie odpala np. dla strzałek ani Escapeevent.key to znak lub nazwa klawisza, a event.code to fizyczny klawisz, niezależny od układu klawiatury. Trzymany klawisz wysyła kolejne keydown z event.repeat równym true, a keyup przychodzi raz.
Praktyczne skróty klawiszowe
Skróty obsługuje jeden listener na dokumencie. Najpierw zapis, wyszukiwanie i Escape.
1// System skrótów klawiszowych dla parku
2document.addEventListener('keydown', (event) => {
3 // Ctrl/Cmd + S - Zapisz
4 if ((event.ctrlKey || event.metaKey) && event.key === 's') {
5 event.preventDefault();
6 console.log('Zapisywanie konfiguracji parku...');
7 saveParkConfiguration();
8 return;
9 }
10
11 // Ctrl/Cmd + F - Szukaj
12 if ((event.ctrlKey || event.metaKey) && event.key === 'f') {
13 event.preventDefault();
14 console.log('Otwieranie wyszukiwarki dinozaurów');
15 openSearchDialog();
16 return;
17 }
18
19 // Escape - Zamknij wszystkie okna
20 if (event.key === 'Escape') {
21 console.log('Zamykanie wszystkich okien dialogowych');
22 closeAllModals();
23 return;
24 }Sprawdzamy ctrlKey albo metaKey, żeby skrót działał też z Cmd na Macu, a preventDefault() zatrzymuje zapis strony przez przeglądarkę. Dalej strzałki, spacja, F1 i cyfry.
1 // Strzałki - Nawigacja po mapie
2 const arrowKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
3 if (arrowKeys.includes(event.key)) {
4 event.preventDefault();
5
6 const direction = {
7 'ArrowUp': 'north',
8 'ArrowDown': 'south',
9 'ArrowLeft': 'west',
10 'ArrowRight': 'east'
11 }[event.key];
12
13 console.log(`Przesunięcie mapy w kierunku: ${direction}`);
14 moveMap(direction);
15 return;
16 }
17
18 // Spacja - Pauza/Wznów monitoring
19 if (event.key === ' ') {
20 event.preventDefault();
21 console.log('Toggle pauzy monitoringu');
22 toggleMonitoringPause();
23 return;
24 }
25
26 // F1 - Pomoc
27 if (event.key === 'F1') {
28 event.preventDefault();
29 console.log('Otwieranie pomocy');
30 openHelp();
31 return;
32 }
33
34 // Cyfry 1-9 - Szybki dostęp do zagród
35 if (event.key >= '1' && event.key <= '9') {
36 const enclosureNumber = parseInt(event.key);
37 console.log(`Przełączanie na zagrodę #${enclosureNumber}`);
38 jumpToEnclosure(enclosureNumber);
39 return;
40 }
41});Spacja ma nazwę ' ', a mapa kierunków zamienia klawisz na stronę świata. Dalej proste funkcje wykonawcze.
1function saveParkConfiguration() {
2 console.log('Konfiguracja zapisana');
3}
4
5function openSearchDialog() {
6 console.log('Dialog wyszukiwania otwarty');
7}
8
9function closeAllModals() {
10 document.querySelectorAll('.modal').forEach(modal => {
11 modal.style.display = 'none';
12 });
13 console.log('Wszystkie okna zamknięte');
14}
15
16function moveMap(direction) {
17 console.log(`Mapa przesunięta w kierunku: ${direction}`);
18}
19
20function toggleMonitoringPause() {
21 console.log('Monitoring wstrzymany/wznowiony');
22}
23
24function openHelp() {
25 console.log('Pomoc otwarta');
26}
27
28function jumpToEnclosure(number) {
29 console.log(`Przejście do zagrody #${number}`);
30}Listener rozpoznaje skrót, a logika żyje w funkcjach.
Walidacja wejścia z klawiatury
Listener keydown może odrzucić znak, zanim trafi do pola: kod bezpieczeństwa przyjmuje tylko cyfry, telefon także spacje, nawiasy i myślniki, a imię dostaje wielką literę.
1const securityCodeInput = document.getElementById('security-code');
2const phoneInput = document.getElementById('phone-number');
3const nameInput = document.getElementById('visitor-name');
4
5// Tylko cyfry - kod bezpieczeństwa
6securityCodeInput.addEventListener('keydown', (event) => {
7 // Zezwól na klawisze funkcyjne
8 const allowedKeys = [
9 'Backspace', 'Delete', 'Tab', 'Escape', 'Enter',
10 'ArrowLeft', 'ArrowRight', 'Home', 'End'
11 ];
12
13 if (allowedKeys.includes(event.key)) {
14 return; // Pozwól
15 }
16
17 // Tylko cyfry
18 if (event.key >= '0' && event.key <= '9') {
19 // Maksymalnie 6 cyfr
20 if (securityCodeInput.value.length >= 6 && !event.ctrlKey && !event.metaKey) {
21 event.preventDefault();
22 console.log('Maksymalnie 6 cyfr');
23 }
24 return;
25 }
26
27 // Wszystko inne - blokuj
28 event.preventDefault();
29 console.log('Tylko cyfry są dozwolone');
30});
31
32// Formatowanie numeru telefonu
33phoneInput.addEventListener('keydown', (event) => {
34 const allowedKeys = [
35 'Backspace', 'Delete', 'Tab', 'Escape', 'Enter',
36 'ArrowLeft', 'ArrowRight', 'Home', 'End'
37 ];
38
39 if (allowedKeys.includes(event.key)) return;
40
41 // Tylko cyfry, spacje, myślniki, nawiasy
42 const phoneChars = /^[0-9 ()-]$/;
43
44 if (!phoneChars.test(event.key)) {
45 event.preventDefault();
46 console.log('Niedozwolony znak w numerze telefonu');
47 }
48});
49
50// Auto-kapitalizacja pierwszej litery
51nameInput.addEventListener('keyup', (event) => {
52 const value = nameInput.value;
53
54 if (value.length === 1) {
55 nameInput.value = value.toUpperCase();
56 }
57});Taki filtr to tylko wygoda, bo tekst da się wkleić z menu kontekstowego, więc wartość sprawdzaj też przy input lub submit.
Kombinacja myszy i klawiatury
"Najlepsze interfejsy," mówi Ray, "łączą oba typy wejścia." Klawisze modyfikujące zmieniają znaczenie kliknięcia.
1const enclosureCards = document.querySelectorAll('.enclosure-card');
2let selectedCards = new Set();
3
4enclosureCards.forEach(card => {
5 card.addEventListener('click', (event) => {
6 const enclosureId = card.dataset.enclosureId;
7
8 // Różne zachowania w zależności od klawiszy modyfikujących
9 if (event.ctrlKey || event.metaKey) {
10 // Ctrl + Click = Toggle selekcji
11 console.log('Ctrl+Click: Toggle selekcji');
12
13 if (selectedCards.has(enclosureId)) {
14 selectedCards.delete(enclosureId);
15 card.classList.remove('selected');
16 } else {
17 selectedCards.add(enclosureId);
18 card.classList.add('selected');
19 }
20 } else if (event.shiftKey) {
21 // Shift + Click = Wybór zakresu
22 console.log('Shift+Click: Wybór zakresu');
23 selectRange(lastSelected, enclosureId);
24 } else {
25 // Zwykłe kliknięcie = Tylko ta jedna karta
26 console.log('Zwykłe kliknięcie: Pojedyncza selekcja');
27
28 selectedCards.clear();
29 document.querySelectorAll('.enclosure-card.selected').forEach(c => {
30 c.classList.remove('selected');
31 });
32
33 selectedCards.add(enclosureId);
34 card.classList.add('selected');
35 }
36
37 lastSelected = enclosureId;
38 updateSelectionInfo();
39 });
40});
41
42let lastSelected = null;
43
44function selectRange(startId, endId) {
45 console.log(`Wybieranie zakresu od ${startId} do ${endId}`);
46 // Logika wyboru zakresu
47}
48
49function updateSelectionInfo() {
50 console.log(`Wybrano ${selectedCards.size} zagród`);
51}Ctrl lub Cmd przełącza zaznaczenie, Shift wybiera zakres, zwykłe kliknięcie zostawia jedną kartę. Dalej strzałki.
1// Nawigacja klawiaturą przez karty
2let focusedCardIndex = 0;
3const cards = Array.from(enclosureCards);
4
5document.addEventListener('keydown', (event) => {
6 // Tylko gdy żadne pole input nie jest aktywne
7 if (document.activeElement.tagName === 'INPUT') return;
8
9 switch(event.key) {
10 case 'ArrowRight':
11 case 'ArrowDown':
12 event.preventDefault();
13 focusedCardIndex = Math.min(focusedCardIndex + 1, cards.length - 1);
14 focusCard(focusedCardIndex);
15 break;
16
17 case 'ArrowLeft':
18 case 'ArrowUp':
19 event.preventDefault();
20 focusedCardIndex = Math.max(focusedCardIndex - 1, 0);
21 focusCard(focusedCardIndex);
22 break;
23
24 case 'Enter':
25 case ' ':
26 event.preventDefault();
27 cards[focusedCardIndex].click();
28 break;
29 }
30});
31
32function focusCard(index) {
33 cards.forEach(card => card.classList.remove('focused'));
34 cards[index].classList.add('focused');
35 cards[index].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
36
37 console.log(`Fokus na karcie #${index}: ${cards[index].dataset.enclosureName}`);
38}Warunek z activeElement nie przeszkadza w pisaniu w polach, a click() na karcie uruchamia ten sam listener co mysz.
Wydarzenia okna i dotyku
DOMContentLoaded odpala, gdy HTML jest wczytany, choć obrazy mogą się jeszcze ładować. load na window czeka na całą stronę z zasobami. Przewijanie zgłasza scroll, a window.scrollY podaje przewinięte piksele, więc większa wartość niż poprzednio oznacza ruch w dół. Na ekranach dotykowych palec wysyła po kolei touchstart, touchmove i touchend, do których wrócimy przy gestach.
Podsumowanie
"Wydarzenia myszy i klawiatury," podsumowuje Ray, "to fundament interaktywności. Podobnie jak różne sensory w Parku Jurajskim wykrywają różne typy zagrożeń, tak różne wydarzenia pozwalają na różnorodne interakcje użytkownika. Kluczem jest używanie odpowiednich wydarzeń do odpowiednich zadań."
Kluczowe wydarzenia myszy:
click,dblclick,contextmenu- kliknięciamousedown,mouseup- kontrola przyciskówmousemove- śledzenie pozycji (używaj ostrożnie!)mouseenter,mouseleave- najechanie (nie bąbelkują)mouseover,mouseout- najechanie (bąbelkują)
Kluczowe wydarzenia klawiatury:
keydown- naciśnięcie klawisza (użyj tego)keyup- puszczenie klawiszaevent.key- znak/nazwa klawisza ('a', 'Enter', 'ArrowUp')event.code- fizyczny klawisz ('KeyA', 'Enter', 'ArrowUp')- Klawisze modyfikujące:
ctrlKey,shiftKey,altKey,metaKey
Moja rada: skróty opieraj na keydown, a przenoszenie między strefami na wbudowanym Drag and Drop. Następna lekcja: formularze.
Pamiętaj: każdy czujnik w parku reaguje na inny sygnał, więc dobieraj wydarzenie do zadania.
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>Wydarzenia myszy i klawiatury - Sterowanie kamerą</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: 1000px;
18 margin: 0 auto;
19 }
20
21 h1 {
22 text-align: center;
23 margin-bottom: 20px;
24 }
25
26 .instructions {
27 background: rgba(52, 152, 219, 0.2);
28 border: 1px solid #3498db;
29 border-radius: 10px;
30 padding: 15px;
31 margin-bottom: 20px;
32 text-align: center;
33 }
34
35 .instructions kbd {
36 background: rgba(255, 255, 255, 0.2);
37 padding: 2px 8px;
38 border-radius: 4px;
39 font-family: monospace;
40 margin: 0 3px;
41 }
42
43 .camera-viewport {
44 position: relative;
45 width: 100%;
46 height: 400px;
47 background: linear-gradient(180deg, #34495e 0%, #2c3e50 100%);
48 border-radius: 15px;
49 overflow: hidden;
50 cursor: move;
51 margin-bottom: 20px;
52 border: 3px solid #7f8c8d;
53 }
54
55 .camera-content {
56 position: absolute;
57 width: 200%;
58 height: 200%;
59 transition: transform 0.3s ease;
60 }
61
62 .dinosaur {
63 position: absolute;
64 font-size: 60px;
65 cursor: pointer;
66 transition: transform 0.2s ease;
67 user-select: none;
68 }
69
70 .dinosaur:hover {
71 transform: scale(1.2);
72 filter: drop-shadow(0 0 10px rgba(255, 255, 255, 0.5));
73 }
74
75 .dinosaur.clicked {
76 animation: bounce 0.5s ease;
77 }
78
79 @keyframes bounce {
80 0%, 100% { transform: scale(1); }
81 50% { transform: scale(1.3); }
82 }
83
84 .controls-panel {
85 display: grid;
86 grid-template-columns: 1fr 1fr;
87 gap: 20px;
88 margin-bottom: 20px;
89 }
90
91 .control-box {
92 background: rgba(0, 0, 0, 0.6);
93 border-radius: 10px;
94 padding: 20px;
95 border: 1px solid #34495e;
96 }
97
98 .control-box h3 {
99 margin-top: 0;
100 color: #3498db;
101 }
102
103 .stats {
104 display: grid;
105 grid-template-columns: repeat(2, 1fr);
106 gap: 10px;
107 }
108
109 .stat-item {
110 background: rgba(255, 255, 255, 0.1);
111 padding: 10px;
112 border-radius: 5px;
113 }
114
115 .stat-label {
116 color: #bdc3c7;
117 font-size: 12px;
118 }
119
120 .stat-value {
121 color: #ecf0f1;
122 font-size: 18px;
123 font-weight: bold;
124 }
125
126 .arrow-buttons {
127 display: grid;
128 grid-template-areas:
129 ". up ."
130 "left . right"
131 ". down .";
132 gap: 5px;
133 justify-content: center;
134 align-items: center;
135 }
136
137 .arrow-btn {
138 width: 60px;
139 height: 60px;
140 background: linear-gradient(45deg, #3498db, #2980b9);
141 border: none;
142 border-radius: 10px;
143 color: white;
144 font-size: 24px;
145 cursor: pointer;
146 transition: all 0.2s ease;
147 }
148
149 .arrow-btn:hover {
150 background: linear-gradient(45deg, #2980b9, #21618c);
151 transform: scale(1.05);
152 }
153
154 .arrow-btn:active {
155 transform: scale(0.95);
156 }
157
158 .arrow-btn.up { grid-area: up; }
159 .arrow-btn.down { grid-area: down; }
160 .arrow-btn.left { grid-area: left; }
161 .arrow-btn.right { grid-area: right; }
162
163 .event-log {
164 background: rgba(0, 0, 0, 0.8);
165 border-radius: 10px;
166 padding: 15px;
167 max-height: 200px;
168 overflow-y: auto;
169 font-family: 'Courier New', monospace;
170 font-size: 12px;
171 }
172
173 .log-entry {
174 padding: 5px;
175 border-bottom: 1px solid rgba(255, 255, 255, 0.1);
176 color: #bdc3c7;
177 }
178
179 .log-entry.mouse {
180 color: #3498db;
181 }
182
183 .log-entry.keyboard {
184 color: #e74c3c;
185 }
186
187 .log-entry.click {
188 color: #f39c12;
189 }
190
191 @media (max-width: 768px) {
192 .controls-panel {
193 grid-template-columns: 1fr;
194 }
195 }
196 </style>
197</head>
198<body>
199 <div class="container">
200 <h1>Sterowanie Kamerą w Parku Jurajskim</h1>
201
202 <div class="instructions">
203 <strong>Sterowanie:</strong> Strzałki <kbd>↑</kbd> <kbd>↓</kbd> <kbd>←</kbd> <kbd>→</kbd> |
204 <strong>Pauza:</strong> <kbd>Space</kbd> |
205 <strong>Zoom:</strong> Podwójne kliknięcie |
206 <strong>Zamknij:</strong> <kbd>Escape</kbd>
207 </div>
208
209 <div class="camera-viewport" id="viewport">
210 <div class="camera-content" id="camera">
211 <div class="dinosaur" style="top: 50px; left: 100px;" data-name="T-Rex">T</div>
212 <div class="dinosaur" style="top: 150px; left: 400px;" data-name="Velociraptor">V</div>
213 <div class="dinosaur" style="top: 300px; left: 200px;" data-name="Triceratops">Tr</div>
214 <div class="dinosaur" style="top: 100px; left: 600px;" data-name="Brachiosaurus">B</div>
215 <div class="dinosaur" style="top: 250px; left: 700px;" data-name="Stegosaurus">S</div>
216 </div>
217 </div>
218
219 <div class="controls-panel">
220 <div class="control-box">
221 <h3>Statystyki</h3>
222 <div class="stats">
223 <div class="stat-item">
224 <div class="stat-label">Pozycja myszy</div>
225 <div class="stat-value" id="mouse-pos">0, 0</div>
226 </div>
227 <div class="stat-item">
228 <div class="stat-label">Kliknięcia</div>
229 <div class="stat-value" id="click-count">0</div>
230 </div>
231 <div class="stat-item">
232 <div class="stat-label">Pozycja kamery</div>
233 <div class="stat-value" id="camera-pos">0, 0</div>
234 </div>
235 <div class="stat-item">
236 <div class="stat-label">Ostatni klawisz</div>
237 <div class="stat-value" id="last-key">-</div>
238 </div>
239 </div>
240 </div>
241
242 <div class="control-box">
243 <h3>Sterowanie kamerą</h3>
244 <div class="arrow-buttons">
245 <button class="arrow-btn up" data-direction="up">↑</button>
246 <button class="arrow-btn left" data-direction="left">←</button>
247 <button class="arrow-btn right" data-direction="right">→</button>
248 <button class="arrow-btn down" data-direction="down">↓</button>
249 </div>
250 </div>
251 </div>
252
253 <div class="control-box">
254 <h3>Log wydarzeń</h3>
255 <div class="event-log" id="event-log"></div>
256 </div>
257 </div>
258
259 <script>
260 // Referencje do elementów
261 const camera = document.getElementById('camera');
262 const viewport = document.getElementById('viewport');
263 const mousePosDisplay = document.getElementById('mouse-pos');
264 const clickCountDisplay = document.getElementById('click-count');
265 const cameraPosDisplay = document.getElementById('camera-pos');
266 const lastKeyDisplay = document.getElementById('last-key');
267 const eventLog = document.getElementById('event-log');
268
269 // Stan kamery
270 let cameraX = 0;
271 let cameraY = 0;
272 let clickCount = 0;
273 let isPaused = false;
274 const moveSpeed = 50;
275
276 // Funkcja logowania wydarzeń
277 function logEvent(message, type = 'mouse') {
278 const logEntry = document.createElement('div');
279 logEntry.className = `log-entry ${type}`;
280 logEntry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
281 eventLog.insertBefore(logEntry, eventLog.firstChild);
282
283 // Ograniczamy liczbę wpisów
284 if (eventLog.children.length > 20) {
285 eventLog.removeChild(eventLog.lastChild);
286 }
287 }
288
289 // Funkcja przesuwania kamery
290 function moveCamera(direction) {
291 if (isPaused) {
292 logEvent('Kamera wstrzymana! Naciśnij Space aby wznowić', 'keyboard');
293 return;
294 }
295
296 switch(direction) {
297 case 'up':
298 cameraY = Math.max(cameraY - moveSpeed, -200);
299 break;
300 case 'down':
301 cameraY = Math.min(cameraY + moveSpeed, 200);
302 break;
303 case 'left':
304 cameraX = Math.max(cameraX - moveSpeed, -200);
305 break;
306 case 'right':
307 cameraX = Math.min(cameraX + moveSpeed, 200);
308 break;
309 }
310
311 camera.style.transform = `translate(${cameraX}px, ${cameraY}px)`;
312 cameraPosDisplay.textContent = `${cameraX}, ${cameraY}`;
313 logEvent(`Przesunięcie kamery: ${direction}`, 'keyboard');
314 }
315
316 // OBSŁUGA MYSZY
317
318 // Mousemove - śledzenie pozycji myszy
319 viewport.addEventListener('mousemove', function(e) {
320 const rect = viewport.getBoundingClientRect();
321 const x = Math.floor(e.clientX - rect.left);
322 const y = Math.floor(e.clientY - rect.top);
323 mousePosDisplay.textContent = `${x}, ${y}`;
324 });
325
326 // Click - pojedyncze kliknięcie
327 viewport.addEventListener('click', function(e) {
328 clickCount++;
329 clickCountDisplay.textContent = clickCount;
330
331 const rect = viewport.getBoundingClientRect();
332 const x = Math.floor(e.clientX - rect.left);
333 const y = Math.floor(e.clientY - rect.top);
334
335 logEvent(`Kliknięcie w pozycji: (${x}, ${y})`, 'click');
336 });
337
338 // Double click - podwójne kliknięcie (zoom)
339 viewport.addEventListener('dblclick', function(e) {
340 const currentScale = camera.style.transform.includes('scale') ? 1 : 1.5;
341 const newScale = currentScale === 1 ? 1.5 : 1;
342
343 camera.style.transform = `translate(${cameraX}px, ${cameraY}px) scale(${newScale})`;
344 logEvent(`Zoom: ${newScale === 1 ? 'Wyłączony' : 'Włączony'}`, 'click');
345 });
346
347 // Mouseenter/mouseleave - najechanie na viewport
348 viewport.addEventListener('mouseenter', function() {
349 viewport.style.borderColor = '#3498db';
350 logEvent('Mysz weszła w obszar kamery', 'mouse');
351 });
352
353 viewport.addEventListener('mouseleave', function() {
354 viewport.style.borderColor = '#7f8c8d';
355 logEvent('Mysz opuściła obszar kamery', 'mouse');
356 });
357
358 // Kliknięcie na dinozaura
359 document.querySelectorAll('.dinosaur').forEach(dino => {
360 dino.addEventListener('click', function(e) {
361 e.stopPropagation(); // Zatrzymujemy propagację do viewport
362
363 const name = this.dataset.name;
364 this.classList.add('clicked');
365
366 setTimeout(() => {
367 this.classList.remove('clicked');
368 }, 500);
369
370 logEvent(`Kliknięto dinozaura: ${name}`, 'click');
371 console.log(`Obserwacja: ${name}`);
372 });
373
374 dino.addEventListener('mouseenter', function() {
375 const name = this.dataset.name;
376 logEvent(`Najechano na: ${name}`, 'mouse');
377 });
378 });
379
380 // OBSŁUGA KLAWIATURY
381
382 // Keydown - naciśnięcie klawisza
383 document.addEventListener('keydown', function(e) {
384 lastKeyDisplay.textContent = e.key;
385
386 switch(e.key) {
387 case 'ArrowUp':
388 e.preventDefault();
389 moveCamera('up');
390 break;
391 case 'ArrowDown':
392 e.preventDefault();
393 moveCamera('down');
394 break;
395 case 'ArrowLeft':
396 e.preventDefault();
397 moveCamera('left');
398 break;
399 case 'ArrowRight':
400 e.preventDefault();
401 moveCamera('right');
402 break;
403 case ' ':
404 e.preventDefault();
405 isPaused = !isPaused;
406 logEvent(`Pauza: ${isPaused ? 'WŁĄCZONA' : 'WYŁĄCZONA'}`, 'keyboard');
407 viewport.style.opacity = isPaused ? '0.5' : '1';
408 break;
409 case 'Escape':
410 logEvent('Naciśnięto Escape - zamykanie widoku', 'keyboard');
411 alert('Widok kamery zostanie zamknięty');
412 break;
413 case 'r':
414 case 'R':
415 // Reset kamery
416 cameraX = 0;
417 cameraY = 0;
418 camera.style.transform = 'translate(0px, 0px)';
419 cameraPosDisplay.textContent = '0, 0';
420 logEvent('Reset pozycji kamery', 'keyboard');
421 break;
422 default:
423 logEvent(`Naciśnięto klawisz: ${e.key}`, 'keyboard');
424 }
425 });
426
427 // Obsługa przycisków strzałek
428 document.querySelectorAll('.arrow-btn').forEach(btn => {
429 btn.addEventListener('click', function() {
430 const direction = this.dataset.direction;
431 moveCamera(direction);
432 });
433 });
434
435 // Inicjalizacja
436 logEvent('System kamery uruchomiony', 'keyboard');
437 console.log('System monitoringu Parku Jurajskiego - ONLINE');
438 console.log('Użyj strzałek do poruszania kamerą');
439 console.log('Kliknij na dinozaury, aby je obserwować');
440 </script>
441</body>
442</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. Które zdarzenie klawiatury przeglądarka wysyła po wciśnięciu każdego klawisza (także Shift, Ctrl czy strzałek) i powtarza, dopóki klawisz jest przytrzymany?
2. Które wydarzenie wywoływane jest po całkowitym załadowaniu strony?
Zadania praktyczne w grze
- Edytor kodu
W pliku index.html są dwie zagrody (.enclosure) i karty dinozaurów z atrybutem draggable="true". Przy dragstart karta zapisuje swoje id w dataTransfer. Uzupełnij luki: ___BLANK1___ to zdarzenie, które zagroda dostaje, gdy przeciągana karta jest nad nią (w nim preventDefault() zezwala na upuszczenie), a ___BLANK2___ to metoda dataTransfer, która w drop odczytuje zapisane id. Upuszczona karta ma trafić do zagrody, nad którą ją upuszczono.
- Układanie w pionie
Ułóż wydarzenia myszy w kolejności ich następowania podczas kliknięcia:
- Klikanie w kolejności
Ułóż kod sprawdzający kombinację Ctrl+S.
- Układanie w pionie
Ułóż wydarzenia dotykowe w kolejności ich występowania: