/** * -------------------------------------------- * @file AdminLTE base-component.ts * @description Shared component lifecycle for AdminLTE plugins: a per-element * instance registry (getInstance / getOrCreateInstance / dispose) and a * consistent custom-event contract, mirroring Bootstrap's component API. * @license MIT * -------------------------------------------- */ /** * Component registry: element -> (data key -> component instance). A single * element can host several components at once, each stored under its own * DATA_KEY. WeakMap keys don't prevent garbage collection, so instances die * with their elements — important under Hotwired Turbo, which swaps the whole * on navigation. */ const componentRegistry = new WeakMap>() class BaseComponent { /** * Subclasses must override this getter to declare their own name. */ static get NAME(): string { throw new Error('Component subclasses must override the static NAME getter.') } /** * Key this component is registered under: `lte.`. */ static get DATA_KEY(): string { return `lte.${this.NAME}` } /** * Untyped registry lookup. Every component exposes a typed wrapper * (e.g. CardWidget.getInstance()) built on top of this. * * @param element The element to look up. * @returns The instance for this component, or null if there is none. */ protected static _getInstance(element: Element | null | undefined): BaseComponent | null { if (!element) { return null } return componentRegistry.get(element)?.get(this.DATA_KEY) ?? null } /** * The element this instance is attached to. */ _element: HTMLElement /** * Attach a new instance to the given element and register it under the * subclass's DATA_KEY. * * @param element The element to attach this instance to. */ constructor(element: HTMLElement) { this._element = element const instances = componentRegistry.get(element) ?? new Map() componentRegistry.set(element, instances) instances.set((this.constructor as typeof BaseComponent).DATA_KEY, this) } /** * Remove this instance from the registry so getInstance() no longer * returns it. Subclasses release their own resources, then call * super.dispose(). */ dispose(): void { const instances = componentRegistry.get(this._element) instances?.delete((this.constructor as typeof BaseComponent).DATA_KEY) // Drop the element's entry once it holds no components at all. if (instances?.size === 0) { componentRegistry.delete(this._element) } } } /** * Dispatch a namespaced custom event that bubbles — so applications can * listen once on `document` — and can optionally carry a payload or be * canceled. Returns the event so callers can check `defaultPrevented`. * * @param element The element to dispatch the event on. * @param name The namespaced event name, e.g. `collapse.lte.push-menu`. * @param options `cancelable` opts the event into preventDefault(); `detail` * is the payload handed to listeners. * @returns The dispatched event, after listeners have run. */ const dispatchCustomEvent = ( element: Element, name: string, options: { cancelable?: boolean; detail?: T } = {} ): CustomEvent => { const event = new CustomEvent(name, { bubbles: true, cancelable: options.cancelable ?? false, detail: options.detail }) element.dispatchEvent(event) return event } export { BaseComponent, dispatchCustomEvent }