Disassembler

Artificial intelligence is no match for natural stupidity.
18červen2017

Něžné doteky v JavaScriptu


V článku o blogískovém faceliftu jsem zmiňoval, že mi JavaScriptové dotykové a klikací eventy při výrobě nového vzhledu blogu poněkud napálily kudrlinku. Google Analytics sice tvrdí, že na mém blogu tvoří přístupy z mobilů, tabletů a jiných osahávacích zařízení jen 6 % návštěvnosti, ale to může taky znamenat, že mé stránky byly tak nepoužitelné, že na ně ze smartphonů nikdo nechtěl přistupovat. Teď už se na mobilních zařízeních zobrazují celkem obstojně, takže si konečně můžu pobrečet, čím jsem si při vývoji prošel a jak ony dotykové události vlastně fungují.

Panoptikum eventů


Na úvod trocha seznámení se vstupními událostmi, které je uživatel schopen pomocí prstu a myši vyvolat. Předpokládám, že každý, kdo v posledních dvaceti letech programoval nějakou část webové stránky v JavaScriptu, zná událost click a nejspíše i mouseover a mouseout. To je však jen špička ledovce. Kromě mouse* ještě existují i touch* eventy pro dotyková zařízení a pointer* eventy, které mají za úkol sdružovat události pro všechny ukazatele, ať už se jedná o myš, prst, více prstů zároveň nebo stylus. Podpora není kdovíjak slavná ani u touch ani u pointer eventů, ale u mobilních prohlížečů, kde hrozí, že podpora doteků bude potřeba nejvíc, naštěstí touch eventy podporovány jsou. Všechny eventy a okamžik jejich vzniku shrnuje následující seznam

Abych zjistil, jaké všechny eventy takové obyčejné blbé kliknutí zavolá a v jakém pořadí, udělal jsem si malou testovací stránku, jejímž jediným prvkem je <div> a pomocí hromádky JavaScriptu nechávám do konzole zaznamenávat výše uvedené události (vyjma mousemove, pointermove a touchmove, které příliš spamují), protože mě zajímá rozdíl v posloupnosti při doteku a kliknutí.

<!DOCTYPE html>
<html>
    <head>
        <title>JS event test</title>
        <style type="text/css">
            div {
                padding: 20px;
                border: 1px solid #000;
            }
        </style>
        <script type="text/javascript">
            window.onload = function() {
                var div = document.getElementsByTagName('div')[0];
                ['click', 'mousedown', 'mouseenter', 'mouseleave', 'mouseout', 'mouseover', 'mouseup',
                'gotpointercapture', 'lostpointercapture', 'pointercancel', 'pointerdown', 'pointerenter', 'pointerleave', 'pointerout', 'pointerover', 'pointerup',
                'touchcancel', 'touchend', 'touchstart'].map(function(event) {
                    div.addEventListener(event, function(event) {
                        console.log(event.type);
                    });
                });
            }
        </script>
    </head>
    <body>
        <div>Push me<br>And then just touch me<br>Till I can get my satisfaction</div>
    </body>
</html>

Odpovídající kód jsem pro případné další hračičky šoupnul i na JSFiddle.

Šťouchy šťouch


Tučně jsou zvýrazněny události podporované drtivou většinou prohlížečů v aktuálních verzích. Tedy takové, na které se dá spolehnout a navázat na ně programovou logiku bez potřeby nějakého divokého ladění kompatibility. Testoval jsem hlavně v Google Chrome, protože Firefox je hloupoučký a při zapnuté emulaci dotykového módu stále zpracovává :hover styly už při najetí kurzoru na element a nikoliv až při doteku. Firefox je ale notoricky známý tím, že jeho komponenty pro zpracování HTML a pro zpracování CSS o sobě netuší, což často vede k úsměvným bugům, které jsou v řešení třeba 15 let, takže mě to vlastně až tak moc nepřekvapuje. Při obyčejném kliknutí myší se děje následující:

# kurzor vstoupí do oblasti elementu
pointerover
pointerenter
mouseover
mouseenter

