core_CustomEvents.js

/**
 * CustomEvents
 * Lightweight event system implementation providing DOM-like event API.
 * Used as base class for App and provides centralized event handling.
 *
 * @class
 *
 * @example
 * // Basic usage with custom class
 * class MyService extends CustomEvents {
 *     constructor() {
 *         super();
 *     }
 *
 *     start() {
 *         this.dispatchEvent(new CustomEvent('start', {
 *             detail: { timestamp: Date.now() }
 *         }));
 *     }
 * }
 *
 * const service = new MyService();
 * service.addEventListener('start', (e) => {
 *     console.log('Service started at', e.detail.timestamp);
 * });
 * service.start();
 *
 * @example
 * // Using with InfrontJS App
 * const app = new IF.App(container);
 *
 * // Listen to framework events
 * app.addEventListener(CustomEvents.TYPE.READY, () => {
 *     console.log('App is ready');
 * });
 *
 * app.addEventListener(CustomEvents.TYPE.BEFORE_STATE_CHANGE, (e) => {
 *     console.log('Changing from', e.detail.currentStateId, 'to', e.detail.nextStateId);
 * });
 *
 * @example
 * // Removing event listeners
 * const handler = (e) => console.log('State changed');
 * app.addEventListener(CustomEvents.TYPE.AFTER_STATE_CHANGE, handler);
 * // Later...
 * app.removeEventListener(CustomEvents.TYPE.AFTER_STATE_CHANGE, handler);
 */
class CustomEvents {
    /**
     * Built-in event types used by InfrontJS framework
     *
     * @static
     * @readonly
     * @returns {Object.<string, string>} Object mapping event names to their types
     *
     * @property {string} READY - App initialization complete
     * @property {string} POPSTATE - Browser back/forward navigation
     * @property {string} BEFORE_STATE_CHANGE - Before state transition (cancelable)
     * @property {string} AFTER_STATE_CHANGE - After state transition complete
     * @property {string} BEFORE_LANGUAGE_SWITCH - Before language change
     * @property {string} AFTER_LANGUAGE_SWITCH - After language change complete
     * @property {string} ON_STATE_NOT_FOUND - When no matching state found (404)
     *
     * @example
     * // Using built-in event types
     * app.addEventListener(CustomEvents.TYPE.READY, () => {
     *     console.log('App ready');
     * });
     */
    static get TYPE() {
        return {
            'READY': 'ready',
            'POPSTATE': 'popstate',
            'BEFORE_STATE_CHANGE': 'beforeStateChange',
            'AFTER_STATE_CHANGE': 'afterStateChange',
            'BEFORE_LANGUAGE_SWITCH': 'beforeLanguageSwitch',
            'AFTER_LANGUAGE_SWITCH': 'afterLanguageSwitch',
            'ON_STATE_NOT_FOUND': 'onStateNotFound'
        };
    }

    /**
     * Create a new CustomEvents instance
     *
     * @constructor
     */
    constructor() {
        /**
         * Internal map of event type to array of listeners
         * @private
         * @type {Map<string, Array<Function>>}
         */
        this.listeners = new Map();
    }

    /**
     * Add an event listener for a specific event type
     *
     * @param {string} type - Event type to listen for
     * @param {Function|EventListenerOrEventListenerObject} listener - Callback function or event listener object
     *
     * @example
     * // Function listener
     * app.addEventListener('customEvent', (event) => {
     *     console.log('Custom event fired:', event.detail);
     * });
     *
     * @example
     * // Object listener
     * const listener = {
     *     handleEvent(event) {
     *         console.log('Event:', event.type);
     *     }
     * };
     * app.addEventListener('myEvent', listener);
     */
    addEventListener(type, listener) {
        if (!this.listeners.has(type)) {
            this.listeners.set(type, []);
        }
        this.listeners.get(type).push(listener);
    }

    /**
     * Remove an event listener for a specific event type
     *
     * @param {string} type - Event type to remove listener from
     * @param {Function|EventListenerOrEventListenerObject} listener - The exact listener function or object to remove
     *
     * @example
     * // Remove specific listener
     * const handler = (e) => console.log('Event fired');
     * app.addEventListener('myEvent', handler);
     * app.removeEventListener('myEvent', handler);
     *
     * @example
     * // One-time event listener pattern
     * const onceHandler = (e) => {
     *     console.log('This runs only once');
     *     app.removeEventListener('myEvent', onceHandler);
     * };
     * app.addEventListener('myEvent', onceHandler);
     */
    removeEventListener(type, listener) {
        if (!this.listeners.has(type)) {
            return;
        }
        const listeners = this.listeners.get(type);
        const index = listeners.indexOf(listener);
        if (index > -1) {
            listeners.splice(index, 1);
        }
    }

    /**
     * Dispatch an event to all registered listeners
     * Executes all listeners in the order they were added.
     *
     * @param {Event|CustomEvent} event - Event object to dispatch. Use CustomEvent for custom data.
     * @returns {boolean} True unless event.preventDefault() was called
     *
     * @example
     * // Dispatch simple event
     * app.dispatchEvent(new Event('refresh'));
     *
     * @example
     * // Dispatch event with data
     * app.dispatchEvent(new CustomEvent('userLogin', {
     *     detail: {
     *         userId: 123,
     *         username: 'john',
     *         timestamp: Date.now()
     *     }
     * }));
     *
     * @example
     * // Cancelable event
     * const event = new CustomEvent('beforeSave', {
     *     detail: { data: formData },
     *     cancelable: true
     * });
     * const allowed = app.dispatchEvent(event);
     * if (!allowed) {
     *     console.log('Save was prevented');
     * }
     */
    dispatchEvent(event) {
        if (!this.listeners.has(event.type)) {
            return true;
        }
        const listeners = this.listeners.get(event.type).slice();
        for (const listener of listeners) {
            listener.call(this, event);
        }
        return !event.defaultPrevented;
    }
}

export { CustomEvents };