Tworzymy własne eventy w JS!

Sam JavaScript nie posiada tego typu funkcji ani zdarzeń, a kiedy w sieci szukałem gotowego kodu do obsługi javascriptowych zdarzeń typu swipe, niczego fajnego nie znalazłem. Postanowiłem więc, że napiszę własne rozwiązanie

Często w swoich projektach natrafiałem na chęć integracji z dotykiem lub kursorem. Interaktywne karuzele, slidery, okienka itd. to elementy, do których bardziej pasuje mazanie palcem po ekranie, aniżeli statyczne przyciski ze zdarzeniem onClick, tak ja na desktopie. Niestety czysty JavaScript nie udostępnia takiego zdarzenia typu onswipe. Na pomoc przychodzi tutaj konstruktor new Event, który pozwoli na stworzenie własnych zdarzeń, a co za tym idzie, spełni moje oczekiwania. Docelowo napisany dzisiaj kod ma polegać na obsłudzę zdarzenia ontouchmove a nie mousemove, dlatego będzie ona działać jedynie na urządzeniach mobilnych. Stworzenie jednak takiej funkcji dla myszy jest jeszcze prostsze niż to co tutaj pokaże. Zapraszam więc do samodzielnego eksperymentowania z myszą.

Zaczynamy

Stwórzmy sobie dwa pliki: 

index.html oraz swipe-touch-events.js. Ten drugi będzie w zamierzeniu modułem, ale na potrzeby pisania funkcji zaimportujmy go na razie klasycznie:

<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
  </head>
  <body></body>
  <script src="swipe-touch-events.js"></script>
</html>

Następnie stworzymy sobie funkcję swipe, w której umieścimy całą logikę dodawania własnych zdarzeń. Funkcja przyjmuje element node z drzewa DOM, a jej domyślną wartość będzie document, bo to na całym dokumencie zechcemy używać zdarzeń:

const swipe = function (element = document) {};

Pierwsze co musimy zrobić, to zainicjowanie nowego zdarzenia dla podanego w argumencie elementu. Będzie to zdarzenie touchstart

const swipe = function (element = document) {
    element.addEventListener('touchstart', (e) => {
        console.dir(e);    //wyświetlenie obiektu zdarzenia
  });
};

swipe();

Od razu możmy zobaczyć jakie przydatne wartośći możemy wyciągnać z obiektu zdarzenia. W tym celu, przepuszczam e do argumentu funkcji, tak jak wyżej i sprawdzam otrzymaną wartość w konsoli. Przed tym uruchamiam funkcję na końcu pliku, żeby dodać listener do dokumentu. Teraz wystarczy, że włącze narzędzia developerskie i włącze widok mobliny i kliknę na dokument (przypominam, że zdarzenie będzie działało tylko w ten sposób, ponieważ zdarzenia touch przedstawiają dotknięcia, a nie kliknięcia myszą):

zdjęcie tematyczne
wizualizacja obiektu zdarzenia w konsoli

W obiekcie znajdziemy interesujący klucz touches, który jest tablicą. Pierwszy element tablicy (Touch List) to obiekt zawierający oczekiwane przez nas właściwości:

zdjęcie tematyczne
tablica Touch List z obiektu zdarzenia touchstart

Z tego obiektu wyciągniemy dwie wartości: clientX oraz clientY, które pomogą nam w ustaleniu drogi jaką przemierzył palec po ekranie. Te dwie wartości będą potrzebne w całym zakresie funkcji, więc definiujemy zmienne na górze i przypisuję wartość 0, a w event listenerze przepisuje do nich wyciągnięte ze zdarzenia wartości. Na ten moment kod będzie wyglądał tak:

const swipe = function (element = document) {
  let startingX = 0;
  let startingY = 0;

  element.addEventListener('touchstart', (e) => {
    const { clientX, clientY } = e.touches[0];

    startingX = clientX;
    startingY = clientY;
  });
};

swipe();

Teraz posiadamy współrzędne startowe dotknięcia. W następnym kroku zainicjujemy zdarzenie touchmove, w którym wykorzystamy funkcję updateMovement zliczającą przebytą drogę podczas jednego przeciągnięcia palcem. Potrzebne więc nam będą dwie dodatkowe zmienne widoczne dla całego scopu funkcji. Nazwałem je movementX oraz movementY:

