'use strict'; import { LooseObject } from './helpers.js'; import { trigger, on, one, off, BubbleController } from './events.js'; /** Determines whether the current runtime environment is a web browser. This function works by attempting to access `this` within an anonymous function and checking if it refers to the global `window` object. It returns `true` when executed in a browser context, and `false` otherwise (e.g., Node.js). Note: While modern environments typically provide more reliable detection mechanisms, this fallback remains compatible across older runtimes where other detection techniques may be unreliable. @returns {boolean} `true` if the code is running in a browser environment, `false` otherwise. */ const isBrowser = new Function('try {return this===window;}catch(e){ return false;}'); /** Represents a DOM element that has been enhanced with microcomponent functionality. @template HTMLElement - The base HTML element type */ export interface HTMLMcElement extends HTMLElement { /** Hash object containing inner microcomponents, keyed by their unique identifiers (strings). Each value is a reference to the corresponding `MicroComponent` instance or its configuration, depending on the implementation. This property allows direct access to nested microcomponents for programmatic interaction (e.g., invoking methods or accessing state), and supports internal lifecycle management such as mounting/unmounting or updating. @type {LooseObject} @readonly */ _mc: LooseObject; } function isDOM(element: { [key: string]: any }) { return element && element instanceof HTMLElement; } /** Adds a microcomponent instance to a target DOM node under the specified key. This function initializes or updates the internal `_mc` collection on the target element, which stores references to child microcomponents. It also adds the `data-mc` attribute if missing, indicating that this DOM node hosts at least one microcomponent. @param {HTMLElement} target - The DOM element to attach the microcomponent to. @param {string} key - The property name/key under which the element will be stored in `_mc`. @param {any} [element={}] - The microcomponent instance or configuration object to attach. @returns {HTMLMcElement} The target DOM node, now enhanced with the attached microcomponent. */ function add(target: HTMLElement, key: string, element: any = {}): HTMLMcElement { let node = target as HTMLMcElement; // create the _mc property if necessary if (!node._mc) { node._mc = new (DC as any)(target); } const _mc = node._mc; // add the element with the given property key to the _mc property if (key.length && !_mc[key]) { _mc[key] = element; // add attribute if HTMLElement if (target instanceof HTMLElement && !target.getAttribute('data-mc')) { target.setAttribute('data-mc', ''); } } //console.warn('add() args: target, key, element', arguments, node); return node; } /** Removes a named microcomponent from the internal `_mc` collection of a target DOM node. This function deletes the specified key from the element's `_mc` hash object and, if the collection becomes empty, removes the `data-mc` attribute from the DOM node. @param {HTMLElement} target - The DOM element whose microcomponent should be removed. @param {string} key - The property name/key corresponding to the microcomponent to remove. */ function remove(target: HTMLElement, key: string) { if (!key) return; // silent fail const _mc = (target as unknown as HTMLMcElement)?._mc || {}; // delete the property from the HTMLElement _mc collection if (_mc && _mc[key]) { delete _mc[key]; } // remove the _mc attribute if the DOM collection is empty if (!Object.keys(_mc).length && target instanceof HTMLElement && target.hasAttribute('data-mc')) { target.removeAttribute('data-mc'); } return; } /** Runs the "Init" lifecycle event on a microcomponent instance asynchronously. This function schedules the `trigger('Init')` call within a zero-delay timeout, ensuring that all synchronous initialization is complete before the lifecycle hook executes. It skips execution for the base `DC` class itself. @param {DC} element - The microcomponent instance on which to trigger the Init event. */ function init(element: DC) { //console.warn('init(): args', ...arguments); setTimeout(() => { if (element.constructor === DC) return; element.trigger('Init'); }, 0); } /** Attaches event listeners to DOM elements based on method naming conventions. Scans the prototype of the provided element and attaches listeners for methods prefixed with `on` or `one`, corresponding to either native HTML events (e.g., 'onclick') or microcomponent-prefixed custom events (e.g., '_mcUpdate'). @param {MC|DC} element - The instance whose prototype should be scanned for event handlers. */ function autoAttachListeners(element: MC | DC) { if (!isBrowser() || element.constructor === DC) return; const target = element.target; if (target instanceof HTMLElement) { let that: LooseObject = element; Object.getOwnPropertyNames(that.constructor.prototype) .filter( (key) => /^(on[A-Z]|one[A-Z])/.test(key) && typeof that.constructor.prototype[key] === 'function' ) .forEach((key) => { let once = /^one/.test(key), withoutOnOne = key.replace(/^on[e]{0,1}/, ''), // preserves upper/lowercase for non-native events nativeEventName = withoutOnOne.toLowerCase(); // lowercase for native event names if ('on' + nativeEventName in target) { // it is a native HTML event let listener = (ev: Event) => { that[key](ev); }; // console.log( 'AAL: attach native event listener', nativeEventName, 'to', target ); target.addEventListener(nativeEventName, listener); } else { // it is a customEvent // console.log( 'AAL: attach custom event listener', '_mc' + withoutOnOne , 'to key', key, 'in', element ); let listener = (ev: CustomEvent) => { that[key](ev); }; on(target, withoutOnOne, listener as EventListenerOrEventListenerObject, once); } }); } } /** Retrieves immediate child microcomponents of a DOM node. Queries the DOM for descendant elements marked with `data-mc` that are direct children of the provided element and returns their associated `_mc` instances. Temporarily sets an internal attribute on the node to prevent recursive matches within nested subtrees. @param {HTMLElement} domNode - The DOM element whose child microcomponents to find. @returns {DC[]|null} An array of child microcomponent instances, or null if none found. */ function getMcChildNodes(domNode: HTMLElement): DC[] | null { const id = Math.random().toString().replace('.', ''); const selector = '[data-mc]:not([temp_id="' + id + '"] [data-mc] [data-mc])'; domNode.setAttribute('temp_id', id); const mcArray: unknown[] = ([...domNode.querySelectorAll(selector)] as HTMLMcElement[]) .filter((e) => !!e._mc) .map((e) => e._mc); domNode.removeAttribute('temp_id'); return mcArray.length ? (mcArray as DC[]) : null; } /** The base class for all microcomponents. Implements a `WeakRef` to the target DOM element or other data, avoiding circular references. Automatically triggers the "Init" lifecycle event and attaches listeners via `autoAttachListeners` upon construction. */ class McBase { private _target: WeakRef; // avoid circular reference /** The constructor for the base `McBase` class. Initializes a weak reference to the target and triggers auto-attachment of listeners and initialization lifecycle events. @param {any} [target={}] - The object or element this microcomponent represents. */ constructor(target?: { [key: string]: any }) { let element = this as unknown as MC | DC; this._target = new WeakRef(target || {}); //console.warn('McBase: this, element, args', target, this, element, arguments); autoAttachListeners(element); init(element as DC); // async lifecycle event } /** Returns the current target object (DOM node or other data) referenced by this microcomponent. Uses a `WeakRef` internally to avoid circular references, and safely dereferences it here. @returns {any} The target object, or undefined if the reference has been garbage collected. */ get target(): any { return this._target.deref(); } } /** A general-purpose base microcomponent that does not require a DOM target. Inherits from `McBase`, but is primarily used as a lightweight container or for logic not directly tied to DOM elements. */ class MC extends McBase { /** Constructs an instance of the general-purpose microcomponent. @param {any} [target=undefined] - Optional target object or data. */ constructor(target?: any) { super(target); } } /** A DOM-centric microcomponent that operates on a specific HTML element. Extends `McBase` and associates itself with an actual DOM node via the `_target` reference. Provides methods for event handling, DOM traversal, and interaction with sibling or ancestor microcomponents. */ class DC extends McBase { constructor(target: HTMLElement) { super(target); //console.warn('DC constructor:', arguments); } /** Static helper to attach a microcomponent instance to a DOM element. Wrapper around the `add()` utility function, providing a class-level API for adding child microcomponents to a target node. @param {HTMLElement} target - The DOM element to which to attach the microcomponent. @param {string} key - The property name/key under which to store the instance in `_mc`. @param {any} [element={}] - The microcomponent instance or config object to attach. @returns {HTMLMcElement} The target DOM element with the attached microcomponent. */ static add(target: HTMLElement, key: string, element: any = {}): HTMLMcElement { //console.warn('static DC.add() args', arguments); return add(target, key, element); } /** Static helper to remove a named microcomponent from a DOM element. Wrapper around the `remove()` utility function, providing a class-level API for removing child microcomponents. @param {HTMLElement} target - The DOM element from which to remove the microcomponent. @param {string} key - The property name/key corresponding to the microcomponent to remove. */ static remove(target: HTMLElement, key: string) { remove(target, key); } /* event handling */ off(eventName: string, handler: EventListenerOrEventListenerObject) { off(this.target, eventName, handler); } /** Attaches a persistent event listener to the DOM node. This is an instance method wrapper for `on()`, using the internal `_target` DOM reference. @param {string} eventName - The name of the event (e.g., 'click', 'update'). @param {EventListenerOrEventListenerObject} handler - The callback function or object to invoke. @returns {EventListenerOrEventListenerObject} The attached handler reference. */ on( eventName: string, handler: EventListenerOrEventListenerObject ): EventListenerOrEventListenerObject { return on(this.target, eventName, handler); } /** Attaches a one-time event listener to the DOM node. This is an instance method wrapper for `one()`, using the internal `_target` DOM reference. @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. */ one( eventName: string, handler: EventListenerOrEventListenerObject ): EventListenerOrEventListenerObject { return one(this.target, eventName, handler); } /** Triggers a custom event on the DOM node and optionally bubbles it. This is an instance method wrapper for `trigger()`, using the internal `_target` DOM reference. @param {string} eventName - 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. */ trigger(eventName: string, data: any = {}, bubble: string = 'l', controller?: BubbleController) { //console.warn('MC.trigger args', ...arguments, this); trigger(this.target, eventName, data, bubble, controller); } /** Returns an array of all ancestor microcomponents up the DOM tree. Traverses upward from this element's target, collecting all ancestor elements marked with `data-mc` and returning their `_mc` instances. @returns {DC[]|null} An array of ancestor microcomponent instances, or null if none found. */ ancestors(): DC[] | null { let currentNode: HTMLElement | null = (this as DC)?.target as HTMLMcElement, result: DC[] = []; if (!isDOM(currentNode)) return []; while ((currentNode = currentNode.closest('[data-mc]'))) { if ((currentNode as HTMLMcElement)?._mc) { result.push((currentNode as HTMLMcElement)?._mc as DC); } } return result.length ? result : null; } /** Returns the closest ancestor microcomponent or null if none found. This method is equivalent to calling `ancestors()[0]`, but more concise. @returns {DC|null} The nearest ancestor microcomponent, or null if not found. */ closest(): DC | null { let target: HTMLElement = (this as DC).target; if (!isDOM(target)) return null; return ((target.closest('[data-mc]') as HTMLMcElement)?._mc as DC) || null; } /** Returns an array of direct child microcomponents. Calls `getMcChildNodes()` internally to find immediate descendants only, excluding nested grandchildren and deeper levels. @returns {DC[]|null} An array of child microcomponent instances, or null if none found. */ children(): DC[] | null { let target: any = (this as any)?.target; if (!isDOM(target)) return null; return getMcChildNodes(target); } /** Returns an array of all descendant microcomponents recursively. Uses a `querySelectorAll` query to collect every nested element marked with `data-mc`, returning their associated `_mc` instances. @returns {DC[]|null} An array of descendant microcomponent instances, or null if none found. */ descendants(): DC[] | null { const target: any = (this as DC)?.target; if (!isDOM(target)) return []; const mcArray: unknown[] = ([...target.querySelectorAll('[data-mc]')] as HTMLMcElement[]) .filter((e) => !!e._mc) .map((e) => e._mc); return mcArray.length ? (mcArray as DC[]) : null; } } /* Autoload Section... */ const loadingDCs = new Set(); /** Creates a closure that attaches DOM elements with the specified tag name to their corresponding microcomponent instances. Used internally by `makeLoadScript()` when dynamically loading external custom element definitions. The returned function is invoked after the element definition becomes available, and performs: - Querying of all matching elements in the DOM - Instantiation of new `DC` instances - Attachment to each DOM node via `DC.add()` @param {string} element - The tag name (custom element name) of the microcomponent to instantiate. @returns {Function} A closure that performs the attachment logic when invoked. */ function attachDC(element: string): Function { return () => { const elements: HTMLElement[] = Array.from( document.querySelectorAll(`[data-mc*="${element}"]`) ).filter((element) => element instanceof HTMLElement) as HTMLElement[]; if (elements.length === 0) return; //console.log( 'loadedPromise elements', elements ); const Klass = customElements.get(element); elements.forEach((node: HTMLElement) => { try { //console.log( 'add', klass!.name, 'to', node ); DC.add(node, Klass!.name, new Klass!(node)); } catch (error) { if ((Klass as LooseObject)['create']) { let element: HTMLMcElement = (Klass as LooseObject).create(node); element.setAttribute('data-mc', node.getAttribute('data-mc')!); node.replaceWith(element); node = element; } else { throw `Class ${(Klass as LooseObject).name} doesn't have a callable constructor AND class doesn't have a static .create() method.`; } } //@ts-ignore: override - node may be null const new_mc = node .getAttribute('data-mc') .split(' ') .filter((name) => name !== element && name !== '') .join(' ') .trim(); //@ts-ignore: override - node may be null node.setAttribute('data-mc', new_mc); }); }; } /** Dynamically loads and registers a custom element definition from an external JS file. Constructs the script URL based on kebab-case naming (e.g., `my-element` → `./my/element.js`). Registers a handler to be executed once the element is defined, then appends the `