import { Controller } from '@hotwired/stimulus'; import debounce from 'debounce'; import invariant from 'tiny-invariant'; export type Detail = Record; // see: https://www.quirksmode.org/blog/archives/2008/04/delegating_the.html const FOCUS_EVENTS = ['focus', 'blur']; export class ApplicationController extends Controller { #debounced = new Map<() => void, ReturnType>(); protected debounce(fn: () => void, interval: number): void { this.globalDispatch('debounced:added'); let debounced = this.#debounced.get(fn); if (!debounced) { const wrapper = () => { fn.bind(this)(); this.#debounced.delete(fn); if (this.#debounced.size == 0) { this.globalDispatch('debounced:empty'); } }; debounced = debounce(wrapper.bind(this), interval); this.#debounced.set(fn, debounced); } debounced(); } protected cancelDebounce(fn: () => void) { this.#debounced.get(fn)?.clear(); } protected globalDispatch(type: string, detail?: T): void { this.dispatch(type, { detail: detail as object, prefix: '', target: document.documentElement }); } protected on( target: EventTarget, eventName: string, handler: (event: HandlerEvent) => void ): void; protected on( eventName: string, handler: (event: HandlerEvent) => void ): void; protected on( targetOrEventName: EventTarget | string, eventNameOrHandler: string | ((event: HandlerEvent) => void), handler?: (event: HandlerEvent) => void ): void { if (typeof targetOrEventName == 'string') { invariant(typeof eventNameOrHandler != 'string', 'handler is required'); this.onTarget( this.element, targetOrEventName, eventNameOrHandler, FOCUS_EVENTS.includes(targetOrEventName) ); } else { invariant(eventNameOrHandler == 'string', 'event name is required'); invariant(handler, 'handler is required'); this.onTarget(targetOrEventName, eventNameOrHandler, handler); } } protected onGlobal( eventName: string, handler: (event: HandlerEvent) => void ): void { this.onTarget(document.documentElement, eventName, handler); } private onTarget( target: EventTarget, eventName: string, handler: (event: HandlerEvent) => void, capture?: boolean ): void { const disconnect = this.disconnect; const callback = (event: Event): void => { handler(event as HandlerEvent); }; target.addEventListener(eventName, callback, capture); this.disconnect = () => { target.removeEventListener(eventName, callback, capture); disconnect.call(this); }; } }