const swipe = function (element = document) {
  let startingX = 0;
  let startingY = 0;
  let movementX = 0;
  let movementY = 0;

  const updateMovement = (e) => {
    const { clientX, clientY } = e.touches[0];

    movementX = startingX - clientX;
    movementY = startingY - clientY;
    console.log(movementX, movementY);               //wyświetlanie aktualnie przebytej drogi
  };

  element.addEventListener('touchstart', (e) => {
    const { clientX, clientY } = e.touches[0];

    startingX = clientX;
    startingY = clientY;

    document.addEventListener('touchmove', updateMovement);
  });
};

swipe();

Dzięki odjęciu wartości startowej od aktualnych współrzędnych, otrzymujemy drogę liczoną w pikselach w czasie rzeczywistym - to będzie punkt zaczepny czy dane zdarzenie swipe ma się wykonać.

zdjęcie tematyczne
podczas zdarzenia touchmove otrzymujemy aktualne wartości przebytej przez palec drogi w osi X oraz osi Y

Jak się pewnie domyślasz, musimy napisać teraz callback, który uruchomi się na koniec dotknięcia. Dodaję więc listener na zdarzenie touchend wykonywane na całym dokumencie dostępne w natywnym API JS i:

  • usuwam zdefiniowany wcześniej listener na touchmove (bo kiedy użytkownik odrywa palec od ekranu, to jest już ono więcej niepotrzebne),
  • defniuje typ zdarzenia, który przypiszę do elementu, zależnie od drogi jaką przebył (będą to cztery zdarzenia, które chcę zdefiniować: swiperight, swipeleft, swipeupi odpowiednio swipedown),
  • dispatchuje event (tworzę nowe zdarzenie) na danym elemencie
document.addEventListener('touchend', (e) => {
    document.removeEventListener('touchmove', updateMovement);

    let eventType;

    if (movementX > threshold && movementX >= movementY) eventType = 'swipeleft';
    else if (movementY > threshold && movementY >= movementX) eventType = 'swipeup';
    else if (movementX < -threshold && movementX <= movementY) eventType = 'swiperight';
    else if (movementY < -threshold && movementY <= movementX) eventType = 'swipedown';
    else return;

    console.log('x:', movementX, 'y:', movementY, eventType);

    const event = new Event(eventType, { bubbles: true });
    target.dispatchEvent(event);
  });

Zdefiniowane przeze mnie warunki sprawdzają, czy droga w pikselach przebyta przez palec mieści się powyżej progu ukrytym pod zmienną threshold, której wartość będziemy definiować w argumencie funkcji. Wartość domyślna dla threshold wynosi 50 - warunek sprawdza więc czy palec przemierzył przynajmniej 50 pikseli po ekranie. Sprawdza również, która droga (czy w osoi X czy Y) ma większą/mniejszą wartość i na tej podstawie zapisuje w zmiennej eventType nazwę pożądanego eventu.

zdjęcie tematyczne
sprawdzanie typu zdarzenia na podstawie przebytej drogi

dispatchEvent

