🏠 Pointer Events

By Robert Laing

Device-agnostic drag-and-drop

One of my goals writing these games was to have the same code work on a mobile phone using a touchscreen or a desktop with a mouse. It turns out JavaScript has pointer events for exactly that:

The Pointer Events specification defines a unified hardware-agnostic framework for handling input from various devices, including mice, touchscreens, and pens/styluses. By providing a single set of events (e.g., pointerdown, pointermove, pointerup), it allows developers to support diverse input methods without writing device-specific logic for each. — W3C Specification

Using pointer events to drag and drop images turned out to be easy if you know how, which I didn’t before going down the cul-de-sac of JavaScript’s legacy drag and drop events. So I made these notes to help others avoid that mistake.

Why not use drag and drop events?

The first snag with JavaScript’s DragEvent is it assumes a mouse, so doesn’t work on touchscreens. Another reason I don’t like default drag events is if the dragged object is an image, it automagically gets resized and made transparent, whereas I want to create the illusion the original image is being moved.

But even though I’m not using the default drag event API, Mozilla’s Kanban board example makes a good starting point.

function dragstartHandler(dragEvent) {
  dragEvent.target.id = "dragged-task";
  dragEvent.dataTransfer.effectAllowed = "move";
  dragEvent.dataTransfer.setData("task", "");
}

document.addEventListener("ondragstart", dragstartHandler);

The drag event has a dataTransfer property which is a DataTransfer Object which is a DataTransferItemList of DataTransferItems which is… , uhm, completely overcomplicated crap easily replaced by a simple object literal I call dragObject.

Listening to everything

JavaScript would make event-driven programing really easy courtesy of Element.addEventListener(type, listener) if not for the overwhelming number of event types, many of which are nearly synonymous with very subtle differences.

The bottom of my main “client” script files look like this:

document.addEventListener("DOMContentLoaded", startGame);
DOM.newgame.addEventListener("click", newGame);
document.addEventListener("pointerdown", pointerdownHandler);
document.addEventListener("pointermove", pointermoveHandler);
document.addEventListener("pointerup", pointerupHandler);
document.addEventListener("pointercancel", pointerupHandler);
document.addEventListener("ondragstart", () => false);

My way of adding the event listener to the document rather than individual HTML elements goes agains what is considered best practice. For instance, back to the Kanban example:

const columns = document.querySelectorAll(".task-column");

columns.forEach((column) => {
  column.addEventListener("dragover", (event) => {
    // Test a custom type we will set later
    if (event.dataTransfer.types.includes("task")) {
      event.preventDefault();
    }
  });
});

A danger with adding, say, pointerdown to everything in the document is users of your website may find they can longer scroll or pinch the screen to zoom in — both a bug and a feature is elements listen to lots of events, some of which are selected by you, others not, requring clutter like document.addEventListener("ondragstart", () => false); and pointerEvent.preventDefault();.

My rationale for adding handlers for desired events to the entire document and then placing filters inside their handlers is that in games, sometimes I want an image to be draggable if it’s a legal move, other times not.

Thanks to event.target, a listener’s handler can identify which HTML element triggered it, whether event.target.localName equals “img”, or whether event.target.tagName equals “IMG” thanks to JavaScript’s tradition of cluttering itself with pointless synonyms.

Where to start?

My staring point for an event-driven program looks something like:

function startGame() {
  game.start(state);
  render();
}

document.addEventListener("DOMContentLoaded", startGame);

I settled on DOMContentLoaded somewhat randomly since load is nearly identical, except apparently DOMContentLoaded has less delay as far as I understand.

Using either of these to launch the starting main function makes iifes unnecessary in contemporary JavaScript, though some content management systems still rewrite JavaScript files that way, which is why this site is handcoded.

As explained in the next section, the startGame callback receives an event object as a parameter from the listener. But since it doesn’t use it, I leave it out. The pointer handlers, however, do need data from these objects.

Handling Callbacks

