Webkrauts Logo

Webkrauts Webkrauts Schriftzug

- für mehr Qualität im Web

Drei unter einem Dach

Pointer-Events für Maus, Stylus und Touchscreen

Drei unter einem Dach

Um Interaktionen über einen Touchscreen zu berücksichtigen, müssen Entwickler oft separaten Code für Maus- und Touch-Events schreiben. Im Internet Explorer 10 wurde mit Pointer-Events ein komplett umgedachtes Modell eingeführt: Damit ist es möglich, Interaktionen über Maus, Stylus oder Touchscreen in einem Event zusammenzufassen.

Im letzjährigen Adventskalender gab es eine schnelle Einführung zu Touch-Events. Wie schon damals erwähnt, werden Touch-Events in fast allen modernen Browsern unterstützt… mit Ausnahme von Internet Explorer. Obwohl Microsoft lange Zeit auf Touch-fähigen mobilen und »Desktop«-Geräten präsent war, gab es anfangs keine Möglichkeit, Touchscreen-spezifisches JavaScript im Internet Explorer einzusetzen. Entwickler waren darauf angewiesen, auf (simulierte) Maus-Events zu reagieren.

Anstatt anderen Browsern nachzuziehen und auch Touch-Events (mit den damit verbundenen Problemen, die schon im Adventskalender-Artikel zum Thema kurz zusammengefasst wurden) einzusetzen, wurde im Internet Explorer 10 (in Windows Phone 8 und Windows 8/RT) mit Pointer-Events ein komplett umdachtes Modell eingeführt.

Vorteile von Pointer-Events im Vergleich zu Touch-Events

Touch-Events sind speziell auf Touchscreens ausgesetzt – Entwicklern müssen deshalb in vielen Fällen separaten Code für Maus- und Touch-Interaktionen schreiben.

  1. /* Pseudo-Code: zur Bearbeitung von Maus- und Finger-Bewegungen
  2.    benötigt im Touch-Event Modell separate Funktionen */
  3.  
  4. foo.addEventListener('mousemove', function(e) { ... }, false);
  5. foo.addEventListener('touchmove', function(e) { ... }, false);

Darüber hinaus müssen sich Entwickler noch mit einer Eigenheit von Touch-Events auseinandersetzen: die Koordinaten der verschiedenen Touch-Punkte sind bei Touch-Events nicht direkt im Event, sondern in separaten TouchList-Arrays untergebracht (siehe dazu die W3C Spezifikation zu TouchEvent und Touch). Im Endeffekt benötigen sie also separaten Code, um Maus- und Finger-Bewegungen in JavaScript zu bearbeiten.

  1. /* Pseudo-Code: Koordinaten in Maus- und Touch-Events */
  2.  
  3. foo.addEventListener('mousemove', function(e) {
  4.     ...
  5.     /* Bei Maus-Events sind die Koordinaten
  6.        direkt im Event-Objekt vorhanden */
  7.     posX = e.clientX;
  8.     posY = e.clientY;
  9.     ...
  10. }, false);
  11.  
  12. foo.addEventListener('touchmove', function(e) {
  13.     ...
  14.     /* Bei Touch-Events muss man mit
  15.        TouchList-Arrays arbeiten, selbst
  16.        wenn man nur einen einzigen Touch-Punkt
  17.        bearbeiten moechte */
  18.     posX = e.targetTouches[0].clientX;
  19.     posY = e.targetTouches[0].clientY;
  20.     ...
  21. }, false);

Pointer-Events bieten hingegen einen Abstraktionslayer, der alle möglichen Inputs (Maus, Touch, Stylus) in ein einheitliches Event-Modell unterbringt. Die neuen Events sind den traditionellen Maus-Events sehr ähnlich: pointerenter, pointerover, pointerdown, pointermove, pointerup, pointerout, pointerleave. Darüber hinaus gibt es im Pointer-Event-Modell noch einige spezielle Events – pointercancel, gotpointercapture, lostpointercapture – auf die in dieser Einführung aber nicht weiter eingegangen wird.

