import { ctorToFunction, EventEmitter, defaultInjector, module, Event as klEvent, } from '@akala/core'; import type { Module, SpecialNextParam, MiddlewarePromise, Subscription, IEventSink, IEvent } from '@akala/core'; /** * The main module for bootstrapping Akala client-side services. */ export const bootstrapModule: Module = module('akala', 'akala-services', 'controls'); bootstrapModule.activateEvent.maxListeners = 100; /** * Module for registering core Akala services. */ const serviceModule: Module = module('akala-services'); export { serviceModule }; /** * Resolves a relative URL against the base URL defined in the document's tag. * @param namespace The relative URL path to resolve. * @returns The full resolved URL string. */ export function resolveUrl(namespace: string): string { const root = document.head.querySelector('base').href; return new URL(namespace, root).toString(); } defaultInjector.register('$resolveUrl', resolveUrl); /** * Decorator for registering a service with dependency injection. * @param name The service identifier. * @param toInject Dependency names to inject into the service constructor. */ export function service(name: string | symbol, ...toInject: string[]) { return function (target: new (...args: unknown[]) => unknown) { let instance: unknown = null; if (toInject == null || (toInject.length === 0 && target.length > 0)) throw new Error('missing inject names'); else serviceModule.registerFactory(name, () => { return instance || serviceModule.injectWithName(toInject, (...args: unknown[]) => { instance = ctorToFunction(target)(...args); return instance; })(); }); }; } // import component, { webComponent } from './decorators/component.js'; import { Container, type ICommandProcessor, Metadata, type StructuredParameters } from '@akala/commands/browser'; // export { component, webComponent }; export { AttributeComposer, type WebComponent, webComponent, wcObserve, databind, type HtmlControlElement } from './behaviors/shared.js'; /** * Command processor that handles events after remote processing. */ export class LocalAfterRemoteProcessor implements ICommandProcessor { constructor( private readonly inner: ICommandProcessor, public readonly eventEmitter: EventEmitter, Metadata.Command]>>> = new EventEmitter() ) { } /** * Handles a command and emits events on failure. * @param origin The command origin container. * @param cmd The command metadata. * @param param The structured command parameters. */ async handle( origin: Container, cmd: Metadata.Command, param: StructuredParameters ): MiddlewarePromise { try { const error = await this.inner.handle(origin, cmd, param); return error; } catch (e) { try { this.eventEmitter.emit(cmd.name, e, param, cmd); } catch (e) { return e; } throw e; } } } export { FormInjector, FormComposer } from './behaviors/form.js'; export { IfComposer } from './behaviors/if.js'; export { DataBind, DataContext } from './behaviors/context.js'; export { EventComposer } from './behaviors/events.js'; export { CssClass, CssClassComposer } from './behaviors/cssClass.js'; export { I18nComposer } from './behaviors/i18n.js'; export { ClientBindings } from './client-bindings.js'; export * from './dom-helpers.js'; /** * Event sink for client-side events. */ export type IClientEventSink = IEventSink<[TEvent], void, { once?: boolean }>; /** * Event type for client-side events. */ export type IClientEvent = IEvent<[TEvent], void>; /** * Base class for client-side events with disposal support. */ export class ClientEvent extends klEvent<[TEvent], void> { } /** * Subscribes to DOM events with cleanup management. * @param item The HTML element to observe. * @param eventName The event name or object mapping names to handlers. * @param handler The event handler function. * @returns Subscription(s) to manage event listeners. */ export function subscribe void }>>( item: HTMLElement, eventHandlers: T ): Record; export function subscribe( item: HTMLElement, eventName: T, handler: (ev: HTMLElementEventMap[T]) => void ): Subscription; export function subscribe( item: HTMLElement, eventNameOrHandlers: T | { [key in T]: (ev: HTMLElementEventMap[key]) => void }, handler?: (ev: HTMLElementEventMap[T]) => void ): Subscription | Record { if (typeof (eventNameOrHandlers) == 'string' && handler) { item.addEventListener(eventNameOrHandlers, handler); let removed = false; return () => { if (removed) return false; removed = true; item.removeEventListener(eventNameOrHandlers, handler) }; } else if (typeof (eventNameOrHandlers) == 'object') { return Object.fromEntries(Object.entries(eventNameOrHandlers).map(([eventName, handler]) => { return [eventName, subscribe(item, eventName as T, handler as (ev: HTMLElementEventMap[T]) => void) as Subscription] })) as Record; } } /** * Creates an `IClientEventSink` that listens for a specified event on a given `EventTarget`. * * @template TEventName - The name of the event to listen for, which must be a key of `HTMLElementEventMap`. * @template TEvent - The type of the event object, which defaults to the event type corresponding to `TEventName` in `HTMLElementEventMap`. * * @param {EventTarget} x - The target to listen for events on. * @param {TEventName} eventName - The name of the event to listen for. * @returns {IClientEventSink} An `IClientEventSink` that emits the specified event. */ export function fromEvent(x: EventTarget, eventName: TEventName): IClientEventSink { const event = new ClientEvent(); const handler = event.emit.bind(event); event[Symbol.dispose] = () => { x.removeEventListener(eventName, handler); klEvent.prototype[Symbol.dispose].call(event); } x.addEventListener(eventName, handler); return event; } /** * Creates a client event sink that pipes events from a specified event target. * * @param source - The source event sink that controls the subscription. * @param x - The event target from which to listen for events. * @param eventName - The name of the event to listen for on the event target. * @returns A client event sink that emits events from the specified event target. * * @template TEventName - The type of the event name. * @template TEvent - The type of the event. */ export function pipefromEvent(source: IEventSink<[boolean], void, { once?: boolean }>, x: EventTarget, eventName: TEventName): IClientEventSink { const event = new ClientEvent(); let sub: Subscription; let clientEvent: IClientEventSink; source.addListener(ev => { if (ev && !sub) { sub = (clientEvent || (clientEvent = fromEvent(x, eventName))).addListener((ev) => { event.emit(ev); }) } else if (sub && !ev) { sub(); sub = null; } }); event[Symbol.dispose] = () => { sub?.(); clientEvent[Symbol.dispose](); ClientEvent.prototype[Symbol.dispose].call(event); } return event; }