# tlačítko myši je stisknuto a uvolněno (klik)
pointerdown
mousedown
pointerup
mouseup
click

# kurzor opustí oblast elementu
pointerout
pointerleave
mouseout
mouseleave

Zatímco při doteku se děje tohle:

pointerover
pointerenter
pointerdown
touchstart
gotpointercapture
pointerup
lostpointercapture
pointerout
pointerleave
touchend
# 300 ms delay
mouseover
mouseenter
mousedown
mouseup
click

U doteku je zejména zajímavé umělé 300ms zpoždění, které vkládají prohlížeče mezi touch eventy a mouse eventy. To zajistí, že v případě vícenásobného doteku (například double tap pro přiblížení) nebo tažení, bude mít uživatel šanci tuto dotykovou akci zahájit dříve, než ji prohlížeč předá dalším handlerům. Právě to mi způsobilo několikeré poškrábání na hlavě, neboť jsem potřeboval docílit toho, že dropdowny v horním menu budou na myšových zařízeních fungovat jako odkazy rovnou, kdežto na dotykových se při prvním doteku menu jen rozbalí a navigace se provede až při druhém doteku. Další zradou pak je, že pokud je používána myš, pseudotřída :hover je aplikována ještě před voláním handleru prvního pointerover eventu, zatímco v případě doteku až mezi touchend a mouseover. Této vlastnosti jsem tedy vyžil pro napsání jednoduché logiky, která mi kýženou funkci zajistí. Principelně to funguje tak, že při touchstart se zkontroluje, zda element již má :hover. Pokud nemá, nastaví se elementu příznak (například třída nebo HTML5 data atribut). Pokud již :hover má, příznak se naopak odstraní. Přítomnost příznaku se následně kontroluje při click eventu odpálenému 300 ms po doteku. Má-li element nastavený flag, klik se ignoruje a element zůstane aktivní. Následný dotek pak provede tutéž kontrolu, zjistí, že :hover je přítomen, takže flag zruší a click projde. Nakonec už jen přihodím mouseout, který mi bude flag uklízet i v případě, že se uživatel rozhodne podruhé tapnout na jiný element.

$element.on('touchstart', function() {
    $(this).toggleClass('disableclick', !$(this).is(':hover'));
}).on('mouseout', function() {
    $(this).removeClass('disableclick');
}).click(function() {
    if ($(this).hasClass('disableclick'))
        return false;
});

Jen dva prstíčky tam strčíme


Když už mluvím o osahávání displejů, přidám ještě potenciálně užitečný kód vyňatý (a lehce upravený) ze StackOverflow pro zachytávání a identifikaci jednotlivých gest – krátký stisk (tap), dlouhý stisk, rychlé švihnutí prstem a pomalé tažení.

var touchStartTime;
var touchStartLocation;
var touchEndTime;
var touchEndLocation;

$element.on('touchstart', function() {
     var d = new Date();
     touchStartTime = d.getTime();
     touchStartLocation = mouse.location(x,y);
}).on('touchend', function() {
     var d = new Date();
     touchEndTime = d.getTime();
     touchEndLocation = mouse.location(x,y);
     doTouchLogic();
});

function doTouchLogic() {
     var distance = touchEndLocation - touchStartLocation;
     var duration = touchEndTime - touchStartTime;

     if (duration <= 100ms && distance <= 10px) {
          // Person tapped their finger (do click/tap stuff here)
     } else if (duration > 100ms && distance <= 10px) {
          // Person pressed their finger (not a quick tap)
     } else if (duration <= 100ms && distance > 10px) {
          // Person flicked their finger
     } else if (duration > 100ms && distance > 10px) {
          // Person dragged their finger
     }
}

Samozřejmě se veškeré dotykové události dají obsluhovat nějakými polyfilly nebo knihovnami k tomu přímo určenými (jQuery Mobile, Hammer.js, Pointer Events Polyfill atd.), ale já potřeboval řešit jednu jedinou věc, takže už samotné základní jQuery, které na webu používám k řešení více záležitostí, je poněkud overkill.