// ============================================================================ // Stylescape | Base Element Manager // ============================================================================ // Abstract base class for managing HTML elements via data-ss-* attributes. // Provides template for auto-discovering and initializing custom components. // ============================================================================ /** * Configuration options for BaseElementManager */ export interface BaseElementManagerOptions { /** Custom attribute name to look for (default: data-ss-{componentName}) */ attribute?: string; /** Whether to auto-initialize on DOMContentLoaded */ autoInit?: boolean; /** Root element to search within */ root?: Element | Document; } /** * Abstract base class for managing collections of elements. * Subclasses should implement the abstract methods to define * component-specific behavior. * * @example * ```typescript * class MyWidgetManager extends BaseElementManager { * protected getAttributeName(): string { * return "data-ss-widget" * } * * protected async createElement(element: HTMLElement, config: any): Promise { * return new MyWidget(element, config) * } * } * ``` */ export abstract class BaseElementManager { /** Map of element IDs to component instances */ protected elements: Map = new Map(); /** Configuration options */ protected options: Required; /** Unique ID counter for elements without IDs */ private static idCounter = 0; constructor(options: BaseElementManagerOptions = {}) { this.options = { attribute: options.attribute || `data-ss-${this.getComponentName()}`, autoInit: options.autoInit !== false, root: options.root || document, }; if (this.options.autoInit) { if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => this.init(), ); } else { this.init(); } } } // ======================================================================== // Abstract Methods - Must be implemented by subclasses // ======================================================================== /** * Returns the component name used in data attributes. * E.g., "preloader" for data-ss-preloader */ protected abstract getComponentName(): string; /** * Creates a component instance for the given element. * * @param element - The DOM element to create the component for * @param config - Configuration parsed from data attributes * @returns The created component instance */ protected abstract createElement( element: HTMLElement, config: Record, ): Promise | T; // ======================================================================== // Public Methods // ======================================================================== /** * Initialize all elements matching the attribute selector */ public async init(): Promise { const selector = `[${this.options.attribute}]`; const elements = this.options.root.querySelectorAll(selector); for (const element of Array.from(elements)) { await this.processElement(element); } } /** * Initialize a single element */ public async initElement(element: HTMLElement): Promise { return this.processElement(element); } /** * Get a component instance by element ID */ public get(elementId: string): T | undefined { return this.elements.get(elementId); } /** * Get all component instances */ public getAll(): Map { return new Map(this.elements); } /** * Check if an element has been initialized */ public has(elementId: string): boolean { return this.elements.has(elementId); } /** * Destroy a component instance */ public destroy(elementId: string): boolean { const instance = this.elements.get(elementId); if ( instance && typeof (instance as unknown as { destroy?: () => void }) .destroy === "function" ) { (instance as unknown as { destroy: () => void }).destroy(); } return this.elements.delete(elementId); } /** * Destroy all component instances */ public destroyAll(): void { this.elements.forEach((instance, id) => { this.destroy(id); }); } // ======================================================================== // Protected Methods // ======================================================================== /** * Process a single element - parse config and create component */ protected async processElement(element: HTMLElement): Promise { // Ensure element has an ID if (!element.id) { element.id = `${this.getComponentName()}-${++BaseElementManager.idCounter}`; } // Skip if already initialized if (this.elements.has(element.id)) { return this.elements.get(element.id) || null; } // Parse configuration from data attributes const config = this.parseConfig(element); try { // Create component instance const instance = await this.createElement(element, config); this.elements.set(element.id, instance); // Mark as initialized element.setAttribute( `${this.options.attribute}-initialized`, "true", ); return instance; } catch (error) { console.error( `[Stylescape] Error initializing ${this.getComponentName()}:`, error, ); return null; } } /** * Parse configuration from element's data attributes */ protected parseConfig(element: HTMLElement): Record { const config: Record = {}; const prefix = `${this.options.attribute}-`; const jsonAttr = `${this.options.attribute}-config`; // Check for JSON config first const jsonConfig = element.getAttribute(jsonAttr); if (jsonConfig) { try { Object.assign(config, JSON.parse(jsonConfig)); } catch (_e) { console.warn( `[Stylescape] Invalid JSON config for ${this.getComponentName()}`, ); } } // Parse individual data attributes Array.from(element.attributes).forEach((attr) => { if (attr.name.startsWith(prefix) && attr.name !== jsonAttr) { const key = attr.name .slice(prefix.length) .replace(/-([a-z])/g, (_, c) => c.toUpperCase()); let value: unknown = attr.value; if (value === "true") value = true; else if (value === "false") value = false; else if (!isNaN(Number(value)) && value !== "") value = Number(value); config[key] = value; } }); return config; } } export default BaseElementManager;