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 | 13x 13x 13x 13x 13x 13x 13x 11x 11x 11x 11x 3x 3x 3x 3x 3x 3x 11x 11x 13x 13x 13x 13x 13x 13x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 47x 47x 1x 47x 46x 46x 47x 23x 23x 23x 23x 23x 23x 6x 6x 6x 6x 6x 6x 23x 23x 46x 46x 46x 140x 97x 97x 97x 105x 12x 12x 97x 97x 97x 140x 3x 3x 140x 7x 7x 7x 7x 87x 140x 140x 85x 94x 94x 85x 46x 46x 46x 46x 12x 12x 7x 8x 8x 11x 11x 11x 11x 11x 11x 8x 8x 11x 11x 8x 8x 3x 11x 11x 7x 7x 7x 7x 7x 7x 46x 46x 23x 23x 6x 6x 1x 1x 1x 1x 6x 23x 23x 1x 1x 2x 2x 1x 1x 2x 2x 2x 2x 1x 1x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 23x 23x 23x | /**
* Checks if a given DOM element belongs to the component defined by root.
* Deals with nested component boundaries and transcluded slots.
* @param {Element} element
* @param {Element} root
* @returns {boolean}
*/
function belongsToComponent(element, root) {
let current = element;
let isTranscluded = false;
while (current && current !== root) {
if (current.nodeType === 1) {
if (current.nodeName === 'SLOT' && current.hasAttribute && current.hasAttribute('data-avenx-transcluded')) {
isTranscluded = true;
} else if (current.hasAttribute && current.hasAttribute('data-avenx-comp')) {
if (isTranscluded) {
isTranscluded = false;
} else {
return false;
}
}
}
current = current.parentNode;
}
return !isTranscluded;
}
/**
* Responsible for binding event listeners to DOM elements based on attributes.
* Uses event delegation on the root element.
*/
export class EventBinder {
/**
* Stores bound events and handlers.
* @type {WeakMap<Element, Map<string, Function>>}
* @private
*/
#boundEvents = new WeakMap();
/**
* Binds event listeners to all elements under the root that have attributes starting with '@'.
* Uses event delegation on Element roots, falls back to direct binding on DocumentFragments.
* @param {Element|DocumentFragment} root - The root element to bind events on.
* @param {Object} dispatcher - The object responsible for executing the event handler.
* @param {function(string, Event): void} dispatcher.execute - Method to execute the event.
*/
bind(root, dispatcher) {
if (!root) return;
if (root.nodeType === 11) {
this.#bindDirect(root, dispatcher);
} else {
this.#bindDelegated(root, dispatcher);
}
}
/**
* Removes all event listeners for the given root.
* @param {Element|DocumentFragment} root
*/
unbind(root) {
if (!root) return;
if (root.nodeType === 11) {
this.#unbindDirect(root);
} else {
this.#unbindDelegated(root);
}
}
#bindDelegated(root, dispatcher) {
// Collect all unique event names from elements belonging to this component
const eventNames = new Set();
const traverse = (node) => {
if (node.nodeType !== 1) return;
if (node.attributes) {
Array.from(node.attributes).forEach(attr => {
if (attr.name.startsWith('@')) {
eventNames.add(attr.name.substring(1));
}
});
}
if (node.nodeName === 'SLOT' && node.hasAttribute && node.hasAttribute('data-avenx-transcluded')) {
return;
}
if (node !== root && node.hasAttribute && node.hasAttribute('data-avenx-comp')) {
// If it's a child component root, we might want its parent-defined event handlers,
// but we do NOT traverse its children.
return;
}
const children = node.childNodes || node.children;
if (children) {
for (let i = 0; i < children.length; i++) {
traverse(children[i]);
}
}
};
traverse(root);
eventNames.forEach(eventName => {
const existing = this.#boundEvents.get(root) || new Map();
if (!existing.has(eventName)) {
const handler = event => {
let current = (event && event.target) || root;
while (current) {
if (belongsToComponent(current, root)) {
let handlerExpression = null;
if (typeof current.getAttribute === 'function') {
handlerExpression = current.getAttribute('@' + eventName);
} else if (current.attributes) {
const matchedAttr = Array.from(current.attributes)
.find(a => a.name === '@' + eventName);
handlerExpression = matchedAttr ? matchedAttr.value : null;
}
if (handlerExpression) {
dispatcher.execute(handlerExpression, event);
}
}
if (current === root) {
break;
}
current = current.parentNode;
if (event.cancelBubble) {
break;
}
}
};
root.addEventListener(eventName, handler);
existing.set(eventName, handler);
this.#boundEvents.set(root, existing);
}
});
}
#unbindDelegated(root) {
const existing = this.#boundEvents.get(root);
if (!existing) return;
existing.forEach((handler, eventName) => {
root.removeEventListener(eventName, handler);
});
this.#boundEvents.delete(root);
}
#bindDirect(root, dispatcher) {
const elements = [];
const traverse = (node) => {
if (node.nodeType !== 1 && node.nodeType !== 11) return;
if (node.nodeType === 1) {
elements.push(node);
}
if (node.nodeName === 'SLOT' && node.hasAttribute && node.hasAttribute('data-avenx-transcluded')) {
return;
}
const children = node.childNodes || node.children;
if (children) {
for (let i = 0; i < children.length; i++) {
traverse(children[i]);
}
}
};
traverse(root);
elements.forEach(el => {
if (el.nodeType !== 1) return;
if (!el.attributes) return;
Array.from(el.attributes).forEach(attr => {
if (attr.name.startsWith('@')) {
const eventName = attr.name.substring(1);
const existing = this.#boundEvents.get(el) || new Map();
if (!existing.has(eventName)) {
const handler = event => {
let handlerExpression = null;
if (typeof el.getAttribute === 'function') {
handlerExpression = el.getAttribute('@' + eventName);
} else if (el.attributes) {
const matchedAttr = Array.from(el.attributes)
.find(a => a.name === '@' + eventName);
handlerExpression = matchedAttr ? matchedAttr.value : null;
}
if (handlerExpression) {
dispatcher.execute(handlerExpression, event);
}
};
el.addEventListener(eventName, handler);
existing.set(eventName, handler);
this.#boundEvents.set(el, existing);
}
}
});
});
}
#unbindDirect(root) {
const elements = [];
const traverse = (node) => {
if (node.nodeType !== 1 && node.nodeType !== 11) return;
if (node.nodeType === 1) {
elements.push(node);
}
if (node.nodeName === 'SLOT' && node.hasAttribute && node.hasAttribute('data-avenx-transcluded')) {
return;
}
const children = node.childNodes || node.children;
if (children) {
for (let i = 0; i < children.length; i++) {
traverse(children[i]);
}
}
};
traverse(root);
elements.forEach(el => {
const existing = this.#boundEvents.get(el);
if (!existing) return;
existing.forEach((handler, eventName) => {
el.removeEventListener(eventName, handler);
});
this.#boundEvents.delete(el);
});
}
}
|