Wskazywanie drogi naprzód

Sérgio Gomes

Kiedyś zwracanie uwagi na rzeczy w internecie było proste. Chodziło o myszkę, poruszaliśmy nią, naciskaliśmy przyciski i tak dalej. Wszystko, co nie było myszką, było emulowane jako jedno, a deweloperzy wiedzieli, na co dokładnie liczyć.

Prostota nie musi być jednak łatwa. Z czasem coraz ważniejsze było to, że nie wszystko było (lub udawało) myszką. Mając pióra czułe na nacisk i z odchyleniem, aby wykazać się twórczą swobodą, wystarczyło już tylko urządzenie i dłoń, a do tego można było używać więcej niż 1 palca.

Od jakiegoś czasu używamy zdarzeń dotyku, aby nam w tym pomagać, ale są to zupełnie odrębne interfejsy API przeznaczone specjalnie do obsługi dotyku. Jeśli chcesz obsługiwać zarówno mysz, jak i dotyk, musisz zakodować 2 osobne modele zdarzeń. W Chrome 55 obowiązuje nowszy standard, który ujednolica oba modele: zdarzenia wskaźnika.

Model pojedynczego zdarzenia

Zdarzenia wskaźnika ujednolicają model wprowadzania wskaźnika dla przeglądarki, łącząc dotyk, pióra i myszy w jeden zestaw zdarzeń. Na przykład:

document.addEventListener('pointermove',
    ev => console.log('The pointer moved.'));
foo.addEventListener('pointerover',
    ev => console.log('The pointer is now over foo.'));

Oto lista wszystkich dostępnych zdarzeń, które powinny wyglądać znajomo, jeśli znasz się na zdarzeniach myszy:

pointerover Wskaźnik wszedł do ramki ograniczającej elementu. Dzieje się tak natychmiast w przypadku urządzeń, które obsługują najechanie kursorem, lub przed zdarzeniem pointerdown w przypadku urządzeń, które nie obsługują tej funkcji.
pointerenter Podobna do pointerover, ale nie wyświetla się jako dymki i obsługuje elementy podrzędne w inny sposób. Szczegółowe informacje o specyfikacji.
pointerdown Wskaźnik wszedł w stan aktywnego przycisku i został naciśnięty przycisk lub nawiązywany jest kontakt w zależności od semantyki urządzenia wejściowego.
pointermove Wskaźnik zmienił położenie.
pointerup Wskaźnik pozostawił aktywny przycisk.
pointercancel Stało się coś, co oznacza, że wskaźnik prawdopodobnie nie będzie generować więcej zdarzeń. Oznacza to, że należy anulować wszystkie trwające działania i wrócić do neutralnego stanu danych wejściowych.
pointerout Wskaźnik opuścił ramkę ograniczającą elementu lub ekranu. Podobnie po pointerup, jeśli urządzenie nie obsługuje najechania kursorem.
pointerleave Podobna do pointerout, ale nie wyświetla się jako dymki i obsługuje elementy podrzędne w inny sposób. Szczegółowe informacje o specyfikacji.
gotpointercapture Element otrzymał zapis wskaźnika.
lostpointercapture Przechwytywany wskaźnik został zwolniony.

Różne typy danych wejściowych

Ogólnie rzecz biorąc, zdarzenia wskaźnika pozwalają pisać kod w sposób niezależny od danych wejściowych, bez konieczności rejestrowania osobnych modułów obsługi zdarzeń dla różnych urządzeń wejściowych. Oczywiście nadal musisz pamiętać o różnicach między typami danych wejściowych, np. o tym, czy pojawia się pojęcie najechania kursorem. Jeśli chcesz rozróżniać różne typy urządzeń wejściowych (na przykład aby udostępniać oddzielny kod/funkcje dla różnych danych wejściowych), możesz to jednak zrobić w tych samych modułach obsługi zdarzeń za pomocą właściwości pointerType interfejsu PointerEvent. Jeśli np. kodujesz panel nawigacji bocznej, w zdarzeniu pointermove możesz mieć taką logiczną logikę:

switch(ev.pointerType) {
    case 'mouse':
    // Do nothing.
    break;
    case 'touch':
    // Allow drag gesture.
    break;
    case 'pen':
    // Also allow drag gesture.
    break;
    default:
    // Getting an empty string means the browser doesn't know
    // what device type it is. Let's assume mouse and do nothing.
    break;
}

Działania domyślne

W przeglądarkach z obsługą dotykową niektóre gesty służą do przewijania, powiększania i odświeżania strony. W przypadku zdarzeń dotknięcia nadal będziesz otrzymywać zdarzenia w trakcie wykonywania tych działań domyślnych, np. gdy użytkownik przewija stronę, wywoływany będzie touchmove.

