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
click
- tlačítko myši je stisknuto a uvolněno na stejném elementumousedown
- tlačítko myši je stisknutomouseenter
– kurzor myši vstoupí do oblasti elementu (na rozdíl odmouseover
neprobublává do nadřazených elementů)mouseleave
– kurzor myši opustí oblast elementu (na rozdíl odmouseout
neprobublává do nadřazených elementů)mousemove
– kurzor myši se pohne v oblasti elementumouseout
- kurzor myši opustí oblast elementu (na rozdíl odmouseleave
probublává do nadřazených elementů)mouseover
- kurzor myši vstoupí do oblasti elementu (na rozdíl odmouseenter
probublává do nadřazených elementů)mouseup
– tlačítko myši je uvolněno
gotpointercapture
– ukazatel zahájil operaci s elementem (užitečné v případech, kdy se ukazatel během operace dostane mimo oblast elementu)lostpointercapture
– ukazatel ukončil operaci s elementempointercancel
– operace s elementem byla zrušena (např. v případě, kdy uživatel ve skutečnosti dotykem posouvá nebo zoomuje stránku)pointerdown
– dotek nebo stisknutí tlačítka ukazovacího zařízení (např. stylusu)pointerenter
– ukazatel vstoupí do oblasti elementu (na rozdíl odpointerover
neprobublává do nadřazených elementů)pointerleave
- ukazatel opustí oblast elementu (na rozdíl odpointerout
neprobublává do nadřazených elementů)pointermove
- ukazatel se pohne v oblasti elementupointerout
- ukazatel opustí oblast elementu (na rozdíl odpointerleave
probublává do nadřazených elementů)pointerover
- ukazatel vstoupí do oblasti elementu (na rozdíl odpointerenter
probublává do nadřazených elementů)pointerup
– uvolnění doteku nebo tlačítka ukazovacího zařízení
touchcancel
- operace s elementem byla zrušena (např. v případě, kdy uživatel ve skutečnosti dotykem posouvá nebo zoomuje stránku)touchend
– dotek nebo gesto bylo ukončenotouchmove
– bod doteku byl změněntouchstart
– dotek nebo gesto bylo zahájeno v oblasti elementu
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.