The “event handlers” called by listeners are what the Lisp community call higher-order functions (aka first-class functions), most commonly used in list-processing for things like map, reduce, and filter.

The core idea is that function names are just variable names, and can be passed as arguments to other functions just as any other variable.

When I first encountered them, I wondered where the round brackets with the required arguments (aka paramaters) vanished? It turns out that just as a “normal variable” gets substituted into a number or text, a variable whose type is function gets substituted into something like:

function (arg1, arg2, arg3, ...) {
  // function body goes here
}

How many arguments the callback receives and in what orders varies. For instance, if used as in myObj["callbackName"] with const myObj = {get callbackName() {...} }, no argument is passed. If used as in myArr.reduce(callbackName), up to three arguments are passed to function (accumulator, currentValue, index).

For event listener callbacks, one argument is passed, an event object. You can call it anything, but for the sake of self-documenting code, I like to state what specific type of event a handler receives to make looking up reference material later easier:

function pointerdownHandler(pointerEvent) {
  ...
}

Since the amount of information passed to a callback is limited, whatever additional data required needs to be stored in a free variable — I like global singletons. Refering to JavaScript listener handlers as callbacks is a misnomer since they don’t return anything. My way of using them is to have them mutate variables in a global singleton.

How pointer differ from mouse events

PointerEvents inherit their properties from MouseEvents which in turn inherit properties from Event. This inheritance model adds up to an enormously confusing selection of properties and methods, of which I only use a few:

event
├── target
├── type
└── mouseEvent
    ├── x
    ├── y
    └── pointerEvent

pointerEvent.x is an alias for pointerEvent.clientX, one of the bewildering number of co-ordinate systems such as pointerEvent.offsetX, pointerEvent.pageX, pointerEvent.screenX, and pointerEvent.layerX, which all have corresponding Y versions.

Something I didn’t think of initially is that on a smartphone, people can use several fingers to drag more than one card simultaniously. There may be reasons to allow that, but since I’m trying to use the same code for a mouse or finger, a simple way to fix that was having the pointerdown handler check three things:

  1. dragObject.moving hasn’t been set to true by another ponterdown event.
  2. The dragged object is an image, which can be checked using either localName or tagName, depending on whether you prefer lowercase or uppercase.
  3. The dragged object isn’t a blank, which I use as null for images.

Since I want to keep the original image’s size, dragObject.clone = pointerEvent.target.cloneNode();. This replaces pointerEvent.target, leaving it free to represent whatever is under the dragged image — the next card in a deck or blank.

By tagging a “not convertible to JSON” type (a DOM element) to dragObject, I lose the ability to store it in localStorage, which is why I don’t simply add its properties to state. The idea is dragObject holds ephemeral data which only updates the state once a legal move is completed via pointerdown. (There should probably be an undo option then, but I’ve not got that advanced yet).

My way of replacing the default drag event seemed a bit of a hack, especially using elementFromPoint to figure out the drop target. After figuring out things myself, I found the Drag’n’Drop with mouse events example on javascript.info which uses the same technique.

Its example uses mousedown, mousemove, and mouseup, which translate nearly directly to pointerdown, pointermove, and pointerup.

Capture and Release

As explained in Mozilla’s Capturing the Pointer, section, setting Element.setPointerCapture(pointerId) on the pointerEvent.target in the pointerdownHandler means we don’t have to worry about pointerover, pointerenter, pointerleave, and pointerout.

Though ELement.releasePointerCapture(pointerId) is done by pointerup anyway, the examples include it in the ponterupHandler to make this explicit.

Don’t forget touch-action: none;

An application using Pointer events will receive a pointercancel event when the browser starts handling a touch gesture. By explicitly specifying which gestures should be handled by the browser, an application can supply its own behavior in pointermove and pointerup listeners for the remaining gestures. — Mozilla’s touch-action article.

Unless the CSS stylesheet contains something like:

.tableau > div { /* Child combinator */
  touch-action: none;
}

smartphone browsers will bring up irritating popups when the user attempts to drag things with their finger.