🏠 Pointer Events

By Robert Laing

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

Via JavaScript’s Element.addEventListener(type, listener), events such as keyboard input or mouse clicks can get dispatched to listener handlers.

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

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

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

Setting ondragstart to false is to disable the default drag and drop event which I don’t want to use because it assumes mouse events, so doesn’t work on mobile phones.

The drag event’s ev.dataTransfer.setData("text/plain", ev.target.innerText); can be done more simply using an object literal which the pointer event handlers can all access by making it global in their module.

/**
 * 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.