playcanvas
Version:
PlayCanvas WebGL game engine
299 lines (296 loc) • 12.8 kB
JavaScript
import { EventHandle } from './event-handle.js';
/**
* @callback HandleEventCallback
* Callback used by {@link EventHandler} functions. Note the callback is limited to 8 arguments.
* @param {any} [arg1] - First argument that is passed from caller.
* @param {any} [arg2] - Second argument that is passed from caller.
* @param {any} [arg3] - Third argument that is passed from caller.
* @param {any} [arg4] - Fourth argument that is passed from caller.
* @param {any} [arg5] - Fifth argument that is passed from caller.
* @param {any} [arg6] - Sixth argument that is passed from caller.
* @param {any} [arg7] - Seventh argument that is passed from caller.
* @param {any} [arg8] - Eighth argument that is passed from caller.
* @returns {void}
*/ /**
* Abstract base class that implements functionality for event handling.
*
* ```javascript
* const obj = new EventHandlerSubclass();
*
* // subscribe to an event
* obj.on('hello', (str) => {
* console.log('event hello is fired', str);
* });
*
* // fire event
* obj.fire('hello', 'world');
* ```
*/ class EventHandler {
/**
* Reinitialize the event handler.
* @ignore
*/ initEventHandler() {
this._callbacks = new Map();
this._callbackActive = new Map();
}
/**
* Registers a new event handler.
*
* @param {string} name - Name of the event to bind the callback to.
* @param {HandleEventCallback} callback - Function that is called when event is fired. Note
* the callback is limited to 8 arguments.
* @param {object} scope - Object to use as 'this' when the event is fired, defaults to
* current this.
* @param {boolean} once - If true, the callback will be unbound after being fired once.
* @returns {EventHandle} Created {@link EventHandle}.
* @ignore
*/ _addCallback(name, callback, scope, once) {
if (!name || typeof name !== 'string' || !callback) {
console.warn(`EventHandler: subscribing to an event (${name}) with missing arguments`, callback);
}
if (!this._callbacks.has(name)) {
this._callbacks.set(name, []);
}
// if we are adding a callback to the list that is executing right now
// ensure we preserve initial list before modifications
if (this._callbackActive.has(name)) {
const callbackActive = this._callbackActive.get(name);
if (callbackActive && callbackActive === this._callbacks.get(name)) {
this._callbackActive.set(name, callbackActive.slice());
}
}
const evt = new EventHandle(this, name, callback, scope, once);
this._callbacks.get(name).push(evt);
return evt;
}
/**
* Attach an event handler to an event.
*
* @param {string} name - Name of the event to bind the callback to.
* @param {HandleEventCallback} callback - Function that is called when event is fired. Note
* the callback is limited to 8 arguments.
* @param {object} [scope] - Object to use as 'this' when the event is fired, defaults to
* current this.
* @returns {EventHandle} Can be used for removing event in the future.
* @example
* obj.on('test', (a, b) => {
* console.log(a + b);
* });
* obj.fire('test', 1, 2); // prints 3 to the console
* @example
* const evt = obj.on('test', (a, b) => {
* console.log(a + b);
* });
* // some time later
* evt.off();
*/ on(name, callback, scope = this) {
return this._addCallback(name, callback, scope, false);
}
/**
* Attach an event handler to an event. This handler will be removed after being fired once.
*
* @param {string} name - Name of the event to bind the callback to.
* @param {HandleEventCallback} callback - Function that is called when event is fired. Note
* the callback is limited to 8 arguments.
* @param {object} [scope] - Object to use as 'this' when the event is fired, defaults to
* current this.
* @returns {EventHandle} - can be used for removing event in the future.
* @example
* obj.once('test', (a, b) => {
* console.log(a + b);
* });
* obj.fire('test', 1, 2); // prints 3 to the console
* obj.fire('test', 1, 2); // not going to get handled
*/ once(name, callback, scope = this) {
return this._addCallback(name, callback, scope, true);
}
/**
* Detach an event handler from an event. If callback is not provided then all callbacks are
* unbound from the event, if scope is not provided then all events with the callback will be
* unbound.
*
* @param {string} [name] - Name of the event to unbind.
* @param {HandleEventCallback} [callback] - Function to be unbound.
* @param {object} [scope] - Scope that was used as the this when the event is fired.
* @returns {EventHandler} Self for chaining.
* @example
* const handler = () => {};
* obj.on('test', handler);
*
* obj.off(); // Removes all events
* obj.off('test'); // Removes all events called 'test'
* obj.off('test', handler); // Removes all handler functions, called 'test'
* obj.off('test', handler, this); // Removes all handler functions, called 'test' with scope this
*/ off(name, callback, scope) {
if (name) {
// if we are removing a callback from the list that is executing right now
// ensure we preserve initial list before modifications
if (this._callbackActive.has(name) && this._callbackActive.get(name) === this._callbacks.get(name)) {
this._callbackActive.set(name, this._callbackActive.get(name).slice());
}
} else {
// if we are removing a callback from any list that is executing right now
// ensure we preserve these initial lists before modifications
for (const [key, callbacks] of this._callbackActive){
if (!this._callbacks.has(key)) {
continue;
}
if (this._callbacks.get(key) !== callbacks) {
continue;
}
this._callbackActive.set(key, callbacks.slice());
}
}
if (!name) {
// remove all events
for (const callbacks of this._callbacks.values()){
for(let i = 0; i < callbacks.length; i++){
callbacks[i].removed = true;
}
}
this._callbacks.clear();
} else if (!callback) {
// remove all events of a specific name
const callbacks = this._callbacks.get(name);
if (callbacks) {
for(let i = 0; i < callbacks.length; i++){
callbacks[i].removed = true;
}
this._callbacks.delete(name);
}
} else {
const callbacks = this._callbacks.get(name);
if (!callbacks) {
return this;
}
for(let i = 0; i < callbacks.length; i++){
// remove all events with a specific name and a callback
if (callbacks[i].callback !== callback) {
continue;
}
// could be a specific scope as well
if (scope && callbacks[i].scope !== scope) {
continue;
}
callbacks[i].removed = true;
callbacks.splice(i, 1);
i--;
}
if (callbacks.length === 0) {
this._callbacks.delete(name);
}
}
return this;
}
/**
* Detach an event handler from an event using EventHandle instance. More optimal remove
* as it does not have to scan callbacks array.
*
* @param {EventHandle} handle - Handle of event.
* @ignore
*/ offByHandle(handle) {
const name = handle.name;
handle.removed = true;
// if we are removing a callback from the list that is executing right now
// ensure we preserve initial list before modifications
if (this._callbackActive.has(name) && this._callbackActive.get(name) === this._callbacks.get(name)) {
this._callbackActive.set(name, this._callbackActive.get(name).slice());
}
const callbacks = this._callbacks.get(name);
if (!callbacks) {
return this;
}
const ind = callbacks.indexOf(handle);
if (ind !== -1) {
callbacks.splice(ind, 1);
if (callbacks.length === 0) {
this._callbacks.delete(name);
}
}
return this;
}
/**
* Fire an event, all additional arguments are passed on to the event listener.
*
* @param {string} name - Name of event to fire.
* @param {any} [arg1] - First argument that is passed to the event handler.
* @param {any} [arg2] - Second argument that is passed to the event handler.
* @param {any} [arg3] - Third argument that is passed to the event handler.
* @param {any} [arg4] - Fourth argument that is passed to the event handler.
* @param {any} [arg5] - Fifth argument that is passed to the event handler.
* @param {any} [arg6] - Sixth argument that is passed to the event handler.
* @param {any} [arg7] - Seventh argument that is passed to the event handler.
* @param {any} [arg8] - Eighth argument that is passed to the event handler.
* @returns {EventHandler} Self for chaining.
* @example
* obj.fire('test', 'This is the message');
*/ fire(name, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8) {
if (!name) {
return this;
}
const callbacksInitial = this._callbacks.get(name);
if (!callbacksInitial) {
return this;
}
let callbacks;
if (!this._callbackActive.has(name)) {
// when starting callbacks execution ensure we store a list of initial callbacks
this._callbackActive.set(name, callbacksInitial);
} else if (this._callbackActive.get(name) !== callbacksInitial) {
// if we are trying to execute a callback while there is an active execution right now
// and the active list has been already modified,
// then we go to an unoptimized path and clone callbacks list to ensure execution consistency
callbacks = callbacksInitial.slice();
}
// eslint-disable-next-line no-unmodified-loop-condition
for(let i = 0; (callbacks || this._callbackActive.get(name)) && i < (callbacks || this._callbackActive.get(name)).length; i++){
const evt = (callbacks || this._callbackActive.get(name))[i];
if (!evt.callback) continue;
evt.callback.call(evt.scope, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8);
if (evt._once) {
// check that callback still exists because user may have unsubscribed in the event handler
const existingCallback = this._callbacks.get(name);
const ind = existingCallback ? existingCallback.indexOf(evt) : -1;
if (ind !== -1) {
if (this._callbackActive.get(name) === existingCallback) {
this._callbackActive.set(name, this._callbackActive.get(name).slice());
}
const callbacks = this._callbacks.get(name);
if (!callbacks) continue;
callbacks[ind].removed = true;
callbacks.splice(ind, 1);
if (callbacks.length === 0) {
this._callbacks.delete(name);
}
}
}
}
if (!callbacks) {
this._callbackActive.delete(name);
}
return this;
}
/**
* Test if there are any handlers bound to an event name.
*
* @param {string} name - The name of the event to test.
* @returns {boolean} True if the object has handlers bound to the specified event name.
* @example
* obj.on('test', () => {}); // bind an event to 'test'
* obj.hasEvent('test'); // returns true
* obj.hasEvent('hello'); // returns false
*/ hasEvent(name) {
return !!this._callbacks.get(name)?.length;
}
constructor(){
/**
* @type {Map<string,Array<EventHandle>>}
* @private
*/ this._callbacks = new Map();
/**
* @type {Map<string,Array<EventHandle>>}
* @private
*/ this._callbackActive = new Map();
}
}
export { EventHandler };