@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
text/typescript
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";
}