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 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 | 10x 10x 10x 10x 383x 27x 27x 27x 27x 27x 80x 7x 73x 31x 42x 42x 42x 378x 1040x 5x 1040x 1038x 1416x 1033x 383x 383x 383x 383x 43x 43x 1x 42x 42x 42x 42x 42x 42x 45x 45x 45x 44x 44x 44x 1x 44x 2x 42x 42x 13x 29x 29x 29x 29x 108x 66x 42x 383x 42x 42x 42x 42x 42x 21x 10x | /**
* 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';
/** Event types listened for from initialize(); others are added on demand. */
const DEFAULT_EVENT_TYPES = [
'click',
'change',
'input',
'submit',
'focus',
'blur',
'keydown',
'keyup',
'keypress',
];
/**
* Events that do not bubble. They are caught in the capture phase and only
* reach a handler on the event's own target.
*/
const NON_BUBBLING_EVENTS = new Set([
'mouseenter', 'mouseleave', 'pointerenter', 'pointerleave',
'load', 'error', 'abort', 'scroll', 'toggle', 'invalid', 'cancel', 'close',
'play', 'pause', 'ended', 'playing', 'waiting', 'seeked', 'seeking',
'canplay', 'canplaythrough', 'durationchange', 'emptied', 'loadeddata',
'loadedmetadata', 'loadstart', 'progress', 'ratechange', 'stalled',
'suspend', 'timeupdate', 'volumechange',
]);
/**
* Focus events do not bubble either, but a handler on a container is expected
* to hear its fields' focus (like focusin), so they are captured and then
* walked up the tree like bubbling events.
*/
const CAPTURED_BUBBLING_EVENTS = new Set(['focus', 'blur']);
/**
* Scroll-blocking events are registered passive, as browsers do by default at
* document level, so delegation never delays scrolling. preventDefault() is
* ignored for them; every other type is registered non-passive.
*/
const PASSIVE_EVENTS = new Set(['touchstart', 'touchmove', 'wheel', 'mousewheel', 'scroll']);
/**
* Listener options for a delegated event type.
* @param {string} eventType
* @returns {{capture: boolean, passive: boolean}}
*/
export function getListenerOptions(eventType) {
return {
capture: CAPTURED_BUBBLING_EVENTS.has(eventType) || NON_BUBBLING_EVENTS.has(eventType),
passive: PASSIVE_EVENTS.has(eventType),
};
}
/**
* 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 listened for by initialize(). Any other type is added the
* first time listen() is called for it.
*/
this.eventTypes = [...DEFAULT_EVENT_TYPES];
}
/**
* 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;
this.initialized = true;
for (const eventType of this.eventTypes) {
this.attach(eventType);
}
}
/**
* Make sure events of `eventType` are delegated. Called by hydrate() for
* every event type a component handles, so types beyond the defaults
* (dblclick, mouseenter, pointerdown, ...) work too.
* @param {string} eventType - DOM event type, e.g. 'dblclick'
*/
listen(eventType) {
if (!this.eventTypes.includes(eventType)) {
this.eventTypes.push(eventType);
}
if (this.initialized) {
this.attach(eventType);
}
}
/** @private */
attach(eventType) {
if (this.boundHandlers.has(eventType)) {
return;
}
const handler = (event) => this.handleEvent(event, eventType);
const options = getListenerOptions(eventType);
this.root.addEventListener(eventType, handler, options);
this.boundHandlers.set(eventType, { handler, options });
}
/**
* Handle a delegated event
*
* Runs the handler of the nearest element carrying
* `data-coherent-{eventType}`, then those of its ancestors, like native
* bubbling, until a handler stops propagation. Non-bubbling events only run
* a handler on the target itself.
*
* @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;
}
const attrName = `data-coherent-${eventType}`;
const selector = `[${attrName}]`;
const bubbles = !NON_BUBBLING_EVENTS.has(eventType);
let current = target.closest(selector);
Iif (!bubbles && current !== target) {
return;
}
while (current) {
const handlerId = current.getAttribute(attrName);
const entry = handlerId ? this.registry.get(handlerId) : undefined;
if (entry) {
// Wrap the event with component context and call the handler
const wrappedEvent = wrapEvent(event, current, entry.componentRef);
entry.handler(wrappedEvent);
if (wrappedEvent.propagationStopped || event.cancelBubble === true) {
return;
}
}
if (!bubbles) {
return;
}
const parent = current.parentElement;
if (!parent || typeof parent.closest !== 'function' || !this.isWithinRoot(parent)) {
return;
}
current = parent.closest(selector);
}
}
/** @private */
isWithinRoot(element) {
const root = this.root;
Eif (!root || root.nodeType === 9 || typeof root.contains !== 'function') {
return true;
}
return root.contains(element);
}
/**
* 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.eventTypes = [...DEFAULT_EVENT_TYPES];
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();
|