Somit ermöglichen Pointer-Events, den Code relativ zu vereinfachen, indem Entwickler nur noch auf eine generelle Art von Events reagieren müssen:

  1. /* Pseudo-Code: zur Bearbeitung von Maus-, Finger- und Stylus-Bewegungen
  2.    brauchen im Pointer-Event Modell nur eine einzige Funktion */
  3.  
  4. foo.addEventListener('pointermove', function(e) { ... }, false);

Im Gegensatz zu Touch-Events, die ein komplett eigenes Event-Objekt haben, sind Pointer-Events an sich nur eine Erweiterung von klassischen Maus-Events. Jegliche Berührungspunkte (Finger oder Stylus auf einem Touchscreen) und Maus-Interaktionen generieren ein Event-Objekt, das alle normalen Maus-Events Properties beinhaltet (inklusive der individuellen Koordinaten-Paare). Dazu kommen weitere Properties, die den Events mehr Informationen zum Input mitliefern (siehe dazu die W3C Spezifikation zur PointerEvent interface). Somit ist es einfach, älteren Code – der speziell auf Maus-Events abgerichtet war – unmodifiziert auch für Pointer-Interaktionen einzusetzen. In den meisten Fällen reicht es aus, anstatt auf Maus-spezifische Ereignisse wie mousemove einfach den gleichen JavaScript bei pointermove auszuführen.

  1. /* Pseudo-Code: zur Bearbeitung von Maus-, Finger- und Stylus-Bewegungen
  2.    brauchen Entwickler im Pointer-Event-Modell nur eine einzige Funktion, und jeder
  3.    Pointer-Event enthält direkt die Koordinaten, genau wie bei Maus-Events */
  4.  
  5. foo.addEventListener('pointermove', function(e) {
  6.  
  7.     /* "e" beinhaltet unter anderem die folgenden Properties:
  8.  
  9.         (traditionelle Maus-Events
  10.          http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-MouseEvent)
  11.         readonly attribute long             screenX;
  12.         readonly attribute long             screenY;
  13.         readonly attribute long             clientX;
  14.         readonly attribute long             clientY;
  15.         readonly attribute boolean          ctrlKey;
  16.         readonly attribute boolean          shiftKey;
  17.         readonly attribute boolean          altKey;
  18.         readonly attribute boolean          metaKey;
  19.         readonly attribute unsigned short   button;
  20.         readonly attribute EventTarget      relatedTarget;
  21.  
  22.         (Maus-Event Erweiterungen
  23.          http://www.w3.org/TR/cssom-view/#extensions-to-the-mouseevent-interface)
  24.         readonly attribute long             pageX;
  25.         readonly attribute long             pageY;
  26.         readonly attribute long             x;
  27.         readonly attribute long             y;
  28.         readonly attribute long             offsetX;
  29.         readonly attribute long             offsetY;
  30.  
  31.         (Pointer-Event-spezifische Erweiterungen
  32.          http://www.w3.org/TR/pointerevents/#pointerevent-interface)
  33.         readonly attribute long             pointerId;
  34.         readonly attribute long             width;
  35.         readonly attribute long             height;
  36.         readonly attribute float            pressure;
  37.         readonly attribute long             tiltX;
  38.         readonly attribute long             tiltY;
  39.         readonly attribute DOMString        <strong>pointerType</strong>;
  40.         readonly attribute boolean          <strong>isPrimary</strong>;
  41.     */
  42.     ...
  43.     posX = e.clientX;
  44.     posY = e.clientY;
  45.     ...
  46. }, false);

Falls eine Applikation trotzdem speziell auf verschiedene Inputs anders reagieren soll (um zum Beispiel ein Hover-Menu nur für Maus-User anzuzeigen), erlauben Pointer-Events, die Quelle/Art des Events mittels der pointerType Property abzufragen.

  1. /* Pseudo-Code: verschiedenen Code je nach pointerType ausführen */
  2. switch (e.pointerType) {
  3.     case "mouse":
  4.         ...
  5.         break;
  6.     case "pen":
  7.         ...
  8.         break;
  9.     case "touch":
  10.         ...
  11.         break;
  12. }

