basic_as-event.js

import {ErrorCode, ExceptionIdMap} from '../../exception/exceptionDesc.js';

const logModule = 'as-event';

/**
 * TComponents Namespace
 * @namespace TComponents
 * @public
 */

/**
 * @description Event manager base class

 * @class TComponents.Eventing_A
 * @memberof TComponents
 * @example
 * const eventingInstance = new TComponents.Eventing_A();
 */
export class Eventing_A {
  constructor() {
    /** @type {Object<string, Function[]>} */
    this.events = {};
  }

  /**
   * @description Subscribe to an event
   * @member TComponents.Eventing_A#on
   * @method
   * @param {string} eventName - Name of the triggering event
   * @param {Function} callback - Function to be called when the event is triggered
   * @param {boolean} [strict=true] - If true (default), checking whether the function has been added is done by function object comparison,
   * otherwise, the comparison is done only by function.name
   */
  on(eventName, callback, strict = true) {
    if (typeof callback !== 'function') throw new Error('callback is not a valid function');
    const handlers = this.events[eventName] || [];

    if (!strict && handlers.some((cb) => cb.name === callback.name)) return;

    if (handlers.some((h) => h === callback || (h && h._original && h._original === callback))) return;
    handlers.push(callback);
    this.events[eventName] = handlers;
  }

  /**
   * @description Unsubscribe from an event
   * @member TComponents.Eventing_A#off
   * @method
   * @param {string} eventName - Name of the triggering event
   * @param {Function} callback - Function to be removed from the event's callbacks
   * @returns {boolean} - True if the callback was removed, false if it was not found
   */
  off(eventName, callback) {
    const handlers = this.events[eventName];
    if (!handlers || handlers.length === 0) {
      return false;
    }
    const index = handlers.findIndex((h) => h === callback || (h && h._original && h._original === callback));
    if (index > -1) {
      handlers.splice(index, 1);
      return true;
    }
    return false;
  }

  /**
   * @description Subscribe to an event, but the callback is called only once
   * @member TComponents.Eventing_A#once
   * @method
   * @param {string} eventName - Name of the triggering event
   * @param {Function} callback - Function to be called when the event is triggered
   * @returns {boolean} - True if the callback was added, false if it was already added
   */
  once(eventName, callback) {
    if (typeof callback !== 'function') throw new Error('callback is not a valid function');
    const handlers = this.events[eventName] || [];
    if (handlers.some((h) => h === callback || (h && h._original && h._original === callback))) return false;

    const onceCallback = (...data) => {
      try {
        callback(...data);
      } finally {
        this.off(eventName, onceCallback);
      }
    };
    onceCallback._original = callback;
    handlers.push(onceCallback);
    this.events[eventName] = handlers;
    return true;
  }

  /**
   * @description Trigger all callbacks subscribed to an event
   * @member TComponent.Eventing_A#trigger
   * @method
   * @param {string} eventName - Name of the event to be triggered
   * @param {...any} data - Data passed to the callback as input parameters
   */
  trigger(eventName, ...data) {
    const handlers = this.events[eventName];
    if (!handlers || handlers.length === 0) {
      return;
    }
    const toCall = handlers.slice();
    toCall.forEach((callback) => {
      try {
        callback(...data);
      } catch (e) {
        Logger.e(logModule, ErrorCode.EventHandlerError, `Error in event handler for ${eventName}:`, e);
      }
    });
  }

  /**
   * @description Get the number of callbacks subscribed to an event
   * @member TComponents.Eventing_A#count
   * @method
   * @param {string} eventName - Name of the triggering event
   * @returns {number} - Number of callbacks subscribed to the event
   */
  count(eventName) {
    return this.events[eventName] ? this.events[eventName].length : 0;
  }

  /**
   * @description Clean up a specific event
   * @member TComponents.Eventing_A#cleanEvent
   * @method
   * @param {string} eventName - The event name you want to clean
   */
  cleanEvent(eventName) {
    if (this.events[eventName]) {
      const handlers = this.events[eventName];
      handlers.forEach((callback) => {
        this.off(eventName, callback);
      });
      // Optionally, remove the event from the events registry
      delete this.events[eventName];
    }
  }

  /**
   * @description Clean up all registered events except those in the ignore list
   * @member TComponents.Eventing_A#cleanUpEvents
   * @method
   * @param {string[]} [ignores=['before:init', 'after:render', 'before:destroy']] - Event list you don't want to clean
   */
  cleanUpEvents(ignores = ['before:init', 'after:render', 'before:destroy']) {
    for (const eventName in this.events) {
      if (!ignores.includes(eventName) && Object.prototype.hasOwnProperty.call(this.events, eventName)) {
        const handlers = this.events[eventName];
        handlers.forEach((callback) => {
          this.off(eventName, callback);
        });
      }
    }
  }

  /**
   * @description Check if an event has been registered already
   * @member TComponents.Eventing_A#hasEvent
   * @method
   * @param {string} eventName - Name of the triggering event
   * @returns {boolean} - True if the event has been registered already, false otherwise
   */
  hasEvent(eventName) {
    return Object.prototype.hasOwnProperty.call(this.events, eventName);
  }

  /**
   * @description Check if any event has been registered already
   * @member TComponents.Eventing_A#anyEvent
   * @method
   * @returns {boolean} - True if any event has been registered already, false otherwise
   */
  anyEvent() {
    return Object.keys(this.events).length > 0;
  }
}