All files / client/src/events delegation.js

95.34% Statements 41/43
95.83% Branches 23/24
83.33% Functions 5/6
97.61% Lines 41/42

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148                                        20x 20x 20x 20x           20x                                   40x 2x     38x   31x     7x   7x 63x     63x     63x         63x 63x     7x                 15x 15x 1x       14x 14x   14x 1x       13x 13x         13x 13x 1x       12x 12x               8x 1x       7x 63x     7x 7x 7x 7x               21x               3x  
/**
 * Event Delegation for Coherent.js
 *
 * Document-level event delegation that routes events to handlers via
 * data-coherent-{eventType} attributes. This ensures event handlers
 * survive DOM updates since they're registered by ID, not by element.
 */
 
import { handlerRegistry as defaultRegistry } from './registry.js';
import { wrapEvent } from './wrapper.js';
 
/**
 * EventDelegation class
 * Manages document-level event listeners and routes to registered handlers
 */
export class EventDelegation {
  /**
   * @param {import('./registry.js').HandlerRegistry} [registry] - Handler registry instance
   */
  constructor(registry = defaultRegistry) {
    this.registry = registry;
    this.initialized = false;
    this.root = null;
    this.boundHandlers = new Map();
 
    /**
     * Event types to delegate
     * Focus/blur use capture phase because they don't bubble
     */
    this.eventTypes = [
      'click',
      'change',
      'input',
      'submit',
      'focus',
      'blur',
      'keydown',
      'keyup',
      'keypress',
    ];
  }
 
  /**
   * Initialize event delegation by attaching listeners to the root element
   * @param {Document|Element} [root=document] - Root element for event delegation
   */
  initialize(root = typeof document !== 'undefined' ? document : null) {
    if (this.initialized) {
      return;
    }
 
    if (!root) {
      // No DOM available (SSR context)
      return;
    }
 
    this.root = root;
 
    for (const eventType of this.eventTypes) {
      const handler = (event) => this.handleEvent(event, eventType);
 
      // Focus and blur don't bubble - must use capture phase
      const useCapture = eventType === 'focus' || eventType === 'blur';
 
      // Submit needs preventDefault capability, others can be passive
      const options = {
        capture: useCapture,
        passive: eventType !== 'submit',
      };
 
      root.addEventListener(eventType, handler, options);
      this.boundHandlers.set(eventType, { handler, options });
    }
 
    this.initialized = true;
  }
 
  /**
   * Handle a delegated event
   * @param {Event} event - The DOM event
   * @param {string} eventType - The type of event (click, change, etc.)
   */
  handleEvent(event, eventType) {
    const target = event.target;
    if (!target || typeof target.closest !== 'function') {
      return;
    }
 
    // Find the nearest element with the appropriate data attribute
    const attrName = `data-coherent-${eventType}`;
    const delegateTarget = target.closest(`[${attrName}]`);
 
    if (!delegateTarget) {
      return;
    }
 
    // Get the handler ID from the attribute
    const handlerId = delegateTarget.getAttribute(attrName);
    Iif (!handlerId) {
      return;
    }
 
    // Look up the handler in the registry
    const entry = this.registry.get(handlerId);
    if (!entry) {
      return;
    }
 
    // Wrap the event with component context and call the handler
    const wrappedEvent = wrapEvent(event, delegateTarget, entry.componentRef);
    entry.handler(wrappedEvent);
  }
 
  /**
   * Destroy the event delegation system
   * Removes all listeners and clears the registry
   */
  destroy() {
    if (!this.initialized || !this.root) {
      return;
    }
 
    // Remove all event listeners
    for (const [eventType, { handler, options }] of this.boundHandlers) {
      this.root.removeEventListener(eventType, handler, options);
    }
 
    this.boundHandlers.clear();
    this.registry.clear();
    this.initialized = false;
    this.root = null;
  }
 
  /**
   * Check if the delegation system is initialized
   * @returns {boolean} True if initialized
   */
  isInitialized() {
    return this.initialized;
  }
}
 
/**
 * Singleton event delegation instance
 * Use this for global event delegation
 */
export const eventDelegation = new EventDelegation();