dom: deliver standalone event target listeners

This commit is contained in:
SGavrl committed 2026-07-26 14:27:29 +00:00
1 parent ca551a996b
commit ec5950da06
4 files changed
+197 -1

No files matched your search

+97 -1
View File
@@ -753,6 +753,94 @@ function _shallowCloneNode(node) {
return el;
}
// EventTarget listener state belongs to the JS wrapper rather than the backing
// DOM node. This is also what makes `new EventTarget()` and subclasses used by
// framework schedulers work: those targets deliberately have no native node id.
const _eventTargetListeners = new WeakMap();
function _eventCapture(options) {
return typeof options === "boolean" ? options : !!(options && options.capture);
}
function _eventTargetAdd(target, type, callback, options) {
if (callback == null) return;
const isFunction = typeof callback === "function";
if (!isFunction && typeof callback.handleEvent !== "function") return;
type = String(type);
const capture = _eventCapture(options);
const signal = options && typeof options === "object" ? options.signal : null;
if (signal && signal.aborted) return;
let byType = _eventTargetListeners.get(target);
if (!byType) {
byType = new Map();
_eventTargetListeners.set(target, byType);
}
let listeners = byType.get(type);
if (!listeners) {
listeners = [];
byType.set(type, listeners);
}
if (listeners.some((entry) => entry.callback === callback && entry.capture === capture)) return;
const entry = {
callback,
capture,
once: !!(options && typeof options === "object" && options.once),
passive: !!(options && typeof options === "object" && options.passive),
signal,
abortHandler: null,
};
listeners.push(entry);
if (signal && typeof signal.addEventListener === "function") {
entry.abortHandler = () => _eventTargetRemove(target, type, callback, capture);
signal.addEventListener("abort", entry.abortHandler, { once: true });
}
}
function _eventTargetRemove(target, type, callback, options) {
const byType = _eventTargetListeners.get(target);
if (!byType) return;
type = String(type);
const listeners = byType.get(type);
if (!listeners) return;
const capture = _eventCapture(options);
for (let i = 0; i < listeners.length; i++) {
const entry = listeners[i];
if (entry.callback !== callback || entry.capture !== capture) continue;
listeners.splice(i, 1);
if (entry.signal && entry.abortHandler && typeof entry.signal.removeEventListener === "function") {
entry.signal.removeEventListener("abort", entry.abortHandler);
}
break;
}
if (listeners.length === 0) byType.delete(type);
if (byType.size === 0) _eventTargetListeners.delete(target);
}
function _eventTargetDispatch(target, event) {
if (!event || typeof event.type === "undefined") {
throw new TypeError("Failed to execute 'dispatchEvent' on 'EventTarget': parameter 1 is not of type 'Event'.");
}
if (String(event.type) === "") {
throw new DOMException("The event's type was not specified.", "InvalidStateError");
}
if (!event.target) event.target = target;
event.currentTarget = target;
event.eventPhase = 2;
const listeners = (_eventTargetListeners.get(target)?.get(String(event.type)) || []).slice();
for (const entry of listeners) {
const current = _eventTargetListeners.get(target)?.get(String(event.type));
if (!current || !current.includes(entry)) continue;
if (entry.once) _eventTargetRemove(target, event.type, entry.callback, entry.capture);
const callback = entry.callback;
try {
if (typeof callback === "function") callback.call(target, event);
else callback.handleEvent.call(callback, event);
} catch (error) {
console.error(error);
}
if (event._immediatePropagationStopped) break;
}
event.currentTarget = null;
event.eventPhase = 0;
return !event.defaultPrevented;
}
class Node {
static ELEMENT_NODE = 1;
static ATTRIBUTE_NODE = 2;
@@ -1033,7 +1121,15 @@ class Node {
return true;
}
isSameNode(other) { return other && this._nid === other._nid; }
addEventListener() {} removeEventListener() {} dispatchEvent() { return true; }
addEventListener(type, callback, options) {
_eventTargetAdd(this, type, callback, options);
}
removeEventListener(type, callback, options) {
_eventTargetRemove(this, type, callback, options);
}
dispatchEvent(event) {
return _eventTargetDispatch(this, event);
}
}
class CharacterData extends Node {
get data() {