'use strict'; import { HTMLMcElement, DC } from './MC.js'; /** @hidden Converts a standard event name to its microcomponent-prefixed variant. This utility is used internally to namespace custom events (e.g., 'Click' → '_mcClick'). @param {string} eventName - The original event name (e.g., 'click', 'update'). @returns {string} The microcomponent-prefixed event name. */ export function getMcEventName(eventName: string): string { return '_mc' + eventName[0].toUpperCase() + eventName.substring(1); } /** Removes a previously attached event handler from the DOM node. See {@link on} and {@link one}. Uses internal microcomponent-prefixed naming for event handlers. @param {Node} domNode - The DOM element from which to remove the listener. @param {string} eventName - The name of the event (e.g., 'click', 'update'). @param {EventListenerOrEventListenerObject} handler - The listener function or object to detach. */ export function off(domNode: Node, eventName: string, handler: EventListenerOrEventListenerObject) { domNode.removeEventListener(getMcEventName(eventName), handler); } /** Attaches a persistent event listener that will fire every time the specified event occurs. @param {Node} domNode - The DOM element to attach the listener to. @param {string} eventName - The name of the event (e.g., 'click', 'update'). @param {EventListenerOrEventListenerObject} handler - The callback function or object to invoke on each event. @param {boolean} [once=false] - If true, remove the listener after first invocation. @returns {EventListenerOrEventListenerObject} The attached handler reference. Useful for later deletion with {@link off}. */ export function on( domNode: Node, eventName: string, handler: EventListenerOrEventListenerObject, once: boolean = false ): EventListenerOrEventListenerObject { domNode.addEventListener(getMcEventName(eventName), handler, { capture: false, once }); return handler; } /** Attaches an event listener that will fire only once. Internally calls {@link on} with the `once` flag set to true. The handler is automatically removed after its first invocation. @param {Node} domNode - The DOM element to attach the listener to. @param {string} eventName - The name of the event (e.g., 'click', 'update'). @param {EventListenerOrEventListenerObject} handler - The callback function or object to invoke once. @returns {EventListenerOrEventListenerObject} The attached one-time handler reference. Useful for later deletion with {@link off}. */ export function one( domNode: Node, eventName: string, handler: EventListenerOrEventListenerObject ): EventListenerOrEventListenerObject { on(domNode, eventName, handler, true); return handler; } /** @hidden A controller object used to manage and potentially abort event bubbling. The `trigger()` function accepts an optional instance of this class, allowing the handler to call `.stop()` before or during dispatch to prevent further propagation in either direction (up/down). */ export class BubbleController { stopped = false; constructor() {} /** Signals that event bubbling should be halted. After calling this method, subsequent `trigger()` calls will abort before dispatching the event to any targets. */ stop() { this.stopped = true; } } /** Creates and dispatches a custom event on the specified DOM node. The event is namespaced with an `_mc` prefix by default, unless already prefixed. Supports bidirectional bubbling (up/down the DOM tree) based on the `bubble` parameter: - 'l': local dispatch only - 'u': bubble up towards ancestors - 'd': bubble down to descendants The optional `controller` allows aborting event propagation via `.stop()`. @param {HTMLElement} domNode - The DOM element on which to trigger the event. @param {string} event - The name of the event (will be prefixed if needed). @param {any} [data={}] - Additional data payload attached to `event.detail.data`. @param {string} [bubble='l'] - Bubbling direction ('l', 'u', 'd' or combinations). @param {BubbleController} [controller] - Optional controller for managing bubbling behavior. */ export function trigger( domNode: HTMLElement, event: string, data: any = {}, bubble: string = 'l', // direction of bubbling controller?: BubbleController ) { //console.warn('trigger args', ...arguments); const eventName = /^_mc/.test(event) ? event : getMcEventName(event), bubbleController = controller ? controller : new BubbleController(), ev = new CustomEvent(eventName, { detail: { data, control: bubbleController }, bubbles: false, }); if (bubbleController.stopped) return; if (/l/.test(bubble)) { domNode.dispatchEvent(ev); ev.stopImmediatePropagation(); if (bubbleController.stopped) return; } // bubble if up or down is indicated if (/[ud]/.test(bubble)) { // add missing 'l', or else no handlers are executed if (bubble.indexOf('l') === -1) { bubble += 'l'; } setTimeout(() => { // bubble up towards document.body if (bubble.indexOf('u') > -1) { let parentMc: HTMLMcElement | null | undefined = domNode.parentElement?.closest('[data-mc]'); if (parentMc) { parentMc?._mc.trigger(eventName, data, bubble, bubbleController); } } // bubble down towards DOM children if (bubble.indexOf('d') > -1) { let childNodes: DC[] | null = (domNode as HTMLMcElement)._mc.children(); if (childNodes) { childNodes.forEach((_mc: DC) => _mc.trigger(eventName, data, bubble)); // event multiplex - dont carry bubble controller } } }, 0); } }