Initial
This commit is contained in:
195
resources/app/client/core/hooks.js
Normal file
195
resources/app/client/core/hooks.js
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* @typedef {object} HookedFunction
|
||||
* @property {string} hook
|
||||
* @property {number} id
|
||||
* @property {Function} fn
|
||||
* @property {boolean} once
|
||||
*/
|
||||
|
||||
/**
|
||||
* A simple event framework used throughout Foundry Virtual Tabletop.
|
||||
* When key actions or events occur, a "hook" is defined where user-defined callback functions can execute.
|
||||
* This class manages the registration and execution of hooked callback functions.
|
||||
*/
|
||||
class Hooks {
|
||||
|
||||
/**
|
||||
* A mapping of hook events which have functions registered to them.
|
||||
* @type {Record<string, HookedFunction[]>}
|
||||
*/
|
||||
static get events() {
|
||||
return this.#events;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Record<string, HookedFunction[]>}
|
||||
* @private
|
||||
* @ignore
|
||||
*/
|
||||
static #events = {};
|
||||
|
||||
/**
|
||||
* A mapping of hooked functions by their assigned ID
|
||||
* @type {Map<number, HookedFunction>}
|
||||
*/
|
||||
static #ids = new Map();
|
||||
|
||||
/**
|
||||
* An incrementing counter for assigned hooked function IDs
|
||||
* @type {number}
|
||||
*/
|
||||
static #id = 1;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Register a callback handler which should be triggered when a hook is triggered.
|
||||
* @param {string} hook The unique name of the hooked event
|
||||
* @param {Function} fn The callback function which should be triggered when the hook event occurs
|
||||
* @param {object} options Options which customize hook registration
|
||||
* @param {boolean} options.once Only trigger the hooked function once
|
||||
* @returns {number} An ID number of the hooked function which can be used to turn off the hook later
|
||||
*/
|
||||
static on(hook, fn, {once=false}={}) {
|
||||
console.debug(`${vtt} | Registered callback for ${hook} hook`);
|
||||
const id = this.#id++;
|
||||
if ( !(hook in this.#events) ) {
|
||||
Object.defineProperty(this.#events, hook, {value: [], writable: false});
|
||||
}
|
||||
const entry = {hook, id, fn, once};
|
||||
this.#events[hook].push(entry);
|
||||
this.#ids.set(id, entry);
|
||||
return id;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Register a callback handler for an event which is only triggered once the first time the event occurs.
|
||||
* An alias for Hooks.on with {once: true}
|
||||
* @param {string} hook The unique name of the hooked event
|
||||
* @param {Function} fn The callback function which should be triggered when the hook event occurs
|
||||
* @returns {number} An ID number of the hooked function which can be used to turn off the hook later
|
||||
*/
|
||||
static once(hook, fn) {
|
||||
return this.on(hook, fn, {once: true});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Unregister a callback handler for a particular hook event
|
||||
* @param {string} hook The unique name of the hooked event
|
||||
* @param {Function|number} fn The function, or ID number for the function, that should be turned off
|
||||
*/
|
||||
static off(hook, fn) {
|
||||
let entry;
|
||||
|
||||
// Provided an ID
|
||||
if ( typeof fn === "number" ) {
|
||||
const id = fn;
|
||||
entry = this.#ids.get(id);
|
||||
if ( !entry ) return;
|
||||
this.#ids.delete(id);
|
||||
const event = this.#events[entry.hook];
|
||||
event.findSplice(h => h.id === id);
|
||||
}
|
||||
|
||||
// Provided a Function
|
||||
else {
|
||||
const event = this.#events[hook];
|
||||
const entry = event.findSplice(h => h.fn === fn);
|
||||
if ( !entry ) return;
|
||||
this.#ids.delete(entry.id);
|
||||
}
|
||||
console.debug(`${vtt} | Unregistered callback for ${hook} hook`);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Call all hook listeners in the order in which they were registered
|
||||
* Hooks called this way can not be handled by returning false and will always trigger every hook callback.
|
||||
*
|
||||
* @param {string} hook The hook being triggered
|
||||
* @param {...*} args Arguments passed to the hook callback functions
|
||||
* @returns {boolean} Were all hooks called without execution being prevented?
|
||||
*/
|
||||
static callAll(hook, ...args) {
|
||||
if ( CONFIG.debug.hooks ) {
|
||||
console.log(`DEBUG | Calling ${hook} hook with args:`);
|
||||
console.log(args);
|
||||
}
|
||||
if ( !(hook in this.#events) ) return true;
|
||||
for ( const entry of Array.from(this.#events[hook]) ) {
|
||||
this.#call(entry, args);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Call hook listeners in the order in which they were registered.
|
||||
* Continue calling hooks until either all have been called or one returns false.
|
||||
*
|
||||
* Hook listeners which return false denote that the original event has been adequately handled and no further
|
||||
* hooks should be called.
|
||||
*
|
||||
* @param {string} hook The hook being triggered
|
||||
* @param {...*} args Arguments passed to the hook callback functions
|
||||
* @returns {boolean} Were all hooks called without execution being prevented?
|
||||
*/
|
||||
static call(hook, ...args) {
|
||||
if ( CONFIG.debug.hooks ) {
|
||||
console.log(`DEBUG | Calling ${hook} hook with args:`);
|
||||
console.log(args);
|
||||
}
|
||||
if ( !(hook in this.#events) ) return true;
|
||||
for ( const entry of Array.from(this.#events[hook]) ) {
|
||||
let callAdditional = this.#call(entry, args);
|
||||
if ( callAdditional === false ) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Call a hooked function using provided arguments and perhaps unregister it.
|
||||
* @param {HookedFunction} entry The hooked function entry
|
||||
* @param {any[]} args Arguments to be passed
|
||||
* @private
|
||||
*/
|
||||
static #call(entry, args) {
|
||||
const {hook, id, fn, once} = entry;
|
||||
if ( once ) this.off(hook, id);
|
||||
try {
|
||||
return entry.fn(...args);
|
||||
} catch(err) {
|
||||
const msg = `Error thrown in hooked function '${fn?.name}' for hook '${hook}'`;
|
||||
console.warn(`${vtt} | ${msg}`);
|
||||
if ( hook !== "error" ) this.onError("Hooks.#call", err, {msg, hook, fn, log: "error"});
|
||||
}
|
||||
}
|
||||
|
||||
/* --------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Notify subscribers that an error has occurred within foundry.
|
||||
* @param {string} location The method where the error was caught.
|
||||
* @param {Error} error The error.
|
||||
* @param {object} [options={}] Additional options to configure behaviour.
|
||||
* @param {string} [options.msg=""] A message which should prefix the resulting error or notification.
|
||||
* @param {?string} [options.log=null] The level at which to log the error to console (if at all).
|
||||
* @param {?string} [options.notify=null] The level at which to spawn a notification in the UI (if at all).
|
||||
* @param {object} [options.data={}] Additional data to pass to the hook subscribers.
|
||||
*/
|
||||
static onError(location, error, {msg="", notify=null, log=null, ...data}={}) {
|
||||
if ( !(error instanceof Error) ) return;
|
||||
if ( msg ) error = new Error(`${msg}. ${error.message}`, { cause: error });
|
||||
if ( log ) console[log]?.(error);
|
||||
if ( notify ) ui.notifications[notify]?.(msg || error.message);
|
||||
Hooks.callAll("error", location, error, data);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user