UNPKG

@byloth/core

Version:

An unopinionated collection of useful functions and classes that I use widely in all my projects. 🔧

324 lines (292 loc) • 10.1 kB
import { KeyException, NotImplementedException, RuntimeException } from "../exceptions/index.js"; import CallableObject from "./callable-object.js"; import type { Callback } from "./types.js"; const Disabler = () => { /* ... */ }; /** * A class representing a callback that can be switched between multiple implementations. * * It can be used to implement different behaviors for the same event handler, allowing * it to respond to different states without incurring any overhead during execution. * * --- * * @example * ```ts * const onPointerMove = new SwitchableCallback<(evt: PointerEvent) => void>(); * * onPointerMove.register("released", () => { [...] }); * onPointerMove.register("pressed", () => { [...] }); * * window.addEventListener("pointerdown", () => onPointerMove.switch("pressed")); * window.addEventListener("pointermove", onPointerMove); * window.addEventListener("pointerup", () => onPointerMove.switch("released")); * ``` * * --- * * @template T The type signature of the callback. Default is `() => void`. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export default class SwitchableCallback<T extends Callback<any[], any> = Callback> extends CallableObject<T> { /** * The currently selected implementation of the callback. */ protected _callback: T; /** * All the implementations that have been registered for the callback. * * The keys are the names of the implementations they were registered with. * The values are the implementations themselves. */ protected readonly _callbacks: Map<string, T>; /** * A flag indicating whether the callback is enabled or not. * * This protected property is the only one that can be modified directly by the derived classes. * If you're looking for the public and readonly property, use * the {@link SwitchableCallback.isEnabled} getter instead. */ protected _isEnabled: boolean; /** * A flag indicating whether the callback is enabled or not. * * It indicates whether the callback is currently able to execute the currently selected implementation. * If it's disabled, the callback will be invoked without executing anything. */ public get isEnabled(): boolean { return this._isEnabled; } /** * The key that is associated with the currently selected implementation. * * This protected property is the only one that can be modified directly by the derived classes. * If you're looking for the public and readonly property, use the {@link SwitchableCallback.key} getter instead. */ protected _key: string; /** * The key that is associated with the currently selected implementation. */ public get key(): string { return this._key; } /** * The function that will be called by the extended class when the object is invoked as a function. */ protected readonly _invoke: (...args: Parameters<T>) => ReturnType<T>; /** * Initializes a new instance of the {@link SwitchableCallback} class. * * --- * * @example * ```ts * const onPointerMove = new SwitchableCallback<(evt: PointerEvent) => void>(); * ``` */ public constructor(); /** * Initializes a new instance of the {@link SwitchableCallback} * class with the specified callback enabled by default. * * --- * * @example * ```ts * const onPointerMove = new SwitchableCallback<(evt: PointerEvent) => void>((evt) => { [...] }); * ``` * * --- * * @param callback The callback that will be executed when the object is invoked as a function by default. * @param key The key that is associated by default to the given callback. Default is `default`. */ public constructor(callback: T, key?: string); public constructor(callback?: T, key = "default") { super(); this._callbacks = new Map<string, T>(); this._isEnabled = true; if (callback) { this._callbacks.set(key, callback); } else { key = ""; callback = ((() => { throw new NotImplementedException( "The `SwitchableCallback` has no callback defined yet. " + "Did you forget to call the `register` method?" ); }) as unknown) as T; } this._key = key; this._callback = callback; this._invoke = (...args: Parameters<T>): ReturnType<T> => this._callback(...args); } /** * Enables the callback, allowing it to execute the currently selected implementation. * * Also note that: * - If any implementation has been registered yet, a {@link KeyException} will be thrown. * - If any key is given and it doesn't have any associated * implementation yet, a {@link KeyException} will be thrown. * - If the callback is already enabled, a {@link RuntimeException} will be thrown. * * --- * * @example * ```ts * window.addEventListener("pointerdown", () => onPointerMove.enable()); * window.addEventListener("pointermove", onPointerMove); * ``` * * --- * * @param key * The key that is associated with the implementation to enable. Default is the currently selected implementation. */ public enable(key?: string): void { if (key === undefined) { if (!(this._key)) { throw new KeyException( "The `SwitchableCallback` has no callback defined yet. " + "Did you forget to call the `register` method?" ); } key = this._key; } else if (!(key)) { throw new KeyException("The key must be a non-empty string."); } else if (!(this._callbacks.has(key))) { throw new KeyException(`The key '${key}' doesn't yet have any associated callback.`); } if (this._isEnabled) { throw new RuntimeException("The `SwitchableCallback` is already enabled."); } this._callback = this._callbacks.get(key)!; this._isEnabled = true; } /** * Disables the callback, allowing it to be invoked without executing any implementation. * * If the callback is already disabled, a {@link RuntimeException} will be thrown. * * --- * * @example * ```ts * window.addEventListener("pointermove", onPointerMove); * window.addEventListener("pointerup", () => onPointerMove.disable()); * ``` */ public disable(): void { if (!(this._isEnabled)) { throw new RuntimeException("The `SwitchableCallback` is already disabled."); } this._callback = Disabler as T; this._isEnabled = false; } /** * Registers a new implementation for the callback. * * Also note that: * - If the callback has no other implementation registered yet, this one will be selected as default. * - If the key has already been used for another implementation, a {@link KeyException} will be thrown. * * --- * * @example * ```ts * onPointerMove.register("pressed", () => { [...] }); * onPointerMove.register("released", () => { [...] }); * ``` * * --- * * @param key The key that will be associated with the implementation. * @param callback The implementation to register. */ public register(key: string, callback: T): void { if (this._callbacks.size === 0) { this._key = key; this._callback = callback; } else if (this._callbacks.has(key)) { throw new KeyException(`The key '${key}' has already been used for another callback.`); } this._callbacks.set(key, callback); } /** * Unregisters an implementation for the callback. * * Also note that: * - If the key is the currently selected implementation, a {@link KeyException} will be thrown. * - If the key has no associated implementation yet, a {@link KeyException} will be thrown. * * --- * * @example * ```ts * onPointerMove.unregister("released"); * ``` * * --- * * @param key The key that is associated with the implementation to unregister. */ public unregister(key: string): void { if (this._key === key) { throw new KeyException("Unable to unregister the currently selected callback."); } if (!(this._callbacks.has(key))) { throw new KeyException(`The key '${key}' doesn't yet have any associated callback.`); } this._callbacks.delete(key); } /** * Switches the callback to the implementation associated with the given key. * * If the key has no associated implementation yet, a {@link KeyException} will be thrown. * * --- * * @example * ```ts * window.addEventListener("pointerdown", () => onPointerMove.switch("pressed")); * window.addEventListener("pointermove", onPointerMove); * window.addEventListener("pointerup", () => onPointerMove.switch("released")); * ``` * * --- * * @param key The key that is associated with the implementation to switch to. */ public switch(key: string): void { if (!(this._callbacks.has(key))) { throw new KeyException(`The key '${key}' doesn't yet have any associated callback.`); } if (this._key === key) { return; } this._key = key; if (this._isEnabled) { this._callback = this._callbacks.get(key)!; } } public override readonly [Symbol.toStringTag]: string = "SwitchableCallback"; }