Somit erlauben Pointer-Events einen »best of both worlds«-Ansatz: meistens ist es eigentlich egal, wie ein User mit einer Web-Applikation interagiert – ob ein Link oder Button per Maus oder Touch aktiviert wurde. Da in allen Fällen die gleichen Pointer-Events abgefeuert werden, ist es somit möglich, »Input-agnostischen« Code zu schreiben. Trotzdem können die verschiedene Input-Arten im Code differenziert werden.

»Feature detection« für Pointer-Events

Um festzustellen, ob ein Browser Pointer-Events unterstützt, reicht ein einfacher Feature-Detect:

  1. if (window.PointerEvent) {
  2.     /* Browser mit Pointer-Events */
  3. }

Man beachte hier, dass Pointer-Events nicht nur für Touchscreens benutzt werden – selbst auf nicht-touch-fähigen Geräten können Pointer-Events benutzt werden, um auf traditionelle Maus/Trackpad-Inputs zu reagieren.

Pointer-Events definieren auch eine separate navigator.maxTouchPoints Property, die die (Hardware-bedingte) maximale Anzahl an Touch-spezifischen Berührungspunkte angibt. Um nun zu testen, ob der Browser auf einem Gerät mit Touch läuft, können Entwickler einen separaten Feature-Detect benutzen:

  1. if (navigator.maxTouchPoints > 0) {
  2.     /* Browser auf einem Touch-fähigen Gerät */
  3. }

Grundlagen zur Bearbeitung von Pointer-Events

Wie schon bei der Abhandlung zu Touch-Events besprochen, sind Browser von Haus aus darauf ausgerichtet, existierende Webseiten so gut wie möglich auf Handys und Tablets darzustellen und benutzbar zu machen. Und da die meisten Seiten im Web immer noch auf Maus-Interaktionen ausgerichtet sind, werden selbst in Internet Explorer 10+ auch für Touchscreen-Interaktion traditionelle Maus-Events simuliert.

Beispiel 1 – Simulierte Maus-Events in Windows Phone 8 / IE10
Beispiel 1 – Simulierte Maus-Events

So ähnlich wie es auch in Chrome, Safari, Firefox und co. der Fall ist, werden bei einem »Tap« die folgenden Events abgefeuert:

mouseover > mouseenter > mousedown > mousemove > focus > mouseup > mouseout > mouseleave > click

Man beachte, dass diese Events in dieser Reihenfolge praktisch ohne Zwischenpausen abgesandt werden.

Wenn eine Webseite auf spezifische Events wie click oder mouseover reagiert, wird diese Seite in den meisten Fällen ohne jegliche Veränderung auch in IE10+ auf Touch-Geräten benutzbar sein. Es gibt aber, wie auch bei Touch-Event-fähigen Browsern, einige wohlbekannte Einschränkungen, wenn sich Entwickler nur auf Maus-Events verlassen – unter anderem, eine spürbare 300ms-Verzögerung bei Touch-Interaktionen, bevor der click Event abgesandt wird, und die Tatsache, dass Fingerbewegungen auf einem Touchscreen nicht – wie man es von einer normalen Maus gewohnt ist – mousemove Events generiert.

Pointer-Events und simulierte Maus-Events

Die Event-Reihenfolge im Touch-Events-Modell sieht folgendermaßen aus:

touchstart > [touchmove]+ > touchend > [300ms delay] > mouseover > mousemove > mousedown > (focus) > mouseup > click

Zuerst werden die Touch-spezifischen Events abgesandt. Dann, nach dem klassichen 300ms Delay, werden die simulierten Maus-Events und zuletzt der click abgefeuert.

Im Pointer-Events-Modell, hingegen, werden die Pointer- und Maus-Events folgendermaßen abgesandt:

pointerover > mouseover > pointerenter > mouseenter > pointerdown > mousedown > [pointermove > mousemove]+ > focus > pointerup > mouseup > pointerout > mouseout > pointerleave > mouseleave > [300ms delay] > click

Im IE10 (immer noch der Standard-Browser auf Windows Phone 8) sind Pointer-Events mit einem Vendor-Prefix versehen. Somit sehen die verschiedenen Events in dieser Version so aus: MSPointerOver, MSPointerEnter, MSPointerDown, MSPointerMove, MSPointerUP, MSPointerOut, MSPointerLeave. In IE11 sind die Events gemäß der W3C Spezifikation ohne Prefix implementiert.

