dom: deliver standalone event target listeners
This commit is contained in:
1 parent
ca551a996b
commit
ec5950da06
4 files changed
+197
-1
No files matched your search
Vendored
+97
-1
@@ -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() {
|
||||
|
||||
@@ -4457,4 +4457,60 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(result, serde_json::json!(["menu", "true"]));
|
||||
}
|
||||
|
||||
/// Framework schedulers commonly subclass EventTarget for their own
|
||||
/// lifecycle events. These targets have no backing DOM node, but must
|
||||
/// still deliver callbacks (including object, once, and signal listeners).
|
||||
#[test]
|
||||
fn standalone_event_target_delivers_framework_lifecycle_events() {
|
||||
let mut rt = setup_runtime("<div></div>");
|
||||
let result = rt
|
||||
.evaluate(
|
||||
r#"
|
||||
const TypedEventTarget = class extends EventTarget {};
|
||||
const target = new TypedEventTarget();
|
||||
const calls = [];
|
||||
const removed = () => calls.push("removed");
|
||||
const controller = new AbortController();
|
||||
const node = { id: "canvas-ref" };
|
||||
|
||||
target.addEventListener("insert", (event) => calls.push(event.node.id));
|
||||
target.addEventListener("insert", { handleEvent() { calls.push("object"); } });
|
||||
target.addEventListener("insert", () => calls.push("once"), { once: true });
|
||||
target.addEventListener("insert", removed);
|
||||
target.removeEventListener("insert", removed);
|
||||
target.addEventListener("insert", () => calls.push("aborted"), {
|
||||
signal: controller.signal,
|
||||
});
|
||||
controller.abort();
|
||||
|
||||
const first = new Event("insert", { cancelable: true });
|
||||
first.node = node;
|
||||
const firstResult = target.dispatchEvent(first);
|
||||
const second = new Event("insert");
|
||||
second.node = node;
|
||||
const secondResult = target.dispatchEvent(second);
|
||||
return [
|
||||
target instanceof EventTarget,
|
||||
calls,
|
||||
firstResult,
|
||||
secondResult,
|
||||
first.target === target,
|
||||
first.currentTarget === null,
|
||||
];
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
serde_json::json!([
|
||||
true,
|
||||
["canvas-ref", "object", "once", "canvas-ref", "object"],
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user