Web Platform Javascript

Events revisited

Now that we've introduced events and DOM manipulations, we can revisit events and flesh out the examples some more.

Keyboard events

The keyboard events are keyup, keydown and keypress, all which send the same event data. To find out which key is pressed, use key or keyCode. It's easiest to experiment pressing the keys to find out what data each one yields.

Also useful are a set of booleans which track whether modifier keys are pressed, such as the shift or control keys. These properties are: ctrlKey, shiftKey, altKey and metaKey.

Try this live demo to explore different key events.

Note that it depends on what element has 'focus' as to whether it will trigger keyboard events or not. Elements like a text INPUT will gobble up keyboard events.

Below is an example that uses shiftKey and key properties of the event data to move an element around according to the cursor keys.

HTML

<div id="result">
Try your cursor keys
</div>
<div id="box"></div>

CSS

#box {
  position: fixed;
  width: 1rem;
  height: 1rem;
  top: 0px;
  left: 0px;
  background-color: DeepPink;
}

JS

document.addEventListener('keydown', (e) => {
    let speed = 1;
    if (e.shiftKey) speed = 10;

    switch (e.key) {
    case "ArrowLeft":
      update("Left");
      move(-1*speed, 0);
      break;
    case "ArrowRight":
      update("Right");
      move(1*speed, 0);
      break;
    case "ArrowUp":
      update("Up");
      move(0, -1*speed);
      break;
    case "ArrowDown":
      update("Down");
      move(0, 1*speed);
      break;
    default:
      update("!?");
    }
});

function update(m) {
    document.getElementById('result').innerHTML = m;
}

function move(x, y) {
    const el = document.getElementById('box');
    el.style.left = (el.offsetLeft + x) + 'px';
    el.style.top = (el.offsetTop + y) + 'px';
}

Try the live demo

Pointer events

Pointer events allow for any kind of interaction with a mouse, touch or stylus to be handled uniformly. On some devices, you can pick up the pressure of touch points, or for stylus input, pressure as well as the tilt of the pen and so forth.

The events are: pointerover, pointerenter, pointerdown, pointermove, pointerup, pointercancel, pointerout and pointerleave.

These events all fire PointerEvent data. Useful properties are:

  • pointerId: Id of pointer causing event, useful for following individual fingertips
  • clientX/clientY: coordinates
  • offsetX/offsetY: coordinates in relation to event target
  • pageX/pageY: coordinates in relation to document
  • screenX/screenY: coordinates in relation to screen
  • movementX/movementY: amount of movement since the last mousemove
  • pointerType: Commonly "mouse" or "touch"

Try the live demo

Like the keyboard events described above, the pointer events also include booleans for whether the user is holding a modifier key during the action. These properties are: altKey, ctrlKey, metaKey and shiftKey.

Read more

Interacting with elements

To associate the pointer with an element there are a few approaches.

The first is to attach your event handler to an element which contains all the elements you're interested in tracking, and then using the event data's target property to figure out which one was interacted with.

The below example listens to events on the SECTION element, rather than listening on each individual DIV. Element attributes are useful to associate visuals with data. For example, numeric attribute values could map to an array of data. In this case though, we just print out the value, and toggle a CSS class to show which element was the target.

<section>
  <div data-attr="0">One</div>
  <div data-attr="1">Two</div>
  <div data-attr="2">Three</div>
</section>
function handle(e) {
  const attr = e.target.getAttribute('data-attr');

  // If there's no data-attr, the event didn't come from a child DIV but the SECTION. Igore these.
  if (attr == null) return;

  document.getElementById('log').innerText = ` ${e.type} fired with target ${attr}`;

  // 'Select' DIV if clicked
  if (e.type == "pointerup") {
    if (!e.target.classList.contains('active')) {
      // Remove the 'active' class from any other element
      var x = document.getElementsByClassName('active');
      for (var withClass of x) withClass.classList.remove('active');

      // Add it to the one that's the current event target
      e.target.classList.add('active');
    }
  }
}

// Listen to pointer events that happen within the section
const el = document.querySelector('section');
el.addEventListener("pointerup", handle);
el.addEventListener("pointerdown", handle);
el.addEventListener("pointermove", handle);

Try the live demo

Another approach is to use hit testing - using the coordinates of the event to figure out what elements are relevant.

document.elementFromPoint returns the top-most element for a viewport coordinate. You can also calculate which elements intersect a coordinate by iterating through a list of elements you care about and checking their bounding rectangle.

// Returns TRUE if x,y is within rect
const within = (x, y, rect) => {
    if (x < rect.left || y < rect.y) return false;
  if (x > rect.left + rect.width) return false;
  if (y > rect.top + rect.height) return false;
    return true;
}

// Listen to pointer events that happen anywhere on document
document.addEventListener('pointermove', (e) => {
     // Remove the 'active' class from all elements
  const x = document.getElementsByClassName('active');
  for (let withClass of x) withClass.classList.remove('active');

  // Enumerate all children of the section
  for (let child of document.querySelector('section').children) {
    const r = child.getBoundingClientRect();
    if (within(e.clientX, e.clientY, r)) {
        // Overlapping! Add 'active' class to show it
        child.classList.add('active');
    }
  }
});

Try the live demo

This is only a simple example and doesn't handle all situations, mind you.