// Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import type { Callback } from "@tslib/typing"; import type { Readable, Writable } from "svelte/store"; import { writable } from "svelte/store"; type Handler = (args: T) => Promise; interface HandlerAccess { callback: Handler; clear(): void; } class TriggerItem { #active: Writable; constructor( private setter: (handler: Handler, clear: Callback) => void, private clear: Callback, ) { this.#active = writable(false); } /** * A store which indicates whether the trigger is currently turned on. */ get active(): Readable { return this.#active; } /** * Deactivate the trigger. Can be safely called multiple times. */ off(): void { this.#active.set(false); this.clear(); } on(handler: Handler): void { this.setter(handler, () => this.off()); this.#active.set(true); } } interface HandlerOptions { once: boolean; } export class HandlerList { #list: HandlerAccess[] = []; /** * Returns a `TriggerItem`, which can be used to attach event handlers. * This TriggerItem exposes an additional `active` store. This can be * useful, if other components need to react to the input handler being active. */ trigger(options?: Partial): TriggerItem { const once = options?.once ?? false; let handler: Handler | null = null; return new TriggerItem( (callback: Handler, doClear: Callback): void => { const handlerAccess = { callback(args: T): Promise { const result = callback(args); if (once) { doClear(); } return result; }, clear(): void { if (once) { doClear(); } }, }; this.#list.push(handlerAccess); handler = handlerAccess.callback; }, () => { if (handler) { this.off(handler); handler = null; } }, ); } /** * Attaches an event handler. * @returns a callback, which removes the event handler. Alternatively, * you can call `off` on the HandlerList. */ on(handler: Handler, options?: Partial): Callback { const once = options?.once ?? false; let offHandler: Handler | null = null; const off = (): void => { if (offHandler) { this.off(offHandler); offHandler = null; } }; const handlerAccess = { callback: (args: T): Promise => { const result = handler(args); if (once) { off(); } return result; }, clear(): void { if (once) { off(); } }, }; offHandler = handlerAccess.callback; this.#list.push(handlerAccess); return off; } private off(handler: Handler): void { const index = this.#list.findIndex( (value: HandlerAccess): boolean => value.callback === handler, ); if (index >= 0) { this.#list.splice(index, 1); } } get length(): number { return this.#list.length; } dispatch(args: T): Promise { const promises: Promise[] = []; for (const { callback } of [...this]) { promises.push(callback(args)); } return Promise.all(promises) as unknown as Promise; } clear(): void { for (const { clear } of [...this]) { clear(); } } [Symbol.iterator](): Iterator, null, unknown> { const list = this.#list; let step = 0; return { next(): IteratorResult, null> { if (step >= list.length) { return { value: null, done: true }; } return { value: list[step++], done: false }; }, }; } } export type { TriggerItem };