Skip to content

EventTarget constructor #860

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 29 additions & 14 deletions src/browser/dom/event_target.zig
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

const std = @import("std");
const Env = @import("../env.zig").Env;
const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page;
Expand All @@ -28,41 +29,51 @@ const nod = @import("node.zig");
pub const Union = union(enum) {
node: nod.Union,
xhr: *@import("../xhr/xhr.zig").XMLHttpRequest,
plain: *parser.EventTarget,
};

// EventTarget implementation
pub const EventTarget = struct {
pub const Self = parser.EventTarget;
pub const Exception = DOMException;

pub fn toInterface(e: *parser.Event, et: *parser.EventTarget, page: *Page) !Union {
// libdom assumes that all event targets are libdom nodes. They are not.
// Extend libdom event target for pure zig struct.
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .plain },

// The window is a common non-node target, but it's easy to handle as
// its a singleton.
if (@intFromPtr(et) == @intFromPtr(&page.window.base)) {
return .{ .node = .{ .Window = &page.window } };
}
pub fn toInterface(et: *parser.EventTarget, page: *Page) !Union {
// libdom assumes that all event targets are libdom nodes. They are not.

// AbortSignal is another non-node target. It has a distinct usage though
// so we hijack the event internal type to identity if.
switch (try parser.eventGetInternalType(e)) {
switch (try parser.eventTargetInternalType(et)) {
.libdom_node => {
return .{ .node = try nod.Node.toInterface(@as(*parser.Node, @ptrCast(et))) };
},
.plain => return .{ .plain = et },
.abort_signal => {
// AbortSignal is a special case, it has its own internal type.
// We return it as a node, but we need to handle it differently.
return .{ .node = .{ .AbortSignal = @fieldParentPtr("proto", @as(*parser.EventTargetTBase, @ptrCast(et))) } };
},
.xhr_event => {
.window => {
// The window is a common non-node target, but it's easy to handle as its a singleton.
std.debug.assert(@intFromPtr(et) == @intFromPtr(&page.window.base));
return .{ .node = .{ .Window = &page.window } };
},
.xhr => {
const XMLHttpRequestEventTarget = @import("../xhr/event_target.zig").XMLHttpRequestEventTarget;
const base: *XMLHttpRequestEventTarget = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et)));
return .{ .xhr = @fieldParentPtr("proto", base) };
},
else => {
return .{ .node = try nod.Node.toInterface(@as(*parser.Node, @ptrCast(et))) };
},
else => return error.MissingEventTargetType,
}
}

// JS funcs
// --------
pub fn constructor(page: *Page) !*parser.EventTarget {
const et = try page.arena.create(EventTarget);
return @ptrCast(&et.base);
}

