'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);
}
}