Verkürzte Reaktionszeit bei click Events

Wie schon letztes Jahr gesehen, gibt es bei Touchscreens in der Regel eine Verzögerung von ungefähr 300ms zwischen dem Moment, in dem der Finger bei einem »Tap« den Touchscreen verlässt, und dem click Event. Um bei Touch-Events diese Verzögerung zu umgehen, müssen sich Entwickler traditionell mit extra Code auseinandersetzen, der sowohl auf touchend (der direkt vor der Verzögerung ausgeführt wird) als auch auf click reagiert, und mittels preventDefault() verhindern, dass die gleiche Funktion doppelt abläuft – einmal für den Touch-Event, und danach für die simulierten Maus-Events.

  1. foo.addEventListener('click', function(e) { ... }, false);
  2. foo.addEventListener('touchend', function(e) {
  3.   ...
  4.   e.preventDefault();
  5. }, false);

Im Pointer-Event-Modell ist die 300ms Verzögerung immer noch vorhanden.

Beispiel 2 – 300ms Verzögerung vor dem click Event auch in Windows Phone 8 / IE10
Beispiel 2 – 300ms Verzögerung vor dem click Event

Zwar könnte man hier einen ähnlichen Ansatz nehmen und auf pointerup und click Events hören. Einziger Haken: Bei Pointer-Events hat preventDefault weder auf die simulierten Maus-Events (die, wie gesehen, »inline« zusammen mit den verschiedenen Pointer-Events abgesandt werden), noch auf den click, einen Effekt.

Pointer-Events haben aber hier eine viel einfachere Alternative. Die Pointer-Events Spezifikation definiert eine neue CSS Property touch-action, mit der es möglich ist, dem Browser explizit mitzuteilen, welche Touchscreen-Interaktionen (und deren »Default« Bearbeitung vom Browser) zugelassen sind.

  1. touch-action: auto | none | pan-x | pan-y
  • auto: der Browser kümmert sich um alle Touch-Gesten, inklusive »double-tap to zoom« (und damit die 300ms Verzögerung).
  • none: alle Standard-Touch-Gesten sind unterdrückt.
  • pan-x und pan-y: überlässt dem Browser auschließlich horizontales oder vertikales Scrolling.

Wie auch mit preventDefault ist hier Vorsicht geboten: touch-action:none unterdrückt nicht nur unerwünschtes Verhalten wie die 300ms Verzögerung, sonder auch andere Interaktionen wie Zooming und Scrolling. Aus diesem Grund sollte diese CSS Property wirklich nur gezielt auf die nötigen Elemente (wie zum Beispiel Links und Buttons), und nicht auf die gesamte Webseite, angewandt werden

Anstatt komplexe JavaScript Routinen einzusetzen, können die 300ms recht elegant mittels einer einzige extra Zeile im CSS unterdrückt werden.

  1. button { touch-action: none; }
Beispiel 3 – touch-action:none unterdrückt 300ms Verzögerung in Windows Phone 8 / IE10
Beispiel 3 – touch-action:none unterdrückt 300ms Verzögerung

Finger-Bewegungen verfolgen

Ein weiteres Problem, das schon im letztjährigen Artikel besprochen wurde, ist die Tatsache, dass Finger-Bewegungen auf einem Touchscreen nicht direkt mittels mousemove verfolgt werden können – sobald sich der Finger mehr als nur ein paar Pixel über den Touchscreen bewegt, wird dies als eine Scroll-Geste interpretiert, und die simulierten Maus-Events werden nicht abgefeuert.

Um Finger-Bewegungen per JavaScript zu verfolgen, müssen Entwickler deshalb im Touch-Event Modell direkt auf touchmove Events reagieren, und mittels preventDefault das Scrollen im Browser unterdrücken.

Das gleiche Problem mit mousemove ist auch in Browsern mit Pointer-Events vorhanden. Als Beispiel dient hier wieder eine Canvas-basierte Spielerei:

