/** * Trigger Manager for LiveComponents * * Handles advanced trigger options: * - delay: Delay before execution * - throttle: Throttle instead of debounce * - once: Execute only once * - changed: Only execute on value change * - from: Event from another element * - load: Execute on element load */ export class TriggerManager { constructor() { this.triggeredOnce = new Set(); // Track elements that have triggered once this.throttleTimers = new Map(); // Track throttle timers this.lastValues = new Map(); // Track last values for 'changed' trigger this.loadHandled = new Set(); // Track elements that have handled load event } /** * Setup trigger for an element * * @param {HTMLElement} element - Element to setup trigger for * @param {string} componentId - Component ID * @param {string} action - Action name * @param {Function} handler - Action handler function * @param {string} defaultEvent - Default event type (click, input, change, etc.) */ setupTrigger(element, componentId, action, handler, defaultEvent = 'click') { // Parse trigger options const delay = this.parseDelay(element.dataset.lcTriggerDelay); const throttle = this.parseThrottle(element.dataset.lcTriggerThrottle); const once = element.dataset.lcTriggerOnce === 'true'; const changed = element.dataset.lcTriggerChanged === 'true'; const from = element.dataset.lcTriggerFrom; const load = element.dataset.lcTriggerLoad === 'true'; // Handle 'once' trigger - check if already triggered if (once) { const triggerKey = `${componentId}_${action}_${this.getElementKey(element)}`; if (this.triggeredOnce.has(triggerKey)) { return; // Already triggered, don't setup again } } // Handle 'load' trigger if (load) { const loadKey = `${componentId}_${action}_${this.getElementKey(element)}`; if (!this.loadHandled.has(loadKey)) { this.loadHandled.add(loadKey); // Execute immediately if element is already loaded if (document.readyState === 'complete' || document.readyState === 'interactive') { this.executeWithOptions(element, componentId, action, handler, delay, throttle, changed); } else { // Wait for load window.addEventListener('load', () => { this.executeWithOptions(element, componentId, action, handler, delay, throttle, changed); }, { once: true }); } } } // Handle 'from' trigger - delegate event from another element if (from) { const sourceElement = document.querySelector(from); if (sourceElement) { this.setupDelegatedTrigger(sourceElement, element, componentId, action, handler, defaultEvent, delay, throttle, changed); return; // Don't setup direct trigger } else { console.warn(`[TriggerManager] Source element not found for trigger-from: ${from}`); } } // Setup direct trigger const eventType = this.getEventType(element, defaultEvent); const wrappedHandler = (e) => { // Check 'once' trigger if (once) { const triggerKey = `${componentId}_${action}_${this.getElementKey(element)}`; if (this.triggeredOnce.has(triggerKey)) { return; // Already triggered } this.triggeredOnce.add(triggerKey); } this.executeWithOptions(element, componentId, action, handler, delay, throttle, changed, e); }; element.addEventListener(eventType, wrappedHandler); } /** * Setup delegated trigger (trigger-from) * * @param {HTMLElement} sourceElement - Element that triggers the event * @param {HTMLElement} targetElement - Element that receives the action * @param {string} componentId - Component ID * @param {string} action - Action name * @param {Function} handler - Action handler * @param {string} eventType - Event type * @param {number} delay - Delay in ms * @param {number} throttle - Throttle in ms * @param {boolean} changed - Only on value change */ setupDelegatedTrigger(sourceElement, targetElement, componentId, action, handler, eventType, delay, throttle, changed) { const wrappedHandler = (e) => { this.executeWithOptions(targetElement, componentId, action, handler, delay, throttle, changed, e); }; sourceElement.addEventListener(eventType, wrappedHandler); } /** * Execute handler with trigger options * * @param {HTMLElement} element - Element * @param {string} componentId - Component ID * @param {string} action - Action name * @param {Function} handler - Handler function * @param {number} delay - Delay in ms * @param {number} throttle - Throttle in ms * @param {boolean} changed - Only on value change * @param {Event} event - Original event */ executeWithOptions(element, componentId, action, handler, delay, throttle, changed, event) { // Check 'changed' trigger if (changed) { const currentValue = this.getElementValue(element); const valueKey = `${componentId}_${action}_${this.getElementKey(element)}`; const lastValue = this.lastValues.get(valueKey); if (currentValue === lastValue) { return; // Value hasn't changed } this.lastValues.set(valueKey, currentValue); } // Apply throttle if (throttle > 0) { const throttleKey = `${componentId}_${action}_${this.getElementKey(element)}`; const lastExecution = this.throttleTimers.get(throttleKey); if (lastExecution && Date.now() - lastExecution < throttle) { return; // Throttled } this.throttleTimers.set(throttleKey, Date.now()); } // Apply delay if (delay > 0) { setTimeout(() => { handler(event); }, delay); } else { handler(event); } } /** * Parse delay value (e.g., "500ms", "1s", "500") * * @param {string} delayStr - Delay string * @returns {number} - Delay in milliseconds */ parseDelay(delayStr) { if (!delayStr) return 0; const match = delayStr.match(/^(\d+)(ms|s)?$/); if (!match) return 0; const value = parseInt(match[1]); const unit = match[2] || 'ms'; return unit === 's' ? value * 1000 : value; } /** * Parse throttle value (e.g., "100ms", "1s", "100") * * @param {string} throttleStr - Throttle string * @returns {number} - Throttle in milliseconds */ parseThrottle(throttleStr) { if (!throttleStr) return 0; const match = throttleStr.match(/^(\d+)(ms|s)?$/); if (!match) return 0; const value = parseInt(match[1]); const unit = match[2] || 'ms'; return unit === 's' ? value * 1000 : value; } /** * Get event type for element * * @param {HTMLElement} element - Element * @param {string} defaultEvent - Default event type * @returns {string} - Event type */ getEventType(element, defaultEvent) { // For form elements, use appropriate event if (element.tagName === 'FORM') { return 'submit'; } if (element.tagName === 'SELECT') { return 'change'; } if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') { return element.type === 'checkbox' || element.type === 'radio' ? 'change' : 'input'; } return defaultEvent; } /** * Get element value for 'changed' trigger * * @param {HTMLElement} element - Element * @returns {string} - Element value */ getElementValue(element) { if (element.tagName === 'INPUT') { if (element.type === 'checkbox' || element.type === 'radio') { return element.checked ? 'true' : 'false'; } return element.value || ''; } if (element.tagName === 'TEXTAREA' || element.tagName === 'SELECT') { return element.value || ''; } return element.textContent || ''; } /** * Get unique key for element (for tracking) * * @param {HTMLElement} element - Element * @returns {string} - Unique key */ getElementKey(element) { return element.id || element.name || `${element.tagName}_${element.className}` || 'unknown'; } /** * Clean up resources for component * * @param {string} componentId - Component ID */ cleanup(componentId) { // Remove all entries for this component const keysToRemove = []; for (const key of this.triggeredOnce) { if (key.startsWith(`${componentId}_`)) { keysToRemove.push(key); } } keysToRemove.forEach(key => this.triggeredOnce.delete(key)); // Clean throttle timers const timersToRemove = []; for (const key of this.throttleTimers.keys()) { if (key.startsWith(`${componentId}_`)) { timersToRemove.push(key); } } timersToRemove.forEach(key => this.throttleTimers.delete(key)); // Clean last values const valuesToRemove = []; for (const key of this.lastValues.keys()) { if (key.startsWith(`${componentId}_`)) { valuesToRemove.push(key); } } valuesToRemove.forEach(key => this.lastValues.delete(key)); // Clean load handled const loadToRemove = []; for (const key of this.loadHandled) { if (key.startsWith(`${componentId}_`)) { loadToRemove.push(key); } } loadToRemove.forEach(key => this.loadHandled.delete(key)); } } // Create singleton instance export const triggerManager = new TriggerManager(); export default triggerManager;