class CustomEvents {
/** @summary <h1>JavaScript regular and custom events abstraction</h1>
* @author Arthur Beaulieu
* @since June 2020
* @description <blockquote>The CustomEvents class provides an abstraction of JavaScript event listener, to allow
* easy binding and removing those events. It also provides an interface to register custom events. This class is
* meant to be used on all scopes you need ; module or global. Refer to each public method for detailed features.
* For source code, please go to <a href="https://github.com/ArthurBeaulieu/CustomEvents.js" alt="custom-events-js">
* https://github.com/ArthurBeaulieu/CustomEvents.js</a></blockquote>
* @param {boolean} [debug=false] - Debug flag ; when true, logs will be output in JavaScript console at each event */
constructor(debug = false) {
// Prevent wrong type for debug
if (typeof debug !== 'boolean') {
debug = false;
}
/** @private
* @member {boolean} - Internal logging flag from constructor options, allow to output each event action */
this._debug = debug;
/** @private
* @member {number} - Start the ID incrementer at pseudo random value, used for both regular and custom events */
this._idIncrementor = (Math.floor(Math.random() * Math.floor(256)) * 5678);
/** @private
* @member {any[]} - We store classical event listeners in array of objects containing all their information */
this._regularEvents = [];
/** @private
* @member {object} - We store custom events by name as key, each key stores an Array of subscribed events */
this._customEvents = {};
/** @public
* @member {string} - Component version */
this.version = '1.2.1';
}
/** @method
* @name destroy
* @public
* @memberof CustomEvents
* @description <blockquote>CustomEvents destructor. Will remove all event listeners and keys in instance.</blockquote> */
destroy() {
// Debug logging
this._raise('log', 'CustomEvents.destroy');
// Remove all existing eventListener
this.removeAllEvents();
// Delete object attributes
Object.keys(this).forEach(key => {
delete this[key];
});
}
/* --------------------------------------------------------------------------------------------------------------- */
/* -------------------------------------- CLASSIC JS EVENTS OVERRIDE ------------------------------------------ */
/* */
/* The following methods are made to abstract the event listeners from the JavaScript layer, so you can easily */
/* remove them when done using, without bothering with binding usual business for them. 'addEvent/removeEvent' */
/* method replace the initial ones. 'removeAllEvents' clears all instance event listeners ; nice for destroy */
/* --------------------------------------------------------------------------------------------------------------- */
/** @method
* @name addEvent
* @public
* @memberof CustomEvents
* @description <blockquote><code>addEvent</code> method abstracts the <code>addEventListener</code> method to easily
* remove it when needed, also to set a custom scope on callback.</blockquote>
* @param {string} eventName - The event name to fire (mousemove, click, context etc.)
* @param {object} element - The DOM element to attach the listener to
* @param {function} callback - The callback function to execute when event is realised
* @param {object} [scope=element] - The event scope to apply to the callback (optional, default to DOM element)
* @param {object|boolean} [options=false] - The event options (useCapture and else)
* @returns {number|boolean} - The event ID to use to manually remove an event, false if arguments are invalid */
addEvent(eventName, element, callback, scope = element, options = false) {
// Debug logging
this._raise('log', `CustomEvents.addEvent: ${eventName} ${element} ${callback} ${scope} ${options}`);
// Missing mandatory arguments
if (eventName === null || eventName === undefined ||
element === null || element === undefined ||
callback === null || callback === undefined) {
this._raise('error', 'CustomEvents.addEvent: Missing mandatory arguments');
return false;
}
// Prevent wrong type for arguments (mandatory and optional)
const err = () => {
this._raise('error', 'CustomEvents.addEvent: Wrong type for argument');
};
// Test argument validity for further process
if (typeof eventName !== 'string' || typeof element !== 'object' || typeof callback !== 'function') {
err();
return false;
}
if ((scope !== null && scope !== undefined) && typeof scope !== 'object') {
err();
return false;
}
if ((options !== null && options !== undefined) && (typeof options !== 'object' && typeof options !== 'boolean')) {
err();
return false;
}
// Save scope to callback function, default scope is DOM target object
callback = callback.bind(scope);
// Add event to internal array and keep all its data
this._regularEvents.push({
id: this._idIncrementor,
element: element,
eventName: eventName,
scope: scope,
callback: callback,
options: options
});
// Add event listener with options
element.addEventListener(eventName, callback, options);
// Post increment to return the true event entry id, then update the incrementer
return this._idIncrementor++;
}
/** @method
* @name removeEvent
* @public
* @memberof CustomEvents
* @description <blockquote><code>removeEvent</code> method abstracts the <code>removeEventListener</code> method to
* really remove event listeners.</blockquote>
* @param {number} eventId - The event ID to remove listener from. Returned when addEvent is called
* @returns {boolean} - The method status ; true for success, false for non-existing event */
removeEvent(eventId) {
// Debug logging
this._raise('log', `Events.removeEvent: ${eventId}`);
// Missing mandatory arguments
if (eventId === null || eventId === undefined) {
this._raise('error', 'CustomEvents.removeEvent: Missing mandatory arguments');
return false;
}
// Prevent wrong type for arguments (mandatory)
if (typeof eventId !== 'number') {
this._raise('error', 'CustomEvents.removeEvent: Wrong type for argument');
return false;
}
// Returned value
let statusCode = false; // Not found status code by default (false)
// Iterate over saved listeners, reverse order for proper splicing
for (let i = (this._regularEvents.length - 1); i >= 0 ; --i) {
// If an event ID match in saved ones, we remove it and update saved listeners
if (this._regularEvents[i].id === eventId) {
// Update status code
statusCode = true; // Found and removed event listener status code (true)
this._clearRegularEvent(i);
}
}
// Return with status code
return statusCode;
}
/** @method
* @name removeAllEvents
* @public
* @memberof CustomEvents
* @description <blockquote>Clear all event listener registered through this class object.</blockquote>
* @returns {boolean} - The method status ; true for success, false for not removed any event */
removeAllEvents() {
// Debug logging
this._raise('log', 'CustomEvents.removeAllEvents');
// Returned value
let statusCode = false; // Didn't removed any status code by default (false)
// Flag to know if there was any previously stored event listeners
const hadEvents = (this._regularEvents.length > 0);
// Iterate over saved listeners, reverse order for proper splicing
for (let i = (this._regularEvents.length - 1); i >= 0; --i) {
this._clearRegularEvent(i);
}
// If all events where removed, update statusCode to success
if (this._regularEvents.length === 0 && hadEvents) {
// Update status code
statusCode = true; // Found and removed all events listener status code (true)
}
// Return with status code
return statusCode;
}
/** @method
* @name _clearRegularEvent
* @private
* @memberof CustomEvents
* @description <blockquote><code>_clearRegularEvent</code> method remove the saved event listener for a
* given index in regularEvents array range.</blockquote>
* @param {number} index - The regular event index to remove from class attributes
* @return {boolean} - The method status ; true for success, false for not cleared any event */
_clearRegularEvent(index) {
// Debug logging
this._raise('log', `CustomEvents._clearRegularEvent: ${index}`);
// Missing mandatory arguments
if (index === null || index === undefined) {
this._raise('error', 'CustomEvents._clearRegularEvent: Missing mandatory argument');
return false;
}
// Prevent wrong type for arguments (mandatory)
if (typeof index !== 'number') {
this._raise('error', 'CustomEvents._clearRegularEvent: Wrong type for argument');
return false;
}
// Check if index match an existing event in attributes
if (this._regularEvents[index]) {
// Remove its event listener and update regularEvents array
const evt = this._regularEvents[index];
evt.element.removeEventListener(evt.eventName, evt.callback, evt.options);
this._regularEvents.splice(index, 1);
return true;
}
return false;
}
/* --------------------------------------------------------------------------------------------------------------- */
/* ------------------------------------------- CUSTOM JS EVENTS ----------------------------------------------- */
/* */
/* The three following methods (subscribe, unsubscribe, publish) are designed to reference an event by its name */
/* and handle as many subscriptions as you want. When subscribing, you get an ID you can use to unsubscribe your */
/* event later. Just publish with the event name to callback all its registered subscriptions. */
/* --------------------------------------------------------------------------------------------------------------- */
/** @method
* @name subscribe
* @public
* @memberof CustomEvents
* @description <blockquote>Subscribe method allow you to listen to an event and react when it occurs.</blockquote>
* @param {string} eventName - Event name (the one to use to publish)
* @param {function} callback - The callback to execute when event is published
* @param {boolean} [oneShot=false] - One shot : to remove subscription the first time callback is fired
* @returns {number|boolean} - The event id, to be used when manually unsubscribing */
subscribe(eventName, callback, oneShot = false) {
// Debug logging
this._raise('log', `CustomEvents.subscribe: ${eventName} ${callback} ${oneShot}`);
// Missing mandatory arguments
if (eventName === null || eventName === undefined ||
callback === null || callback === undefined) {
this._raise('error', 'CustomEvents.subscribe', 'Missing mandatory arguments');
return false;
}
// Prevent wrong type for arguments (mandatory and optional)
const err = () => {
this._raise('error', 'CustomEvents.subscribe: Wrong type for argument');
};
if (typeof eventName !== 'string' || typeof callback !== 'function') {
err();
return false;
}
if ((oneShot !== null && oneShot !== undefined) && typeof oneShot !== 'boolean') {
err();
return false;
}
// Create event entry if not already existing in the registered events
if (!this._customEvents[eventName]) {
this._customEvents[eventName] = []; // Set empty array for new event subscriptions
}
// Push new subscription for event name
this._customEvents[eventName].push({
id: this._idIncrementor,
name: eventName,
os: oneShot,
callback: callback
});
// Post increment to return the true event entry id, then update the incrementer
return this._idIncrementor++;
}
/** @method
* @name unsubscribe
* @public
* @memberof CustomEvents
* @description <blockquote>Unsubscribe method allow you to revoke an event subscription from its string name.</blockquote>
* @param {number} eventId - The subscription id returned when subscribing to an event name
* @returns {boolean} - The method status ; true for success, false for non-existing subscription **/
unsubscribe(eventId) {
// Debug logging
this._raise('log', `CustomEvents.unsubscribe: ${eventId}`);
// Missing mandatory arguments
if (eventId === null || eventId === undefined) {
this._raise('error', 'CustomEvents.unsubscribe: Missing mandatory arguments');
return false;
}
// Prevent wrong type for arguments (mandatory)
if (typeof eventId !== 'number') {
this._raise('error', 'CustomEvents.unsubscribe: Wrong type for argument');
return false;
}
// Returned value
let statusCode = false; // Not found status code by default (false)
// Save event keys to iterate properly on this._events Object
const keys = Object.keys(this._customEvents);
// Reverse events iteration to properly splice without messing with iteration order
for (let i = (keys.length - 1); i >= 0; --i) {
// Get event subscriptions
const subs = this._customEvents[keys[i]];
// Iterate over events subscriptions to find the one with given id
for (let j = 0; j < subs.length; ++j) {
// In case we got a subscription for this events
if (subs[j].id === eventId) {
// Debug logging
this._raise('log', `CustomEvents.unsubscribe: subscription found\n`, subs[j], `\nSubscription n°${eventId} for ${subs.name} has been removed`);
// Update status code
statusCode = true; // Found and unsubscribed status code (true)
// Remove subscription from event Array
subs.splice(j, 1);
// Remove event name if no remaining subscriptions
if (subs.length === 0) {
delete this._customEvents[keys[i]];
}
// Break since id are unique and no other subscription can be found after
break;
}
}
}
// Return with status code
return statusCode;
}
/** @method
* @name unsubscribeAllFor
* @public
* @memberof CustomEvents
* @description <blockquote><code>unsubscribeAllFor</code> method clear all subscriptions registered for given event name.</blockquote>
* @param {string} eventName - The event to clear subscription from
* @returns {boolean} - The method status ; true for success, false for non-existing event **/
unsubscribeAllFor(eventName) {
// Debug logging
this._raise('log', `CustomEvents.unsubscribeAllFor: ${eventName}`);
// Missing mandatory arguments
if (eventName === null || eventName === undefined) {
this._raise('error', 'CustomEvents.unsubscribeAllFor: Missing mandatory arguments');
return false;
}
// Prevent wrong type for arguments (mandatory and optional)
if (typeof eventName !== 'string') {
this._raise('error', 'CustomEvents.unsubscribeAllFor: Wrong type for argument');
return false;
}
// Returned value
let statusCode = false; // Not found status code by default (false)
// Save event keys to iterate properly on this._events Object
const keys = Object.keys(this._customEvents);
// Iterate through custom event keys to find matching event to remove
for (let i = 0; i < keys.length; ++i) {
if (keys[i] === eventName) {
// Get event subscriptions
const subs = this._customEvents[keys[i]];
// Iterate over events subscriptions to find the one with given id, reverse iteration to properly splice without messing with iteration order
for (let j = (subs.length - 1); j >= 0; --j) {
// Update status code
statusCode = true; // Found and unsubscribed all status code (true)
// Remove subscription from event Array
subs.splice(j, 1);
// Remove event name if no remaining subscriptions
if (subs.length === 0) {
delete this._customEvents[keys[i]];
}
}
}
}
// Return with status code
return statusCode;
}
/** @method
* @name publish
* @public
* @memberof CustomEvents
* @description <blockquote><code>Publish</code> method allow you to fire an event by name and trigger all its subscription by callbacks./blockquote>
* @param {string} eventName - Event name (the one to use to publish)
* @param {object} [data=undefined] - The data object to sent through the custom event
* @returns {boolean} - The method status ; true for success, false for non-existing event **/
publish(eventName, data = null) {
// Debug logging
this._raise('log', `CustomEvents.publish: ${eventName} ${data}`);
// Missing mandatory arguments
if (eventName === null || eventName === undefined) {
this._raise('error', 'CustomEvents.publish: Missing mandatory arguments');
return false;
}
// Prevent wrong type for arguments (mandatory and optional)
if (typeof eventName !== 'string' || (data !== undefined && typeof data !== 'object')) {
this._raise('error', 'CustomEvents.publish: Wrong type for argument');
return false;
}
// Returned value
let statusCode = false; // Not found status code by default (false)
// Save event keys to iterate properly on this._events Object
const keys = Object.keys(this._customEvents);
// Iterate over saved custom events
for (let i = 0; i < keys.length; ++i) {
// If published name match an existing events, we iterate its subscriptions. First subscribed, first served
if (keys[i] === eventName) {
// Update status code
statusCode = true; // Found and published status code (true)
// Get event subscriptions
const subs = this._customEvents[keys[i]];
// Iterate over events subscriptions to find the one with given id
// Reverse subscriptions iteration to properly splice without messing with iteration order
for (let j = (subs.length - 1); j >= 0; --j) {
// Debug logging
this._raise('log', `CustomEvents.publish: fire callback for ${eventName}, subscription n°${subs[j].id}`, subs[j]);
// Fire saved callback
subs[j].callback(data);
// Remove oneShot listener from event entry
if (subs[j].os) {
// Debug logging
this._raise('log', 'CustomEvents.publish: remove subscription because one shot usage is done');
subs.splice(j, 1);
// Remove event name if no remaining subscriptions
if (subs.length === 0) {
delete this._customEvents[keys[i]];
}
}
}
}
}
// Return with status code
return statusCode;
}
/* --------------------------------------------------------------------------------------------------------------- */
/* -------------------------------------------- COMPONENT UTILS ----------------------------------------------- */
/* --------------------------------------------------------------------------------------------------------------- */
/** @method
* @name _raise
* @private
* @memberof CustomEvents
* @description <blockquote>Internal method to abstract console wrapped in debug flag./blockquote>
* @param {string} level - The console method to call
* @param {string} errorValue - The error value to display in console method **/
_raise(level, errorValue) {
if (this._debug) {
console[level](errorValue);
}
}
}
export default CustomEvents;