Skip to content

EventManager / createDoubleTapDetector

A lifecycle-scoped event registry backed by AbortController, plus a small pointer-type- agnostic double-tap detector for touch gestures.

The problem it solves is taking listeners back off. removeEventListener only works when you hand it the exact same function reference and options you registered with — wrap a handler in an arrow function on the way in and you can never remove it again. Components that mount and unmount repeatedly then leak a listener per cycle. AbortController turns all of that into a single abort().

Usage

In a Web Component

ts
import { EventManager } from 'ranuts/utils';

class MyElement extends HTMLElement {
  private _events = new EventManager();

  connectedCallback() {
    this._events.on(this._input, 'input', this.handleInput).on(this, 'click', this.handleClick, { capture: true });
  }

  disconnectedCallback() {
    this._events.abort(); // removes every listener, and resets for the next connect
  }
}

In ordinary page code

ts
function initSection(container: HTMLElement) {
  const scope = new EventManager();

  scope.on(input, 'input', handleSearch).delegate(container, '[data-action]', 'click', (ev, target) => {
    handleAction(target.getAttribute('data-action'));
  });

  return () => scope.abort(); // call when the section is torn down
}

API

on

Register a listener scoped to this manager. Chainable.

Parameters

ParameterDescriptionTypeDefault
targetThe event targetEventTargetRequired
typeEvent namestringRequired
handlerHandler functionEventListenerRequired
optionsaddEventListener options minus signalOmit<AddEventListenerOptions,'signal'>-

Return

ArgumentDescriptionType
thisThe manager, for chainingEventManager

delegate

Event delegation: attach one listener to parent and fire handler only when the event originated from a descendant matching selector. Chainable.

The handler receives the original event and the matched element.

ts
scope.delegate(list, '.item', 'click', (ev, item) => {
  console.log(item.getAttribute('data-id'));
});

Parameters

ParameterDescriptionTypeDefault
parentElement the single listener is bound toHTMLElementRequired
selectorSelector a descendant must matchstringRequired
typeEvent namestringRequired
handler(event, matchedElement) => voidFunctionRequired
optionsaddEventListener options minus signalOmit<AddEventListenerOptions,'signal'>-

Return

ArgumentDescriptionType
thisThe manager, for chainingEventManager

abort

Remove every registered listener and reset the internal AbortController. Safe to call more than once; subsequent on() / delegate() calls start from a clean scope.

Return

No return value (void)

signal

The underlying AbortSignal, in case you want to pass it to addEventListener yourself.

ArgumentDescriptionType
signalThe manager's abort signalAbortSignal

createDoubleTapDetector

Double-tap detection over raw (x, y, time) samples — pointer-type-agnostic, so it works the same whether it's fed from Pointer, Touch, or Mouse events. Built for touch gestures (double-tap to seek, to zoom, to like) where re-deriving the timestamp + distance threshold logic at every call site is easy to get subtly wrong: comparing only one axis, or forgetting to reset after a hit so three fast taps count as two overlapping double-taps.

ts
import { createDoubleTapDetector } from 'ranuts/utils';

const detector = createDoubleTapDetector();
el.addEventListener('pointerup', (e) => {
  if (detector.check(e.clientX, e.clientY)) seek();
});

createDoubleTapDetector(options?)

Parameters (DoubleTapDetectorOptions)

OptionDescriptionTypeDefault
windowMsMax gap between the two taps, in msnumber300
maxDistancePxMax 2D distance between the two taps, in pxnumber60

DoubleTapDetector

MemberDescriptionType
checkRecord a tap at (x, y) and report whether it forms a double-tap with the immediately preceding one. A detected double-tap resets tracking, so a third rapid tap starts a fresh pair rather than counting as part of the same double-tap.(x: number, y: number, now?: number) => boolean
resetForget the last recorded tap — call this when a gesture other than a tap (a drag) starts() => void

Released under the MIT License.