Skip to content
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
84 changes: 31 additions & 53 deletions src/browser/dom/event_target.zig
Original file line number Diff line number Diff line change
Expand Up @@ -41,79 +41,46 @@ pub const EventTarget = struct {

// JS funcs
// --------
pub fn _addEventListener(
self: *parser.EventTarget,
typ: []const u8,
listener: EventHandler.Listener,
opts: ?EventHandler.Opts,
page: *Page,
) !void {
_ = try EventHandler.register(page.arena, self, typ, listener, opts);
}

const AddEventListenerOpts = union(enum) {
const RemoveEventListenerOpts = union(enum) {
opts: Opts,
capture: bool,

const Opts = struct {
capture: ?bool,
once: ?bool, // currently does nothing
passive: ?bool, // currently does nothing
signal: ?bool, // currently does nothing
};
};

pub fn _addEventListener(
pub fn _removeEventListener(
self: *parser.EventTarget,
typ: []const u8,
cbk: Env.Function,
opts_: ?AddEventListenerOpts,
page: *Page,
listener: EventHandler.Listener,
opts_: ?RemoveEventListenerOpts,
) !void {
var capture = false;
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;
if (o.passive orelse false) return error.NotImplemented;
capture = o.capture orelse false;
},
}
}

// check if event target has already this listener
const lst = try parser.eventTargetHasListener(
self,
typ,
capture,
cbk.id,
);
if (lst != null) {
return;
capture = switch (opts) {
.capture => |c| c,
.opts => |o| o.capture orelse false,
};
}

const eh = try EventHandler.init(page.arena, try cbk.withThis(self));
const cbk = (try listener.callback(self)) orelse return;

try parser.eventTargetAddEventListener(
self,
typ,
&eh.node,
capture,
);
}

pub fn _removeEventListener(
self: *parser.EventTarget,
typ: []const u8,
cbk: Env.Function,
capture: ?bool,
// TODO: hanle EventListenerOptions
// see #https://github.com/lightpanda-io/jsruntime-lib/issues/114
) !void {
// check if event target has already this listener
const lst = try parser.eventTargetHasListener(
self,
typ,
capture orelse false,
capture,
cbk.id,
);
if (lst == null) {
Expand All @@ -125,7 +92,7 @@ pub const EventTarget = struct {
self,
typ,
lst.?,
capture orelse false,
capture,
);
}

Expand Down Expand Up @@ -244,4 +211,15 @@ test "Browser.DOM.EventTarget" {
.{ "phase", "3" },
.{ "cur.getAttribute('id')", "content" },
}, .{});

try runner.testCases(&.{
.{ "const obj1 = {calls: 0, handleEvent: function() { this.calls += 1; } };", null },
.{ "content.addEventListener('he', obj1);", null },
.{ "content.dispatchEvent(new Event('he'));", null },
.{ "obj1.calls", "1" },

.{ "content.removeEventListener('he', obj1);", null },
.{ "content.dispatchEvent(new Event('he'));", null },
.{ "obj1.calls", "1" },
}, .{});
}
10 changes: 5 additions & 5 deletions src/browser/dom/mutation_observer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -63,37 +63,37 @@ pub const MutationObserver = struct {

// register node's events
if (options.childList or options.subtree) {
try parser.eventTargetAddEventListener(
_ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node),
"DOMNodeInserted",
&observer.event_node,
false,
);
try parser.eventTargetAddEventListener(
_ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node),
"DOMNodeRemoved",
&observer.event_node,
false,
);
}
if (options.attr()) {
try parser.eventTargetAddEventListener(
_ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node),
"DOMAttrModified",
&observer.event_node,
false,
);
}
if (options.cdata()) {
try parser.eventTargetAddEventListener(
_ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node),
"DOMCharacterDataModified",
&observer.event_node,
false,
);
}
if (options.subtree) {
try parser.eventTargetAddEventListener(
_ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Node, node),
"DOMSubtreeModified",
&observer.event_node,
Expand Down
102 changes: 100 additions & 2 deletions src/browser/events/event.zig
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ const Allocator = std.mem.Allocator;

const log = @import("../../log.zig");
const parser = @import("../netsurf.zig");
const Function = @import("../env.zig").Function;
const generate = @import("../../runtime/generate.zig");

const DOMException = @import("../dom/exceptions.zig").DOMException;
Expand Down Expand Up @@ -139,18 +138,97 @@ pub const Event = struct {
};

pub const EventHandler = struct {
once: bool,
capture: bool,
callback: Function,
node: parser.EventNode,
listener: *parser.EventListener,

const Env = @import("../env.zig").Env;
const Function = Env.Function;

pub const Listener = union(enum) {
function: Function,
object: Env.JsObject,

pub fn callback(self: Listener, target: *parser.EventTarget) !?Function {
return switch (self) {
.function => |func| try func.withThis(target),
.object => |obj| blk: {
const func = (try obj.getFunction("handleEvent")) orelse return null;
break :blk try func.withThis(try obj.persist());
},
};
}
};

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;
}

pub fn init(allocator: Allocator, callback: Function) !*EventHandler {
const eh = try allocator.create(EventHandler);
eh.* = .{
.once = once,
.capture = capture,
.callback = callback,
.node = .{
.id = callback.id,
.func = handle,
},
.listener = undefined,
};

eh.listener = try parser.eventTargetAddEventListener(
target,
typ,
&eh.node,
capture,
);
return eh;
}

Expand All @@ -165,6 +243,17 @@ pub const EventHandler = struct {
self.callback.tryCall(void, .{ievent}, &result) catch {
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 {};
}
}
};

Expand Down Expand Up @@ -265,4 +354,13 @@ test "Browser.Event" {
.{ "document.dispatchEvent(new Event('count'))", "true" },
.{ "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" },
}, .{});
}
4 changes: 3 additions & 1 deletion src/browser/netsurf.zig
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,7 @@ pub fn eventTargetAddEventListener(
typ: []const u8,
node: *EventNode,
capture: bool,
) !void {
) !*EventListener {
const event_handler = struct {
fn handle(event_: ?*Event, ptr_: ?*anyopaque) callconv(.C) void {
const ptr = ptr_ orelse return;
Expand All @@ -634,6 +634,8 @@ pub fn eventTargetAddEventListener(
const s = try strFromData(typ);
const err = eventTargetVtable(et).add_event_listener.?(et, s, listener, capture);
try DOMErr(err);

return listener.?;
}

pub fn eventTargetHasListener(
Expand Down
2 changes: 1 addition & 1 deletion src/browser/page.zig
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ pub const Page = struct {
const doc = parser.documentHTMLToDocument(html_doc);

const document_element = (try parser.documentGetDocumentElement(doc)) orelse return error.DocumentElementError;
try parser.eventTargetAddEventListener(
_ = try parser.eventTargetAddEventListener(
parser.toEventTarget(parser.Element, document_element),
"click",
&self.window_clicked_event_node,
Expand Down
Loading