Skip to content

Commit ad225bc

Browse files
committed
AddEventListener object listener
Instead of taking a callback function, addEventListener can take an object that exposes a `handleEvent` function. When used this way, `this` is automatically bound. I don't think the current behavior is correct when `handleEvent` is defined as a property (getter), but I couldn't figure out how to make it work the way WPT expects, and it hopefully isn't a common usage pattern. Also added option support to removeEventListener.
1 parent 1a72bf5 commit ad225bc

File tree

5 files changed

+102
-46
lines changed

5 files changed

+102
-46
lines changed

src/browser/cssom/css_style_declaration.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ test "CSSOM.CSSStyleDeclaration" {
177177
.{ "style.setProperty('color', 'green')", "undefined" },
178178
.{ "style.getPropertyValue('color')", "green" },
179179
.{ "style.length", "4" },
180-
.{ "style.color", "green"},
180+
.{ "style.color", "green" },
181181

182182
.{ "style.setProperty('padding', '10px', 'important')", "undefined" },
183183
.{ "style.getPropertyValue('padding')", "10px" },

src/browser/dom/event_target.zig

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ pub const EventTarget = struct {
5757
pub fn _addEventListener(
5858
self: *parser.EventTarget,
5959
typ: []const u8,
60-
cbk: Env.Function,
60+
listener: EventHandler.Listener,
6161
opts_: ?AddEventListenerOpts,
6262
state: *SessionState,
6363
) !void {
@@ -80,6 +80,8 @@ pub const EventTarget = struct {
8080
}
8181
}
8282

83+
const cbk = (try listener.callback(self)) orelse return;
84+
8385
// check if event target has already this listener
8486
const lst = try parser.eventTargetHasListener(
8587
self,
@@ -90,8 +92,7 @@ pub const EventTarget = struct {
9092
if (lst != null) {
9193
return;
9294
}
93-
94-
const eh = try EventHandler.init(state.arena, try cbk.withThis(self));
95+
const eh = try EventHandler.init(state.arena, cbk);
9596

9697
try parser.eventTargetAddEventListener(
9798
self,
@@ -101,19 +102,34 @@ pub const EventTarget = struct {
101102
);
102103
}
103104

105+
const RemoveEventListenerOpts = union(enum) {
106+
opts: Opts,
107+
capture: bool,
108+
109+
const Opts = struct {
110+
capture: ?bool,
111+
};
112+
};
113+
104114
pub fn _removeEventListener(
105115
self: *parser.EventTarget,
106116
typ: []const u8,
107117
cbk: Env.Function,
108-
capture: ?bool,
109-
// TODO: hanle EventListenerOptions
110-
// see #https://github.com/lightpanda-io/jsruntime-lib/issues/114
118+
opts_: ?RemoveEventListenerOpts,
111119
) !void {
120+
var capture = false;
121+
if (opts_) |opts| {
122+
capture = switch (opts) {
123+
.capture => |c| c,
124+
.opts => |o| o.capture orelse false,
125+
};
126+
}
127+
112128
// check if event target has already this listener
113129
const lst = try parser.eventTargetHasListener(
114130
self,
115131
typ,
116-
capture orelse false,
132+
capture,
117133
cbk.id,
118134
);
119135
if (lst == null) {
@@ -125,17 +141,13 @@ pub const EventTarget = struct {
125141
self,
126142
typ,
127143
lst.?,
128-
capture orelse false,
144+
capture,
129145
);
130146
}
131147

132148
pub fn _dispatchEvent(self: *parser.EventTarget, event: *parser.Event) !bool {
133149
return try parser.eventTargetDispatchEvent(self, event);
134150
}
135-
136-
pub fn deinit(self: *parser.EventTarget, state: *SessionState) void {
137-
parser.eventTargetRemoveAllEventListeners(self, state.arena) catch unreachable;
138-
}
139151
};
140152

141153
const testing = @import("../../testing.zig");
@@ -248,4 +260,11 @@ test "Browser.DOM.EventTarget" {
248260
.{ "phase", "3" },
249261
.{ "cur.getAttribute('id')", "content" },
250262
}, .{});
263+
264+
try runner.testCases(&.{
265+
.{ "const obj1 = {calls: 0, handleEvent: function() { this.calls += 1; } };", null },
266+
.{ "content.addEventListener('he', obj1);", null },
267+
.{ "content.dispatchEvent(new Event('he'));", null },
268+
.{ "obj1.calls", "1" },
269+
}, .{});
251270
}

src/browser/events/event.zig

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ const std = @import("std");
2020
const Allocator = std.mem.Allocator;
2121

2222
const parser = @import("../netsurf.zig");
23-
const Function = @import("../env.zig").Function;
2423
const generate = @import("../../runtime/generate.zig");
2524

2625
const DOMException = @import("../dom/exceptions.zig").DOMException;
@@ -143,6 +142,24 @@ pub const EventHandler = struct {
143142
callback: Function,
144143
node: parser.EventNode,
145144

145+
const Env = @import("../env.zig").Env;
146+
const Function = Env.Function;
147+
148+
pub const Listener = union(enum) {
149+
function: Function,
150+
object: Env.JsObject,
151+
152+
pub fn callback(self: Listener, target: *parser.EventTarget) !?Function {
153+
return switch (self) {
154+
.function => |func| try func.withThis(target),
155+
.object => |obj| blk: {
156+
const func = (try obj.getFunction("handleEvent")) orelse return null;
157+
break :blk try func.withThis(try obj.persist());
158+
},
159+
};
160+
}
161+
};
162+
146163
pub fn init(allocator: Allocator, callback: Function) !*EventHandler {
147164
const eh = try allocator.create(EventHandler);
148165
eh.* = .{

src/browser/xhr/event_target.zig

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,20 @@ pub const XMLHttpRequestEventTarget = struct {
4646
self: *XMLHttpRequestEventTarget,
4747
alloc: std.mem.Allocator,
4848
typ: []const u8,
49-
cbk: Function,
50-
) !void {
49+
listener: EventHandler.Listener,
50+
) !?Function {
5151
const target = @as(*parser.EventTarget, @ptrCast(self));
52-
const eh = try EventHandler.init(alloc, try cbk.withThis(target));
52+
53+
const callback = (try listener.callback(target)) orelse return null;
54+
const eh = try EventHandler.init(alloc, callback);
5355
try parser.eventTargetAddEventListener(
5456
target,
5557
typ,
5658
&eh.node,
5759
false,
5860
);
61+
62+
return callback;
5963
}
6064
fn unregister(self: *XMLHttpRequestEventTarget, typ: []const u8, cbk_id: usize) !void {
6165
const et = @as(*parser.EventTarget, @ptrCast(self));
@@ -88,35 +92,29 @@ pub const XMLHttpRequestEventTarget = struct {
8892
return self.onloadend_cbk;
8993
}
9094

91-
pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, handler: Function, state: *SessionState) !void {
95+
pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, listener: EventHandler.Listener, state: *SessionState) !void {
9296
if (self.onloadstart_cbk) |cbk| try self.unregister("loadstart", cbk.id);
93-
try self.register(state.arena, "loadstart", handler);
94-
self.onloadstart_cbk = handler;
97+
self.onloadstart_cbk = try self.register(state.arena, "loadstart", listener);
9598
}
96-
pub fn set_onprogress(self: *XMLHttpRequestEventTarget, handler: Function, state: *SessionState) !void {
99+
pub fn set_onprogress(self: *XMLHttpRequestEventTarget, listener: EventHandler.Listener, state: *SessionState) !void {
97100
if (self.onprogress_cbk) |cbk| try self.unregister("progress", cbk.id);
98-
try self.register(state.arena, "progress", handler);
99-
self.onprogress_cbk = handler;
101+
self.onprogress_cbk = try self.register(state.arena, "progress", listener);
100102
}
101-
pub fn set_onabort(self: *XMLHttpRequestEventTarget, handler: Function, state: *SessionState) !void {
103+
pub fn set_onabort(self: *XMLHttpRequestEventTarget, listener: EventHandler.Listener, state: *SessionState) !void {
102104
if (self.onabort_cbk) |cbk| try self.unregister("abort", cbk.id);
103-
try self.register(state.arena, "abort", handler);
104-
self.onabort_cbk = handler;
105+
self.onabort_cbk = try self.register(state.arena, "abort", listener);
105106
}
106-
pub fn set_onload(self: *XMLHttpRequestEventTarget, handler: Function, state: *SessionState) !void {
107+
pub fn set_onload(self: *XMLHttpRequestEventTarget, listener: EventHandler.Listener, state: *SessionState) !void {
107108
if (self.onload_cbk) |cbk| try self.unregister("load", cbk.id);
108-
try self.register(state.arena, "load", handler);
109-
self.onload_cbk = handler;
109+
self.onload_cbk = try self.register(state.arena, "load", listener);
110110
}
111-
pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, handler: Function, state: *SessionState) !void {
111+
pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, listener: EventHandler.Listener, state: *SessionState) !void {
112112
if (self.ontimeout_cbk) |cbk| try self.unregister("timeout", cbk.id);
113-
try self.register(state.arena, "timeout", handler);
114-
self.ontimeout_cbk = handler;
113+
self.ontimeout_cbk = try self.register(state.arena, "timeout", listener);
115114
}
116-
pub fn set_onloadend(self: *XMLHttpRequestEventTarget, handler: Function, state: *SessionState) !void {
115+
pub fn set_onloadend(self: *XMLHttpRequestEventTarget, listener: EventHandler.Listener, state: *SessionState) !void {
117116
if (self.onloadend_cbk) |cbk| try self.unregister("loadend", cbk.id);
118-
try self.register(state.arena, "loadend", handler);
119-
self.onloadend_cbk = handler;
117+
self.onloadend_cbk = try self.register(state.arena, "loadend", listener);
120118
}
121119

122120
pub fn deinit(self: *XMLHttpRequestEventTarget, state: *SessionState) void {

src/runtime/js.zig

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
583583

584584
// Given an anytype, turns it into a v8.Object. The anytype could be:
585585
// 1 - A V8.object already
586-
// 2 - Our this JsObject wrapper around a V8.Object
586+
// 2 - Our JsObject wrapper around a V8.Object
587587
// 3 - A zig instance that has previously been given to V8
588588
// (i.e., the value has to be known to the executor)
589589
fn valueToExistingObject(self: *const Scope, value: anytype) !v8.Object {
@@ -952,15 +952,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
952952
if (!js_value.isFunction()) {
953953
return null;
954954
}
955-
956-
const func = v8.Persistent(v8.Function).init(self.isolate, js_value.castTo(v8.Function));
957-
try self.trackCallback(func);
958-
959-
return .{
960-
.func = func,
961-
.scope = self,
962-
.id = js_value.castTo(v8.Object).getIdentityHash(),
963-
};
955+
return try self.createFunction(js_value);
964956
}
965957

966958
const js_obj = js_value.castTo(v8.Object);
@@ -997,6 +989,20 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
997989
return value;
998990
}
999991

992+
fn createFunction(self: *Scope, js_value: v8.Value) !Function {
993+
// caller should have made sure this was a function
994+
std.debug.assert(js_value.isFunction());
995+
996+
const func = v8.Persistent(v8.Function).init(self.isolate, js_value.castTo(v8.Function));
997+
try self.trackCallback(func);
998+
999+
return .{
1000+
.func = func,
1001+
.scope = self,
1002+
.id = js_value.castTo(v8.Object).getIdentityHash(),
1003+
};
1004+
}
1005+
10001006
// Probing is part of trying to map a JS value to a Zig union. There's
10011007
// a lot of ambiguity in this process, in part because some JS values
10021008
// can almost always be coerced. For example, anything can be coerced
@@ -1238,11 +1244,16 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
12381244
};
12391245

12401246
pub fn withThis(self: *const Function, value: anytype) !Function {
1247+
const this_obj = if (@TypeOf(value) == JsObject)
1248+
value.js_obj
1249+
else
1250+
try self.scope.valueToExistingObject(value);
1251+
12411252
return .{
12421253
.id = self.id,
1254+
.this = this_obj,
12431255
.func = self.func,
12441256
.scope = self.scope,
1245-
.this = try self.scope.valueToExistingObject(value),
12461257
};
12471258
}
12481259

@@ -1379,6 +1390,17 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
13791390
.js_obj = gop.value_ptr.castToObject(),
13801391
};
13811392
}
1393+
1394+
pub fn getFunction(self: JsObject, name: []const u8) !?Function {
1395+
const scope = self.scope;
1396+
const js_name = v8.String.initUtf8(scope.isolate, name);
1397+
1398+
const js_value = try self.js_obj.getValue(scope.context, js_name.toName());
1399+
if (!js_value.isFunction()) {
1400+
return null;
1401+
}
1402+
return try scope.createFunction(js_value);
1403+
}
13821404
};
13831405

13841406
// This only exists so that we know whether a function wants the opaque

0 commit comments

Comments
 (0)