Refactor events

Removes some duplication between xhr/event_target and dom/event_target.

Implement 'once' option of addEventListener.
This commit is contained in:
Karl Seguin
2025-05-27 11:00:26 +08:00
parent f0017c3e92
commit 9ce3fc9f8e
6 changed files with 100 additions and 74 deletions

View File

@@ -41,68 +41,14 @@ pub const EventTarget = struct {
// JS funcs // JS funcs
// -------- // --------
const AddEventListenerOpts = union(enum) {
opts: Opts,
capture: bool,
const Opts = struct {
capture: ?bool,
// We ignore this property. It seems to be largely used to help the
// browser make certain performance tweaks (i.e. the browser knows
// that the listener won't call preventDefault() and thus can safely
// run the default as needed).
passive: ?bool,
once: ?bool, // currently does nothing
signal: ?bool, // currently does nothing
};
};
pub fn _addEventListener( pub fn _addEventListener(
self: *parser.EventTarget, self: *parser.EventTarget,
typ: []const u8, typ: []const u8,
listener: EventHandler.Listener, listener: EventHandler.Listener,
opts_: ?AddEventListenerOpts, opts: ?EventHandler.Opts,
page: *Page, page: *Page,
) !void { ) !void {
var capture = false; _ = try EventHandler.register(page.arena, self, typ, listener, opts);
if (opts_) |opts| {
switch (opts) {
.capture => |c| capture = c,
.opts => |o| {
// Done this way so that, for common cases that _only_ set
// capture, i.e. {captrue: true}, it works.
// But for any case that sets any of the other flags, we
// error. If we don't error, this function call would succeed
// but the behavior might be wrong. At this point, it's
// better to be explicit and error.
if (o.once orelse false) return error.NotImplemented;
if (o.signal orelse false) return error.NotImplemented;
capture = o.capture orelse false;
},
}
}
const cbk = (try listener.callback(self)) orelse return;
// check if event target has already this listener
const lst = try parser.eventTargetHasListener(
self,
typ,
capture,
cbk.id,
);
if (lst != null) {
return;
}
const eh = try EventHandler.init(page.arena, cbk);
try parser.eventTargetAddEventListener(
self,
typ,
&eh.node,
capture,
);
} }
const RemoveEventListenerOpts = union(enum) { const RemoveEventListenerOpts = union(enum) {

View File

@@ -63,13 +63,13 @@ pub const MutationObserver = struct {
// register node's events // register node's events
if (options.childList or options.subtree) { if (options.childList or options.subtree) {
try parser.eventTargetAddEventListener( _ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
"DOMNodeInserted", "DOMNodeInserted",
&observer.event_node, &observer.event_node,
false, false,
); );
try parser.eventTargetAddEventListener( _ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
"DOMNodeRemoved", "DOMNodeRemoved",
&observer.event_node, &observer.event_node,
@@ -77,7 +77,7 @@ pub const MutationObserver = struct {
); );
} }
if (options.attr()) { if (options.attr()) {
try parser.eventTargetAddEventListener( _ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
"DOMAttrModified", "DOMAttrModified",
&observer.event_node, &observer.event_node,
@@ -85,7 +85,7 @@ pub const MutationObserver = struct {
); );
} }
if (options.cdata()) { if (options.cdata()) {
try parser.eventTargetAddEventListener( _ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
"DOMCharacterDataModified", "DOMCharacterDataModified",
&observer.event_node, &observer.event_node,
@@ -93,7 +93,7 @@ pub const MutationObserver = struct {
); );
} }
if (options.subtree) { if (options.subtree) {
try parser.eventTargetAddEventListener( _ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node), parser.toEventTarget(parser.Node, node),
"DOMSubtreeModified", "DOMSubtreeModified",
&observer.event_node, &observer.event_node,

View File

@@ -138,8 +138,11 @@ pub const Event = struct {
}; };
pub const EventHandler = struct { pub const EventHandler = struct {
once: bool,
capture: bool,
callback: Function, callback: Function,
node: parser.EventNode, node: parser.EventNode,
listener: *parser.EventListener,
const Env = @import("../env.zig").Env; const Env = @import("../env.zig").Env;
const Function = Env.Function; const Function = Env.Function;
@@ -159,15 +162,73 @@ pub const EventHandler = struct {
} }
}; };
pub fn init(allocator: Allocator, callback: Function) !*EventHandler { pub const Opts = union(enum) {
flags: Flags,
capture: bool,
const Flags = struct {
once: ?bool,
capture: ?bool,
// We ignore this property. It seems to be largely used to help the
// browser make certain performance tweaks (i.e. the browser knows
// that the listener won't call preventDefault() and thus can safely
// run the default as needed).
passive: ?bool,
signal: ?bool, // currently does nothing
};
};
pub fn register(
allocator: Allocator,
target: *parser.EventTarget,
typ: []const u8,
listener: Listener,
opts_: ?Opts,
) !?*EventHandler {
var once = false;
var capture = false;
if (opts_) |opts| {
switch (opts) {
.capture => |c| capture = c,
.flags => |f| {
// Done this way so that, for common cases that _only_ set
// capture, i.e. {captrue: true}, it works.
// But for any case that sets any of the other flags, we
// error. If we don't error, this function call would succeed
// but the behavior might be wrong. At this point, it's
// better to be explicit and error.
if (f.signal orelse false) return error.NotImplemented;
once = f.once orelse false;
capture = f.capture orelse false;
},
}
}
const callback = (try listener.callback(target)) orelse return null;
// check if event target has already this listener
if (try parser.eventTargetHasListener(target, typ, capture, callback.id) != null) {
return null;
}
const eh = try allocator.create(EventHandler); const eh = try allocator.create(EventHandler);
eh.* = .{ eh.* = .{
.once = once,
.capture = capture,
.callback = callback, .callback = callback,
.node = .{ .node = .{
.id = callback.id, .id = callback.id,
.func = handle, .func = handle,
}, },
.listener = undefined,
}; };
eh.listener = try parser.eventTargetAddEventListener(
target,
typ,
&eh.node,
capture,
);
return eh; return eh;
} }
@@ -182,6 +243,17 @@ pub const EventHandler = struct {
self.callback.tryCall(void, .{ievent}, &result) catch { self.callback.tryCall(void, .{ievent}, &result) catch {
log.debug(.event, "handle callback error", .{ .err = result.exception, .stack = result.stack }); log.debug(.event, "handle callback error", .{ .err = result.exception, .stack = result.stack });
}; };
if (self.once) {
const target = (parser.eventTarget(event) catch return).?;
const typ = parser.eventType(event) catch return;
parser.eventTargetRemoveEventListener(
target,
typ,
self.listener,
self.capture,
) catch {};
}
} }
}; };
@@ -282,4 +354,13 @@ test "Browser.Event" {
.{ "document.dispatchEvent(new Event('count'))", "true" }, .{ "document.dispatchEvent(new Event('count'))", "true" },
.{ "nb", "0" }, .{ "nb", "0" },
}, .{}); }, .{});
try runner.testCases(&.{
.{ "nb = 0; function cbk(event) { nb ++; }", null },
.{ "document.addEventListener('count', cbk, {once: true})", null },
.{ "document.dispatchEvent(new Event('count'))", "true" },
.{ "document.dispatchEvent(new Event('count'))", "true" },
.{ "document.dispatchEvent(new Event('count'))", "true" },
.{ "nb", "1" },
}, .{});
} }

