Combining events with "gestures"

Attempts to create a way to listen for "events" (things that can be canceled) and "gestures" (things that can't be canceled)...

by David Iglesias

HTML

<div class="eventSource" id="eventSource">
  Combining events with "gestures".
  <div class="scrollable">
    This content is scrollable!
  </div>
</div>

CSS

* { box-sizing: border-box; }
html, body {
  display: flex;
  width: 100%; height: 100%;
  margin: 0; padding: 0;
}

.eventSource {
  width: 320px;
  height: 320px;
  background: #fabada;
  position: relative;
}

.scrollable {
  position: absolute;
  top: 40px;
  left: 40px;
  height: 180px;
  width: 140px;
  border: 1px solid black;
  line-height: 1000px;
  overflow-y: scroll;
}

JavaScript

// Gestures begin with a pointerdown...
eventSource.addEventListener('pointerdown', startGesture);
eventSource.addEventListener('pointermove', rateLimited(continueGesture, 500));
eventSource.addEventListener('pointerup', finishGesture)
eventSource.addEventListener('pointercancel', finishGesture);
eventSource.addEventListener('pointerleave', finishGesture);
// Some events must cancel gestures
eventSource.addEventListener('contextmenu', finishGesture);
// Other events cancel gestures through `pointercancel`
eventSource.addEventListener('drag', unlessInGesture(rateLimited(log())));
// High level events that we care about
eventSource.addEventListener('click', unlessInGesture(log()));
eventSource.addEventListener('contextmenu', unlessInGesture(log()));
eventSource.addEventListener('pointermove', unlessInGesture(rateLimited(log())));
// Is scroll a gesture?
eventSource.querySelector('.scrollable').addEventListener('scroll', startGesture);
eventSource.querySelector('.scrollable').addEventListener('scrollend', finishGesture);
// Or a 'wheel' event?
eventSource.querySelector('.scrollable').addEventListener('wheel', unlessInGesture(rateLimited(log())));

// Run [handler] unless [inGesture].
function unlessInGesture(handler) {
  return (event) => {
  	if (inGesture) return;
    return handler(event);
  }
}

// Run [handler] unless it's happened earlier than [ms] ago.
function rateLimited(handler, ms = 250) {
  let timeout;
  return (event) => {
  	if (!timeout) {
      handler(event);
      timeout = window.setTimeout(() => {
      	timeout = null;
      }, ms);
    }
  }
}

// Prepare an [event] handler that logs with a [what] prefix.
function log(what = 'EVENT') {
	return (event) => {
    console.log(what, event.type, event.x, event.y);
  }
}

// A timeout to detect composite pointer events
let detectingGesture;
let gestureDetectionDelayMs = 100;
let gestureFinishDelayMs = 20;

// Booleans to decide if we're handling a gesture or not
let inGesture = false; // Used by...