🏠 Pointer Events

By Robert Laing

  1. Device-agnostic drag-and-drop
  2. Why not use drag and drop events?
  3. Event-driven programing
  4. Event listeners and handlers
  5. How pointer differ from mouse events
  6. What is an element?

Learning coding is like playing cards — you learn the rules, then you play, then you go back and learn the rules again, then you play again. — Mozilla’s Web Audio API page

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 so first went down the cul-de-sac of JavaScript’s legacy drag and drop event. 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. The next snag is that if repaced with a PointerEvent, it still messes things up so needs to be explicitly removed in the event listeners section of our code:

document.addEventListener("ondragstart", () => false);

Even though I’m not using the default drag event API, Mozilla’s Kanban board 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.

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.

My way of replacing the default drag event seemed a bit of a hack, especially using elementFromPoint to figure out the drop target, until I saw the Drag’n’Drop with mouse events example on javascript.info.

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

Event-drivent programing

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.

For instance, my staring point for any program is 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.

Event listeners and handlers

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);

Event listeners only take one argument, an Event or a subclass of Event, which is automatically passed to the listener, and the return value is ignored. Therefore, to get data into and out of an event listener, instead of passing the data through parameters and return values, you need to create closures instead. — Mozilla’s Getting data into and out of an event listener

How pointer differ from mouse events

If you use pointer events, you should call preventDefault() to keep the mouse event from being sent as well.

What is an element?

The types of events is fairly overwhelming, so I’ve focused on just using

/**
 * Data is moved from pointerup to pointerdown via this object.
 * @namespace {Object} dragObject
 * @property {Object} clone - HTML img element getting dragged
 * @property {string} fromID - Start div's ID.
 * @property {string[]} legals - IDs of legal move targets.
 * @property {boolean} moving - Set to true by pointer down and false by pointer up.
 * @property {number} shiftX - Pixels to offset x.
 * @property {number} shiftY - Pixels to offset y.
 * @property {?string} toID - End div's ID or null.
 */
const dragObject = {moving: false};

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).

/** 
 * @function pointerdownHandler
 * @listens pointerdown
 */
function pointerdownHandler(pointerEvent) {
  if ( !dragObject.moving &&
       pointerEvent.target.localName === "img" &&
       !pointerEvent.target.src.includes("blank") 
     ) {
    dragObject.fromID = getId(pointerEvent.target);
    pointerEvent.preventDefault();
    dragObject.moving = true;
    dragObject.toID = null;
    DOM[dragObject.fromID].setPointerCapture(pointerEvent.pointerId);
    dragObject.clone = pointerEvent.target.cloneNode();
    dragObject.clone.style.position = "absolute";
    dragObject.clone.style.left = `${DOM[dragObject.fromID].offsetLeft}px`;
    dragObject.clone.style.top = `${DOM[dragObject.fromID].offsetTop}px`;
    dragObject.clone.style["z-index"] = 12;
    document.body.append(dragObject.clone);
    dragObject.shiftX = pointerEvent.clientX - dragObject.clone.getBoundingClientRect().left;
    dragObject.shiftY = pointerEvent.clientY - dragObject.clone.getBoundingClientRect().top;
    dragObject.clone.style.filter = `drop-shadow(3px 3px hsl(120 100 20 / 80%))`;
    pointerEvent.target.src = "./cards/blank.svg";
    highlightLegals();
  } 
}

Note at this stage I’m only changing things in the DOM, not the game state which only gets updated on the completion of a legal move.