Beispiel 4 – Canvas-Spielerei – nur Maus-orientiert; funktioniert nur teilweise auf Windows 8.1/ IE11
Beispiel 4 – Canvas-Spielerei – nur Maus-orientiert

Dieses Beispiel funktioniert einwandfrei mit der Maus. Wenn man aber auf einem Touchscreen den Finger über den Canvas bewegt, wird die Bewegung nur für ein paar Pixel verfolgt … danach interpretiert der Browser die Bewegung als Scrolling.

Als ersten Schritt sollten Entwickler echte Pointer-Events, und nicht die simulierten Maus-Events, einsetzen. Anstatt auf mousemove werden die Event-Listener also auf pointermove gesetzt.

Die nächsten Beispiele funktionieren nur in Browsern mit Pointer-Events-Unterstützung – zur Zeit also nur Internet Explorer 10+

Beispiel 5 – Canvas-Spielerei mit pointermove

Der Effekt ist leider immer noch der gleiche: selbst bei pointermove wird eine Finger-Bewegung als Scroll vom Browser abgefangen. Wie auch mit der 300ms-Verzögerung können Entwickler aber bei Pointer-Events direkt im CSS angeben, ob sich der Browser wie gewohnt um das Scrolling kümmern soll, oder ob Berührungen direkt per Scripting bearbeitet werden sollen. Somit reicht es aus, wieder eine einzige CSS-Zeile zum Beispiel hinzuzufügen:

  1. canvas { touch-action: none; }

Beispiel 6 – Canvas-Spielerei mit pointermove und touch-action:none

Dieses Beispiel funktioniert nun schon recht gut. Bewegungen werden sowohl mit dem Finger als auch mit einer Maus registriert, und von dem gleichen Code bearbeitet. Eine Tücke hat es aber noch: das Beispiel ist von Grund auf nur auf ein einziges Koordinaten-Paar ausgerichtet – wenn mehr als ein Finger auf dem Canvas ist, wird immer nur der Berührungs-Punkt, der sich zuletzt bewegt hat, von dem Script wahrgenommen.

Um genauer zu sein, kommt das Problem bei dem vorherigen Beispiel nicht nur mit mehreren Fingern auf dem Touchscreen vor – die gleiche Problematik tritt auf, wenn mehr als ein Pointer (sei es ein Finger, Stylus oder eine Maus) gleichzeitig benutzt werden. Der Einfachheit halber geht der Beispiel-Code davon aus, dass ein User diese verschiedenen Inputs nicht gleichzeitig verwenden wird.

Um nur den »ersten« Finger auf dem Touchscreen zu verfolgen, muss der Code leicht erweitert werden, um nur auf den »primären« Pointer zu reagieren.

  1. /* Pseudo-Code: Koordinaten nur für den primären Pointer erfassen */
  2.  
  3. foo.addEventListener('pointermove', function(e) {
  4.     ...
  5.     if (e.isPrimary) {
  6.         ...
  7.         posX = e.offsetX;
  8.         posY = e.offsetY;
  9.         ...
  10.     }
  11.     ...
  12. }, false);
Beispiel 7 – Canvas-Spielerei mit pointermove, touch-action:none und isPrimary Check; funktioniert komplett auf Windows Phone 8 / IE10
Beispiel 7 – Canvas-Spielerei mit pointermove, touch-action:none und isPrimary Check

Fazit

Im Gegensatz zu traditionellen Maus- und Touch-Events erlauben Pointer-Events, Web-Applikationen auf elegante Weise auf verschiedene Input-Methoden auszulegen, ohne spezielle »Weichen« (für Maus, Touch, Stylus, usw.) in den Code packen zu müssen.

Momentan sind diese Events leider nur in Internet Explorer 10+ vorhanden – aber Unterstützung in anderen Browsern wird voraussichtlich bald kommen (siehe die aktiven Bugs für Chromium/Blink und Firefox – obwohl es momentan noch unsicher ist, ob Webkit auch nachziehen wird). Um trotzdem schon heute das vereinfachte Pointer-Event-Modell einzusetzen, gibt es schon recht stabile Polyfills wie HandJS und Polymer's PointerEvents.

Weiterführende Links

Die Kommentare sind geschlossen.