Przejdźmy teraz do najważniejszego zadania kodu. Jest nim dispatchowanie eventów. Dzięki funkcji dispatchEvent możliwym jest uruchomienie nasłuchiwanego zdarzenia. Warunek, który napisaliśmy zapisuje nazwę zdarzenia i na jej podstawie dzięki konstruktorowi new Event, tworzy nowy obiekt zdarzenia o podanej nazwie. Instancję tego obiektu parsujemy w funkcji dispatchEvent, która wyknywana jest na danym elemencie DOM (w tym wypadku target, który zdefinujemy dla całego scopu funkcji a wartość elemetu node przypiszemy ze zdarzenia touchstart. To właśnie w momencie dispatchowania eventu dany listener wykonuje dany callback. Kiedy definiujemy nowy obiekt zdarzenia, ważnym jest by w drugim jego argumencie umieścić obiekt z opcjonalnymi właściwościami. Właściwość bubbles: true oznacza, że zdarzenie będzie posiadało tzw. event bubbling, które jest bardzo istotne w przypadku zdarzeń na elementach DOM (dzięki temu możemy łatwo określić element na którym wykonane jest zdarzenie).

W końcowym rozrachunku napisany przez nas kod prezentuje się następująco:

const swipe = function (element = document, threshold = 50) {
  let startingX = 0;
  let startingY = 0;
  let movementX = 0;
  let movementY = 0;
  let target;

  const updateMovement = (e) => {
    const { clientX, clientY } = e.touches[0];

    movementX = startingX - clientX;
    movementY = startingY - clientY;
  };

  element.addEventListener('touchstart', (e) => {
    const { clientX, clientY } = e.touches[0];
    target = e.target;

    startingX = clientX;
    startingY = clientY;

    document.addEventListener('touchmove', updateMovement);
  });

  document.addEventListener('touchend', (e) => {
    document.removeEventListener('touchmove', updateMovement);

    let eventType;

    if (movementX > threshold && movementX >= movementY) eventType = 'swipeleft';
    else if (movementY > threshold && movementY >= movementX) eventType = 'swipeup';
    else if (movementX < -threshold && movementX <= movementY) eventType = 'swiperight';
    else if (movementY < -threshold && movementY <= movementX) eventType = 'swipedown';
    else return;

    const event = new Event(eventType, { bubbles: true });
    target.dispatchEvent(event);
  });
};

swipe();

Dzięki powyższej funkcji możemy zrobić na przykład coś takiego gdziekolwiek w naszej aplikacji do jakiegokolwiek elementu DOM:

document.addEventListener('swipeleft', () => {
  console.log('przesunięto w lewo');
});

document.addEventListener('swiperight', () => {
  console.log('przesunięto w prawo');
});
zdjęcie tematyczne
Po przesunięciu palcem w prawo lub lewo wykonuje się zdefiniowany callback

Nie muszę mówić chyba jakie to ma zalety. Dzięki temu zabiegowi możemy dodawać callbacki kiedy przesuniemy palcem, a tym samym wykonywać zaawansowane integracje z użytkownikiem, takie jak: przesunięcie karuzeli, slidera, okna, elementu itp.

Moduł

Aktualnie aby dodać zdarzenia do dokumentu, trzeba wykonać funkcję swipe. Na początku wspomniałem, że funkcja ma być w zamierzeniu modułem - to znaczy, że będziemy mogli ją importować w momencie, kiedy jej potrzebujemy, a nie od razu dla całego pliku. Zróbmy to więc teraz. Pierwszym krokiem będzie zamiana funkcji swipe na funkcję anonimową IIFE (Immediately Invoked Function Expression), czyli funkcję wykonującą się od razu podczas jej definicji. Dzięki temu nie będziemy musieli uruchamiać funkcji, a wystarczy, że ją tylko zaimportujemy jako moduł. IIFE stworzymy tak:

(function (element = document, threshold = 50) {
  //...
})();

Teraz wystarczy że zaimportujemy ją poprzez słowo kluczowe import, na przykład w ten sposób:

//script.js
import '/swipe-touch-events.js';

document.addEventListener('swipeleft', () => {
  console.log('przesunięto w lewo');
});

document.addEventListener('swiperight', () => {
  console.log('przesunięto w prawo');
});

Należy jednak pamiętać, że zadziała to tylko w modułach (w znaczniku script trzeba dodać atrybut type="module").

Podsumowanie

To by było na tyle, jeśli chodzi o dodawanie własnych zdarzeń w JavaScripcie. Jest to jednak dość obszerny temat, dlatego odsyłam do dokumentacji MDN tutaj oraz do samodzielnego eksperymentowania. W następnym wpisie dodamy napisaną dziś funkcję do rejestru npm. tworząc tym samym własną bilbiotekę, którą inni programiści będą mogli wykorzystać w swoich programach. Taką, którą będziemy mogli na szybko zainstalować za pomocą komendy npm install. Do zobaczenia!

© Damian Kalka 2021
Wszelkie prawa zastrzeżone