MarkUs: Touch Annotations

PR #7736 — Enabling annotation drawing on touch devices (iPad, Surface, Android tablets).


Problem

MarkUs’s annotation system allows graders to draw bounding boxes on student submissions (PDFs and images) and attach feedback. The implementation used mouse events exclusively (mousedown, mousemove, mouseup), making annotation impossible on touch devices. As more instructors and TAs grade on tablets, this became a significant usability gap.


Touch vs Mouse Event Model

Touch events differ from mouse events in several ways that prevent a naive mousedown → touchstart replacement:

PropertyMouse EventsTouch Events
Coordinate accessevent.clientX, event.clientYevent.touches[0].clientX (array of touch points)
Default behaviorNone for most interactionsScroll, zoom, text selection
Event sequencemousedown → mousemove → mouseuptouchstart → touchmove → touchend (then synthesized mouse events)
Passive listenersN/Atouchmove is passive by default in modern browsers; calling preventDefault() requires {passive: false}
Multi-touchSingle pointerMultiple simultaneous touch points

The critical issue: touchmove triggers browser scrolling by default. Drawing an annotation box requires event.preventDefault() on touchmove to suppress scrolling while the user drags. Modern browsers (Chrome 56+) make touchmove listeners passive by default for scroll performance, so addEventListener('touchmove', handler, {passive: false}) is required.


Implementation

Shared Event Handling

The PDF annotation manager and image annotation manager had parallel but slightly different coordinate transformation logic. Rather than duplicating touch handling in both, the refactoring extracted shared helpers:

function getEventCoordinates(event) {
  if (event.touches && event.touches.length > 0) {
    return { x: event.touches[0].clientX, y: event.touches[0].clientY };
  }
  return { x: event.clientX, y: event.clientY };
}

This normalizes the coordinate interface across input modes, allowing the annotation drawing logic to be input-agnostic.

Coordinate Transformation

Annotation coordinates must be transformed from screen space (where the event occurs) to document space (where the annotation is stored):

  1. Screen coordinates from the touch/mouse event
  2. Element-relative coordinates via getBoundingClientRect() offset subtraction
  3. Document coordinates via scaling by the current zoom level and scroll offset

For PDFs, an additional transformation accounts for the PDF.js viewport transform (rotation, page-level offset). Getting this chain correct across both mouse and touch inputs required careful testing at multiple zoom levels and scroll positions.

Event Listener Registration

Switched from inline event handler attributes (ontouchstart="...") to addEventListener calls. This was necessary because:

  1. Inline handlers cannot specify {passive: false} for touchmove
  2. addEventListener supports multiple handlers on the same element
  3. Modern JavaScript style prefers explicit listener management for maintainability

Cross-Browser Issues

Safari’s touch event handling differs from Chrome in timing and coordinate reporting. Safari fires touchend before the final touchmove in some cases, causing annotation boxes to be slightly smaller than the user’s gesture. The fix: compute final box coordinates from the last recorded touchmove position rather than the touchend event coordinates.

Pointer Events API (pointerdown, pointermove, pointerup) unifies mouse, touch, and stylus input into a single event model. MarkUs’s legacy annotation code predates widespread Pointer Events support. A full migration to Pointer Events would eliminate the mouse/touch branching entirely, but was out of scope for this PR. The current implementation provides touch support while maintaining backward compatibility with existing mouse event handling.


Impact

Annotation drawing now works on iPads, Surface tablets, Android tablets, and touchscreen laptops. This unblocked mobile grading workflows for instructors who preferred grading on tablets during commutes or office hours, a use case that had been a known pain point for multiple semesters.