View File

@@ -615,7 +615,7 @@ pub fn eventTargetAddEventListener(
typ: []const u8, typ: []const u8,
node: *EventNode, node: *EventNode,
capture: bool, capture: bool,
) !void { ) !*EventListener {
const event_handler = struct { const event_handler = struct {
fn handle(event_: ?*Event, ptr_: ?*anyopaque) callconv(.C) void { fn handle(event_: ?*Event, ptr_: ?*anyopaque) callconv(.C) void {
const ptr = ptr_ orelse return; const ptr = ptr_ orelse return;
@@ -634,6 +634,8 @@ pub fn eventTargetAddEventListener(
const s = try strFromData(typ); const s = try strFromData(typ);
const err = eventTargetVtable(et).add_event_listener.?(et, s, listener, capture); const err = eventTargetVtable(et).add_event_listener.?(et, s, listener, capture);
try DOMErr(err); try DOMErr(err);
return listener.?;
} }
pub fn eventTargetHasListener( pub fn eventTargetHasListener(

View File

@@ -273,7 +273,7 @@ pub const Page = struct {
const doc = parser.documentHTMLToDocument(html_doc); const doc = parser.documentHTMLToDocument(html_doc);
const document_element = (try parser.documentGetDocumentElement(doc)) orelse return error.DocumentElementError; const document_element = (try parser.documentGetDocumentElement(doc)) orelse return error.DocumentElementError;
try parser.eventTargetAddEventListener( _ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Element, document_element), parser.toEventTarget(parser.Element, document_element),
"click", "click",
&self.window_clicked_event_node, &self.window_clicked_event_node,

View File

@@ -48,17 +48,14 @@ pub const XMLHttpRequestEventTarget = struct {
) !?Function { ) !?Function {
const target = @as(*parser.EventTarget, @ptrCast(self)); const target = @as(*parser.EventTarget, @ptrCast(self));
const callback = (try listener.callback(target)) orelse return null; // The only time this can return null if the listener is already
const eh = try EventHandler.init(alloc, callback); // registered. But before calling `register`, all of our functions
try parser.eventTargetAddEventListener( // remove any existing listener, so it should be impossible to get null
target, // from this function call.
typ, const eh = (try EventHandler.register(alloc, target, typ, listener, null)) orelse unreachable;
&eh.node, return eh.callback;
false,
);
return callback;
} }
fn unregister(self: *XMLHttpRequestEventTarget, typ: []const u8, cbk_id: usize) !void { fn unregister(self: *XMLHttpRequestEventTarget, typ: []const u8, cbk_id: usize) !void {
const et = @as(*parser.EventTarget, @ptrCast(self)); const et = @as(*parser.EventTarget, @ptrCast(self));
// check if event target has already this listener // check if event target has already this listener