/** * ds-base-tool.js * Base class for all DSS tool components * * Enforces DSS coding standards: * - Shadow DOM encapsulation * - Automatic event listener cleanup via AbortController * - Constructable Stylesheets support * - Standardized lifecycle methods * - Logger utility integration * * Reference: .knowledge/dss-coding-standards.json */ import { logger } from '../../utils/logger.js'; /** * Base class for DSS tool components * All tool components should extend this class to ensure compliance with DSS standards */ export default class DSBaseTool extends HTMLElement { constructor() { super(); // WC-001: Shadow DOM Required this.attachShadow({ mode: 'open' }); // EVENT-003: Use AbortController for cleanup this._abortController = new AbortController(); // Track component state this._isConnected = false; logger.debug(`[${this.constructor.name}] Constructor initialized`); } /** * Standard Web Component lifecycle: called when element is added to DOM */ connectedCallback() { this._isConnected = true; logger.debug(`[${this.constructor.name}] Connected to DOM`); // Render the component this.render(); // Setup event listeners after render this.setupEventListeners(); } /** * Standard Web Component lifecycle: called when element is removed from DOM * Automatically cleans up all event listeners via AbortController */ disconnectedCallback() { this._isConnected = false; // EVENT-003: Abort all event listeners this._abortController.abort(); // Create new controller for potential re-connection this._abortController = new AbortController(); logger.debug(`[${this.constructor.name}] Disconnected from DOM, listeners cleaned up`); } /** * Centralized event binding with automatic cleanup * @param {EventTarget} target - Element to attach listener to * @param {string} type - Event type (e.g., 'click', 'mouseover') * @param {Function} handler - Event handler function * @param {Object} options - Additional addEventListener options */ bindEvent(target, type, handler, options = {}) { if (!target || typeof handler !== 'function') { logger.warn(`[${this.constructor.name}] Invalid event binding attempt`, { target, type, handler }); return; } // Add AbortController signal to options const eventOptions = { ...options, signal: this._abortController.signal }; target.addEventListener(type, handler, eventOptions); logger.debug(`[${this.constructor.name}] Event bound: ${type} on`, target); } /** * Event delegation helper for handling multiple elements with data-action attributes * @param {string} selector - CSS selector for the container element * @param {string} eventType - Event type to listen for * @param {Function} handler - Handler function that receives (action, event) */ delegateEvents(selector, eventType, handler) { const container = this.shadowRoot.querySelector(selector); if (!container) { logger.warn(`[${this.constructor.name}] Event delegation container not found: ${selector}`); return; } this.bindEvent(container, eventType, (e) => { // Find element with data-action attribute const target = e.target.closest('[data-action]'); if (target) { const action = target.dataset.action; handler(action, e, target); } }); logger.debug(`[${this.constructor.name}] Event delegation setup for ${eventType} on ${selector}`); } /** * Inject CSS styles using Constructable Stylesheets * STYLE-002: Use Constructable Stylesheets for shared styles * @param {string} cssString - CSS string to inject */ adoptStyles(cssString) { try { const sheet = new CSSStyleSheet(); sheet.replaceSync(cssString); // Append to existing stylesheets this.shadowRoot.adoptedStyleSheets = [ ...this.shadowRoot.adoptedStyleSheets, sheet ]; logger.debug(`[${this.constructor.name}] Styles adopted (${cssString.length} bytes)`); } catch (error) { logger.error(`[${this.constructor.name}] Failed to adopt styles:`, error); } } /** * Set multiple attributes at once * @param {Object} attrs - Object with attribute key-value pairs */ setAttributes(attrs) { Object.entries(attrs).forEach(([key, value]) => { if (value !== null && value !== undefined) { this.setAttribute(key, value); } }); } /** * Get attribute with fallback value * @param {string} name - Attribute name * @param {*} defaultValue - Default value if attribute doesn't exist * @returns {string|*} Attribute value or default */ getAttr(name, defaultValue = null) { return this.hasAttribute(name) ? this.getAttribute(name) : defaultValue; } /** * Render method - MUST be implemented by subclasses * Should set shadowRoot.innerHTML with component template */ render() { throw new Error(`${this.constructor.name} must implement render() method`); } /** * Setup event listeners - should be implemented by subclasses * Use this.bindEvent() or this.delegateEvents() for automatic cleanup */ setupEventListeners() { // Override in subclass if needed logger.debug(`[${this.constructor.name}] setupEventListeners() not implemented (optional)`); } /** * Trigger re-render (useful for state changes) */ rerender() { if (this._isConnected) { // Abort existing listeners before re-render this._abortController.abort(); this._abortController = new AbortController(); // Re-render and re-setup listeners this.render(); this.setupEventListeners(); logger.debug(`[${this.constructor.name}] Component re-rendered`); } } /** * Helper: Query single element in shadow DOM * @param {string} selector - CSS selector * @returns {Element|null} */ $(selector) { return this.shadowRoot.querySelector(selector); } /** * Helper: Query multiple elements in shadow DOM * @param {string} selector - CSS selector * @returns {NodeList} */ $$(selector) { return this.shadowRoot.querySelectorAll(selector); } /** * Helper: Escape HTML to prevent XSS * SECURITY-001: Sanitize user input * @param {string} str - String to escape * @returns {string} Escaped string */ escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } /** * Helper: Dispatch custom event * @param {string} eventName - Event name * @param {*} detail - Event detail payload * @param {Object} options - Event options */ emit(eventName, detail = null, options = {}) { const event = new CustomEvent(eventName, { detail, bubbles: true, composed: true, // Cross shadow DOM boundary ...options }); this.dispatchEvent(event); logger.debug(`[${this.constructor.name}] Event emitted: ${eventName}`, detail); } }