W przypadku zdarzeń wskaźnika po każdym wywołaniu działania domyślnego, np. przewijania lub powiększania, stosowane jest zdarzenie pointercancel informujące, że przeglądarka przejęła kontrolę nad wskaźnikiem. Na przykład:

document.addEventListener('pointercancel',
    ev => console.log('Go home, the browser is in charge now.'));

Wbudowana szybkość: ten model zapewnia domyślnie lepszą skuteczność w porównaniu ze zdarzeniami dotknięciami, w przypadku których osiągnięcie takiego samego poziomu reagowania wymaga użycia pasywnych detektorów zdarzeń.

Możesz zatrzymać przeglądarkę, używając właściwości CSS touch-action. Ustawienie dla elementu wartości none spowoduje wyłączenie wszystkich działań zdefiniowanych w przeglądarce, które rozpoczynają się w związku z tym elementem. Istnieje jednak jeszcze szereg innych wartości umożliwiających bardziej szczegółową kontrolę, np. pan-x, które umożliwiają przeglądarce reagowanie na ruchy na osi X, ale nie na osi Y. Chrome 55 obsługuje te wartości:

auto Domyślnie; przeglądarka może wykonywać dowolne domyślne działania.
none Przeglądarka nie może wykonywać żadnych domyślnych działań.
pan-x Przeglądarka może wykonywać tylko domyślne działanie przewijania w poziomie.
pan-y Przeglądarka może wykonywać tylko domyślne działanie przewijania w pionie.
pan-left Przeglądarka może wykonywać tylko domyślne działanie przewijania w poziomie i przesuwać stronę w lewo.
pan-right Przeglądarka może wykonywać tylko domyślne działanie przewijania w poziomie i przesuwać stronę w prawo.
pan-up Przeglądarka może wykonywać tylko domyślne działanie przewijania w pionie i tylko przesuwać stronę w górę.
pan-down Przeglądarka może wykonywać tylko domyślne działanie przewijania w pionie i tylko przesuwać stronę w dół.
manipulation Przeglądarka może wykonywać tylko czynności przewijania i powiększania.

Przechwytywanie wskaźnika

Czy zdarzyło Ci się spędzić frustrującą godzinę na debugowaniu uszkodzonego zdarzenia mouseup, dopóki nie zorientujesz się, że to dlatego, że użytkownik puszcza przycisk poza Twoim celem kliknięcia? Nie? W takim razie może to tylko ja.

Do tej pory nie było jednak żadnego dobrego sposobu na radzenie sobie z tym problemem. Oczywiście, możesz skonfigurować moduł obsługi mouseup w dokumencie i zapisać stan aplikacji, aby śledzić informacje. Nie jest to jednak najprostsze rozwiązanie, zwłaszcza jeśli tworzysz komponent witryny i chcesz, by wszystko było w porządku i odizolowane.

Zdarzenia typu wskaźnik są znacznie lepsze: możesz zarejestrować wskaźnik, aby mieć pewność, że zdarzenie pointerup (lub któregokolwiek z jego nieuchwytnych znajomych) zostanie odebrane.

const foo = document.querySelector('#foo');
foo.addEventListener('pointerdown', ev => {
    console.log('Button down, capturing!');
    // Every pointer has an ID, which you can read from the event.
    foo.setPointerCapture(ev.pointerId);
});

foo.addEventListener('pointerup', 
    ev => console.log('Button up. Every time!'));

Obsługiwane przeglądarki

W momencie tworzenia tego tekstu zdarzenia wskaźników są obsługiwane w przeglądarkach Internet Explorer 11, Microsoft Edge, Chrome i Opera, a częściowo obsługiwane w przeglądarce Firefox. Aktualną listę znajdziesz na stronie caniuse.com.

Do wypełniania luk możesz użyć kodu polyfill zdarzeń wskaźnika. Aby sprawdzić obsługę przeglądarek w czasie działania aplikacji, możesz też sprawdzić to w prosty sposób:

if (window.PointerEvent) {
    // Yay, we can use pointer events!
} else {
    // Back to mouse and touch events, I guess.
}

Zdarzenia wskaźnika doskonale nadają się do stopniowego udoskonalania: wystarczy zmodyfikować metody inicjowania, aby wykonać powyższy test, dodać moduły obsługi zdarzeń wskaźnika w bloku if, a potem przenieść moduły obsługi zdarzeń myszy/dotyku do bloku else.

Wypróbuj go i daj nam znać, co o nim myślisz.