pub fn _addEventListener(
self: *parser.EventTarget,
typ: []const u8,
Expand Down Expand Up @@ -128,6 +139,10 @@ test "Browser.DOM.EventTarget" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();

try runner.testCases(&.{
.{ "new EventTarget()", "[object EventTarget]" },
}, .{});

try runner.testCases(&.{
.{ "let content = document.getElementById('content')", "undefined" },
.{ "let para = document.getElementById('para')", "undefined" },
Expand Down
2 changes: 1 addition & 1 deletion src/browser/dom/performance.zig
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pub const Performance = struct {
pub const prototype = *EventTarget;

// Extend libdom event target for pure zig struct.
base: parser.EventTargetTBase = parser.EventTargetTBase{},
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .performance },

time_origin: std.time.Timer,
// if (Window.crossOriginIsolated) -> Resolution in isolated contexts: 5 microseconds
Expand Down
12 changes: 6 additions & 6 deletions src/browser/events/event.zig
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,13 @@ pub const Event = struct {
pub fn get_target(self: *parser.Event, page: *Page) !?EventTargetUnion {
const et = try parser.eventTarget(self);
if (et == null) return null;
return try EventTarget.toInterface(self, et.?, page);
return try EventTarget.toInterface(et.?, page);
}

pub fn get_currentTarget(self: *parser.Event, page: *Page) !?EventTargetUnion {
const et = try parser.eventCurrentTarget(self);
if (et == null) return null;
return try EventTarget.toInterface(self, et.?, page);
return try EventTarget.toInterface(et.?, page);
}

pub fn get_eventPhase(self: *parser.Event) !u8 {
Expand Down Expand Up @@ -215,7 +215,7 @@ pub const EventHandler = struct {
.typ = try allocator.dupe(u8, typ),
.signal_target = signal_target,
.signal_listener = undefined,
.node = .{.func = SignalCallback.handle },
.node = .{ .func = SignalCallback.handle },
};

scb.signal_listener = try parser.eventTargetAddEventListener(
Expand Down Expand Up @@ -285,7 +285,7 @@ const SignalCallback = struct {
typ: []const u8,
capture: bool,
callback_id: usize,
node : parser.EventNode,
node: parser.EventNode,
target: *parser.EventTarget,
signal_target: *parser.EventTarget,
signal_listener: *parser.EventListener,
Expand Down Expand Up @@ -435,11 +435,11 @@ test "Browser.Event" {

try runner.testCases(&.{
.{ "nb = 0; function cbk(event) { nb ++; }", null },
.{ "let ac = new AbortController()", null},
.{ "let ac = new AbortController()", null },
.{ "document.addEventListener('count', cbk, {signal: ac.signal})", null },
.{ "document.dispatchEvent(new Event('count'))", "true" },
.{ "document.dispatchEvent(new Event('count'))", "true" },
.{ "ac.abort()", null},
.{ "ac.abort()", null },
.{ "document.dispatchEvent(new Event('count'))", "true" },
.{ "nb", "2" },
.{ "document.removeEventListener('count', cbk)", "undefined" },
Expand Down
3 changes: 1 addition & 2 deletions src/browser/html/AbortController.zig
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,12 @@ pub const AbortSignal = struct {
const DEFAULT_REASON = "AbortError";

pub const prototype = *EventTarget;
proto: parser.EventTargetTBase = .{},
proto: parser.EventTargetTBase = .{ .internal_target_type = .abort_signal },

aborted: bool,
reason: ?[]const u8,

pub const init: AbortSignal = .{
.proto = .{},
.reason = null,
.aborted = false,
};
Expand Down
2 changes: 1 addition & 1 deletion src/browser/html/media_query_list.zig
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pub const MediaQueryList = struct {

// Extend libdom event target for pure zig struct.
// This is not safe as it relies on a structure layout that isn't guaranteed
base: parser.EventTargetTBase = parser.EventTargetTBase{},
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .media_query_list },

matches: bool,
media: []const u8,
Expand Down
2 changes: 1 addition & 1 deletion src/browser/html/window.zig
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ pub const Window = struct {
pub const prototype = *EventTarget;

// Extend libdom event target for pure zig struct.
base: parser.EventTargetTBase = parser.EventTargetTBase{},
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .window },

document: *parser.DocumentHTML,
target: []const u8 = "",
Expand Down
24 changes: 24 additions & 0 deletions src/browser/netsurf.zig
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,13 @@ pub fn eventTargetDispatchEvent(et: *EventTarget, event: *Event) !bool {
return res;
}

pub fn eventTargetInternalType(et: *EventTarget) !EventTargetTBase.InternalType {
var res: u32 = undefined;
const err = eventTargetVtable(et).internal_type.?(et, &res);
try DOMErr(err);
return @enumFromInt(res);
}

pub fn elementDispatchEvent(element: *Element, event: *Event) !bool {
const et: *EventTarget = toEventTarget(Element, element);
return eventTargetDispatchEvent(et, @ptrCast(event));
Expand All @@ -771,12 +778,22 @@ pub fn eventTargetTBaseFieldName(comptime T: type) ?[]const u8 {
// EventTargetBase is used to implement EventTarget for pure zig struct.
pub const EventTargetTBase = extern struct {
const Self = @This();
const InternalType = enum(u32) {
libdom_node = 0,
plain = 1,
abort_signal = 2,
xhr = 3,
window = 4,
performance = 5,
media_query_list = 6,
};

vtable: ?*const c.struct_dom_event_target_vtable = &c.struct_dom_event_target_vtable{
.dispatch_event = dispatch_event,
.remove_event_listener = remove_event_listener,
.add_event_listener = add_event_listener,
.iter_event_listener = iter_event_listener,
.internal_type = internal_type,
},

// When we dispatch the event, we need to provide a target. In reality, the
Expand All @@ -790,6 +807,7 @@ pub const EventTargetTBase = extern struct {
refcnt: u32 = 0,

eti: c.dom_event_target_internal = c.dom_event_target_internal{ .listeners = null },
internal_target_type: InternalType,

pub fn add_event_listener(et: [*c]c.dom_event_target, t: [*c]c.dom_string, l: ?*c.struct_dom_event_listener, capture: bool) callconv(.C) c.dom_exception {
const self = @as(*Self, @ptrCast(et));
Expand Down Expand Up @@ -822,6 +840,12 @@ pub const EventTargetTBase = extern struct {
const self = @as(*Self, @ptrCast(et));
return c._dom_event_target_iter_event_listener(self.eti, t, capture, cur, next, l);
}

pub fn internal_type(et: [*c]c.dom_event_target, internal_type_: [*c]u32) callconv(.C) c.dom_exception {
const self = @as(*Self, @ptrCast(et));
internal_type_.* = @intFromEnum(self.internal_target_type);
return c.DOM_NO_ERR;
}
};

// MouseEvent
Expand Down
2 changes: 1 addition & 1 deletion src/browser/xhr/event_target.zig
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ pub const XMLHttpRequestEventTarget = struct {
pub const prototype = *EventTarget;

// Extend libdom event target for pure zig struct.
base: parser.EventTargetTBase = parser.EventTargetTBase{},
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .xhr },

onloadstart_cbk: ?Function = null,
onprogress_cbk: ?Function = null,
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/js.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2002,14 +2002,14 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller(Self, State).init(info);
defer caller.deinit();

// See comment above. We generateConstructor on all types
// in order to create the FunctionTemplate, but there might
// not be an actual "constructor" function. So if someone
// does `new ClassName()` where ClassName doesn't have
// a constructor function, we'll return an error.
if (@hasDecl(Struct, "constructor") == false) {
const iso = caller.isolate;
log.warn(.js, "Illegal constructor call", .{ .name = @typeName(Struct) });
const js_exception = iso.throwException(createException(iso, "Illegal Constructor"));
info.getReturnValue().set(js_exception);
return;
Expand Down Expand Up @@ -3420,7 +3420,7 @@ fn valueToDetailString(arena: Allocator, value: v8.Value, isolate: v8.Isolate, v
if (debugValueToString(arena, value.castTo(v8.Object), isolate, v8_context)) |ds| {
return ds;
} else |err| {
log.err(.js, "debug serialize value", .{.err = err});
log.err(.js, "debug serialize value", .{ .err = err });
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion vendor/netsurf/libdom