Kurs JavaScript i TypeScript · Moduł 8: Zdarzenia i DOM
Zaawansowane techniki obsługi wydarzeń
Specjalistyczne techniki dla zaawansowanych przypadków użycia.
Goście parku coraz częściej przeglądają galerię dinozaurów na telefonach i tabletach. Tam nie ma myszy, a samo wydarzenie click nie powie Ci, w którą stronę ktoś przesunął palcem. Przeglądarka zgłasza za to trzy wydarzenia dotykowe po kolei: touchstart, gdy palec dotyka ekranu, touchmove, gdy przesuwa się po nim, i touchend, gdy go opuszcza. Z tych trzech sygnałów zbudujemy rozpoznawanie gestów, tak jak tropiciel odczytuje kierunek marszu stada ze śladów w błocie.
Obsługa gestów touch
Cała logika zamieszka w klasie TouchGestureHandler. Konstruktor zapamiętuje element i przygotowuje miejsce na współrzędne początku oraz bieżącej pozycji palca.
1// Obsługa gestów dotykowych dla urządzeń mobilnych
2class TouchGestureHandler {
3 constructor(element) {
4 this.element = element;
5 this.startX = 0;
6 this.startY = 0;
7 this.currentX = 0;
8 this.currentY = 0;
9 this.isTracking = false;
10
11 this.bindEvents();
12 }
13
14 bindEvents() {
15 this.element.addEventListener('touchstart', (e) => this.handleTouchStart(e));
16 this.element.addEventListener('touchmove', (e) => this.handleTouchMove(e));
17 this.element.addEventListener('touchend', (e) => this.handleTouchEnd(e));
18 }Metoda bindEvents() podpina trzy listenery, a funkcje strzałkowe zachowują this jako instancję klasy. Flaga isTracking mówi, czy trwa jakiś gest. Pierwszy sygnał to dotknięcie ekranu.
1 handleTouchStart(e) {
2 this.isTracking = true;
3 this.startX = e.touches[0].clientX;
4 this.startY = e.touches[0].clientY;
5 this.currentX = this.startX; // stuknięcie bez ruchu = zerowe przesunięcie
6 this.currentY = this.startY;
7 }Właściwość e.touches to lista wszystkich palców, które w tej chwili dotykają ekranu, a touches[0] to pierwszy z nich. Jego clientX i clientY działają tak samo jak przy myszy. Dwie ostatnie linie to poprawka: zerują przesunięcie na starcie, więc samo stuknięcie bez ruchu nie porówna startu z pozycją z poprzedniego gestu i nie wywoła fantomowego przesunięcia. Teraz ruch palca.
1 handleTouchMove(e) {
2 if (!this.isTracking) return;
3
4 e.preventDefault();
5 this.currentX = e.touches[0].clientX;
6 this.currentY = e.touches[0].clientY;
7
8 const deltaX = this.currentX - this.startX;
9 const deltaY = this.currentY - this.startY;
10
11 // Emituj custom event z danymi gestu
12 this.element.dispatchEvent(new CustomEvent('gesture', {
13 detail: { deltaX, deltaY, isActive: true }
14 }));
15 }Wywołanie preventDefault() blokuje przewijanie strony podczas gestu, ale działa tylko w listenerze, który nie jest pasywny. Tu nie ma problemu, bo nasłuchujemy na zwykłym elemencie. Na window, document, document.documentElement i body przeglądarki traktują touchstart i touchmove domyślnie jako pasywne i takie wywołanie zignorują. W trakcie ruchu klasa emituje własne wydarzenie gesture z przesunięciem w detail, więc karta może na bieżąco podążać za palcem. Na końcu palec opuszcza ekran.
1 handleTouchEnd(e) {
2 if (!this.isTracking) return;
3
4 this.isTracking = false;
5 const deltaX = this.currentX - this.startX;
6 const deltaY = this.currentY - this.startY;
7
8 // Określ typ gestu
9 if (Math.abs(deltaX) > Math.abs(deltaY)) {
10 // Poziomy gest
11 if (deltaX > 50) {
12 this.element.dispatchEvent(new CustomEvent('swipeRight'));
13 } else if (deltaX < -50) {
14 this.element.dispatchEvent(new CustomEvent('swipeLeft'));
15 }
16 } else {
17 // Pionowy gest
18 if (deltaY > 50) {
19 this.element.dispatchEvent(new CustomEvent('swipeDown'));
20 } else if (deltaY < -50) {
21 this.element.dispatchEvent(new CustomEvent('swipeUp'));
22 }
23 }
24
25 this.element.dispatchEvent(new CustomEvent('gestureEnd', {
26 detail: { deltaX, deltaY }
27 }));
28 }
29}W touchend lista e.touches nie zawiera już podniesionego palca, trafia on do e.changedTouches, dlatego korzystamy z zapamiętanych currentX i currentY. Porównanie Math.abs() ustala dominującą oś ruchu. Próg 50 pikseli odróżnia świadomy gest od drżenia palca. Oś Y na ekranie rośnie w dół, więc dodatnie deltaY oznacza przesunięcie w dół. Cała sekwencja zawsze wygląda tak samo: touchstart, seria touchmove, touchend, a na końcu analiza deltaX i deltaY oraz emisja własnego wydarzenia. Istnieje też czwarte wydarzenie, touchcancel, które przychodzi, gdy system przerwie dotyk, na przykład przy zbyt wielu palcach naraz. W jego obsłudze wystarczy wyzerować flagę isTracking.
Użycie w galerii dinozaurów
Klasa niczego nie wie o galerii, tylko nadaje wydarzenia. Galeria słucha tych, które ją interesują.
1const dinoGallery = document.getElementById('dino-gallery');
2new TouchGestureHandler(dinoGallery);
3
4dinoGallery.addEventListener('swipeLeft', () => showNextDinosaur());
5dinoGallery.addEventListener('swipeRight', () => showPreviousDinosaur());Przesunięcie w lewo pokazuje następnego dinozaura, a w prawo poprzedniego. Pamiętaj, że CustomEvent domyślnie nie bąbelkuje, więc listenery muszą wisieć na tym samym elemencie, który przekazaliśmy do handlera. Dokładnie ten podział ról poznałeś przy własnych wydarzeniach: jeden moduł wykrywa, drugi reaguje.
Moja rada: jeśli budujesz od zera komponent, który ma działać zarówno myszą, jak i palcem, sprawdź Pointer Events (pointerdown, pointermove, pointerup). Obsługują mysz, pióro i dotyk jednym zestawem wydarzeń. Touch Events wciąż przydają się tam, gdzie potrzebujesz listy wszystkich palców, na przykład przy geście szczypania. W piaskownicy poniżej przetestujesz gesty na obszarze dotykowym, a na komputerze także myszą. W następnej lekcji zajmiemy się wydajnością, w tym pasywnymi listenerami.
Pamiętaj: gest to ślad w błocie, który odczytujesz dopiero wtedy, gdy dinozaur podniesie łapę, czyli w touchend.
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>Touch Gestures</title>
7 <style>
8 body { font-family: Arial, sans-serif; background: linear-gradient(135deg, #2c3e50, #34495e); color: white; padding: 20px; touch-action: none; }
9 .touch-area { width: 100%; height: 300px; background: rgba(52, 152, 219, 0.2); border: 2px dashed #3498db; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 24px; margin: 20px 0; }
10 .info { background: rgba(0,0,0,0.6); padding: 15px; border-radius: 8px; margin: 10px 0; }
11 </style>
12</head>
13<body>
14 <h1>Touch Gestures - Sterowanie Dotykowe</h1>
15 <div class="info">
16 <p>Przeciągnij palcem: ←→ (poziomo) lub ↑↓ (pionowo)</p>
17 <p id="gesture-info">Czekam na gest...</p>
18 </div>
19 <div class="touch-area" id="touch-area">Dotknij i przeciągnij</div>
20 <div class="info">
21 <strong>Wykryte gesty:</strong>
22 <ul id="gesture-log"></ul>
23 </div>
24
25 <script>
26 console.log('Touch Gesture Handler initialized');
27
28 const touchArea = document.getElementById('touch-area');
29 const gestureInfo = document.getElementById('gesture-info');
30 const gestureLog = document.getElementById('gesture-log');
31
32 let startX = 0, startY = 0;
33 let currentX = 0, currentY = 0;
34 let isTracking = false;
35
36 function logGesture(message) {
37 const li = document.createElement('li');
38 li.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
39 gestureLog.insertBefore(li, gestureLog.firstChild);
40 if (gestureLog.children.length > 5) {
41 gestureLog.removeChild(gestureLog.lastChild);
42 }
43 }
44
45 touchArea.addEventListener('touchstart', (e) => {
46 isTracking = true;
47 startX = e.touches[0].clientX;
48 startY = e.touches[0].clientY;
49 currentX = startX;
50 currentY = startY;
51 gestureInfo.textContent = 'Rozpoczęto gest...';
52 touchArea.style.background = 'rgba(46, 204, 113, 0.2)';
53 console.log('Touch start:', startX, startY);
54 });
55
56 touchArea.addEventListener('touchmove', (e) => {
57 if (!isTracking) return;
58 e.preventDefault();
59
60 currentX = e.touches[0].clientX;
61 currentY = e.touches[0].clientY;
62
63 const deltaX = currentX - startX;
64 const deltaY = currentY - startY;
65
66 gestureInfo.textContent = `Δ X: ${Math.round(deltaX)}px, Y: ${Math.round(deltaY)}px`;
67 });
68
69 touchArea.addEventListener('touchend', (e) => {
70 if (!isTracking) return;
71 isTracking = false;
72
73 const deltaX = currentX - startX;
74 const deltaY = currentY - startY;
75
76 touchArea.style.background = 'rgba(52, 152, 219, 0.2)';
77
78 let gesture = 'Nierozpoznany';
79
80 if (Math.abs(deltaX) > Math.abs(deltaY)) {
81 if (deltaX > 50) {
82 gesture = '→ Swipe Right';
83 touchArea.textContent = '→ W prawo';
84 } else if (deltaX < -50) {
85 gesture = '← Swipe Left';
86 touchArea.textContent = '← W lewo';
87 }
88 } else {
89 if (deltaY > 50) {
90 gesture = '↓ Swipe Down';
91 touchArea.textContent = '↓ W dół';
92 } else if (deltaY < -50) {
93 gesture = '↑ Swipe Up';
94 touchArea.textContent = '↑ W górę';
95 }
96 }
97
98 logGesture(gesture);
99 console.log(`Gesture detected: ${gesture}`);
100
101 setTimeout(() => {
102 touchArea.textContent = 'Dotknij i przeciągnij';
103 gestureInfo.textContent = 'Czekam na kolejny gest...';
104 }, 1500);
105 });
106
107 // Fallback dla myszy (do testowania na desktop)
108 let mouseDown = false;
109 touchArea.addEventListener('mousedown', (e) => {
110 mouseDown = true;
111 startX = e.clientX;
112 startY = e.clientY;
113 currentX = startX;
114 currentY = startY;
115 });
116
117 touchArea.addEventListener('mousemove', (e) => {
118 if (!mouseDown) return;
119 currentX = e.clientX;
120 currentY = e.clientY;
121 const deltaX = currentX - startX;
122 const deltaY = currentY - startY;
123 gestureInfo.textContent = `Mouse Δ X: ${Math.round(deltaX)}px, Y: ${Math.round(deltaY)}px`;
124 });
125
126 touchArea.addEventListener('mouseup', () => {
127 if (mouseDown) {
128 mouseDown = false;
129 const deltaX = currentX - startX;
130 const deltaY = currentY - startY;
131
132 if (Math.abs(deltaX) > 50 || Math.abs(deltaY) > 50) {
133 logGesture('Mouse gesture detected');
134 }
135 }
136 });
137
138 console.log('Touch gestures ready - swipe to test!');
139 </script>
140</body>
141</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 jest wywoływane, gdy użytkownik przesuwa palcem po ekranie dotykowym?
Zadania praktyczne w grze
- Edytor kodu
W pliku index.js jest klasa TouchGestureHandler z lekcji o gestach. Uzupełnij luki: ___BLANK1___ to zdarzenie dotyku wysyłane, gdy palec przesuwa się po ekranie, ___BLANK2___ to nazwa kierunku, gdy palec przesunął się w górę o więcej niż 50 px, a ___BLANK3___ to pole opcji CustomEvent, w którym zdarzenie swipe niesie dane { direction, deltaX, deltaY }. Przesunięcie o więcej niż 50 px ma wysłać jedno zdarzenie swipe z kierunkiem left, right, up albo down, a stuknięcie i drobny ruch żadnego.
- Układanie w pionie
Ułóż zdarzenia dotykowe w kolejności, w jakiej występują podczas przesunięcia palcem po ekranie: