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

Zaawansowane techniki obsługi wydarzeń

4 min czytania

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. 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:

Przydatne artykuły